Skip to content

feat: write file content end to end - #907

Merged
FSM1 merged 7 commits into
mainfrom
feat/868-write-file-content
Jul 31, 2026
Merged

feat: write file content end to end#907
FSM1 merged 7 commits into
mainfrom
feat/868-write-file-content

Conversation

@FSM1

@FSM1 FSM1 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Content bytes flow end to end: sliced by the client, sealed and staged per block by the engine, uploaded and published by the drain, and downloaded and verified by a second device. Gives ApiClient::upload its first production caller and the contract suite its first live write coverage.

What landed

Facade write handles. Command::Create { content } and Command::UpdateContent are gone; PlaintextContent is deleted. Content crosses as beginWrite(target, size)pushChunk(handle, bytes)*commitWrite(handle), with abortWrite for the abandon path. ContentWriter copies only up to the chunk boundary and hands the rest back, so peak plaintext held is one chunk however much a caller pushes; the op is journaled once at commit.

Budget admission. beginWrite reserves the exact sealed total — size + 40n + root_len, computed by assembling a root over fixed-width placeholder links — in an in-memory ledger of live handles, held from beginWrite to release. Two handles opened before either stages a byte therefore contend for the budget instead of both being admitted against room only one can have. pushChunk enforces the declared shape and never re-checks the budget. The refusal quotes budget - (staged + reserved) and names which of four actions applies: past the platform cap, cut below it by measured headroom, a transient backlog the drain frees, or an unmeasurable host.

Per-block staging. A version stages as N+1 blocks, one staging key per block. stage_op loses its budget and upload parameters and instead re-establishes the binding the drain depends on: the op's root CID must name bytes the store holds and that address to it. Orphan GC now expands a referenced root into the leaf keys its own manifest lists, and fails closed on a root it cannot produce or decode — collecting a live upload's leaves is unrecoverable.

The per-version content key. A new content-key structure tag (0x0b) and a crates/core module sealing the key HPKE-to-self in auth mode under the owner enc-subkey. The epoch rides the AAD as a value, never a key input, so a rotation between commit and drain leaves the blob openable — content bytes are never re-encrypted by any rotation path. The contentCid rides the sealed payload and is re-checked on open, so a blob cannot be moved onto another version's blocks. A key-open failure dead-letters the op as ContentUnrecoverable and, uniquely among abandonments, releases its blocks: bytes no key opens are not recoverable work.

Drain upload. File order, root last, each leaf removed on its confirmed UploadResult, so the still-staged set is always a suffix. A block absent after a present one is loss, not progress, and fails closed. The root stays staged until the record publishes — it is the manifest every retry re-derives the plan from — then the whole set is released. Upload then publish; no CID pre-registration, since NameRegistration has no CID-only form.

The remaining #823 guards, both structural. new_child takes a NewNodeBody carrying exactly the body content its kind can hold, so a folder with a file version is unrepresentable; OpKind::Create mirrors it with NewNode so the same shape cannot be journaled either. SealedContent carries content_cid, size, and leaf_cids from one decoded manifest and yields the Version, so open_content's manifest-vs-version size reject cannot fire on bytes this build authored. Both are release-active by construction and covered by tests that fire in a release build.

content_cids threaded into held records. One list feeds both RecordPublishRequest and HeldRecord, so a sub-EOL renewal re-registers exactly the content its record points at.

The observed size. The pushChunk total is cross-checked against the beginWrite declaration at commit and against the staged root's own manifest at drain. A backing file truncated mid-read fails the commit rather than publishing a short version as a success.

Coverage

  • crates/engine/tests/write_plane.rs: a file create round-trips its bytes to a second device that only ever saw the network; an updateContent version takes the head of the list and round-trips too; the whole block set is registered; a truncated file fails the commit and publishes nothing; a hole in the staged suffix fails closed; an unopenable key blob dead-letters and releases its blocks.
  • crates/contract/tests/contract.rs: a live upload, register and retire leg over a version's whole block set, asserting per-block ingress, batch registration, quota accounting over the sum, and idempotent retirement.
  • crates/engine/src/content/budget.rs: the reservation equals what staging actually holds for every framing shape, and concurrent handles cannot over-admit.

Review gates

/simplify, /security-review and /crypto-privacy-review all ran on this diff. What they found and what changed:

  • A dead-lettered content op destroyed its key but kept its blocks. The only copy of a version's content key rides the op record, which every abandonment deletes — so "staged bytes are preserved" left unopenable ciphertext holding the staging budget forever. Every abandonment now releases the version's blocks, and the special case that used to do it for one reason is gone.
  • beginWrite allocated a full chunk per handle and sized its reservation by the sealed total, a 10,000× mis-accounting for a small declaration; and sealed_total_bytes materialised the whole leaf set before it could refuse, so an absurd declared size aborted the wasm instance instead of returning RootTooLarge. The buffer is now sized to the declaration, the root length is computed arithmetically (dag::root_block_len, held to the real encoder by a test), and open handles are capped.
  • An absent leaf before the first present one was assumed to be uploaded. A partially-evicted staging store would have published a version whose manifest names blocks nothing holds — silently. Progress is now recorded durably (UPLOAD_MARK_KEY), and an absence past the mark is loss.
  • Orphan GC would have collected an open handle's leaves, which are staged before any op is journaled. orphan_staging_keys now takes the live-handle set.
  • The content-key AAD bound an epoch and nothing else, and the drain read the vault root rather than the target's scope. It now binds {scope, epoch}, with negative tests for both transplant axes.
  • A forward-version key blob was destroyed rather than retained, contradicting the op record's own doctrine. The version gate now fires before the frame-field scan and the drain retains what it cannot interpret.
  • The new sealed structure shipped with no KAT vectors while claiming to be frozen by the manifest. It now has a contentKey section with 2 accept and 13 reject vectors, plus a cross-structure separation test proving an op-record and a content-key blob never open as each other.
  • Cross-tab writes were unordered and unowned. Pipelined pushChunks could apply out of order — scrambling a file while every integrity check still passed — and any tab could push into or abort another's handle, or strand its reservation by closing. The relay now serializes per handle, binds handles to their client, and aborts a departing client's writes.

Also folded in: the flat-DAG ceiling is now its own error rather than a budget verdict quoting a meaningless figure; the admission refusal lost a dead arm; commit_write reuses record_seal so the two seals to one recipient key cannot share an ephemeral; and speculative public exports were pulled back.

Verification

cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace, cargo check -p cipherbox-wasm --target wasm32-unknown-unknown --all-targets (which caught a match the workspace build cannot see), both KAT generators diff-clean, pnpm --filter @cipherbox/client test, root pnpm typecheck, pnpm exec eslint .. The contract suite was run live against a real API, Postgres, and Kubo: 15 of 15 pass, before and after the review fixes.

Surfaced, not fixed

#906 — the hosted ingress pins blocks under Kubo's own UnixFS address rather than the caller-computed content address, so a published record's headCid and its registered contentCids name bytes the accelerator does not hold. It predates this slice (the metadata publish path has the same dependency) but was unreachable until ApiClient::upload gained a production caller. Confirmed empirically against the local stack. Fixing it needs an API surface change, so it is filed with dependency edges rather than folded in here.

