feat: the #12/#25 remnants — README class table + box rename, unpinned-install note, box effective check, coolify marker warnings #29

Merged
dan-claude-bot merged 6 commits from feat/class-model-remnants into main 2026-07-18 17:36:33 +00:00
6 changed files with 254 additions and 4 deletions

View file

@ -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/<ref>` — 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

View file

@ -531,7 +531,30 @@ 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.
# 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
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
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

View file

@ -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"|"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

View file

@ -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 <pin> 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"|"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

View file

@ -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 <cmd>` 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.

View file

@ -136,6 +136,45 @@ 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 —
# 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
# 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
@ -170,6 +209,57 @@ 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 <marker_path> <cmd...> — 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"
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
# 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