chore(kaish): upgrade the read-only shell to kaish-kernel 0.17.0 - #173
Conversation
Cross-family review — kaibo cast
|
| surface | who reads it |
|---|---|
src/consult/prompts.rs — explorer preamble |
every sweep |
src/server/mod.rs — MCP run_kaish tool description |
the calling agent, resident |
src/server/mod.rs — kaibo://tools doc |
the calling agent, on demand |
src/kaish_syntax.rs — kaibo://kaish/sandbox recipes |
the model, on demand |
src/cli.rs — kaibo kaish -c help |
the operator |
Fixing one was worse than fixing none. A model reads the addendum every turn and the explorer preamble every sweep, so a half-fix taught two contradictory idioms with no way to tell which was right — and the dotted one produces exactly the ./src/foo.rs:12 citations this change existed to prevent. The review's framing of this as the headline finding is correct and I've recorded it as the durable lesson in AGENTS.md: when a kaish idiom changes, grep for it, because it is never in one place.
Also taken: kaibo://kaish/sandbox's exit-code list still quoted the pre-0.16 bare command not found.
Deliberately not changed, and the review agreed on the first: test fixtures that merely contain the idiom (progress.rs, consult/engine.rs) are inputs, not teaching. And a named-directory operand (grep -rn PATTERN DIR/) is correct and stays — GNU joins the operand as written, so a named directory still cites a usable path. It is . specifically that adds the ./.
Finding 2 (should fix, taken) — the containment test promised three existence states and built two
The doc comment claimed an existing target, a missing one, and an unreadable one are indistinguishable; the test created only to_existing and to_missing. That gap was load-bearing, because the no-existence-oracle property is the justification for accepting the target-string disclosure at all — and as the review put it, a permission-shaped difference probes the host just as well as an existence-shaped one.
Added the third arm (a mode-000 target), with a recorded positive control: point it at an in-tree file and the assertion fires.
left: "exit: 1 ... permission denied: path escapes root: <TARGET> is not under /tmp/..."
right: "exit: 0 --- stdout --- inside"
Checked and not taken
IgnoreScopewildcard arm — review concurred it is correct as written. The renderer returns a plainStringwith no error channel,#[non_exhaustive]requires some arm, andunreachable!()would panic on a variant a future kaish release may legitimately add. Naming the variant is the option that neither hides the gap nor invents a label.- Timing oracle — raised as a theoretical uncovered channel. Not taken: the refusal is decided by path arithmetic before any syscall reaches the target, which is why there is nothing to time. A timing assertion in a unit test would be flaky and would pin the implementation rather than the property.
--jsonline anchors,$(cmd)typed values,ls -R— review found no kaibo impact; I confirmed independently.orientation.rsis the only place kaibo parses kaish JSON (glob,stat,wc -l), none of which carry line anchors, and its tests run through the real kernel.
Gates after the fixes
1319 pass, 0 fail, plus the known tests/credentials.rs ETXTBSY parallel flake that passes serially and reproduces on unmodified code.
Jumps 0.14.1 -> 0.17.0, inheriting three releases' breaking changes at once. Amy: "once kaish 0.17 drops we'll upgrade and release kaibo 0.4." Compile-time exposure was one line pair, exactly what the 2026-08-21 pre-bump audit predicted: `IgnoreScope` became `#[non_exhaustive]` in 0.16 and broke the exhaustive match in `server/config_resource.rs`. The wildcard arm names the unrecognized variant instead of rendering a default label, mirroring the parse direction in `config::merge_kaish`, which refuses an unrecognized `scope` outright. Everything else was free: kaibo builds no `ExecContext` literal (0.17's other BREAKING item), uses no plan-side redaction API, walks no AST, and 0.16's `execute -> Result<_, KernelError>` flows through `anyhow`'s `?` unchanged. The compiler found less than the shell did, and that is the lesson worth keeping. One compile error, but four behavioral changes reached the model-facing surface and only running the shell found them. Two made our own prose false: - 0.16 made `grep -r` prefix hits with the operand as written, matching GNU. The `grep -rn PATTERN .` idiom the sandbox addendum teaches therefore started emitting `./src/foo.rs:12` for every citation the explorer earns. We now teach the bare `grep -rn PATTERN`, the only form that always yields a repo-relative `file:line` — a named file drops the filename entirely, which is the half a citation needs, so the old sentence's promise that the idiom works "whether the target is a file or a directory" went with it. Not a kaish ask: 0.16's behavior is GNU-exact on purpose, and asking kaish to diverge for our convenience would trade a correct rule for a cosmetic one. - 0.16 replaced the bare `command not found` for a refused external command with a message that names the refusal. The addendum quoted the old text. Two more arrived free through `kaish-help`, which kaibo composes rather than restates: compound statements now feed pipes, and `yes`/`no` stopped being lexer errors. The bump also moved the sandbox boundary. 0.17's lstat-by-default means a symlink inside the project pointing outside now renders its target path string through `ls -l`/`stat`/`readlink`/`find -type l`, where 0.14 refused. We accept that rather than narrow it, on a condition measured rather than assumed: there is no existence oracle. Existing, missing, and unreadable targets refuse byte-identically, decided by path arithmetic before any syscall reaches the target, so a hostile repo gets back only the string it wrote into its own link. A link's target is bytes stored inside the allowed tree, so reading it is reading project content; refusing would make `ls -l` misdescribe a directory kaibo is allowed to list. The new containment test states both halves and fails loudly if the second ever weakens. Full battery A-G re-run live, since a kernel/VFS bump trips the probe trigger. All clear. Three pass criteria in the runbook were false against 0.17 and are corrected in place; Battery G is new for the symlink boundary. Worth recording: 0.16 fixed an `env` that bypassed the external-commands gate, and kaibo was never exposed, because lever (0) compiles `subprocess` out and the host-spawn path it escaped through does not exist here. 1320 tests pass, 0 fail. `cargo tree -i` empty for aws-lc-rs, mimalloc, and openssl-sys; all six kaish crates at 0.17.0 in lockstep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e state Acting on the cross-family review (kaibo cast `crusoe`: DS4-Flash explorer, GLM-5.2 synth). Every citation was verified against the tree before I trusted it; both findings were real. The grep fix was applied to KAISH_SANDBOX_ADDENDUM and missed five other surfaces that still taught `grep -rn PATTERN .`: src/consult/prompts.rs the explorer preamble — the one that drives sweeps src/server/mod.rs the MCP `run_kaish` tool description src/server/mod.rs the `kaibo://tools` doc src/kaish_syntax.rs the `kaibo://kaish/sandbox` browsing recipes src/cli.rs `kaibo kaish -c` help Fixing one was worse than fixing none. A model reads the addendum every turn and the explorer preamble every sweep, so a half-fix taught two contradictory idioms with no way to tell which was right — and the dotted one produces exactly the `./src/foo.rs:12` citations the change existed to prevent. The sandbox resource's exit-code list still quoted the pre-0.16 bare `command not found` for a refused external command; it now names the refusal the way the addendum does. Deliberately left alone: test fixtures that merely contain the idiom (`progress.rs`, `consult/engine.rs`) are inputs, not teaching. A named-directory operand (`grep -rn PATTERN DIR/`) is correct and stays — GNU joins the operand as written, so a named directory still cites a usable path. It is `.` specifically that adds the `./`. Second finding: the new containment test's doc comment promised that an existing target, a missing one, and an unreadable one are indistinguishable, but the test built only two links. That gap mattered because the no-existence-oracle property is the justification for accepting the target-string disclosure at all, and a permission-shaped difference probes the host just as well as an existence-shaped one. Added the third arm (a mode-000 target) with a recorded positive control: point it at an in-tree file and the assertion fires. 1319 pass, 0 fail, plus the known `tests/credentials.rs` ETXTBSY parallel flake that passes serially and reproduces on unmodified code. Reviewed-by: kaibo cast `crusoe` (deepseek-ai/Deepseek-V4-Flash explorer, zai/GLM-5.2 synth) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Amy, on this bump: "we're not gonna release kaibo until we get on 0.17.1, so those symlink issues won't ship." The existing gate said to confirm the pins are "current", and that was not strong enough to catch this case. Being on the newest tag is not the same as being on a good one — this bump's own probe run is what FOUND `readlink -f` broken on every operand on a rooted mount, so "current" and "known broken" were true at the same moment. Shipping it would have been a decision rather than an oversight, which is the distinction the rule now names. The split it draws: developing on the new pin is fine and continues, because a merge is reversible and a release is not. So this PR still merges; v0.4.0 waits for 0.17.1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The release waits for kaish 0.17.1 — this PR does notAmy's call, verbatim: "we're not gonna release kaibo until we get on 0.17.1, so those symlink issues won't ship." What merges vs. what ships. This PR merges and What it keeps out of a release. The bump's own probe run found To be clear about which symlink item this is: the target-string disclosure documented above is not a bug and is not what we are waiting on — it is measured, accepted, tested, and documented in Battery G. It is The rule this produced (
The old gate said to confirm the pins are "current", and that was not strong enough here — this bump's probe run is what found the bug, so "we are on the current pin" and "we are on a known-broken pin" were true at the same moment. Being on the newest tag is not the same as being on a good one. Shipping it anyway would have been a decision rather than an oversight, which is the distinction the rule now names. Rebased onto the merged main#171 (transient-transport retry) and #172 (the brace) are both in
|
…ey serve Amy: "sandbox-probes.md is getting a bit heavy with history, should we streamline that doc?" It was — and the 2026-09-01 entry I had just written was the single heaviest thing in it. Two jobs were tangled: a runbook you read while probing, and a run log that only accreted. The log had reached 99 lines of a 494-line doc across five entries, in no particular chronological order. The rule now stated in the section itself, so it holds without anyone policing it: newest first, the current run in full, older runs compressed to a line. Git has the detail, and this doc is in git. The part that is not mere shortening: durable findings were trapped inside dated entries, where they are read once and then never again by the person who needs them. The 2026-07-29 run recorded that `cd / && ls` returns `dev`, `home`, `v` as synthetic VFS scaffolding rather than host content — a fact you need while reading Battery C's output, not a historical note. It now lives in Battery C, with the confirmation procedure, and is called out as not-a-finding because a reader meeting that listing cold reasonably suspects a hole. The 2026-08-13 run's two instrument lessons were already promoted into section 0, so those entries compress with nothing lost. **That promotion is the point of the compression, not a side effect of it** — the log shrinks because its contents found better homes. Battery G tightened too (63 -> 58); the rest is commands and pass criteria that carry their weight. Net: 494 -> 430 lines, run log 99 -> 31, and the batteries now sit in an even 29-58 band instead of one outlier. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…the PR Amy: "way too much added to AGENTS.md, can the addition be minimal/none?" It was too much — the first cut added a net 43 lines to a file charged twice over: once to every session here (CLAUDE.md is a symlink to it) and again to every kaibo consult on this repo, since AGENTS.md is the default --project-context-file and gets spliced into the preamble. Four bullets of bump narrative is a per-call tax on a story that only needed telling once. What stays is what changes an agent's behavior next time: the pin version, run the shell rather than trusting a green build, diff the composed contract, and grep for an idiom before assuming it lives in one file. Everything else — which match arm broke, which prose went false, the symlink boundary reasoning, the env-bypass we were structurally immune to — is in PR #173, which becomes the merge commit, and in docs/sandbox-probes.md, which is where someone probing the boundary actually looks. Applied the same discipline to what was already there: the 0.13.0 -> 0.14.0 "previous pin" bullet was three versions stale, and its one durable lesson (check that a probe reports differently if its subject is broken versus if the probe itself is) has lived in sandbox-probes.md section 0 since August, verified before deleting. Its other content described a test that still exists and still runs; the test is the guard, the prose was history. Net: AGENTS.md is 19 lines SHORTER than before the bump. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Jumps
kaish-kernel0.14.1 → 0.17.0, inheriting three releases' breaking changes at once. Amy: "once kaish 0.17 drops we'll upgrade and release kaibo 0.4."Stacked on #172 (the one-brace fix that makes
tests/config.rscompile again). Base retargets tomainautomatically when that merges.Compile-time exposure was one line pair
Exactly what the 2026-08-21 pre-bump audit predicted.
IgnoreScopebecame#[non_exhaustive]in 0.16 and broke the exhaustive match inserver/config_resource.rs. The wildcard arm names the unrecognized variant rather than rendering a default label, mirroring the parse direction inconfig::merge_kaish, which refuses an unrecognizedscopeoutright rather than falling back.Everything else was free: kaibo builds no
ExecContextliteral (0.17's other BREAKING item), uses no plan-side redaction API, walks no AST, and 0.16'sexecute → Result<_, KernelError>flows throughanyhow's?unchanged becauseKernelError: Error.The compiler found less than the shell did
One compile error — but four behavioral changes reached the model-facing surface, and only running the shell found them. Two made kaibo's own prose false:
0.16 made
grep -rprefix hits with the operand as written, matching GNU. Thegrep -rn PATTERN .idiom our sandbox addendum teaches therefore started emitting./src/foo.rs:12for every citation the explorer earns. Measured:We now teach the bare form, the only one that always yields a repo-relative
file:line. The old sentence's promise that the idiom works "whether the target is a file or a directory" went with it — the file case drops the filename, which is the half a citation needs.Deliberately not a kaish ask. 0.16's behavior is GNU-exact on purpose; asking kaish to diverge for our convenience would trade a correct rule for a cosmetic one.
0.16 replaced the bare
command not foundfor a refused external command with a message that names the refusal. The addendum quoted the old text.Two more arrived free through
kaish-help, which kaibo composes rather than restates: compound statements now feed pipes, andyes/nostopped being lexer errors.The sandbox boundary moved, and we accept it on a measured condition
0.17's lstat-by-default means a symlink inside the project pointing outside now renders its target path string through
ls -l/stat/readlink/find -type l, where 0.14 refused outright.We accept this rather than narrow it, because the property that makes it safe was measured rather than assumed — there is no existence oracle:
cat/stat -L/file/wc[[ -e ]]/etc/hostname(exists)permission denied: path escapes root/etc/DEFINITELY-NOT-HERE(missing)/root/.ssh/id_rsa(unreadable)The refusal is decided by path arithmetic before any syscall reaches the target, so a hostile repo gets back exactly the string it wrote into its own link — nothing about the host, not even existence. And a link's target is bytes stored inside the allowed tree, so reading it is reading project content; refusing would make
ls -lmisdescribe a directory kaibo is allowed to list.mount_layer_symlink_discloses_its_target_string_but_no_host_factstates both halves and fails loudly if the second ever weakens. Its leak assertion has a recorded positive control: point the link at an in-tree file carrying the marker and the assertion fires.Full sandbox battery A–G re-run live
A kernel/VFS bump trips the probe trigger. All clear. Three pass criteria in
docs/sandbox-probes.mdwere false against 0.17 and are corrected in place (Battery A'sln -srefusal reason, Battery B's 127 message, Battery C's now non-emptyenv). Battery G is new for the symlink boundary.Worth recording: 0.16 fixed an
envthat bypassed the external-commands gate, and kaibo was never exposed. Lever (0) compilessubprocessout, so the host-spawn path it escaped through does not exist here — verified against both versions. That is the four-levers design paying for itself.Gates
cargo build,cargo clippy --all-targetscleantests/credentials.rsETXTBSY parallel flake passes serially and reproduces on unmodified code)cargo tree -iempty foraws-lc-rs,mimalloc,openssl-sysCargo.lockSent upstream, not fixed here
Two kernel bugs found while probing, reported to kaish-lead for 0.17.1:
!binds only in a condition position, contradicting the 0.16.0 changelog's claim that! true && trueis(! true) && true.if ! falseandwhile ! truework;! true && echo xis a parse error at 1:1.readlink -ffails on every operand on a rooted LocalFs mount — an in-root regular file, an in-root link, a dangling link, and a missing file all returnNo such file or directory: /tmp, naming neither the operand nor its target. Plainreadlinkis fine.🤖 Generated with Claude Code