Repository navigation
Desktop fs-data key-rotation migration is not crash-resumable (mixed-key state possible) #359
Description
Activity
- added 4 commits that reference this issue
on Aug 13, 2026 Live status audit against main
34b83a22: the historicalservices/fs/fsEncryptionMigration.ts/ protected-FS migration path is not present on current main; project persistence currently writes plaintext throughprojectFsStore.tsandwriteTextFileAtomic, whileencryptText/decryptTextinfsCore.tsare not production callers. The issue wording therefore describes a former/proposed path, not a currently shipped encrypted migration. The underlying requirement remains OPEN and is a prerequisite of the renderer-neutral R-15 secure-storage implementation: if encrypted FS storage is admitted, rotation must have a durable inventory/journal/cursor, key epochs, commit semantics, and crash-resumable recovery. Do not close this issue based on the existing IDB journal.qnbs commented
on Aug 24, 2026 OwnerAuthorMore actionsR-15 / Rev-3 architecture reconciliation — 2026-08-24
Keep the original failure analysis as provenance, but implement the real fix under #445/R-15 renderer-neutral migration authority, not by growing a second long-lived fs/Tauri migration subsystem.
Reuse-first direction
The existing IndexedDB migration journal/orchestrator is an important semantic seed: resumable cursor, phases, recovery-required state and deterministic admission behavior already exist. Reuse the proven state-machine ideas and failure invariants where appropriate, but do not force browser-specific IDB implementation details into native Core.
Native R-15 should own one versioned migration/rekey journal model for native protected records, including:
- operation/version;
- deterministic record inventory or resumable cursor;
- source/target key epochs;
- pending/in-progress/done or equivalent state;
- durable commit/finalization phase;
- interruption/restart semantics;
- recovery-required and rollback policy.
Avoid duplicate migration authority
Do not leave Tauri fs migration and future Qt/Core migration as separate permanent authorities. Tauri may adapt to the Core implementation during transition.
Closure rule
Close only when #445 explicitly maps this issue's mixed-key interruption threat to implementation plus interruption/resume tests. The existing marker-file warning is an interim diagnostic, not closure.
qnbs commented
on Sep 5, 2026 OwnerAuthorMore actionsQueue / execution status — 2026-09-05
Priority:
P0as an R-15 acceptance requirement. Canonical implementation owner: #445.This issue preserves the crash-resumable rekey/migration invariant. Do not revive the historical filesystem bridge as a separate authority merely to close this ticket.
The renderer-neutral #445 path must provide a versioned, idempotent, resumable migration/journal contract with source/target key epochs, deterministic protected-record inventory, progress/checkpoint/finalization semantics and recovery-required behavior. Interruption must not silently strand a mixed-key dataset.
Close #359 only when the canonical #445 implementation has explicit interruption/fault evidence satisfying this requirement.
qnbs commented
on Sep 24, 2026 OwnerAuthorMore actions2026-09-24 curation checkpoint
Still OPEN as an R-15/#445 acceptance/provenance child. The temporary fs migration marker remains mitigation/evidence, not crash-resumable migration closure.
Do not start a competing Tauri-only journal. #553 has been reopened after an incorrect merge-time closure; #445 must recompute live readiness before implementation. Once admitted, reconcile this issue through the canonical Core migration/rekey journal and interruption evidence, then close it explicitly from #445 evidence.
qnbs commented
on Sep 30, 2026 OwnerAuthorMore actionsR-15 / v1.30.0 sequencing update — 2026-10-01
This P0 is now a direct closure input to Gate 4 owner #922 under #445 / release #926.
Gate 4 owns the durable authenticated journal/checkpoint, source/target epochs, resumable cursor/state machine, recovery-required semantics and exact resume/abort/finalize behavior needed to eliminate the mixed-key crash state described here.
Do not implement a second independent migration state machine beside R-15. Close #359 from Gate 4 evidence when the original crash-resumability acceptance is actually proven.
qnbs commented
on Oct 4, 2026 OwnerAuthorMore actionsGate 4D sequence checkpoint — 2026-10-04
#957 is merged and resulting-main remains exact at
8f1400c4bb6a84be8da940584b86b00233df1d4b. Vercel Production remainsREADYon that SHA. The five #957 preview candidates remain present and individually classify as non-production / non-rollback.Cursor could not mutate Vercel: MCP has read capability but no deployment/alias delete wrapper, its CLI session is logged out, and no token is present. No deletion was attempted. Therefore:
POST_#957_RETENTION = NOT COMPLETE PR_958 = FROZEN MERGE_#958 = NO FURTHER_PUSH_#958 = NO SLICE_B_IMPLEMENTATION = NO NEXT = authenticated local Vercel cleanup via Claude Code CLI Sonnet / existing operator session PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NOAfter retention becomes terminal, re-fetch the full #958 three-channel review census and address its two root-cause families in one docs-only correction wave.
- added a commit that references this issue
on Oct 4, 2026 74 remaining items
Gate 4D C2c-1 (authenticated page access, the page-local cursor) TERMINAL — PR #1016
CURRENT_MAIN = 4428fdd81c9e656be37861895a6cb001ef129dbc PR_1016 = MERGED (final head ab016ac9; 3 signed commits; 6 files; 0 inline review threads, 1 reviewer note fixed) RESULTING_MAIN_CI_CD = 37901688894 / SUCCESS CODEQL = 37901688906 / SUCCESS VERCEL_PRODUCTION = dpl_DBXbZkbSJDN2ii4yqSxfsDuC9XMp / READY / exact main / canonical HTTP 200 ROLLBACK = dpl_BKPsiSMc6X6PipP9Nt9S3oJuUYaL RETENTION = TERMINAL (2 previews deleted by exact ID, the one branch alias removed first, absence proven; 33 production/PROMOTED deployments + 3 canonical aliases retained; 0 previews; UNKNOWN = 0) B2 = TERMINAL C1 = TERMINAL D1 = TERMINAL D2a = TERMINAL D2b = TERMINAL EPOCH_RELATION = TERMINAL D3 = TERMINAL STREAMING_S1 = TERMINAL STREAMING_S2A = TERMINAL STREAMING_S2B = TERMINAL C2A = TERMINAL C2B1 = TERMINAL C2B2 = TERMINAL C2C1 = TERMINAL GATE_4D = IN PROGRESS OPEN_PRS = 0 PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NOLanded.
ConversionSession::verify_inventoryauthenticates the whole page set the committed root names, once, keylessly (the key is routed per call through two new keyless readers inauthority.rs,verify_committed_inventoryandload_committed_page), one page in memory at a time with only the references kept; the set must be the inventory the session's snapshot names (otherwiseSuperseded, spent); a read failure writes nothing and does not spend the session.pagereads one authenticated page by the exact path of its reference (the envelope must hash to the reference before it is opened).advance_cursornow needs the verified set (InventoryNotVerified) and refuses an entry index outside the authenticatedpage_entry_countof the selected page, after the existing extent check and before any write. The contract fixes what the cursor means:cursor_entry_indexis the index, from zero, within pagecursor_page_indexof the next entry to process; finishing a page moves it to(page + 1, 0); the end of the last page has no cursor value, so completion is the transition toVERIFY. This closes the criterion recorded from the review of #1015 (issuecomment-6074817348).Proof. 41 cases in
gate4d_conversion_entry_test, 6 new and 2 rebuilt, over a stored inventory of ten entries in pages of six and four: the references say six and four and both pages equal what was captured;(1, 9)and(1, 4)are refused (inside the total, outside page 1), forward and equal cursors accepted, a regressive cursor and an out-of-extent one refused; the cursor without the verified set refused with nothing written; a tampered, a missing and a swapped page refused by the verification with nothing written and the session not spent; a lost admission reported before a read, after a read and at the moment a read fails; a session another owner took over from cannot move the cursor. Mutation-checked; two checks survive by design (the unreachableNonearm for a page index outside the references, and the comparison of the verified set with the snapshot's inventory, which cannot differ without a defect because the inventory fields are frozen once captured). Windows evidence green.Review. One correction wave (the QNB-11 target). No inline thread. CodeAnt's PR Risk "Medium" noted that a failed inventory read returned before the post-read admission check, so a loss during a failed read was reported as the read error:
VALID_AND_FIXEDinab016ac9(tested, mutation-checked, recorded on the PR); Low Risk on the final head, quality gate passed. CodeRabbit: no actionable comment. CodeScene approved. Codex was rate-limited for this PR and never reviewed it (NO_SIGNAL, recorded on the PR); the substantive review of this security-adjacent slice is CodeAnt, CodeRabbit and CodeScene plus the mutation-checked tests, and the Codex review of the first slice that follows is the check on this contract change.Disclosed. Size: 6 files, 595 meaningful lines, 3 commits (target 400 lines / 6 commits): over the line target, inside the hard tier; about 330 lines are tests (a stored-inventory fixture and the rebuilt cursor tests). The page-local meaning of the entry index is proposed and fixed in the contract by this slice; the maintainer may correct it before C2c-2 builds on it.
Open before Gate 4 terminal:
- C2c-2: the cursor-driven iteration with a caller-supplied per-entry step (idempotent by contract), checkpoint and lease renewal inside the loop, crash/resume evidence,
CONVERT->VERIFY. - Handle-relative journal and root I/O in the platform adapter (issuecomment-6073349264; Gate 4E/5 adapter work).
- Inheriting unchanged pages; bounded
read_at_most/list_dir_at_mostdefaults before a second adapter; pending-directory reclamation; Gate 4E write barrier. Gate 5 must not start early. - For the maintainer (unchanged): may exclusive admission replace lease expiry as the proof a former owner is gone (takeover question).
NEXT. Read-only admission of C2c-2 on exact main; no source mutation before it returns READY.
PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO.- C2c-2: the cursor-driven iteration with a caller-supplied per-entry step (idempotent by contract), checkpoint and lease renewal inside the loop, crash/resume evidence,
Gate 4D C2c-2 (the cursor-driven iteration,
CONVERT->VERIFY) — read-only admission: READY as one bounded sliceBase main
4428fdd8, 0 open PRs. Authority: the QNB-11 "Next — C2c-2" section, maintainer decision D, §10.3 (CONVERT: each record is converted, verified, then checkpointed with the current fencing generation; already checkpointed records are replayed idempotently, pending records keep source authority;VERIFYfollows when every record of the final inventory is done), and the page-local cursor fixed by C2c-1.Findings on exact main.
- The session has every part of the loop but not the loop:
verify_inventory,page,enter_convert,advance_cursor(page-local, forward-only),renew_lease. Nothing calls a per-entry step, nothing decides when the inventory is done, and nothing leavesCONVERT. - The end of the last page has no cursor value (C2c-1), so "done" cannot be read from the cursor: the iteration has to know it saw the end, and a crash after the last entry must resume by processing that last entry again.
- Core reads no clock, so lease renewal inside a loop needs the caller's time; a loop that owns the clock would put a policy in Core that the maintainer has not decided.
Scope (<= 8 files / <= 400 meaningful lines, one correction wave).
conversion.rs: a traitEntryStep { type Error; fn convert(&mut self, entry: &JournalInventoryEntry) -> Result<(), Self::Error> }, idempotent by contract (it may run again for an entry after a crash or a failed checkpoint; real record conversion is Gate 5), and one bounded unit per call:ConversionSession::convert_next(fs, provider, ConvertBatch{step, entries})processes up toentriesentries of the page the cursor points into (a batch never crosses a page boundary), then records the new cursor throughadvance_cursorand returnsProgress::MoreorProgress::Done. Cursor after a batch:(page, next), or(page + 1, 0)at a page end, or the last entry itself at the end of the last page. A session that has seen the end answersDonewithout processing; an empty inventory isDoneat once. The caller loops, renewing the lease with its own clock between calls (renew_lease); the module documentation shows the loop.finish_convert:CONVERT->VERIFYthrough the shared step (transition_phase, cursor back to(0, 0)), only after this session saw the end (NotConvertedotherwise), so a premature call cannot skip entries.- Failure semantics: a step error returns
Step(error)with nothing written and the session intact (the already converted prefix of the batch is replayed after the next call or a resume); a failed checkpoint after the steps returns the session error, the batch is replayed on retry; a spent session, a lost admission or an unverified inventory are refused as everywhere else. Memory: one page at a time. - Tests (in the conversion test file, over the stored inventory of pages of six and four): a whole run in batches, every entry converted once in order and
VERIFYreached (a fresh gate refusesVERIFY, the root names it); batches do not cross a page boundary; a step failing in the middle: nothing written, then a restart (drop session and guard, begin again, verify) resumes at the persisted cursor and converts only the entries from there on, all entries converted at least once, none before the cursor again; the anchor refusing a checkpoint: the batch is replayed on retry;finish_convertbefore the end isNotConverted, inADMITWrongPhase; the empty inventory; the last entry processed again after a crash that followed it. Mutation-checked; contract, evidence, CHANGELOG. The feat(core): renew the lease through the conversion session (#445) #1014 checklist is applied before the first push.
Decisions flagged for correction (mine).
- A stepper (
convert_next), not a monolithic loop: each call is a bounded, testable unit and the lease/clock policy stays with the caller. - A batch never crosses a page boundary, so one call loads one page and writes one checkpoint.
- The last entry of the last page is processed again after a crash that followed it, because the cursor cannot point past the end; harmless under the idempotent contract.
- No checkpoint on a step failure: the prefix is replayed. The contract's per-record checkpoint is satisfied with
entries = 1; a larger batch is a caller's performance choice that relies on the same idempotence. VERIFYonly after this session saw the end of the last page.
Not in C2c-2: real record conversion and source adapters (Gate 5), the
VERIFYwork itself and later phases, takeover through the session, handle-relative adapter I/O, inheriting unchanged pages, adapter defaults, pending-directory reclamation, the write barrier. Open for the maintainer (unchanged): may exclusive admission replace lease expiry as the proof a former owner is gone.PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO. Implementation of C2c-2 proceeds on a branch from exact main.- The session has every part of the loop but not the loop:
C2c-2 (PR #1017): answer to the exact-head blocker of 2026-10-09 09:38 — NOT terminal, merge pending final-head review
The P1 on
ca26d16(the invalidation of a hand-moved cursor is lost throughbegin_conversion) is closed at the root in371de5dd:advance_cursorexists only with the test-onlytest-supportfeature, the cursor step is private and only the iteration moves the cursor, so a persisted cursor is the record of entries a session converted (residual: the authority's lower level checkpoint can still write a cursor inside the extent; for the Gate 4E/5 orchestrator not to use). Codex completed its review of371de5ddwithDidn't find any major issues; CodeAnt's note on the same head (the wording "a failed step writes nothing" could be read as a rollback) was valid and is fixed in20afde67, comments and documents only. The point-by-point proof against the QNB-11 list (durable restart repros, stored cursor continuity, idempotent last-entry replay, takeover/fence re-check across the callback, page bounds, phase transition, keyless/no-clock) is on the PR: #1017. The merge waits for the Codex review of20afde67and for CI on that head; C2c-2 stays non-terminal until the post-merge sequence is complete.PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO.Gate 4D C2c-2 — TERMINAL (PR #1017, merged as
92bf64ac)C2c-2 is terminal. The conversion session now iterates the inventory and leaves
CONVERTforVERIFY. Real record conversion (Gate 5), handle-relative adapter I/O, the write barrier and the rest of Gate 4E are not part of it.PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO.Merge squash 92bf64ac185fde9a399ed476f701d424778441ca, exact reviewed head20afde67, merged with--match-head-commitSize 6 files, +782 / −21 (source, tests, contract, evidence, CHANGELOG), 5 signed commits, inside the hard tier Resulting main CI / CD 37923143912success, CodeQL37923143923successProduction dpl_B31kADJeBvN6uXPhg4dAFusYkEw3, READY, PROMOTED, exact92bf64ac; the three canonical aliases are on it;https://worldscript-studio.vercel.app/answers 200 (the two team-scoped hosts redirect to Vercel SSO, deployment protection)Rollback dpl_DBXbZkbSJDN2ii4yqSxfsDuC9XMp(previous Production, exact4428fdd8)Retention the four #1017 previews ( 44ba1be0,ca26d16a,371de5dd,20afde67) and one branch alias removed by exact ID after a dry classification with every gate passed; final: 34 Production deployments READY / PROMOTED, 3 aliases, 0 previews, 0 unclassifiedGit branch tree identical to merged main, remote and local branch deleted, worktree detached at 92bf64acCapability.
convert_next(fs, provider, ConvertBatch { step, entries })converts up toentriesentries of the page the cursor points into, never across a page boundary, through the caller's idempotentEntryStep(which is handed the owner's fence), and records the next cursor(page, next)/(page + 1, 0)/ the last entry itself at the end of the last page.Progress::Doneis a property of the session;finish_convert(CONVERT->VERIFY, cursor back to(0, 0)) needs a session that saw the end. A step failure records no checkpoint and rolls nothing back, so the step must be idempotent; the lease policy and the clock stay with the caller (renew_leasebetween calls). The cursor is moved only by the iteration:advance_cursorexists only with the test-onlytest-supportfeature, so a persisted cursor is the record of entries a session converted.Review of the exact heads. Codex on
44ba1be0: three findings, fixed inca26d16a; onca26d16a: one P1 (the invalidation of a hand-moved cursor lost throughbegin_conversion), closed at the root in371de5dd; on371de5ddand on the final head20afde67: "Didn't find any major issues". CodeAnt's wording note (a failed step "writes nothing" read as a rollback) fixed in20afde67. CodeScene approved every head. CodeRabbit raised one cosmetic point on the final head (see below). 5 threads, 0 unresolved. The point-by-point proof against the maintainer's QNB-11 list of 2026-10-09 09:38 (durable restart repros, stored cursor continuity, idempotent last-entry replay, takeover and fence re-check across the callback, page bounds, phase transition, keyless / no-clock) is on the PR.CI. 38 checks green on
20afde67.E2E Deep Coveragewas cancelled once at the 30-minute job timeout while still in "Install Playwright browsers" (a runner hang, before any test ran) and passed on a re-run of that job on the same head; no code or commit was changed for it.Deferred, with a criterion (
VALID_AND_DEFERRED_WITH_ACCEPTANCE_CRITERION): CodeRabbit on the final head,conversion.rs:519reads "The cursor The cursor is page-local" in the rustdoc of the test-onlyadvance_cursor. Cosmetic, no behaviour or contract text, and the head was frozen after the consolidated wave. Criterion: the next Gate 4D slice that touchesconversion.rscorrects the sentence in its first commit.Still residual (unchanged list, none closed by this slice): the authority's lower-level
commit_journal_checkpointcan still write a cursor inside the manifest's extent (for the Gate 4E/5 orchestrator not to use to claim progress); handle-relative journal/root I/O (criterion above); inheriting unchanged inventory pages across retries; boundedread_at_most/list_dir_at_mostdefaults before a secondDurableFsadapter; pending-directory reclamation; Gate 4E write barrier, phase-aware refusal and restart evidence. Open for the maintainer: may exclusive admission replace lease expiry as the proof a former owner is gone. Gate 5 does not start early.Gate 4D F1 (bounded reads are required of every
DurableFsadapter) — read-only admission: READY as one bounded sliceBase main
92bf64ac, 0 open PRs, no new maintainer text on QNB-11 or here since the C2c-2 terminal proof (issuecomment-6080320510). Authority: the QNB-11 list "Remaining before Gate 4 terminal" (boundedread_at_most/list_dir_at_mostdefaults before additionalDurableFsadapters), the Gate 4D evidence (the manifest read "is bounded"; residual list: both defaults must be overridden by an adapter over real files).Findings on exact main.
DurableFs::read_at_mostandlist_dir_at_mosthave default bodies that callread/list_dirand compare the length afterwards, i.e. they load the whole file or directory before the limit is applied. They exist so that the test doubles that interceptread/list_dirstay faithful. The only protection against an adapter that forgets to override them is a sentence in the doc comment. Every Gate 4D journal read (manifest envelope, page envelope, the page-directory listing of at most 64 names, the staged-capture reads) goes through these two methods precisely so that a corrupted or hostile size cannot exhaust memory; an adapter that keeps the default loses that bound silently and nothing fails.StdFsoverrides both correctly (take(limit + 1)); there are 15 other implementors, all test doubles undertests/. Only 5 test files sharetests/support, so a helper kept there would not reach the rest.- Outside this slice, and recorded rather than fixed here: the Gate 3 record, root and catalog paths (
commit.rs,root_store.rs,authority.rs) still call the unboundedread/list_diron authority files and directories, andjournal/durable.rsreads back its own just-promoted file withread. They predate the Gate 4D bounded-read rule and have no limit constants yet; a per-file limit needs the codec's maximum envelope size and is its own admission.
Scope (<= ~13 files, small line count, one correction wave). The trait is fixed at the type level, not by a comment:
durable.rs:read_at_mostandlist_dir_at_mostlose their default bodies and become required methods, so an adapter cannot compile without stating how it bounds the allocation. The docs say what the contract is (never more thanlimit + 1bytes / names in memory;Noneabove the limit; the error of the underlying call otherwise). Two helpers,read_at_most_via_readandlist_dir_at_most_via_list_dir, compiled only with the existingtest-supportfeature (the same pattern asgenerate_with_randombehindtest-randomness), give a test double its old faithful behaviour in one line, documented as loading everything first and never for an adapter over real files.- The 15 test doubles implement the two methods in one line each (the helper, or a delegation to the inner
StdFswhere the double already wraps it). No behaviour of any test changes. - A compile-fail doctest pair on the trait pins the rule mechanically: one doctest implements every method and compiles; the other differs only by omitting the two methods and must not compile. Existing
StdFstests (std_read_at_most_never_loads_more_than_the_limit, the listing test) stay and keep covering the bound itself. - Contract / evidence / CHANGELOG: the adapter obligation moves from "should override" to "must implement"; the unbounded Gate 3 paths are listed as an explicit residual with this finding as the reason.
Decisions flagged for correction (mine).
- Required methods rather than a bounded default: a bounded default is not expressible without an additional required primitive (an
open/handle or an iterator), which would change every double's fault interception and belongs to the handle-relative adapter work (Gate 4E/5). - The faithful helpers are public items behind
test-supportrather than atests/supportmodule, because only 5 of the test files share that module and the feature is already the crate's boundary for test-only API. - The Gate 3 unbounded reads are recorded, not changed: they need a limit constant per file kind, which is a design decision about the record envelope.
Not in F1: the Gate 3 bounded reads, inheriting unchanged inventory pages, pending-directory reclamation, handle-relative adapter I/O, Gate 4E, any behaviour change in
conversion.rs(the deferred rustdoc typo from C2c-2 is therefore not triggered here and stays on its criterion). Open for the maintainer (unchanged): may exclusive admission replace lease expiry as the proof a former owner is gone.PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO. Implementation proceeds on a branch from exact main; the #1014 checklist (outcome matrix, Windows portability of every helper, residual lists) is applied before the first push.Gate 4D F1 — TERMINAL (PR #1018, merged as
d1710f0a)F1 is terminal.
DurableFs::read_at_mostandlist_dir_at_mostare required trait methods: an adapter cannot compile without stating how it bounds the allocation. This closes the QNB-11 item boundedread_at_most/list_dir_at_mostdefaults before additionalDurableFsadapters. No runtime behaviour ofStdFsor of any journal read changed.PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO.Merge squash d1710f0a0f14ef3e3a8ce70f0c1e59582c3209f9, exact reviewed head65de48617e91078d5d2d941f45c29d0e6f7a3d62, merged with--match-head-commitafter the exact-head CI and review closeout the maintainer's 14:20 CEST checkpoint asked forSize 13 files, +340 / −15 (about 90 lines of it rustdoc; 9 of the files are one-purpose test doubles), 2 signed commits, inside the hard tier Resulting main CI / CD 37930964197success, CodeQL37930964418successProduction dpl_9T9o3RvF7LkaFmToPnaUX5VqMMgb, READY, PROMOTED, exactd1710f0a; the three canonical aliases are on it;https://worldscript-studio.vercel.app/answers 200 (the two team-scoped hosts redirect to Vercel SSO, deployment protection)Rollback dpl_B31kADJeBvN6uXPhg4dAFusYkEw3(previous Production, exact92bf64ac)Retention the one #1018 preview ( 65de4861) and its branch alias removed by exact ID after a dry classification with every gate passed; final: 35 Production deployments READY / PROMOTED, 3 aliases, 0 previews, 0 unclassifiedGit branch tree identical to merged main, remote and local branch deleted, worktree detached at d1710f0aWhat landed. The two methods lose their default bodies (which loaded the whole file or directory before applying the limit, protected only by a doc sentence). The contract is stated on the trait: at most
limit + 1bytes or names held,Noneabove the limit, the underlying error otherwise;StdFsis unchanged (take(limit + 1)). The 15 in-tree test doubles implement them through two public helpers compiled only withtest-support(read_at_most_via_read,list_dir_at_most_via_list_dir: what the defaults did, documented as never for an adapter over real files). A doctest triple on the trait pins the rule: an adapter implementing every method compiles; the same adapter withoutread_at_most, and withoutlist_dir_at_most, each fails withE0046.Proof. The rule is compile-time, so the mutation is the rule itself: re-adding the default body of
read_at_most, or oflist_dir_at_most, makes exactly its own compile-fail doctest fail ("compiled successfully, but it's markedcompile_fail"); the file was restored byte-identical each time. An earlier version of the doctest (one compile-fail case omitting both methods) was found by the mutation check to leave each method unpinned on its own, and was split before the first push. Every suite that uses a double passes unchanged (13 binaries, listed on the PR). Clippy over all targets,cargo fmt --check,docs:check; the library builds withouttest-support.Review of the exact head. Codex on
65de48617e: "Didn't find any major issues". CodeAnt: gate passed, PR Risk Low. CodeScene approved. CodeRabbit: no actionable comment. Sourcery: rate-limited (NO_SIGNAL). 0 review threads. 38 checks green on the exact head (codecov/patch, the Windows secure-store evidence, E2E and E2E Deep Coverage included), no re-run needed.Still residual (none closed by this slice): the Gate 3 post-promotion verify and the page, marker, root, record and catalog reads (
commit.rs,root_store.rs,authority.rs) still use the whole-fileread/list_dir(limits per file kind, the separate acceptance criterion already recorded on #359); inheriting unchanged inventory pages across retries; pending-directory reclamation; handle-relative journal/root I/O (Gate 4E/5 adapter, criterion above); Gate 4E write barrier, phase-aware refusal and restart evidence; the lower-levelcommit_journal_checkpointcursor residual. Open criterion still pending: the next Gate 4D slice that touchesconversion.rscorrects the repeated "The cursor The cursor" in the rustdoc ofadvance_cursorin its first commit (F1 did not touch that file). Open for the maintainer: may exclusive admission replace lease expiry as the proof a former owner is gone. Gate 5 does not start early.Gate 4E E1 (the ordinary-operation barrier of a bound root, pinned with evidence) — read-only admission: READY as one bounded, tests-and-docs slice
Base main
d1710f0a, 0 open PRs, no new maintainer text on QNB-11 since the 14:20 CEST checkpoint, which lists Gate 4E ("complete phase-aware writer barrier, final capture/admission refusal, restart/crash evidence") as pending. This admission takes the write barrier because it is the part of Gate 4 that protects data. Authority: contract §10.3 (phase table:DISCOVERadmits no ordinary writes;PREPAREadmits ordinary reads and writes, the root binding copied forward unchanged;ADMITonward andRECOVERY_REQUIREDblock ordinary writes;DONEpermits normal policy), §5.4 / §10.1.1, the open criterion "write barrier of the final capture" on #359 (issuecomment-6038697749), the predicateordinary_mutating_writes_admitted(true exactly forPREPAREandDONE).Findings on exact main (a first reading of this item, that the writer never looks at the migration, was wrong and is corrected here).
- Every ordinary operation of
ProtectedStorage(try_write_record,try_reconcile_record,list_recordsandread_record, the last two also through anAuthoritySnapshotGuard) loads the catalog throughcatalog(), which returnsMigrationRequiredas soon as the committed root carries a live-migration binding (operations.rs,catalog).try_lock(from an unlocked key),try_unlockandtry_shutdown(when not already locked) refuse withRecoveryPendingfor the same reason (verify_transition). The input is the authenticated committed root alone, so the barrier is durable across a crash and needs no journal read and no journal location. - The barrier is therefore phase-independent and stricter than §10.3: while a binding exists it also refuses the reads and the
PREPARE-window writes that the contract admits. That is fail-closed and safe; it is not yet "phase-aware". - No test combines
ProtectedStoragewith a bound root (the test files that useProtectedStorageand those that use a live binding are disjoint). The barrier that stops an ordinary writer from mixing keys during a migration has no direct evidence, and no cold-start evidence. ordinary_mutating_writes_admittedhas no caller outside its own test; it is the specification for the relaxation, not a gate in use.- No production caller binds a root today (bind is a Gate 4E/5 enable step), so the barrier is currently unobservable in production; the evidence is what keeps it from regressing before the first binder exists.
- A phase transition out of
PREPAREis only safe against a running ordinary writer if it happens under exclusive admission (a shared writer holds shared admission for its whole write). The conversion gate holds it by type (C2a); the lower-level checkpoint API does not require it. That is criterion (5) and belongs to E3.
Split by proof boundary (Gate 4E).
- E1 (this slice): evidence and documentation of the existing barrier. No behaviour change.
- E2: the phase-aware relaxation, only when the first binder needs it:
PREPAREandDONEadmit ordinary reads and writes through the root-named manifest (read_committed_journal), which needs a journal location as writer configuration (it is caller-supplied everywhere else and is not in the root), a fail-closed rule for a binding without a location, and a policy for reconciliation under the barrier. - E3: the
ADMITtransition and the final capture's entry require exclusive admission at the type level; the final-capture admission refusal.
E1 scope (<= 8 files, tests and documents). In
tests/support/operations.rsa constructor that commits a root binding a live migration before the storage is built (the same body-commit as the conversion fixture'sbind), so the storage starts cold over an already bound tree. A new test file over the real tree:try_write_record,try_reconcile_record,list_records,read_record(through anAuthoritySnapshotGuard; whether the guard itself is obtained over a bound root is pinned by the test, not assumed) refuse withMigrationRequired;try_lock/try_unlock/try_shutdownrefuse withRecoveryPending; the tree is byte-identical afterwards (nothing written, no marker, no staging, no root); the refusal does not depend on what the binding names (an operation id, a fence and a manifest digest that name no journal at all are refused identically, which pins "the root alone decides"); the same fixture without the binding admits (control, so the refusal is the binding's); a second, cold storage over the same tree refuses the same way. Mutation-checked: the live-binding refusal incatalog()and inverify_transitionremoved one at a time must fail the tests that own them. Contract §10.3 and the evidence document state the current behaviour as an intentional, stricter interim, with the relaxation as E2.Decisions flagged for correction (mine).
- Keep the stricter behaviour for now and prove it, rather than relax to the contract's
PREPAREwindow: relaxing needs a journal location and a reconciliation policy, and no binder exists to need it. Say so if you want thePREPARE-window availability earlier. try_authority_snapshotis not changed in E1: whatever it does over a bound root is recorded by the test; the guard's reads refuse either way.- Reconciliation of a pending ordinary write under the barrier stays refused in E1 (a pending write from before the barrier must be reconciled by the orchestrator under its exclusive admission); the policy question is recorded for E2.
Not in E1: the relaxation, the journal location, the transition rule (E3), binding (bootstrap) and clearing, the Gate 3 read limits, inheriting unchanged pages, pending reclamation, handle-relative I/O, any
conversion.rschange (so the rustdoc typo criterion is not triggered). Open for the maintainer (unchanged): may exclusive admission replace lease expiry as the proof a former owner is gone.PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO. Implementation proceeds on a branch from exact main; the #1014 checklist is applied before the first push.- Every ordinary operation of
Gate 4E E1 — TERMINAL (PR #1019, merged as
8424a7c6)E1 is terminal. The ordinary-operation barrier of a bound root is pinned with evidence. No behaviour changed in
src/. This does not close Gate 4E: E2 (phase-aware relaxation) and E3 (exclusiveADMIT/ final capture) are not admitted and not started; per the maintainer's instruction the source lane is paused after this merge.PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO.Merge squash 8424a7c640074dedac8de24a98fd6932c80c8230, exact reviewed head10cfd539ad7af3b44819071fe957adf752c19b5e, merged with--match-head-commitSize 6 files, +863 / −55 (test, test support in two modules, contract, evidence, CHANGELOG), 8 signed commits (over the target of 6, inside the hard tier of 10) Resulting main CI / CD 37953963432success, CodeQL37953963093success, both on exactly8424a7c6Production dpl_BXtcjwQKG8XC8uS8kZziTyNb2SZz, READY, PROMOTED, exact8424a7c6; the three canonical aliases are on it;https://worldscript-studio.vercel.app/answers 200 (the two team-scoped hosts redirect to Vercel SSO, deployment protection)Rollback dpl_9T9o3RvF7LkaFmToPnaUX5VqMMgb(previous Production, exactd1710f0a)Retention the seven #1019 previews ( feb62f91,d41d55ce,65307534,616ec8a7,324d22f8,992c2440,10cfd539) removed by exact ID after a dry classification with every gate passed (PR merged, branch gone, no Production alias, not rollback, not an open-PR head); final: 7 ×ABSENT, 36 Production deployments READY / PROMOTED, 3 aliases, 0 previews, 0 unclassifiedGit branch tree identical to merged main, remote and local branch deleted, worktree detached at 8424a7c6What is pinned (
gate4b_operations_test, 38 cases: 20 existing, 18 new). With an unlocked provider, a loadable catalog and no interrupted root commit pending, a committed root that carries a live-migration binding makesProtectedStoragerefuse every ordinary operation, in every phase. The data operations (write, reconcile, list, read) decide incatalog()from the committed root before any catalog page is read (MigrationRequired); the lifecycle transitions (lock from an unlocked key, unlock, shutdown) load the catalog first and then refuse withRecoveryPending. Also pinned: the installation tree unchanged apart from the two top-level coordination resources (which must hold no data), with a negative proof that the snapshot sees every other change including symlink replacements; a binding that names no journal; a cold start; a provider that starts locked (try_lockidempotent,try_unlockrefused, a later write fails withLocked) against an unbound control; a lost catalog page (transitionsRecoveryRequired(CatalogSetMismatch), data operations including reconcile stillMigrationRequired); an interrupted root commit with a completed and with a discarded recovery, write and reconcile each on a fresh fixture. Mutation-checked at both refusal sites and at the two snapshot rules (top-level-only exclusion, links recorded as links); every mutation restored byte-identical.Review convergence on the exact head. Codex on
10cfd539ad: "Didn't find any major issues". CodeScene approved. CodeAnt: quality gate passed. 12 review threads, 0 unresolved, all replied to with the resolving commit. 38 checks green (codecov/patch, Windows / macOS / Linux secure-store evidence, E2E, E2E Deep Coverage, CodeQL, the CHANGELOG-number guard and the governance check included). Five correction waves, all bounded: 12 Codex P2 findings (every one valid, every one about a claim in the contract, evidence or tests that was broader than the code or the tests established, or about the depth of the evidence itself), and CodeScene failed the gate twice on my own fixture changes (critical Low Cohesion ontests/support/operations.rs, then Large Method in the extracted module): fixed structurally by moving the setup intotests/support/preconfigured.rsand decomposing it, not suppressed. Dispositions of the two late CodeAnt notes are on the PR: the Medium-Risk note is the documented stricter-than-§10.3 interim (INVALID_AS_DEFECT), the symlink nitpick on the coordination data check is deferred with a criterion.One CI event, recorded as an event. On head
65307534theSecure Store Platform Evidence (ubuntu-latest)job failed once:platform_keystore_test::full_lifecycle_against_the_real_os_secure_store, cleanupr15-anchor-v1: SecureAnchorUnavailable; left behind: []. Diagnosis (inference, not proof): the test body never deletes the anchor and asserts it present immediately before cleanup, so it existed when the delete call returned the platform error; the followinggetfound it absent with the daemon reachable, so the delete took effect while the call reported failure (keyringopens a new D-Bus connection with a DH key exchange for every call and mapsPlatformFailure/NoStorageAccesstoSecureAnchorUnavailable); the failed attempt ran 7.15 s against 3.4–5.9 s on passing runs. The 29 earlier runs of the job passed; a same-SHA re-run of only the failed jobs passed (3.5 s); the job passed on the final head. Not reproduced and not fixed; no assertion or cleanup verification was changed. Criterion (separate, not admitted): harden the evidence cleanup so a reported delete error is retried a bounded number of times and the final state is still verified absent, without weakening either check.Criteria recorded by this slice. (1) The next slice that touches the operations test support records the coordination resources with
symlink_metadataand treats a link at those names as not data-free (CodeAnt nitpick). (2) The Linux evidence-cleanup hardening above. Unchanged and still open: the rustdoc typo inadvance_cursor(first commit of the next slice that touchesconversion.rs); the phase-aware relaxation (E2), including the decision to check the binding before the catalog is loaded inverify_transition; exclusiveADMIT/ final capture (E3); Gate 3 whole-file read limits; unchanged-page inheritance; pending-directory reclamation; handle-relative I/O; the refusal codes this slice states as not pinned (a tampered page, an unresolvable key, an I/O error, shutdown over a locked provider). Open for the maintainer: may exclusive admission replace lease expiry as the proof a former owner is gone. Gate 5 / 6 / 7 andv1.30.0remain gated.- added 6 commits that reference this issue
on Oct 10, 2026 WorldScript control-plane reconciliation — 2026-10-11
Protected main is
c87d1cda171ac511ec65350ec7feed1780e829af. Push-triggered CI/CD run 38071809656 and CodeQL run 38071809617 are SUCCESS on that exact SHA. Latest published release remains v1.29.1.PR #1017 is merged, not an active unmerged lane. Crash/restart resume, replay safety and explicit recovery from mixed-key state remain required. Neither #1017's merge nor the later tag-boundary documentation fix proves the historical Codex P1 resolved. No closure or new migration implementation is claimed.
Gate 4 remains open. Gate 4E E1 is terminal; E2/E3 are not admitted. Gates 5/6/7 and v1.30 are not admitted. RELEASE_ALLOWED=NO; PRODUCTION_AUTHORITY_SWITCH_ALLOWED=NO.
Canonical mirror: https://linear.app/qnbs/issue/QNB-11. This publishes the revalidated QNB-194 handover packet through the existing maintainer gh principal. Historical specifications and issue state remain preserved; no source or workflow change is implied.
LIVE C2c-2 CRASH/RESUME GOVERNANCE — 2026-10-09
Latest Codex P1 durable cursor invalidation across restart demonstrates that after
advance_cursorskips directly to the final entry,begin_conversionclears the session-onlycursor_movedinvalidation, allowing the newly resumed session to enterVERIFYwith preceding inventory entries unconverted. This is a live exact-head security/correctness blocker, not review-noise or a reason for another unrelated refactor. Fix/accept only with regression that persists the malicious manual jump through process/session restart, proves restart cannot grant completion, and ensures adversarial takeover/missing admission never invokesEntryStep::convertprematurely. A design could remove completion-capable raw cursor advancement or durably encode/prove contiguous progress; Claude/maintainer must decide without weakening preserve-first or extending the Core clock boundary. Also prove checkpoint error replay, last-entry idempotent replay, page boundary, authenticated final inventory,CONVERT→VERIFYguarded transition and Root/Journal fence after every committed step. Unit-only loop tests do not close the crash/resume acceptance. Existing tracked Gate-4E handle-relative platform I/O remains separate.Retain one coherent correction epoch; require exact-final-head Codex disposition and deterministic CI/CodeQL, then normal protected merge and resulting-main + Production/retention proof before calling C2c-2 terminal. No simultaneous new source writer, no Gate5 record conversion in this PR, no premature Gate7 switch. Earlier issue-body snapshots below are historical; authoritative current slice links: PR #1017, C2c-2 admission, QNB-11.
CURRENT CONTROL-PLANE CHECKPOINT — 2026-10-06 — RETENTION TERMINAL / R4 READ-ONLY ADMISSION NEXT
R4 is the next planned bounded question: a stale caller manifest paired with its matching stale fence must not be allowed to publish when committed LiveMigration authority names another durable generation. Before source mutation, re-audit the live contracts and prove whether the smallest safe slice is a pre-I/O authority refusal in
promote_manifest_fenced/promote_page_fenced.Explicitly out of scope for this R4 slice: root LiveMigration advance, deletion of r+1, R2B semantic-open recovery, key-epoch changes/alignment, mixed-key conversion, Gate 4E/5/6/7, and production authority switch.
HISTORICAL / SUPERSEDED CONTROL-PLANE CHECKPOINT — 2026-10-04 — SEQUENCE CORRECTION
HISTORICAL / SUPERSEDED — previous control-plane checkpoint
Successor B is correctly based on the #953 resulting main and carries the semantic A+B union: #953 SessionBinding/rebind semantics plus the mutation/root-recovery/lifecycle mechanisms extracted from frozen #952. The prepared-root Step-F same-instance recovery R1–R5 proof remains this PR's material closure scope.
Do not close Gate 4B, #922, #445, or advance #359/4D until #954 converges on its exact final head, merges normally, and the exact resulting-main CI/CD + CodeQL + Vercel Production gate is terminal.
HISTORICAL / SUPERSEDED — predecessor update — 2026-10-03
PR #953 successor A is now on final-correction head
12cf7833822c453aeba1fb2156ba06301c6172c6. This still does not admit Gate 4D. The prepared-root coordinator-recovery Critical remains successor-B scope; #359 remains gated behind complete 4B and 4C.LIVE PREDECESSOR NOTE — 2026-10-03
PR #953 is the live Gate 4B successor A (Read/Snapshot Admission Closure), head
590bdb868054648a82edad3a61680add5a876e07. This does not admit 4D. The prepared-root coordinator-recovery Critical remains successor-B scope; #359 remains gated behind complete 4B and 4C.CURRENT EXECUTION HOLD — 2026-10-03 — 4D NOT PULLED FORWARD
The newly validated prepared-root Step-F recovery gap does not pull this issue forward. That gap is ordinary-operation/root-publication coordinator recovery and belongs to Gate 4B successor B, using the existing authenticated
recover_rootprimitive. This issue remains the later crash-resumable rotation/rekey owner and must not be used to import journal/rekey state-machine scope into the current 4B split.The current resource-bounded coding-agent run stops after the first successor PR reaches terminal resulting-main state; it must not begin this issue.
CURRENT R-15 RECONCILIATION — 2026-10-02
#359 is no longer blocked on Gate 3. It remains open and sequenced to Gate 4 slice 4D, where it also closes the Gate-3-carried key-epoch crash window.
HISTORICAL / SUPERSEDED — CURRENT R-15 RECONCILIATION — 2026-10-01 — superseded 2026-10-02
The historical Tauri migration defect below remains valid provenance, but the implementation destination is now renderer-neutral Gate 4. Do not add a second Tauri-only migration authority; Gate 4 owns the shared journal/state machine and cross-process transition semantics.
Summary
services/fs/fsEncryptionMigration.ts#migrateAllProtectedFsData(added to fix the disable/rotate/set data-stranding bugs — see the CHANGELOG entries for those fixes) re-keys every fs-backed protected file directly, one file at a time, with no persistent journal, checkpoint, or resumable cursor. If the desktop process is killed (crash, forced quit, power loss) partway through a passphrase rotation, the result can be a mixed-key filesystem state: some files already re-encrypted under the new key, others still under the old one — while the durable IDB sentinel (which determines which key gets derived on next unlock) has not yet been updated, sincerotateIdbPassphrase()runs after the fs bridge completes.Why this is safer than it sounds, but still a real gap
unprotectTextValue,getApiKey, etc.) already fail closed and preserve (never discard) a file that fails to decrypt under the current key — a mixed-key state does not crash the app or silently destroy data outright. A file re-keyed by the interrupted migration will simply read as "temporarily unreadable," the same as a locked session.services/storage/encryptionMigrationJournal.ts,encryptionMigrationOrchestrator.ts) with per-store checkpoints, a resumable cursor, and arecovery-requiredterminal state surfaced byEncryptionRecoveryModal.Interim mitigation shipped alongside this issue
migrateAllProtectedFsDatanow writes a durable marker file (config/fs-migration-marker.json) before starting and deletes it only on successful completion. On next startup,FsCore.initialize()detects a stale marker and surfaces a status notification warning the user that a previous encryption migration did not finish and some files may be in a mixed key state — an honest "stuck state detected" signal rather than silence, but not automated recovery.Real fix
Give the fs bridge the same journal/checkpoint architecture the IDB path already has: a durable per-file inventory + status (pending/done), a source/target key epoch, a resumable cursor, and a commit marker — so an interrupted rotation can resume exactly where it left off (or be cleanly rolled back) instead of requiring manual recovery. Ideally this should be a shared state machine coordinating both IDB and fs stores under one journal, since a rotation logically spans both.
Found via
Surfaced during the PR #356 (
fix/desktop-project-data-encryption) review-correction loop, external assessment of the migration bridge's crash-safety properties (2026-08-13).