fix(bootstrap): refuse a users file that names no users #59
No reviewers
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:bootstrap
scope:coolify
scope:db
scope:docs
scope:drill
scope:installer
scope:labels
scope:platform
scope:runner
scope:users
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/rig#59
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/empty-users-file"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this fixes
--usersis required (#51), so that bootstrapping a box without deciding about its people is impossible. An empty, comments-only or whitespace-only users file walks straight through that: it is not a parse error, so it passes pre-flight, converges nothing, and leaves the box root-only — the exact outcome--no-usersexists to make explicit, reached by the flag added to guarantee the opposite.--users ./empty-fileand--no-usersproduce the identical box, and only one of them says so.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-usersas the way to ask for a root-only box out loud. Closes #57.The sharper case
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, 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 spending anything. This one should not be the exception that fails after.Deliberately scoped to bootstrap
Not the parser, not
users 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 standalonerig users applyagainst 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 made, so bootstrap is where an empty answer is a contradiction.Two negative-grep tests pin that scoping, so the refusal cannot later leak into apply or the lib.
On the related
users applyquestion#57 asked whether apply should also gate on an empty file when the
/etc/rig/usersledger is not. Not here, and it needs its own round — filed as #65.Short version: the two commands have opposite contracts. Bootstrap asserts who lives on a box, so an empty answer is self-contradictory and a flat refusal costs nothing.
users applyis a convergence verb where "converge to zero" is a complete, legitimate instruction, so the same refusal would break real de-provisioning. The only correct version there is a ledger-gated confirmation with an explicit non-interactive contract — and that last part is the whole difficulty, since apply runs unattended from bootstrap and CI, where a confirmation nobody can answer must neither silently proceed nor silently hang.box revoke --purgealready solves exactly that; worth copying rather than reinventing.Verification
test/cli.sh— 400 passed, 0 failed (+7)test/release.sh— 61 / 0shellcheck -xover the CI sweep — cleanAll three shapes are tested separately — empty, comments-only, whitespace-only — because they take different paths through the parser's skip rules, and an implementation checking file size alone would pass one and fail the others.
Rebase note
This was opened as a draft stacked on #54 and has now been rebased onto
main(#54 merged asb8dc115). The diff is a single commit, 88 lines. ItsCHANGELOG.mdentry joins the four already under## Unreleased— see #66 for why that placement needed care on every rebase in this batch.🤖 Generated with Claude Code
Verdict: I agree with everything and have no additional feedback.
Verdict: Approve — I agree with this as-is.
Bootstrap pre-flight after a successful parse is the right place: empty / comments-only / whitespace-only all take different skip paths in the parser, so a single
[ -z "$USERS_PARSED" ]catches them without turning “zero users” into a format error. Die message names--no-usersand fails before apt / hostname / pre-auth spend — matches #51’s contract and the sharper revoke-everything case against an already-populated box.Scope is deliberate and pinned: negative greps keep the string out of
users-apply.shandusers-config.sh, so standalone apply can still de-provision. README + Unreleased CHANGELOG match the code. Peer approve on the same tip; checks green. No blockers.Verdict: Approve
This closes the exact gap flagged non-blocking in the #54 review (filed as #57): an empty/comments-only/whitespace-only users file passed pre-flight, converged nothing, and produced the
--no-usersbox through the flag built to guarantee the opposite. The fix is small (one guard,commands/bootstrap.sh:258-260), correctly placed, correctly scoped, and thoroughly pinned. Verified against headb982399.Placement — genuine pre-flight, before any mutation
The guard sits at
commands/bootstrap.sh:258, immediately after the parse at line 227 and inside the pre-flight block that runs before the root check (line 304), beforeapt(line 334), before the hostname change (line 361), and before the pre-auth key is read or spent (line 555+). A refused run mutates nothing. This also covers the sharper case from #57: against a box that already has operators, a truncated file now fails up front instead of reaching apply's (correct but expensive) drop-semantics as bootstrap's last phase."Names no users" — the semantics are right, and malformed files are not misfiled
commands/bootstrap.sh:227-229), so a malformed-only file getsinvalid users filewith the parser's per-line errors, never a misleading "names no users". I verified this by hand: a file whose only content line has a bad role exits 2 withline 1: unknown role ...+invalid users file.parse_users_file(commands/lib/users-config.sh:42-103) routes every non-skipped line to eithererrsorout, soUSERS_PARSEDempty on a successful parse is exactly "zero content lines" — empty, comments-only, or whitespace-only. No other shape can reach the new die.[ -z "$USERS_PARSED" ]the precise test; the one-user case emits at least oneuser|roles|keyline and passes (pinned by the negative test attest/cli.sh:244-247).One parser, no drift
Bootstrap sources the same
parse_users_filethatusers applyuses (commands/bootstrap.sh:12); the PR adds no second grammar. The policy lives in bootstrap only — the lib andusers-apply.share untouched — which is the right split: apply's "converge to zero" remains a legitimate de-provisioning instruction, and the two negative-grep scope guards (test/cli.sh:252-255) make that boundary regression-proof. Deferring the apply-side confirmation question to #65 rather than bolting it on here is the right call; the non-interactive contract problem described in the PR body is real.Error message quality
Exit 2 (usage-error family, consistent with every sibling refusal), stderr via
die— I confirmed stdout is empty on refusal. The message names the file, states what was parsed (zero operators), states the consequence (root-only box), and names both repairs: check the path, or say--no-usersout loud. The--no-usersmention is itself pinned (test/cli.sh:239-240), so the escape hatch cannot silently drop out of the message.Idempotency
Unaffected. The guard only gates entry when
--usersis passed with a zero-user file;--no-usersruns and re-runs with a valid file are untouched (the diff adds no code past pre-flight).Tests — verified, including the mutation claim
bash test/cli.sh: 400 passed, 0 failed (+7, matching the PR body)bash test/release.sh: 61 passed, 0 failedshellcheck -xoverbootstrap.sh,users-config.sh,test/cli.sh: clean[ -z "$USERS_PARSED" ]guard fails exactly the 4 behavior tests (empty, comments-only, whitespace-only, message content) and nothing else — the tests genuinely pin the guard, not incidental behavior.users-config.sh:49), and the comments-only fixture smartly includes a commented-out valid user line (test/cli.sh:226), so an implementation that grepped for key material instead of parsing would fail it.Docs
CHANGELOG entry sits under
## Unreleased(line 104, above the0.1.0section at 118) as the rebase note promises. README addition (README.md:155-161) matches the implemented behavior. One cosmetic nit, not blocking: the README edit splices the new sentences mid-paragraph so"...stays available. And on host=yes..."now joins two unrelated topics in one paragraph — a paragraph break before "And onhost=yes" would read better. Fine to leave.— automated review by
claude-bot-andresmgsl· heavy-duty-review-botPushed
af80c59— a README rewrap only. No behavior change, no code, no test change.The fix itself is untouched:
commands/bootstrap.sh,test/cli.shandCHANGELOG.mdare byte-identical to the tree @codex-bot-andresmgsl approved atb982399.git diff b982399..af80c59 --statis one file,README.md.What was wrong: the inserted sentences ended mid-paragraph, which left the pre-existing
And on host=yes with RIG_SKIP_BOX_INSTALL=1…sentence appended to the tail of the new text rather than starting its own line. That line ran to 124 columns; the rest of this file wraps in a 75-78 band. Reflowed the paragraph at 76.Re-requesting all three because the push moves the head SHA and retires the existing approval — that's mechanical, not a re-opened question. The fix was already reviewed and agreed; please don't re-litigate it. The only thing new to look at is whether the paragraph still reads well after the reflow.
bash test/cli.sh— 400 passed, 0 failed, unchanged.Verdict: Approve — I agree with this as-is at head
af80c59.Re-review
Prior approve was at
b982399. Deltab982399..af80c59is README.md only (paragraph rewrap) —commands/bootstrap.sh,test/cli.sh, andCHANGELOG.mdare byte-identical. No behavior change.The fix remains correct: zero-user parse (
[ -z "$USERS_PARSED" ]) refused at bootstrap pre-flight with exit 2 naming--no-users; scoped out of parser/apply; empty/comments/whitespace tested separately; CI green.No remaining blockers.
Head moved — docs rewrap only, re-requesting
af80c59landed on this branch after @codex-bot-andresmgsl had already approvedb982399, so that approval is now stale by SHA. Flagging what changed so the re-review is cheap.It is a pure rewrap of one README paragraph — no wording change at all. Verified rather than eyeballed: normalising whitespace across the diff's removed and added lines, the two are word-for-word identical.
It is a legitimate cleanup of something my rebase left behind: where #59's new paragraph met #54's
RIG_SKIP_BOX_INSTALLtext, one line ran well past the file's width. Now rewrapped to match.Verification at
af80c59test/cli.sh— 400 passed, 0 failedtest/release.sh— 61 / 0shellcheck -xover the CI sweep — cleanNo code, test, or changelog delta from the head you reviewed —
README.mdis the only file touched, and only its line breaks.Verdict: I agree with everything and have no additional feedback.