fix: harden rotation read-plane durability and deep crash-resume soundness#598
Conversation
Entire-Checkpoint: 570d1b905004
Entire-Checkpoint: 020b4d928299
Grounds the locked D-01..D-10 approach against live code: confirms CONTEXT.md's line citations, and surfaces two undisclosed landmines — the desktop FUSE/WinFsp Rust rotation engine shares the same depth-1 consumption bug and is further behind (non-recursive verify_subtree_clean, missing-root-treated-as-clean) but is outside CONTEXT's stated file scope, and no durable-store seam is currently wired into either rotation engine at all, so the new ECIES key-checkpoint requires inventing an end-to-end callback seam from scratch. Entire-Checkpoint: 29d80117a191
Entire-Checkpoint: 9bb05f9016f1
…1..D-13) Entire-Checkpoint: 2adf9387dd48
Entire-Checkpoint: df4c5b7e6b17
Entire-Checkpoint: 7817addd10e0
…-14..D-17) Entire-Checkpoint: 52bceeb7dd0f
Entire-Checkpoint: f90ef04757e4
…4..D-17) Entire-Checkpoint: b2efec6c397a
Entire-Checkpoint: 73a507dab3be
…nning complete Entire-Checkpoint: 447d12ea33f6
Entire-Checkpoint: a757d4446581
- Add depth-3 recursive dirty-edge test expecting parent_ipns_name on the frontier entry (fails to compile against the current naive DirtyFrontierEntry / Vec-returning verify_subtree_clean) - Add missing-root-is-dirty test expecting a VerifySubtreeOutcome shape instead of an empty converged Vec
…tests - Add RED tests for D-08 fail-closed put propagation surfaced through enforce_resolved, D-06 single combined-record atomicity, Pitfall-4 old-two-file fold-forward migration, and D-07 wrapped-key checkpoint round-trip on the combined record - All new cases reference production items that do not exist yet (RotationError::PersistFailed, CombinedFloorRecord, persist_wrapped_key/get_wrapped_key/delete_wrapped_key) and fail to compile -- confirmed RED
- Widen DirtyFrontierEntry to carry parent_ipns_name, node_read_key,
child_pub_kind, and enqueued_generation (Rust twin of TS
DirtyFrontierItem) -- D-12 prerequisite for a depth-aware consumption
fix
- Make verify_subtree_clean recurse the full subtree via a new
VerifySubtreeOutcome { is_dirty, frontier } return shape and an
explicit work-stack walk (collect_dirty_frontier): descend below
clean folder edges only, stop below a dirty edge
- Surface a missing root as is_dirty: true with an empty frontier,
distinct from a fully converged (also-empty) frontier -- the
resume-skip call site in rotate_read_from_node now branches on
is_dirty instead of frontier.is_empty()
- Extract derive_child_read_key, a shared read-only child-key
derivation helper, out of enqueue_child's unseal_child_read_key
logic and reuse it from collect_dirty_frontier
… plan Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- rotation-high-water.test.ts: collapse the fake HighWaterStore fixture from two independent generation/seq Maps to ONE combined per-nodeId record store, matching the SC#4/D-06 combined-record target - add a new describe block asserting enforceResolved writes generation AND seq into a single combined record via exactly one store.put call, and that a rejected regression issues zero store.put calls - existing scalar-floor/regression-gate cases updated to the new single-store constructor signature -- currently RED against the production code (still two-store), pending Task 2/3
Adds two RED unit tests for the SC#1/SC#2 rotation engine bugs: a depth-2 dirty-resume Bug A case (intermediate parent decrement mis-attributed to root, item dropped) and a depth-3 Bug B ordering case (a deep dirty child dequeued before its real parent's parentTracking entry exists, dropping the decrement on both the missing-guard and skipped-idempotency paths). Both assert concrete per-parent publishWithCas call counts, not subjective pass/fail.
- Add CombinedFloorRecord { generation, seq, wrapped_key_checkpoint } and
collapse the two independent generation/seq sidecar files into ONE
combined JSON sidecar (rotation-high-water.json); for_generation/for_seq
now both point at that same file and differ only in which field they
read/write (D-06)
- Fold the OLD two-file shape forward on first read when the combined
sidecar is absent, so an upgrading device never resets either floor
(Pitfall 4)
- Add RotationError::PersistFailed and change HighWaterStore::put to
return Result<(), RotationError>; a write failure (including refusing to
write over a corrupt sidecar) now fails closed instead of being
log-and-swallowed (D-08)
- Propagate the new fallible put through bump_floor/bump_generation/
bump_seq/seed_from_grant/enforce_resolved
- Add JsonSidecarFloorStore::persist_wrapped_key/get_wrapped_key/
delete_wrapped_key on the same combined record, fail-closed on persist
(D-07 Rust half) -- the concrete target the FUSE RotationDeps adapter
(Plan 09) will delegate to
- All Task-1 RED tests now pass GREEN; existing floor_store/high_water
suites updated for the new Result-returning put signature
…ature - Update the three in-memory MemStore test fixtures (listing.rs, emit.rs, crates/fuse/src/fs.rs) to satisfy the new HighWaterStore::put -> Result<(), RotationError> signature - Fix 3 direct bump_generation call sites in listing.rs test fixtures that now return an unused Result - Re-export CombinedFloorRecord from crates/sdk/src/lib.rs alongside JsonSidecarFloorStore - Update adapter.rs doc comments to describe the combined single-sidecar file (rotation-high-water.json) instead of the retired two-file shape - content_ops.rs/inode.rs/events.rs/replay.rs needed no changes -- they only reference RotationHighWater<JsonSidecarFloorStore> as a type, never call .put()/bump_floor() directly, confirmed via clean `cargo check -p cipherbox-fuse --features fuse --tests` - No swallowed puts introduced (grep for `let _ = .*\.put(` is empty); cargo test -p cipherbox-sdk (139) and cipherbox-fuse --features fuse (96 + 1) all green
…DB store
- rotation-state.service.ts: bump DB_VERSION 1 to 2, replace the two
generation-high-water/seq-high-water object stores with a single
rotation-floor store keyed by nodeId holding
{ generation?, seq?, wrappedKeyCheckpoint? } (SC#4/D-06)
- onupgradeneeded folds any existing legacy two-store data into the
combined store via cursor before retiring the old stores -- floors
are never silently dropped (Pitfall 4)
- idbPutFloors/idbPutWrappedKey each read-back inside ONE readwrite
transaction and write independently max-preserved generation/seq
fields while merging/overwriting the wrappedKeyCheckpoint ciphertext
blob (never max'd)
- add persistWrappedKey/getWrappedKey/deleteWrappedKey accessors on
the same combined store for the SC#3 ECIES wrapped-key checkpoint
(D-07) -- client-side IndexedDB only, no API/network call
- add fake-indexeddb as a devDependency and a new colocated
rotation-state.service.test.ts proving the wrapped-key round trip
and old-schema migration-preservation against a real (faked)
IndexedDB -- kept out of packages/sdk's test file since this
browser-only IndexedDB adapter must not be exercised from a
published, browser-agnostic SDK package's own test suite
…rdering fix Fixes the two depth-1-only CONSUMPTION bugs in the rotation engine (dirty-frontier COLLECTION was already depth-aware from Phase 70): - Bug A (SC#1): the dirty-resume branch used a hard-coded rootNode.children.find lookup and attributed every decrement to root, mis-attributing depth>=2 items and dropping them. Replaced with a group-by-parentIpnsName pass that seeds a ParentTrackingState for every DISTINCT parent (root or otherwise) via a new resolveParentTrackingState helper, then enqueues every frontier item through the existing enqueueDirtyFrontierItem helper (which already threads item.parentIpnsName correctly). - Bug B (SC#2): the normal branch enqueues root's children first then appends all preRotationDirtyFrontier items regardless of depth, so a deep dirty node could dequeue before its own parent's parentTracking entry exists. Added a bounded requeue guard (defer an item whose parent isn't tracked yet but is still pending in the queue), a lazy resolveParentTrackingState fallback at the re-seal gate, and a decrement on the skipped-idempotency path so a parent's batched republish is never silently dropped. New shared helpers: findParentNodeByIpnsName (module-level tree walk reusing the existing resolveChildKeyAndEnvelope key-chain primitive) and resolveParentTrackingState (resolve-or-seed an arbitrary parent's ParentTrackingState), both following the file's terminal-owner zeroization discipline.
- rotation-high-water.ts: createRotationHighWater now takes a SINGLE
combined HighWaterStore seam instead of separate generationStore/
seqStore -- CombinedFloorRecord holds
{ generation?, seq?, wrappedKeyCheckpoint? } for a nodeId
- enforceResolved reads the combined record once and, on a
non-regressing resolve, writes generation AND seq back via ONE
store.put call -- closes the cross-store sequencing window the
module doc previously flagged as a DOC-ONLY residual (SC#4/D-06)
- bumpGeneration/bumpSeq/seedFromGrant reuse the same single-record
read-modify-write helper, always preserving the other field and the
wrappedKeyCheckpoint untouched
- public RotationHighWater interface (getGenerationFloor/bumpGeneration/
seedFromGrant/getSeqFloor/bumpSeq/enforceResolved) is unchanged, so
client.ts and the existing client-rotation/folder-listing-gate/
folder-reresolve test mocks keep compiling against the same shape
- export CombinedFloorRecord from the package barrel for apps/web's
concrete IndexedDB adapter
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
axiosInstance.delete rejects on non-2xx before the status check runs, so a failed cleanup revoke aborted Part B; wrapped in try/catch to keep the intended non-fatal behavior. CodeRabbit finding 4. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: b50674a909a2
SC#6 row reflects the completed green live-stack run; PATTERNS intro no longer over-claims all files self-analogous. CodeRabbit findings 1 and 2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 09d1efafa6ed
|
Warning Review limit reached
Next review available in: 52 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughThis PR advances Phase 70.1 by updating planning/state docs, implementing checkpoint-backed rotation durability and fail-closed gate handling across TS/Rust/FUSE, and adding combined-floor storage plus fresh-generation reconcile gating. It also adds desktop/e2e coverage for shared-scope-exit rotation and crash-resume scenarios. ChangesPhase 70.1 planning and verification
Rotation implementation
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Release Preview
Cascade Details
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #598 +/- ##
==========================================
+ Coverage 65.99% 75.53% +9.54%
==========================================
Files 154 195 +41
Lines 14748 31803 +17055
Branches 1731 1839 +108
==========================================
+ Hits 9733 24023 +14290
- Misses 4770 7530 +2760
- Partials 245 250 +5
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Greptile SummaryThis phase hardens the read-key rotation engines (TypeScript
Confidence Score: 5/5Safe to merge. The correctness fixes are well-scoped and covered by the new CI suite. No data-loss or key-material exposure issues were found in the changed paths. The ECIES checkpoint seam and combined floor store are backed by new integration tests using fake-indexeddb and real sealNode/unsealNode calls. The Rust changes mirror TS semantics closely and are validated by the acceptance suite. The two flagged items are non-blocking: the reconcileFolderSequence AEAD failure is intentional fail-closed behavior missing error-message polish, and the migration || edge case is theoretically unreachable with the old store schema. packages/sdk/src/client.ts for the new AEAD failure mode in reconcileFolderSequence; packages/sdk/src/state/rotation-idb-store.ts for the migration early-exit condition. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Client
participant Engine as RotateReadFromNode
participant Store as ECIES Checkpoint Store
participant IPNS
Note over Engine: Pre-rotation verify_subtree_clean
Engine->>IPNS: resolve(root_ipns)
IPNS-->>Engine: cid + seq
Engine->>Engine: collectDirtyFrontier (generation compare BEFORE unseal)
Engine->>IPNS: rotate_one root mint readKeyPrime
Engine->>Store: persist_wrapped_key nodeId ECIES readKeyPrime
Store-->>Engine: OK D-03 checkpoint before publish
Engine->>IPNS: publish root new_read_key
loop BFS children
Engine->>Store: persist_wrapped_key childId
Engine->>IPNS: publish child
Engine->>Store: delete_wrapped_key childId after parent mirror commits D-04
end
alt Crash-resume dirty node
Engine->>Store: get_wrapped_key nodeId
Store-->>Engine: ECIES ciphertext
Engine->>Engine: unwrap repairDirtyNode
end
Note over Client: reconcileFolderSequence SC5
Client->>IPNS: resolveIpnsRecord folderIpns
Client->>IPNS: fetchFromIpfs cid
Client->>Client: unsealNode pub folderKey real generation
Client->>Store: enforceResolved seq generation versionFloor
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Client
participant Engine as RotateReadFromNode
participant Store as ECIES Checkpoint Store
participant IPNS
Note over Engine: Pre-rotation verify_subtree_clean
Engine->>IPNS: resolve(root_ipns)
IPNS-->>Engine: cid + seq
Engine->>Engine: collectDirtyFrontier (generation compare BEFORE unseal)
Engine->>IPNS: rotate_one root mint readKeyPrime
Engine->>Store: persist_wrapped_key nodeId ECIES readKeyPrime
Store-->>Engine: OK D-03 checkpoint before publish
Engine->>IPNS: publish root new_read_key
loop BFS children
Engine->>Store: persist_wrapped_key childId
Engine->>IPNS: publish child
Engine->>Store: delete_wrapped_key childId after parent mirror commits D-04
end
alt Crash-resume dirty node
Engine->>Store: get_wrapped_key nodeId
Store-->>Engine: ECIES ciphertext
Engine->>Engine: unwrap repairDirtyNode
end
Note over Client: reconcileFolderSequence SC5
Client->>IPNS: resolveIpnsRecord folderIpns
Client->>IPNS: fetchFromIpfs cid
Client->>Client: unsealNode pub folderKey real generation
Client->>Store: enforceResolved seq generation versionFloor
Reviews (4): Last reviewed commit: "docs: capture todo - deep scope-exit rot..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.planning/debug/scope-exit-part-a-fail.md:
- Around line 460-504: The current “fixed” description overstates the scope
because the deep scope-exit path still leaves intermediate parents resealing
with stale in-memory keys. Update the status in this note to clearly mark the
change as shallow D-16/grant-root-only, or extend the fix in the scope-exit flow
so the deep path also refreshes all rotated parents; reference the
`rotate_read_on_scope_exit`, `refresh_grant_root_read_key`, and
`rotate_read_from_node_with_root_children` paths when adjusting the wording or
implementation.
In
@.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-11-SUMMARY.md:
- Around line 76-81: The summary overstates test closure by implying the full
crate suite is green, while the deferred-failures section still notes
private-delete tests fail outside the scoped grant_scope run. Update the wording
in the summary block and the related deferred-failures text to clearly limit the
claim to the scoped verification, using the existing grant_scope.rs /
Grant-Scope Gate Fail-Closed Correctness Summary language so readers do not
infer full-crate success.
In `@crates/sdk/src/floor_store.rs`:
- Around line 697-708: The permission-failure test in the floor store logic can
still succeed under root, so the `expect_err` assertion may panic in root-based
CI. Update the test around `store.put` to either skip the chmod-based failure
path when running as root or use an alternative write-failure setup that
reliably fails regardless of `euid`; keep the change localized to the
`#[cfg(unix)]` permission block and the subsequent failure assertion in
`floor_store.rs`.
In `@packages/sdk-core/src/rotation/engine.ts`:
- Around line 1789-1796: Checkpoint GC is happening too early in the repair
flow, before the batched parent republish has fully committed. Update the
`decrementPendingAndMaybeRepublish` / `updateFolderMetadataAndPublish` path so
`deleteWrappedKey` is deferred until the final republish completes, or store
pending child IDs on `ParentTrackingState` and flush them there. Keep the
`deleteWrappedKey(item.childPubId)` cleanup out of the immediate repair branch
so the checkpoint survives until the parent mirror update is safely committed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 58a22cc8-8452-4cf3-924c-f7201fc1bb04
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!**/pnpm-lock.yaml
📒 Files selected for processing (82)
.planning/ROADMAP.md.planning/STATE.md.planning/debug/macos-first-publish-timeout.md.planning/debug/rotation-crash-safety-depth3.md.planning/debug/scope-exit-part-a-fail.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-01-PLAN.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-01-SUMMARY.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-02-PLAN.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-02-SUMMARY.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-03-PLAN.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-03-SUMMARY.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-04-PLAN.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-04-SUMMARY.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-05-PLAN.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-05-SUMMARY.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-06-PLAN.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-06-SUMMARY.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-07-PLAN.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-07-SUMMARY.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-08-PLAN.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-08-SUMMARY.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-09-PLAN.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-09-SUMMARY.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-10-PLAN.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-10-SUMMARY.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-11-PLAN.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-11-SUMMARY.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-12-PLAN.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-12-SUMMARY.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-13-PLAN.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-13-SUMMARY.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-CONTEXT.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-DISCUSSION-LOG.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-PATTERNS.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-RESEARCH.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-SECURITY.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-VALIDATION.md.planning/phases/70.1-rotation-read-plane-durability-and-deep-crash-resume-soundne/70.1-VERIFICATION.md.planning/todos/pending/2026-07-07-fuse-shared-scope-exit-rotation-live-wiring.md.planning/todos/pending/2026-07-08-desktop-query-grants-rooted-at-remint-noop.md.planning/todos/pending/2026-07-08-winfsp-d15d-gate-ordering-parity.md.planning/todos/pending/2026-07-09-macos-fuse-t-cross-client-sync-invalidation.mdapps/desktop/src-tauri/src/fuse/mod.rsapps/desktop/src-tauri/src/fuse/windows/mod.rsapps/web/src/services/rotation-state.service.tscrates/fuse/src/fs.rscrates/fuse/src/platform/windows/write_ops.rscrates/fuse/src/test_support.rscrates/fuse/src/write_ops/grant_scope.rscrates/fuse/src/write_ops/implementation/delete.rscrates/fuse/src/write_ops/implementation/rename.rscrates/fuse/src/write_ops/mod.rscrates/fuse/src/write_ops/rotation_deps.rscrates/sdk/src/adapter.rscrates/sdk/src/emit.rscrates/sdk/src/floor_store.rscrates/sdk/src/lib.rscrates/sdk/src/listing.rscrates/sdk/src/queue.rscrates/sdk/src/rotation/engine.rscrates/sdk/src/rotation/high_water.rscrates/sdk/src/rotation/mod.rspackages/sdk-core/package.jsonpackages/sdk-core/src/__tests__/rotation/engine.test.tspackages/sdk-core/src/index.tspackages/sdk-core/src/rotation/engine.tspackages/sdk-core/src/rotation/index.tspackages/sdk/package.jsonpackages/sdk/src/__tests__/client-rotation.test.tspackages/sdk/src/__tests__/reconcile-generation-gate.test.tspackages/sdk/src/__tests__/rotation-high-water.test.tspackages/sdk/src/__tests__/rotation-idb-store.test.tspackages/sdk/src/client.tspackages/sdk/src/index.tspackages/sdk/src/state/rotation-high-water.tspackages/sdk/src/state/rotation-idb-store.tspackages/sdk/src/types.tspackages/sdk/tsup.config.tstests/desktop-e2e/scripts/run-all.ps1tests/desktop-e2e/scripts/run-all.shtests/desktop-e2e/scripts/shared-scope-exit-rotation.mtstests/sdk-e2e/src/suites/rotation-crash-safety.test.ts
…ommits The TS engine GC'd a repaired dirty child's ECIES key-checkpoint (deleteWrappedKey) immediately, even when its parent had pendingChildCount>1 and had NOT yet republished. A crash in that window left the child's edge dirty on resume with no checkpoint to recover from -> DirtyNodeUnrecoverableError (permanent loss of read access to the subtree). Now queues repaired child ids on ParentTrackingState.pendingCheckpointNodeIds and flushes deleteWrappedKey only in decrementPendingAndMaybeRepublish's count===0 branch, AFTER updateFolderMetadataAndPublish durably commits — porting the Rust twin's already-correct pending_checkpoint_node_ids / complete_pending_child ordering (restores D-01/D-04 parity). CodeRabbit PR review (Major). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: d2524b6f16e5
The grant_scope test module used crate::test_support (fuse-only harness) but was gated on #[cfg(test)] alone, so the Windows 'cargo test --features winfsp' build compiled it and failed with E0433 (test_support configured out). Gated on #[cfg(all(test, feature = "fuse"))] like every sibling module. Surfaced by the first full PR CI run on the branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 1d805ec219c5
…er key (1) Add the // sc6-allow marker on ApiClientTransport::resolve's resolve_ipns_verified call — the rotation transport's OWN verified fail-closed chokepoint (D-08), not a read-plane bypass, satisfying the SC#6 single-gated-read CI grep gate. (2) Hold FuseRotationDeps.owner_private_key in Zeroizing so the ECIES private key is wiped on drop (Greptile PR review). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 5bc796044848
Add a scope note under the Resolution headline marking status: fixed as shallow D-16 / grant-root-only; deep-path intermediate-node key refresh stays a documented out-of-scope follow-up. CodeRabbit PR review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 629901f6d06f
put_fails_closed_on_write_failure_and_enforce_resolved_surfaces_it induces the write failure by chmod-ing the dir read-only (0o500), which Windows' permission model does not honor — the create still succeeds, so the unconditional expect_err panicked on the Windows 'cargo test --features winfsp' job. Gated the whole test on #[cfg(unix)] (matching the other permission tests); the fail-closed D-08 behavior is platform-agnostic and stays covered on Linux/macOS. Surfaced by the first full PR CI run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: f3fece7e4fc5
Ship-cycle learnings: TS/Rust twin-parity is not automatic (CodeRabbit caught the checkpoint-GC divergence); open the PR earlier so branch-wide CI gates run sooner; verify whether CI fmt-checks before running cargo fmt; adversarial-reviewer fix-or-refute pattern. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 2902e4f9064f
Move the two resolves_phase: 70.1 todos to completed/: the depth>=2 crash-resume soundness gap (delivered by SC#1/2/3/6/7, depth-3 crash-safety e2e green) and the FUSE shared-scope-exit rotation live-wiring (delivered by SC#8, D-16 acceptance green on all 3 platforms, incl. the D-15a/b/c/d gate-correctness findings). The ROT-04 remint no-op, WinFsp D-15d rename parity, and macOS FUSE-T Test 5/7 invalidation remain pending as sanctioned deferrals. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: da8cc1282572
…efresh Deep scope-exit rotation refreshes only the grant-root inode's in-memory read key; intermediate parent inodes keep stale pre-rotation keys (latent deep-path revocation-bypass). Documented in the Phase 70.1 D-16 debug note but was untracked as a forward item. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: e7c0a0d3701c
…store Phase 70.1 (D-06, #598) collapsed the generation-high-water and seq-high-water IndexedDB stores into a single rotation-floor store keyed by nodeId holding a { generation?, seq?, wrappedKeyCheckpoint? } record. The rotation-durability e2e helpers still probed the retired store names, so readDurableFloors short-circuited and returned generation: undefined, failing the SC#4 setup assertion at line 216 on every main run since #598. Point readDurableFloors and writeDurableSeqFloor at the rotation-floor store; the write does a read-modify-write so it preserves the node's existing generation/wrappedKeyCheckpoint floors. Test-only -- the product write path (rotation-idb-store.ts) is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 545aa91edc15
…store (#600) * test(web-e2e): align rotation-durability floor helpers with combined store Phase 70.1 (D-06, #598) collapsed the generation-high-water and seq-high-water IndexedDB stores into a single rotation-floor store keyed by nodeId holding a { generation?, seq?, wrappedKeyCheckpoint? } record. The rotation-durability e2e helpers still probed the retired store names, so readDurableFloors short-circuited and returned generation: undefined, failing the SC#4 setup assertion at line 216 on every main run since #598. Point readDurableFloors and writeDurableSeqFloor at the rotation-floor store; the write does a read-modify-write so it preserves the node's existing generation/wrappedKeyCheckpoint floors. Test-only -- the product write path (rotation-idb-store.ts) is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 545aa91edc15 * docs(web-e2e): clarify writeDurableSeqFloor seq is a direct overwrite The doc comment claimed the read-modify-write "matches production's max-preserving write", which is misleading: production idbPutFloors max-preserves seq, whereas this staging helper intentionally overwrites seq so a test can pin an exact sequence floor. Only the preservation of the other fields (generation / wrappedKeyCheckpoint) matches production. Comment-only; no behavior change. Addresses CodeRabbit review on #600. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: f73432632360 * test(sdk-e2e): accept idempotent two-200 outcome in D-06 concurrent publish Test 21 (D-06) fires two concurrent first-publishes of the same brand-new ipnsName with the SAME CID at sequence=1, and asserted exactly one 200 plus one 409. But same-CID / same-sequence is idempotent by design (#599): when the loser serializes after the winner commits, it re-reads the identical CID and returns an idempotent 200 -- so both calls can legitimately succeed, flaking the strict one-409 assertion (observed red on #600's SDK E2E leg). Accept both correct shapes: at least one winner (never zero) and any loser is a clean 409 (never a 5xx). The follow-up resolve still asserts exactly one row at sequence=1. Test-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: d752b84767ae * test: stabilize web-e2e batch-op and shared-mkdir timeouts Batch delete/move (4.10/4.11), version restore poll (6.6.2), the serial root-file delete chain (8.2), and recipient-side shared mkdirs each drive IPNS publish round-trips whose serial-calibrated waits were too tight, flaking locally and under parallel-worker CI load. Raise the explicit per-call timeouts (CI already floors waiters to 60s via ciFloor) and make 6.6.2 poll the editor for the restored content instead of reading once. Verified: full-workflow.spec.ts 53/53 green locally under the CI 60s-floor timeout regime. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Entire-Checkpoint: 1fe70eb98011 --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Phase 70.1 — Rotation read-plane durability & deep crash-resume soundness
Hardens the read-key rotation engines (TS
sdk-core+ Rustsdktwin) and the FUSE/WinFsp shared-scope-exit rotation live-wiring so that read-plane rotation is durable across crashes and sound under deep (depth ≥ 3) fan-out, with an anti-rollback floor plane that fails closed.13 plans across 7 waves. Full per-plan detail in the phase
SUMMARY.mdfiles.What changed
DirtyNodeUnrecoverableError/RotationError::DirtyNodeUnrecoverablesurface a clear, actionable error instead of an opaque AEAD failure.reconcileFolderSequencegates on the freshly-resolved generation (realunsealNode→enforceResolved), not the cachednodeGeneration.verify_subtree_clean, missing-root-as-dirty, depth-3 crash-resume viaFakeDeps.RotationDepsadapter; D-15a/b/c/d fail-closed gates; the scope-exit read-key revocation-bypass fix (refresh_grant_root_read_keycopies the rotated key into the in-memory grant-root inode so the old key is dead); coalesced single publish; WinFsp delete-path parity.Acceptance — D-16 (SC#8)
Real-mount
shared-scope-exit-rotation.mtsacceptance leg is GREEN on macOS + Linux + Windows (CI run 28984795137): coalesced single rotation publish, pre-rotation key dead, recipient (Bob) cut off, private-delete = zero rotation.Ship-gate results
SECURITY.mdunchanged.rotation-crash-safety6/6 green — including the depth-3 fan-out≥2 mid-walk-crash anti-vacuous scenario and the multi-dirty-edge single-batched-repair scenario. Full suite 102/104; the 2 failures are an unrelated localtee-republishprecondition (tee_key_statenot seeded — the TEE worker init recipe, not run here; this phase touches zero TEE code).CodeRabbit disposition (7 findings)
Every finding was verified against the actual code before acting.
is_dirtysignal fromverify_subtree_clean, so a missing/unresolvable root (empty frontier withis_dirty) could be mis-marked converged. Now consultsis_dirtyand falls through to the fail-closed resolve. Verified as real-but-dormant (unreachable today: FUSEpersist_jobis a no-op so no resume repopulatescompleted_node_ids), fixed anyway since crash-resume soundness is this phase's point. Applied the same parity fix to the TS reference engine (identicalfrontier.lengthre-derivation).build_scope_exit_child_overridefailed open per-child (dropped a transiently-missing sibling or abuild_child_refserror), and the override replaces the grant-root's published child list — silently orphaning the dropped child with no EIO. Now returnsResult, propagates those two cases as errors, and the caller maps to fail-closed EIO — mirroring the fail-closed siblingbuild_folder_metadataexactly (still skips the same legitimately-unlinkable children it skips).persist_wrapped_keynow holds decoded plaintext key bytes inZeroizing, matching its siblings.Deferrals (tracked as todos, not phase-blocking)
query_grants_rooted_atre-mint no-op —2026-07-08-desktop-query-grants-rooted-at-remint-noop.md(sanctioned in plan 70.1-09).handle_renameungated-dest D-15d parity —2026-07-08-winfsp-d15d-gate-ordering-parity.md.2026-07-09-macos-fuse-t-cross-client-sync-invalidation.md.Note
The phase 70.1-09 accept rationale said "zero new dependencies"; the diff adds two test-only devDependencies (
@noble/secp256k1promoted,fake-indexeddbnew). Zero new runtime/production deps and zero new Rust crates.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes