chore(kaish): bump the read-only shell to kaish-kernel 0.17.1 - #174
Conversation
0.17.1 carries the fix that has been blocking kaibo v0.4.0: `readlink -f` and `realpath` failed on *every* operand on a rooted mount, with a message naming neither the operand nor its target. Both resolve now, and an escape refuses by name. We reported that shape upstream during the 0.17.0 bump; it became two structural kaish PRs rather than a patch. The compiler found nothing — no API break reached kaibo, and the composed tool contract is byte-identical under both pins. So the check was the shell, per the rule the last bump wrote: every battery in `docs/sandbox-probes.md` run against both 0.17.0 and 0.17.1 binaries and diffed. A, B, D, E, F and G came back identical; two changes are the whole delta a model can see. The second one is new and we accept it: the directories *above* the project mount list again, where 0.17.0 answered "not found". It is synthesis from kaibo's own mount table, not a host read — each level names only the next component down to the project, so a model walking up recovers the root path string the caller already handed it. Counted rather than argued: `ls /tmp` returns one entry where the host holds 3575. Adjacent secrets, real siblings, and the state db and media CAS all stay invisible. That observable is pinned by a new containment test with a recorded positive control — move the mount up and the leak assertion fires — because a battery where everything comes back empty proves nothing otherwise. Battery C's claim that `/home` is an inert stub is now false and is corrected in place, the same way the 0.17.0 bump corrected three others. The probe caught itself once, worth recording: E1 run without `--root` created a state db, because the fixture was then outside every allowed tree and the guard correctly did not fire. The §0 question — would this read differently if the probe were broken? — is what found it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cross-family review — kaibo cast
|
…e it could hide Cross-family review (kaibo cast `crusoe`, DS4-Flash explorer + GLM-5.2 synth) found three real gaps in the previous commit. Every citation checked before acting. The test's doc comment claimed "each ancestor lists exactly the one component" and then probed one level — the immediate parent. A synthesis bug a level higher would have fired nothing. The fixture is now two levels deep, each with a sibling that must never appear, so the test proves what it says. The bigger miss: it covered bytes (`cat`, `grep`) and discovery (`ls`, `find`) but not existence. `stat` and `realpath` answer "does this exist" without returning any bytes, and `realpath` resolves for the first time in this very release — so that pair is where an oracle would most plausibly have arrived. Measured: a real host file beside the chain and a path that was never created refuse byte-identically, and only the synthesized components resolve. Now pinned, in the shape Battery G3 uses for links. Third, Battery C's opening pass criterion still read "everything outside the single mount comes back `not found`", contradicting the carve-out two lines below it. Scoped to reads, which is what it always meant. The positive control was re-run against the rewritten test: move the mount up and the leak assertion fires. Reviewed-by: kaibo cast `crusoe` (DeepSeek-V4-Flash explorer, GLM-5.2 synth) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI note: the
|
| parallel | serial | |
|---|---|---|
| this branch | 6 / 20 failed | 0 / 10 failed |
main @ f741a84, unmodified |
3 / 15 failed | — |
So: pre-existing, independent of the kaish bump, and a ~20–30% failure rate per run.
Shape. stub_script (tests/credentials.rs:208-215) writes an 0o755 #!/bin/sh
stub which resolve_key_from_cmd then execs. Rust opens files O_CLOEXEC, but in the
window between another test thread's fork and its exec the child holds a duplicate of
this thread's write fd — and execing a file that is open for writing is ETXTBSY. It is
the harness, not the credential code.
Fix shape, for its own PR: a process-wide mutex the stub-using tests hold across both
stub_script and the resolve_key_from_cmd call, so a fork can never happen while a stub
write fd is open. That serializes ~10 tests rather than the suite. Retrying on ETXTBSY
would also go green but would hide a real race behind a loop.
I re-ran the job rather than papering over it. Flagging it because a merge gate that fails
a fifth of the time is one we will start ignoring — worth closing before v0.4.0.
…176) The credentials suite failed CI on #174 and had been flaking for weeks. Amy's call was to fix it ahead of the cosmetic pre-release items — *"a merge gate that fails a fifth of the time is one we will start ignoring"* — so this goes first in the pre-release queue. ## Measure before asserting Our own note called this a rare race. It is not: | | parallel | serial | |---|---|---| | the #174 branch | **6 / 20 failed** | 0 / 10 | | `main` @ `f741a84`, unmodified | **3 / 15 failed** | — | A fifth to a third of runs. That number is what moved this from "known flake, re-run it" to "fix it now." ## What it actually is `ETXTBSY`. Writing a stub holds a write fd on it, and `fork` hands every open fd to the child; a sibling test forking in that window gives its child a duplicate, which it holds until it reaches `exec`. An `exec` of that stub in the meantime is "Text file busy". `O_CLOEXEC` does not save it — the fd closes *at* exec, and the entire window is before it. Nothing to do with the credential code: the harness owns this one. ## Three decisions **Fix the race, not the symptom.** Retrying on `ETXTBSY` would go green while leaving a real fork/exec race in the tree, and a retry loop around a race teaches the next reader that the race is acceptable. This removes the overlap instead. **A read/write lock, not a mutex** — the two operations are not symmetric. Creation takes the exclusive side (brief, and it must exclude every spawn); spawning takes the shared side, so resolves still run concurrently with each other. That matters for one test specifically: `a_blocking_key_resolve_does_not_stall_a_sibling_task` exists to pin a concurrency property, and a coarse mutex would have quietly serialized it into a tautology that passes without testing anything. **Every resolve goes through the helper**, including the ones whose command is not a stub — a spawn that fails at `exec` has already forked, so it is in the race too. Poisoning is stepped over deliberately: otherwise one test's panicking assertion fails every later test with a lock error instead of its own message. ## The number, and the control that makes it mean something **0 of 60** parallel runs fail with the fix. A zero is only worth as much as its control, so: remove *only* the two lock-acquisition lines, keep the helper and every other change, and it goes back to **11 of 20 failing**. The lock is doing the work, and the instrument still detects the race it claims to have closed. Full suite 1328 passed / 0 failed. `cargo clippy --all-targets` clean. ## No changelog entry The test harness is not a user-facing surface, and the house rule is that the git log is the record for an internal fix. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
) Pre-release item four. The house rule is that a changelog bullet is one line — the change, and at most one clause of reason — and that the whole unreleased section gets shortened *before* it is retitled for a release. It had drifted a long way from that. Someone scanning for what changed was reading the reasoning behind it instead: | | before | after | |---|---|---| | bullets | 47 | 46 | | running past one line | **41** | 0 | | lines | 120 | 95 | The reasoning did not disappear — it lives in the pull requests, which is where the rule says it belongs. ## The better find Shortening surfaced a wrong fact. The kaish bullet said *"upgraded to 0.17.1 (from 0.14.1)"* while a separate **Fixed** bullet announced *"Bump `kaish-kernel` to 0.14.1"* — the same cumulative jump stated twice, from a baseline that was not the released one. v0.3.0 shipped on kaish-kernel **0.14.0**: ``` $ git show v0.3.0:Cargo.toml | grep kaish-kernel kaish-kernel = { version = "0.14.0", … } ``` So a reader upgrading from the last release was told the wrong starting point. It now reads "from 0.14.0", once — and 0.14.1's user-visible fix (the explorer's shell dropping piped stdin) stays in **Fixed** as the fix it is, rather than as a second version bump. This is the same class of error #174's review asked me to double-check on the "from" baseline. That check confirmed 0.17.0 had never shipped; it did not look at whether 0.14.1 had. It hadn't. ## Compression was checked for loss A shortening pass that quietly drops a change is worse than a long changelog, so this was verified rather than trusted: diffing the vocabulary of both versions turned up four things a user would act on that the first pass had dropped. All four are back — - a slow generation comes back as a **`job-N` handle** rather than holding the call; - base64 **`content`** is still the way in for an image that is not a file; - the four formats `write_cas` accepts **by signature** (png, jpeg, gif, webp); - the **twenty-fold** cost spread across operations — an exact number is the whole point of publishing one. Everything else that fell out was reasoning, an implementation detail (`multipart`, `init_image`/`style_image`, `ultra`/`sd3`), or a duplicate. Changelog only — no code. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
0.17.1 carries the fix that has been blocking kaibo v0.4.0:
readlink -fandrealpathfailed on every operand on a rooted mount, with a message namingneither the operand nor its target (
readlink: o-exists: No such file or directory: /tmp). Both resolve now, and an escape refuses by name. We reported that shapeupstream during the 0.17.0 bump; it became two structural kaish PRs rather than a
patch — #435 (the router's mount-ancestor
synthesis was unreachable whenever
/is mounted) and#434 (
Filesystem::canonicalizeas adefaulted trait method, moving containment out of the builtin into the VFS).
The compiler found nothing, so the shell was the check
No API break reached kaibo —
Filesystem::canonicalizeis defaulted and kaiboimplements no
FilesystemorKernelBackend. The composed tool contract isbyte-identical under both pins (throwaway crate calling
compose(&Recipe::tool_description(), …)), and so are all elevenhelpsurfaceskaibo renders, 43941 bytes each side.
So the check was the shell, per the rule the 0.17.0 bump wrote into AGENTS.md. Every
battery in
docs/sandbox-probes.mdwas run against both a 0.17.0 and a 0.17.1binary and diffed. A, B, D, E, F and G came back identical. Two changes are the whole
delta a model can see.
1. The release blocker is fixed.
readlink -f/realpathresolve an in-treepath (exit 0) and refuse an escape by name. Battery G3 re-run on the new canonicalize
path: existing, missing, and unreadable targets still refuse byte-identically, so the
new code introduces no existence oracle.
2. The directories above the mount list again — new, and accepted. 0.17.0 answered
"not found" for every directory above the project; 0.17.1 restores them as synthesized
directories. It is synthesis from kaibo's own mount table, not a host read: each level
names only the next component down to the project, so a model walking up recovers the
root path string the caller already handed it and nothing else. Counted rather than
argued —
ls /tmpreturns one entry where the host/tmpholds 3575. Adjacentsecrets, real siblings, and the state db and media CAS all stay invisible (E2/F2
re-run against a non-empty store: 4 KiB db, 210 CAS objects, both
not found).That observable is pinned by a new containment test with a recorded positive
control — move the mount up and the leak assertion fires — because a battery where
everything comes back empty proves nothing otherwise. Battery C's claim that
/homeis "an inert stub that cannot be walked" is now false and is corrected in place, the
same way the 0.17.0 bump corrected three other criteria.
The probe caught itself once
E1 run without
--rootcreated a state db, because the fixture was then outside everyallowed tree and the guard correctly did not fire. Re-run with the root pointing at the
fixture, both E1 and F1 refuse loudly and create nothing. The §0 question — would this
read differently if the probe were broken, versus if the thing it audits were broken?
— is what found it.
Gates
cargo clippy --all-targets: clean.cargo test: 1147 passed. The lone failure is the knowntests/credentials.rsETXTBSY exec race under parallelism (green serially, reproduces on unmodified code).
cargo tree -iempty foraws-lc-rs,mimalloc,openssl-sys.Cross-family review posted as a comment below.
🤖 Generated with Claude Code