fix: headless credential prompts refuse loudly, naming their variable (#42)

A bare 'read -rsp' with no tty exits non-zero and set -e ends the script
with no output at all: the release drill watched 'rig runner remove' exit 1
in complete silence, and a guest bootstrap stop mid-log the same way. Every
prompt now checks for a tty first and dies naming the variable that
unblocks an unattended run; every read is || die-guarded so EOF at a real
prompt also gets a last word. The no-bare-read test swept up runner
repoint's two prompts, which the issue had not counted.

Fixes #42

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
dan-claude-bot 2026-07-19 12:15:08 +00:00
parent fb0c3067e3
commit e72663ef62
6 changed files with 60 additions and 6 deletions

View file

@ -6,6 +6,22 @@ on the way to cutting its first release, and this file starts there.
## Unreleased
### Fixed
- **Headless credential prompts refuse loudly instead of dying silently**
(#42) — the interactive credential prompts (`TS_AUTHKEY` in `bootstrap`,
`RUNNER_TOKEN` in `runner install`, `RUNNER_REMOVE_TOKEN` in
`runner remove`, and both tokens in `runner repoint` — a site the new
no-bare-read test caught after the issue counted three) were bare
`read -rsp`: with stdin not a tty (CI,
`box exec`, any script), `read` fails, `set -e` ends the run, and the
log just *stops* — exit 1, no last word, measured live in the
2026-07-19 release drill. Each prompt now checks for a tty first and
dies naming the variable that unblocks an unattended run (`runner
remove` also names `--local`), and every `read` is `|| die`-guarded so
EOF at a real prompt gets the same courtesy. `db.sh` already held the
line here; now all of rig does.
### Added
- **Tagged releases, and an installer that installs them** (#32) — the rig

View file

@ -396,9 +396,15 @@ elif [ "$JOIN" = "login" ]; then
tailscale up --hostname="$TS_HOSTNAME"
verify_user_owned back-out
else
# env override, else prompt; never touches disk
# env override, else prompt; never touches disk. The prompt only fires on a
# tty: with no terminal, a bare `read` exits non-zero and `set -e` would end
# the whole bootstrap with NO last word — the 2026-07-19 drill met exactly
# that, a log that stops mid-converge with exit 1 and nothing to grep. An
# unattended run gets the same refusal every other guard gives: loud, and
# naming the variable that unblocks it.
if [ -z "${TS_AUTHKEY:-}" ]; then
read -rsp "tailscale pre-auth key (single-use, tagged, <=1h expiry): " TS_AUTHKEY
[ -t 0 ] || die "TS_AUTHKEY is unset and stdin is not a tty — set TS_AUTHKEY to run unattended"
read -rsp "tailscale pre-auth key (single-use, tagged, <=1h expiry): " TS_AUTHKEY || { echo; die "no pre-auth key read (EOF) — set TS_AUTHKEY to run unattended"; }
echo
fi
[ -n "${TS_AUTHKEY:-}" ] || die "empty pre-auth key"

View file

@ -120,8 +120,11 @@ fi
# --- registration token — only when registration is actually pending -------
if [ "$REG_PENDING" -eq 1 ]; then
RUNNER_TOKEN="${RUNNER_TOKEN:-}"
# Prompt only on a tty: headless, a bare `read` dies under set -e with no
# message at all (drill-measured). Refuse loudly, naming the variable.
if [ -z "$RUNNER_TOKEN" ]; then
read -rsp "runner registration token (short-lived): " RUNNER_TOKEN
[ -t 0 ] || die "RUNNER_TOKEN is unset and stdin is not a tty — set RUNNER_TOKEN to run unattended"
read -rsp "runner registration token (short-lived): " RUNNER_TOKEN || { echo; die "no registration token read (EOF) — set RUNNER_TOKEN to run unattended"; }
echo
fi
[ -n "$RUNNER_TOKEN" ] || die "empty registration token"

View file

@ -70,8 +70,12 @@ fi
REMOVE_TOKEN=""
if [ -e "$RUNNER_DIR/.runner" ] && [ "$LOCAL" -eq 0 ]; then
REMOVE_TOKEN="${RUNNER_REMOVE_TOKEN:-}"
# Prompt only on a tty: headless, a bare `read` dies under set -e with no
# message at all — the drill hit exactly this (wrong env var, exit 1, zero
# output). Refuse loudly, naming both the variable and the tokenless out.
if [ -z "$REMOVE_TOKEN" ]; then
read -rsp "runner removal token (short-lived): " REMOVE_TOKEN
[ -t 0 ] || die "RUNNER_REMOVE_TOKEN is unset and stdin is not a tty — set RUNNER_REMOVE_TOKEN to run unattended, or use --local"
read -rsp "runner removal token (short-lived): " REMOVE_TOKEN || { echo; die "no removal token read (EOF) — set RUNNER_REMOVE_TOKEN to run unattended, or use --local"; }
echo
fi
[ -n "$REMOVE_TOKEN" ] || die "empty removal token"

View file

@ -128,8 +128,11 @@ log "labels: ${LABELS}"
# while the runner is still registered and working.
if [ "$LOCAL" -eq 0 ]; then
RUNNER_REMOVE_TOKEN="${RUNNER_REMOVE_TOKEN:-}"
# Prompt only on a tty — headless, a bare `read` dies under set -e with no
# message (issue #42; same cure as runner-install/remove).
if [ -z "$RUNNER_REMOVE_TOKEN" ]; then
read -rsp "removal token for ${CURRENT_URL} (short-lived): " RUNNER_REMOVE_TOKEN
[ -t 0 ] || die "RUNNER_REMOVE_TOKEN is unset and stdin is not a tty — set RUNNER_REMOVE_TOKEN to run unattended, or use --local"
read -rsp "removal token for ${CURRENT_URL} (short-lived): " RUNNER_REMOVE_TOKEN || { echo; die "no removal token read (EOF) — set RUNNER_REMOVE_TOKEN to run unattended, or use --local"; }
echo
fi
[ -n "$RUNNER_REMOVE_TOKEN" ] || die "empty removal token"
@ -138,7 +141,8 @@ fi
RUNNER_TOKEN="${RUNNER_TOKEN:-}"
if [ -z "$RUNNER_TOKEN" ]; then
read -rsp "registration token for ${TARGET_URL} (short-lived): " RUNNER_TOKEN
[ -t 0 ] || die "RUNNER_TOKEN is unset and stdin is not a tty — set RUNNER_TOKEN to run unattended"
read -rsp "registration token for ${TARGET_URL} (short-lived): " RUNNER_TOKEN || { echo; die "no registration token read (EOF) — set RUNNER_TOKEN to run unattended"; }
echo
fi
[ -n "$RUNNER_TOKEN" ] || die "empty registration token"

View file

@ -461,6 +461,27 @@ fi
check "runner: bad subcommand exits 2" 2 "usage:" "$ROOT/bin/rig" runner frobnicate
# --- headless prompts refuse loudly (issue #42) ------------------------------
# The three credential prompts (TS_AUTHKEY, RUNNER_TOKEN, RUNNER_REMOVE_TOKEN)
# used to be bare `read -rsp`: with no tty, read exits non-zero and `set -e`
# ends the script with NO output at all — the drill watched a bootstrap die
# mid-converge with exit 1 and nothing to grep. Each prompt now refuses first,
# naming its variable. The prompts live behind the root check (and, for the
# runner pair, behind a real registration), so the harness cannot reach them
# non-root; grep the guards so a deleted one cannot ship green (repo
# precedent: the login-path tag refusals above).
check "bootstrap: headless TS_AUTHKEY prompt refuses loudly" 0 "" \
grep -q 'TS_AUTHKEY is unset and stdin is not a tty' "$ROOT/commands/bootstrap.sh"
check "runner install: headless token prompt refuses loudly" 0 "" \
grep -q 'RUNNER_TOKEN is unset and stdin is not a tty' "$ROOT/commands/runner-install.sh"
check "runner remove: headless token prompt refuses loudly" 0 "" \
grep -q 'RUNNER_REMOVE_TOKEN is unset and stdin is not a tty' "$ROOT/commands/runner-remove.sh"
# The EOF-at-the-prompt path (Ctrl-D on a real tty) must also die with a last
# word rather than ride set -e into silence: every read is `|| die`-guarded,
# so a bare `read -rsp` (no `||` on its line) must not exist anywhere.
check "prompts: no bare read -rsp remains" 1 "" \
grep -RE 'read -rsp[^|]*$' "$ROOT/commands/"
# --- runner install: --repo must agree with what the box is already on -------
# The bug: `install --repo B` on a box registered to repo A skipped configure,
# restarted the service on A, and reported success — --repo accepted, validated,