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
+20 -5
View File
@@ -349,13 +349,26 @@ sandbox-pi () {
export AI_API_TOKEN=$(pass aipi/dev-token)
ai-sandbox --allow aipi.reeseapps.com --allow github.com \
--allow pi.dev --allow release-assets.githubusercontent.com \
--env AI_API_TOKEN
--env AI_API_TOKEN "$@"
}
```
That last `"$@"` is the whole contract. A function launcher is called *with* the run: the
flags, then `--`, then the command to run inside, and one that ends at `--env AI_API_TOKEN`
drops all of it. It then starts the image's default harness with no prompt — and a `pi`
with no prompt on the other side of a launcher that attached your terminal sits waiting
for keystrokes while its output goes into a pipe nobody is watching. The driver names a
launcher like that in `--status` and refuses to start the pipeline; aliases and whole
command lines do not need it, because the shell appends the words to those by itself.
A script cannot see an interactive shell's functions, so the driver looks the name up in
`~/.bashrc.d/*`, `~/.bashrc`, `~/.bash_aliases` and `~/.profile` (or `AISB_AP_RC_FILES`)
and takes the *definition* — probing never calls it, so a preview cannot fetch a token.
Reading that definition is done in a throwaway session of its own, with no stdin and a
bounded read: an interactive bash started from your terminal takes your terminal, gets
stopped for it, and a stopped process ignores the timeout sent to it, so the probe used to
be able to wedge the whole script before it printed a single line.
Each child then runs exactly what your shell would have, `pass` included, once per unit
so an overnight run is not holding yesterday's token. An alias or a plain program works
too; `--pi-cmd` also accepts a whole command line.
@@ -367,10 +380,12 @@ ai-sandbox-autophase --dry-run # the exact child and gate commands
ai-sandbox-autophase # every pending task, in pipeline order
```
Every child is started as `<your command> --no-tty --no-interactive -- pi <args> <prompt>`:
your flags first, then the two that keep an unattended run off your terminal (no TTY to
corrupt the JSON stream, no stdin for your keystrokes to fall into), then `--`, after
which ai-sandbox reads nothing but the command.
Every child is started as `<your command> --no-tty --no-interactive -- pi <args> <prompt>`,
with its stdin on `/dev/null`: your flags first, then the two that keep an unattended run
off your terminal (no TTY to corrupt the JSON stream, no stdin for your keystrokes to fall
into), then `--`, after which ai-sandbox reads nothing but the command. Those two are
flags, and a launcher is free to lose them, so the `/dev/null` is what actually guarantees
the second one.
| flag | |
|---|---|