feat: the #12/#25 remnants — README class table + box rename, unpinned-install note, box effective check, coolify marker warnings #29
6 changed files with 254 additions and 4 deletions
36
README.md
36
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/<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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@
|
|||
# explicit act.
|
||||
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: %s\n' "$*"; }
|
||||
warn() { printf 'rig-coolify: WARNING: %s\n' "$*" >&2; }
|
||||
die() { printf 'rig-coolify: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
|
||||
|
||||
usage() {
|
||||
|
|
@ -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
|
||||
|
|
|
|||
62
docs/plans/2026-07-18-class-model-remnants.md
Normal file
62
docs/plans/2026-07-18-class-model-remnants.md
Normal 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.
|
||||
90
test/cli.sh
90
test/cli.sh
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue