From ea91a00b986519c78fa8b0bc55c9391cf47ffdd5 Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sat, 18 Jul 2026 14:50:16 +0000 Subject: [PATCH 1/6] =?UTF-8?q?docs(plans):=20class-model=20remnants=20pla?= =?UTF-8?q?n=20=E2=80=94=20what=20still=20keeps=20#12=20and=20#25=20open?= =?UTF-8?q?=20after=20PRs=20#27/#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- docs/plans/2026-07-18-class-model-remnants.md | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docs/plans/2026-07-18-class-model-remnants.md diff --git a/docs/plans/2026-07-18-class-model-remnants.md b/docs/plans/2026-07-18-class-model-remnants.md new file mode 100644 index 0000000..120ce68 --- /dev/null +++ b/docs/plans/2026-07-18-class-model-remnants.md @@ -0,0 +1,62 @@ +# Class-model remnants — closing issues #12 and #25 + +**Goal:** finish the four remnants that keep rig#12 (the `dev` role) and rig#25 +(machine classes) open. The bulk of both issues already landed on `main` via +PR #27 (traits model: role→class/host/join map, the `dev` and `workstation` +roles, `/etc/rig/role`, the effective-tag refusals, `rig users`) and PR #28 +(host-class bootstrap installs box + runs its `setup-host`). What remains is +documentation debt and two small behaviors both issues explicitly asked for. + +## Tasks + +- [x] **README: `claudebox` → `box`** (#12 comment, item 1). The philosophy + line still links `heavy-duty/claudebox`; the repo renamed to + `heavy-duty/box`. The redirect works today and is one squatted rename away + from not working. Negative-grep test pins the stale slug out. +- [x] **README: the per-role identity table** (#25, item 4). *The identity + model* section carries the prose but not #25's at-a-glance class comparison. + Add a compact table translated onto the current traits (class/host/join per + role, who lives there, root SSH's fate) plus a who-installs-what / + who-runs-as-what paragraph. No wholesale rewrite of the section. +- [x] **README: say out loud that the box install is unpinned** (#12 comment, + item 5). box has no tags/releases and its installer resolves `refs/heads` + only, so host-class bootstrap can only track a moving `heavy-duty/box@main`. + The issue's decision: install `main` **and the README says so and why** — + `BOX_REPO`/`BOX_REF` as the pin points, `RIG_SKIP_BOX_INSTALL=1` as the + opt-out. +- [x] **bootstrap: verify the box install took effect** (#12 comment, item 3 — + "rig trusting an exit code instead of checking effective state"). After the + installer claims success, prove `command -v box` resolves; a hollow success + WARNS (box is the host extra — never fatal) with the manual pointer. +- [x] **coolify verbs: role-marker sanity warnings** (#25, item 3's named + consumer — "`rig ` sanity warnings later (e.g. `coolify install` on a + non-control-plane box)"). Both `coolify install` and `coolify backup + install` read `/etc/rig/role` via the lib's `read_role_marker` and warn when + it names a non-control-plane role. +- [x] **Tests** in `test/cli.sh`, existing patterns only: live marker-warning + checks through `RIG_ROLE_MARKER` fixtures (non-root), grep-the-shipped-script + guards for the root-gated paths, line-number ordering assert for the + effective check, fail-closed defaults. + +## Behavior contract + +- **bootstrap, box block** (`commands/bootstrap.sh`): on the installer-success + path, `command -v box` decides the message — found → success log naming + `box new` and `box doctor` (box's own effective-state verdict; rig never + interrogates Incus, so the deeper verification is delegated, not + reimplemented); absent → `warn` with the `BOX_MANUAL` pointer. Never `die` + in either branch; exit codes and all skip/failure paths unchanged. +- **coolify install / coolify backup install**: the marker check is + ADVISORY — absent marker or `role=control-plane …` stays silent; any other + marker line warns and proceeds. It runs after arg validation and **before** + the root check (testable non-root; a 0644 file needs no privilege), reads + the path from `RIG_ROLE_MARKER` (default `/etc/rig/role`), and never + changes an exit code: usage errors stay 2, the root refusal stays 1. +- **README**: content-only edits; no command semantics described differently + from what ships. + +## Non-goals + +- No new roles, traits, flags, or marker consumers beyond the two coolify + verbs. No changes to box. No gating (warn-only) — `close-root` remains the + only command the marker can refuse. From d0ec6a654800db50952e16beb9971ea770ee4685 Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sat, 18 Jul 2026 14:50:16 +0000 Subject: [PATCH 2/6] =?UTF-8?q?docs(readme):=20the=20#12/#25=20documentati?= =?UTF-8?q?on=20remnants=20=E2=80=94=20box=20rename,=20per-role=20identity?= =?UTF-8?q?=20table,=20unpinned-install=20note?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three content-only edits. The philosophy line pointed at heavy-duty/claudebox, a slug that only works through a GitHub redirect; the repo is heavy-duty/box now (#12). The identity model section gets issue #25's at-a-glance class comparison, translated onto the traits that replaced the class binary, plus the who-installs-what / who-runs-as-what paragraph. And the host-class box install now says out loud that it tracks a moving heavy-duty/box@main — box cuts no tags and its installer resolves refs/heads only, so there is nothing to pin to; BOX_REPO/BOX_REF are the pin points the day that changes, and RIG_SKIP_BOX_INSTALL=1 the opt-out. Issue #12's decision was that silently tracking main on the box that runs the agents is the option not to pick. Co-Authored-By: Claude Fable 5 --- README.md | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ec1cd00..efd0361 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A CLI that turns a **pristine Debian server into a hardened, tailnet-joined node** — one curl, one command. A second command installs a version-pinned Coolify on a control-plane box. -Philosophy (shared with [claudebox](https://github.com/heavy-duty/claudebox)): +Philosophy (shared with [box](https://github.com/heavy-duty/box)): **public tool, private state**. rig carries plumbing logic only — no hostnames, no bindings, no secrets, nothing about *your* infrastructure. It takes arguments, does its work, and stores no credential, ever. @@ -188,6 +188,18 @@ which legitimately lack it. (The world-readable global install path — box unde `/opt/box` readable by every non-root user — depends on box PR #71; until that merges box's root install lands in `/root`.) +> **The box install is unpinned — on purpose, and out loud.** `coolify install` +> demands a version pin; the box step tracks a moving `heavy-duty/box@main`. +> Not because box self-updates (it doesn't — it has Coolify's shape, not the +> runner's) but because there is nothing to pin *to*: box cuts no tags and no +> releases, and its installer resolves `refs/heads/` — branches only — so +> a `BOX_REF=v0.5.0` would 404 even if the tag existed. Issue #12's call was +> that silently tracking `main` on the box that runs the agents is the option +> not to pick — hence this paragraph. `BOX_REPO` / `BOX_REF` are the pin +> points the day box cuts a tag (or you point at a frozen branch of your own +> fork); `RIG_SKIP_BOX_INSTALL=1` opts out entirely for a host whose box you +> manage by hand. + `dev` is `staging`'s human-class sibling — the same VM-hosting, `tag:local` shape with a person living on it, box CLI installed the same way — and `workstation` is the machine at the keyboard end of all the SSH connections: @@ -203,6 +215,28 @@ root login is unattributable by construction. `rig users apply` puts named operators on every box, server-class included; a human always enters as themself and elevates via sudo. +Per role, the whole identity picture at a glance — issue #25's class +comparison, translated onto the traits that replaced the class binary: + +| role | class | host | join | who lives here | root SSH after `rig users apply` | +|-----------------|--------|------|---------|--------------------------------------|----------------------------------| +| `control-plane` | server | no | authkey | nobody — Coolify runs here | open — the automation door | +| `workload` | server | no | authkey | nobody — deployed services run here | open — the automation door | +| `runner` | server | no | authkey | nobody — CI jobs as `github-runner` | open — the automation door | +| `staging` | server | yes | authkey | nobody — an unattended VM appliance | open — the automation door | +| `dev` | human | yes | authkey | operators, minting boxes | closed by `rig users close-root` | +| `workstation` | human | yes | login | its owner | closed by `rig users close-root` | + +Who installs what, and who runs as what: **bootstrap is always root** and +installs everything a role needs — on `host=yes` that includes the box CLI +(globally) and box's own `setup-host`. **Humans always run as themselves**: +operators land via `rig users apply` on every class and elevate through sudo +(roles `admin`/`rig`) or the `incus` group (role `box`) — never by logging in +as root. **Machine identities stay machine-shaped**: Coolify's automation +SSHes in as root (that is what server-class root *is*), CI jobs run as the +unprivileged `github-runner`, and guest VMs are their own server-class boxes, +converged from inside by `rig bootstrap workload`. + **`class` decides root SSH's fate — after `rig users apply`, never before.** On `class=human`, root SSH closes entirely (`rig users close-root`, below). On `class=server` it stays open — key-only, as bootstrap left it — because From 34f1986da039b9476981fa2bb34094479e0f5cc7 Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sat, 18 Jul 2026 14:50:46 +0000 Subject: [PATCH 3/6] =?UTF-8?q?feat(bootstrap):=20prove=20box=20landed=20o?= =?UTF-8?q?n=20PATH=20after=20a=20claimed=20install=20success=20=E2=80=94?= =?UTF-8?q?=20don't=20trust=20exit=20codes=20(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #12's review comment named the failure shape exactly: box's setup-host is written for a sudo-capable user and one of its paths exits 0 after only adding a group, asking for a re-login — so an installer's exit code can claim a success that never took effect. That is the sshd first-wins bug's shape, and rig's doctrine is to assert effective state. The check stays deliberately light: command -v box proves the one artifact rig asked the installer for. Anything deeper — daemon, pool, network — is box's domain; rig never interrogates Incus, so the success log hands the operator 'box doctor' (box's own effective-state verdict) instead of reimplementing it. A hollow success WARNS with the manual pointer, never dies: box is the host extra, and the OS+tailnet core is already done and asserted by the time this block runs. Tests grep the shipped script (the check needs root + network to exercise): the call, the warn wording, the delegation to box doctor, and a fail-closed line-number assert that the check follows the installer run. Rides along: the README rename greps (#12) — the stale heavy-duty/claudebox slug is negative-grepped out for good. Co-Authored-By: Claude Fable 5 --- commands/bootstrap.sh | 20 +++++++++++++++++++- test/cli.sh | 30 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/commands/bootstrap.sh b/commands/bootstrap.sh index c2197f8..f8f4822 100755 --- a/commands/bootstrap.sh +++ b/commands/bootstrap.sh @@ -531,7 +531,25 @@ if [ "$HOST" = "yes" ]; then # else — a warning, never an abort: box is the host extra, the OS+tailnet core # is already done. if curl -fsSL "$BOX_INSTALL_URL" | BOX_YES=1 bash; then - log "box installed and host set up — mint guest boxes with 'box new'" + # Don't trust the exit code — prove the effective state (issue #12). An + # installer can exit 0 having done less than it claims: box's setup-host + # is written for a sudo-capable user, and one of its paths exits 0 after + # only adding a group, asking for a re-login. That is the sshd first-wins + # bug's exact shape — asserting what was REQUESTED (here: the installer's + # claimed success) instead of what actually TOOK. The check stays + # deliberately LIGHT: box on PATH is the one artifact rig asked the + # installer for. Anything deeper — the incus daemon, the pool, the + # network — is box's domain, and rig never interrogates Incus (the same + # delegation law as the install itself: box owns the daemon); box ships + # its own effective-state verdict as `box doctor`, so the success line + # hands the operator that verb instead of half-reimplementing it here. + # And a failed check WARNS, never dies: box is the host EXTRA, and the + # OS+tailnet core above is already done and asserted. + if command -v box >/dev/null 2>&1; then + log "box installed and host set up — mint guest boxes with 'box new'; 'box doctor' verifies the host end to end" + else + warn "box's installer reported success but no 'box' is on PATH — the install did not take effect. Finish the host by hand: ${BOX_MANUAL}" + fi else warn "box install did not complete (no network, or box's installer failed); bootstrap's core work is done. Finish the host by hand: ${BOX_MANUAL}" fi diff --git a/test/cli.sh b/test/cli.sh index ef069e2..5449708 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -136,6 +136,36 @@ check "bootstrap: box install runs after the role marker write" \ # host whose box did not install is never left without the next move. check "bootstrap: box skip/failure keeps a pointer to the manual install" 0 "" \ grep -q "prepare Incus" "$ROOT/commands/bootstrap.sh" +# "Don't trust exit codes" (#12): box's installer can exit 0 having done less +# than it claims (its setup-host has a path that exits 0 after only adding a +# group, asking for a re-login). After a claimed success bootstrap must prove +# the one artifact it asked for — box on PATH — and a hollow success WARNS, +# never dies: box is the host extra. Exercising it needs root + the network, +# so grep the shipped script (repo precedent: the tag-refusal greps). Match +# the CALL, not the word — the rationale comment says `command -v box` too. +check "bootstrap: a box-install success is verified, not trusted" 0 "" \ + grep -qE '^[[:space:]]*if command -v box' "$ROOT/commands/bootstrap.sh" +check "bootstrap: a hollow box-install success warns, never dies" 0 "" \ + grep -q "reported success but no 'box' is on PATH" "$ROOT/commands/bootstrap.sh" +# Ordering: the effective check must sit AFTER the installer run it verifies. +# Line-number compare, defaults fail closed (same idiom as the marker/install +# ordering assert above; box_install_at is computed there). +box_check_at="$(grep -nE '^[[:space:]]*if command -v box' "$ROOT/commands/bootstrap.sh" | head -n1 | cut -d: -f1)" +check "bootstrap: the effective check follows the installer run" \ + 0 "" test "${box_install_at:-999999}" -lt "${box_check_at:-0}" +# rig's delegation law caps the check's depth: rig never interrogates Incus, so +# the deeper verdict is handed to box's own verb rather than reimplemented. +check "bootstrap: the deeper host verification is delegated to box doctor" 0 "" \ + grep -q "box doctor" "$ROOT/commands/bootstrap.sh" +# --- README: the box rename (#12) -------------------------------------------- +# The philosophy line must point at heavy-duty/box — the old claudebox slug +# only works through a GitHub redirect that one squatted rename away from +# breaking (box's own installer was already bitten by the rename once). A +# negative grep (exit 1 = pass) keeps the stale slug from creeping back. +check "README: no stale heavy-duty/claudebox links" 1 "" \ + grep -n "heavy-duty/claudebox" "$ROOT/README.md" +check "README: points at heavy-duty/box" 0 "" \ + grep -q "github.com/heavy-duty/box" "$ROOT/README.md" if [ "$(id -u)" -ne 0 ]; then check "bootstrap: refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workload check "bootstrap: runner role parses, refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" runner From 900697bdc29ca2d28f679463d4625cc39e02905c Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sat, 18 Jul 2026 14:50:46 +0000 Subject: [PATCH 4/6] =?UTF-8?q?feat(coolify):=20warn=20when=20the=20role?= =?UTF-8?q?=20marker=20names=20a=20non-control-plane=20box=20=E2=80=94=20a?= =?UTF-8?q?dvisory,=20never=20a=20gate=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #25 named this consumer when it introduced /etc/rig/role: 'rig sanity warnings later (e.g. coolify install on a non-control-plane box)'. Both coolify verbs now read the marker through the lib's read_role_marker (RIG_ROLE_MARKER overrides the path for fixtures, repo precedent) and warn when it names any role but control-plane — the likeliest story is the wrong SSH session about to put a control plane on a workload box. The marker stays advisory: it may be absent (pre-marker boxes, hand-built boxes) and absence stays silent — warning there would nag every legitimate run — and a present-but-different marker warns and proceeds, because an advisory file must never outrank the operator (contrast close-root, where the marker IS the gate: shutting the root door blind is irreversible in a way an extra Coolify is not). The check sits after arg validation and before the root check, so exit codes are untouched (usage stays 2, the root refusal stays 1) and the harness proves it non-root. Tests drive the live matrix through fixture markers (warns on workload, silent on control-plane and on absence, still exits 1 at the root check) and pin the warning's presence in both shipped scripts for root-run environments. Co-Authored-By: Claude Fable 5 --- commands/coolify-backup-install.sh | 16 +++++++++++ commands/coolify-install.sh | 29 +++++++++++++++++-- test/cli.sh | 46 ++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/commands/coolify-backup-install.sh b/commands/coolify-backup-install.sh index 4f9a64b..f7c0570 100755 --- a/commands/coolify-backup-install.sh +++ b/commands/coolify-backup-install.sh @@ -7,6 +7,10 @@ # already-filled bindings file is left untouched. set -euo pipefail +HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" +# shellcheck source=SCRIPTDIR/lib/users-config.sh +. "$HERE/lib/users-config.sh" # read_role_marker — the traits line bootstrap wrote + log() { printf 'rig-coolify-backup: %s\n' "$*"; } warn() { printf 'rig-coolify-backup: WARNING: %s\n' "$*" >&2; } die() { printf 'rig-coolify-backup: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; } @@ -67,6 +71,18 @@ done [ -n "$PG_USER" ] || die "--pg-user must not be empty" 2 [ -n "$PG_DB" ] || die "--pg-db must not be empty" 2 +# --- role-marker sanity (issue #25) ------------------------------------------ +# Same advisory check as `rig coolify install`, same reasoning: this command +# dumps the CONTROL PLANE's database, so a marker naming any other role almost +# certainly means the wrong SSH session — but the marker is advisory and may be +# absent, so WARN, never die, and warn before the root check so the harness can +# prove it non-root (RIG_ROLE_MARKER points it at fixtures, repo precedent). +MARKER_LINE="$(read_role_marker "${RIG_ROLE_MARKER:-/etc/rig/role}")" +case "$MARKER_LINE" in + ""|"role=control-plane "*) ;; + *) warn "this box's role marker says '${MARKER_LINE}' — not a control-plane box. The nightly dump targets Coolify's own database, which lives on role control-plane; if this is the wrong box, stop here and re-check your SSH session." ;; +esac + # --- guards ---------------------------------------------------------------- [ "$(id -u)" -eq 0 ] || die "must run as root" if [ -r /etc/os-release ]; then diff --git a/commands/coolify-install.sh b/commands/coolify-install.sh index 9c18cdd..aee7868 100755 --- a/commands/coolify-install.sh +++ b/commands/coolify-install.sh @@ -4,8 +4,13 @@ # explicit act. set -euo pipefail -log() { printf 'rig-coolify: %s\n' "$*"; } -die() { printf 'rig-coolify: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; } +HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" +# shellcheck source=SCRIPTDIR/lib/users-config.sh +. "$HERE/lib/users-config.sh" # read_role_marker — the traits line bootstrap wrote + +log() { printf 'rig-coolify: %s\n' "$*"; } +warn() { printf 'rig-coolify: WARNING: %s\n' "$*" >&2; } +die() { printf 'rig-coolify: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; } usage() { cat <<'EOF' @@ -32,6 +37,26 @@ if [ -z "$VERSION" ]; then die "--version is required" 2 fi +# --- role-marker sanity (issue #25) ------------------------------------------ +# Coolify belongs on the control-plane box and nowhere else — but the marker is +# ADVISORY, never a gate. It may legitimately be absent (a box bootstrapped +# before rig wrote markers, or a hand-built one), and rig refuses to guess from +# silence. When the marker EXISTS and names another role, the likeliest story +# is an operator in the wrong SSH session about to put a control plane on a +# workload box — so say it loudly. But WARN, never die: the operator may also +# be deliberately repurposing the box, and an advisory file must never outrank +# the human running the command (contrast close-root, where the marker IS the +# gate — shutting the root door blind is irreversible in a way an extra +# Coolify is not). Placed BEFORE the root check for the same reason arg errors +# are: the harness proves it non-root, and reading a 0644 file needs no +# privilege. RIG_ROLE_MARKER overrides the path so tests point it at fixtures +# (repo precedent: users-apply, users-close-root). +MARKER_LINE="$(read_role_marker "${RIG_ROLE_MARKER:-/etc/rig/role}")" +case "$MARKER_LINE" in + ""|"role=control-plane "*) ;; + *) warn "this box's role marker says '${MARKER_LINE}' — not a control-plane box. Coolify belongs on role control-plane; if this is the wrong box, stop here and re-check your SSH session. Repurposing it on purpose? Re-run 'rig bootstrap control-plane' first so the marker tells the truth." ;; +esac + [ "$(id -u)" -eq 0 ] || die "must run as root" export AUTOUPDATE=false diff --git a/test/cli.sh b/test/cli.sh index 5449708..4429c55 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -200,6 +200,52 @@ else echo "skip: coolify backup non-root refusal (running as root)" fi +# --- role-marker sanity: coolify verbs off the control plane (#25) ----------- +# Both coolify commands read /etc/rig/role and WARN — never die — when the +# marker names a non-control-plane role: the likeliest story is the wrong SSH +# session, but the marker is advisory and must not outrank the operator. The +# warning fires BEFORE the root check (same testability rule as arg errors), +# so a non-root run prints it and then hits the root refusal — provable here +# with RIG_ROLE_MARKER pointed at fixtures (repo precedent: the close-root +# marker gate). Counting fires proves silence too: a control-plane marker, an +# absent marker, and a marker-less box must all stay quiet, because warning on +# absence would nag every pre-marker box on every legitimate run. +marker_warns() { # marker_warns — how many warnings fired + local marker="$1"; shift + env RIG_ROLE_MARKER="$marker" "$@" 2>&1 | grep -c "not a control-plane box" || true +} +MARKER_FIX="$(mktemp -d)" +printf 'role=workload class=server host=no join=authkey\n' > "$MARKER_FIX/workload" +printf 'role=control-plane class=server host=no join=authkey\n' > "$MARKER_FIX/control-plane" +if [ "$(id -u)" -ne 0 ]; then + check "coolify: warns on a non-control-plane marker" 0 "1" \ + marker_warns "$MARKER_FIX/workload" "$ROOT/commands/coolify-install.sh" --version 4.1.2 + check "coolify: control-plane marker stays silent" 0 "0" \ + marker_warns "$MARKER_FIX/control-plane" "$ROOT/commands/coolify-install.sh" --version 4.1.2 + check "coolify: absent marker stays silent (advisory, not a gate)" 0 "0" \ + marker_warns "$MARKER_FIX/absent" "$ROOT/commands/coolify-install.sh" --version 4.1.2 + # The warning must stay a warning: the run proceeds past it and stops at the + # root check (exit 1), never turned into a marker refusal. + check "coolify: the marker warns but never refuses" 1 "must run as root" \ + env RIG_ROLE_MARKER="$MARKER_FIX/workload" "$ROOT/commands/coolify-install.sh" --version 4.1.2 + check "coolify backup: warns on a non-control-plane marker" 0 "1" \ + marker_warns "$MARKER_FIX/workload" "$ROOT/commands/coolify-backup-install.sh" + check "coolify backup: control-plane marker stays silent" 0 "0" \ + marker_warns "$MARKER_FIX/control-plane" "$ROOT/commands/coolify-backup-install.sh" + check "coolify backup: the marker warns but never refuses" 1 "must run as root" \ + env RIG_ROLE_MARKER="$MARKER_FIX/workload" "$ROOT/commands/coolify-backup-install.sh" +else + echo "skip: coolify role-marker warning checks (running as root)" +fi +rm -rf "$MARKER_FIX" +# Root runs skip the live checks above, so also pin the warning's presence in +# both shipped scripts — a deleted advisory cannot ship green (repo precedent: +# the staging/runner tag greps). +check "coolify: marker warning present in the shipped script" 0 "" \ + grep -q "not a control-plane box" "$ROOT/commands/coolify-install.sh" +check "coolify backup: marker warning present in the shipped script" 0 "" \ + grep -q "not a control-plane box" "$ROOT/commands/coolify-backup-install.sh" + # --- rig db (ad-hoc dump/restore) ------------------------------------------- check "bare db shows usage, exit 2" 2 "usage:" "$ROOT/bin/rig" db check "db --help exits 0" 0 "usage:" "$ROOT/bin/rig" db --help From cee7d4575e83a9698c76264e1679c9f3fa1a6a7e Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sat, 18 Jul 2026 16:59:43 +0000 Subject: [PATCH 5/6] fix(bootstrap): gate the host-set-up claim on 'box doctor', not on PATH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three reviewers, same substance: 'command -v box' proves the CLI landed, not that setup-host took effect — and the success line claimed both. The claim is now split to match its proofs: PATH proves the install; 'box doctor' (box's own effective-state verdict — the daemon stays box's domain) gates "host set up". A failed doctor WARNS with the verdict verb and the manual path, and claims nothing it cannot prove. Also: the coolify marker guard matched 'role=control-plane ' by its trailing space, coupling it to the marker's field formatting — a bare 'role=control-plane' line now reads the same (claude-bot's nit), with a fixture proving it. Co-Authored-By: Claude Fable 5 --- commands/bootstrap.sh | 23 ++++++++++++++--------- commands/coolify-backup-install.sh | 2 +- commands/coolify-install.sh | 2 +- test/cli.sh | 22 ++++++++++++++++++---- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/commands/bootstrap.sh b/commands/bootstrap.sh index f8f4822..e6a229a 100755 --- a/commands/bootstrap.sh +++ b/commands/bootstrap.sh @@ -537,16 +537,21 @@ if [ "$HOST" = "yes" ]; then # only adding a group, asking for a re-login. That is the sshd first-wins # bug's exact shape — asserting what was REQUESTED (here: the installer's # claimed success) instead of what actually TOOK. The check stays - # deliberately LIGHT: box on PATH is the one artifact rig asked the - # installer for. Anything deeper — the incus daemon, the pool, the - # network — is box's domain, and rig never interrogates Incus (the same - # delegation law as the install itself: box owns the daemon); box ships - # its own effective-state verdict as `box doctor`, so the success line - # hands the operator that verb instead of half-reimplementing it here. - # And a failed check WARNS, never dies: box is the host EXTRA, and the - # OS+tailnet core above is already done and asserted. + # Two proofs, one claim each. `command -v box` proves the CLI landed + # (box's root install symlinks into /usr/local/bin, already on this + # shell's PATH — no login shell needed). "Host set up" is a separate + # claim and gets box's OWN effective-state verdict, `box doctor` — + # the daemon, the pool, the network stay box's domain (the same + # delegation law as the install itself: box owns the daemon), rig + # just refuses to claim what that verdict does not answer. Both + # failures WARN, never die: box is the host EXTRA, and the OS+tailnet + # core above is already done and asserted. if command -v box >/dev/null 2>&1; then - log "box installed and host set up — mint guest boxes with 'box new'; 'box doctor' verifies the host end to end" + if box doctor >/dev/null 2>&1; then + log "box installed and host set up — 'box doctor' passed; mint guest boxes with 'box new'" + else + warn "box is on PATH but 'box doctor' does not pass — the CLI landed, the host stack is unproven. Run 'box doctor' for the verdict, then 'box setup-host' (or finish by hand: ${BOX_MANUAL})" + fi else warn "box's installer reported success but no 'box' is on PATH — the install did not take effect. Finish the host by hand: ${BOX_MANUAL}" fi diff --git a/commands/coolify-backup-install.sh b/commands/coolify-backup-install.sh index f7c0570..a656472 100755 --- a/commands/coolify-backup-install.sh +++ b/commands/coolify-backup-install.sh @@ -79,7 +79,7 @@ done # prove it non-root (RIG_ROLE_MARKER points it at fixtures, repo precedent). MARKER_LINE="$(read_role_marker "${RIG_ROLE_MARKER:-/etc/rig/role}")" case "$MARKER_LINE" in - ""|"role=control-plane "*) ;; + ""|"role=control-plane"|"role=control-plane "*) ;; *) warn "this box's role marker says '${MARKER_LINE}' — not a control-plane box. The nightly dump targets Coolify's own database, which lives on role control-plane; if this is the wrong box, stop here and re-check your SSH session." ;; esac diff --git a/commands/coolify-install.sh b/commands/coolify-install.sh index aee7868..888e838 100755 --- a/commands/coolify-install.sh +++ b/commands/coolify-install.sh @@ -53,7 +53,7 @@ fi # (repo precedent: users-apply, users-close-root). MARKER_LINE="$(read_role_marker "${RIG_ROLE_MARKER:-/etc/rig/role}")" case "$MARKER_LINE" in - ""|"role=control-plane "*) ;; + ""|"role=control-plane"|"role=control-plane "*) ;; *) warn "this box's role marker says '${MARKER_LINE}' — not a control-plane box. Coolify belongs on role control-plane; if this is the wrong box, stop here and re-check your SSH session. Repurposing it on purpose? Re-run 'rig bootstrap control-plane' first so the marker tells the truth." ;; esac diff --git a/test/cli.sh b/test/cli.sh index 4429c55..877cfff 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -153,10 +153,19 @@ check "bootstrap: a hollow box-install success warns, never dies" 0 "" \ box_check_at="$(grep -nE '^[[:space:]]*if command -v box' "$ROOT/commands/bootstrap.sh" | head -n1 | cut -d: -f1)" check "bootstrap: the effective check follows the installer run" \ 0 "" test "${box_install_at:-999999}" -lt "${box_check_at:-0}" -# rig's delegation law caps the check's depth: rig never interrogates Incus, so -# the deeper verdict is handed to box's own verb rather than reimplemented. -check "bootstrap: the deeper host verification is delegated to box doctor" 0 "" \ - grep -q "box doctor" "$ROOT/commands/bootstrap.sh" +# rig's delegation law caps the check's depth: rig never interrogates Incus — +# the host verdict is box's own verb, and the "host set up" CLAIM is gated on +# it. Two asserts: the gate exists as a call (not just prose naming the verb), +# and the claim line sits inside/after it (line order, fail-closed defaults — +# a claim that outruns its proof is exactly the overclaim this closes). +check "bootstrap: the host-set-up claim is gated on box doctor" 0 "" \ + grep -qE '^[[:space:]]*if box doctor' "$ROOT/commands/bootstrap.sh" +doctor_at="$(grep -nE '^[[:space:]]*if box doctor' "$ROOT/commands/bootstrap.sh" | head -n1 | cut -d: -f1)" +claim_at="$(grep -n 'box installed and host set up' "$ROOT/commands/bootstrap.sh" | head -n1 | cut -d: -f1)" +check "bootstrap: the claim follows the doctor gate" \ + 0 "" test "${doctor_at:-999999}" -lt "${claim_at:-0}" +check "bootstrap: a failed doctor warns without claiming the host" 0 "" \ + grep -q "the CLI landed, the host stack is unproven" "$ROOT/commands/bootstrap.sh" # --- README: the box rename (#12) -------------------------------------------- # The philosophy line must point at heavy-duty/box — the old claudebox slug # only works through a GitHub redirect that one squatted rename away from @@ -217,11 +226,16 @@ marker_warns() { # marker_warns — how many warnings fir MARKER_FIX="$(mktemp -d)" printf 'role=workload class=server host=no join=authkey\n' > "$MARKER_FIX/workload" printf 'role=control-plane class=server host=no join=authkey\n' > "$MARKER_FIX/control-plane" +printf 'role=control-plane\n' > "$MARKER_FIX/bare-control-plane" if [ "$(id -u)" -ne 0 ]; then check "coolify: warns on a non-control-plane marker" 0 "1" \ marker_warns "$MARKER_FIX/workload" "$ROOT/commands/coolify-install.sh" --version 4.1.2 check "coolify: control-plane marker stays silent" 0 "0" \ marker_warns "$MARKER_FIX/control-plane" "$ROOT/commands/coolify-install.sh" --version 4.1.2 + # A bare marker line with no trailing traits must read the same as the full + # one — the guard must not couple to the marker's field formatting. + check "coolify: a bare 'role=control-plane' line (no traits) stays silent" 0 "0" \ + marker_warns "$MARKER_FIX/bare-control-plane" "$ROOT/commands/coolify-install.sh" --version 4.1.2 check "coolify: absent marker stays silent (advisory, not a gate)" 0 "0" \ marker_warns "$MARKER_FIX/absent" "$ROOT/commands/coolify-install.sh" --version 4.1.2 # The warning must stay a warning: the run proceeds past it and stops at the From af327cb66064806fed02578e499e264508b6c0b4 Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sat, 18 Jul 2026 17:16:29 +0000 Subject: [PATCH 6/6] docs(bootstrap): finish the sentence the doctor-gate edit truncated Co-Authored-By: Claude Fable 5 --- commands/bootstrap.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/bootstrap.sh b/commands/bootstrap.sh index e6a229a..0146f04 100755 --- a/commands/bootstrap.sh +++ b/commands/bootstrap.sh @@ -536,7 +536,7 @@ if [ "$HOST" = "yes" ]; then # is written for a sudo-capable user, and one of its paths exits 0 after # only adding a group, asking for a re-login. That is the sshd first-wins # bug's exact shape — asserting what was REQUESTED (here: the installer's - # claimed success) instead of what actually TOOK. The check stays + # claimed success) instead of what actually TOOK. # Two proofs, one claim each. `command -v box` proves the CLI landed # (box's root install symlinks into /usr/local/bin, already on this # shell's PATH — no login shell needed). "Host set up" is a separate