Measured live on the first e2e mint: cloud-init runs runcmd as root but
with NO $HOME in the environment, and rig's installer (set -u) reads
$HOME for its DEST — the mint died with 'HOME: unbound variable'. The
seed line now pins HOME=/root explicitly, with the scar documented next
to it in every tenant template.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tenant content that lived in claude/codex/grok's cloud-init — agent CLI
installs, docker, node, the per-template agent-context heredocs — moves to
rig's bootstrap roles (rig#31), where it is convergent, idempotent and
testable end to end. What remains per template is a thin seed: the tenant
user, tmux (#65), and rig preinstalled — nothing that joins a tailnet or
admits credentials.
- BOX_BOOTSTRAP_ROLE: a template names the creds-free rig role cmd_new
auto-runs inside the guest after cloud-init settles. The value is a role
NAME by allowlist — anything shell-shaped dies at parse time, on the
host. A failed role leaves the box up and names the re-run.
- render_userdata: the seed's ONE substitution. @RIG_REPO@/@RIG_REF@
resolve from the mint environment (default heavy-duty/rig @ main —
unpinned, the honest rig#29 treatment, until rig#32's releases); values
are whole-string-validated before touching the YAML, because they land
inside a runcmd shell line.
- templates/staging: the re-cut of #69's layering — user ops,
BOX_REQUIRE_VM=1, BOX_AUTOSTART=1, role staging. The tailnet workload
join holds a key and stays operator-run; cmd_new prints it as the next
step and box never sees the key.
- blank stays a box with nobody home: no rig, no role, nothing auto-runs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two things:
1. The host scripts are first-class verbs now — 'box setup-host',
'box teardown-host [--purge-incus]', 'box migrate-host --box <n>'.
Nobody should have to run ~/.local/share/box/host/<script>.sh; that
read like an external script and exposed an install path. Each verb
execs the installed script with its flags passed through (same
pattern as 'box doctor'). README, doctor hints, and the uninstall
section point at the verbs now.
2. The repo-runbook convention is '.box/', not '.claudebox/'. Renamed
across docs and the templates' agent briefing; the briefing tells
the agent to read either, and box-recipe.md notes the rename, so
repos still shipping '.claudebox/' keep working through the
transition. (Consuming repos rename their own folder — tracked
separately.)
Also widened the help command column for the longer verb names, and
fixed one sed-casualty where a broad '.claudebox'→'.box' pass had
turned the README's legacy user.claudebox tag into user.box.
The 0.4.0 rename left surface leftovers the user hit through the
CLAUDEBOX_* env vars. Sweep them, drawing a clean line:
box = everything the user touches — env vars (BOX_REPO/REF/HOME/
BIN), installer messages (box-install:), the install tree
(~/.local/share/box, with the installer sweeping the old
~/.local/share/claudebox on upgrade), tool prose, and the
docs (docs/box-{design,recipe}.md).
claudebox = the GitHub repo name (URLs, the claudebox-<ref> tarball
dir, issue refs), the legacy user.claudebox=1 tag, the
old-stack cleanup code (claudenet/claude-dev/claude-isolate/
claudebox-firewall), and the .claudebox/ runbook convention
— a deliberate v1 hold, since renaming it breaks consuming
repos.
Renamed the two doc files and their links; updated drill.sh/doctor.sh
paths and BOX_REPO/BOX_REF; fixed the claude template's in-box briefing
to say 'box'. RUNS.md left as-is (append-only history). No behavior
change beyond the install-dir move, which the installer migrates.
The tool underneath was already generic: a thin, honest wrapper over
Incus. What was Claude-specific was welded on — one image, one profile,
one cloud-init file, one hardcoded 'sudo -u claude'. The weld is now a
template.
The mechanic: 'box new' stamps the template's identity onto the
instance (user.box=1, user.box.template, user.box.user); shell/exec/
tmux read the user back off the instance, and 'incus copy' carries
user.* keys (audit B2), so a clone knows what it is without consulting
the template. Templates are box.env (parsed against a strict allowlist,
never sourced — no key for a network exists, on purpose) plus a
verbatim cloud-init. Every template launches with the shared box-net
profile: the isolated NIC and root disk, nothing template-controlled —
resources land per-instance from box.env, overridable via BOX_CPU/
BOX_MEMORY/BOX_DISK (which is also how the drill shrinks boxes on a
small host now that profile edits can't).
The three open calls, taken as recommended: clean cut at 0.4.0 (no
claudebox shim; the installer retires the old symlink); default
template = claude (muscle memory survives); repo stays heavy-duty/
claudebox, binary is box.
Compat is the tag, not the name: resolve_box and list honor the legacy
user.claudebox=1 forever, and the legacy tag maps to the claude user —
a pre-rename box lists, shells, clones, unchanged.
Deliberate divergence from #17's table: the host-stack resource names
(claudenet, claude-isolate, nft tables, claudebox-firewall.*) are NOT
renamed — they are host-internal, invisible to users, and renaming
them breaks every provisioned host for zero user-visible gain.
claude-dev is no longer created; setup-host creates box-net, teardown
removes both.
Closes#17