stop the driver hanging on a terminal and on a launcher

ai-sandbox-autophase could sit forever, printing nothing, in two places
that are both before the first unit -- which is why both looked like a
script that "does nothing".

The rc probe ran `timeout 15 bash -ic` in front of the user's terminal.
An interactive bash calls tcsetpgrp() to take that terminal, is in its
background, and is stopped for trying; a stopped process does not take
SIGTERM, so the timeout waited in front of it and the driver waited on
the command substitution behind it. It now runs in its own session
(setsid), with stdin closed, under `timeout -k`, and reads the
definition from a temp file rather than a pipe, because a grandchild an
rc file leaves behind holds a pipe's EOF open long after the shell it
came from has been reaped.

A launcher is called *with* the run, and a function ending at
`--env NAME` -- the shape everyone writes for typing by hand, including
every example this repo printed -- discards the flags and the
in-container command. The container then starts the image's default
harness with no prompt and, with --no-interactive gone, the terminal
attached: an agent waiting for keystrokes behind a pipe nobody reads.
`ap_forwards_args` checks the captured definition, --status and the
preflight name it with the one-line fix, and every child now starts with
stdin on /dev/null so a launcher cannot put the sandbox back on the
keyboard. Aliases and whole command lines need no forwarding: the shell
appends the words to those.

Tests: a launcher fixture that drops its arguments, one that leaves a
process behind, and shape assertions on ap_probe_rc.

LLM-Generated: true
This commit is contained in:
2026-10-29 21:07:39 -04:00
parent 209f167e43
commit a91744f390
8 changed files with 273 additions and 22 deletions
+11 -2
View File
@@ -171,8 +171,17 @@ A script cannot see an interactive shell's functions, so `sandbox-pi` (default,
`--pi-cmd`/`AISB_AP_PI_CMD`) is looked up in `~/.bashrc.d/*`, `~/.bashrc`,
`~/.bash_aliases`, `~/.profile`: the rc file is sourced in a throwaway `bash -ic`
with `exit` neutered and the *definition* returned -- never called, which is what lets
`--status`/`--dry-run` promise no token fetch, no image build, no container. Each child
then re-sources and runs it, so `pass` runs per unit rather than once a night.
`--status`/`--dry-run` promise no token fetch, no image build, no container. That probe
is a `setsid`, `</dev/null`, `timeout -k` read into a **file**: an interactive bash
started from a terminal takes that terminal, is stopped for trying, and a stopped process
does not take the SIGTERM meant for it, so a piped probe could wedge the whole script in
silence -- and any grandchild the rc files leave behind holds a pipe's EOF open long
after the shell it came from is gone. A function launcher is called *with* the run
(`NAME --no-tty --no-interactive -- pi <args> <prompt>`, stdin on `/dev/null`), so it has
to forward its arguments; one that ends at `--env NAME` drops the command, and is named
by `--status` and refused by the preflight rather than running the image default with no
prompt. Each child then re-sources and runs it, so `pass` runs per unit rather than once
a night.
Integration is by shadowing five functions of the sourced library (`pi`,
`first_prompt`, `fix_prompt`, `run_validation`, `push_phase`) behind a `declare -F`