Files
rclone/docs/plans/001-protondrive-tier1-workspace/progress.md
T
2026-11-12 15:12:51 +01:00

51 KiB
Raw Blame History

SDD ledger — plan: docs/plans/001-protondrive-tier1.md

Spec: docs/specs/001-protondrive-tier1.md (approved 2026-11-11). Branch protondrive-tier1. Implementer model: opus (owner's instruction). Merge base for final review: 06700aaf2

Preflight scan

Tasks Produces vs consumes Finding
1 -> 2,3,4,7,9,11 protontest.Server.handle/Inject/Requests used by later route files consistent
2 -> 3 session, credentialStore; 3 adds loginSRP/fork to same file consistent
3 -> 5 Task 3 creates crypto.go (computeKeyPassword, decryptForkPayload); Task 5 modifies it consistent
4 -> 6,8 loadAccount, account.primary(); 8 newClient consumes consistent
5 -> 6,8,10,11,12 crypto function set consistent
7 -> 8,10 fake read routes, Seed*/Corrupt/Dump consistent
8 -> 9,10,11,14 client, node, apiCall, errNotFound/errExists, newTestClient Task 8 test TestShouldRetry collides with the old test of that name in protondrive_internal_test.go
9,10,11 -> 14 mutations, open, upload consistent
13 -> 14,15 setupFakeRemote, protonfake 13 is skipped until 14 adds endpoint; consistent
14 -> 19 14 keeps old libs for diff tests; 19 removes consistent
6,12 -> 19 frozen fixtures survive removal consistent
each task self-check tests vs code, files created vs touched Task 1: 2064 for bad app version is the fake's choice, self-consistent. Task 17 TestBudgetNoLegacyRoutes needs a TestMain hook in package protondrive_test - none defined elsewhere, fine. Others agree with themselves

Ruling: Task 8's retry test is named TestClientShouldRetry — the old TestShouldRetry exists until Task 14 — costs nothing if wrong. Ruling: no separate worktree; work happens on branch protondrive-tier1 in the main checkout — the branch is already isolated from master and the owner works here — cost: owner's checkout is busy during execution.

Progress

Ruling: fake keeps Proton's published app-version grammar (channel optional); the client's own tests assert a channel is always sent — fake stays a model of the service — a channel-less regression would only be caught by the Task 2 unit test. Task 1: minor (deferred): fake accepts app version without channel (ruled above) Task 1: minor (deferred): wrong-method request to a registered path reports 404 "no route" Task 1: minor (deferred): test counter called in server_test.go is a plain int across goroutines Task 1: complete (commits 06700aa..2e3dbac, review clean) Ruling: Task 2 two-session refresh race — fix by re-reading the store after a rejected refresh (bounded wait up to ~3s for the winner's save) before declaring the session dead; no cross-Fs lock, since the spec allows no package state beyond unlockMu — cost if wrong: a rare false "session expired" when the winner takes longer than the wait. Task 2: minor (deferred): adopted stored tokens with a stale access token fail one call with a plain 401 Task 2: minor (deferred): refresh HTTP call uses the first waiter's ctx Task 2: minor (deferred): serverTime can step back under out-of-order responses Task 2: fix round 1/5 (1 addressed, 0 open — two-session refresh race; commits ea41704..9d894ab) Task 2: complete (commits 2e3dbac..9d894ab, review clean) Ruling: key salt must be chosen by the primary user key's ID, not the first entry (old library behaviour; SDK takes the first) — carried into Task 4 — cost if wrong: none, first-entry is the fallback. Task 3: minor (deferred): half-signed-in cleanup DELETE is untested and uses the caller's (possibly cancelled) ctx Task 3: minor (deferred): empty SRP salt not rejected up front Task 3: minor (deferred): TestForkLogin goroutine may assert after test end on a failure path Task 3: note for Task 14: forkRequest.URL carries the fork key - show it to the user only, never log it Task 3: fix round 1/5 (1 addressed, 0 open — bad server proof untested; commits 2ba0848..5a8bb02) Task 3: complete (commits 9d894ab..5a8bb02, review clean) Task 4: minor (deferred): publicKeys waiters share the fetching caller's ctx error Task 4: minor (deferred): GET /core/v4/users fetched at sign-in and again in loadAccount Task 4: minor (deferred): fake AddAddress/EncryptToAddress read key state outside the lock Live probe 2026-11-11 (controller, throwaway test, not committed): new loginSRP works against the real test account (TestProtonDrive: remote configured in ~/.config/rclone/rclone.conf with the old backend's keys). Account has NO drive volume: GET drive/v2/shares/my-files -> 422 code 2501 "Volume not found". Old backend fails on it (legacy GET /drive/shares redirects https->http). Ruling: Task 8 newClient must create the volume when my-files returns 2501, as the SDK's shares module does (POST drive/volumes), and the fake must support a user without a volume — a new account is otherwise unusable — cost if wrong: a malformed volume in a throwaway test account. Task 4: fix round 1/5 (1 addressed, 0 open — address-key signature rejection untested; commits 6ec4984..483c3a1) Task 4: complete (commits 5a8bb02..483c3a1, review clean) Owner provided a second, paid test account 2026-11-11: remote TestProtonDrivePaid: configured (no sign-in attempted yet). Live matrix for Task 21 is now free + paid. Ruling: Task 5 content format follows the SDK and Proton's stated target (v4 node key advertising SEIPDv2, v6 PKESK content key, SEIPDv2 blocks, block signature encrypted with the content session key) even where the old library's comments disagree — the old library is what must stay readable, the SDK decides what is written — cost if wrong: uploads rejected by the real server; to be settled by a live upload probe as soon as Task 11 exists, now that test accounts are available. Task 5: minor (deferred): xattrs.marshal compacts/HTML-escapes Extra members on re-encode (semantics kept) Task 5: minor (deferred): node passphrase not checked for valid UTF-8 as the SDK does Task 5: open for live check: block-signature packet format and node key locking mode cannot be confirmed from the SDK checkout (in docs/TODO.md) Task 5: fix round 1/5 (1 addressed, 0 open — decryption key accepted as signer; commits 0bb2dbd..a1e1333) Task 5: complete (commits 666df15..a1e1333, review clean) Ruling: keep the intended-recipient check in decryptMessage (stricter than the SDK may be) — the old library and go-crypto enforce it — cost if wrong: a spurious verification warning (not a failure) on data from a client that lists other recipients. Task 6: minor (deferred): no test for a decryption-key signature that lists recipients with verifyKR excluding it (hardening for intendedRecipientsError ordering) Task 6: minor (deferred): recipient match accepts any key in decKR; plain error type instead of SignatureVerificationError Task 6: minor (deferred): relvacode/iso8601 is a direct dep only for the differential test (goes in Task 19) Task 6: complete (commits a1e1333..3b63f3c, review clean) Ruling: the fake must reject client-written names, passphrases, hash keys and xattrs that are SEIPDv2/v6-PKESK (official clients cannot be assumed to read them), since Dump alone would not notice — carried into Tasks 9 and 11 as a fake strictness rule — cost if wrong: none for correctness, slight extra fake code. Task 7: minor (deferred): TestCorrupt can pass without asserting if a node is missing from Dump Task 7: minor (deferred): plan Step 2 (RED run) ticked though strict TDD was skipped for the fake's drive state Task 7: minor (deferred): fake block URLs never expire; wire-name test covers only the links response; drive.go is 1518 lines Task 7: note: fake error codes for unknown AnchorID (2001), missing storage token (2011) and drafts hidden from children are assumptions until a live run Task 7: complete (commits 3b63f3c..27d06dc, review clean) Ruling: honoured Retry-After is capped at 60s — the SDK waits whatever the server says, but an uncapped value lets one bad header freeze the client through the shared pacer — cost if wrong: retrying sooner than Proton asked after a very long Retry-After (then another 429). Task 8: confirmed gap from ⚠️: fetchPublicKeys unpaced/unretried — sent to fix round 1 Task 8: note for Task 9: findChild cannot tell "absent" from "present but hidden" (drafts, Docs, unreadable names): create paths must map server code 2500 to errExists Task 8: note for Task 10: pre-2023 content-key-packet fallback (SDK) not yet implemented; listing does not decrypt the content key Task 8: minor (deferred): paging error does not cancel in-flight batches Task 8: minor (deferred): folderKeys map grows unbounded; close() does not clear file-node keys held by the cache Task 8: minor (deferred): cache.put leaves a moved node in its old parent's listing (Task 9 must invalidate on move) Task 8: minor (deferred): volume creation signs with the first unlocked address key, not the one flagged Primary Task 8: minor (deferred): root node name not validated Task 8: fix round 1/5 (2 addressed, 0 open — Retry-After cap/single wait, public key lookups paced; commits 2065b7b..a8856ff) Task 8: minor (deferred): TestListCancel compares NumGoroutine to a baseline and could flake Task 8: minor (deferred): Retry-After sleep inside pacer.Call holds a connection token for up to 60s Task 8: complete (commits 27d06dc..a8856ff, review clean) Live probe 2026-11-11 (controller, throwaway test, not committed): against the real FREE account, loginSRP + newClient created the volume (server accepted share/root key material), listed an empty root, about returned total 2147483648. Paid account untouched so far. Task 9: minor (deferred): apiCall retries non-idempotent mutations after 5xx/network errors; a lost response gives a spurious errExists/errNotFound/stale-hash error (Fs layer in Task 14 must treat Mkdir errExists as success and delete errNotFound as done) Task 9: minor (deferred): trash accepts any 1xxx code as OK instead of 1000/1001/1002 Task 9: minor (deferred): anonymous detection on move uses SignatureEmail=="" even when the parent-key signature failed Task 9: minor (deferred): cache.setChildren bypasses put's changed-parent rule (matters for external changes, M5) Task 9: minor (deferred): copyNode reports a made copy as an error if the follow-up getNode fails Task 9: minor (deferred): nodes.go is ~1,160 lines; small fake-side coverage gaps on copy/rename formats Task 9: note: fake rejects NodePassphraseSignature on a signed node - stricter than known real behaviour Task 9: complete (commits a8856ff..ee81e74, review clean) Task 10: minor (deferred): Close can wait behind the context-free downloadTokens.Get (in docs/TODO.md) Task 10: minor (deferred): pm-storage-token would follow a cross-host redirect from the storage host (block-scoped token) Task 10: note: per-reader read-ahead is 4 blocks (implementer's choice; client-wide cap 10 stands); content key decrypted on every open Task 10: fix round 1/5 (1 addressed, 0 open — unbounded claimed block sizes; commits 4bcafe5..e8b1359) Task 10: complete (commits ee81e74..e8b1359, review clean) Live probe 2026-11-11 13:09 UTC (controller, throwaway test, not committed), FREE account, new code at 9a4ed61a8: createFolder rclone-live-probe-20261111-130916; upload hello.txt (25 B), empty.bin (0 B), five-mib.bin (5 MiB+3, 2 blocks), then a second revision of hello.txt; each downloaded back byte-identical with manifest verification, SHA1/size/modtime correct; ranged read across the block boundary correct. => Real server accepts v6 PKESK + SEIPDv2 content, verifier tokens, manifest, xattrs, new revisions as written. Still unverified: official web/CLI client reading these files (owner asked to look in the web app). Ruling: Task 11 commit outcome unknown (commit errored and the follow-up read failed) => delete nothing and return an "outcome unknown" error rather than success — never risk deleting a file that may have committed, never report unverified success — cost if wrong: an orphan draft that the own-draft rule cleans up on the next upload of that name. Ruling: fake's "draft ownership by ClientUID" is satisfied by reporting the owner in conflict details; it does not gate block/commit/delete routes — the real server's rule is unknown — cost: a client deleting another client's draft would not be caught by the fake. Task 11: note for Task 14: draft_client_uid config key must be generated and persisted or the own-draft rule never fires Task 11: minor (deferred): an own leftover draft REVISION on an active file is never deleted (safe DELETE of ConflictDraftRevisionID possible) Task 11: minor (deferred): two fault tests print an expected ERROR log line Task 11: note: unconfirmed real-server codes for bad verifier / missing block / stale CurrentRevisionID; delete_multiple on an active link (docs/TODO.md) Owner observation 2026-11-11 (web app, free account): folder rclone-live-probe-20261111-130916 is visible with empty.bin, five-mib.bin and hello.txt => official web client decrypts names written by the new code. Content open and signature-warning status not yet reported. Owner observation 2026-11-11 (web app, free account): hello.txt opens and shows "hello again - second revision" => official web client decrypts content written by the new code and sees the second revision as active. Signature-warning status not yet reported. Owner observation 2026-11-11 (web app details for hello.txt, free account): "Digital signature verified. This file was securely uploaded by "; original size 30 bytes; Modified May 6 2024 9:08 AM local (= 07:08:09 UTC as set); SHA1 shown; media type application/octet-stream. => official web client verifies signatures and reads xattrs written by the new code. Open: reverse direction (official-client-created files read by the new code), paid account. Live probe 2026-11-11 (controller, scratch worktree at 9a4ed61a8, removed afterwards), FREE account, read-only: listed the probe folder; folder "my-little-folder" created by the WEB APP listed; file "Readme.txt" (66 B, text/plain) uploaded by the WEB APP inside it downloaded with manifest verification, SHA1 matches the web app's claimed digest, no signature errors logged. => reverse direction (official web client -> new code) works for folder, name, content, xattrs. Open: paid account, official CLI, full test_all after Task 14. Task 11: fix round 1/5 (6 addressed, 1 new open — "not active" treated as "not committed" deletes a committed file when a third revision/trash intervenes; commits 9a4ed61..1f1b2bb) Ruling: after a failed commit, delete a draft only on positive proof it never committed (new file: node still in draft state; new revision: active revision still the one we started from); any other observation => delete nothing, errUploadOutcomeUnknown — a wrong delete is permanent data loss, a leftover draft is not — cost if wrong: occasional orphan drafts. Task 11: fix round 2/5 (1 addressed, 0 open — delete only a proven uncommitted draft; commits 1f1b2bb..dc7d977) Task 11: minor (deferred): errUploadSuperseded message misleading when the node reads back draft/trashed Task 11: minor (deferred): if another client "restores" the starting revision keeping its ID after our commit landed, cleanup deletes our superseded history entry (no live content lost; real restore semantics unverified) Task 11: complete (commits e8b1359..dc7d977, review clean) Flake hunt 2026-11-11 (controller, 8 x go test -race ./backend/protondrive/ at e76b6783b): TestClientBootstrapCreateVolumeRace failed 1/8 (Task 8 code). To be diagnosed by a dedicated dispatch after Task 12's fix round. Task 12: minor (deferred): no RED runs for the two crypto tests (mutation checks only reported); signTimeOf uses the primary key's creation time only Task 12: fix round 1/5 (1 addressed, 0 open — v6 fixture provenance comment; commits e76b678..e65c785) Task 12: complete (commits dc7d977..e65c785, review clean) Flake dispatch: opus agent diagnosing TestClientBootstrapCreateVolumeRace (report: flake-volume-race-report.md); Task 13 waits for it.

Flake: TestClientBootstrapCreateVolumeRace

  • Fixed in e23de0b58 (opus). Root cause: test timing assumption (unlockMu serialises loadAccount, so second client often saw the created volume); client correct. Fake gained Gather fault option; fake data race on shared address key fixed (per-check key copy). 200/200 at -race.
  • Concerns carried: (1) TestUploadShortSource requireNoLeaks failed once under heavy load (whole-process goroutine count, 2s allowance) — deferred to final review; (2) test takes ~2.3s; (3) loser of a real-server 2500 race assumes volume readable at once — no retry, fails cleanly.
  • Review dispatched (sonnet). Task 13 BASE = e23de0b58.
  • Flake fix review (sonnet): APPROVED. Deferred minors for final review: test's 2s wait is wasted time (Gather already forces ordering; start both clients together); no direct assertion that one POST got 2500; Gather counts abandoned requests toward Times; Copy() of the address key still reads the shared key. Flake: complete.
  • Task 13 dispatched (opus), BASE e23de0b58.

Task 13: complete — 414de88c9. Review (sonnet): spec ✅, quality approved. Deferred minors: protonfake main_test leaks run goroutine if a require fails (close stop in t.Cleanup). Carry to Task 14: client_salted_key_pass in config is base64(StdEncoding) of raw key password — config store decodes on load, encodes on save; keep the -remote skip guard in TestIntegrationFake; record protontest in docs/ARCHITECTURE.md.

  • Task 14 dispatched (opus), BASE 414de88c9.
  • Task 14 implemented: 0ec7dce0e (DONE_WITH_CONCERNS). Contract suite vs fake: 126 subtests, 97 pass, 29 feature-skips, no Opt opt-outs.
  • Ruling: leave github.com/rclone/Proton-API-Bridge in go.mod until Task 19 — plan assigns removal there; costs nothing if wrong (unused requirement for a few commits).
  • Ruling: browser sign-in is default only when username+password are not both set — a scripted config create with credentials must not block 10 min on a browser poll; if wrong, users with saved credentials see password sign-in offered first.
  • Review dispatched (opus).
  • Task 14 review (opus): spec ❌ (Rmdir), 3 Important: Rmdir trashes folders with unlisted children; Config always forces new sign-in; Shutdown leaves unusable client. Review file task-14-review.md.
  • Ruling: Shutdown must leave the Fs usable (no key clearing; drop caches/stop background only); key clearing only on Disconnect — supersedes brief's "Shutdown calls client.close once" because fs cache shuts down expired entries still held by rc jobs; cost if wrong: keys stay in memory until process exit.
  • Task 14 fix round 1 dispatched (resume implementer), BASE 0ec7dce0e. Includes minors: inactive-node refusal, range maths with original_file_size=false, XXX comment, TestTwoRemotesIsolated refresh.
  • Task 14 deferred minors: cross-account Move/Copy/DirMove not refused; env-shared tokens across two processes undocumented; cross-Fs invalidation race with in-flight listing; no reconnect hint on wrong key password / browser hint on CAPTCHA; code 2000 fatal mapping (plan-mandated).
  • Task 14 fix round 1: c00162ed2. Re-review dispatched (sonnet).

Task 14: complete — 0ec7dce0e, c00162ed2. Re-review (sonnet): ALL CLOSED, no Critical/Important. Deferred minors (fix round): Rmdir refuses a folder holding only a leftover draft/trashed child if the real server lists them (check live in Task 21); countContent re-downloads on each suffix-range open of a size-less file; a call in flight during Disconnect may get an unclear error rather than errNotSignedIn (session.go ~215-225); Shutdown test may not fail against old key-clearing code.

  • Task 15 dispatched (opus), BASE c00162ed2.
  • Live smoke (free account, binary built from c00162ed2 in scratch worktree scratchpad/live-c00162ed2): password sign-in saved client_* to config; lsd/lsl/cat/sha1sum/about correct on the earlier probe folder (hello.txt SHA1 matches web app); write round-trip in rclone-smoke-task14: sync up 3 files (9 MB, 0 B, text), check 0 differences, second revision, moveto, copyto (server-side), sync back byte-identical, rmdir of non-empty refused, purge OK. Note for final review: ModTime returned with ms while Precision is time.Second.
  • Task 15 implemented: 6e2410de6, 14128d59c, 860081538 (DONE_WITH_CONCERNS). Backend fixes: CAPTCHA error names real remote + config falls back to method choice; signature-failure error hints ignore_signature_errors. Concerns: ManyNewFs can't prove unlock serialisation black-box (internal TestLoadAccountSerialised covers); errNotSignedIn says literal remote: (handed to Task 16). Review dispatched (sonnet).
  • Task 16 dispatched (opus), BASE 860081538.

Task 15: complete — 6e2410de6, 14128d59c, 860081538. Review (sonnet): spec ✅ (ManyNewFs split with internal test accepted), quality approved. Deferred minors (regression_test.go): BadSignatureFile Contains "signature" satisfied by hint text alone (:623); MultiThreadDownload evidence indirect (:719); DocsNodeSkipped doesn't assert node still on server (:587); QuotaExceeded uses generic fault message (:788); TwoProcessRefreshRace 500ms delay vs adoptWait margin (:464); FileOverDirectory no non-retryable assert.

  • Task 16 implemented: 606580196 (real bug: file with bad node-passphrase signature was readable), 59f0f31b3, db97ef7c8 (401 during Disconnect retried with empty auth), f4105adfc. DONE_WITH_CONCERNS: manifest checked only at EOF so swapped/rolled-back same-size blocks reach a streaming reader before the error; --dump headers shows pm-storage-token/x-pm-uid; export_test.go added. Review dispatched (opus).
  • Task 16 review (opus): spec ❌, 2 Important: forged folder key + anonymous child bypasses owner-signature refusal (nodes.go ~446-488); tests encode manifest-at-EOF gap vs spec 9.6. Review file task-16-review.md.
  • Ruling: children inherit an unverified parent key's error; creating anything inside an unverified-key folder is refused unless ignore_signature_errors — names/passphrases would be encrypted to a key the server may hold; cost if wrong: a legitimately odd folder needs the flag to write into.
  • Ruling: full reads verify the signed manifest over the listed block hashes BEFORE releasing any byte (spec 6.4/9.6 updated to match); ranged reads deferred (TODO: bounded per-revision verified-hash cache); sequential paging accepted (first-byte latency ~1 request per 80 MiB). Whole-revision rollback under current revision ID is a protocol limit (SDK can't detect either) — document. Cost if wrong: slower first byte on very large full reads.
  • Task 16 fix round 1 dispatched (resume implementer), BASE f4105adfc. Deferred minors: parentless directly-shared file refused; on-the-fly remote hint names bare backend; DECISIONS entry placement.
  • Task 16 fix round 1: 88654eb04, 9598e22d9, 248f46705, 751a3ea35. Re-review dispatched (opus). Task 17 dispatched (opus), BASE 751a3ea35.
  • Task 16 re-review (opus): NOT ALL CLOSED. B, C1–C5 closed. A open: move/copy/DirMove OUT of a forged folder launders it (nodes.go:1086-1092, encryptForNewParent). New Important 2: forged key signed under a foreign email whose key the server serves passes (account.go:254-281; share CreatorEmail nodes.go:333) — protocol limit, but cheap partial fix. Minors: bytes=0- treated as ranged so skips manifest check (protondrive.go:1150); listing-phase memory cap from unsigned EncryptedSize (download.go:127-130); shareKeys.unverified unused; TestPasswordStoredObscured leaves configfile installed. File: task-16-rereview.md.
  • Ruling: refuse move/copy/DirMove whose SOURCE has unverified keys unless ignore_signature_errors. Require My Files share creator and root folder signer to be one of the user's own addresses, else root is unverified. Foreign-signer trust (server-served public keys) documented as protocol limit (SDK same). Accept that a folder signed by a deleted own address refuses writes without the flag — error must say so plainly; cost if wrong: such users must set the flag.
  • Task 16 fix round 2 waits for Task 17 implementer to finish (same tree).
  • Task 17 implemented: 3fac25de7 (parent listing no longer dropped after each create — request storm), 32710eefd (decompression limits on metadata), 90b2a48e7 (skip nodes with empty ID/negative size), 12ce57ed3 (budgets + fuzz + TestMain legacy-route check). DONE_WITH_CONCERNS: NewObject budget is 8 not 7 (4 folders × 2, no lookup-by-name route); block-size sum not checked at parse; API error messages unbounded; TODO items logged; race suite ~300s. Review dispatched (opus).
  • Ruling: accept NewObject budget 8 — plan's 7 miscounted the root level; no cost.
  • USER (2026-11-11): finish Task 16 (fix round 2 + re-review) and Task 17 (review + fixes), then wind down: write handoff into committed docs so nothing depends on this conversation, then push to main. Do NOT start Task 18.
  • Task 16 fix round 2 dispatched (resume implementer), BASE 12ce57ed3.
  • Task 17 review (opus): spec ✅; 1 Important (reproduced): a listing that started before an add can overwrite the cached listing without the new child, and the stale listing now persists (cache.go:96-110, nodes.go:779) — fix with per-folder generation counter; Minors: negative xattr Size with BlockSizes lists as 0 (crypto.go:835, nodes.go:134-138); FuzzValidateName lacks UTF-8 assert; legacy tally map unbounded; WarmList re-seeds 251 files (~27s). File: task-17-review.md.
  • Task 17 fix round 1 waits for Task 16 fix round 2 (same tree).
  • Task 16 fix round 2: 787bb3b3f, cb6e6759b, 58fde59f3, 202b64cf1, 2a1a8f046, e7e43095d. Broader than ruled: EVERY node beneath an unverified folder is unverified (not only anonymous ones), reads included. Block cap 2^19 (2 TiB). Root of trust: share creator + root signer must be own address — needs live confirmation. Re-review 2 dispatched (opus). Task 17 fix round 1 dispatched (resume implementer), BASE e7e43095d.
  • Live check 2026-11-11 (free account, binary at e7e43095d in scratch worktree scratchpad/live-e7e43095d): with root-of-trust + subtree-distrust + manifest-first in place, no flag needed: lsl, sha1sum, full read of web-app-created Readme.txt (verified), copyto upload, Range: bytes=0- read, purge all OK. NB this drive's volume was created by this client; a drive created by Proton's own clients (paid account, never signed in) is still unchecked for the root-of-trust rule.
  • Task 16 re-review 2 (opus): NOT ALL CLOSED. Items 1,3,4,5 closed. CRITICAL: own-address check is case-insensitive but key lookup exact (account.go:246-253 vs :235-243,:267) — server sets CreatorEmail to a case variant of the user's address, keys then fetched from server => forged root trusted (probe-confirmed). Important: with ignore_signature_errors set, laundering by move/copy is silent (spec 6.4 promises a logged ERROR). Minors: parent cycle A→B→A recurses unbounded (nodes.go:667,:619); block cap bounds count not bytes (~250-350 MiB; no response-body limit); whole-file range with unknown size / understated signed size. Proportionality judgement: keep subtree-wide distrust for tier 1; count failures live in Task 21; if they occur add a narrower option. File: task-16-rereview2.md.
  • Ruling: keep "every node beneath an unverified folder is unverified, reads included" — reviewer judged it technically justified (forged parent key exposes child keys; ranged reads don't check manifest); cost if wrong: users with a legacy folder signed by a removed address need the flag for that subtree.
  • Task 16 fix round 3 (critical case-fold, ERROR log with flag, parent-cycle depth limit, TODO for response-body limit and range edge) waits for Task 17 fix round 1 (same tree).
  • Task 17 fix round 1: f89be42ea (listing generation count; fake Hold/Held fault), 18b4f7d1d (size from block sizes when Size invalid), 16480e3d8 (refuse non-UTF-8 names on write — real gap found by fuzz assert), c28077174 (warm list folded into TestBudgetList). Re-review dispatched (sonnet). Task 16 fix round 3 dispatched (resume implementer), BASE c28077174.

Task 17: complete — 3fac25de7, 32710eefd, 90b2a48e7, 12ce57ed3, f89be42ea, 18b4f7d1d, 16480e3d8, c28077174. Re-review (sonnet): ALL CLOSED (compare mutation-checked). Deferred minors: TestListingRacingChange can hang until package timeout if it fails before close(release) (add t.Cleanup + timeout on <-held); xattr "Size":null treated as size 0, skipping the block-size fallback (crypto.go ~840); legacy-route tally map unbounded in protonfake; report's 8 TiB/16 MiB claim was wrong (code comment right); API error messages unbounded/unsanitised; block-size sum vs size not checked at parse; "invalidate of uncached node bumps all in-progress listings" costs requests only.

  • Task 16 fix round 3: 15bc60ead (ASCII-only case rule, own addresses never looked up on server), 52a44e41c (ERROR per unverified node+operation with the flag), 8666b6603 (parent cycle / depth 2048), 0cd3af429 (whole-file range returns only requested bytes), 9c90998f7 (docs: ~250-350 MiB at cap). Re-review 3 dispatched (opus).

Task 16: complete — 606580196, 59f0f31b3, db97ef7c8, f4105adfc, 88654eb04, 9598e22d9, 248f46705, 751a3ea35, 787bb3b3f, cb6e6759b, 58fde59f3, 202b64cf1, 2a1a8f046, e7e43095d, 15bc60ead, 52a44e41c, 8666b6603, 0cd3af429, 9c90998f7 (3 fix rounds). Re-review 3 (opus): ALL CLOSED, trust-root bypass probes failed (address-key injection x4 refused). Deferred minors: Update of an unverified file with the flag logs "reading" not "updating" (upload.go:306 → download.go:196-200); below slice copied per level (~33 MB transient at depth 2048); depth-limit error ~88 KB because each level re-wraps (nodes.go:533,:546); relabelled-empty-email quirk (not exploitable); non-ASCII case variants of an own address count as foreign (check live); with the flag on a long mount, one ERROR per unverified node+operation and the reported map grows; ranged reads still unverified against the manifest; parentless directly-shared file refused; on-the-fly remote hint names bare backend.

  • Live check 2026-11-11 (free account, binary at 9c90998f7, scratch worktree): sync up 3 files, check 0 differences, multi-thread download (4 streams) byte-identical, verified full read of web-app file, server-side moveto, purge — no flag needed.

PAUSED 2026-11-11 at 9c90998f7 (owner's instruction)

Tasks 1–17 complete and reviewed. Tasks 18–21 not started. Final whole-branch review not run. Handoff: docs/plans/001-protondrive-tier1-HANDOFF.md. This ledger is copied to docs/plans/001-protondrive-tier1-workspace/progress.md.

RESUMED 2026-11-12 (owner: "Continue work from HANDOFF") on branch protondrive-tier1 at d211b45e6

  • Ruling: Task 18 writes .gitea/workflows/ci.yml even though the runner is unknown (plan says so; unverifiable locally) — cost if wrong: an unused file.
  • Ruling: Task 18 implementer decides the shared-session design for protonfake under test_all after checking token expiry/Disconnect; any change stays inside protontest — cost if wrong: flaky wider suites, visible at once.
  • Task 18 dispatched (opus), BASE d211b45e6.
  • 2026-11-12: owner stopped the session to switch the controller to Opus. Task 18 implementer stopped mid-task, no commits, no report. Uncommitted work left in the tree (init.d/TestProtonDriveFake, .gitea/, PORTS.md, test_all config.yaml, docs/{ARCHITECTURE,DECISIONS,PERMISSIONS,TODO}.md); tracked-file diff saved as task-18-partial.diff. Unreviewed: next controller should inspect it, then resume Task 18 from it or discard it, and re-dispatch. A stray local protonfake process was killed.
  • 2026-11-12: controller now Opus 5.5. Task 18 implementer resumed (same agent, context intact) from its uncommitted work.
  • OWNER ANSWERS 2026-11-12: (1) Gitea HAS an Actions runner, but it is slow and not very capable — keep ci.yml light. (2) Task 21 may install a Proton desktop/CLI app and use the web app, preferably inside a Docker container (docker available on host). (3) YES to removing the test-account usernames from git history with a force push. Only 901d7fd5b adds them (85f6b2f0e removes them). Plan: after Task 18 commits land and the implementer is idle, rewrite from 901d7fd5b (redact usernames in that commit only, replay the rest), force-push master once, record in DECISIONS.md and handoff.
  • Task 18 implemented: 28f1aaf8a. DONE_WITH_CONCERNS: 5 compare-dest/copy-dest/checksum tests ignored (per-Fs listing cache; a second Fs on same account doesn't see the first's writes); gitannex ignored (test-harness env-remote limitation); .claude/settings.json not edited (owner must add golangci-lint allow — agent cannot widen own perms); shared session OK (no 401 in standalone fake). Review dispatched (sonnet).
  • Task 18 review (sonnet): spec ✅, quality approved, no Critical/Important. Cache diagnosis confirmed; init.d lifecycle checked (no orphans, build failure and busy port both rc=1). Minors (deferred): stop has no SIGKILL escalation; shellcheck SC2034 unused i; ci.yml lacks concurrency group; golangci-lint 2.15.0 build-Go vs go 1.26 may fail on first Gitea run. File: task-18-review.md.
  • Ruling: keep the 5 ignored cross-Fs-cache tests and leave the fix for M5 (events) — shared per-account cache is forbidden package state, re-list-on-miss breaks budgets, TTL doesn't fix the tests; added mount-staleness note to TODO; flagged as owner question 5 in handoff — cost if wrong: stale --compare-dest/--copy-dest only when the reference is written by another Fs in the same process; mounts never see external changes until M5.

Task 18: complete — 28f1aaf8a (rewritten as 5afbe9e4f). Review: approved, minors deferred.

  • HISTORY REWRITE 2026-11-12 (owner approved): 901d7fd5b→31f6aafd9 (usernames redacted), 85f6b2f0e→4282540e0, d211b45e6→65840bdc5, 28f1aaf8a→5afbe9e4f; final tree identical. Gitea master force-pushed 85f6b2f0e→4282540e0 with lease. Local master at 65840bdc5 (not pushed beyond what remote had). Recorded in DECISIONS.md and handoff.
  • Task 19 dispatched (opus), BASE b667a1ab0.
  • Owner added Bash(golangci-lint run:*) to .claude/settings.json allow list (2026-11-12). After Task 19 lands: remove that item from docs/TODO.md and the PERMISSIONS.md Known gaps, commit with the settings change.
  • Task 19 implemented: cf7577dc1. DONE_WITH_CONCERNS: binary 152,460,576 -> 148,995,400; tidy removals only (2 libs, iso8601, 8 indirect); 29 cross targets OK; whole repo 186 pass, cmd/bisync TestRcBackupDir fails pre-existing (also at 06700aaf2); differential coverage lost (multi-block/empty old-lib files, v1.76 reading new writes). Review dispatched (sonnet).
  • Committed owner's golangci-lint allow rule + doc cleanup. Found: deny rule 'git push --force-with-lease:*' does not match '--force-with-lease=...' — logged in PERMISSIONS Known gaps; proposed fix to owner.
  • Task 19 review (sonnet): spec ✅, quality approved, no Critical/Important. Minor: handoff table stale (fixed). File: task-19-review.md.
  • Ruling: do not generate multi-block/downgrade fixtures now — b667a1ab0 keeps the differential tests and libraries reachable in history, so it stays reproducible; noted in TODO — cost if wrong: a later agent spends an hour building fixtures from a worktree.

Task 19: complete — cf7577dc1. Review approved.

  • Task 20 dispatched (opus), BASE after docs commit.
  • Task 21 prep (controller): official Proton Drive CLI 0.9.0 exists (proton.me/download/drive/cli/0.9.0/linux-x64/proton-drive, SHA-512 3533025b...7e91ca8 on the download page). Login is browser-only (proton-drive auth login prints a URL) → owner must complete it once per account; run in Docker with PROTON_DRIVE_CREDENTIALS_STORE=unsafe_file in a throwaway volume. Commands: filesystem list|upload|download --json. Live rclone runs (TestIntegration, test_all -n 1) done by controller from a scratch worktree; v1.76 downgrade check uses a COPY of the config (refresh tokens single-use; new code falls back to password if the real config's token dies).
  • Task 20 implemented: 7be50f9e2. DONE_WITH_CONCERNS: generated options block stale (needs make backenddocs before upstream; logged); browser sign-in documented as primary but unproven live; docs follow code where spec differs (name rules); SDK ref tag question; ARCHITECTURE rough-edges line kept; about = account usage unverified live. Review dispatched (sonnet).
  • Task 21 prep: official CLI 0.9.0 downloaded, SHA-512 verified, image proton-drive-cli:0.9.0 built (debian:trixie-slim, non-root, unsafe_file store). Dockerfile in controller scratchpad proton-cli/. Next: owner completes 'auth login' URL for the free account.
  • Task 20 review (sonnet): spec ✅, quality needs fixes. Important: (1) docs say a ranged read of a file without recorded block sizes "may fail"; code FIXME (download.go:142-145) says it returns wrong bytes, and the check at download.go:~614 only fires on the 2nd block → single-block range silently wrong. (2) SDK version cited as tag js/v0.23.0 at 05c9115; cite commit hash + CHANGELOG entry. Minor: enable_caching Help vs page. File: task-20-review.md.
  • Ruling: fix (1) in code, not just docs — silent wrong bytes is a correctness bug for files from other clients. When block sizes are unrecorded and the range starts after block 0, read from the start of the file and discard (or another approach that is correct and checked), accepting the cost on such files; regression test with the fake writing non-4MiB blocks without sizes; docs then describe the cost. Cost if wrong: slower ranged reads (mount) on old-client files only.
  • Task 20 fix round 1 dispatched (resume implementer), BASE 7be50f9e2.
  • Task 20 fix round 1: 22b9c4fdc (ranged reads of unrecorded-size files read from block 0 and discard; regression TestDownloadRangesUnrecordedBlockSizes + fake SeedFileInBlocks; SDK cited by commit; enable_caching help aligned). Re-review dispatched (opus).
  • Task 20 re-review (opus): ALL CLOSED (mutation-checked: 7/9 subtests fail on old download.go). Minors: package comment tag wording (fixed by controller), spec 167-170 same tag wording (left: spec history), TestDownloadEndlessBlockList "assumed" name, TestDownloadRanges bound always 3, two unclear protontest/drive.go comments (deferred), perf TODO added by controller.

Task 20: complete — 7be50f9e2, 22b9c4fdc, + controller follow-up commit. Review approved after 1 fix round.

  • Task 21 live 2026-11-12 (free account, binary 502a32b5f, scratch worktree live-502a32b5f): TestIntegration -remote TestProtonDrive: PASS (98 pass, 29 skip, 0 fail, 120 s; only expected "directory not found" errors; no leftovers).
  • Task 21 downgrade check vs released rclone v1.76.0 (downloads.rclone.org, SHA256SUMS verified; run with a COPY of the config, deleted after): v1.76 downloaded every file the new backend wrote (incl. web-app file) byte-identical; new backend read v1.76's 9 MiB + small uploads byte-identical incl. a mid-file range. Test folder purged.
  • Official CLI sign-in #1 timed out (~19 min, owner not yet at it); restarted, new URL given to owner.
  • Task 21: test_all -n 1 -maxtries 1 against TestProtonDrive: running (bisync/gitannex ignoretests as configured upstream).
  • Official CLI 0.9.0 signed in (free account) by owner via browser fork URL on 3rd try (CLI polls only ~10 min; a late sign-in shows "Invalid code" in the browser).
  • Interop 2026-11-12 (free account, binary 502a32b5f, CLI in Docker): CLI downloaded rclone-written probe folder byte-identical, 0 failures; CLI info shows keyAuthor/nameAuthor/contentAuthor ok:true for rclone files. CLI uploaded 13 MiB random, small, empty, unicode-named nested file → rclone (no ignore flag) copied all byte-identical, mid-file range OK. rclone rcat new revision + moveto (unicode) + mkdir → CLI lists and downloads correct content, signatures ok. Folder rclone-interop-20261112 left on the account as evidence.
  • OWNER 2026-11-12: golden fixtures = option 2 (re-key captured nodes onto test-only keys; nothing from the account committed). Brief task-21g-brief.md written by controller. Ruling: implementer builds capture+rekey+TestGoldenFixtures against the fake only; controller runs the single live capture (web-app folder my-little-folder + CLI folder rclone-interop-20261112) and reviews output before committing fixtures. Task 21g dispatched (opus), BASE 502a32b5f.
  • Task 21 live test_all (free account, 502a32b5f, -n 1 -maxtries 1, 46 min): FAIL 8 = the 5 known cross-Fs-cache tests (confirmed live) + 3 live-only: operations TestPurge ("Failed to put A1/B1/C1/one: object not found"), vfs TestDirRemoveAll and TestZipDirsInRoot ("upload committed, but the file now has another revision (revision is active, not X)" — EMPTY active revision). Manual repro of nested put x3: no failure (timing). Logs: scratchpad/live-testall/2026-11-12-104408. backend suite inside test_all: OK.
  • Ruling: treat as one real-server read-after-write lag in the post-commit confirmation; fix = retry the confirmation briefly when node not found or active revision empty, distinguish from a genuine other revision; fake gets a lag fault to reproduce. Task 21f (fix) queued after 21g (same tree). Cost if wrong: diagnosis off, re-run live shows it.
  • Task 21g implemented: a2dc6f71f. DONE_WITH_CONCERNS: signed SEIPD (names/hash keys/xattrs) cannot stay byte-identical when inner signatures are re-made — decrypted, inner packets kept, sig/OPS re-made, re-encrypted with same session key/cipher/version/AEAD/chunk; unsigned messages and all blocks byte-identical; node passphrases new; block signatures, user key, thumbnailed files left out; untested real packet shapes fail loudly. Review dispatched (opus). Controller live capture into scratchpad (not tree) pending review.
  • Task 21f (post-commit read-after-write lag fix) dispatched (opus) in parallel with 21g review, BASE a2dc6f71f.
  • Task 21g review (opus): spec ❌. Important: I-1 foreign intended-recipient fingerprint written into Rekeyed note (golden_internal_test.go:1256-1259); I-2 identity() keeps user-ID name part and foreign emails inside armored keys (:898-908); I-3 capture reuses -remote → a bad -run would run TestIntegration (writes/deletes) live (:38-39,:108-128). Packet-level leak check of round trip by reviewer: clean. Capture is read-only on the account. File: task-21g-review.md. Fix round 1 dispatched (resume implementer). Live capture waits.
  • Task 21f implemented: af6fdd789. Diagnosis confirmed (both from the single post-commit read: draft/empty revision, or not found). Retry 100ms→2s, 8 reads (~7s). errUploadSuperseded only for non-empty revision that is neither ours nor the starting one. Ruling (accept implementer's choice over my lean): lag beyond retries after successful commit → errUploadUnconfirmed (deletes nothing, not retried at once) rather than success with a made-up node — cost if wrong: a rare spurious error where a later sync pass finds the file. Concerns: lag length unmeasured; activeNode not lag-tolerant; failed-commit stale read can still delete draft (ruling 11 risk now real); cancellation waits up to ~7s. Review dispatched (opus).
  • Task 21f review (opus): spec ❌. CRITICAL (pre-existing ruling-11 path, now likely under lag; reproduced in scratch): after a FAILED commit a single stale read (draft / old revision) is taken as proof it didn't land → delete_multiple on the now-active file or DELETE on the now-active revision (upload.go:401-403,425). Pacer resends commit on 5xx/408/network, ctx cancel also leads here. Minors: empty-revision lag state untested; 7s retry ignores Ctrl-C; --retries pass may add an identical revision; two tests not seen failing. File: task-21f-review.md.
  • Ruling (adopt reviewer's rule): delete a draft ONLY when the server explicitly refused the commit (4xx with a Proton code) on a single attempt that was not resent; no read-back decides deletion. Every other failure → errUploadOutcomeUnknown, delete nothing. Scratch repro becomes a regression test. Supersedes the "positive proof by read" half of ruling 11. Cost if wrong: orphan drafts accumulate on the server after ambiguous failures (cleaned by CleanUp / server expiry) — acceptable vs data loss.
  • Task 21f fix round 1 queued until 21g fix round 1 finishes (shared tree).
  • Task 21g fix round 1: 1b2fcbd21 (fail closed on foreign keys/recipients, placeholder user IDs, -capture-remote flag, packet-level leak check also in TestGoldenFixtures, tamper tests). Ruling: accept allowlist of generic node-key emails no-reply@proton.me / noreply@protonmail.com (C# SDK/CLI uses "Drive key no-reply@proton.me") — written keys still carry only the placeholder; cost if wrong: none (no account data). Re-review dispatched (opus).
  • Task 21f fix round 1 dispatched (resume implementer), BASE 1b2fcbd21.
  • Task 21g re-review (opus): NOT ALL CLOSED — I-3 open: TestIntegration ignores -capture/-capture-remote, so a mistyped -run with the real config runs the write/delete suite live. I-1, I-2 (allowlist proviso holds), M-1..M-5 closed; 4 new minors. File: task-21g-rereview.md.
  • Ruling: controller makes the trivial I-3 fix (TestIntegration skips when -capture or -capture-remote is set) after 21f lands, then live capture.
  • Task 21f fix round 1: be0053607 (delete draft only on single-attempt coded 4xx refusal, attempt counter via pacedCall; all else errUploadOutcomeUnknown; 4 regression tests failed before fix; Ctrl-C honoured post-commit; LagNoRevision tested). Re-review dispatched (opus).
  • Controller fixed 21g I-3: f8e398fce TestIntegration skips when -capture/-capture-remote set (verified skip + lint). Task 21g: complete — a2dc6f71f, 1b2fcbd21, f8e398fce. Live capture next.
  • Live capture 2026-11-12 by controller (free account, HEAD f8e398fce): web = my-little-folder (Readme.txt, 1 block), cli = rclone-golden-cli-20261112 (empty.bin no block sizes, nine-mib.bin 3 blocks, small.txt). Leak inspection: 11 secret needles (usernames, tokens, uid, salted key pass raw+decoded, both passwords) 0 hits; emails only golden@example.invalid; 0 Proton IDs; key UIDs only placeholder (gpg --show-keys); no 'proton.me'/'stereotypical' bytes anywhere. TestGoldenFixtures PASS offline. Committed 61733fce6 (~9.5 MB, mostly 3 random blocks). Plan Task 21 boxes ticked: capture, interop, upgrade/downgrade.
  • Task 21f re-review (opus): ALL CLOSED (resend guard mutation-checked). New minors: N1 --retries pass can reach own-draft delete_multiple in resolveConflict (logged in TODO, b440df2e1); N2 upload godoc wording; N3 no test pins 408 exclusion (deferred).

Task 21f: complete — af6fdd789, be0053607.

  • Live test_all re-run dispatched at b440df2e1 (scratch worktree live-b440df2e1).
  • Live test_all re-run (free account, b440df2e1, 37.5 min): FAIL only the 5 known cross-Fs-cache tests (TestCheckSumDownload, TestCopyFileCompareDest, TestCopyFileCopyDestImmutable, TestSyncCompareDest, TestSyncCopyDestImmutable). TestPurge, vfs TestDirRemoveAll/TestZipDirsInRoot now PASS — lag fix confirmed live. vfs fully green live. No leftovers. Logs scratchpad/live-testall-2/.
  • Paid account FIRST sign-in 2026-11-12 (b440df2e1): password/SRP OK, about = 64 GiB total, 1.152 MiB used; drive empty; mkdir/rmdir OK without ignore_signature_errors. Unknown whether the volume pre-existed (made by Proton apps) or was created now by the client (createVolume logs nothing) — asking owner.
  • rclone BROWSER sign-in proven live 2026-11-12 (b440df2e1, scratch config, free account, owner completed fork URL): session saved, listing + verified full read of the web-app file OK. Then config disconnect (logout) OK and later use gives the clear reconnect error; scratch config deleted.
  • Incident: rclone config create printed the new remote's config INCLUDING client_* tokens and salted key pass to stdout; the controller's redaction regex failed, so those values for that one session appeared in the controller's tool output (conversation transcript). Mitigation: that session was logged out immediately (tokens revoked server-side); the salted key pass is derived from the account password and is NOT revoked by logout — throwaway free account; owner informed. TODO: check whether client_* options are marked Sensitive so config create/show redact them.
  • Owner: paid account had (probably) never opened Drive before → its volume was most likely created by rclone; root-of-trust on a Proton-app-made drive still unverified (needs an account used only by Proton apps).

Task 21: complete as far as accounts allow — live TestIntegration, test_all (5 known cache fails), browser sign-in, paid sign-in, CLI interop, v1.76 up/downgrade, golden fixtures; commits a2dc6f71f 1b2fcbd21 f8e398fce 61733fce6 af6fdd789 be0053607 b440df2e1 29ad26944. Open: 2FA, CAPTCHA, desktop/mobile clients, Proton-app-made drive root of trust.

  • Final whole-branch review next (base 06700aaf2..29ad26944).
  • Final whole-branch review dispatched 2026-11-12: 4 parallel opus reviewers (auth/session/Fs, crypto/nodes/cache, upload/download, tests/fake/harness/docs/hygiene) over 06700aaf2..29ad26944; shared brief final-review-common.md; outputs final-review-{auth,crypto,transfer,tests}.md. Then ONE fix wave + scoped re-review.
  • Final review results: crypto 2C/3I/8m (C1 unsigned content key packet accepted → Update encrypts to server key, ranged reads forgeable; C2 server-supplied Argon2 params → OOM; I1 unverified hash key used on write; I2 rename reuses unverified name session key; I3 unsigned xattrs trusted); transfer 0C/2I/9m (draft_client_uid not persisted for env remotes; quadratic multi-thread reads of unrecorded-block-size files); auth 0C/2I/11m (2000/2028 fatal "upgrade", real 5003/5001 unmapped; x-pm-drive-requirements not logged); tests 0C/5I/19m (CI race timeout; CI Go 1.26.0 + govulncheck@latest; stale workspace copy; stale handoff §3; upstream recipe includes fork-only protontest dependents).
  • Rulings: see final-fix-brief.md (C1 = unverified file like ruling 18/22 → handoff ruling 29; error codes → ruling 30). Fix wave dispatched (opus, one implementer) for code+CI, BASE 29ad26944; controller fixes tests I-3/I-4/I-5 (handoff, workspace copy, spec 13 recipe) in parallel on disjoint files.
  • Final fix wave: e95317b52 (C1), db7afafc0 (C2), b3f5685e6 (I1), 67e4cfeb7 (I2), 94eedc942 (I3), 674b538ca (draft uid), 788a711dc (block-size cache 145→39 fetches), 5dbf5b04c (codes), b4a23a31e (requirements header), b5da26169 (CI), 634a2423e (tests honour RCLONE_CONFIG regardless of which tests run — earlier unit tests read the real config file; no Proton contact), 30cfcac41 (refresh retry), d3aee2af1 bfa9b7122 ad3552e2e 944b36005 (minors/docs).
  • Rulings: accept deviations — I3 size from verified block sizes (encrypted size would fail every copy); draft uid derived from volume ID (user ID not kept); C2 caps foreign keys only (own keys logged in TODO); 2064 unmapped. Scoped re-review dispatched (opus).
  • Live check of final fixes (944b36005, free account, no flag): all files in the 3 evidence folders (written by web app, official CLI, rclone, rclone v1.76) copy + check OK, bytes match original SHA1s; upload 9 MiB, mid-file range, moveto, full read OK; test folder purged. Stricter C1/I1/I3 checks accept real Proton-client files.
  • Final re-review (opus): ALL CLOSED; every fix mutation-checked; golden fixtures pass; Argon2 caps above gopenpgp/OpenPGP.js output; RCLONE_CONFIG fix proven with fake HOME. New minors M-a (ranged start from unsigned xattr block sizes), M-b (TestMain applies RCLONE_CONFIG only when set), M-c (draft uid random when section exists but unsaveable) → TODO.

Final whole-branch review: complete. Plan 001 execution complete as far as test accounts allow.