Merge pull request #127 from codex-bot-andresmgsl/build/63-bootstrap-undo

feat: add guarded bootstrap undo
This commit is contained in:
Daniel Marin 2026-07-24 11:41:28 +01:00 committed by GitHub
commit 20438f09e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 158 additions and 7 deletions

View file

@ -13,6 +13,7 @@ on the way to cutting its first release, and this file starts there.
- `rig platform` prints a stable machine `ID`, derived from `/etc/machine-id`, never the raw value (#95)
- Platform, drill, docs and labels changes receive dedicated scope labels (#119)
- CI drills the install lifecycle against a real tree — install from the checkout, converge to an empty diff, uninstall to proven absence (#106)
- `rig bootstrap --undo` removes only a tailnet join rig can prove it made (#63)
- `kimi-box` joins the box tenant roles — the Kimi CLI agent guest (#109)
- The `changelog-armed` guard returns, version-keyed (#112, ceremony#13)
- The `.ceremony/` doctrine mirror, verified by `docs-sync` on every PR (#112, ceremony#19)

View file

@ -237,11 +237,33 @@ the only shapes it manages — every other role refuses an effective
`tag:server` after join, one rule instead of per-role exceptions.
After the tag verification passes, bootstrap writes `/etc/rig/role` — one
line, `role=… root-door=… host=… join=…` — recording the **effective** traits,
overrides and all, so an overridden role never lies to the commands that read
the marker later (`rig users` keys root policy off `root-door=`). Written
post-join and cmp-guarded, so a marker never describes a box that failed to
become what it claims.
line, `role=… root-door=… host=… join=… join-by=…` — recording the
**effective** traits, overrides and all, plus whether this run performed the
tailnet join. `join-by=rig` means bootstrap called `tailscale up`;
`join-by=preexisting` means it found the node already joined. Old markers name
neither and are treated as unknown, never as permission to remove a join.
Written post-join and cmp-guarded, so a marker never describes a box that
failed to become what it claims.
### `rig bootstrap --undo`
```sh
sudo rig bootstrap --undo
```
Leaves the tailnet and then removes `/etc/rig/role`, but only when the marker
says `join-by=rig`. A pre-existing join, an old marker with no provenance, or
no marker at all is refused without calling `tailscale logout`; the refusal
names the manual repair. Re-running bootstrap writes the current marker shape.
Undo also refuses while a GitHub runner is installed and points at
`rig runner remove`, because restoring the local machine while leaving an
off-box runner registration would create a ghost in the repository. If
`tailscale logout` fails, the marker stays in place so the command is retryable.
This is intentionally not a general rollback. It does not uninstall packages,
reverse sshd hardening, remove Docker, Node, agent CLIs, or users. Those changes
are convergent rather than transactional and cannot be safely inferred away.
Immediately after it, bootstrap stamps `/etc/rig/manifest`**provenance**:
which rig converged this box and when (see [`rig

View file

@ -33,6 +33,10 @@ commands:
tenant roles); custom and workstation take none. join=login
(workstation) needs no key: interactive login, node must come up
untagged. Run as root.
bootstrap --undo
Leave the tailnet only when /etc/rig/role proves rig performed the
join, then remove that marker. Refuses for pre-existing or old unknown
joins, and while a GitHub runner is installed. Run as root.
bootstrap <claude-box|codex-box|grok-box|kimi-box|staging-box> [--user <name>]
The box TENANT roles: converge a box-minted guest. The '-box' suffix
names the family (a guest, vs the '-server' machine roles above).

52
commands/bootstrap-undo.sh Executable file
View file

@ -0,0 +1,52 @@
#!/usr/bin/env bash
# rig bootstrap --undo — remove only off-box state rig can prove it created.
set -euo pipefail
log() { printf 'rig-bootstrap: %s\n' "$*"; }
die() { printf 'rig-bootstrap: ERROR: %s\n' "$*" >&2; exit 1; }
MARKER="${RIG_ROLE_MARKER:-/etc/rig/role}"
[ "$(id -u)" -eq 0 ] || die "must run as root"
[ -e "$MARKER" ] || die "no /etc/rig/role marker — refusing to touch the tailnet"
runner_installed=0
if [ -n "${RIG_RUNNER_DIR:-}" ]; then
[ -e "$RIG_RUNNER_DIR/.runner" ] && runner_installed=1
else
for runner_config in /home/*/actions-runner/.runner /root/actions-runner/.runner; do
[ -e "$runner_config" ] && runner_installed=1
done
compgen -G '/etc/systemd/system/actions.runner.*.service' >/dev/null \
&& runner_installed=1
fi
if [ "$runner_installed" -eq 1 ]; then
die "a GitHub runner is installed — run 'rig runner remove' first so undo does not leave a ghost runner in the repository"
fi
join_by=""
while IFS= read -r field; do
case "$field" in
join-by=*) join_by="${field#join-by=}" ;;
esac
done < <(tr '[:space:]' '\n' < "$MARKER")
case "$join_by" in
rig) ;;
preexisting)
die "the tailnet join predates this bootstrap run (join-by=preexisting), so rig will not remove state it did not create; run 'tailscale logout' by hand if that is intended" ;;
"")
die "the role marker predates join-by provenance, so rig cannot prove it made this tailnet join and will not remove it; re-run bootstrap to write a current marker, or run 'tailscale logout' by hand" ;;
*)
die "the role marker has unknown join-by=$join_by, so rig cannot prove it made this tailnet join and will not remove it; run 'tailscale logout' by hand if that is intended" ;;
esac
# The same back-out/keep law as first-join verification: logout is earned only
# when the marker proves rig performed the join. Preserve the marker on failure
# so the operation remains retryable and never reports a half-undone machine.
if ! tailscale logout; then
die "tailscale logout failed; role marker kept so 'rig bootstrap --undo' can be retried"
fi
rm -f -- "$MARKER"
log "tailnet join removed; role marker removed"

View file

@ -32,6 +32,9 @@ usage: rig bootstrap <control-plane-server|workload-server|runner-server|
rig bootstrap <claude-box|codex-box|grok-box|kimi-box|staging-box> [--user <name>]
(the box TENANT roles — see their own --help; they take
no --users, see below)
rig bootstrap --undo
leave the tailnet only when the role marker proves rig
performed the join, then remove the role marker
--users the users file this box's operators come from — REQUIRED. It is
applied as bootstrap's last phase, exactly as `rig users apply
@ -114,6 +117,10 @@ EOF
# --- args (validated before the root check, so errors are testable) ---------
ROLE="${1:-}"
case "$ROLE" in
--undo)
shift
[ $# -eq 0 ] || die "bootstrap --undo takes no arguments" 2
exec "$HERE/bootstrap-undo.sh" ;;
control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom) shift ;;
claude-box|codex-box|grok-box|kimi-box|staging-box)
# The box TENANT roles (#31) are a different family — guests a box mints,
@ -534,6 +541,7 @@ if ! command -v tailscale >/dev/null 2>&1; then
log "installing tailscale"
curl -fsSL https://tailscale.com/install.sh | sh
fi
JOIN_BY=preexisting
if tailscale status >/dev/null 2>&1; then
log "tailnet already joined; skipping tailscale up (no pre-auth key needed)"
# ...but skipping `tailscale up` also skipped --hostname, so the TAILNET name
@ -574,6 +582,7 @@ elif [ "$JOIN" = "login" ]; then
log "joining tailnet as ${TS_HOSTNAME} (interactive login; follow the URL tailscale prints)"
tailscale up --hostname="$TS_HOSTNAME"
verify_user_owned back-out
JOIN_BY=rig
else
# 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
@ -594,6 +603,7 @@ else
log "joining tailnet as ${TS_HOSTNAME} (tag comes from the pre-auth key)"
tailscale up --authkey="$TS_AUTHKEY" --hostname="$TS_HOSTNAME"
verify_effective_tag back-out
JOIN_BY=rig
fi
# --- role marker --------------------------------------------------------------
@ -611,11 +621,12 @@ fi
# those exist in the field by the thousand and nothing will rewrite them.
MARKER=/etc/rig/role
MARKER_TMP="$(mktemp)"
printf 'role=%s root-door=%s host=%s join=%s\n' "$ROLE" "$ROOT_DOOR" "$HOST" "$JOIN" > "$MARKER_TMP"
printf 'role=%s root-door=%s host=%s join=%s join-by=%s\n' \
"$ROLE" "$ROOT_DOOR" "$HOST" "$JOIN" "$JOIN_BY" > "$MARKER_TMP"
if ! cmp -s "$MARKER_TMP" "$MARKER" 2>/dev/null; then
mkdir -p /etc/rig
install -m 0644 "$MARKER_TMP" "$MARKER"
log "role marker written: role=$ROLE root-door=$ROOT_DOOR host=$HOST join=$JOIN"
log "role marker written: role=$ROLE root-door=$ROOT_DOOR host=$HOST join=$JOIN join-by=$JOIN_BY"
else
log "role marker already current"
fi

View file

@ -143,6 +143,67 @@ check "bootstrap: login verify fails closed on a stalled backend" 0 "" \
# The marker is the traits' ground truth for rig users; assert the write exists.
check "bootstrap: role marker write is present" 0 "" \
grep -q "/etc/rig/role" "$ROOT/commands/bootstrap.sh"
check "bootstrap: role marker records join provenance" 0 "join-by=%s" \
grep -F "join-by=%s" "$ROOT/commands/bootstrap.sh"
check "bootstrap: both first-join paths record join-by=rig" 0 "2" \
grep -c "^[[:space:]]*JOIN_BY=rig$" "$ROOT/commands/bootstrap.sh"
check "bootstrap: already-joined path defaults to join-by=preexisting" 0 "JOIN_BY=preexisting" \
grep -F "JOIN_BY=preexisting" "$ROOT/commands/bootstrap.sh"
# Drive the narrow inverse end to end. Every refusal also asserts the tailscale
# shim was NOT called: exit status alone would miss the destructive regression.
UNDO_FIX="$(mktemp -d)"
UNDO_BIN="$UNDO_FIX/bin"
UNDO_MARKER="$UNDO_FIX/role"
UNDO_RUNNER="$UNDO_FIX/runner"
UNDO_CALLS="$UNDO_FIX/tailscale.calls"
mkdir -p "$UNDO_BIN" "$UNDO_RUNNER"
cat > "$UNDO_BIN/tailscale" <<'SH'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "$UNDO_CALLS"
if [ "${TAILSCALE_LOGOUT_FAIL:-0}" = 1 ]; then exit 1; fi
SH
cat > "$UNDO_BIN/id" <<'SH'
#!/usr/bin/env bash
if [ "${1:-}" = -u ]; then printf '0\n'; else exec /usr/bin/id "$@"; fi
SH
chmod +x "$UNDO_BIN/tailscale" "$UNDO_BIN/id"
undo() {
env PATH="$UNDO_BIN:$PATH" UNDO_CALLS="$UNDO_CALLS" \
RIG_ROLE_MARKER="$UNDO_MARKER" RIG_RUNNER_DIR="$UNDO_RUNNER" \
"$ROOT/bin/rig" bootstrap --undo
}
undo_untouched() {
: > "$UNDO_CALLS"
if undo >"$UNDO_FIX/undo.out" 2>&1; then return 1; fi
[ ! -s "$UNDO_CALLS" ]
}
rm -f "$UNDO_MARKER"
check "bootstrap --undo: no marker refuses without touching tailnet" 0 "" undo_untouched
printf '%s\n' 'role=workload-server root-door=open host=no join=authkey' > "$UNDO_MARKER"
check "bootstrap --undo: old marker names missing provenance" \
1 "marker predates join-by provenance" undo
check "bootstrap --undo: old marker leaves tailnet untouched" 0 "" undo_untouched
printf '%s\n' 'role=workload-server root-door=open host=no join=authkey join-by=preexisting' > "$UNDO_MARKER"
check "bootstrap --undo: pre-existing join refuses by name" 1 "join-by=preexisting" undo
check "bootstrap --undo: pre-existing join leaves tailnet untouched" 0 "" undo_untouched
printf '%s\n' 'role=runner-server root-door=open host=no join=authkey join-by=rig' > "$UNDO_MARKER"
printf '%s\n' '{}' > "$UNDO_RUNNER/.runner"
check "bootstrap --undo: installed runner points at its removal verb" \
1 "rig runner remove" undo
check "bootstrap --undo: installed runner leaves tailnet untouched" 0 "" undo_untouched
rm -f "$UNDO_RUNNER/.runner"
check "bootstrap --undo: failed logout is loud" \
1 "role marker kept" env TAILSCALE_LOGOUT_FAIL=1 PATH="$UNDO_BIN:$PATH" \
UNDO_CALLS="$UNDO_CALLS" RIG_ROLE_MARKER="$UNDO_MARKER" \
RIG_RUNNER_DIR="$UNDO_RUNNER" "$ROOT/bin/rig" bootstrap --undo
check "bootstrap --undo: failed logout preserves the marker" 0 "" test -e "$UNDO_MARKER"
: > "$UNDO_CALLS"
check "bootstrap --undo: proven rig join succeeds" 0 "tailnet join removed" undo
check "bootstrap --undo: successful logout was called" 0 "logout" cat "$UNDO_CALLS"
check "bootstrap --undo: success removes the marker" 1 "" test -e "$UNDO_MARKER"
check "bootstrap --undo: second run refuses cleanly" 1 "no /etc/rig/role marker" undo
rm -rf "$UNDO_FIX"
# ...and that it is written in the CURRENT vocabulary (#77). New markers say
# root-door=; the retired class= spelling is something rig READS forever and
# WRITES never, so a marker line that reintroduces it must not ship green.