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:
parent
11b02eb070
commit
79363c2631
3 changed files with 279 additions and 118 deletions
18
bin/rig
18
bin/rig
|
|
@ -37,15 +37,19 @@ commands:
|
|||
Leave the tailnet only when /etc/rig/role proves rig performed the
|
||||
join, then remove that marker. Refuses for pre-existing or old unknown
|
||||
joins, and while a GitHub runner is installed. Run as root.
|
||||
bootstrap <claude-box|codex-box|grok-box|kimi-box|staging-box> [--user <name>]
|
||||
bootstrap <role>-box [--user <name>]
|
||||
The box TENANT roles: converge a box-minted guest. The '-box' suffix
|
||||
names the family (a guest, vs the '-server' machine roles above).
|
||||
claude-box|codex-box|grok-box|kimi-box land the agent toolbelt (git, gh, tmux, docker), the agent's CLI on
|
||||
the system PATH, and the agent-context file (with the box#80 guard:
|
||||
never run box setup-host inside a box). staging-box 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.
|
||||
The agent tenants (claude-box, codex-box, grok-box, kimi-box, …) are
|
||||
DEFINED in the heavy-duty/rig-templates registry — resolved via
|
||||
RIG_TEMPLATES_DIR > RIG_TEMPLATES_REF > the in-tree pin — and land
|
||||
the agent toolbelt (git, gh, tmux, docker), the agent's CLI on the
|
||||
system PATH, and the agent-context file (with the box#80 guard:
|
||||
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>
|
||||
Pinned Coolify install (AUTOUPDATE=false). Control-plane box only.
|
||||
coolify backup install [options]
|
||||
|
|
|
|||
|
|
@ -104,7 +104,13 @@ while [ $# -gt 0 ]; do
|
|||
-h|--help) usage; exit 0 ;;
|
||||
--user)
|
||||
[ $# -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)
|
||||
# The machine-role traits, refused with a story rather than "unknown
|
||||
# flag": a tenant is a guest, not a tailnet machine — its shape comes
|
||||
|
|
@ -119,36 +125,6 @@ while [ $# -gt 0 ]; do
|
|||
esac
|
||||
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 ------------------------------------------------------------------
|
||||
# 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
|
||||
|
|
@ -195,6 +171,34 @@ if [ -n "$EXISTING_ROOT_DOOR" ]; then
|
|||
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"
|
||||
if [ -r /etc/os-release ]; then
|
||||
# Sourced in a subshell: os-release defines VERSION, NAME, ID, etc. —
|
||||
|
|
|
|||
313
test/cli.sh
313
test/cli.sh
|
|
@ -572,19 +572,53 @@ else
|
|||
echo "skip: bootstrap non-root refusals (running as root)"
|
||||
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),
|
||||
# parameterized per tenant through lib/tenant-config.sh, dispatched from
|
||||
# bootstrap.sh so `rig bootstrap <role>` stays the single entrypoint. 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: the whole
|
||||
# arg/refusal surface, the pure parameter table, the rendered agent-context
|
||||
# file (guard note included), and grep-pins on the shipped script.
|
||||
# parameterized per DEFINITION fetched from the template registry
|
||||
# (heavy-duty/rig-templates; lib/templates.sh resolves RIG_TEMPLATES_DIR >
|
||||
# RIG_TEMPLATES_REF > the in-tree pin), dispatched from bootstrap.sh on the
|
||||
# '-box' FAMILY SUFFIX so `rig bootstrap <role>` stays the single entrypoint
|
||||
# and a template added to the registry is mintable with zero code changes
|
||||
# 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
|
||||
# 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
|
||||
# still quietly converges. Checked at BOTH entrypoints, since bootstrap.sh has
|
||||
# its own dispatch list and a name could survive in one and not the other.
|
||||
# shape that survives review. And the #110 cut on top: the mechanism no longer
|
||||
# KNOWS any agent tenant by name — which '-box' roles exist is the registry's
|
||||
# fact, so the old names die on the family-suffix rule, not an enumerated list.
|
||||
for r in claude codex grok staging; do
|
||||
check "tenant: the pre-#76 name '$r' is gone (tenant entrypoint)" 2 "unknown tenant role" \
|
||||
"$ROOT/commands/bootstrap-tenant.sh" "$r"
|
||||
|
|
@ -593,39 +627,46 @@ for r in claude codex grok staging; do
|
|||
done
|
||||
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: 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: --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'
|
||||
# The docker converge asserts the DAEMON answers, not just the client binary —
|
||||
# a dead dockerd passing `docker --version` is the "linked but cannot run"
|
||||
# scar in daemon form. Grep-pinned so the assert cannot ship deleted.
|
||||
# The suffix rule admits ANY '-box' name, so the charset gate must catch a
|
||||
# crafted one BEFORE it is used as a path component (the valid_version
|
||||
# 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 "" \
|
||||
grep -qF "docker info" "$ROOT/commands/bootstrap-tenant.sh"
|
||||
# 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" \
|
||||
"$ROOT/commands/bootstrap-tenant.sh" claude-box --root-door closed
|
||||
check "tenant: --hostname dies the same way" 2 "have no traits" \
|
||||
"$ROOT/commands/bootstrap-tenant.sh" staging-box --hostname my-guest
|
||||
# Dispatch: the machine-role entrypoint hands tenant roles to the tenant
|
||||
# mechanism with args intact (--help reaching the TENANT usage proves both).
|
||||
check "bootstrap: tenant roles dispatch through bootstrap.sh" 0 "claude-box|codex-box|grok-box|kimi-box|staging-box" \
|
||||
# Dispatch: the machine-role entrypoint hands ANY '-box' role to the tenant
|
||||
# mechanism on the family suffix — enumerating them would re-chain template
|
||||
# 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
|
||||
# The marker guard fires BEFORE the root check (repo precedent: the coolify
|
||||
# marker warning), so the refusals are provable here off fixture markers. A
|
||||
check "bootstrap: an unheard-of '-box' role still dispatches (zero code changes)" 0 "Box TENANT roles" \
|
||||
"$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,
|
||||
# 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
|
||||
# tolerates ONLY root-door=open with host=no — that is the guest after its
|
||||
# operator-run workload join, and re-converging it is what convergence is for.
|
||||
# A closed-door machine (root-door=closed via custom) is NOT that guest, and
|
||||
# open-door hardening would die at it with root-door=open-specific messaging —
|
||||
# refuse instead.
|
||||
# tolerates exactly the workload-joined guest (root-door=open host=no) and refuses the
|
||||
# rest. These need no registry: the guards run before the resolution, so a
|
||||
# poisoning converge is refused even when the registry is unreachable.
|
||||
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=custom root-door=closed host=no join=login\n' > "$TEN_FIX/closed"
|
||||
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=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"
|
||||
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
|
||||
|
|
@ -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
|
||||
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
|
||||
|
||||
# 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
|
||||
# must ask it in BOTH vocabularies. Kept deliberately at the retired spelling,
|
||||
# same reason as the close-root fixtures below: a pre-#77 box that stops
|
||||
# looking like a machine here is the fail-OPEN direction of this rename — the
|
||||
# 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"
|
||||
# a root-door policy?" through the resolver, so the pre-#77 spelling counts —
|
||||
# pattern-matching one spelling would fail OPEN here: the marker stops looking
|
||||
# like a machine's, the refusal never fires, and a tenant converge clobbers a
|
||||
# live fleet box's marker.
|
||||
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
|
||||
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
|
||||
# ...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
|
||||
# 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.
|
||||
check "tenant: claude-box parses, refuses non-root" 1 "must run as root" \
|
||||
env RIG_ROLE_MARKER="$TEN_FIX/absent" "$ROOT/commands/bootstrap-tenant.sh" claude-box
|
||||
check "tenant: codex-box parses, refuses non-root" 1 "must run as root" \
|
||||
env RIG_ROLE_MARKER="$TEN_FIX/absent" "$ROOT/commands/bootstrap-tenant.sh" codex-box
|
||||
check "tenant: grok-box parses, refuses non-root" 1 "must run as root" \
|
||||
env RIG_ROLE_MARKER="$TEN_FIX/absent" "$ROOT/commands/bootstrap-tenant.sh" grok-box
|
||||
# Reaching the root check proves the whole pre-root surface passed: the
|
||||
# name, the flags, the guard, the resolution AND the parse.
|
||||
check "tenant: a valid definition parses, refuses non-root" 1 "must run as root" \
|
||||
env RIG_ROLE_MARKER="$TEN_FIX/absent" RIG_TEMPLATES_DIR="$TPL_FIX" \
|
||||
"$ROOT/commands/bootstrap-tenant.sh" scratch-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" \
|
||||
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
|
||||
|
|
@ -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" \
|
||||
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" \
|
||||
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
|
||||
echo "skip: tenant non-root refusals (running as root)"
|
||||
fi
|
||||
rm -rf "$TEN_FIX"
|
||||
|
||||
# The per-tenant parameter table and the agent-context renderer are pure lib
|
||||
# functions on purpose (repo precedent: parse_users_file, json_string_array):
|
||||
# the CLI path to them sits behind root + a real tenant user, so the harness
|
||||
# proves them here, sourced, non-root and network-free.
|
||||
tuser() { bash -c 'set -euo pipefail
|
||||
. "$1/commands/lib/tenant-config.sh"; tenant_user "$2"' _ "$ROOT" "$1"; }
|
||||
tpath() { bash -c 'set -euo pipefail
|
||||
. "$1/commands/lib/tenant-config.sh"; tenant_context_path "$2" "$3"' _ "$ROOT" "$1" "$2"; }
|
||||
tctx() { bash -c 'set -euo pipefail
|
||||
. "$1/commands/lib/tenant-config.sh"; render_tenant_context "$2"' _ "$ROOT" "$1"; }
|
||||
check "tenant params: agent users are named after their agent" 0 "claude" tuser claude-box
|
||||
check "tenant params: kimi's user drops the suffix too" 0 "kimi" tuser kimi-box
|
||||
check "tenant params: staging's user is box#69's ops" 0 "ops" tuser staging-box
|
||||
check "tenant params: claude context lands in ~/.claude/CLAUDE.md" 0 "/home/claude/.claude/CLAUDE.md" tpath claude-box /home/claude
|
||||
check "tenant params: codex context lands in ~/.codex/AGENTS.md" 0 "/home/codex/.codex/AGENTS.md" tpath codex-box /home/codex
|
||||
check "tenant params: grok context lands in ~/.grok/AGENTS.md" 0 "/home/grok/.grok/AGENTS.md" tpath grok-box /home/grok
|
||||
check "tenant params: kimi context lands in ~/.kimi/AGENTS.md" 0 "/home/kimi/.kimi/AGENTS.md" tpath kimi-box /home/kimi
|
||||
check "tenant params: staging has no context file" 1 "" tpath staging-box /home/ops
|
||||
# The box#80 guard note lives ONCE, in the renderer, and every agent's file
|
||||
# carries it — the layering decision's whole point: never per-template again.
|
||||
check "tenant context: claude carries the box#80 guard" 0 "box setup-host" tctx claude-box
|
||||
check "tenant context: codex carries the box#80 guard" 0 "box setup-host" tctx codex-box
|
||||
check "tenant context: grok carries the box#80 guard" 0 "box setup-host" tctx grok-box
|
||||
check "tenant context: kimi carries the box#80 guard" 0 "box setup-host" tctx kimi-box
|
||||
check "tenant context: the guard says whose host this is not" 0 "not a host you own" tctx claude-box
|
||||
check "tenant context: the guard cites box#80" 0 "box#80" tctx claude-box
|
||||
check "tenant context: the creds-free contract is stated" 0 "Creds-free by default" tctx claude-box
|
||||
check "tenant context: claude names /login as the operator's flow" 0 "/login" tctx claude-box
|
||||
check "tenant context: codex names its login flow" 0 "login flow (\`codex\`)" tctx codex-box
|
||||
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
|
||||
check "tenant context: staging renders nothing (no agent lives there)" 1 "" tctx staging-box
|
||||
# The same definition served from a REF (tarball fetch, curl stubbed — the
|
||||
# release.sh discipline) and from a local DIR must resolve to identical
|
||||
# converge inputs: the parsed TPL_* table and the rendered context file are
|
||||
# everything the mechanism consumes, so identical inputs ARE the identical
|
||||
# converge (#110's acceptance criterion, provable offline).
|
||||
TPL_WORK="$(mktemp -d)"
|
||||
mkdir -p "$TPL_WORK/bin" "$TPL_WORK/stage/rig-templates-testref"
|
||||
cp -r "$TPL_FIX"/. "$TPL_WORK/stage/rig-templates-testref/"
|
||||
tar -czf "$TPL_WORK/reg.tar.gz" -C "$TPL_WORK/stage" rig-templates-testref
|
||||
cat > "$TPL_WORK/bin/curl" <<'CURLEOF'
|
||||
#!/usr/bin/env bash
|
||||
# stub: invoked as `curl -fsSL <url> -o <out>` by templates_resolve
|
||||
echo "$2" >> "${CURL_LOG:?}"
|
||||
cp "${CURL_TARBALL:?}" "$4"
|
||||
CURLEOF
|
||||
chmod +x "$TPL_WORK/bin/curl"
|
||||
# Single quotes deliberate throughout (SC2016): the $-expressions expand in
|
||||
# the INNER bash, against the sourced lib's state, never in the harness.
|
||||
# shellcheck disable=SC2016
|
||||
tpl_inputs_script='set -euo pipefail
|
||||
. "$1/commands/lib/templates.sh"
|
||||
templates_resolve
|
||||
template_parse_env "$REGISTRY_DIR/scratch-box/template.env"
|
||||
printf "USER=%s|CTX=%s|CLI=%s|SRC=%s|PATH=%s|NODE=%s|APT=%s\n" \
|
||||
"$TPL_USER" "$TPL_CONTEXT_PATH" "$TPL_CLI_NAME" "$TPL_CLI_SRC" \
|
||||
"$TPL_PATH_LINE" "$TPL_NEEDS_NODE" "$TPL_APT_EXTRAS"
|
||||
render_tenant_context scratch-box "$REGISTRY_DIR/scratch-box/creds.md"'
|
||||
tpl_from_dir() { # tpl_from_dir <outfile> — the local-folder path
|
||||
env RIG_TEMPLATES_DIR="$TPL_FIX" bash -c "$tpl_inputs_script" _ "$ROOT" > "$1"
|
||||
}
|
||||
tpl_from_ref() { # tpl_from_ref <outfile> — the tarball path, curl stubbed
|
||||
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
|
||||
# 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 "" \
|
||||
grep -nE 'tailscale|TS_AUTHKEY' "$ROOT/commands/bootstrap-tenant.sh"
|
||||
check "tenant: non-interactive — nothing prompts" 1 "" \
|
||||
grep -nE '\bread -r' "$ROOT/commands/bootstrap-tenant.sh"
|
||||
check "tenant: never apt-installs incus (box owns the daemon)" 1 "" \
|
||||
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
|
||||
# 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 "" \
|
||||
|
|
@ -746,7 +900,6 @@ check "tenant: the marker write follows the context-file converge" \
|
|||
# shellcheck disable=SC2016
|
||||
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"
|
||||
|
||||
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: version needs value" 2 "needs a value" "$ROOT/commands/coolify-install.sh" --version
|
||||
|
|
|
|||
Loading…
Reference in a new issue