docs(probes): the CAS write path gets a battery, and it found a missing test - #177
Merged
Conversation
…ng test Item two on the v0.4.0 pre-release list, carried since v0.3.0: the media CAS had containment probes (Battery F — a path inside a project is refused at startup, kaish can neither read nor enumerate the store) but nothing that measured its *write* shape. That shape is the argument the CAS rests on — safe by shape, not by policy, because the address is the content hash and no write surface takes a destination. It was a sentence in AGENTS.md and nowhere a number. Battery H turns it into measurements, and is written from its own first run rather than from reading the code: - The stored object's name equals `sha256sum` of the file that produced it, which an operator can check independently. Two writes of one image make one object. - A repeat write leaves inode and mtime unchanged to the nanosecond — `create_new` never reopens an occupied address, so stored bytes are immutable. The battery says to compare mtime rather than object count, because a rewrite with identical bytes would keep the count and still prove the store mutable. - A refused write leaves the file count where it was, taken on both sides. - A fresh object stays invisible to kaish, with the host-side listing as the denominator — F2 alone would pass against an empty store. - With persistence off, the store refuses rather than hand back a digest that dies at exit. Silent fallbacks are the failure that would matter here. Writing it found the gap worth having. `write_cas`'s source path is containment-checked, and the out-of-tree case was tested — but not the case one indirection along, a symlink inside the tree pointing outside. That is the leg `read_contained_file`'s own doc calls the one worth not skipping, and its doc points at `tests/containment.rs`'s `mount_layer_symlink_*` battery for it, which drives `run_kaish` rather than this path. A store that followed such a link would be an exfiltration route with a read-back verb attached. The new test carries a positive control (a real file inside the tree still stores), and the teeth were checked: replace `containing_tree`'s refusal in `read_contained_file` with a fallback to the file's own parent and both refusal tests fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Item two on the v0.4.0 pre-release list, carried since v0.3.0. The media CAS had
containment probes — Battery F: a path inside a project is refused at startup, kaish can
neither read nor enumerate the store — but nothing that measured its write shape.
That shape is the argument the CAS rests on. From AGENTS.md: the store is "safe by shape,
not policy — the address is the content hash, so its API has no destination-path parameter
for a model to aim; it is write-only (
create_new; no unlink/truncate/rename, so an editis copy-on-write)." Every clause of that was a sentence we believed and nowhere a number.
Battery H, written from its own first run
Not from reading the code — the probes were run against a scratch store first, and the
battery records what actually came back.
equals
sha256sumof the file that produced it. Two writes of one image make oneobject; a different image makes a second.
nanosecond. The battery says to compare mtime rather than object count on purpose — a
rewrite with identical bytes would keep the count and still prove the store mutable.
refusal happened" is not the same claim as "nothing landed."
denominator. F2 alone would pass against an empty store and prove nothing.
hand back a digest that dies the moment the process exits; on tmpfs it stores but warns
the artifacts will not survive the host. A silent fallback is the failure that would
matter here.
Also worth noting for what is absent:
cas writetakes the file to read and nothingelse, and
write_castakespathorcontentplus alabel— wherepathis asource, containment-checked like any other read. No write surface accepts a destination.
The gap it found
write_cas's source path is containment-checked, and the out-of-tree case was tested. Thecase one indirection along was not: a symlink inside the allowed tree pointing outside
it. That is the leg
read_contained_file's own doc comment calls "the one worth notskipping" — and that doc points at
tests/containment.rs'smount_layer_symlink_*battery as proof, which drives
run_kaish, not this path.A store that followed such a link would be an exfiltration route with a read-back verb
attached, since
read_cashands the bytes back to the client. The behavior was alreadycorrect —
canonicalizeresolves the link before the containing-tree check — so this addsthe test, not a fix.
The new test carries a positive control (a real file inside the tree still stores through
the same handler), and the teeth were checked rather than assumed: replace
containing_tree's refusal inread_contained_filewith a fallback to the file's ownparent, and both refusal tests fail.
Gates
Suite 1329 passed / 0 failed (one new).
cargo clippy --all-targetsclean. The diff isadditions only — no reformatting rode along.
No changelog entry: a runbook battery and a test are not user-facing surfaces.
🤖 Generated with Claude Code