Deliberately left for their own slices, each noted rather than half-done:

  • Idle-sweeping a write handle the host never closes. The relay now aborts a departing tab's handles and the open-handle count is capped, so the denial vector is closed; a time-based sweep wants the scheduler seam and belongs with engine: cancel an upload and collect orphaned staged blocks #869 (cancel upload / orphan GC), which also owns wiring orphan_staging_keys to a caller.
  • Pipelining block uploads. The drain uploads and deletes one block at a time, which is 3 sequential round-trips per leaf. A bounded in-order window preserves the suffix invariant, but it changes the failure shape and is not this slice's business.
  • Moving the write-session state machine into the content plane. LiveWrite/LiveWrites sit on Engine today; the reviewer is right that they want to be a content::write type the facade merely drives. That is a refactor of working, tested code and is better done on its own.
  • An engine-level test for a rotation landing between commit and drain. The property is implemented (the blob seals under the epoch-independent enc subkey and the drain opens on the epoch carried by the op) and the binding is pinned by core's transplant tests, but asserting it end to end needs a rotation fixture that republishes the scope root, which the rotation slices own.

Closes #868
Closes #812
Closes #797
Part of #655

Unblocks #869, #871, #873, #896, #860 and #846.

Summary by CodeRabbit

  • New Features
    • Added streaming file writes with begin, chunk, commit, and abort operations.
    • Added support for creating and updating multi-block file versions.
    • Added authenticated, per-version content-key protection.
    • Added storage admission checks with clearer quota, capacity, size, and concurrency errors.
    • Added recovery and synchronization support for staged content across devices.
  • Breaking Changes
    • File content must now use the streaming write workflow instead of create commands.
    • Replaced the previous update-content command with streamed writes.
  • Bug Fixes
    • Preserved chunk ordering and safely cleaned up abandoned or failed writes.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@FSM1, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e083199b-b7a0-4a33-b7da-93623390f170

📥 Commits

Reviewing files that changed from the base of the PR and between 67de2b0 and d8a5d6c.

📒 Files selected for processing (1)
  • crates/engine/src/sync/staging.rs

Walkthrough

The PR adds authenticated per-version content keys, streaming content writes, bounded staging admission, staged publication and cleanup, and write APIs across the engine, WASM boundary, worker protocol, transports, and client facade.

Changes

Content-key protocol

Layer / File(s) Summary
Content-key sealing and vectors
crates/core/src/seal/*, crates/core/examples/kat_gen.rs, crates/core/kat/*, blueprint/core.md
Adds the content-key structure tag, HPKE-authenticated sealing and opening, manifest metadata, acceptance vectors, rejection vectors, and KAT validation.
Content lifecycle contract test
crates/contract/tests/contract.rs
Adds coverage for content-version registration, idempotent registration, retirement, and quota release.

Engine content plane

Layer / File(s) Summary
Streaming framing and DAG assembly
crates/engine/src/content/*, crates/engine/examples/kat_gen.rs, crates/engine/tests/kat_content.rs
Adds ContentWriter, reusable chunk sealing, CID-based DAG assembly, root-size estimation, and bounded plaintext buffering.
Staging admission
crates/engine/src/content/budget.rs, crates/engine/src/storage_policy.rs
Adds exact sealed-size accounting, reservation tracking, refusal categories, and staging-cap policy data.
Write lifecycle and publication
crates/engine/src/facade.rs, crates/engine/src/sync/*, crates/engine/src/net/author.rs
Replaces inline content commands with write handles, stages and uploads blocks, seals content keys, registers referenced CIDs, publishes versions, and cleans up failed or abandoned content.

WASM and client write API

Layer / File(s) Summary
WASM boundary and error mapping
crates/wasm/src/*, crates/fuse/src/*
Exposes write-handle methods, removes inline content from create commands, removes updateContent, and maps new content errors and dead-letter reasons.
Worker and transport contracts
packages/client/src/worker/*, packages/client/src/transport.ts, packages/client/src/correlatedTransport.ts, packages/client/src/engineClient.ts, packages/client/src/facade.ts
Adds WriteTarget, WriteHandle, write request variants, bigint results, and client methods for beginning, pushing, committing, and aborting writes.
Broadcast relay and ordering
packages/client/src/broadcast*.ts, packages/client/src/leaderRelay.ts, packages/client/src/writeQueue.ts
Routes correlated write messages, serializes steps per handle, permits concurrency across handles, enforces ownership, and aborts handles on client or relay shutdown.
Integration and browser coverage
packages/client/src/*.test.ts, packages/client/test/browser/*
Updates command tests and adds write lifecycle, ordering, ownership, cleanup, error, and journal persistence coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

  • FSM1/cipher-box#700 — Provides content-plane foundations extended by the streaming writer and DAG changes.
  • FSM1/cipher-box#728 — Provides related WASM and client transport changes extended by the write-handle API.
  • FSM1/cipher-box#898 — Provides related engine drain, staging, and write-error paths extended by this PR.

Suggested labels: v2-build, comp:engine

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR's primary change: implementing end-to-end file content writes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/868-write-file-content

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

FSM1 added 5 commits July 31, 2026 15:33
The settings-record tag landed on main at 0x0b first, so the content-key
tag moves to 0x0c to keep the registry byte-ordered and 1-based dense.
The tag is AAD-bound, so the content_key KAT vectors are regenerated
from kat_gen; only the AEAD tags move, no other vector family changes.
@FSM1
FSM1 force-pushed the feat/868-write-file-content branch from 57e5f51 to 35a662c Compare July 31, 2026 13:42
@FSM1

FSM1 commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto main at 7759cb6e1 (#903, the vault settings record).

#903 claimed struct tag 0x0b for settings-record first, so content-key on this branch renumbers 0x0b -> 0x0c. The registry stays byte-ordered and 1-based dense (content-key placed last, twelve tags), and blueprint/core.md's registry list now carries it.

The struct tag is AAD-bound, so the content_key KAT vectors were regenerated with cargo run -p cipherbox-core --example kat_gen. Only vectors/content_key/content_key_accept.json and content_key_reject.json moved, and only in their AEAD tags — the ciphertext bodies are byte-identical, which is exactly what an AAD-only change produces. manifest.json records "structTag": 12 for contentKey; no other vector family, manifest entry, or struct tag changed, and a second generator run produces an identical tree.

Green locally: cargo test --workspace, cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, and cargo check -p cipherbox-wasm --target wasm32-unknown-unknown.

@FSM1
FSM1 marked this pull request as ready for review July 31, 2026 13:57
@FSM1
FSM1 marked this pull request as draft July 31, 2026 13:58
@FSM1
FSM1 marked this pull request as ready for review July 31, 2026 13:58
@FSM1
FSM1 marked this pull request as draft July 31, 2026 14:01
@FSM1
FSM1 marked this pull request as ready for review July 31, 2026 14:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/fuse/src/error.rs (1)

172-192: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add the missing TooManyWrites cause to this test.

The test iterates [StagingLimit, DeviceFull, StagingBacklog, StorageUnmeasured, AccountQuota]. OverBudgetCause also has a TooManyWrites variant (derived from Refusal::TooManyWrites in crates/engine/src/facade.rs). The test's own doc comment states every cause "has to survive the crossing", so leave no variant untested.

🧪 Proposed fix
         for cause in [
             OverBudgetCause::StagingLimit,
             OverBudgetCause::DeviceFull,
             OverBudgetCause::StagingBacklog,
             OverBudgetCause::StorageUnmeasured,
             OverBudgetCause::AccountQuota,
+            OverBudgetCause::TooManyWrites,
         ] {
🤖 Prompt for 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.

In `@crates/fuse/src/error.rs` around lines 172 - 192, Update the cause list in
the each_budget_keeps_its_own_cause test to include
OverBudgetCause::TooManyWrites, ensuring every OverBudgetCause variant is
verified through the VfsError conversion.
🧹 Nitpick comments (18)
blueprint/core.md (1)

165-166: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider documenting the content-key KAT set, like the neighboring tags.

The registry entry is correct and matches STRUCT_TAG_CONTENT_KEY and manifest.json (content-key: 12). Every other recently added tag (owner-write-blob, op-record, settings-record) also documents its accept/reject vector families in this section. The content-key families (content_key_accept, content_key_reject) are not described here.

🤖 Prompt for 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.

In `@blueprint/core.md` around lines 165 - 166, Update the content-key section in
the manifest documentation to describe its KAT vector families,
content_key_accept and content_key_reject, consistently with the neighboring tag
entries.
crates/engine/src/content/write.rs (2)

241-268: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The realloc test also documents the stall.

the_pending_buffer_is_never_reallocated breaks out of the loop when remaining.len() == rest.len(), which is exactly the no-progress condition. The test therefore encodes a caller contract that differs from the push doc comment. If push starts to fail closed on an over-push, assert the error here instead of breaking on the stalled slice.

🤖 Prompt for 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.

In `@crates/engine/src/content/write.rs` around lines 241 - 268, Update the test
the_pending_buffer_is_never_reallocated to assert the expected push error when
the input exceeds the declared size, rather than breaking when remaining.len()
equals rest.len(). Preserve the capacity and observed_size assertions after
validating the fail-closed behavior.

74-92: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Add a small guard in push so stale unit loops cannot hide over-length input.

push_chunk_inner rejects excess input before push, but ContentWriter still accepts larger initial chunks by declaring one full chunk while observed_size() remains smaller. This leaves the push-only test behavior inconsistent with the write-handles path. Add a bound check in push for observed + bytes.len() > declared_size and fail closed before buffering excess bytes.

🤖 Prompt for 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.

In `@crates/engine/src/content/write.rs` around lines 74 - 92, Update
ContentWriter::push to reject input when self.observed plus bytes.len() exceeds
the declared size, returning the appropriate existing error before extending
pending or updating observed. Preserve normal chunk sealing and remainder
handling for inputs within the declared bound.
crates/engine/src/content/mod.rs (1)

12-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider pub(crate) mod budget; for consistency.

Every item in budget is pub(crate) (Refused, StagingLedger, sealed_total_bytes, ReservationId, MAX_OPEN_WRITES), so pub mod budget exposes no public API. limits uses pub(crate) mod for the same situation.

🤖 Prompt for 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.

In `@crates/engine/src/content/mod.rs` around lines 12 - 22, The budget module
declaration uses pub mod while all of its exports (Refused, StagingLedger,
sealed_total_bytes, ReservationId, MAX_OPEN_WRITES) are pub(crate), which means
the module exposes no public API. Change pub mod budget to pub(crate) mod budget
to align with the consistency pattern already established by the limits module
in the same visibility scope.
crates/core/examples/kat_gen.rs (1)

6437-6493: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Consider adding a low-order enc reject vector for the content-key family.

The settings-record family pins hpke-non-contributory with a short and a low-order enc. The content-key family only varies enc by one bit (tampered_enc) and by width (wide_enc), so the contributory check on this family's open path is not pinned by its own vector. A low-order enc vector would freeze that behavior for open_content_key.

🤖 Prompt for 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.

In `@crates/core/examples/kat_gen.rs` around lines 6437 - 6493, Add a new reject
vector in build_content_key_reject that uses reframe_content_key to set the enc
field to a low-order value, similar to how tampered_enc and wide_enc are
constructed. This vector should mirror the low-order enc vector pattern used in
the settings-record family to pin the contributory check behavior on the
open_content_key path, alongside the existing tampered_enc and wide_enc
variants.
crates/core/src/seal/mod.rs (1)

15-15: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Confirm whether seal::content_key should remain public

seal::content_key exposes only the already re-exported content_key helpers, and no current code imports it through that path. If only seal::seal_*/open_* and constants are part of the intended public API, make content_key private.

