10 KiB
rig bootstrap staging Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a staging bootstrap role alongside control-plane / workload /
runner — the host archetype for a machine whose job is to host staging
VMs: Incus VMs minted by the box CLI (heavy-duty/box) from its staging
template, each later converged from inside with rig bootstrap workload and
registered in the control plane as its own server.
Why this shape: the host and its guests sit on opposite sides of a trust
boundary. The guests are servers the control plane manages; the host is
not — it must never carry tag:server, and the fleet has already been bitten
once by a host that wrongly did. The role is deliberately minimal host
plumbing: hardening, tailnet join (key minted with tag:local), hostname —
and nothing about Incus or box. box's own setup-host owns Incus
configuration; two tools converging the same daemon is drift by construction,
so rig only points at box's installer in its closing log. The guest side
needs zero rig changes — rig bootstrap workload already is the staging-box
role.
Architecture: staging joins the existing role case in
commands/bootstrap.sh; no new files, no new flags. Since issue #16 / PR #20,
the tailnet tag is not a rig argument — the pre-auth key carries its tags
and rig asserts on the tag control actually granted (.Self.Tags), post-join
and on every re-run. So the role's tag policy lands in verify_effective_tag,
exactly where the runner policy lives: role staging refuses an effective
tag:server (die, exit 1 — a runtime refusal, not a usage error). The
/dev/kvm advisory and the box next-step pointer live in the execution path;
argument validation stays pure and root-free.
Tech Stack: bash only, shellcheck, existing ci.yml (globstar shellcheck +
bash test/cli.sh) — no workflow change needed.
Non-Goals
- No Incus, no box install — box's
setup-hostis the single owner of the Incus daemon's configuration. rig prints a pointer, nothing more. - No VM provisioning — minting boxes is box's job (
box new --template staging, companion issue heavy-duty/box#68). - No control-plane/Coolify API usage — guests register themselves via the existing workload flow.
- No
devrole — a dev-box host role is anticipated (same plumbing, honest name) but explicitly out of scope here.
Global Constraints
#!/usr/bin/env bash+set -euo pipefail; log prefixrig-bootstrap:via the existinglog/warn/diehelpers.- Exit codes:
2= usage/argument error,1= runtime refusal. All argument validation runs BEFORE the root check so error paths are testable as non-root. - The tag policy asserts the effective tag, never a requested one — there
is no
--ts-tagto refuse anymore (it died in PR #20; passing it exits 2 with a pointer at the key). The issue's original "refuse--ts-tag tag:server" acceptance is therefore satisfied at the stronger, post-join layer, same asrunner. /dev/kvmabsence is a warning, not a failure — the role is rehearsed in containers where/dev/kvmlegitimately isn't there.- Convergent: a second run changes nothing and exits 0.
- shellcheck-clean exactly as CI runs it (
shopt -s globstar; shellcheck -x bin/* **/*.sh);bash test/cli.shgreen as non-root. - Keep the diff minimal — no drive-by refactors. (One deliberate exception:
bin/rig's bootstrap usage line still advertises the removed--ts-tagflag and the oldtag:cidefault — stale since PR #20. It gets corrected in the same breath as addingstagingto the role list, because shipping a new role into a help text that lies about the flag surface would be worse than the drive-by.)
Task 1: role wiring in commands/bootstrap.sh + dispatcher usage + tests
Files:
- Modify:
commands/bootstrap.sh(role case, effective-tag refusal,/dev/kvmadvisory, closing next-step log, usage heredoc) - Modify:
bin/rig(bootstrap usage line: role list + stale-flag correction) - Modify:
test/cli.sh(bootstrap section additions)
Behavior contract, in file order:
- Usage heredoc: role list becomes
<control-plane|workload|runner|staging>; one added sentence: staging hosts box-minted staging VMs, its key should be minted withtag:local, and it refusestag:server— the host is never managed by the control plane; its guest VMs are. - Role case arm:
control-plane|workload|runner|staging) shift ;;and both error messages (role required,unknown role) name the four roles. verify_effective_tag: after therunnerrefusal, thestagingone — same shape (grep -qx 'tag:server'against the effective tags), messagerole staging joined with tag:server ...naming the repair (mint atag:localkey), rationale comment: hosts are never managed by the control plane, their guest VMs are; the fleet has been bitten by a host wrongly carryingtag:server.diewith default status → exit 1.- Guards section (execution path, after the root check): when role is
stagingand/dev/kvmis absent,warn— the host exists to run VMs, but a container rehearsal legitimately has no/dev/kvm, so this must not fail. - Closing log:
stagingbranch pointing at the box CLI — install box, runbox setup-hostto prepare Incus, thenbox new --template staging. bin/rigusage:bootstrap <control-plane|workload|runner|staging> [--hostname <name>]; drop the stale[--ts-tag <tag>]andtag:ci-default sentence; say the tag comes from the pre-auth key and that rolesrunnerandstagingrefusetag:server.
- Step 1: Append failing tests
In test/cli.sh, bootstrap section:
check "bootstrap: staging + removed --ts-tag exits 2" 2 "comes from the pre-auth key" \
"$ROOT/commands/bootstrap.sh" staging --ts-tag tag:server
# The staging tag:server refusal rides the EFFECTIVE tag, inside
# verify_effective_tag — a path that needs a real tailnet, so it belongs to the
# rehearsal. What the harness CAN prove is that the refusal exists in the shipped
# script: grep the die message, so a deleted guard cannot ship green (the same
# reason the runner-install repo guard is grepped below).
check "bootstrap: staging effective-tag refusal is present" 0 "" \
grep -q "role staging joined with tag:server" "$ROOT/commands/bootstrap.sh"
and in the existing non-root block:
check "bootstrap: staging role parses, refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" staging
The existing unknown role exits 2 (potato) check already covers the
still-fails-usage path and stays untouched.
- Step 2: Run tests to verify the new ones fail
Run: bash test/cli.sh
Expected: the staging checks FAIL (unknown role: staging → wrong exit/output
for the first and third; missing die message for the grep); everything existing
stays green; harness exits 1.
- Step 3: Implement the role
Per the behavior contract above.
- Step 4: Run tests to verify they pass
Run: bash test/cli.sh
Expected: all checks pass, exit 0.
- Step 5: shellcheck + syntax
Run: shopt -s globstar; shellcheck -x bin/* **/*.sh (exactly CI's
invocation) and bash -n on each edited script.
Expected: exit 0, no findings.
- Step 6: Commit
git add commands/bootstrap.sh bin/rig test/cli.sh
git commit -m "feat(bootstrap): staging role — the host archetype for box-minted staging VMs"
Task 2: README roles documentation
Files:
-
Modify:
README.md(bootstrap section: heading role list, example block, the roles paragraph) -
Step 1: Write it
Content requirements (in the README's existing voice):
-
Heading/example gain
staging(rig bootstrap staging --hostname my-vm-host). -
One honest paragraph in the roles discussion:
stagingis the box that hosts staging boxes — Incus VMs minted by theboxCLI, each converged from inside withrig bootstrap workloadand registered in the control plane as its own server. Mint its key withtag:local; the role refuses an effectivetag:server— the host is never managed by the control plane, its guests are. rig deliberately installs no Incus and no box (box'ssetup-hostowns that); it points there when done. -
Step 2: Full local gate
Run: CI's shellcheck invocation + bash test/cli.sh.
Expected: silent shellcheck; all tests pass.
- Step 3: Commit
git add README.md
git commit -m "docs: README section for the staging bootstrap role"
Test Plan
- Harness (
bash test/cli.sh, non-root, network-free): staging parses and reaches the root check (exit 1must run as root); staging + the removed--ts-tagdies at arg validation (exit 2, message points at the key — proving validation precedes the root check); the effective-tag refusal message is present in the script; unknown roles still exit 2. - CI: unchanged
ci.ymlcovers the edits (globstar shellcheck + harness). - Rehearsal (manual, out of harness): pristine Debian box →
rig bootstrap stagingwith a real single-usetag:localkey → hardened sshd drop-in, tailnet join, hostnamestaging,/dev/kvmwarning absent on real hardware, closing log points at box; second run is a no-op. Atag:serverkey must die post-join with the staging refusal.
Addendum (2026-07-17, written before implementation)
Issue #22 predates the merge of PR #20 (issue #16: the tag comes from the key).
Its acceptance criterion "rig bootstrap staging --ts-tag tag:server exits 1
with a refusal, before the root check" names a flag that no longer exists —
--ts-tag now dies (exit 2) for every role, before the root check, pointing at
the key. The staging tag:server policy therefore lands where the runner's
did: on the effective tag in verify_effective_tag, exit 1, which is the
strictly stronger check (it guards the tag the key actually granted, not the
one rig hoped for). This plan is the up-to-date statement of the work.