Date: 2026-07-06. Branch: feat/mcp-agentic-sync.
This file now also documents work landed on feat/mcp-agentic-sync after the
audit/gap-fill mesh: the MCP server surface and hash-based sync engine
(WP1–WP4, PRD-mcp-agentic-sync.md / ADR-0013), including WP2's fix of the B2
nested-folder upload gap below, plus a docs-accuracy audit reconciling those
docs against the code as landed. The milestone/blocker tables below carry over
from the audit/gap-fill snapshot except where noted.
Supersedes the 2026-05-28 snapshot. Between that snapshot and this one, a
seven-agent audit mesh (wp1–wp9) reviewed the codebase against the JS SDK
reference and landed fix packages for every CONFIRMED finding it judged
worth acting on. See audit-2026-07-05.md for the
full audit summary (38 confirmed findings, fix-per-package breakdown, and what
was deliberately deferred). This file documents the resulting state, not the
audit process itself.
Every M0–M7 milestone has a real implementation (no NotImplemented on the
happy path), including M6 (events), which was DTOs-only at the previous
snapshot and is now a fully wired, tested consumer reaching into pdtui's UI.
| M | Goal | State | Evidence |
|---|---|---|---|
| M0 | Workspace + CI + traits | Done | cargo check/clippy/fmt/test/doc green; deny-lints active |
| M1 | JSON API DTOs | Done | serde round-trip tests; happy-path subset for list/upload/download/events |
| M2 | Crypto (rpgp 0.16/0.19) | Done | decrypt_session_key, encrypt_and_sign, decrypt_and_verify, manifest sign/verify, generate_passphrase (Zeroizing<String>), explicit SEIPDv2/AEAD rejection guard (reject_seipd_v2, see ADR-0006); validated by JS-encoded wire fixtures incl. a compressed-payload fixture |
| M3 | HTTP + SRP auth | Done | ReqwestHttpClient (retry/backoff/jitter/x-pm-appversion, 429 + Retry-After honoured); SRP login via proton-srp 0.8.2, incl. TOTP 2FA (POST /core/v4/auth/2fa, CLI prompt + TUI code form, see plan-2fa-totp.md; FIDO2 out of scope, not yet live-validated); SessionManager proactive + reactive refresh (ADR-0010), single-flight-coalesced concurrent refresh, real OS-keyring backend |
| M4 | Upload block protocol | Done (one known interop gap) | create file (HMAC-SHA256 name hash) → request revision → 4 MiB chunk → encrypt+sign → block tokens → PUT BareURL → commit; draft cleanup (DeleteDraft) on failure. See blockers below (upload into a nested, non-root folder is still unresolved) |
| M5 | Download block protocol | Done (interop gaps closed) | get revision (paginated) → decrypt content key → per-block GET/hash/decrypt-verify → stream → manifest verify (post-delivery) → XAttr size/SHA1/mtime cross-check. See blockers below |
| M6 | Events | Done (SDK consumer + pdtui UI wired) | proton-drive-core::events::{drain_volume_events, spawn_volume_event_loop} is a real background poll/backoff loop (Fibonacci, JS-faithful ~30s cadence); ProtonDriveClient::subscribe_drive_events exposes it publicly; apps/pdtui/src/events_bridge.rs subscribes it into the remote pane's staleness flag, with graceful degradation to manual/pull-on-focus refresh if the subscription fails to establish |
| M7 | pdtui TUI | Done | local + remote panes, F3-upload/F2-download, progress gauge, live remote-pane refresh on drive events, signature_verified surfaced in the UI, PdtuiAccount key-unlock bootstrap, session persistence |
cargo fmt --all --check: cleancargo clippy --workspace --all-targets -- -D warnings: clean, zero warningscargo test --workspace: 192 passing, 0 failing, 6 ignored- Ignored (all require external state, by design): live
auth_integration(needs a real account),wire_rust_encrypted_decryptable_by_node(needsnode+openpgpnpm), one timing-sensitivesession.rstest (manual--ignoredrun only), one live-credentialsaccount.rstest, one live-credentialsdownload.rsround-trip test. - Do not hard-code this number in prose elsewhere without re-running the gate — it has drifted before. Prefer "all tests pass" in docs that aren't the primary status snapshot.
- Ignored (all require external state, by design): live
- Wire-format fixtures (ADR-0012): JS→Rust decrypt+verify, tamper rejection,
wrong-signer rejection, and compressed-payload (
compress: true) decrypt all pass.
| # | Item | Where | State |
|---|---|---|---|
| B1 | Name hash | upload.rs (resolve_parent_context + name-hash computation) |
Fixed. HMAC-SHA256(parent_hash_key, name_bytes), with parent_hash_key decrypted from the parent's FolderProperties.NodeHashKey. |
| B2 | Nested-folder key derivation | download/listing + upload now share keys.rs::{resolve_node_key_via_chain, resolve_parent_context} (delegated to from client.rs and upload.rs) |
Fixed (both halves). The full-chain parent resolution was extracted into proton-drive-core/src/keys.rs as the single source of truth; the download/listing path (client.rs) and the upload/folder-create path (upload.rs::resolve_parent_context, revision_uploader, create_folder) all delegate to it, so nested (non-root) parents derive the correct NodeHashKey/parent node key at any depth. Regression tests (WP2, upload.rs): create_file_uses_nested_parent_hash_key and create_folder_uses_nested_parent_hash_key assert the nested parent's hash key is used (not the root's); revision_upload_resolves_nested_node_key_chain asserts the node-key walk reaches the root for a nested file; crypto correctness at depth remains covered by client.rs::resolve_node_key_via_chain_unlocks_three_level_nesting. |
| B3 | XAttr modification-time / size / SHA1 | download.rs::verify_xattr |
Fixed. Modification time is decrypted and returned in DownloadStats. Size and SHA1 mismatches are logged at error level but are deliberately non-fatal, matching the JS SDK's own fallback behaviour (JS never asserts assembled size against XAttr.Common.Size either; see ADR-0009 correction). |
Vec<u8> |
account.rs, download.rs |
Fixed. Wrapped in Zeroizing, as before. |
|
| B5 | Detached per-block enc_signature fetched but not independently verified |
download.rs |
Not a bug: JS-faithful by design, and now says so explicitly in code. The JS reference has no code path that decrypts or verifies the per-block detached signature either; only the manifest signature over block hashes establishes per-revision authenticity, and the SHA-256 hash check guards data integrity per block. |
| B6 | Download partial-write leaves a truncated file on mid-stream failure | download.rs |
Fixed. A mid-stream block failure now removes the partial file via tokio::fs::remove_file, covered by dedicated tests. |
decrypt_node_key public stub returns NotImplemented |
— | Fixed (deleted). No symbol named decrypt_node_key remains anywhere in the tree; decrypt_node_private_key is the only path. |
|
| B8 (new) | Core node-read/list operations use @deprecated-marked legacy v1 share-scoped endpoints (/drive/shares/{shareID}/links/{linkID}, /drive/shares/{shareID}/folders/{linkID}/children) instead of the v2 volume-scoped, cursor-paginated endpoints (/drive/v2/volumes/{volumeID}/folders/{linkID}/children + batched POST /drive/v2/volumes/{volumeID}/links) that reference/client/js/src/internal/nodes/apiService.ts uses in production |
client.rs: node, fetch_folder_children, resolve_node_key_via_chain, file_downloader |
Known gap, deliberately deferred. Functions correctly today (proven by the live round-trip); the risk is Proton removing the deprecated routes without notice. Migrating requires new DTOs (LinkIDs/More/AnchorID cursor shape vs. the current flat Links[]/page-index GetChildrenResponse) and is out of scope for this docs pass. See audit-2026-07-05.md. |
See audit-2026-07-05.md for the full per-package breakdown. Highlights not
already covered by the B-table above:
- Error taxonomy: 2500 now maps to
AlreadyExists(name collision), 2011 to a newPermissionDeniedvariant (previously both were mis-mapped). - HTTP transport stopped leaking
Authorization: Bearer/x-pm-uidonto storageBareURLs (blocks live on a different host than the API). - Block-list fetch is paginated (JS
PageSize/FromBlockIndex); large revisions no longer silently truncate. - Filename sanitisation closes a path-traversal gap.
NodePassphraseSignatureandContentKeyPacketSignatureare now verified JS-faithfully (non-fatally, matching JS's own non-fatal authorship checks).- Upload progress gauge fixed (previously stuck/incorrect).
- Session hardening: real OS-keyring backend (file fallback retained),
SessionExpirednow wipes the 0600 secret file andsession.json(not just the keyring entry), concurrent-401 refreshes coalesce into a single in-flight request, interactive-login password wrapped inZeroizing, serverExpiresInhonoured. - Crypto: decompress-before-verify so JS
compress: trueXAttr payloads verify;EncryptOptions.compresswired;Zeroizingat thegenerate_passphrasetrait boundary; redactedDebugon key material; explicitreject_seipd_v2guard (ADR-0006). pdtui:subscribe_drive_eventswired into the remote pane viaevents_bridge.rs(M6, see milestone table above), with graceful degradation to pull-on-focus if the subscription can't be established.
- Full OpenAPI surface: only list/upload/download/events DTOs. Mechanical to extend.
- Migrating off the deprecated v1 share-scoped node/list endpoints to the v2 volume-scoped ones (B8 above); functions today, deferred pending live re-validation of the fix packages above.
- SEIPDv2 / AEAD: rejected by ADR-0006, deferred to M2.5.
- SQLite cache: in-memory only, ADR-0003.
The MVP was code-complete and unit/wire-verified end-to-end as of the last
live-account round-trip (SRP login → list → upload → byte-identical download,
including a 646 MB nested-file download). The wp1–wp9 mesh then found and
fixed 38 confirmed defects across error handling, transport, crypto, upload,
download, auth/session, and pdtui's event wiring; all unit/wire-tested, none
yet re-validated against a live account since landing. Before trusting this
state end-to-end, re-run the live round-trip (pdtui mvp) to confirm the
fix packages didn't regress the happy path; nothing in the wp1–wp9 diffs
touched the core protocol shape, but that is an assumption, not a proof, until
someone runs it against production again.
Remaining known gaps are all deliberately scoped out or deferred, not
oversights: the v1→v2 endpoint migration (B8), SEIPDv2/AEAD (ADR-0006), and
B5's non-verification of per-block detached signatures (JS-faithful by design).
The B2 upload-half gap (nested-folder upload) has since been fixed by WP2:
upload, revision upload, and folder creation all resolve nested parent contexts
through the shared keys.rs full-chain resolver. None of these remaining gaps
block the MVP's stated scope (SRP login → list → upload/download at the My Files
root and nested reads).