🤖 Prompt for 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.

In `@crates/core/src/seal/mod.rs` at line 15, Review the public API exposed by the
seal module and make the content_key module private if its helpers are already
re-exported elsewhere and no callers require the seal::content_key path. Update
the module declaration in seal and preserve the existing seal_* and open_*
functions and constants as the intended public API.
crates/engine/examples/kat_gen.rs (1)

241-246: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse leaf_cids for the later assertion and the hex output.

Lines 254 and 262 rebuild the same CID list from leaves. Reuse leaf_cids so the vector, the assertion, and the hex encoding cannot drift.

♻️ Proposed refactor
-        assert_eq!(
-            manifest.leaf_cids,
-            leaves.iter().map(|l| l.cid.clone()).collect::<Vec<_>>(),
-            "{name}: links preserve file order"
-        );
+        assert_eq!(
+            manifest.leaf_cids, leaf_cids,
+            "{name}: links preserve file order"
+        );
-            leaf_cids: leaves.iter().map(|l| hex::encode(&l.cid)).collect(),
+            leaf_cids: leaf_cids.iter().map(hex::encode).collect(),
🤖 Prompt for 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.

In `@crates/engine/examples/kat_gen.rs` around lines 241 - 246, Reuse the existing
leaf_cids variable from the assembly setup for the later assertion and
hexadecimal output in the kat generation flow, removing the duplicate
leaves.iter CID collection at those locations. Keep the current assertion and
encoding behavior unchanged while ensuring both consume the same vector.
crates/engine/src/content/chunk.rs (1)

16-20: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Pin SEALED_LEAF_OVERHEAD against a real sealed leaf.

The staging reservation is exact only while SEALED_LEAF_OVERHEAD equals the true per-leaf overhead of seal_chunk. Nothing in this file asserts that. A change to the core wire layout would silently make every reservation wrong. Add a test that seals one chunk and compares the lengths.

💚 Proposed test
#[test]
fn sealed_leaf_overhead_matches_a_real_sealed_leaf() {
    let key = ContentKey::from_bytes([2u8; KEY_LEN]);
    let chunk = vec![0xABu8; 13];
    let leaf = seal_one_chunk(&key, &chunk, &mut SeededEntropy::new(4)).unwrap();
    assert_eq!(
        leaf.sealed.len() as u64,
        chunk.len() as u64 + SEALED_LEAF_OVERHEAD,
        "the reservation arithmetic must match the sealed wire layout"
    );
}
🤖 Prompt for 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.

