diff --git a/CHANGELOG.md b/CHANGELOG.md index 8388990..492b4f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,60 @@ which records not just what changed but what each drill run proved. ## Unreleased +### Changed + +- **The tenant templates carry rig's family suffix: `claude` → `claude-box`, + `codex` → `codex-box`, `grok` → `grok-box`, `staging` → `staging-box`** + (#123, following heavy-duty/rig#76) — rig is growing a second family of + roles, and once a `staging` role can mean either a fleet machine or a box + tenant, the bare name stops naming anything. rig's answer is a suffix on + the role itself — `-server` for fleet machines, `-box` for box tenants — + and box's answer is that a template keeps being named for the role it + converges. Templates are the only surface that spells a rig role out loud + (`BOX_BOOTSTRAP_ROLE`, auto-run at mint since #81), so a template whose + directory says one thing and whose role key says another is a trap with a + 15-minute fuse: it mints clean and dies at convergence. `blank` keeps its + name — it seeds no tenant role, sets no `BOX_BOOTSTRAP_ROLE`, and + therefore has nothing to agree with. Two namespaces move apart here and + only one of them moved: the template name and the role are now + `claude-box`, while the seed USER stays `claude`, because that is the user + the rig role converges and the one `box shell` lands in. `test/cli.sh` + pins the pair per tenant rather than each half alone — a future rename + that moves one and forgets the other mints a box whose role dies looking + for a user nobody created. + + **This lands after rig's rename, not before, and the ordering is not a + preference.** The seeds install rig from `RIG_REPO`/`RIG_REF`, which + default to `heavy-duty/rig@main` and are unpinned until rig#32's releases + — so a box minted from these templates asks whatever `main` happens to be + for `rig bootstrap claude-box`. Against a pre-rename rig that role does + not exist, `cmd_new` refuses to call the box ready, and the operator is + handed a failed mint for a change neither repo has finished making. Merged + in the other order the window closes instead of opening: rig's roles are a + hard cut with no aliases, so the day rig's rename lands, every unmerged + box seed naming a bare role is the broken one. + + One deliberate asymmetry: the mint-time hints in `cmd_new` match both the + new and the old spelling of `user.box.template`. That is not an alias for + the role — nothing here softens the cut, and `rig bootstrap claude` is + gone. It reads a stamp left on an *instance* at its own mint time, which + every box minted before today carries forever and every clone carries + forward; refusing the old spelling would cut nothing over and only drop + the login hint on boxes that predate the rename, the same reason + `user.claudebox` is honored everywhere else. `migrate-host.sh` stamps + re-homed legacy boxes `claude-box`, the name the template has today, so a + re-homed box looks like a fresh mint rather than a fossil. + + The **machine**-role half of rig's rename reaches box too, in one place: + the tailnet workload join box prints as a next step for a `staging-box` + guest is now `sudo rig bootstrap workload-server`. box never runs it — it + holds a pre-auth key, and that it stays operator-run is the absence + keeping box creds-free end to end — but box does *print* it, in three + places that all had to move together (`cmd_new`'s hint, the + `staging-box` seed's own comment, and the README). A next step an + operator copy-pastes is as wrong as a role box executes, and it fails + later and further from the cause. + ## 0.8.0 — 2026-07-19 ### Added diff --git a/README.md b/README.md index 71e535b..68f5f93 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ **Headless, trust-less, throwaway dev VMs.** One command mints a fresh, network-isolated Incus box from a **template**; the coding-agent templates -hand you a CLI agent on Debian 13 — `claude` (Claude Code), `codex` (OpenAI -Codex), `grok` (xAI Grok) — **box mints, [rig](https://github.com/heavy-duty/rig) +hand you a CLI agent on Debian 13 — `claude-box` (Claude Code), `codex-box` +(OpenAI Codex), `grok-box` (xAI Grok) — **box mints, [rig](https://github.com/heavy-duty/rig) converges**: the template is a thin seed, and the agent tooling lands via a creds-free `rig bootstrap` role auto-run at mint ([#81](https://github.com/heavy-duty/box/issues/81)). The box is the product @@ -217,13 +217,14 @@ VM boundary itself is proven on real hardware, like the rest of the drill). ## Quick start ```sh -box new --name work --template claude # a creds-free coding-agent box (~10 min cold) -box shell work # enter as the template's user +box new --name work --template claude-box # a creds-free coding-agent box (~10 min cold) +box shell work # enter as the template's user ``` -Pick whichever coding-agent template you like — `claude`, `codex`, `grok` — or -`blank` for none. Inside the box, authenticate as needed. The `claude` template -looks like this; the others follow the same shape with their own login step: +Pick whichever coding-agent template you like — `claude-box`, `codex-box`, +`grok-box` — or `blank` for none. Inside the box, authenticate as needed. The +`claude-box` template looks like this; the others follow the same shape with +their own login step: ```sh claude # then run /login — copy the URL (press c), open it @@ -238,13 +239,13 @@ claude # if the repo has .box/, the agent reads it and No coding agent is special — each is one template among several, and adding another is just another directory. What ships today: -| Template | What it becomes | -| --------- | ---------------------------------------------------------------------- | -| `blank` | Bare Debian 13 — same isolation, no tooling. The default. | -| `claude` | Claude Code, creds-free — where this project started | -| `codex` | OpenAI Codex CLI, creds-free | -| `grok` | xAI Grok CLI, creds-free | -| `staging` | Server-class: docker + sshd hardening via rig; VM-only, autostarts | +| Template | What it becomes | +| ------------- | ------------------------------------------------------------------ | +| `blank` | Bare Debian 13 — same isolation, no tooling. The default. | +| `claude-box` | Claude Code, creds-free — where this project started | +| `codex-box` | OpenAI Codex CLI, creds-free | +| `grok-box` | xAI Grok CLI, creds-free | +| `staging-box` | Server-class: docker + sshd hardening via rig; VM-only, autostarts | **Templates are thin seeds; rig does the becoming** ([#81](https://github.com/heavy-duty/box/issues/81)). A template is a @@ -255,8 +256,11 @@ pin tokens below). The seed is deliberately small — the tenant user, tmux, and [rig](https://github.com/heavy-duty/rig) preinstalled, nothing that joins a tailnet or admits credentials — and after cloud-init settles, box auto-runs the template's **creds-free** tenant role inside the guest -(`rig bootstrap claude` / `codex` / `grok` / `staging`, -[rig#31](https://github.com/heavy-duty/rig/issues/31)). The agent CLI, +(`rig bootstrap claude-box` / `codex-box` / `grok-box` / `staging-box`, +[rig#31](https://github.com/heavy-duty/rig/issues/31); the roles carry a +family suffix — `-box` for box tenants, `-server` for fleet machines — and a +template is named for the role it converges, +[rig#76](https://github.com/heavy-duty/rig/issues/76)). The agent CLI, docker, the server posture and the agent-context file all come from that role — convergent and idempotent, so the same command re-run later converges an *existing* box to a newer spec (`box shell ` → @@ -265,9 +269,9 @@ an *existing* box to a newer spec (`box shell ` → `box setup-host`, `box teardown-host` or the drill *inside* a box — once, from rig's roles, instead of copy-pasted per template. -**Anything that joins or admits stays operator-run.** The `staging` box's -tailnet workload join holds a pre-auth key, so box only prints it as the -next step — `box shell `, then `sudo rig bootstrap workload` — and +**Anything that joins or admits stays operator-run.** The `staging-box` +tenant's tailnet workload join holds a pre-auth key, so box only prints it as the +next step — `box shell `, then `sudo rig bootstrap workload-server` — and never sees the key ([#69](https://github.com/heavy-duty/box/issues/69)'s split, kept). @@ -279,9 +283,9 @@ carries `@RIG_REPO@`/`@RIG_REF@` tokens that box resolves at mint from the environment: ```sh -box new --name work --template claude # heavy-duty/rig @ main +box new --name work --template claude-box # heavy-duty/rig @ main RIG_REPO=you/rig RIG_REF=my-branch \ - box new --name trial --template claude # a rig branch under review + box new --name trial --template claude-box # a rig branch under review ``` Both directions of that edge track `main` unpinned today — said honestly, diff --git a/bin/box b/bin/box index 344dd65..d877ee9 100755 --- a/bin/box +++ b/bin/box @@ -185,7 +185,7 @@ OPTIONS EXAMPLES # mint a claude box and log in inside it — the tool never handles your token - box new --name work --template claude + box new --name work --template claude-box box shell work # then: run 'claude', then /login # log in once, reuse forever: checkpoint the authed box, clone from it @@ -242,13 +242,13 @@ help_cmd() { case "$1" in new) cat <<'EOF' Mint a box. Without --from, launches a fresh box from a template (default: -blank — bare Debian 13, nobody home; --template claude gets Claude Code +blank — bare Debian 13, nobody home; --template claude-box gets Claude Code installed, creds-free, ~10 min cold). With --from, clones an existing box or one of its snapshots — login state, git creds and clones carry over, isolation is preserved, and the clone knows its template's user without being told. -A tenant template (claude, codex, grok, staging) is a THIN seed — the user, +A tenant template (claude-box, codex-box, grok-box, staging-box) is a THIN seed — the user, tmux, rig (#81) — and after cloud-init box auto-runs the creds-free tenant role inside it ('rig bootstrap ', rig#31): that role installs the agent CLI / server posture and the agent-context file. rig is preinstalled @@ -286,8 +286,8 @@ BOX_LAUNCH_TIMEOUT= (default 600) bounds the 'incus launch' call — a launch that overruns it fails loudly instead of hanging forever (#93). box new --name scratch # blank, the default - box new --name work --template claude - box new --name lean --template claude --cpu 2 --memory 3GiB + box new --name work --template claude-box + box new --name lean --template claude-box --cpu 2 --memory 3GiB box new --name feature --from work/authed EOF ;; @@ -473,7 +473,7 @@ The in-box server must listen on 0.0.0.0:, not only its own loopback — a VM's forwarder connects to the box over the network. Inside an isolated box that is safe: nothing but this proxy can reach the port. - box new --name web --template claude + box new --name web --template claude-box box shell web # inside: run a dev server on 0.0.0.0:3000 box expose web 3000 # then open http://127.0.0.1:3000 in your browser EOF @@ -1183,16 +1183,25 @@ cmd_new() { fi fi fi - # The login hint belongs to the claude template — read the EFFECTIVE - # template off the instance, so a clone of a claude box gets it too and a - # blank box is not told to run a binary it doesn't have. + # The login hint belongs to the claude-box template — read the EFFECTIVE + # template off the instance, so a clone of a claude-box box gets it too and + # a blank box is not told to run a binary it doesn't have. + # + # Both spellings match, and that is not an alias for the ROLE. The role + # names are a hard cut (rig#76) — 'rig bootstrap claude' is gone, so the + # seeds ask for 'claude-box' and nothing here softens that. What these arms + # read is user.box.template, a stamp left on an INSTANCE at its own mint + # time: every box minted before the rename carries the bare name forever, + # and a clone carries it forward. Refusing the old spelling here would not + # cut anything over, it would only drop the hint on boxes that predate the + # rename — the same reason user.claudebox is honored everywhere else. local eff; eff="$(incus config get "$instance" user.box.template 2>/dev/null || true)" - [ -z "$eff" ] && [ "$(incus config get "$instance" user.claudebox 2>/dev/null || true)" = 1 ] && eff=claude - if [ "$eff" = claude ]; then + [ -z "$eff" ] && [ "$(incus config get "$instance" user.claudebox 2>/dev/null || true)" = 1 ] && eff=claude-box + if [ "$eff" = claude-box ] || [ "$eff" = claude ]; then echo "box: ready — 'box shell $name'. Log into Claude inside: run 'claude' then /login." - elif [ "$eff" = staging ]; then + elif [ "$eff" = staging-box ] || [ "$eff" = staging ]; then echo "box: ready — 'box shell $name'. The tailnet join stays operator-run (it holds a key box must never see):" - echo " box shell $name # then: sudo rig bootstrap workload --hostname $name" + echo " box shell $name # then: sudo rig bootstrap workload-server --hostname $name" else echo "box: ready — 'box shell $name'." fi diff --git a/docs/box-design.md b/docs/box-design.md index 92a0bca..c4b0cce 100644 --- a/docs/box-design.md +++ b/docs/box-design.md @@ -1,8 +1,8 @@ # box design `box` is a CLI that mints and manages **trust-less, network-isolated VMs -with a coding agent installed** (`claude`, `codex`, `grok`, or `blank` for -none). It is infrastructure, not a project provisioner. +with a coding agent installed** (`claude-box`, `codex-box`, `grok-box`, or +`blank` for none). It is infrastructure, not a project provisioner. See issue #3 for the full reframe and rationale. This doc captures the durable design decisions. @@ -60,15 +60,17 @@ instead of pretending to sanitize it. A template is a **thin, creds-free seed** — base image, the tenant user, tmux, and [rig](https://github.com/heavy-duty/rig) preinstalled — and what the box *becomes* lives in rig's bootstrap roles (rig#31): box auto-runs the -template's creds-free tenant role after cloud-init (`rig bootstrap claude` / -`codex` / `grok` / `staging`), which installs the agent CLI or server -posture. The split is deliberate: cloud-init is a first-boot one-shot — +template's creds-free tenant role after cloud-init (`rig bootstrap claude-box` +/ `codex-box` / `grok-box` / `staging-box` — the roles carry a family suffix, +`-box` for box tenants and `-server` for fleet machines, and the templates are +named for the roles they converge, rig#76), which installs the agent CLI or +server posture. The split is deliberate: cloud-init is a first-boot one-shot — not convergent, not re-runnable, only parse-and-grep testable — while a rig role is an idempotent script with effective-state asserts that can also converge an *existing* box to a newer spec. Anything that joins a tailnet or -holds a key (staging's workload join) stays operator-run through -`box shell`; box prints it as a next step and never sees the key. The seed's -rig install is pinned by `RIG_REPO`/`RIG_REF` at mint (default +holds a key (the staging-box tenant's workload join) stays operator-run +through `box shell`; box prints it as a next step and never sees the key. The +seed's rig install is pinned by `RIG_REPO`/`RIG_REF` at mint (default `heavy-duty/rig@main`, unpinned — the honest edge until rig#32's releases), and box's template suite holds the line with fail-closed absence greps: no agent CLI, no docker, no tailscale, no context-file heredocs in any diff --git a/docs/box-recipe.md b/docs/box-recipe.md index 6544c93..3effb11 100644 --- a/docs/box-recipe.md +++ b/docs/box-recipe.md @@ -2,9 +2,9 @@ `box` mints trust-less, creds-free, isolated VMs with a coding agent already installed (`box new/shell/snapshot/restore/exec/down/start/rm/status`) — the -`claude`, `codex`, and `grok` templates each ship a CLI agent. The tool knows -**nothing** about your project. There is no `install` step and no host-run -setup script. +`claude-box`, `codex-box`, and `grok-box` templates each ship a CLI agent. The +tool knows **nothing** about your project. There is no `install` step and no +host-run setup script. A project makes itself easy to stand up inside a box by shipping an optional `.box/` folder. This folder is **agent-facing documentation** — read and diff --git a/drill/drill.sh b/drill/drill.sh index c40d8be..2d63a50 100755 --- a/drill/drill.sh +++ b/drill/drill.sh @@ -378,7 +378,7 @@ if ! timeout -k 10 300 ~/.local/share/box/current/host/setup-host.sh; then fi inf "host setup complete" -# A real server has room for the claude template's resources (8GiB/4cpu), and +# A real server has room for the claude-box template's resources (8GiB/4cpu), and # drilling the real numbers is worth more than drilling shrunken ones. Only # shrink if we must. Since 0.4.0 resources are per-box, stamped from the # template at mint — a profile edit no longer reaches them; the supported @@ -386,9 +386,9 @@ inf "host setup complete" ram="$(awk '/MemTotal/{print int($2/1024/1024)}' /proc/meminfo)" if [ "$ram" -lt 20 ]; then export BOX_MEMORY=3GiB BOX_CPU=2 - note "host has ${ram}GiB RAM — minting at 3GiB/2cpu via BOX_MEMORY/BOX_CPU (the claude template's 8GiB/4cpu is what was NOT drilled)" + note "host has ${ram}GiB RAM — minting at 3GiB/2cpu via BOX_MEMORY/BOX_CPU (the claude-box template's 8GiB/4cpu is what was NOT drilled)" else - inf "host has ${ram}GiB RAM — drilling the claude template's resources (8GiB/4cpu) unchanged" + inf "host has ${ram}GiB RAM — drilling the claude-box template's resources (8GiB/4cpu) unchanged" fi KVM=0; [ -e /dev/kvm ] && KVM=1 @@ -492,10 +492,10 @@ fi # --- templates: the mint surface is itself a surface to test ---------------- tpl_missing="" -for t in blank claude codex grok; do +for t in blank claude-box codex-box grok-box; do box templates 2>/dev/null | grep -q "^ $t" || tpl_missing="$tpl_missing $t" done -[ -z "$tpl_missing" ] && ok "templates: lists blank, claude, codex, grok" \ +[ -z "$tpl_missing" ] && ok "templates: lists blank, claude-box, codex-box, grok-box" \ || no "templates listing is missing:$tpl_missing" box new --name tpl --template nosuch 2>&1 | grep -q 'no such template' \ && ok "unknown template refused, points at 'box templates'" || no "an unknown template was not refused" @@ -551,19 +551,22 @@ else fi # The generic mechanic (metadata, placement, user, isolation parity) is proven -# once by blank+claude and needs no per-template repeat. What a NEW template +# once by blank+claude-box and needs no per-template repeat. What a NEW template # still has to prove is its own payload: the CLI installs, lands on the # non-interactive exec PATH, and answers --version. One mint each. +# The box NAME stays the bare agent name — it is what the pre-flight banner +# announces and what teardown deletes — while the TEMPLATE carries rig#76's +# family suffix. They are two different namespaces and only one of them moved. for t in codex grok; do case "$t" in codex) bin=codex; user=codex ;; grok) bin=grok; user=grok ;; esac printf '\n minting a %s box (cold — validates the template install)…\n' "$t" - if mint_box "/tmp/mint-$t.log" --name "$t" --template "$t"; then + if mint_box "/tmp/mint-$t.log" --name "$t" --template "$t-box"; then [ "$(incus config get "$t" user.box.user 2>/dev/null)" = "$user" ] \ && ok "$t: template user stamped ($user)" || no "$t: user.box.user not $user" if timeout -k 5 30 box exec "$t" -- "$bin" --version /dev/null 2>&1; then ok "$t: '$bin --version' answers via box exec — installed and on the non-interactive PATH" else - no "$t: '$bin --version' FAILED via exec — not installed, or not on exec's PATH (the claude template's #15 bug)" + no "$t: '$bin --version' FAILED via exec — not installed, or not on exec's PATH (the claude-box template's #15 bug)" inf "PATH as exec sees it: $(timeout -k 5 20 box exec "$t" -- printenv PATH /dev/null)" # Do not throw the evidence away — say WHAT the installer actually left. # Do NOT throw the evidence away — say what the installer actually left @@ -583,10 +586,10 @@ for t in codex grok; do fi done -printf '\n minting a claude box (cold, ~10 min)…\n' +printf '\n minting a claude-box box (cold, ~10 min)…\n' t0=$SECONDS -if mint_box /tmp/mint-drill.log --name drill --template claude; then - ok "box new --name drill --template claude ($((SECONDS - t0))s)" +if mint_box /tmp/mint-drill.log --name drill --template claude-box; then + ok "box new --name drill --template claude-box ($((SECONDS - t0))s)" else no "box new FAILED — tail: $(tail -3 /tmp/mint-drill.log | tr '\n' ' ')" timeout -k 5 60 incus delete -f drill >/dev/null 2>&1 @@ -799,7 +802,7 @@ fi # =========================================================================== phase "E. box expose — a deliberate loopback door (#55)" # =========================================================================== -# archive is a running claude box (node is installed). Start a DETACHED +# archive is a running claude-box box (node is installed). Start a DETACHED # listener on 0.0.0.0 inside it, expose the port, and prove the door works # from the HOST's loopback. Then prove removing it closes the door, and that a # NON-exposed port still obeys the ingress drop — the feature must not diff --git a/host/migrate-host.sh b/host/migrate-host.sh index 19a2dde..f31fefd 100644 --- a/host/migrate-host.sh +++ b/host/migrate-host.sh @@ -64,7 +64,11 @@ legacy_boxes() { } # Re-home one box. Legacy boxes are all claude boxes (the only template the old -# tool minted), so the new metadata is the claude template's. +# tool minted), so the new metadata is the claude-box template's. The stamp +# names the template as it is called TODAY, not as it was called when the box +# was minted: it is what 'box shell' and the mint hints read, so a re-homed +# box should look like a fresh claude-box mint, not like a fossil (rig#76's +# family suffix — the template is named for the role it converges). rehome_one() { local b="$1" st incus config get "$b" user.claudebox >/dev/null 2>&1 || { warn "$b is not a legacy box (no user.claudebox tag) — skipping"; return 1; } @@ -75,7 +79,7 @@ rehome_one() { # 1. TAG FIRST — additive and reversible. A box that stops here is still a # valid legacy box (the old tag is untouched) AND now a new one. - incus config set "$b" user.box=1 user.box.template=claude user.box.user=claude \ + incus config set "$b" user.box=1 user.box.template=claude-box user.box.user=claude \ || { warn "$b: could not set new metadata — left untouched"; return 1; } # 2. Stop, reassign the profile (this is the network move), restart. Incus diff --git a/templates/claude/box.env b/templates/claude-box/box.env similarity index 61% rename from templates/claude/box.env rename to templates/claude-box/box.env index 6ae89fc..325ea3b 100644 --- a/templates/claude/box.env +++ b/templates/claude-box/box.env @@ -1,7 +1,11 @@ -# The claude template — a thin, creds-free seed (#81): Debian 13, the +# The claude-box template — a thin, creds-free seed (#81): Debian 13, the # 'claude' user, tmux and rig. What the box BECOMES — the Claude Code CLI, # docker, node, the agent-context file with its #80 guard — is rig's job: -# box auto-runs 'rig bootstrap claude' after mint (heavy-duty/rig#31). +# box auto-runs 'rig bootstrap claude-box' after mint (heavy-duty/rig#31). +# The template is named for the role it converges, suffix and all +# (heavy-duty/rig#76): rig's roles carry a family suffix — '-server' for +# fleet machines, '-box' for box tenants — and a seed that named the bare +# 'claude' would ask a post-rename rig for a role that no longer exists. # KEY="value" only. Parsed against an allowlist, never sourced; there is no # key for a network or a security flag, on purpose — the shared box-net # profile is the placement contract and no template can weaken it. @@ -14,4 +18,4 @@ BOX_USER="claude" BOX_CPU="4" BOX_MEMORY="8GiB" BOX_DISK="60GiB" -BOX_BOOTSTRAP_ROLE="claude" +BOX_BOOTSTRAP_ROLE="claude-box" diff --git a/templates/claude/user-data.yaml b/templates/claude-box/user-data.yaml similarity index 91% rename from templates/claude/user-data.yaml rename to templates/claude-box/user-data.yaml index f5ae844..9a7baf8 100644 --- a/templates/claude/user-data.yaml +++ b/templates/claude-box/user-data.yaml @@ -2,7 +2,7 @@ # A thin, creds-free seed (#81): the tenant user, tmux (#65), and rig — # nothing that joins a tailnet or admits credentials, no agent CLI, no # docker, no context-file heredoc. Everything this box becomes comes from -# 'rig bootstrap claude' (heavy-duty/rig#31), which box auto-runs after +# 'rig bootstrap claude-box' (heavy-duty/rig#31), which box auto-runs after # mint; the agent-context file — including the #80 guard (never run # 'box setup-host' or the drill inside a box) — is rendered by that role, # once, instead of being copy-pasted per template. @@ -21,7 +21,7 @@ packages: - ca-certificates runcmd: # Preinstall rig so the box can converge — and re-converge — via - # 'rig bootstrap claude'. @RIG_REPO@/@RIG_REF@ are the pin point (#81): + # 'rig bootstrap claude-box'. @RIG_REPO@/@RIG_REF@ are the pin point (#81): # box substitutes them at mint from the RIG_REPO/RIG_REF environment # (default heavy-duty/rig @ main — unpinned, tracking main, the same # honest edge as rig's own unpinned box install, until rig#32 ships a diff --git a/templates/codex/box.env b/templates/codex-box/box.env similarity index 61% rename from templates/codex/box.env rename to templates/codex-box/box.env index 21b30d4..4546ff2 100644 --- a/templates/codex/box.env +++ b/templates/codex-box/box.env @@ -1,7 +1,11 @@ -# The codex template — a thin, creds-free seed (#81): Debian 13, the +# The codex-box template — a thin, creds-free seed (#81): Debian 13, the # 'codex' user, tmux and rig. What the box BECOMES — the OpenAI Codex CLI, # docker, node, the agent-context file with its #80 guard — is rig's job: -# box auto-runs 'rig bootstrap codex' after mint (heavy-duty/rig#31). +# box auto-runs 'rig bootstrap codex-box' after mint (heavy-duty/rig#31). +# The template is named for the role it converges, suffix and all +# (heavy-duty/rig#76): rig's roles carry a family suffix — '-server' for +# fleet machines, '-box' for box tenants — and a seed that named the bare +# 'codex' would ask a post-rename rig for a role that no longer exists. # KEY="value" only. Parsed against an allowlist, never sourced; there is no # key for a network or a security flag, on purpose — the shared box-net # profile is the placement contract and no template can weaken it. @@ -14,4 +18,4 @@ BOX_USER="codex" BOX_CPU="4" BOX_MEMORY="8GiB" BOX_DISK="60GiB" -BOX_BOOTSTRAP_ROLE="codex" +BOX_BOOTSTRAP_ROLE="codex-box" diff --git a/templates/codex/user-data.yaml b/templates/codex-box/user-data.yaml similarity index 91% rename from templates/codex/user-data.yaml rename to templates/codex-box/user-data.yaml index aaedfe8..32fa730 100644 --- a/templates/codex/user-data.yaml +++ b/templates/codex-box/user-data.yaml @@ -2,7 +2,7 @@ # A thin, creds-free seed (#81): the tenant user, tmux (#65), and rig — # nothing that joins a tailnet or admits credentials, no agent CLI, no # docker, no context-file heredoc. Everything this box becomes comes from -# 'rig bootstrap codex' (heavy-duty/rig#31), which box auto-runs after +# 'rig bootstrap codex-box' (heavy-duty/rig#31), which box auto-runs after # mint; the agent-context file — including the #80 guard (never run # 'box setup-host' or the drill inside a box) — is rendered by that role, # once, instead of being copy-pasted per template. @@ -21,7 +21,7 @@ packages: - ca-certificates runcmd: # Preinstall rig so the box can converge — and re-converge — via - # 'rig bootstrap codex'. @RIG_REPO@/@RIG_REF@ are the pin point (#81): + # 'rig bootstrap codex-box'. @RIG_REPO@/@RIG_REF@ are the pin point (#81): # box substitutes them at mint from the RIG_REPO/RIG_REF environment # (default heavy-duty/rig @ main — unpinned, tracking main, the same # honest edge as rig's own unpinned box install, until rig#32 ships a diff --git a/templates/grok/box.env b/templates/grok-box/box.env similarity index 61% rename from templates/grok/box.env rename to templates/grok-box/box.env index d0a7dd9..fc91c81 100644 --- a/templates/grok/box.env +++ b/templates/grok-box/box.env @@ -1,7 +1,11 @@ -# The grok template — a thin, creds-free seed (#81): Debian 13, the +# The grok-box template — a thin, creds-free seed (#81): Debian 13, the # 'grok' user, tmux and rig. What the box BECOMES — the xAI Grok CLI, # docker, the agent-context file with its #80 guard — is rig's job: -# box auto-runs 'rig bootstrap grok' after mint (heavy-duty/rig#31). +# box auto-runs 'rig bootstrap grok-box' after mint (heavy-duty/rig#31). +# The template is named for the role it converges, suffix and all +# (heavy-duty/rig#76): rig's roles carry a family suffix — '-server' for +# fleet machines, '-box' for box tenants — and a seed that named the bare +# 'grok' would ask a post-rename rig for a role that no longer exists. # KEY="value" only. Parsed against an allowlist, never sourced; there is no # key for a network or a security flag, on purpose — the shared box-net # profile is the placement contract and no template can weaken it. @@ -14,4 +18,4 @@ BOX_USER="grok" BOX_CPU="4" BOX_MEMORY="8GiB" BOX_DISK="60GiB" -BOX_BOOTSTRAP_ROLE="grok" +BOX_BOOTSTRAP_ROLE="grok-box" diff --git a/templates/grok/user-data.yaml b/templates/grok-box/user-data.yaml similarity index 91% rename from templates/grok/user-data.yaml rename to templates/grok-box/user-data.yaml index 24b4178..28c2ab3 100644 --- a/templates/grok/user-data.yaml +++ b/templates/grok-box/user-data.yaml @@ -2,7 +2,7 @@ # A thin, creds-free seed (#81): the tenant user, tmux (#65), and rig — # nothing that joins a tailnet or admits credentials, no agent CLI, no # docker, no context-file heredoc. Everything this box becomes comes from -# 'rig bootstrap grok' (heavy-duty/rig#31), which box auto-runs after +# 'rig bootstrap grok-box' (heavy-duty/rig#31), which box auto-runs after # mint; the agent-context file — including the #80 guard (never run # 'box setup-host' or the drill inside a box) — is rendered by that role, # once, instead of being copy-pasted per template. @@ -21,7 +21,7 @@ packages: - ca-certificates runcmd: # Preinstall rig so the box can converge — and re-converge — via - # 'rig bootstrap grok'. @RIG_REPO@/@RIG_REF@ are the pin point (#81): + # 'rig bootstrap grok-box'. @RIG_REPO@/@RIG_REF@ are the pin point (#81): # box substitutes them at mint from the RIG_REPO/RIG_REF environment # (default heavy-duty/rig @ main — unpinned, tracking main, the same # honest edge as rig's own unpinned box install, until rig#32 ships a diff --git a/templates/staging/box.env b/templates/staging-box/box.env similarity index 62% rename from templates/staging/box.env rename to templates/staging-box/box.env index 2996662..e69bbc9 100644 --- a/templates/staging/box.env +++ b/templates/staging-box/box.env @@ -1,9 +1,14 @@ -# The staging template — a thin, creds-free, server-class seed (#81, the +# The staging-box template — a thin, creds-free, server-class seed (#81, the # re-cut of #69's layering): Debian 13, the 'ops' user, tmux and rig. The # server posture — docker, sshd hardening — is rig's job: box auto-runs -# 'rig bootstrap staging' after mint (heavy-duty/rig#31). The tailnet +# 'rig bootstrap staging-box' after mint (heavy-duty/rig#31). The template is +# named for the role it converges, suffix and all (heavy-duty/rig#76): rig's +# roles carry a family suffix — '-server' for fleet machines, '-box' for box +# tenants — and a seed that named the bare 'staging' would ask a post-rename +# rig for a role that no longer exists. Server-CLASS, not a fleet machine: +# this is still a box tenant, so it takes '-box', not '-server'. The tailnet # workload join holds a key and therefore STAYS operator-run: -# box shell # then: sudo rig bootstrap workload --hostname +# box shell # then: sudo rig bootstrap workload-server --hostname # KEY="value" only. Parsed against an allowlist, never sourced; there is no # key for a network or a security flag, on purpose — the shared box-net # profile is the placement contract and no template can weaken it. @@ -21,4 +26,4 @@ BOX_MEMORY="8GiB" BOX_DISK="60GiB" BOX_REQUIRE_VM="1" BOX_AUTOSTART="1" -BOX_BOOTSTRAP_ROLE="staging" +BOX_BOOTSTRAP_ROLE="staging-box" diff --git a/templates/staging/user-data.yaml b/templates/staging-box/user-data.yaml similarity index 86% rename from templates/staging/user-data.yaml rename to templates/staging-box/user-data.yaml index 444c35d..3a230d3 100644 --- a/templates/staging/user-data.yaml +++ b/templates/staging-box/user-data.yaml @@ -2,9 +2,9 @@ # A thin, creds-free, server-class seed (#81): the 'ops' user, tmux (#65), # and rig — nothing that joins a tailnet or admits credentials, no docker, # no sshd config, no keys. The server posture comes from -# 'rig bootstrap staging' (heavy-duty/rig#31), which box auto-runs after +# 'rig bootstrap staging-box' (heavy-duty/rig#31), which box auto-runs after # mint; the tailnet workload join holds a pre-auth key and stays -# operator-run ('box shell' → 'sudo rig bootstrap workload'), exactly as +# operator-run ('box shell' → 'sudo rig bootstrap workload-server'), exactly as # #69 designed it — box never sees the key. users: - name: ops @@ -22,7 +22,7 @@ packages: - ca-certificates runcmd: # Preinstall rig so the box can converge — and re-converge — via - # 'rig bootstrap staging'. @RIG_REPO@/@RIG_REF@ are the pin point (#81): + # 'rig bootstrap staging-box'. @RIG_REPO@/@RIG_REF@ are the pin point (#81): # box substitutes them at mint from the RIG_REPO/RIG_REF environment # (default heavy-duty/rig @ main — unpinned, tracking main, the same # honest edge as rig's own unpinned box install, until rig#32 ships a diff --git a/test/cli.sh b/test/cli.sh index 2ca1d2e..2857c5b 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -310,16 +310,22 @@ done # is its trust boundary (its guest runs docker, via rig) and a server returns # from a host reboot without an operator. Pinned to the FILE so neither can # quietly vanish in a rewrite. -check "staging: demands VM mode (BOX_REQUIRE_VM=1)" 0 "" \ - grep -qx 'BOX_REQUIRE_VM="1"' "$ROOT/templates/staging/box.env" -check "staging: demands autostart (BOX_AUTOSTART=1)" 0 "" \ - grep -qx 'BOX_AUTOSTART="1"' "$ROOT/templates/staging/box.env" -check "staging: the tenant role is 'staging'" 0 "ROLE=staging" tpl "$ROOT" staging -check "staging: the seed user is rig's default for the role ('ops')" 0 "USER=ops" tpl "$ROOT" staging -# The agent tenants: role = user = template name, rig's default mapping. -for t in claude codex grok; do - check "$t: role and user are '$t' (rig's default tenant mapping)" 0 "USER=$t REQUIRE_VM= AUTOSTART= ROLE=$t" \ - tpl "$ROOT" "$t" +check "staging-box: demands VM mode (BOX_REQUIRE_VM=1)" 0 "" \ + grep -qx 'BOX_REQUIRE_VM="1"' "$ROOT/templates/staging-box/box.env" +check "staging-box: demands autostart (BOX_AUTOSTART=1)" 0 "" \ + grep -qx 'BOX_AUTOSTART="1"' "$ROOT/templates/staging-box/box.env" +check "staging-box: the tenant role is 'staging-box'" 0 "ROLE=staging-box" tpl "$ROOT" staging-box +check "staging-box: the seed user is rig's default for the role ('ops')" 0 "USER=ops" tpl "$ROOT" staging-box +# The agent tenants. Two names, not one: the TEMPLATE is named for the rig role +# it converges — suffix and all, since rig's roles carry a family suffix +# ('-box' for box tenants, '-server' for fleet machines, rig#76) — while the +# seed USER stays the bare agent name, because that is the user rig's role +# converges and the one 'box shell' lands in. The pairing is the whole point of +# pinning it here: a rename that moves one and forgets the other mints a box +# whose role dies looking for a user that was never created. +for u in claude codex grok; do + check "$u-box: role is '$u-box', seed user is '$u' (rig's tenant mapping)" \ + 0 "USER=$u REQUIRE_VM= AUTOSTART= ROLE=$u-box" tpl "$ROOT" "$u-box" done # blank stays a box with NOBODY home: no rig, no role — same isolation, no # tooling, and nothing auto-runs in it. @@ -426,11 +432,11 @@ check "new: the overran-but-registered branch says so (not the wedge story)" 0 " # shellcheck disable=SC2016 # the $-strings are literals in the target file check "new: BOX_LAUNCH_TIMEOUT is documented in box help new" 0 "" bash -c ' "'"$ROOT"'/bin/box" help new | grep "BOX_LAUNCH_TIMEOUT" | grep -q 600' -# staging's creds-holding join stays OPERATOR-run: cmd_new may print it as a -# next step, but no template and no code path auto-runs "rig bootstrap -# workload" — the one absence that keeps box creds-free end to end. +# staging-box's creds-holding join stays OPERATOR-run: cmd_new may print it as +# a next step, but no template and no code path auto-runs "rig bootstrap +# workload-server" — the one absence that keeps box creds-free end to end. check "new: the workload join is printed, never exec'd" 1 "" bash -c ' - grep "rig bootstrap workload" "'"$ROOT"'/bin/box" | grep -q "incus exec"' + grep "rig bootstrap workload-server" "'"$ROOT"'/bin/box" | grep -q "incus exec"' check "templates: no template names a creds-holding role" 1 "" bash -c ' grep -h "^BOX_BOOTSTRAP_ROLE=" "'"$ROOT"'"/templates/*/box.env | grep -qE "workload|host|custom"'