Merge pull request #59 from dan-claude-bot/fix/empty-users-file

fix(bootstrap): refuse a users file that names no users
This commit is contained in:
Daniel Marin 2026-07-19 20:46:12 +01:00 committed by GitHub
commit 71c749e818
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 95 additions and 7 deletions

View file

@ -104,6 +104,16 @@ on the way to cutting its first release, and this file starts there.
the socket, and rig falls back to removing the group itself, as it also
does where box is not installed. Every fallback path carries the session
warning, because the silence was the bug.
- **`rig bootstrap` refuses a users file that names no users** (#57) — an
empty, comments-only or whitespace-only file is not a parse error, so it
passed pre-flight, converged nothing, and left the box root-only: the exact
outcome `--no-users` exists to make explicit, reached by the flag added to
guarantee the opposite. Bootstrap's pre-flight now catches the zero-user
parse — before `apt`, the hostname change, or a spent pre-auth key — and
refuses, naming `--no-users` as the way to ask for a root-only box out loud.
Scoped to `rig bootstrap`'s contract only: a standalone `rig users apply`
against an emptied file is a real de-provisioning operation and is
unchanged.
## 0.1.0 — 2026-07-19

View file

@ -152,13 +152,20 @@ error too — rig will not silently pick a winner.
A bad users file is caught **up front**, in the same breath as a bad
`--class`: bootstrap pre-flights it with the same parser apply uses, before
`apt`, before the hostname change, and before a single-use pre-auth key is
spent. And on `host=yes` with `RIG_SKIP_BOX_INSTALL=1`, a box-role user with
no `incus` group refuses immediately rather than a hundred lines later —
that is the one case where the outcome is already certain, since the run has
been told it will not install box. rig still **never** installs Incus or runs
`box setup-host` on its own account; the box CLI's own installer does that
(see the `host` trait), and every other way that step can fail lands in
apply's existing refusal at the end.
spent. A file that names **no users** is refused there too — empty,
comments-only and whitespace-only files all parse fine, but bootstrapping
with one converges the same root-only box `--no-users` asks for, via the
flag that exists to guarantee the opposite. The refusal names `--no-users`,
because that box is reachable; it just has to be asked for out loud. This is
bootstrap's contract only: `rig users apply` against an emptied file is a
genuine de-provisioning operation and stays available. And on `host=yes`
with `RIG_SKIP_BOX_INSTALL=1`, a box-role user with no `incus` group refuses
immediately rather than a hundred lines later — that is the one case where
the outcome is already certain, since the run has been told it will not
install box. rig still **never** installs Incus or runs `box setup-host` on
its own account; the box CLI's own installer does that (see the `host`
trait), and every other way that step can fail lands in apply's existing
refusal at the end.
`--users` does **not** reach the box TENANT roles (`claude`, `codex`, `grok`,
`staging`). A tenant is a box-minted *guest*: box auto-runs its bootstrap at

View file