In `@crates/engine/src/content/chunk.rs` around lines 16 - 20, Add a test function
to verify that the SEALED_LEAF_OVERHEAD constant matches the actual per-leaf
overhead produced by seal_one_chunk. The test should create a sample ContentKey
and plaintext chunk, call seal_one_chunk to produce a sealed leaf, and then
assert that the sealed output length equals the plaintext chunk length plus
SEALED_LEAF_OVERHEAD. This pins the constant against the real wire layout so
that future changes to the sealing format are caught before they silently break
the staging admission ledger reservation calculation.
crates/engine/src/content/dag.rs (1)

572-599: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Cover the 3-byte to 5-byte CBOR head boundary.

The comment states the test holds at every leaf count where a CBOR head width changes. The counts stop at 300. head_len also changes at 0x1_0000, and both the links-array count and the size uint cross that width on real production files. At 65536 leaves the production root is about 2.5 MiB, so it still assembles and the case is reachable. Add 65535 and 65536 so the sizing arithmetic is pinned at that width too.

💚 Proposed change
-            for leaves in [0u64, 1, 23, 24, 255, 256, 300] {
+            for leaves in [0u64, 1, 23, 24, 255, 256, 300, 65_535, 65_536] {
🤖 Prompt for 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.

In `@crates/engine/src/content/dag.rs` around lines 572 - 599, The test
root_block_len_matches_the_assembled_root does not cover the CBOR head width
boundary at 0x1_0000 where both the links-array count and size uint widen from 3
bytes to 5 bytes. Extend the leaves array (currently [0u64, 1, 23, 24, 255, 256,
300]) to include 65535 and 65536 so the sizing arithmetic is pinned at this
boundary transition and validates at every leaf count where a CBOR head width
changes.
crates/engine/src/sync/staging.rs (1)

128-157: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two copies of the write-handle framing loop. Both files reproduce the same ContentWriter push/finish loop to build a version's sealed leaves and root block. The shared root cause is the absence of one test helper for that loop, so a change to the ContentWriter contract must be applied twice.

  • crates/engine/src/sync/staging.rs#L128-L157: replace the body of framed with a call to a shared helper exposed from crate::testkit.
  • crates/engine/tests/write_plane.rs#L899-L921: replace frame_version with the same testkit helper, returning the root CID it already derives from finished.content.content_cid().

testkit is reachable from both an in-crate unit test module and an integration test, so one definition serves both call sites.

🤖 Prompt for 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.

In `@crates/engine/src/sync/staging.rs` around lines 128 - 157, The duplicated
ContentWriter framing loop should be centralized in a shared testkit helper. In
crates/engine/src/sync/staging.rs lines 128-157, update framed to call the
helper while preserving its blocks, root block, and StagedContent outputs; in
crates/engine/tests/write_plane.rs lines 899-921, replace frame_version’s
duplicated loop with the same helper and continue returning the root CID derived
from finished.content.content_cid(). Expose one helper from crate::testkit for
both call sites.
crates/engine/tests/write_plane.rs (1)

1004-1119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a staged-byte assertion to the two block-loss tests.

Both tests assert the dead letter and the absent publish. Neither asserts what happens to the remaining staged blocks. The key-failure test at Line 992 pins the release side, so the preservation side of the same rule stays unasserted. blueprint/engine.md states that an unrecoverable record dead-letters with staged content preserved, while #818 releases blocks only for the no-key case. A regression that released the surviving blocks here would pass both tests.

Add an assertion on staged_keys() (or staged_bytes_total()) after the tick in each test.

The two tests also differ only by the removed leaf index. Consider one helper parameterized on that index.

🤖 Prompt for 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.

In `@crates/engine/tests/write_plane.rs` around lines 1004 - 1119, Add assertions
after tick in an_evicted_prefix_of_the_block_set_is_loss_not_progress and
a_hole_in_the_staged_block_suffix_is_loss_and_fails_closed verifying the
remaining staged content is preserved via staged_keys() or staged_bytes_total(),
matching the documented unrecoverable-record behavior. Keep the existing
dead-letter and no-publish assertions, and optionally extract their shared
setup/assertion flow into a helper parameterized by the removed leaf index.
packages/client/src/correlatedTransport.ts (1)

68-71: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a named alias for the durable op id.

commitWrite returns a durable opId, while beginWrite, pushChunk, and abortWrite use WriteHandle. A new OpId type would distinguish write handles from op ids in the worker protocol and across implementers.

🤖 Prompt for 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.

In `@packages/client/src/correlatedTransport.ts` around lines 68 - 71, Introduce a
named OpId type for durable operation identifiers and update commitWrite to
return Promise<OpId> instead of Promise<bigint>. Propagate this alias through
the worker protocol and all implementations or consumers of commitWrite, while
keeping WriteHandle for beginWrite, pushChunk, and abortWrite.

Source: Coding guidelines

packages/client/test/browser/leadership.ts (1)

104-119: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider preserving the original error if abortWrite also fails.

If abortWrite throws inside the catch block, its error replaces the original pushChunk/commitWrite failure, so the root cause is lost from the harness's return value. This only affects diagnostics in this Playwright test harness.

🔧 Optional improvement
     try {
       await client!.facade.pushChunk(handle, content.buffer);
       await client!.facade.commitWrite(handle);
     } catch (error) {
-      await client!.facade.abortWrite(handle);
+      await client!.facade.abortWrite(handle).catch(() => {});
       throw error;
     }
🤖 Prompt for 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.

In `@packages/client/test/browser/leadership.ts` around lines 104 - 119, Update
the inner error handling in window.cbCreateFile so an abortWrite failure cannot
replace the original pushChunk or commitWrite error; preserve and rethrow the
initial failure while still attempting abortWrite, allowing the outer
settle(error) path to return the root cause.
packages/client/src/writeQueue.ts (1)

1-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a direct unit test for WriteQueue.

The header states two guarantees: steps for one handle run in call order, and a rejected step does not stall the remaining steps for that handle. serve.test.ts covers the class only indirectly through serveEngine. A direct test pins both guarantees and the map pruning, and it fails loudly if previous.then(step, step) is ever simplified to previous.then(step).

Suggested cases:

  • Interleave two handles. Assert that handle A's steps complete in call order and that handle B does not wait for A.
  • Reject the first step for one handle. Assert that the second step still runs and that run rejects for the failed step only.

Do you want me to generate the test file?

🤖 Prompt for 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.

In `@packages/client/src/writeQueue.ts` around lines 1 - 33, Add a direct unit
test for the WriteQueue class covering per-handle call-order serialization,
independent progress for interleaved handles, and continuation after a rejected
step. Assert that only the failed run rejects, the subsequent run executes, and
the internal chains map is pruned after each handle drains.
packages/client/test/browser/journalEngine.worker.ts (1)

79-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Return an op id that differs from the handle.

commitWrite returns the write handle as the durable op id. The two are separate id spaces: beginWrite yields a handle and commitWrite yields an op id. A client that conflates them passes against this fake. ReadHost in packages/client/src/worker/serve.test.ts keeps them distinct (11n for the handle, 2048n for the commit).

♻️ Proposed change
 class JournalHost implements EngineHostLike {
   private nextHandle = 1n;
+  private nextOpId = 1000n;
   private readonly open = new Map<bigint, { size: number; received: number }>();
     this.open.delete(handle);
     await journal('commitWrite');
-    return handle;
+    return this.nextOpId++;
   }
🤖 Prompt for 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.

In `@packages/client/test/browser/journalEngine.worker.ts` around lines 79 - 86,
Update commitWrite in the journalEngine worker fake to return a distinct durable
operation id rather than the input handle, matching the separate handle/op-id
contract and the ReadHost values used by serve.test.ts.
packages/client/src/worker/engineHost.ts (1)

71-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Narrow WriteTarget once.

The method tests 'node' in target three times. A single branch makes the two exclusive shapes explicit and removes the chance that a later edit updates one test and not the others.

♻️ Proposed refactor
   beginWrite(target: WriteTarget, size: number): Promise<WriteHandle> {
-    const node = 'node' in target ? this.wasm.NodeId.fromBytes(target.node) : undefined;
-    const parent = 'node' in target ? undefined : this.wasm.NodeId.fromBytes(target.parent);
-    const name = 'node' in target ? undefined : target.name;
-    return this.handle.beginWrite(parent, name, node, size);
+    return 'node' in target
+      ? this.handle.beginWrite(undefined, undefined, this.wasm.NodeId.fromBytes(target.node), size)
+      : this.handle.beginWrite(
+          this.wasm.NodeId.fromBytes(target.parent),
+          target.name,
+          undefined,
+          size
+        );
   }
🤖 Prompt for 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.

In `@packages/client/src/worker/engineHost.ts` around lines 71 - 76, Refactor
beginWrite so it narrows target once using a single branch on the exclusive
'node' shape, deriving node, parent, and name within the respective branches
before calling this.handle.beginWrite. Preserve the existing NodeId conversions
and arguments for both target variants.
packages/client/src/worker/commandCodec.test.ts (1)

34-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the parent argument too.

The test pins the argument count, the name, and the kind. It leaves calls[0][0] unchecked, so a swap that passed the wrong bytes to NodeId.fromBytes would still pass. The stub returns { bytes }, so the assertion is one line.

♻️ Proposed assertion
     expect(calls).toHaveLength(1);
     expect(calls[0]).toHaveLength(3);
+    expect(calls[0][0]).toEqual({ bytes: new Uint8Array(16).fill(1) });
     expect(calls[0][1]).toBe('a.txt');
     expect(calls[0][2]).toBe(fakeWasmEnums.NodeKind.File);
🤖 Prompt for 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.

In `@packages/client/src/worker/commandCodec.test.ts` around lines 34 - 37, Update
the test assertions around the recorded call to also verify calls[0][0] equals
the expected parent argument bytes, using the stubbed { bytes } value returned
by NodeId.fromBytes. Keep the existing assertions for argument count, filename,
and node kind unchanged.
crates/engine/src/sync/overlay.rs (1)

279-297: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the empty-file case to the version-rendering test.

The test contrasts NewNode::File { content: Some(..) } with NewNode::Folder. It does not cover NewNode::File { content: None }, which is the third branch the create arm discriminates. A future change that keyed content_version off node.kind() instead of staged_content() would pass this test and still render a phantom version on every empty file.

♻️ Proposed extra case
         let bare = Op::create(id(2), id(0), "dir", NewNode::Folder, 1, AT);
+        let empty = Op::create(id(3), id(0), "e.txt", NewNode::File { content: None }, 1, AT);
 
-        let view = apply_overlay(&base, &[with_content, bare]);
+        let view = apply_overlay(&base, &[with_content, bare, empty]);
 
         assert_eq!(view.node(id(1)).unwrap().content_version, Some(1));
         assert_eq!(view.node(id(2)).unwrap().content_version, None);
+        assert_eq!(
+            view.node(id(3)).unwrap().content_version,
+            None,
+            "an empty file create authors no version"
+        );
🤖 Prompt for 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.

In `@crates/engine/src/sync/overlay.rs` around lines 279 - 297, Extend the test
function overlay_renders_the_one_version_a_content_bearing_create_authors with a
NewNode::File { content: None } create operation, apply it alongside the
existing operations, and assert its rendered node has content_version set to
None. Keep the existing assertions for content-bearing files and folders
unchanged.
🤖 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 `@crates/engine/src/content/budget.rs`:
- Around line 119-120: Align the documentation for the reservation API,
including the `reserved()` docs around the `committed` calculation and the
related text near the later reservation logic, to state that each reservation
remains held in full until release. Keep the existing accounting implementation
unchanged; do not introduce staged-progress tracking.

In `@crates/engine/src/facade.rs`:
- Around line 1691-1746: Validate WriteTarget::Version in begin_write before
calling staged_bytes_total or admitting the reservation: require that the node
exists and is a file, rejecting unknown nodes and NodeKind::Folder with the
appropriate EngineError. Keep WriteTarget::NewFile behavior unchanged, and
ensure invalid version targets consume no staging work, node IDs, entropy, or
reservation.
- Around line 1914-1924: The error mapping for the seal_content_key call is
incorrectly classifying the failure as EngineError::Seam, which is documented
for host I/O failures and retryable scenarios, but seal_content_key failures are
deterministic and non-retryable. Replace the EngineError::Seam variant in the
.map_err block with either a new ContentKeySealFailed variant (modeling the seal
failure separately from host seams) or reuse the existing seal_error taxonomy
that already handles sealing errors. Preserve the error's check field in the
mapped result.

In `@crates/engine/src/sync/drain.rs`:
- Around line 1400-1411: The test section at lines 293-340 in
crates/engine/tests/sync.rs is named "staged bytes preserved" with assertions
that expect dead-lettered staged bytes to remain available, but this contradicts
the release_staged_blocks implementation which now drops staging bytes on every
abandonment. Rename the test section to reflect the actual drop behavior, update
the test's comment to match the current abandonment policy, and change the
assertions from expecting staged_bytes to exist to verifying that staged_bytes
is absent or properly dropped after abandonment occurs.
- Around line 1813-1823: The registered_by function should include
content_root_cid-derived retirement for every content-bearing operation, not
only OpKind::Create. Keep derive_write_name child retirement gated to Create,
while allowing UpdateContent and other applicable operations to return the
encoded content root so abandoned uploaded blocks can be released.

In `@crates/wasm/src/host.rs`:
- Around line 236-274: Validate size in begin_write before casting it to u64,
requiring it to be finite and non-negative like the existing
storage_headroom_bytes validation. Return a clear JsError for invalid values,
and preserve the existing write flow for valid sizes.

In `@packages/client/src/leaderRelay.ts`:
- Around line 218-220: Update the abortWrite arm in the relay step handler to
await this.transport.abortWrite(handle) before deleting handle from
this.writeOwners, matching the commitWrite policy so failed aborts leave the
handle owned and retryable. Preserve the existing successful abort return
behavior.

---

Outside diff comments:
In `@crates/fuse/src/error.rs`:
- Around line 172-192: Update the cause list in the
each_budget_keeps_its_own_cause test to include OverBudgetCause::TooManyWrites,
ensuring every OverBudgetCause variant is verified through the VfsError
conversion.

---

Nitpick comments:
In `@blueprint/core.md`:
- Around line 165-166: Update the content-key section in the manifest
documentation to describe its KAT vector families, content_key_accept and
content_key_reject, consistently with the neighboring tag entries.

In `@crates/core/examples/kat_gen.rs`:
- Around line 6437-6493: Add a new reject vector in build_content_key_reject
that uses reframe_content_key to set the enc field to a low-order value, similar
to how tampered_enc and wide_enc are constructed. This vector should mirror the
low-order enc vector pattern used in the settings-record family to pin the
contributory check behavior on the open_content_key path, alongside the existing
tampered_enc and wide_enc variants.

In `@crates/core/src/seal/mod.rs`:
- Line 15: Review the public API exposed by the seal module and make the
content_key module private if its helpers are already re-exported elsewhere and
no callers require the seal::content_key path. Update the module declaration in
seal and preserve the existing seal_* and open_* functions and constants as the
intended public API.

In `@crates/engine/examples/kat_gen.rs`:
- Around line 241-246: Reuse the existing leaf_cids variable from the assembly
setup for the later assertion and hexadecimal output in the kat generation flow,
removing the duplicate leaves.iter CID collection at those locations. Keep the
current assertion and encoding behavior unchanged while ensuring both consume
the same vector.

In `@crates/engine/src/content/chunk.rs`:
- Around line 16-20: Add a test function to verify that the SEALED_LEAF_OVERHEAD
constant matches the actual per-leaf overhead produced by seal_one_chunk. The
test should create a sample ContentKey and plaintext chunk, call seal_one_chunk
to produce a sealed leaf, and then assert that the sealed output length equals
the plaintext chunk length plus SEALED_LEAF_OVERHEAD. This pins the constant
against the real wire layout so that future changes to the sealing format are
caught before they silently break the staging admission ledger reservation
calculation.

In `@crates/engine/src/content/dag.rs`:
- Around line 572-599: The test root_block_len_matches_the_assembled_root does
not cover the CBOR head width boundary at 0x1_0000 where both the links-array
count and size uint widen from 3 bytes to 5 bytes. Extend the leaves array
(currently [0u64, 1, 23, 24, 255, 256, 300]) to include 65535 and 65536 so the
sizing arithmetic is pinned at this boundary transition and validates at every
leaf count where a CBOR head width changes.

In `@crates/engine/src/content/mod.rs`:
- Around line 12-22: The budget module declaration uses pub mod while all of its
exports (Refused, StagingLedger, sealed_total_bytes, ReservationId,
MAX_OPEN_WRITES) are pub(crate), which means the module exposes no public API.
Change pub mod budget to pub(crate) mod budget to align with the consistency
pattern already established by the limits module in the same visibility scope.

In `@crates/engine/src/content/write.rs`:
- Around line 241-268: Update the test the_pending_buffer_is_never_reallocated
to assert the expected push error when the input exceeds the declared size,
rather than breaking when remaining.len() equals rest.len(). Preserve the
capacity and observed_size assertions after validating the fail-closed behavior.
- Around line 74-92: Update ContentWriter::push to reject input when
self.observed plus bytes.len() exceeds the declared size, returning the
appropriate existing error before extending pending or updating observed.
Preserve normal chunk sealing and remainder handling for inputs within the
declared bound.

In `@crates/engine/src/sync/overlay.rs`:
- Around line 279-297: Extend the test function
overlay_renders_the_one_version_a_content_bearing_create_authors with a
NewNode::File { content: None } create operation, apply it alongside the
existing operations, and assert its rendered node has content_version set to
None. Keep the existing assertions for content-bearing files and folders
unchanged.

In `@crates/engine/src/sync/staging.rs`:
- Around line 128-157: The duplicated ContentWriter framing loop should be
centralized in a shared testkit helper. In crates/engine/src/sync/staging.rs
lines 128-157, update framed to call the helper while preserving its blocks,
root block, and StagedContent outputs; in crates/engine/tests/write_plane.rs
lines 899-921, replace frame_version’s duplicated loop with the same helper and
continue returning the root CID derived from finished.content.content_cid().
Expose one helper from crate::testkit for both call sites.

In `@crates/engine/tests/write_plane.rs`:
- Around line 1004-1119: Add assertions after tick in
an_evicted_prefix_of_the_block_set_is_loss_not_progress and
a_hole_in_the_staged_block_suffix_is_loss_and_fails_closed verifying the
remaining staged content is preserved via staged_keys() or staged_bytes_total(),
matching the documented unrecoverable-record behavior. Keep the existing
dead-letter and no-publish assertions, and optionally extract their shared
setup/assertion flow into a helper parameterized by the removed leaf index.

In `@packages/client/src/correlatedTransport.ts`:
- Around line 68-71: Introduce a named OpId type for durable operation
identifiers and update commitWrite to return Promise<OpId> instead of
Promise<bigint>. Propagate this alias through the worker protocol and all
implementations or consumers of commitWrite, while keeping WriteHandle for
beginWrite, pushChunk, and abortWrite.

In `@packages/client/src/worker/commandCodec.test.ts`:
- Around line 34-37: Update the test assertions around the recorded call to also
verify calls[0][0] equals the expected parent argument bytes, using the stubbed
{ bytes } value returned by NodeId.fromBytes. Keep the existing assertions for
argument count, filename, and node kind unchanged.

In `@packages/client/src/worker/engineHost.ts`:
- Around line 71-76: Refactor beginWrite so it narrows target once using a
single branch on the exclusive 'node' shape, deriving node, parent, and name
within the respective branches before calling this.handle.beginWrite. Preserve
the existing NodeId conversions and arguments for both target variants.

In `@packages/client/src/writeQueue.ts`:
- Around line 1-33: Add a direct unit test for the WriteQueue class covering
per-handle call-order serialization, independent progress for interleaved
handles, and continuation after a rejected step. Assert that only the failed run
rejects, the subsequent run executes, and the internal chains map is pruned
after each handle drains.

In `@packages/client/test/browser/journalEngine.worker.ts`:
- Around line 79-86: Update commitWrite in the journalEngine worker fake to
return a distinct durable operation id rather than the input handle, matching
the separate handle/op-id contract and the ReadHost values used by
serve.test.ts.

In `@packages/client/test/browser/leadership.ts`:
- Around line 104-119: Update the inner error handling in window.cbCreateFile so
an abortWrite failure cannot replace the original pushChunk or commitWrite
error; preserve and rethrow the initial failure while still attempting
abortWrite, allowing the outer settle(error) path to return the root cause.
🪄 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 Plus

Run ID: b86fc20f-4b3e-4afd-bcfd-3e0511504eec

📥 Commits

Reviewing files that changed from the base of the PR and between 7759cb6 and 35a662c.

📒 Files selected for processing (64)
  • blueprint/core.md
  • crates/contract/tests/contract.rs
  • crates/core/examples/kat_gen.rs
  • crates/core/kat/manifest.json
  • crates/core/kat/vectors/content_key/content_key_accept.json
  • crates/core/kat/vectors/content_key/content_key_reject.json
  • crates/core/src/seal/aad.rs
  • crates/core/src/seal/content_key.rs
  • crates/core/src/seal/mod.rs
  • crates/core/tests/kat_manifest.rs
  • crates/engine/examples/kat_gen.rs
  • crates/engine/src/content/budget.rs
  • crates/engine/src/content/chunk.rs
  • crates/engine/src/content/dag.rs
  • crates/engine/src/content/mod.rs
  • crates/engine/src/content/write.rs
  • crates/engine/src/entropy.rs
  • crates/engine/src/facade.rs
  • crates/engine/src/lib.rs
  • crates/engine/src/net/author.rs
  • crates/engine/src/storage_policy.rs
  • crates/engine/src/sync/boot.rs
  • crates/engine/src/sync/drain.rs
  • crates/engine/src/sync/mod.rs
  • crates/engine/src/sync/op.rs
  • crates/engine/src/sync/overlay.rs
  • crates/engine/src/sync/rebase.rs
  • crates/engine/src/sync/record.rs
  • crates/engine/src/sync/staging.rs
  • crates/engine/tests/facade.rs
  • crates/engine/tests/kat_content.rs
  • crates/engine/tests/sync.rs
  • crates/engine/tests/write_plane.rs
  • crates/fuse/src/error.rs
  • crates/fuse/src/ops.rs
  • crates/fuse/tests/fuse_op_core.rs
  • crates/wasm/src/host.rs
  • crates/wasm/src/lib.rs
  • crates/wasm/tests/boundary.rs
  • packages/client/src/broadcast.ts
  • packages/client/src/broadcastTransport.test.ts
  • packages/client/src/broadcastTransport.ts
  • packages/client/src/correlatedTransport.ts
  • packages/client/src/engineClient.ts
  • packages/client/src/facade.test.ts
  • packages/client/src/facade.ts
  • packages/client/src/index.ts
  • packages/client/src/leaderRelay.ts
  • packages/client/src/testkit.ts
  • packages/client/src/transport.test.ts
  • packages/client/src/transport.ts
  • packages/client/src/worker/commandCodec.test.ts
  • packages/client/src/worker/commandCodec.ts
  • packages/client/src/worker/engineHost.ts
  • packages/client/src/worker/engineWasm.ts
  • packages/client/src/worker/protocol.ts
  • packages/client/src/worker/serve.test.ts
  • packages/client/src/worker/serve.ts
  • packages/client/src/writeQueue.ts
  • packages/client/test/browser/engineHarness.ts
  • packages/client/test/browser/fakeEngine.worker.ts
  • packages/client/test/browser/journalEngine.worker.ts
  • packages/client/test/browser/leadership.spec.ts
  • packages/client/test/browser/leadership.ts
💤 Files with no reviewable changes (1)
  • crates/fuse/src/ops.rs

Comment thread crates/engine/src/content/budget.rs
Comment thread crates/engine/src/facade.rs
Comment thread crates/engine/src/facade.rs Outdated
Comment thread crates/engine/src/sync/drain.rs
Comment thread crates/engine/src/sync/drain.rs Outdated
Comment thread crates/wasm/src/host.rs
Comment thread packages/client/src/leaderRelay.ts Outdated
Retire the version root of any content-bearing op on abandonment, not just a
create's: an abandoned updateContent had uploaded — and so charged — blocks that
no record links and nothing ever retired.

Validate a WriteTarget::Version before admission, so a version of an unknown or
folder node is refused before it burns staging, entropy or budget rather than
halting the drain after a whole upload.

Classify a content-key seal failure as its own deterministic error instead of
EngineError::Seam, which hosts retry as availability, and reject a non-finite or
negative beginWrite size at the wasm boundary before the saturating cast turns
it into a zero-byte reservation.

Drop a relay write owner only once its abort settles, matching the commit arm,
so a rejected abort leaves the handle retryable instead of stranding its
reservation.

Correct the release-on-abandonment docs to name the failure-valve paths they
actually cover, add the low-order enc reject vector to the content-key KAT
family, and document that family in the tag registry.

Entire-Checkpoint: 37500a3daed1
@FSM1

FSM1 commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

All 26 review items worked in 67de2b0. The 7 inline threads carry their own replies and are resolved; the outside-diff comment and the 18 nitpicks have no threads, so their disposition is below.

Outside diff range

Item Verdict
crates/fuse/src/error.rs — missing TooManyWrites cause Fixed. Every OverBudgetCause variant now crosses the VfsError conversion.

Nitpicks

Item Verdict
blueprint/core.md — document the content-key KAT families Fixed.
content/write.rs — realloc test documents the stall Fixed, differently: the test now asserts the declared-size boundary explicitly instead of breaking on the no-progress condition.
content/write.rs — bound check inside push Declined. push returns EntropyError; a size arm would mean a new error type duplicating an enforcement Engine::push_chunk already does precisely and with better diagnostics. The misleading "the loop always makes progress" doc line was the real defect and is corrected.
content/mod.rspub(crate) mod budget Fixed.
core/examples/kat_gen.rs — low-order enc reject vector Fixed. content_key_reject is 14 vectors; the regenerated manifest and vector file are additive only, no existing vector moved.
core/src/seal/mod.rs — make content_key private False positive. Every sibling in seal/ is pub modbody, envelope, grant, op_record, settings_record, structure, write_body. Making this one private would be the inconsistent choice.
engine/examples/kat_gen.rs — reuse leaf_cids Fixed. Generator re-run confirms byte-identical output.
content/chunk.rs — pin SEALED_LEAF_OVERHEAD Fixed, over four chunk lengths including the empty and exact-chunk cases.
content/dag.rs — 3-to-5 byte CBOR head boundary Fixed.
sync/staging.rs + tests/write_plane.rs — duplicated framing loop Fixed. One testkit::frame_version helper now serves the in-crate unit test and the integration suite.
tests/write_plane.rs — staged-byte assertion on the block-loss tests Fixed, with the opposite assertion to the one suggested. Both tests abandon through the failure valve, which releases; preservation is the terminally-unrebasable rule, not this one. The shared setup is now one evict_leaf helper parameterized on the index.
correlatedTransport.tsOpId alias Declined as speculative.
commandCodec.test.ts — assert the parent argument Fixed.
engineHost.ts — narrow WriteTarget once Fixed.
writeQueue.ts — direct unit test Fixed. New writeQueue.test.ts pins per-handle call order, independent progress across handles, and continuation after a rejected step — the last is what fails if previous.then(step, step) is ever simplified.
journalEngine.worker.ts — distinct op id Fixed.
leadership.ts — preserve the original error Fixed.
sync/overlay.rs — empty-file case Fixed.

Split out

The updateContent retirement finding turned out to reach further than the version root: POST /content/upload inserts a charged pin row per block, so every leaf is charged and no path retires leaves, on the Create path either. Filed as #916 with a dependency edge on #868.

Verification

cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo check -p cipherbox-wasm --target wasm32-unknown-unknown --all-targets, and cargo test for core / engine / fuse / wasm are all clean. Both KAT generators are idempotent on re-run. @cipherbox/client typecheck, its 109 unit tests across 14 files, eslint and prettier are clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/engine/src/sync/staging.rs (1)

185-214: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate every manifest leaf before queueing the content operation.

Root presence and root CID validation do not prove that every referenced leaf is staged. A valid root can reference a missing leaf. The queue can then retain an operation with no complete upload set.

Decode the root and require each referenced leaf to exist and address to its CID before enqueue_op. Add a case that stages the valid root but omits one leaf, then assert that no operation is queued.

As per path instructions, blueprint/engine.md requires “CID verification on every block.”

🤖 Prompt for 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.

In `@crates/engine/src/sync/staging.rs` around lines 185 - 214, Update the
content-operation staging flow, including the logic exercised by
a_content_op_enqueues_once_its_root_block_is_staged, to decode the staged root
manifest and validate every referenced leaf before enqueue_op: each leaf must
exist in staging and its contents must match its CID. Add a test alongside
a_content_op_whose_root_is_not_staged_fails_closed_and_queues_nothing that
stages the valid root while omitting one leaf, then verifies staging fails and
queued_ops remains empty.

Source: Path instructions

🤖 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 `@packages/client/src/writeQueue.test.ts`:
- Around line 1-69: Add named required CI status checks for both test suites:
configure a check that runs the Vitest suite containing WriteQueue tests in
packages/client/src/writeQueue.test.ts and another that runs the engine tests
covering the staging logic in crates/engine/src/sync/staging.rs. Ensure both
checks execute before merges and are enforced as required gates.

---

Outside diff comments:
In `@crates/engine/src/sync/staging.rs`:
- Around line 185-214: Update the content-operation staging flow, including the
logic exercised by a_content_op_enqueues_once_its_root_block_is_staged, to
decode the staged root manifest and validate every referenced leaf before
enqueue_op: each leaf must exist in staging and its contents must match its CID.
Add a test alongside
a_content_op_whose_root_is_not_staged_fails_closed_and_queues_nothing that
stages the valid root while omitting one leaf, then verifies staging fails and
queued_ops remains empty.
🪄 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 Plus

Run ID: 3c312fc4-23d1-412e-9858-bc29b11a84ef

📥 Commits

Reviewing files that changed from the base of the PR and between 35a662c and 67de2b0.

📒 Files selected for processing (26)
  • blueprint/core.md
  • crates/core/examples/kat_gen.rs
  • crates/core/kat/manifest.json
  • crates/core/kat/vectors/content_key/content_key_reject.json
  • crates/engine/examples/kat_gen.rs
  • crates/engine/src/content/budget.rs
  • crates/engine/src/content/chunk.rs
  • crates/engine/src/content/dag.rs
  • crates/engine/src/content/mod.rs
  • crates/engine/src/content/write.rs
  • crates/engine/src/facade.rs
  • crates/engine/src/sync/drain.rs
  • crates/engine/src/sync/overlay.rs
  • crates/engine/src/sync/staging.rs
  • crates/engine/src/testkit/content.rs
  • crates/engine/src/testkit/mod.rs
  • crates/engine/tests/sync.rs
  • crates/engine/tests/write_plane.rs
  • crates/fuse/src/error.rs
  • crates/wasm/src/host.rs
  • packages/client/src/leaderRelay.ts
  • packages/client/src/worker/commandCodec.test.ts
  • packages/client/src/worker/engineHost.ts
  • packages/client/src/writeQueue.test.ts
  • packages/client/test/browser/journalEngine.worker.ts
  • packages/client/test/browser/leadership.ts
🚧 Files skipped from review as they are similar to previous changes (16)
  • crates/core/kat/manifest.json
  • crates/engine/src/content/chunk.rs
  • crates/fuse/src/error.rs
  • packages/client/test/browser/leadership.ts
  • packages/client/src/worker/commandCodec.test.ts
  • crates/engine/src/sync/overlay.rs
  • packages/client/src/worker/engineHost.ts
  • crates/engine/src/content/dag.rs
  • crates/engine/src/content/budget.rs
  • crates/engine/tests/sync.rs
  • crates/engine/examples/kat_gen.rs
  • crates/engine/src/content/mod.rs
  • crates/wasm/src/host.rs
  • packages/client/src/leaderRelay.ts
  • crates/engine/src/sync/drain.rs
  • crates/engine/src/facade.rs

Comment thread packages/client/src/writeQueue.test.ts
stage_op binds only the root: staging is mutable between the journal entry and
the upload, so a leaf sweep here would pass and still leave the drain re-reading
every block. Assert that a missing leaf enqueues rather than fails, so the
division of responsibility is a test rather than prose.
@FSM1

FSM1 commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

Second pass worked. The actionable thread is replied to and resolved; the outside-diff-range item has no thread, so its disposition is here.

Outside diff range — crates/engine/src/sync/staging.rs, validate every manifest leaf before enqueue_op

Declined, with a test added to pin why. d8a5d6cce adds a_missing_leaf_still_enqueues_because_the_drain_owns_that_gate.

The check already exists, one layer down and at the only point where it can hold. Drain::staged_block CID-verifies every block it reads, fail-closed:

verify_cid(key, &block).map_err(|_| CONTENT_LOST)?;

and upload_version treats a leaf absent past the durable upload mark as CONTENT_LOST, i.e. Halt::Permanent(ContentUnrecoverable) — a dead letter, never a published version. Two tests on this PR already cover it end to end: an_evicted_prefix_of_the_block_set_is_loss_not_progress and a_hole_in_the_staged_block_suffix_is_loss_and_fails_closed.

Moving that gate to enqueue time would be strictly worse on both axes:

  • It cannot hold. Staging is mutable between the journal entry and the drain — host eviction, a co-tenant, bit-rot. Those two tests simulate exactly that, evicting a leaf after the op is queued. Passing an enqueue-time sweep proves nothing at upload time, so the drain must re-check regardless; the sweep would be a pure TOCTOU snapshot.
  • It costs a second full pass over the file. Verifying every leaf means reading and BLAKE3-hashing the whole version again, synchronously inside commitWrite. On the web target that is a full re-read of OPFS on the commit path — a direct regression in the streaming behaviour engine: write file content end to end #868 exists to deliver.

The root is checked at enqueue for a structural reason the leaves do not share, already stated on stage_op: it is the manifest. Without it the drain cannot enumerate the leaves to check at all, and orphan_staging_keys cannot expand the root into its leaf set, so the leaves would be classed as orphans and GC'd.

blueprint/engine.md line 505 says "The engine verifies CIDs" of blocks it reads — which is what staged_block does. It does not require a pre-flight sweep at journal time.

Actionable — named required CI gates

False positive, answered in the thread with evidence. Both suites already gate: packages/client/src/writeQueue.test.ts runs under Test via pnpm testpnpm -r --if-present run testvitest run, and crates/engine/src/sync/staging.rs runs under Engine Tests plus the three Cargo Check & Test legs. New tests inside existing suites, no new suite.

Verification

cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo check --workspace --all-targets, and cargo check -p cipherbox-wasm --target wasm32-unknown-unknown --all-targets are clean. cargo test for core / engine / fuse / wasm: 0 failures, engine now 541 lib tests. @cipherbox/client vitest: 109 tests across 14 files, passing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant