fix: teardown-host refuses a terminal-less run instead of aborting mute

host/teardown-host.sh had no `[ -t 0 ]` check before its confirmation
prompt. Without --yes/BOX_YES and without a terminal — CI, a pipe, a
nohup — it fell into `read`, took the instant EOF and exited 1 saying
only "aborted": a refusal naming neither the cause nor the override, in
the most destructive script in the tree.

It now refuses with the override named, exit 2 — "you invoked this
wrong", matching host/revoke-user.sh --purge and install.sh's confirm(),
versus 1 for "you were asked and you said no".

The gate sits below the --yes/BOX_YES arm, so consent given
non-interactively still runs headless, and above the first incus call,
so the refusal needs no daemon — which is what lets test/cli.sh drive it
for real instead of grepping for it.

Refs #113
This commit is contained in:
dan-claude-bot 2026-07-19 23:36:32 +00:00
parent a9e52325f4
commit b3fa94a7d4
3 changed files with 32 additions and 0 deletions

View file

@ -154,6 +154,21 @@ which records not just what changed but what each drill run proved.
wrong default: it is the only thing to roll back *to*, at exactly the
moment that matters. No behaviour change.
- **`teardown-host.sh` refuses a terminal-less run instead of aborting mute**
(#113) — the most destructive script in the tree was the one that would not
tell a non-interactive caller how to proceed. With no `--yes`/`BOX_YES` and
no TTY it fell straight into `read`, took the instant EOF and exited 1
saying only `aborted` — a refusal that names neither the cause nor the
override. It now checks `[ -t 0 ]` and refuses with
`--yes (or BOX_YES=1) means yes.`, exit **2** — "you invoked this wrong",
the same contract and the same code as `host/revoke-user.sh --purge` and
`install.sh`'s `confirm()`, versus 1 for "you were asked and you said no".
The gate sits *below* the `--yes`/`BOX_YES` arm, so consent given
non-interactively still runs headless — CI's uninstall drill and
`box uninstall --purge-host --force` forward `--yes` and are unaffected —
and *above* the first `incus` call, so the refusal costs no daemon and
`test/cli.sh` drives it for real rather than grepping for it.
## 0.8.0 — 2026-07-19
### Added

View file

@ -28,6 +28,18 @@ $purge && echo "Incus itself will also be uninstalled (--purge-incus)."
if [ "$yes" -eq 1 ]; then
echo "(confirmed non-interactively: --yes/BOX_YES)"
else
# No terminal to ask on, and no consent given: refuse and say how to proceed,
# rather than fall into 'read', hit instant EOF and abort with nothing but
# "aborted" (#113). This must stay BELOW the --yes/BOX_YES arm above — the
# order is the contract: consent given non-interactively still runs headless
# (CI's uninstall drill and 'box uninstall --purge-host --force' depend on
# it), consent NOT given without a terminal is a usage error, exit 2, the
# same shape as host/revoke-user.sh and install.sh. It also lands before the
# first 'incus' call below, so the refusal needs no daemon.
if [ ! -t 0 ]; then
echo "teardown-host: refusing to run without a terminal to confirm on. --yes (or BOX_YES=1) means yes." >&2
exit 2
fi
# EOF (Ctrl-D) refuses, out loud: unguarded, errexit would end the run on
# this line and the 'aborted' below would never print (#111).
read -rp "Continue? [y/N] " a || { echo "aborted"; exit 1; }

View file

@ -1993,6 +1993,11 @@ check "teardown-host: honors --yes/BOX_YES (CI runs it unattended)" 0 "" \
grep -qF 'BOX_YES' "$ROOT/host/teardown-host.sh"
check "teardown-host: points at box uninstall when done" 0 "" \
grep -qF "box uninstall" "$ROOT/host/teardown-host.sh"
# ...and the other side of that contract (#113): consent NOT given and no
# terminal to ask on is a usage error, not a mute 'aborted'. Driven for real —
# the gate sits above the first 'incus' call, so a daemon-free run reaches it.
check "teardown-host: refuses without a TTY and names the override (#113)" 2 \
"--yes (or BOX_YES=1) means yes" bash "$ROOT/host/teardown-host.sh" </dev/null
# #102's race, in the one other file that sets pipefail. A daemon-free run
# cannot exercise a UFW teardown, so the shape is pinned instead: no `ufw