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:
+28
-6
@@ -1001,6 +1001,13 @@ command -v setpriv >/dev/null || fail "setpriv is missing from the image; rebuil
|
||||
# the entrypoint has no CAP_DAC_OVERRIDE, so root could not write there anyway. It is
|
||||
# also the honest identity -- these are the user's own files.
|
||||
#
|
||||
# Stdout belongs to the command that was asked for. Everything the sandbox says about
|
||||
# itself goes to stderr, because two readers take that stream as data: ai-sandbox-autophase's
|
||||
# preflight reads its own answer from it, and progress.mjs reads the child pi's JSON event
|
||||
# stream from it. A notice on stdout is not cosmetic -- it is a line the reader has to
|
||||
# explain. tests/unit/06_entrypoint.sh runs this block with stdout split off and fails if it
|
||||
# is not empty, and checks every `[aisb]` line in the entrypoint for the redirect.
|
||||
#
|
||||
# The two files are managed differently, on purpose.
|
||||
#
|
||||
# models.json is a catalog, so it is managed whole: replaced when it is missing or still
|
||||
@@ -1052,12 +1059,12 @@ if [[ -d "$POLICY_DIR/pi" ]]; then
|
||||
echo "[aisb] pi config: cannot read $f -- its mode or SELinux label blocks this container, and nothing inside can repair it (remove it from the host, or use --ephemeral-home)" >&2
|
||||
elif [ -z "$cur" ]; then
|
||||
install -m 600 "$src" "$tgt" && printf %s "$want" > "$mark"
|
||||
echo "[aisb] pi config: seeded $f"
|
||||
echo "[aisb] pi config: seeded $f" >&2
|
||||
elif [ "$cur" = "$prev" ]; then
|
||||
install -m 600 "$src" "$tgt" && printf %s "$want" > "$mark"
|
||||
echo "[aisb] pi config: refreshed $f from the host config"
|
||||
echo "[aisb] pi config: refreshed $f from the host config" >&2
|
||||
else
|
||||
echo "[aisb] pi config: left $f alone (changed inside the sandbox; delete it to take the host version again)"
|
||||
echo "[aisb] pi config: left $f alone (changed inside the sandbox; delete it to take the host version again)" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1067,8 +1074,23 @@ if [[ -d "$POLICY_DIR/pi" ]]; then
|
||||
tgt="$dst/$f"; wrote="$dst/.aisb-$f.wrote"; tmp="$dst/.aisb-$f.tmp"
|
||||
rm -f "$dst/.aisb-$f.seeded" # the whole-file marker is .wrote now
|
||||
if [ ! -e "$tgt" ]; then
|
||||
install -m 600 "$src" "$tgt" && cp "$tgt" "$wrote"
|
||||
echo "[aisb] pi config: seeded $f"
|
||||
# The level is policy, so it goes in on the very first seed too. Copying the host
|
||||
# file alone left a fresh sandbox home running one unit at the default of pi itself,
|
||||
# with the merge on the next run quietly correcting it -- policy that waits a run is
|
||||
# not policy.
|
||||
if [ -n "$lvl" ]; then
|
||||
first="$(jq --arg lvl "$lvl" .defaultThinkingLevel=\$lvl "$src" 2>/dev/null)"
|
||||
else
|
||||
first="$(jq . "$src" 2>/dev/null)"
|
||||
fi
|
||||
if [ -n "$first" ] && printf "%s\n" "$first" > "$tmp" && install -m 600 "$tmp" "$tgt"; then
|
||||
:
|
||||
else
|
||||
install -m 600 "$src" "$tgt" # not JSON we could rewrite, so take it as it is
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
cp "$tgt" "$wrote"
|
||||
echo "[aisb] pi config: seeded $f" >&2
|
||||
elif [ ! -r "$tgt" ]; then
|
||||
echo "[aisb] pi config: cannot read $f -- its mode or SELinux label blocks this container, and nothing inside can repair it (remove it from the host, or use --ephemeral-home)" >&2
|
||||
else
|
||||
@@ -1093,7 +1115,7 @@ if [[ -d "$POLICY_DIR/pi" ]]; then
|
||||
if [ -n "$merged" ] && printf "%s\n" "$merged" > "$tmp" \
|
||||
&& install -m 600 "$tmp" "$tgt" && rm -f "$tmp" && cp "$tgt" "$wrote"; then
|
||||
if [ -n "$lvl" ] && [ "$had" != "$lvl" ]; then
|
||||
echo "[aisb] pi config: thinking -> $lvl (policy: --thinking, or host)"
|
||||
echo "[aisb] pi config: thinking -> $lvl (policy: --thinking, or host)" >&2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -697,14 +697,23 @@ ap_git_plan() {
|
||||
# preflight: one throwaway container, before any agent tokens are spent
|
||||
# --------------------------------------------------------------------------
|
||||
ap_preflight() {
|
||||
local out errf rc=0
|
||||
local out got="" line errf rc=0
|
||||
ap_log "preflight: one sandboxed shell, to prove the launcher, the image and the policy"
|
||||
errf="$(mktemp)"
|
||||
# </dev/null for the same reason as pi() below: this call has the terminal on its stdin
|
||||
# and the answer in a pipe, so anything that reads stdin inside the container would sit
|
||||
# there forever with nothing on screen to explain it.
|
||||
out="$("${AP_RUN[@]}" -- bash -c 'printf "aisb-ok aisb=%s git=%s user=%s cwd=%s\n" "${AISB:-none}" "$(git --version 2>/dev/null | cut -d" " -f3 || echo none)" "$(id -un)" "$PWD"' 2>"$errf" </dev/null)" || rc=$?
|
||||
if (( rc != 0 )) || [[ "$out" != aisb-ok* ]]; then
|
||||
# The answer is the marker line, wherever it turned up. stdout inside the container belongs
|
||||
# to the command the driver asked for and the sandbox's own notices belong on stderr; but an
|
||||
# image built before that was settled, or a launcher that echoes, can still put a line in
|
||||
# front. Name that loudly -- the same stream is read as data by progress.mjs in every unit
|
||||
# -- but do not refuse a sandbox that answered for it. The refusal is for a non-zero exit,
|
||||
# for silence, and for a launcher that runs nothing.
|
||||
while IFS= read -r line; do
|
||||
[[ "$line" == aisb-ok* ]] && { got="$line"; break; }
|
||||
done <<< "$out"
|
||||
if (( rc != 0 )) || [[ -z "$got" ]]; then
|
||||
printf '%s: preflight failed (exit %s): the launcher does not give a working sandbox.\n' "$PROG" "$rc" >&2
|
||||
printf ' command: %s\n' "$(ap_child_preview)" >&2
|
||||
tail -n 12 "$errf" | sed 's/^/ | /' >&2
|
||||
@@ -716,13 +725,22 @@ ap_preflight() {
|
||||
printf ' the sandbox started and answered nothing at all: a launcher that runs no command, or one\n' >&2
|
||||
printf ' that prints its answer somewhere other than stdout, fails exactly like this. It has to run\n' >&2
|
||||
printf ' follows the -- it was given (a function needs "$@" at the end; see --help).\n' >&2
|
||||
else
|
||||
printf ' stdout carried no aisb-ok answer at all; this is what came back instead:\n' >&2
|
||||
printf '%s\n' "$out" | awk -v max=6 '{ if (++n <= max) printf " > %s\n", $0 }' >&2
|
||||
fi
|
||||
rm -f "$errf"
|
||||
ap_die "Fix the launcher or the sandbox before running the pipeline: every unit would fail this way. (--no-preflight to skip the check.)"
|
||||
fi
|
||||
rm -f "$errf"
|
||||
echo " $out"
|
||||
if [[ "$out" == *"git=none"* ]]; then
|
||||
printf ' %s\n' "$got"
|
||||
if [[ "$out" != "$got" ]]; then
|
||||
ap_warn "the sandbox put lines of its own on stdout before answering. That stream is data: the agent"
|
||||
ap_warn "stream is parsed from it, and so is this check. Notices belong on stderr -- rebuild the"
|
||||
ap_warn "image if they read [aisb], or move any echo in your launcher there. Continuing: it answered."
|
||||
printf '%s\n' "$out" | awk -v max=4 '!/^aisb-ok/ { if (++n <= max) printf " > %s\n", $0 }' >&2
|
||||
fi
|
||||
if [[ "$got" == *"git=none"* ]]; then
|
||||
ap_warn "no git inside the sandbox -- the agent cannot read its own diff. Add it to the image (--install git) or expect blind fixer runs."
|
||||
fi
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user