forked from heavy-duty/rig
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:
commit
71c749e818
4 changed files with 95 additions and 7 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
21
README.md
21
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
40
test/cli.sh
40
test/cli.sh
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue