fix: the unattended rehearsal consents explicitly to the restore gate

CI's multi-user rehearsal drives 'box restore' with no terminal, so the
new gate correctly refused it. --force there is the drill proving the
gate is real. README says restore asks, and that scripts must pass it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
dan-claude-bot 2026-07-19 19:46:06 +00:00
parent 12713c5012
commit b38518ee54
4 changed files with 23 additions and 4 deletions

View file

@ -59,7 +59,11 @@ which records not just what changed but what each drill run proved.
one verb that already worked would be a regression shipped as a one verb that already worked would be a regression shipped as a
refactor. A row marked `confirm` with no words is now a hard internal refactor. A row marked `confirm` with no words is now a hard internal
error rather than a blank question. `--force` and the no-TTY refusal come error rather than a blank question. `--force` and the no-TTY refusal come
free — `confirm()` already had both. Coverage went from two free — `confirm()` already had both. The one automated caller had to
consent explicitly: `drill/multiuser.sh` drives restore unattended on real
Incus and now passes `--force`, which is the rehearsal proving the gate
rather than working around it — the CI run of this very PR failed there
first, which is the shape a gate is supposed to have. Coverage went from two
argument-validation checks that never reached dispatch to the destructive argument-validation checks that never reached dispatch to the destructive
path itself, driven against a fake incus: refusing leaves the call log path itself, driven against a fake incus: refusing leaves the call log
empty, `--force` produces exactly one `incus snapshot restore`. Not empty, `--force` produces exactly one `incus snapshot restore`. Not

View file

@ -320,7 +320,10 @@ box new --name feature --from work/authed # clone the authed state into a new
`--from` copies the whole box (agent login, git creds, clones and all) while `--from` copies the whole box (agent login, git creds, clones and all) while
preserving isolation. You can also `box new --name x --from work` to clone preserving isolation. You can also `box new --name x --from work` to clone
a box's live state, or roll a box back with `box restore work authed`. a box's live state, or roll a box back with `box restore work authed` — which
asks first, since a rollback discards everything since the snapshot (`--force`
skips the prompt, and scripts must pass it: with no terminal to ask on, box
refuses rather than assuming yes).
Forgotten what you called a checkpoint? `box info work` prints the box's Forgotten what you called a checkpoint? `box info work` prints the box's
snapshot labels and the `--from` line to clone one. snapshot labels and the `--from` line to clone one.
@ -388,7 +391,8 @@ box shell <box> # enter as the template's user
box exec <box> -- <cmd...> # run a command in the box box exec <box> -- <cmd...> # run a command in the box
box tmux <box> [session] # attach/create a tmux session — survives disconnects box tmux <box> [session] # attach/create a tmux session — survives disconnects
box snapshot <box> [label] # checkpoint (label defaults to manual-<epoch>) box snapshot <box> [label] # checkpoint (label defaults to manual-<epoch>)
box restore <box> <snap> # roll back to a snapshot box restore <box> <snap> [--force]
# roll back to a snapshot — destructive, asks first
box export <box> [<file>] [--instance-only] box export <box> [<file>] [--instance-only]
# one portable file (snapshots incl.) — survives rm & host # one portable file (snapshots incl.) — survives rm & host
box import <file> [--name <box>] box import <file> [--name <box>]

View file

@ -206,7 +206,13 @@ as_u "$U1" box info mine 2>/dev/null | grep -qF "$(boxnet_pfx)" \
&& ok "(g) box info shows a boxnet ($(boxnet_pfx)x) address — placed on the hardened network" \ && ok "(g) box info shows a boxnet ($(boxnet_pfx)x) address — placed on the hardened network" \
|| no "(g) mine has no boxnet address in box info" || no "(g) mine has no boxnet address in box info"
as_u "$U1" box snapshot mine s1 >/dev/null 2>&1 && ok "(b) box snapshot mine s1" || no "(b) snapshot refused" as_u "$U1" box snapshot mine s1 >/dev/null 2>&1 && ok "(b) box snapshot mine s1" || no "(b) snapshot refused"
as_u "$U1" box restore mine s1 >/dev/null 2>&1 && ok "(b) box restore mine s1 (the incus 6 'snapshot restore' spelling)" || no "(b) restore failed" # --force, and it is the assertion as much as the fix: restore is destructive
# and now asks (#105), so an unattended drill MUST consent explicitly. Without
# the flag this line fails — correctly — because confirm() refuses rather than
# assuming yes when there is no terminal to ask on. Adding --force here is not
# working around the gate; it is the drill proving the gate is real, and that
# the documented non-interactive path through it works on real Incus.
as_u "$U1" box restore mine s1 --force >/dev/null 2>&1 && ok "(b) box restore mine s1 --force (the incus 6 'snapshot restore' spelling, through the #105 gate)" || no "(b) restore failed"
as_u "$U1" box new --name c1 --from mine/s1 >/dev/null 2>&1 && ok "(b) box new --from mine/s1 — the clone workflow" || no "(b) clone failed" as_u "$U1" box new --name c1 --from mine/s1 >/dev/null 2>&1 && ok "(b) box new --from mine/s1 — the clone workflow" || no "(b) clone failed"
# c1 stays alive through phase g: it is the distinctly-NAMED sibling the # c1 stays alive through phase g: it is the distinctly-NAMED sibling the
# enumeration probe needs (both users' primaries are 'mine' by design of d). # enumeration probe needs (both users' primaries are 'mine' by design of d).

View file

@ -877,6 +877,11 @@ check "table: every 'confirm' row supplies a prompt (#105)" 0 "" \
# shellcheck disable=SC2016 # the $-strings are literals in the target file # shellcheck disable=SC2016 # the $-strings are literals in the target file
check "dispatch: the confirm prompt comes from the row, not a constant (#105)" 0 "" \ check "dispatch: the confirm prompt comes from the row, not a constant (#105)" 0 "" \
grep -qF 'confirm "$(fill "$cnf" "$inst")"' "$ROOT/bin/box" grep -qF 'confirm "$(fill "$cnf" "$inst")"' "$ROOT/bin/box"
# The rehearsal drives restore unattended on real Incus, so it must consent
# EXPLICITLY — the gate is only real if the one automated caller had to change.
# Pinned here because the rehearsal itself needs a daemon and this suite has none.
check "rehearsal: the unattended restore passes --force (#105)" 0 "" \
grep -qF 'box restore mine s1 --force' "$ROOT/drill/multiuser.sh"
rm -rf "$CSHIM" "$CWORK" rm -rf "$CSHIM" "$CWORK"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------