test: the tenant surface re-proven against fixture registries — offline, by refusal and by identity (#110)

The suite drives the whole new surface without a network: the family-suffix
dispatch and hard cut, the marker guards firing with the registry
unreachable, the unknown-role refusal listing the resolved source, every
malformed-definition refusal named by key, DIR-vs-REF identity through a
stubbed curl (tags-first precedence pinned), the renderer's box#80 guard,
the one-line pin, and template-lint's whole gate.
This commit is contained in:
claude-bot-andresmgsl 2026-07-24 23:08:47 +00:00
parent 11b02eb070
commit 79363c2631
3 changed files with 279 additions and 118 deletions

18
bin/rig
View file

@ -37,15 +37,19 @@ commands:
Leave the tailnet only when /etc/rig/role proves rig performed the Leave the tailnet only when /etc/rig/role proves rig performed the
join, then remove that marker. Refuses for pre-existing or old unknown join, then remove that marker. Refuses for pre-existing or old unknown
joins, and while a GitHub runner is installed. Run as root. joins, and while a GitHub runner is installed. Run as root.
bootstrap <claude-box|codex-box|grok-box|kimi-box|staging-box> [--user <name>] bootstrap <role>-box [--user <name>]
The box TENANT roles: converge a box-minted guest. The '-box' suffix The box TENANT roles: converge a box-minted guest. The '-box' suffix
names the family (a guest, vs the '-server' machine roles above). names the family (a guest, vs the '-server' machine roles above).
claude-box|codex-box|grok-box|kimi-box land the agent toolbelt (git, gh, tmux, docker), the agent's CLI on The agent tenants (claude-box, codex-box, grok-box, kimi-box, …) are
the system PATH, and the agent-context file (with the box#80 guard: DEFINED in the heavy-duty/rig-templates registry — resolved via
never run box setup-host inside a box). staging-box lands box#69's RIG_TEMPLATES_DIR > RIG_TEMPLATES_REF > the in-tree pin — and land
server posture — docker + sshd hardening; its tailnet join stays the agent toolbelt (git, gh, tmux, docker), the agent's CLI on the
operator-run via 'rig bootstrap workload-server'. Creds-free and non-interactive: box system PATH, and the agent-context file (with the box#80 guard:
auto-runs these at mint. Run as root, inside the box. never run box setup-host inside a box). staging-box, in rig's own
tree, lands box#69's server posture — docker + sshd hardening; its
tailnet join stays operator-run via 'rig bootstrap workload-server'.
Creds-free and non-interactive: box auto-runs these at mint. Run as
root, inside the box.
coolify install --version <pin> coolify install --version <pin>
Pinned Coolify install (AUTOUPDATE=false). Control-plane box only. Pinned Coolify install (AUTOUPDATE=false). Control-plane box only.
coolify backup install [options] coolify backup install [options]

View file

@ -104,7 +104,13 @@ while [ $# -gt 0 ]; do
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
--user) --user)
[ $# -ge 2 ] || die "--user needs a value" 2 [ $# -ge 2 ] || die "--user needs a value" 2
TENANT_USER_OVERRIDE="$2"; shift 2 ;; TENANT_USER_OVERRIDE="$2"; shift 2
# Same charset the users file enforces, for the same reasons (a leading
# '-' reads as a usermod flag; '|', ':' corrupt things downstream).
# Checked HERE, at parse — the definition's USER is checked by the
# parser — so the refusal needs no registry and no network.
[[ "$TENANT_USER_OVERRIDE" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] \
|| die "invalid user: '$TENANT_USER_OVERRIDE' — must match ^[a-z_][a-z0-9_-]{0,31}\$" 2 ;;
--hostname|--root-door|--host|--join) --hostname|--root-door|--host|--join)
# The machine-role traits, refused with a story rather than "unknown # The machine-role traits, refused with a story rather than "unknown
# flag": a tenant is a guest, not a tailnet machine — its shape comes # flag": a tenant is a guest, not a tailnet machine — its shape comes
@ -119,36 +125,6 @@ while [ $# -gt 0 ]; do
esac esac
done done
# --- the definition ----------------------------------------------------------
# Resolved and parsed BEFORE the root check, so the two refusals a definition
# can earn — unknown role (listing what the resolved source actually
# contains) and malformed data (naming the failing key) — are testable
# non-root, offline, via RIG_TEMPLATES_DIR fixtures. The parse is the mint's
# own guard, deliberately duplicating the registry CI's lint: CI protects the
# registry, this protects a mint served through RIG_TEMPLATES_REPO/_DIR that
# CI never saw. template.env is parsed, NEVER sourced — a definition cannot
# execute arbitrary shell through its data file; install.sh is the one
# deliberately executable part, and it runs only after the root check below.
trap '[ -n "$TEMPLATES_TMP" ] && rm -rf "$TEMPLATES_TMP"' EXIT
TPL_DIR=""
if [ "$ROLE" = "staging-box" ]; then
TENANT_USER="${TENANT_USER_OVERRIDE:-ops}" # box#69's ops
else
templates_resolve \
|| die "cannot resolve the template registry ($(templates_source_desc)) — see above" 2
TPL_DIR="$REGISTRY_DIR/$ROLE"
if [ ! -f "$TPL_DIR/template.env" ]; then
die "unknown tenant role: $ROLE — the resolved registry ($(templates_source_desc)) defines: $(templates_roles "$REGISTRY_DIR" | tr '\n' ' ')— and staging-box is in rig's own tree. A misconfigured RIG_TEMPLATES_REPO/_REF/_DIR looks exactly like this; check the source before the spelling." 2
fi
template_parse_env "$TPL_DIR/template.env" \
|| die "invalid definition for $ROLE in $(templates_source_desc) — the failing key is named above. The registry's CI lints every PR ('rig template-lint'); a malformed definition reaching a mint means the source above was never linted." 2
TENANT_USER="${TENANT_USER_OVERRIDE:-$TPL_USER}"
fi
# Same charset the users file enforces, for the same reasons (a leading '-'
# reads as a usermod flag; '|', ':' corrupt things downstream).
[[ "$TENANT_USER" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] \
|| die "invalid user: '$TENANT_USER' — must match ^[a-z_][a-z0-9_-]{0,31}\$" 2
# --- guards ------------------------------------------------------------------ # --- guards ------------------------------------------------------------------
# A tenant role converges a box GUEST. A box already carrying a machine-role # A tenant role converges a box GUEST. A box already carrying a machine-role
# marker is a tailnet machine rig built on purpose, and quietly turning it into # marker is a tailnet machine rig built on purpose, and quietly turning it into
@ -195,6 +171,34 @@ if [ -n "$EXISTING_ROOT_DOOR" ]; then
fi fi
fi fi
# --- the definition ----------------------------------------------------------
# Resolved and parsed BEFORE the root check (but after the marker guards,
# which need no definition and must stay refusable with no registry in
# reach), so the two refusals a definition can earn — unknown role (listing
# what the resolved source actually contains) and malformed data (naming the
# failing key) — are testable non-root, offline, via RIG_TEMPLATES_DIR
# fixtures. The parse is the mint's
# own guard, deliberately duplicating the registry CI's lint: CI protects the
# registry, this protects a mint served through RIG_TEMPLATES_REPO/_DIR that
# CI never saw. template.env is parsed, NEVER sourced — a definition cannot
# execute arbitrary shell through its data file; install.sh is the one
# deliberately executable part, and it runs only after the root check below.
trap '[ -n "$TEMPLATES_TMP" ] && rm -rf "$TEMPLATES_TMP"' EXIT
TPL_DIR=""
if [ "$ROLE" = "staging-box" ]; then
TENANT_USER="${TENANT_USER_OVERRIDE:-ops}" # box#69's ops
else
templates_resolve \
|| die "cannot resolve the template registry ($(templates_source_desc)) — see above" 2
TPL_DIR="$REGISTRY_DIR/$ROLE"
if [ ! -f "$TPL_DIR/template.env" ]; then
die "unknown tenant role: $ROLE — the resolved registry ($(templates_source_desc)) defines: $(templates_roles "$REGISTRY_DIR" | tr '\n' ' ')— and staging-box is in rig's own tree. A misconfigured RIG_TEMPLATES_REPO/_REF/_DIR looks exactly like this; check the source before the spelling." 2
fi
template_parse_env "$TPL_DIR/template.env" \
|| die "invalid definition for $ROLE in $(templates_source_desc) — the failing key is named above. The registry's CI lints every PR ('rig template-lint'); a malformed definition reaching a mint means the source above was never linted." 2
TENANT_USER="${TENANT_USER_OVERRIDE:-$TPL_USER}"
fi
[ "$(id -u)" -eq 0 ] || die "must run as root" [ "$(id -u)" -eq 0 ] || die "must run as root"
if [ -r /etc/os-release ]; then if [ -r /etc/os-release ]; then
# Sourced in a subshell: os-release defines VERSION, NAME, ID, etc. — # Sourced in a subshell: os-release defines VERSION, NAME, ID, etc. —

View file

@ -572,19 +572,53 @@ else
echo "skip: bootstrap non-root refusals (running as root)" echo "skip: bootstrap non-root refusals (running as root)"
fi fi
# --- box tenant roles (#31/#76): claude-box|codex-box|grok-box|kimi-box|staging-box --- # --- box tenant roles (#31/#76/#110): <role>-box from the registry + staging-box ---
# What a box-minted guest becomes — ONE mechanism (bootstrap-tenant.sh), # What a box-minted guest becomes — ONE mechanism (bootstrap-tenant.sh),
# parameterized per tenant through lib/tenant-config.sh, dispatched from # parameterized per DEFINITION fetched from the template registry
# bootstrap.sh so `rig bootstrap <role>` stays the single entrypoint. The real # (heavy-duty/rig-templates; lib/templates.sh resolves RIG_TEMPLATES_DIR >
# converge needs root, a tenant user, and the network — the container # RIG_TEMPLATES_REF > the in-tree pin), dispatched from bootstrap.sh on the
# rehearsal's job — so the harness proves what it can non-root: the whole # '-box' FAMILY SUFFIX so `rig bootstrap <role>` stays the single entrypoint
# arg/refusal surface, the pure parameter table, the rendered agent-context # and a template added to the registry is mintable with zero code changes
# file (guard note included), and grep-pins on the shipped script. # here. The real converge needs root, a tenant user, and the network — the
# container rehearsal's job — so the harness proves what it can non-root and
# OFFLINE: the whole refusal surface, the resolution precedence, the parser
# and the renderer, against fixture definitions via RIG_TEMPLATES_DIR.
# The fixture registry: one valid scratch definition, plus broken ones the
# parser must refuse BY NAME. Synthetic on purpose — the real definitions
# live in heavy-duty/rig-templates, and this suite must hold whatever those
# say (offline is the point: no fetch, no network, no coupling).
TPL_FIX="$(mktemp -d)"
mkdir -p "$TPL_FIX/scratch-box"
cat > "$TPL_FIX/scratch-box/template.env" <<'TPLEOF'
# comments and blank lines are the only non-KEY="value" grammar
USER="scratch"
CONTEXT_PATH=".scratch/AGENTS.md"
CLI_NAME="scratch"
CLI_SRC="~/.local/bin/scratch"
PATH_LINE="export PATH="$HOME/.local/bin:$PATH""
NEEDS_NODE="no"
APT_EXTRAS="zsh"
TPLEOF
printf '#!/usr/bin/env bash\nexit 0\n' > "$TPL_FIX/scratch-box/install.sh"
printf -- '- **Creds-free by default.** The scratch vendor paragraph.\n' > "$TPL_FIX/scratch-box/creds.md"
mkdir -p "$TPL_FIX/badkey-box"
printf 'USER="x"\nCOLOR="red"\n' > "$TPL_FIX/badkey-box/template.env"
mkdir -p "$TPL_FIX/missing-box"
printf 'USER="x"\nCONTEXT_PATH=".x/A.md"\nPATH_LINE="p"\n' > "$TPL_FIX/missing-box/template.env"
mkdir -p "$TPL_FIX/garbled-box"
printf 'USER=unquoted\n' > "$TPL_FIX/garbled-box/template.env"
mkdir -p "$TPL_FIX/badnode-box"
printf 'USER="x"\nCONTEXT_PATH=".x/A.md"\nCLI_NAME="x"\nPATH_LINE="p"\nNEEDS_NODE="maybe"\n' > "$TPL_FIX/badnode-box/template.env"
mkdir -p "$TPL_FIX/badapt-box"
printf 'USER="x"\nCONTEXT_PATH=".x/A.md"\nCLI_NAME="x"\nPATH_LINE="p"\nAPT_EXTRAS="zsh -o"\n' > "$TPL_FIX/badapt-box/template.env"
# THE HARD CUT, tenant half (#76). The pre-rename names are gone and must fail # THE HARD CUT, tenant half (#76). The pre-rename names are gone and must fail
# as UNKNOWN — asserted per name, because an alias left in for one tenant is the # as UNKNOWN — asserted per name, because an alias left in for one tenant is the
# shape that survives review: the taxonomy reads complete while one old name # shape that survives review. And the #110 cut on top: the mechanism no longer
# still quietly converges. Checked at BOTH entrypoints, since bootstrap.sh has # KNOWS any agent tenant by name — which '-box' roles exist is the registry's
# its own dispatch list and a name could survive in one and not the other. # fact, so the old names die on the family-suffix rule, not an enumerated list.
for r in claude codex grok staging; do for r in claude codex grok staging; do
check "tenant: the pre-#76 name '$r' is gone (tenant entrypoint)" 2 "unknown tenant role" \ check "tenant: the pre-#76 name '$r' is gone (tenant entrypoint)" 2 "unknown tenant role" \
"$ROOT/commands/bootstrap-tenant.sh" "$r" "$ROOT/commands/bootstrap-tenant.sh" "$r"
@ -593,39 +627,46 @@ for r in claude codex grok staging; do
done done
check "tenant: --help exits 0" 0 "usage:" "$ROOT/commands/bootstrap-tenant.sh" --help check "tenant: --help exits 0" 0 "usage:" "$ROOT/commands/bootstrap-tenant.sh" --help
check "tenant: role required, exit 2" 2 "tenant role required" "$ROOT/commands/bootstrap-tenant.sh" check "tenant: role required, exit 2" 2 "tenant role required" "$ROOT/commands/bootstrap-tenant.sh"
check "tenant: unknown role exits 2" 2 "unknown tenant role" "$ROOT/commands/bootstrap-tenant.sh" potato check "tenant: a suffix-less role exits 2" 2 "unknown tenant role" "$ROOT/commands/bootstrap-tenant.sh" potato
check "tenant: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/bootstrap-tenant.sh" claude-box --nope check "tenant: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/bootstrap-tenant.sh" claude-box --nope
check "tenant: --user needs value" 2 "needs a value" "$ROOT/commands/bootstrap-tenant.sh" claude-box --user check "tenant: --user needs value" 2 "needs a value" "$ROOT/commands/bootstrap-tenant.sh" claude-box --user
check "tenant: bad --user charset exits 2" 2 "invalid user" "$ROOT/commands/bootstrap-tenant.sh" claude-box --user 'fo|o' check "tenant: bad --user charset exits 2" 2 "invalid user" "$ROOT/commands/bootstrap-tenant.sh" claude-box --user 'fo|o'
# The docker converge asserts the DAEMON answers, not just the client binary — # The suffix rule admits ANY '-box' name, so the charset gate must catch a
# a dead dockerd passing `docker --version` is the "linked but cannot run" # crafted one BEFORE it is used as a path component (the valid_version
# scar in daemon form. Grep-pinned so the assert cannot ship deleted. # discipline): uppercase, dots, a leading '-' all die at the name, never in a
# registry lookup.
check "tenant: a crafted role name dies at the charset gate" 2 "invalid tenant role name" \
"$ROOT/commands/bootstrap-tenant.sh" 'UPPER-box'
check "tenant: dockerd effective-state assert is present" 0 "" \ check "tenant: dockerd effective-state assert is present" 0 "" \
grep -qF "docker info" "$ROOT/commands/bootstrap-tenant.sh" grep -qF "docker info" "$ROOT/commands/bootstrap-tenant.sh"
# The machine-role traits die with the tenant story, never "unknown flag" — an # The machine-role traits die with the tenant story, never "unknown flag" — an
# operator reaching for --hostname must learn where the trait family went. # operator coming from the machine families needs the boundary, not a shrug.
check "tenant: trait flags die with the tenant story" 2 "have no traits" \ check "tenant: trait flags die with the tenant story" 2 "have no traits" \
"$ROOT/commands/bootstrap-tenant.sh" claude-box --root-door closed "$ROOT/commands/bootstrap-tenant.sh" claude-box --root-door closed
check "tenant: --hostname dies the same way" 2 "have no traits" \ check "tenant: --hostname dies the same way" 2 "have no traits" \
"$ROOT/commands/bootstrap-tenant.sh" staging-box --hostname my-guest "$ROOT/commands/bootstrap-tenant.sh" staging-box --hostname my-guest
# Dispatch: the machine-role entrypoint hands tenant roles to the tenant # Dispatch: the machine-role entrypoint hands ANY '-box' role to the tenant
# mechanism with args intact (--help reaching the TENANT usage proves both). # mechanism on the family suffix — enumerating them would re-chain template
check "bootstrap: tenant roles dispatch through bootstrap.sh" 0 "claude-box|codex-box|grok-box|kimi-box|staging-box" \ # velocity to rig edits, the exact coupling #110 removes.
check "bootstrap: tenant roles dispatch through bootstrap.sh" 0 "Box TENANT roles" \
"$ROOT/commands/bootstrap.sh" claude-box --help "$ROOT/commands/bootstrap.sh" claude-box --help
# The marker guard fires BEFORE the root check (repo precedent: the coolify check "bootstrap: an unheard-of '-box' role still dispatches (zero code changes)" 0 "Box TENANT roles" \
# marker warning), so the refusals are provable here off fixture markers. A "$ROOT/commands/bootstrap.sh" scratch-box --help
# The tenant marker guard (#83), against marker FIXTURES (never the harness
# machine's real /etc/rig/role): converging a tenant onto a machine-role box or a
# VM host (host=yes) refuses for every tenant — and names the staging PAIR, # VM host (host=yes) refuses for every tenant — and names the staging PAIR,
# because whoever lands here has the two halves confused and wants the metal # because whoever hits it has the halves confused: the guest (staging-box), the metal
# (staging-server). An agent tenant refuses ANY machine-role box; staging-box # (staging-server). An agent tenant refuses ANY machine-role box; staging-box
# tolerates ONLY root-door=open with host=no — that is the guest after its # tolerates exactly the workload-joined guest (root-door=open host=no) and refuses the
# operator-run workload join, and re-converging it is what convergence is for. # rest. These need no registry: the guards run before the resolution, so a
# A closed-door machine (root-door=closed via custom) is NOT that guest, and # poisoning converge is refused even when the registry is unreachable.
# open-door hardening would die at it with root-door=open-specific messaging —
# refuse instead.
TEN_FIX="$(mktemp -d)" TEN_FIX="$(mktemp -d)"
printf 'role=dev-server root-door=closed host=yes join=authkey\n' > "$TEN_FIX/host" printf 'role=workload-server root-door=open host=no join=authkey\n' > "$TEN_FIX/machine"
printf 'role=workload-server root-door=open host=no join=authkey\n' > "$TEN_FIX/machine" printf 'role=custom root-door=closed host=no join=authkey\n' > "$TEN_FIX/closed"
printf 'role=custom root-door=closed host=no join=login\n' > "$TEN_FIX/closed" printf 'role=staging-server root-door=open host=yes join=authkey\n' > "$TEN_FIX/host"
printf 'role=workload class=server\n' > "$TEN_FIX/pre77-machine"
printf 'role=dev class=human\n' > "$TEN_FIX/pre77-human"
printf 'role=claude-box tenant=yes host=no\n' > "$TEN_FIX/tenant" printf 'role=claude-box tenant=yes host=no\n' > "$TEN_FIX/tenant"
check "tenant: staging-box refuses a closed-door machine box" 1 "root door is not open" \ check "tenant: staging-box refuses a closed-door machine box" 1 "root door is not open" \
env RIG_ROLE_MARKER="$TEN_FIX/closed" "$ROOT/commands/bootstrap-tenant.sh" staging-box env RIG_ROLE_MARKER="$TEN_FIX/closed" "$ROOT/commands/bootstrap-tenant.sh" staging-box
@ -635,30 +676,69 @@ check "tenant: the host refusal sends you to the metal half of the pair" 1 "stag
env RIG_ROLE_MARKER="$TEN_FIX/host" "$ROOT/commands/bootstrap-tenant.sh" staging-box env RIG_ROLE_MARKER="$TEN_FIX/host" "$ROOT/commands/bootstrap-tenant.sh" staging-box
check "tenant: an agent role refuses a machine-role box" 1 "never tailnet machines" \ check "tenant: an agent role refuses a machine-role box" 1 "never tailnet machines" \
env RIG_ROLE_MARKER="$TEN_FIX/machine" "$ROOT/commands/bootstrap-tenant.sh" claude-box env RIG_ROLE_MARKER="$TEN_FIX/machine" "$ROOT/commands/bootstrap-tenant.sh" claude-box
# The tenant guard's compat read (#77). This guard asks "does this marker name # The tenant guard's compat read (#77). This guard asks "does this marker name
# a root-door policy?" as its proxy for "is this a real fleet machine?", and it # a root-door policy?" through the resolver, so the pre-#77 spelling counts —
# must ask it in BOTH vocabularies. Kept deliberately at the retired spelling, # pattern-matching one spelling would fail OPEN here: the marker stops looking
# same reason as the close-root fixtures below: a pre-#77 box that stops # like a machine's, the refusal never fires, and a tenant converge clobbers a
# looking like a machine here is the fail-OPEN direction of this rename — the # live fleet box's marker.
# agent-tenant refusal never fires, and `rig bootstrap claude-box` converges a
# tenant straight over a live fleet box, clobbering the marker that holds its
# root-door policy. Do not modernize these two fixtures.
printf 'role=workload-server class=server host=no join=authkey\n' > "$TEN_FIX/pre77-machine"
printf 'role=custom class=human host=no join=login\n' > "$TEN_FIX/pre77-human"
check "tenant: an agent role refuses a PRE-#77 machine marker" 1 "never tailnet machines" \ check "tenant: an agent role refuses a PRE-#77 machine marker" 1 "never tailnet machines" \
env RIG_ROLE_MARKER="$TEN_FIX/pre77-machine" "$ROOT/commands/bootstrap-tenant.sh" claude-box env RIG_ROLE_MARKER="$TEN_FIX/pre77-machine" "$ROOT/commands/bootstrap-tenant.sh" claude-box
check "tenant: staging-box refuses a PRE-#77 closed-door machine box" 1 "root door is not open" \ check "tenant: staging-box refuses a PRE-#77 closed-door machine box" 1 "root door is not open" \
env RIG_ROLE_MARKER="$TEN_FIX/pre77-human" "$ROOT/commands/bootstrap-tenant.sh" staging-box env RIG_ROLE_MARKER="$TEN_FIX/pre77-human" "$ROOT/commands/bootstrap-tenant.sh" staging-box
# ...and the guard needs no registry: an unreachable RIG_TEMPLATES_DIR must
# not stop a refusal that protects a live fleet box.
check "tenant: the marker guard fires even with the registry unreachable" 1 "never tailnet machines" \
env RIG_ROLE_MARKER="$TEN_FIX/machine" RIG_TEMPLATES_DIR=/nonexistent/registry \
"$ROOT/commands/bootstrap-tenant.sh" claude-box
# The definition surface (#110), offline via RIG_TEMPLATES_DIR. An unknown
# role's refusal LISTS what the resolved source actually contains and names
# the source — a misconfigured RIG_TEMPLATES_REPO/_REF/_DIR must be visible
# in the error rather than looking like a typo.
check "tenant: unknown role lists the resolved registry" 2 "scratch-box" \
env RIG_ROLE_MARKER="$TEN_FIX/absent" RIG_TEMPLATES_DIR="$TPL_FIX" \
"$ROOT/commands/bootstrap-tenant.sh" nosuch-box
check "tenant: the unknown-role refusal names the source" 2 "RIG_TEMPLATES_DIR" \
env RIG_ROLE_MARKER="$TEN_FIX/absent" RIG_TEMPLATES_DIR="$TPL_FIX" \
"$ROOT/commands/bootstrap-tenant.sh" nosuch-box
check "tenant: an unreadable RIG_TEMPLATES_DIR refuses loudly" 2 "not a directory" \
env RIG_ROLE_MARKER="$TEN_FIX/absent" RIG_TEMPLATES_DIR=/nonexistent/registry \
"$ROOT/commands/bootstrap-tenant.sh" scratch-box
# _DIR outranks _REF: with both set, resolution must not touch the network —
# provable offline exactly because a fetch attempt would fail here.
check "tenant: RIG_TEMPLATES_DIR outranks RIG_TEMPLATES_REF" 2 "scratch-box" \
env RIG_ROLE_MARKER="$TEN_FIX/absent" RIG_TEMPLATES_DIR="$TPL_FIX" RIG_TEMPLATES_REF=some-branch \
"$ROOT/commands/bootstrap-tenant.sh" nosuch-box
# A malformed definition is refused at bootstrap BY KEY (the box.env
# discipline: parsed, never sourced — so a template cannot execute arbitrary
# shell through the data file). The registry CI's lint is the other gate;
# this one protects a mint served through a source CI never saw.
check "tenant: an unknown key is refused by name" 2 "unknown key: COLOR" \
env RIG_ROLE_MARKER="$TEN_FIX/absent" RIG_TEMPLATES_DIR="$TPL_FIX" \
"$ROOT/commands/bootstrap-tenant.sh" badkey-box
check "tenant: a missing required key is refused by name" 2 "missing required key: CLI_NAME" \
env RIG_ROLE_MARKER="$TEN_FIX/absent" RIG_TEMPLATES_DIR="$TPL_FIX" \
"$ROOT/commands/bootstrap-tenant.sh" missing-box
check "tenant: a non-KEY=\"value\" line is refused with its line number" 2 'not KEY="value"' \
env RIG_ROLE_MARKER="$TEN_FIX/absent" RIG_TEMPLATES_DIR="$TPL_FIX" \
"$ROOT/commands/bootstrap-tenant.sh" garbled-box
check "tenant: a bad NEEDS_NODE value is refused by key" 2 "NEEDS_NODE" \
env RIG_ROLE_MARKER="$TEN_FIX/absent" RIG_TEMPLATES_DIR="$TPL_FIX" \
"$ROOT/commands/bootstrap-tenant.sh" badnode-box
check "tenant: an option riding APT_EXTRAS is refused by key" 2 "APT_EXTRAS" \
env RIG_ROLE_MARKER="$TEN_FIX/absent" RIG_TEMPLATES_DIR="$TPL_FIX" \
"$ROOT/commands/bootstrap-tenant.sh" badapt-box
if [ "$(id -u)" -ne 0 ]; then if [ "$(id -u)" -ne 0 ]; then
# RIG_ROLE_MARKER pinned to the absent fixture: the marker guard runs before # RIG_ROLE_MARKER pinned to the absent fixture: the marker guard runs before
# the root check, and the harness machine may carry a real /etc/rig/role. # the root check, and the harness machine may carry a real /etc/rig/role.
check "tenant: claude-box parses, refuses non-root" 1 "must run as root" \ # Reaching the root check proves the whole pre-root surface passed: the
env RIG_ROLE_MARKER="$TEN_FIX/absent" "$ROOT/commands/bootstrap-tenant.sh" claude-box # name, the flags, the guard, the resolution AND the parse.
check "tenant: codex-box parses, refuses non-root" 1 "must run as root" \ check "tenant: a valid definition parses, refuses non-root" 1 "must run as root" \
env RIG_ROLE_MARKER="$TEN_FIX/absent" "$ROOT/commands/bootstrap-tenant.sh" codex-box env RIG_ROLE_MARKER="$TEN_FIX/absent" RIG_TEMPLATES_DIR="$TPL_FIX" \
check "tenant: grok-box parses, refuses non-root" 1 "must run as root" \ "$ROOT/commands/bootstrap-tenant.sh" scratch-box
env RIG_ROLE_MARKER="$TEN_FIX/absent" "$ROOT/commands/bootstrap-tenant.sh" grok-box check "tenant: staging-box needs no registry at all" 1 "must run as root" \
env RIG_ROLE_MARKER="$TEN_FIX/absent" RIG_TEMPLATES_DIR=/nonexistent/registry \
"$ROOT/commands/bootstrap-tenant.sh" staging-box
check "tenant: staging-box tolerates a workload-joined guest's marker" 1 "must run as root" \ check "tenant: staging-box tolerates a workload-joined guest's marker" 1 "must run as root" \
env RIG_ROLE_MARKER="$TEN_FIX/machine" "$ROOT/commands/bootstrap-tenant.sh" staging-box env RIG_ROLE_MARKER="$TEN_FIX/machine" "$ROOT/commands/bootstrap-tenant.sh" staging-box
# ...and the same guest joined before #77: reaching the root check (rather # ...and the same guest joined before #77: reaching the root check (rather
@ -666,53 +746,127 @@ if [ "$(id -u)" -ne 0 ]; then
check "tenant: staging-box tolerates a PRE-#77 workload-joined guest" 1 "must run as root" \ check "tenant: staging-box tolerates a PRE-#77 workload-joined guest" 1 "must run as root" \
env RIG_ROLE_MARKER="$TEN_FIX/pre77-machine" "$ROOT/commands/bootstrap-tenant.sh" staging-box env RIG_ROLE_MARKER="$TEN_FIX/pre77-machine" "$ROOT/commands/bootstrap-tenant.sh" staging-box
check "tenant: a tenant marker re-runs fine (convergence)" 1 "must run as root" \ check "tenant: a tenant marker re-runs fine (convergence)" 1 "must run as root" \
env RIG_ROLE_MARKER="$TEN_FIX/tenant" "$ROOT/commands/bootstrap-tenant.sh" claude-box env RIG_ROLE_MARKER="$TEN_FIX/tenant" RIG_TEMPLATES_DIR="$TPL_FIX" \
"$ROOT/commands/bootstrap-tenant.sh" scratch-box
else else
echo "skip: tenant non-root refusals (running as root)" echo "skip: tenant non-root refusals (running as root)"
fi fi
rm -rf "$TEN_FIX" rm -rf "$TEN_FIX"
# The per-tenant parameter table and the agent-context renderer are pure lib # The same definition served from a REF (tarball fetch, curl stubbed — the
# functions on purpose (repo precedent: parse_users_file, json_string_array): # release.sh discipline) and from a local DIR must resolve to identical
# the CLI path to them sits behind root + a real tenant user, so the harness # converge inputs: the parsed TPL_* table and the rendered context file are
# proves them here, sourced, non-root and network-free. # everything the mechanism consumes, so identical inputs ARE the identical
tuser() { bash -c 'set -euo pipefail # converge (#110's acceptance criterion, provable offline).
. "$1/commands/lib/tenant-config.sh"; tenant_user "$2"' _ "$ROOT" "$1"; } TPL_WORK="$(mktemp -d)"
tpath() { bash -c 'set -euo pipefail mkdir -p "$TPL_WORK/bin" "$TPL_WORK/stage/rig-templates-testref"
. "$1/commands/lib/tenant-config.sh"; tenant_context_path "$2" "$3"' _ "$ROOT" "$1" "$2"; } cp -r "$TPL_FIX"/. "$TPL_WORK/stage/rig-templates-testref/"
tctx() { bash -c 'set -euo pipefail tar -czf "$TPL_WORK/reg.tar.gz" -C "$TPL_WORK/stage" rig-templates-testref
. "$1/commands/lib/tenant-config.sh"; render_tenant_context "$2"' _ "$ROOT" "$1"; } cat > "$TPL_WORK/bin/curl" <<'CURLEOF'
check "tenant params: agent users are named after their agent" 0 "claude" tuser claude-box #!/usr/bin/env bash
check "tenant params: kimi's user drops the suffix too" 0 "kimi" tuser kimi-box # stub: invoked as `curl -fsSL <url> -o <out>` by templates_resolve
check "tenant params: staging's user is box#69's ops" 0 "ops" tuser staging-box echo "$2" >> "${CURL_LOG:?}"
check "tenant params: claude context lands in ~/.claude/CLAUDE.md" 0 "/home/claude/.claude/CLAUDE.md" tpath claude-box /home/claude cp "${CURL_TARBALL:?}" "$4"
check "tenant params: codex context lands in ~/.codex/AGENTS.md" 0 "/home/codex/.codex/AGENTS.md" tpath codex-box /home/codex CURLEOF
check "tenant params: grok context lands in ~/.grok/AGENTS.md" 0 "/home/grok/.grok/AGENTS.md" tpath grok-box /home/grok chmod +x "$TPL_WORK/bin/curl"
check "tenant params: kimi context lands in ~/.kimi/AGENTS.md" 0 "/home/kimi/.kimi/AGENTS.md" tpath kimi-box /home/kimi # Single quotes deliberate throughout (SC2016): the $-expressions expand in
check "tenant params: staging has no context file" 1 "" tpath staging-box /home/ops # the INNER bash, against the sourced lib's state, never in the harness.
# The box#80 guard note lives ONCE, in the renderer, and every agent's file # shellcheck disable=SC2016
# carries it — the layering decision's whole point: never per-template again. tpl_inputs_script='set -euo pipefail
check "tenant context: claude carries the box#80 guard" 0 "box setup-host" tctx claude-box . "$1/commands/lib/templates.sh"
check "tenant context: codex carries the box#80 guard" 0 "box setup-host" tctx codex-box templates_resolve
check "tenant context: grok carries the box#80 guard" 0 "box setup-host" tctx grok-box template_parse_env "$REGISTRY_DIR/scratch-box/template.env"
check "tenant context: kimi carries the box#80 guard" 0 "box setup-host" tctx kimi-box printf "USER=%s|CTX=%s|CLI=%s|SRC=%s|PATH=%s|NODE=%s|APT=%s\n" \
check "tenant context: the guard says whose host this is not" 0 "not a host you own" tctx claude-box "$TPL_USER" "$TPL_CONTEXT_PATH" "$TPL_CLI_NAME" "$TPL_CLI_SRC" \
check "tenant context: the guard cites box#80" 0 "box#80" tctx claude-box "$TPL_PATH_LINE" "$TPL_NEEDS_NODE" "$TPL_APT_EXTRAS"
check "tenant context: the creds-free contract is stated" 0 "Creds-free by default" tctx claude-box render_tenant_context scratch-box "$REGISTRY_DIR/scratch-box/creds.md"'
check "tenant context: claude names /login as the operator's flow" 0 "/login" tctx claude-box tpl_from_dir() { # tpl_from_dir <outfile> — the local-folder path
check "tenant context: codex names its login flow" 0 "login flow (\`codex\`)" tctx codex-box env RIG_TEMPLATES_DIR="$TPL_FIX" bash -c "$tpl_inputs_script" _ "$ROOT" > "$1"
check "tenant context: grok names its login flow" 0 "grok login" tctx grok-box }
check "tenant context: kimi names its login flow" 0 "Kimi Code OAuth" tctx kimi-box tpl_from_ref() { # tpl_from_ref <outfile> — the tarball path, curl stubbed
check "tenant context: staging renders nothing (no agent lives there)" 1 "" tctx staging-box env PATH="$TPL_WORK/bin:$PATH" CURL_LOG="$TPL_WORK/curl.log" \
CURL_TARBALL="$TPL_WORK/reg.tar.gz" RIG_TEMPLATES_REF=testref \
bash -c "$tpl_inputs_script" _ "$ROOT" > "$1"
}
check "templates: a local DIR resolves and parses" 0 "" tpl_from_dir "$TPL_WORK/from-dir"
check "templates: a REF resolves through the tarball fetch (stubbed curl)" 0 "" \
tpl_from_ref "$TPL_WORK/from-ref"
check "templates: DIR and REF yield byte-identical converge inputs" 0 "" \
diff "$TPL_WORK/from-dir" "$TPL_WORK/from-ref"
# The fetch's first candidate is refs/tags — a tag must outrank a branch that
# happens to share its name (install.sh's own precedence, the pin must win).
check "templates: the fetch asks refs/tags first" 0 "/archive/refs/tags/testref.tar.gz" \
head -n1 "$TPL_WORK/curl.log"
check "templates: the rendered context carries the box#80 guard" 0 "box setup-host" \
cat "$TPL_WORK/from-dir"
check "templates: the guard says whose host this is not" 0 "not a host you own" \
cat "$TPL_WORK/from-dir"
check "templates: the guard cites box#80" 0 "box#80" cat "$TPL_WORK/from-dir"
check "templates: the definition's creds paragraph is spliced in" 0 "The scratch vendor paragraph" \
cat "$TPL_WORK/from-dir"
check "templates: the bootstrap runbook note survives the split" 0 "Bootstrap runbook" \
cat "$TPL_WORK/from-dir"
# The default ref is the IN-TREE PIN (the BOX_RELEASE discipline, ruled on
# #110: pinned, not main-tracked): exactly one greppable assignment, so a pin
# bump is a one-line PR and the drill can read the pin from an installed tree.
# shellcheck disable=SC2016
check "templates: the pin is one greppable line" 0 "1" \
bash -c 'grep -c "^RIG_TEMPLATES_PIN=" "$1/commands/lib/templates.sh"' _ "$ROOT"
# shellcheck disable=SC2016
check "templates: unset knobs fall back to the pin" 0 "the in-tree pin" \
bash -c '. "$1/commands/lib/templates.sh" && templates_source_desc' _ "$ROOT"
# rig template-lint — the registry repo's CI gate, same schema as the mint's
# parser (rig defines validity; rig-templates CI enforces it on every PR).
check "template-lint: --help exits 0" 0 "usage:" "$ROOT/commands/template-lint.sh" --help
check "template-lint: a directory is required" 2 "role directory required" "$ROOT/commands/template-lint.sh"
check "template-lint: dispatched from bin/rig" 0 "usage:" "$ROOT/bin/rig" template-lint --help
check "template-lint: a valid definition passes" 0 "OK: " "$ROOT/commands/template-lint.sh" "$TPL_FIX/scratch-box"
check "template-lint: an unknown key fails by name" 1 "unknown key: COLOR" \
"$ROOT/commands/template-lint.sh" "$TPL_FIX/badkey-box"
check "template-lint: one bad definition fails the whole run" 1 "FAIL: " \
"$ROOT/commands/template-lint.sh" "$TPL_FIX/scratch-box" "$TPL_FIX/badkey-box"
mkdir -p "$TPL_FIX/plain"
cp "$TPL_FIX/scratch-box"/* "$TPL_FIX/plain/"
check "template-lint: a suffix-less role directory is refused (#76)" 1 "family suffix" \
"$ROOT/commands/template-lint.sh" "$TPL_FIX/plain"
mkdir -p "$TPL_FIX/noinstall-box"
cp "$TPL_FIX/scratch-box/template.env" "$TPL_FIX/scratch-box/creds.md" "$TPL_FIX/noinstall-box/"
check "template-lint: a missing install.sh is refused by name" 1 "install.sh missing" \
"$ROOT/commands/template-lint.sh" "$TPL_FIX/noinstall-box"
mkdir -p "$TPL_FIX/blankcreds-box"
cp "$TPL_FIX/scratch-box/template.env" "$TPL_FIX/scratch-box/install.sh" "$TPL_FIX/blankcreds-box/"
printf ' \n\t\n' > "$TPL_FIX/blankcreds-box/creds.md"
check "template-lint: a blank creds.md is refused by name" 1 "creds.md missing or blank" \
"$ROOT/commands/template-lint.sh" "$TPL_FIX/blankcreds-box"
mkdir -p "$TPL_FIX/noshebang-box"
cp "$TPL_FIX/scratch-box/template.env" "$TPL_FIX/scratch-box/creds.md" "$TPL_FIX/noshebang-box/"
printf 'exit 0\n' > "$TPL_FIX/noshebang-box/install.sh"
check "template-lint: an install.sh without a shebang is refused" 1 "no shebang" \
"$ROOT/commands/template-lint.sh" "$TPL_FIX/noshebang-box"
rm -rf "$TPL_FIX" "$TPL_WORK"
# Creds-free BY CONSTRUCTION, provable by absence (box#69's grep-refusal # Creds-free BY CONSTRUCTION, provable by absence (box#69's grep-refusal
# idiom): nothing in the tenant mechanism touches the tailnet, prompts, or # idiom): nothing in the tenant mechanism touches the tailnet, prompts, or
# apt-installs incus. A grep that finds nothing (exit 1) is the pass. # apt-installs incus. A grep that finds nothing (exit 1) is the pass. The
# same absences hold for the templates lib — it fetches DATA, unauthenticated
# by contract, and must never grow a credential to do it.
check "tenant: never touches the tailnet" 1 "" \ check "tenant: never touches the tailnet" 1 "" \
grep -nE 'tailscale|TS_AUTHKEY' "$ROOT/commands/bootstrap-tenant.sh" grep -nE 'tailscale|TS_AUTHKEY' "$ROOT/commands/bootstrap-tenant.sh"
check "tenant: non-interactive — nothing prompts" 1 "" \ check "tenant: non-interactive — nothing prompts" 1 "" \
grep -nE '\bread -r' "$ROOT/commands/bootstrap-tenant.sh" grep -nE '\bread -r' "$ROOT/commands/bootstrap-tenant.sh"
check "tenant: never apt-installs incus (box owns the daemon)" 1 "" \ check "tenant: never apt-installs incus (box owns the daemon)" 1 "" \
grep -nE 'apt-get install.* incus' "$ROOT/commands/bootstrap-tenant.sh" grep -nE 'apt-get install.* incus' "$ROOT/commands/bootstrap-tenant.sh"
check "templates lib: the fetch carries no credential" 1 "" \
grep -nE 'Authorization|gh api|GITHUB_TOKEN' "$ROOT/commands/lib/templates.sh"
# The data file is PARSED, never executed: the parse loop reads lines, and
# no source statement may ever reach template.env. Grep-pinned because the
# failure is silent and total — a sourced template.env is arbitrary shell
# running as root at every mint.
check "templates lib: the parser READS template.env line by line" 0 "" \
grep -qF 'while IFS= read -r line' "$ROOT/commands/lib/templates.sh"
check "templates lib: template.env is never sourced" 1 "" \
grep -nE '(source|^[[:space:]]*\.)[[:space:]]+[^#]*template\.env' "$ROOT/commands/lib/templates.sh" "$ROOT/commands/bootstrap-tenant.sh"
# staging-box's posture rides the SAME hardening code as the machine roles — the # staging-box's posture rides the SAME hardening code as the machine roles — the
# shared lib call is the anti-drift property, so pin the call, not the words. # shared lib call is the anti-drift property, so pin the call, not the words.
check "tenant: staging-box hardens through the shared sshd lib" 0 "" \ check "tenant: staging-box hardens through the shared sshd lib" 0 "" \
@ -746,7 +900,6 @@ check "tenant: the marker write follows the context-file converge" \
# shellcheck disable=SC2016 # shellcheck disable=SC2016
check "tenant: the marker write is gated on the resolved root-door, not a spelling" 0 "" \ check "tenant: the marker write is gated on the resolved root-door, not a spelling" 0 "" \
grep -qxF 'if [ -z "$EXISTING_ROOT_DOOR" ]; then' "$ROOT/commands/bootstrap-tenant.sh" grep -qxF 'if [ -z "$EXISTING_ROOT_DOOR" ]; then' "$ROOT/commands/bootstrap-tenant.sh"
check "coolify: version required, exit 2" 2 "--version" "$ROOT/commands/coolify-install.sh" check "coolify: version required, exit 2" 2 "--version" "$ROOT/commands/coolify-install.sh"
check "coolify: --help exits 0" 0 "usage:" "$ROOT/commands/coolify-install.sh" --help check "coolify: --help exits 0" 0 "usage:" "$ROOT/commands/coolify-install.sh" --help
check "coolify: version needs value" 2 "needs a value" "$ROOT/commands/coolify-install.sh" --version check "coolify: version needs value" 2 "needs a value" "$ROOT/commands/coolify-install.sh" --version