Keep the sandbox's own chatter off the stream the command owns

stdout inside the container belongs to the command that ran. Two readers take it as
data: ai-sandbox-autophase's preflight reads its answer from that stream, and
progress.mjs reads the child pi's JSON event stream from it. Five notices in the
pi-seeding block went there instead of stderr, and the steady state (host file
unchanged, so cur = prev) prints one on *every* pi-bridged run -- the driver refused a
working sandbox on every project. They all go to stderr now, like log() and fail().

The preflight takes the aisb-ok marker from wherever it appears instead of insisting it
come first: a banner from an older image or from the user's own launcher is warned about
and quoted back, because that stream is parsed as data in every unit. It still refuses on
a non-zero exit, on silence, and on a launcher that runs no command -- the failures that
would repeat in every unit -- and quotes the foreign stdout instead of a bare exit code.

The same block, second contract: the thinking level is policy, so a home the sandbox
creates gets it on the *first* seed, not one run later via the merge.

tests/unit/06_entrypoint.sh runs the seeding block with the two streams split and asserts
stdout empty in every state (cold, steady, edited inside, unparseable, level changed), and
greps the entrypoint for any [aisb] line without >&2. tests/unit/10_autophase.sh adds the
noisy and the yapping launcher fixtures.

LLM-Generated: true
This commit is contained in:
2026-10-30 22:53:57 -04:00
parent 8b70967907
commit 90d98dcc9e
5 changed files with 171 additions and 11 deletions
+19 -1
View File
@@ -125,7 +125,18 @@ networking or the image, `make e2e` green too.
anywhere in there silently closes the argument and the rest runs in the *outer* shell
— valid bash, wrong program, clean `bash -n` and clean shellcheck. Nothing in it may
use `'`; jq programs there are written unquoted (`.defaultThinkingLevel=\$lvl`), and
`tests/unit/06_entrypoint.sh` fails the build if that ever changes.
`tests/unit/06_entrypoint.sh` fails the build if that ever changes. (It bit once while
editing a comment: one apostrophe in a word like `pi's` is enough.)
**Stdout inside the container belongs to the command that ran.** Everything the sandbox
says about itself goes to stderr — `log()` and `fail()` do, and so must every new line,
because the driver's preflight reads its answer from that stream and progress.mjs reads the
child pi event stream from it. The pi-seeding notices were on stdout once, and the steady
state (host file unchanged, so `cur = prev`) prints one on *every* pi-bridged run: it made
`ai-sandbox-autophase` refuse a working sandbox on every project. `tests/unit/06_entrypoint.sh`
now runs the seeding block with the two streams split and asserts stdout empty in each state
(cold, steady, edited-in-sandbox, unparseable, level changed), and greps the entrypoint for
any `[aisb]` line without `>&2`. Same block, second contract: the thinking level is policy,
so it is written on the *first* seed too, not only by the merge on the second run.
* `split_target()` / `classify_allows()` — the mapping from what the user typed to
what the firewall and the proxy enforce.
* `build_argv()` — every isolation flag lives here.
@@ -189,6 +200,13 @@ networking or the image, `make e2e` green too.
the preflight name it, and every child starts `</dev/null` so the launcher cannot put
the terminal back. Keep the `"$@"` in every launcher this repo prints — `install.sh`,
`README.md`, `docs/recipes.md`.
- **The preflight reads the marker, not the first line.** `ap_preflight` asks for one
sandboxed shell and takes the line starting `aisb-ok` wherever it appears, because a banner
from an older image or from a launcher of the user's own is not a broken sandbox — it is
warned about and quoted back, since the same stream is parsed as data in every unit. It
still refuses on a non-zero exit, on silence, and on a launcher that runs no command: those
are the failures that would repeat in every unit. Tests: the noisy and yapping launcher
fixtures in `tests/unit/10_autophase.sh`.
- **The push is the delivery contract.** `ap_push_pending` runs at start, at every
phase boundary and at end of run; start is a warning, the boundary and the end are
fatal, because exiting 0 with the work unpushed is the silent miss this script