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
+60
View File
@@ -138,6 +138,66 @@ jq -n --slurpfile T "$M_IN/broken.json" --slurpfile B /dev/null --slurpfile N "$
-f "$MJQ" > /dev/null 2>&1 \
&& _bad "unparseable input fails loudly for the caller to handle" || _ok "unparseable input fails loudly for the caller to handle"
t_name "the sandbox keeps its own chatter off the stream the command owns"
# stdout inside the container is data. ai-sandbox-autophase reads its preflight answer from
# it and progress.mjs reads the child pi event stream from it, so every word the sandbox says
# about itself belongs on stderr. Five notices in this very block were once on stdout, and one
# of them fires on every pi-bridged run -- which made the driver refuse a working sandbox.
# Hence a test that runs the block and separates the two streams, not one that reads it.
if grep -nE '(echo|printf).*\[aisb\]' "$EP" | grep -v '>&2' | grep -q .; then
_bad "every [aisb] line in the entrypoint is redirected" \
"$(grep -nE '(echo|printf).*\[aisb\]' "$EP" | grep -v '>&2' | head -2 | tr '\n' ' ')"
else
_ok "every [aisb] line in the entrypoint is redirected"
fi
# The extracted block, run as the entrypoint runs it (bash -c with the policy directory, the
# level and the merge program as $1..$3). Only this snippet sees the substituted HOME, and it
# starts no container, so podman's own HOME is untouched.
SP="$WORK/seed"; mkdir -p "$SP/pi"
printf '{ "providers": { "gateway": { "models": [ { "id": "turbo" } ] } } }\n' > "$SP/pi/models.json"
printf '{ "theme": "dark/dark" }\n' > "$SP/pi/settings.json"
SEED_OUT="$WORK/seed.out"; SEED_ERR="$WORK/seed.err"
seed_run() { # <level>
HOME="$SP/home" bash -c "$(cat "$INNER")" aisb-seed "$SP" "${1-xhigh}" "$(cat "$MJQ")" \
> "$SEED_OUT" 2> "$SEED_ERR"
}
seed_says() { grep -qF -- "$1" "$SEED_ERR"; }
seed_stdout() { [[ -s "$SEED_OUT" ]] && printf '%s' "$(tr '\n' ';' < "$SEED_OUT")"; }
seed_level() { jq -r '.defaultThinkingLevel // "absent"' "$SP/home/.pi/agent/settings.json" 2>/dev/null; }
seed_run xhigh; RC=$?
assert_eq 0 "$RC" "seeding runs outside a container"
[[ -z "$(seed_stdout)" ]] && _ok "and it writes nothing on stdout (cold seed)" \
|| _bad "stdout belongs to the command (cold seed): $(seed_stdout)"
seed_says "seeded models.json" && _ok "the notice went to stderr" || _bad "the notice went to stderr"
seed_says "seeded settings.json" && _ok "including the settings one" || _bad "including the settings one"
# the level is policy, so a home we create must not run one unit at the default of pi itself
assert_eq "xhigh" "$(seed_level)" "the first seed already carries the thinking policy"
seed_run xhigh; RC=$?
assert_eq 0 "$RC" "and on the next run"
[[ -z "$(seed_stdout)" ]] && _ok "nothing on stdout once the files exist (the steady state)" \
|| _bad "steady-state stdout is not clean: $(seed_stdout)"
seed_says "refreshed models.json from the host config" \
&& _ok "the line that broke the driver is on stderr" || _bad "the line that broke the driver is on stderr"
sed -i 's/turbo/juggernaut/' "$SP/home/.pi/agent/models.json"
seed_run xhigh
[[ -z "$(seed_stdout)" ]] && _ok "an in-sandbox edit is reported without touching stdout" \
|| _bad "edited-file stdout: $(seed_stdout)"
seed_says "left models.json alone" && _ok "and the explanation still arrives" || _bad "and the explanation still arrives"
printf '{oops' > "$SP/home/.pi/agent/settings.json"
seed_run xhigh
[[ -z "$(seed_stdout)" ]] && _ok "even a repair is announced off stdout" || _bad "broken-json stdout: $(seed_stdout)"
seed_says "not usable JSON" && _ok "with the warning on stderr" || _bad "with the warning on stderr"
seed_run low
assert_eq "low" "$(seed_level)" "a changed level is applied over the merged file"
seed_says "thinking -> low" && _ok "and announced there too" || _bad "and announced there too"
[[ -z "$(seed_stdout)" ]] && _ok "with stdout still empty" || _bad "level-change stdout: $(seed_stdout)"
t_name "privilege drop is the last thing that happens"
tail -5 "$EP" | grep -q 'setpriv' && _ok "exec setpriv at the end" || _bad "setpriv last: $(tail -3 "$EP" | tr '\n' ' ')"
grep -q -- '--bounding-set=-all' "$EP" && _ok "bounding set cleared" || _bad "bounding set cleared"