refactor(templates): the tenant seeds carry rig's -box family suffix #125

Merged
dan-claude-bot merged 2 commits from feat/template-family-suffix into main 2026-07-20 13:09:30 +00:00
16 changed files with 194 additions and 95 deletions

View file

@ -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

View file

@ -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 <box>` →
@ -265,9 +269,9 @@ an *existing* box to a newer spec (`box shell <box>` →
`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 <name>`, 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 <name>`, 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,

35
bin/box
View file

@ -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 <role>', 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=<seconds> (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:<port>, 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

View file

@ -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

View file

@ -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

View file

@ -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 >/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 2>/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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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 <name> # then: sudo rig bootstrap workload --hostname <name>
# box shell <name> # then: sudo rig bootstrap workload-server --hostname <name>
# 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"

View file

@ -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

View file

@ -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"'