diff --git a/CHANGELOG.md b/CHANGELOG.md index e5cc1dc..4ec42f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/host/teardown-host.sh b/host/teardown-host.sh index a3fdce0..3d6c978 100755 --- a/host/teardown-host.sh +++ b/host/teardown-host.sh @@ -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; } diff --git a/test/cli.sh b/test/cli.sh index c10154b..7983e43 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -7,6 +7,13 @@ # a fixture, or grep the load-bearing line so a deleted guard cannot ship green. # Deliberately no `set -e` — the harness asserts on failing commands. set -u +# BOX_YES is this family's documented automation switch, so an operator's CI +# wrapper may well export it. Checks that drive a destructive script for real +# would then take the CONSENT arm instead of the refusal they are asserting — +# turning this suite into `box uninstall --purge-host` on the host it runs on. +# Individual call sites use `env -u BOX_YES`; this is the belt to that braces, +# so the header's "runnable anywhere" promise cannot be broken by one export. +unset BOX_YES ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PASS=0 FAIL=0 @@ -1993,6 +2000,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" env -u BOX_YES bash "$ROOT/host/teardown-host.sh"