diff --git a/CHANGELOG.md b/CHANGELOG.md index f7c1ad1..3f72587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,29 @@ on the way to cutting its first release, and this file starts there. ### Changed +- **BREAKING: the box tenant roles carry a `-box` suffix** (#76) — the other + half of the rename below. `claude` → `claude-box`, `codex` → `codex-box`, + `grok` → `grok-box`, `staging` → `staging-box`, so a role name always says + which family it belongs to: `-server` builds a fleet machine, `-box` + converges a guest a box minted. + + **The role carries the suffix; nothing inside the guest does.** A tenant user + is the account the box *seed* created (`BOX_USER`) and each agent CLI reads + its own dotdir, so `claude-box` still converges the `claude` user and still + writes `~/.claude/CLAUDE.md`. The suffix is rig's word for "this is a guest", + not a rename of anything the guest contains — no path, no account, and no CLI + binary moved. + + **Migration: hard cut, no aliases**, same as the machine roles. The old names + are refused as unknown tenant roles at both entrypoints — `rig bootstrap + ` and the tenant script directly — and the suite asserts each one at + both, because an alias left in for a single tenant is exactly the shape that + survives review: the taxonomy reads complete while one old name still quietly + converges. The practical consequence is cross-repo: a box seed carrying + `BOX_BOOTSTRAP_ROLE="claude"` now fails its own mint-time bootstrap, so + heavy-duty/box#125 (closing heavy-duty/box#123) updates the seeds and must + land after this. + - **BREAKING: machine roles carry a `-server` suffix, and the VM host gets its name back** (#76) — rig builds two kinds of thing that sit on opposite sides of a trust boundary: tailnet **machines** it converges, and **guests** a box diff --git a/README.md b/README.md index 23890ea..50de763 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,9 @@ 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. And inside a [box](https://github.com/heavy-duty/box)-minted -guest, the same verb converges the **box tenants** — claude, codex, grok, -staging — from thin, creds-free seeds (see *the box tenants* below). +guest, the same verb converges the **box tenants** — claude-box, codex-box, +grok-box, staging-box — from thin, creds-free seeds (see *the box tenants* +below). Philosophy (shared with [box](https://github.com/heavy-duty/box)): **public tool, private state**. rig carries plumbing logic only — no @@ -93,9 +94,9 @@ itself is untouched — what bootstrap converged stays converged. ### `rig bootstrap ` Run as root on the fresh box (over SSH). Convergent — safe to re-run; a -second run changes nothing. (The box TENANT roles — `claude`, `codex`, -`grok`, `staging` — share the verb but are their own family; see *the box -tenants* below.) +second run changes nothing. (The box TENANT roles — `claude-box`, `codex-box`, +`grok-box`, `staging-box` — share the verb but are their own family; the +`-box` suffix says so. See *the box tenants* below.) ```sh rig bootstrap control-plane-server --hostname my-coolify-box --users ./users @@ -167,8 +168,8 @@ its own account; the box CLI's own installer does that (see the `host` trait), and every other way that step can fail lands in apply's existing refusal at the end. -`--users` does **not** reach the box TENANT roles (`claude`, `codex`, `grok`, -`staging`). A tenant is a box-minted *guest*: box auto-runs its bootstrap at +`--users` does **not** reach the box TENANT roles (`claude-box`, `codex-box`, +`grok-box`, `staging-box`). A tenant is a box-minted *guest*: box auto-runs its bootstrap at mint, non-interactively, with no file to hand it; the guest never joins the tailnet and has no SSH door of its own — you enter with `box shell`, gated by the **host's** `incus` grants, which the host's own users file already @@ -373,15 +374,15 @@ VM-host appliance) — and `workstation` is the machine at the keyboard end of all the SSH connections: human-class, `join=login`, entering the tailnet as *your* device rather than the fleet's. -### `rig bootstrap ` — the box tenants +### `rig bootstrap ` — the box tenants Run as root, **inside** a [box](https://github.com/heavy-duty/box)-minted guest. Convergent — safe to re-run; a second run changes nothing. ```sh -rig bootstrap claude # or codex, grok — the agent tenants -rig bootstrap staging # the server tenant (docker + sshd hardening) -rig bootstrap claude --user dev # when the seed's BOX_USER differs +rig bootstrap claude-box # or codex-box, grok-box — the agent tenants +rig bootstrap staging-box # the server tenant (docker + sshd hardening) +rig bootstrap claude-box --user dev # when the seed's BOX_USER differs ``` **The layering** (rig#31 ↔ box#81): a box template stops being where tenant @@ -397,12 +398,18 @@ the guests were the hole. It is **one mechanism, parameterized per tenant** (`lib/tenant-config.sh` holds the whole per-tenant table), not four hand-maintained scripts: -| tenant | user | what lands | -|-----------|----------|------------| -| `claude` | `claude` | the agent toolbelt (git, gh, tmux, ripgrep, jq, age, unzip, build-essential), docker, node 22, the Claude Code CLI on the system PATH, zsh + oh-my-zsh, and `~/.claude/CLAUDE.md` | -| `codex` | `codex` | the toolbelt, docker, node 22, `@openai/codex` on the system PATH, and `~/.codex/AGENTS.md` | -| `grok` | `grok` | the toolbelt, docker, the grok CLI on the system PATH, and `~/.grok/AGENTS.md` | -| `staging` | `ops` | box#69's server posture: docker + the same sshd hardening the machine roles get (shared `lib/sshd.sh`, `class=server` acceptance) | +| tenant role | user | what lands | +|---------------|----------|------------| +| `claude-box` | `claude` | the agent toolbelt (git, gh, tmux, ripgrep, jq, age, unzip, build-essential), docker, node 22, the Claude Code CLI on the system PATH, zsh + oh-my-zsh, and `~/.claude/CLAUDE.md` | +| `codex-box` | `codex` | the toolbelt, docker, node 22, `@openai/codex` on the system PATH, and `~/.codex/AGENTS.md` | +| `grok-box` | `grok` | the toolbelt, docker, the grok CLI on the system PATH, and `~/.grok/AGENTS.md` | +| `staging-box` | `ops` | box#69's server posture: docker + the same sshd hardening the machine roles get (shared `lib/sshd.sh`, `class=server` acceptance) | + +**The role carries the suffix; the user does not.** A tenant user is the +account the box *seed* created (`BOX_USER`) and the agent CLI's own dotdir +hangs off it — `claude-box` converges the `claude` user and writes +`~/.claude/CLAUDE.md`. The suffix is rig's word for "this is a guest", not a +rename of anything inside the box, so nothing in the guest's filesystem moved. Every install is **asserted on effective state**, not exit codes: the CLI must *answer* (`--version`, run as the tenant user — a CLI that exists but cannot @@ -412,14 +419,14 @@ hardening. The CLI also lands on the **system** PATH (`/usr/local/bin`): files, so a PATH export alone is invisible to it. **Creds-free and non-interactive, by contract.** box auto-runs these at mint -(`box exec … rig bootstrap claude`), so nothing here prompts, joins, or admits +(`box exec … rig bootstrap claude-box`), so nothing here prompts, joins, or admits — no tailnet, no keys (the harness pins this by *absence*: no `tailscale`, no prompt, in the shipped script). The one creds-holding step a staging guest eventually needs — the tailnet workload join — stays **operator-run**, exactly as box#69 designed it: `box shell` → `sudo rig bootstrap workload-server --hostname --users ` (or `--no-users` — a guest's door is `box shell`, gated by the host's grants) with a single-use tagged pre-auth key. After that join, re-running -`rig bootstrap staging` still converges docker + hardening and leaves the +`rig bootstrap staging-box` still converges docker + hardening and leaves the workload marker alone — the machine role is the truer statement of what the box became. @@ -1033,6 +1040,6 @@ the real converge belongs to the rehearsal. The end-to-end rehearsal is a throwaway VM/container: pristine Debian → install → `bootstrap workload-server` with a real single-use key → assert the sshd drop-in, tailnet join, and a no-op second run → destroy, remove the node from the tailnet. The tenant rehearsal -is the same shape, creds-free: container + seed user → `rig bootstrap claude` -/ `staging` → assert the CLI answers, docker answers, `sshd -T`, the context +is the same shape, creds-free: container + seed user → `rig bootstrap claude-box` +/ `staging-box` → assert the CLI answers, docker answers, `sshd -T`, the context file — then re-run and watch it no-op. diff --git a/bin/rig b/bin/rig index e0a2b02..3de7315 100755 --- a/bin/rig +++ b/bin/rig @@ -33,13 +33,14 @@ commands: tenant roles); custom and workstation take none. join=login (workstation) needs no key: interactive login, node must come up untagged. Run as root. - bootstrap [--user ] - The box TENANT roles: converge a box-minted guest. claude|codex|grok - land the agent toolbelt (git, gh, tmux, docker), the agent's CLI on + bootstrap [--user ] + 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 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 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 + 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. coolify install --version Pinned Coolify install (AUTOUPDATE=false). Control-plane box only. diff --git a/commands/bootstrap-tenant.sh b/commands/bootstrap-tenant.sh index f5ea140..66ac4b5 100755 --- a/commands/bootstrap-tenant.sh +++ b/commands/bootstrap-tenant.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# rig bootstrap — the box TENANT roles: what a +# rig bootstrap — the box TENANT +# roles ('-box' names the family: a guest, vs the '-server' machine roles): what a # box-minted guest becomes (issue #31). box mints the thin, creds-free seed # (base image, user, rig preinstalled — heavy-duty/box#81); rig converges the # tenant content that used to live in the templates' cloud-init, idempotent and @@ -8,8 +9,8 @@ # lib/tenant-config.sh — never four hand-maintained copies. # # Creds-free BY CONTRACT: box auto-runs these at mint ('box exec … rig -# bootstrap claude'), so every path here is non-interactive and nothing joins -# or admits — no tailnet, no keys, no prompts. staging's tailnet join stays +# bootstrap claude-box'), so every path here is non-interactive and nothing joins +# or admits — no tailnet, no keys, no prompts. staging-box's tailnet join stays # operator-run ('rig bootstrap workload-server' through 'box shell'), exactly the # creds split box#69 designed. # Convergent: safe to re-run; a second run changes nothing. @@ -21,7 +22,7 @@ HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" # shellcheck source=SCRIPTDIR/lib/users-config.sh . "$HERE/lib/users-config.sh" # read_role_marker # shellcheck source=SCRIPTDIR/lib/sshd.sh -. "$HERE/lib/sshd.sh" # harden_sshd (the staging tenant) +. "$HERE/lib/sshd.sh" # harden_sshd (the staging-box tenant) log() { printf 'rig-bootstrap: %s\n' "$*"; } warn() { printf 'rig-bootstrap: WARNING: %s\n' "$*" >&2; } @@ -29,27 +30,29 @@ die() { printf 'rig-bootstrap: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; } usage() { cat <<'EOF' -usage: rig bootstrap [--user ] +usage: rig bootstrap [--user ] Box TENANT roles — what a box-minted guest becomes. box mints the thin, creds-free seed (base image, user, rig preinstalled); this converges the tenant on top, and re-runs converge an existing box to a new spec. - claude|codex|grok the agent tenants: base tooling (git, gh, tmux, …), + claude-box|codex-box|grok-box + the agent tenants: base tooling (git, gh, tmux, …), docker, the agent's CLI on the system PATH, and the agent-context file — including the box#80 guard: never run `box setup-host` or the drill inside a box. - staging the server tenant (box#69's posture): docker + sshd + staging-box the server tenant (box#69's posture): docker + sshd hardening. The tailnet workload join is deliberately NOT here — it holds a credential, so it stays operator-run: `box shell` → `sudo rig bootstrap workload-server` with a tagged pre-auth key. --user the tenant user the box seed created (default: the - role's name; staging defaults to `ops`) + role's name minus the suffix; staging-box defaults to + `ops`) Tenant roles are creds-free and non-interactive by contract — box auto-runs -them at mint (`box exec … rig bootstrap claude`). They take none of the +them at mint (`box exec … rig bootstrap claude-box`). They take none of the machine-role traits (--hostname/--class/--host/--join): a tenant is a guest, not a tailnet machine. Run as root, inside the box. EOF @@ -58,10 +61,10 @@ EOF # --- args (validated before the root check, so errors are testable) --------- ROLE="${1:-}" case "$ROLE" in - claude|codex|grok|staging) shift ;; + claude-box|codex-box|grok-box|staging-box) shift ;; -h|--help) usage; exit 0 ;; - "") usage >&2; die "tenant role required (claude|codex|grok|staging)" 2 ;; - *) die "unknown tenant role: $ROLE (want claude|codex|grok|staging)" 2 ;; + "") usage >&2; die "tenant role required (claude-box|codex-box|grok-box|staging-box)" 2 ;; + *) die "unknown tenant role: $ROLE (want claude-box|codex-box|grok-box|staging-box)" 2 ;; esac TENANT_USER="$(tenant_user "$ROLE")" @@ -74,13 +77,13 @@ while [ $# -gt 0 ]; do --hostname|--class|--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 - # from the box seed, and the one trait-shaped thing a staging guest + # from the box seed, and the one trait-shaped thing a staging-box guest # eventually does (join the tailnet as a workload) is deliberately not # here: it holds a credential, so it stays operator-run. - die "tenant roles have no traits: $1 belongs to the machine roles (control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom). A tenant box's shape comes from its seed; staging's tailnet join is operator-run via 'rig bootstrap workload-server'. The METAL that hosts these guests is 'rig bootstrap staging-server'." 2 ;; + die "tenant roles have no traits: $1 belongs to the machine roles (control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom). A tenant box's shape comes from its seed; staging-box's tailnet join is operator-run via 'rig bootstrap workload-server'. The METAL that hosts these guests is 'rig bootstrap staging-server'." 2 ;; --ts-tag) [ $# -ge 2 ] && shift - die "--ts-tag is gone and tenant roles never join the tailnet anyway. staging's join is operator-run via 'rig bootstrap workload-server', where the tag comes from the pre-auth key." 2 ;; + die "--ts-tag is gone and tenant roles never join the tailnet anyway. staging-box's join is operator-run via 'rig bootstrap workload-server', where the tag comes from the pre-auth key." 2 ;; *) die "unknown flag: $1" 2 ;; esac done @@ -97,28 +100,28 @@ done # markers (repo precedent: the coolify marker warning). Two refusals, one # tolerance: # - host=yes → refuse, every tenant: a VM HOST is the opposite of a guest. -# Names the staging rename out loud — before #31, `staging` was the VM-host -# PRESET; that shape is now spelled through the traits. +# Names the staging PAIR out loud, because whoever lands here has the two +# halves confused: the metal is `staging-server`, the guest `staging-box`. # - class= (agent tenants) → refuse: an agent box is never a tailnet machine. -# - class=server with host=no (staging only) → PROCEED, and leave the marker -# alone: that is the staging guest AFTER its operator-run workload join, and +# - class=server with host=no (staging-box only) → PROCEED, and leave the +# marker alone: that is the guest AFTER its operator-run workload join, and # re-converging docker+hardening on it is exactly what convergence is for. # ONLY that shape — any other class (say class=human, via `custom`) is a -# machine rig built on purpose, and staging hardening it with server rules +# machine rig built on purpose, and staging-box hardening it with server rules # would die with server-specific messaging on a box that was never one. MARKER_PATH="${RIG_ROLE_MARKER:-/etc/rig/role}" EXISTING_MARKER="$(read_role_marker "$MARKER_PATH")" case "$EXISTING_MARKER" in *host=yes*) - die "this box hosts VMs (${EXISTING_MARKER}) — a tenant role converges box GUESTS, never the host under them. Note: before rig#31, 'staging' was the VM-host preset; that shape is now 'rig bootstrap custom --class server --host yes --join authkey' (or 'dev --class server')." ;; + die "this box hosts VMs (${EXISTING_MARKER}) — a tenant role converges box GUESTS, never the host under them. You want the other half of the pair: the metal is 'rig bootstrap staging-server', and the guests it mints are 'staging-box'." ;; *class=*) - if [ "$ROLE" != "staging" ]; then + if [ "$ROLE" != "staging-box" ]; then die "this box already carries a machine role (${EXISTING_MARKER}) — the agent tenants converge box guests, never tailnet machines. If this really is a guest, remove ${MARKER_PATH} and re-run." fi case "$EXISTING_MARKER" in *class=server*) ;; *) - die "this box carries a non-server machine role (${EXISTING_MARKER}) — staging tolerates only the workload-joined guest (class=server host=no). If this really is a staging guest, remove ${MARKER_PATH} and re-run." ;; + die "this box carries a non-server machine role (${EXISTING_MARKER}) — staging-box tolerates only the workload-joined guest (class=server host=no). If this really is a staging-box guest, remove ${MARKER_PATH} and re-run." ;; esac ;; esac @@ -161,13 +164,13 @@ export DEBIAN_FRONTEND=noninteractive log "installing base packages (tenant ${ROLE})" apt-get update -qq case "$ROLE" in - claude) - # The claude tenant keeps zsh (its shell UX ships with the box); the + claude-box) + # The claude-box tenant keeps zsh (its shell UX ships with the box); the # remaining list is the shared agent toolbelt the templates carried. apt-get install -y -qq git gh curl ca-certificates gnupg ripgrep jq tmux age unzip build-essential zsh ;; - codex|grok) + codex-box|grok-box) apt-get install -y -qq git gh curl ca-certificates gnupg ripgrep jq tmux age unzip build-essential ;; - staging) + staging-box) # openssh-server: the hardening drop-in below targets /etc/ssh/sshd_config.d/, # which only exists once the package is installed — pristine container/VM # images (and thin seeds) do not ship it. @@ -177,13 +180,13 @@ esac # contract ('box tmux' runs tmux new-session inside every box) and gh is how # the operator's git credential lands. command -v tmux >/dev/null 2>&1 || die "tmux missing after package install — 'box tmux' (box#65) needs it" -if [ "$ROLE" != "staging" ]; then +if [ "$ROLE" != "staging-box" ]; then command -v gh >/dev/null 2>&1 || die "gh missing after package install" command -v git >/dev/null 2>&1 || die "git missing after package install" fi # --- docker ------------------------------------------------------------------ -# Every tenant gets docker (the templates all carried it; staging's guests run +# Every tenant gets docker (the templates all carried it; staging-box's workloads run # their workloads in it). Docker's own installer, convergence-guarded — its # script is not a no-op when docker exists, so rig supplies the guard. if ! command -v docker >/dev/null 2>&1; then @@ -214,9 +217,9 @@ else warn "no docker group after install — skipping the ${TENANT_USER} group add; check docker's install" fi -# --- node (claude, codex) ---------------------------------------------------- +# --- node (claude-box, codex-box) ---------------------------------------------------- # Codex is an npm global needing Node 22+ (the SCOPED @openai/codex — verified -# upstream when the template was written); the claude tenant ships node as part +# upstream when the template was written); the claude-box tenant ships node as part # of its toolbelt, same pin. grok's CLI is a self-contained binary: no node. node_ok() { command -v node >/dev/null 2>&1 || return 1 @@ -224,7 +227,7 @@ node_ok() { major="$(node --version 2>/dev/null | sed -E 's/^v([0-9]+)\..*$/\1/')" [ "${major:-0}" -ge 22 ] 2>/dev/null } -if [ "$ROLE" = "claude" ] || [ "$ROLE" = "codex" ]; then +if [ "$ROLE" = "claude-box" ] || [ "$ROLE" = "codex-box" ]; then if node_ok; then log "node $(node --version) already present" else @@ -241,10 +244,10 @@ fi # 'box exec -- …' runs a NON-interactive shell that reads no rc # files, so a PATH export alone is invisible to it (the #15 lesson) — and # assert it ANSWERS as the tenant user: a CLI that exists but cannot run is -# what cost the last drill (the grok template's scar). +# what cost the last drill (the grok-box template's scar). CLI="" CLI_SRC="" case "$ROLE" in - claude) + claude-box) CLI=claude CLI_SRC="$TENANT_HOME/.local/bin/claude" if [ ! -e "$CLI_SRC" ]; then log "installing the Claude Code CLI as ${TENANT_USER}" @@ -252,7 +255,7 @@ case "$ROLE" in else log "claude CLI already installed" fi ;; - codex) + codex-box) CLI=codex if ! command -v codex >/dev/null 2>&1; then log "installing the Codex CLI (npm global)" @@ -261,7 +264,7 @@ case "$ROLE" in log "codex CLI already installed" fi CLI_SRC="$(npm prefix -g)/bin/codex" ;; - grok) + grok-box) # The OFFICIAL installer (x.ai/cli/install.sh): installs the CLI as `grok`, # a SYMLINK under $HOME/.grok/bin pointing into its versioned download dir. # Run it AS the tenant user, never root: a symlink into root's 0700 home @@ -273,7 +276,7 @@ case "$ROLE" in else log "grok CLI already installed" fi ;; - staging) ;; # no agent lives on the staging tenant + staging-box) ;; # no agent lives on the staging-box tenant esac if [ -n "$CLI" ]; then [ -e "$CLI_SRC" ] || die "the ${CLI} installer produced no ${CLI_SRC} — upstream layout changed?" @@ -289,11 +292,11 @@ if [ -n "$CLI" ]; then # point: the line must expand in the USER's shell, not here. # shellcheck disable=SC2016 case "$ROLE" in - claude) + claude-box) append_line_once "$TENANT_HOME/.bashrc" 'export PATH="$HOME/.local/bin:$PATH"' ;; - codex) + codex-box) append_line_once "$TENANT_HOME/.bashrc" 'export PATH="$(npm prefix -g)/bin:$PATH"' ;; - grok) + grok-box) append_line_once "$TENANT_HOME/.bashrc" 'export PATH="$HOME/.grok/bin:$PATH"' ;; esac fi @@ -324,11 +327,11 @@ if CTX_PATH="$(tenant_context_path "$ROLE" "$TENANT_HOME")"; then rm -f "$CTX_TMP" fi -# --- claude shell niceties --------------------------------------------------- -# The claude template shipped zsh + oh-my-zsh + tmux mouse mode; they move with +# --- claude-box shell niceties --------------------------------------------------- +# The claude-box template shipped zsh + oh-my-zsh + tmux mouse mode; they move with # the tenant. oh-my-zsh is a cosmetic EXTRA: its failure warns, never aborts a # bootstrap whose real work (CLI, context, docker) already converged. -if [ "$ROLE" = "claude" ]; then +if [ "$ROLE" = "claude-box" ]; then if [ "$(getent passwd "$TENANT_USER" | cut -d: -f7)" != "/usr/bin/zsh" ]; then chsh -s /usr/bin/zsh "$TENANT_USER" log "login shell set to zsh for ${TENANT_USER}" @@ -350,20 +353,20 @@ if [ "$ROLE" = "claude" ]; then append_line_once "$TENANT_HOME/.tmux.conf" 'set -g mouse on' fi -# --- staging server posture -------------------------------------------------- +# --- staging-box server posture -------------------------------------------------- # box#69's posture, minus the join: docker (above) + sshd hardening, through -# the SAME code the machine roles use (lib/sshd.sh) — the staging guest is a +# the SAME code the machine roles use (lib/sshd.sh) — the staging-box guest is a # workload server in waiting, and its door must never be password-open even # before the operator joins it. class=server: root SSH stays the control # plane's future automation door. -if [ "$ROLE" = "staging" ]; then +if [ "$ROLE" = "staging-box" ]; then harden_sshd server fi # --- role marker -------------------------------------------------------------- # Same ground truth the machine roles write, tenant-shaped: no class= (a tenant # has no root-door policy of its own — close-root fails closed on it), and -# host=no so `rig users apply` box-role gating keeps working. staging SKIPS the +# host=no so `rig users apply` box-role gating keeps working. staging-box SKIPS the # write when a machine marker is already present: after the operator-run # workload join, the workload marker is the truer statement and rig never # clobbers state a joined box earned. @@ -383,7 +386,7 @@ else fi log "done — tenant ${ROLE}, user ${TENANT_USER}" -if [ "$ROLE" = "staging" ]; then +if [ "$ROLE" = "staging-box" ]; then log "next (operator-run, holds a credential): box shell → sudo rig bootstrap workload-server --hostname with a tagged pre-auth key" else log "next: creds stay with the operator — ${CLI} authenticates through its own interactive login when a human decides" diff --git a/commands/bootstrap.sh b/commands/bootstrap.sh index 1d9a7de..17c8bfc 100755 --- a/commands/bootstrap.sh +++ b/commands/bootstrap.sh @@ -27,7 +27,7 @@ usage: rig bootstrap | --no-users) [--hostname ] [--class ] [--host ] [--join ] - rig bootstrap [--user ] + rig bootstrap [--user ] (the box TENANT roles — see their own --help; they take no --users, see below) @@ -51,7 +51,7 @@ and per-human accounts keep attribution intact for the times someone does go in. So the complete path is the default path and skipping it is a deliberate --no-users, not an omission. ---users does NOT reach the box TENANT roles (claude|codex|grok|staging). A +--users does NOT reach the box TENANT roles (claude-box|codex-box|grok-box|staging-box). A tenant is a box-minted GUEST: box auto-runs its bootstrap at mint, non-interactively, with no file to hand it; the guest never joins the tailnet and has no SSH door of its own — entry is `box shell`, gated by the HOST's @@ -109,15 +109,15 @@ EOF ROLE="${1:-}" case "$ROLE" in control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom) shift ;; - claude|codex|grok|staging) + claude-box|codex-box|grok-box|staging-box) # The box TENANT roles (#31) are a different family — guests a box mints, # never tailnet machines — and live in their own mechanism, one script # parameterized per tenant. Dispatched here so `rig bootstrap ` # stays the single entrypoint for both families. exec "$HERE/bootstrap-tenant.sh" "$@" ;; -h|--help) usage; exit 0 ;; - "") usage >&2; die "role required (control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom — or a tenant role: claude|codex|grok|staging)" 2 ;; - *) die "unknown role: $ROLE (want control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom — or a tenant role: claude|codex|grok|staging)" 2 ;; + "") usage >&2; die "role required (control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom — or a tenant role: claude-box|codex-box|grok-box|staging-box)" 2 ;; + *) die "unknown role: $ROLE (want control-plane-server|workload-server|runner-server|staging-server|dev-server|workstation|custom — or a tenant role: claude-box|codex-box|grok-box|staging-box)" 2 ;; esac # Role→traits map — the single place a role's shape is declared (issue #26). diff --git a/commands/lib/tenant-config.sh b/commands/lib/tenant-config.sh index cd6950e..7c6938c 100644 --- a/commands/lib/tenant-config.sh +++ b/commands/lib/tenant-config.sh @@ -1,30 +1,34 @@ #!/usr/bin/env bash -# Shared parameters for the box TENANT roles (claude, codex, grok, staging) — +# Shared parameters for the box TENANT roles (claude-box, codex-box, grok-box, +# staging-box) — the '-box' suffix names the FAMILY (a guest a box mints, vs the +# '-server' machine roles rig converges); see #76. # sourced by bootstrap-tenant.sh and by the test harness. Pure text→text, no # side effects: the per-tenant differences live HERE, in one table, so the # mechanism stays one script parameterized per tenant instead of four # hand-maintained copies (repo precedent: parse_users_file, runner-config). # tenant_user — the user the box seed creates (box.env BOX_USER). The -# agent tenants are named after their agent; staging keeps box#69's `ops`. +# agent tenants are named after their agent (minus the suffix — the USER is not +# the role); staging-box keeps box#69's `ops`. tenant_user() { case "$1" in - claude) printf 'claude' ;; - codex) printf 'codex' ;; - grok) printf 'grok' ;; - staging) printf 'ops' ;; + claude-box) printf 'claude' ;; + codex-box) printf 'codex' ;; + grok-box) printf 'grok' ;; + staging-box) printf 'ops' ;; *) return 1 ;; esac } # tenant_context_path — where the agent-context file lands. Each -# agent CLI reads its own instructions file from its own dotdir; staging has no -# agent and no context file (return 1). +# agent CLI reads its own instructions file from its own dotdir (named for the +# agent, not the role — the dotdir is the CLI's, and the suffix is rig's); +# staging-box has no agent and no context file (return 1). tenant_context_path() { case "$1" in - claude) printf '%s/.claude/CLAUDE.md' "$2" ;; - codex) printf '%s/.codex/AGENTS.md' "$2" ;; - grok) printf '%s/.grok/AGENTS.md' "$2" ;; + claude-box) printf '%s/.claude/CLAUDE.md' "$2" ;; + codex-box) printf '%s/.codex/AGENTS.md' "$2" ;; + grok-box) printf '%s/.grok/AGENTS.md' "$2" ;; *) return 1 ;; esac } @@ -32,27 +36,27 @@ tenant_context_path() { # render_tenant_context — the agent-context file's content, on stdout. # One renderer for all three agents: only the creds paragraph is per-vendor, # and the box#80 guard note lives HERE once — never copy-pasted per template. -# staging renders nothing (return 1): no agent lives there. +# staging-box renders nothing (return 1): no agent lives there. render_tenant_context() { local role="$1" creds # The single-quoted markdown below carries literal `$`-free backtick prose; # single quotes are deliberate — nothing in it may expand here. # shellcheck disable=SC2016 case "$role" in - claude) + claude-box) creds='- **Creds-free by default.** The box starts with no Claude and no git credentials. If you need to authenticate Claude, the operator runs `/login` interactively. For git, the operator adds their own credentials (a PAT or `gh auth login`). Never assume credentials are present; never ask for or store secrets on disk beyond what the operator sets up.' ;; - codex) + codex-box) creds='- **Creds-free by default.** The box starts with no OpenAI and no git credentials. If you need to authenticate Codex, the operator runs the login flow (`codex`) interactively. For git, the operator adds their own credentials (a PAT or `gh auth login`). Never assume credentials are present; never ask for or store secrets on disk beyond what the operator sets up.' ;; - grok) + grok-box) creds='- **Creds-free by default.** The box starts with no xAI and no git credentials. If you need to authenticate, the operator runs `grok login` interactively (SuperGrok / X Premium+). For git, the diff --git a/test/cli.sh b/test/cli.sh index 1ff95ac..a5f4729 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -47,12 +47,12 @@ check "bootstrap: --ts-tag is removed (with value), exit 2" 2 "comes from the pr "$ROOT/commands/bootstrap.sh" runner-server --ts-tag tag:server check "bootstrap: --ts-tag is removed (no value), exit 2" 2 "comes from the pre-auth key" \ "$ROOT/commands/bootstrap.sh" runner-server --ts-tag -# staging is a box TENANT role since #31 (the guest, not the VM host), and it +# staging-box is a box TENANT role (the guest, not the VM host), and it # never joins the tailnet — but --ts-tag on it must still die with a story, # not an "unknown flag": scripts from its trait-preset life may pass it, and # the message must say where both the tag AND the join went. -check "bootstrap: staging + removed --ts-tag exits 2" 2 "never join the tailnet" \ - "$ROOT/commands/bootstrap.sh" staging --ts-tag tag:server +check "bootstrap: staging-box + removed --ts-tag exits 2" 2 "never join the tailnet" \ + "$ROOT/commands/bootstrap.sh" staging-box --ts-tag tag:server # The VM-HOST shape has a named role again (staging-server, #76), but it is # still not one of the two the control plane manages, so the catch-all # tag:server refusal must own it. Grep-pinned so a deleted guard cannot ship @@ -80,8 +80,8 @@ fi # UNKNOWN rather than quietly resolving to anything. Asserted per name because # an alias accidentally left in for one role is exactly the shape that survives # review — the taxonomy reads as complete while one old name still works. -# 'staging' is deliberately absent: it still routes to the TENANT mechanism at -# this point in the stack, and the tenant rename lands in its own change. +# 'staging' is deliberately absent HERE: it is a TENANT name, and its own hard +# cut is asserted in the tenant section below, at both entrypoints. for r in control-plane workload runner dev; do check "bootstrap: the pre-#76 name '$r' is gone (hard cut)" 2 "unknown role" \ "$ROOT/commands/bootstrap.sh" "$r" @@ -416,7 +416,7 @@ check "rig usage documents the bootstrap users flags" 0 "(--users | --no- # decision (a box-minted guest has no SSH door of its own; entry is `box shell`, # gated by the HOST's incus grants) is documented in usage and the README. check "bootstrap: --users does not reach the tenant roles" 2 "unknown flag" \ - "$ROOT/commands/bootstrap.sh" claude --users "$BOOT_USERS/ok" + "$ROOT/commands/bootstrap.sh" claude-box --users "$BOOT_USERS/ok" check "bootstrap: usage explains why tenants take no --users" 0 "box-minted GUEST" \ "$ROOT/commands/bootstrap.sh" --help # --- README: the box rename (#12) -------------------------------------------- @@ -451,13 +451,13 @@ if [ "$(id -u)" -ne 0 ]; then check "bootstrap: --users file reaches the root check" 1 "must run as root" \ env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workload-server --users "$BOOT_USERS/ok" check "bootstrap: runner role parses, refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" runner-server --no-users - # staging dispatches to the tenant mechanism now; reaching ITS root check + # staging-box dispatches to the tenant mechanism; reaching ITS root check # through bootstrap.sh proves the dispatch and the tenant arg pass in one go. # RIG_ROLE_MARKER points at an absent fixture: the tenant marker guard runs # before the root check, and the machine running this harness may well have # a real /etc/rig/role of its own. - check "bootstrap: staging dispatches to the tenant mechanism, refuses non-root" 1 "must run as root" \ - env RIG_ROLE_MARKER=/nonexistent/rig-role "$ROOT/commands/bootstrap.sh" staging + check "bootstrap: staging-box dispatches to the tenant mechanism, refuses non-root" 1 "must run as root" \ + env RIG_ROLE_MARKER=/nonexistent/rig-role "$ROOT/commands/bootstrap.sh" staging-box check "bootstrap: dev role parses, refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" dev-server --no-users check "bootstrap: workstation parses, refuses non-root" 1 "must run as root" env -u TS_AUTHKEY "$ROOT/commands/bootstrap.sh" workstation --no-users check "bootstrap: custom parses, refuses non-root" 1 "must run as root" \ @@ -466,7 +466,7 @@ else echo "skip: bootstrap non-root refusals (running as root)" fi -# --- box tenant roles (#31): claude|codex|grok|staging ------------------------ +# --- box tenant roles (#31/#76): claude-box|codex-box|grok-box|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 ` stays the single entrypoint. The real @@ -474,12 +474,23 @@ fi # 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. +# 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. +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" + check "tenant: the pre-#76 name '$r' is gone (bootstrap dispatch)" 2 "unknown role" \ + "$ROOT/commands/bootstrap.sh" "$r" +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: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/bootstrap-tenant.sh" claude --nope -check "tenant: --user needs value" 2 "needs a value" "$ROOT/commands/bootstrap-tenant.sh" claude --user -check "tenant: bad --user charset exits 2" 2 "invalid user" "$ROOT/commands/bootstrap-tenant.sh" claude --user 'fo|o' +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. @@ -488,19 +499,19 @@ check "tenant: dockerd effective-state assert is present" 0 "" \ # The machine-role traits die with the tenant story, never "unknown flag" — an # operator reaching for --hostname must learn where the trait family went. check "tenant: trait flags die with the tenant story" 2 "have no traits" \ - "$ROOT/commands/bootstrap-tenant.sh" claude --class human + "$ROOT/commands/bootstrap-tenant.sh" claude-box --class human check "tenant: --hostname dies the same way" 2 "have no traits" \ - "$ROOT/commands/bootstrap-tenant.sh" staging --hostname my-guest + "$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|codex|grok|staging" \ - "$ROOT/commands/bootstrap.sh" claude --help +check "bootstrap: tenant roles dispatch through bootstrap.sh" 0 "claude-box|codex-box|grok-box|staging-box" \ + "$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 -# VM host (host=yes) refuses for every tenant — and names the staging rename, -# because a pre-#31 staging HOST re-running its old command is exactly who -# lands here. An agent tenant refuses ANY machine-role box; staging tolerates -# ONLY class=server with host=no — that is the staging guest after its +# 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 +# (staging-server). An agent tenant refuses ANY machine-role box; staging-box +# tolerates ONLY class=server with host=no — that is the guest after its # operator-run workload join, and re-converging it is what convergence is for. # A non-server machine (class=human via custom) is NOT that guest, and server # hardening would die at it with server-specific messaging — refuse instead. @@ -508,28 +519,28 @@ TEN_FIX="$(mktemp -d)" printf 'role=dev-server class=human host=yes join=authkey\n' > "$TEN_FIX/host" printf 'role=workload-server class=server host=no join=authkey\n' > "$TEN_FIX/machine" printf 'role=custom class=human host=no join=login\n' > "$TEN_FIX/human" -printf 'role=claude tenant=yes host=no\n' > "$TEN_FIX/tenant" -check "tenant: staging refuses a non-server machine box" 1 "non-server machine role" \ - env RIG_ROLE_MARKER="$TEN_FIX/human" "$ROOT/commands/bootstrap-tenant.sh" staging +printf 'role=claude-box tenant=yes host=no\n' > "$TEN_FIX/tenant" +check "tenant: staging-box refuses a non-server machine box" 1 "non-server machine role" \ + env RIG_ROLE_MARKER="$TEN_FIX/human" "$ROOT/commands/bootstrap-tenant.sh" staging-box check "tenant: refuses a host=yes box (a VM host is never a guest)" 1 "hosts VMs" \ - env RIG_ROLE_MARKER="$TEN_FIX/host" "$ROOT/commands/bootstrap-tenant.sh" claude -check "tenant: the host refusal names the old staging preset's new spelling" 1 "custom --class server --host yes" \ - env RIG_ROLE_MARKER="$TEN_FIX/host" "$ROOT/commands/bootstrap-tenant.sh" staging + env RIG_ROLE_MARKER="$TEN_FIX/host" "$ROOT/commands/bootstrap-tenant.sh" claude-box +check "tenant: the host refusal sends you to the metal half of the pair" 1 "staging-server" \ + 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 + env RIG_ROLE_MARKER="$TEN_FIX/machine" "$ROOT/commands/bootstrap-tenant.sh" claude-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 parses, refuses non-root" 1 "must run as root" \ - env RIG_ROLE_MARKER="$TEN_FIX/absent" "$ROOT/commands/bootstrap-tenant.sh" claude - check "tenant: codex parses, refuses non-root" 1 "must run as root" \ - env RIG_ROLE_MARKER="$TEN_FIX/absent" "$ROOT/commands/bootstrap-tenant.sh" codex - check "tenant: grok parses, refuses non-root" 1 "must run as root" \ - env RIG_ROLE_MARKER="$TEN_FIX/absent" "$ROOT/commands/bootstrap-tenant.sh" grok - check "tenant: staging 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 + 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 + 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 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 + env RIG_ROLE_MARKER="$TEN_FIX/tenant" "$ROOT/commands/bootstrap-tenant.sh" claude-box else echo "skip: tenant non-root refusals (running as root)" fi @@ -545,24 +556,24 @@ 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 -check "tenant params: staging's user is box#69's ops" 0 "ops" tuser staging -check "tenant params: claude context lands in ~/.claude/CLAUDE.md" 0 "/home/claude/.claude/CLAUDE.md" tpath claude /home/claude -check "tenant params: codex context lands in ~/.codex/AGENTS.md" 0 "/home/codex/.codex/AGENTS.md" tpath codex /home/codex -check "tenant params: grok context lands in ~/.grok/AGENTS.md" 0 "/home/grok/.grok/AGENTS.md" tpath grok /home/grok -check "tenant params: staging has no context file" 1 "" tpath staging /home/ops +check "tenant params: agent users are named after their agent" 0 "claude" tuser claude-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: 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 -check "tenant context: codex carries the box#80 guard" 0 "box setup-host" tctx codex -check "tenant context: grok carries the box#80 guard" 0 "box setup-host" tctx grok -check "tenant context: the guard says whose host this is not" 0 "not a host you own" tctx claude -check "tenant context: the guard cites box#80" 0 "box#80" tctx claude -check "tenant context: the creds-free contract is stated" 0 "Creds-free by default" tctx claude -check "tenant context: claude names /login as the operator's flow" 0 "/login" tctx claude -check "tenant context: codex names its login flow" 0 "login flow (\`codex\`)" tctx codex -check "tenant context: grok names its login flow" 0 "grok login" tctx grok -check "tenant context: staging renders nothing (no agent lives there)" 1 "" tctx staging +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: 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: staging renders nothing (no agent lives there)" 1 "" tctx staging-box # 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. @@ -572,15 +583,15 @@ 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" -# staging'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. -check "tenant: staging hardens through the shared sshd lib" 0 "" \ +check "tenant: staging-box hardens through the shared sshd lib" 0 "" \ grep -qE '^[[:space:]]*harden_sshd server$' "$ROOT/commands/bootstrap-tenant.sh" check "tenant: docker lands via docker's own installer" 0 "" \ grep -q "get.docker.com" "$ROOT/commands/bootstrap-tenant.sh" # The #15 lesson pinned: 'box exec' shells read no rc files, so the CLI must # land on the SYSTEM path — and a claimed install is verified, not trusted: -# it must ANSWER as the tenant user (the grok template's scar: linked but +# it must ANSWER as the tenant user (the grok-box template's scar: linked but # cannot run). The $CLI/$TENANT_USER are literals we grep for in the script. # shellcheck disable=SC2016 check "tenant: the agent CLI lands on the system PATH" 0 "" \ @@ -1473,7 +1484,7 @@ fi # post-close-root state, strictly harder than what bootstrap installs. Byte-grep # the widened assertion so a revert cannot ship green. The hardening block # lives in lib/sshd.sh since #31 — ONE converger shared by the machine roles -# and the staging tenant — so the greps pin the lib, and a call-site grep pins +# and the staging-box tenant — so the greps pin the lib, and a call-site grep pins # that bootstrap actually runs it (a function nobody calls is not hardening). check "sshd lib: permitrootlogin assertion accepts the closed state" 0 "" \ grep -qF "permitrootlogin (no|prohibit-password|without-password)" "$ROOT/commands/lib/sshd.sh"