@ -227,6 +227,37 @@ if [ -n "$USERS_FILE" ]; then
if ! USERS_PARSED="$(parse_users_file "$USERS_FILE")"; then
die "invalid users file: $USERS_FILE — every error is listed above; nothing was changed" 2
fi
# A file that parses to ZERO users is not a parse error — empty, comments-only
# and whitespace-only files are all perfectly valid input, and the parser is
# right to accept them. But they walk straight through the requirement #51
# built: `--users ./empty-file` and `--no-users` converge the identical
# root-only box, and only one of them says so. The ambiguity the required flag
# exists to kill would survive in a narrower form — an operator who pointed at
# the wrong path and one who meant root-only would again produce indis-
# tinguishable boxes, which is precisely the state the flag was made to end.
#
# Refuse, and name --no-users: the outcome is reachable, it just has to be
# said out loud. That is the whole shape of #51's contract — both answers are
# available, neither is a side effect of what you did not type.
#
# The sharper reason this belongs at pre-flight rather than nowhere: against a
# box that ALREADY has operators, a truncated file does not converge nothing,
# it revokes every one of them. That is apply's correct and documented
# drop-semantics and it warns per user, so it is loud rather than silent — but
# a stray '>' is all it takes to produce that file, and every other failure
# mode on this command was deliberately made to fail before apt, the hostname
# change, or a spent pre-auth key. This one should not be the exception that
# fails after them.
#
# Deliberately scoped to BOOTSTRAP, not to the parser and not to apply. The
# lib stays a parser — "zero users is not allowed here" is bootstrap's policy,
# not a property of the file format — and a standalone `rig users apply`
# against an emptied file remains a real de-provisioning operation that must
# keep working. Bootstrap is where the claim "this box's people are these" is
# being made, so bootstrap is where an empty answer is a contradiction.
if [ -z "$USERS_PARSED" ]; then
die "users file names no users: $USERS_FILE parsed to zero operators (it is empty, or only comments and blank lines). Bootstrapping with it would converge a box only root can enter — the same outcome as --no-users, reached by the flag that exists to guarantee the opposite. Check the path, or pass --no-users to leave this box root-only deliberately" 2
fi
# Does anyone in the file carry role box? That single fact decides whether the
# incus precondition below applies at all — a users file naming only admins
# converges perfectly well on a host that has never seen Incus, and refusing

View file

@ -213,6 +213,46 @@ check "bootstrap: the invalid-file refusal carries the parser's own line error"
# ('--no-users' then apply by hand) named.
check "bootstrap: --users - is refused, naming the pre-auth key prompt" 2 "pre-auth key prompt" \
"$ROOT/commands/bootstrap.sh" workload --users -
# A file that parses to ZERO users (#57). Not a parse error — the parser is
# right to accept empty, comments-only and whitespace-only files — but it walks
# straight through #51's requirement: `--users ./empty` and `--no-users`
# converge the identical root-only box, and only one of them says so. All three
# shapes are tested separately because they take different paths through the
# parser's skip rules, and an implementation that checked, say, file size alone
# would pass one and fail the others.
: > "$BOOT_USERS/empty"
cat > "$BOOT_USERS/comments" <<'USERS'
# the operators for this box
#dan admin ssh-ed25519 AAAAC3fixture dan@laptop
USERS
printf ' \n\t\n\n' > "$BOOT_USERS/blank"
check "bootstrap: an empty users file exits 2" 2 "names no users" \
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/empty"
check "bootstrap: a comments-only users file exits 2" 2 "names no users" \
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/comments"
check "bootstrap: a whitespace-only users file exits 2" 2 "names no users" \
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/blank"
# The refusal must name --no-users, for the same reason the missing-flag one
# does: the root-only box IS reachable, it just has to be said out loud. An
# error that only reported "no users" would leave the operator who genuinely
# wants root-only with no named way to ask for it.
check "bootstrap: the zero-user refusal names --no-users as the way to say it" 2 "pass --no-users to leave this box root-only" \
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/empty"
# It must NOT over-refuse: a file that names even one operator passes pre-flight
# untouched. Reaching the root check (exit 1) is the proof — same idiom as the
# incus precondition's negative cases below.
if [ "$(id -u)" -ne 0 ]; then
check "bootstrap: a users file naming operators still passes pre-flight" 1 "must run as root" \
env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/ok"
fi
# Scope guard (#57): the refusal is BOOTSTRAP's contract, not the parser's and
# not apply's. A standalone `rig users apply` against an emptied file is a real
# de-provisioning operation and must stay possible — greps that find nothing
# (exit 1) are the pass, the repo's negative-law idiom.
check "users apply: an empty file is still a legal de-provisioning input" 1 "" \
grep -nE 'names no users' "$ROOT/commands/users-apply.sh"
check "users-config: zero users stays bootstrap policy, not a parser error" 1 "" \
grep -nE 'names no users' "$ROOT/commands/lib/users-config.sh"
# The host=yes box-role precondition, surfaced EARLY — but only where the
# outcome is already proven: RIG_SKIP_BOX_INSTALL=1 means this run will not
# install box, so a missing incus group can no longer be rescued by the install