From 3a7a9f6c6ba81f052df6c1251c2705b64b7ac9b6 Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sat, 18 Jul 2026 20:03:55 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20the=20thin-template=20story=20=E2=80=94?= =?UTF-8?q?=20box=20mints,=20rig=20converges=20(#81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README: the templates section tells the split — thin seeds (user, tmux, rig), the creds-free tenant role auto-run at mint, the operator-run workload join, and the RIG_REPO/RIG_REF pin point with the honest unpinned note (both directions of the rig<->box edge track main until rig#32/#83). The #80 guard note is cross-referenced as living once, in rig's roles. box-design.md: a layering section (why rig roles and not cloud-init: convergent, re-runnable, effective-state-asserted vs a first-boot one-shot), and the announce section now says who renders the context file. CHANGELOG: staging template, BOX_BOOTSTRAP_ROLE + auto-run, the pin point under Added; the tenant-content move under Changed. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 41 +++++++++++++++++++++++++ README.md | 74 ++++++++++++++++++++++++++++++++++++---------- docs/box-design.md | 26 ++++++++++++++-- 3 files changed, 124 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03fdaa0..05e2744 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,30 @@ which records not just what changed but what each drill run proved. The agent-context guard for the templates (suggested fix 4) lands in heavy-duty/rig#31's bootstrap roles per the thin-templates split (#81). +- **The `staging` template** (#81, the re-cut of #69's layering) — a + server-class, creds-free seed: Debian 13, user `ops`, tmux, rig, + `BOX_REQUIRE_VM=1` (the VM is its trust boundary), `BOX_AUTOSTART=1` (a + server returns from a host reboot without an operator), and + `BOX_BOOTSTRAP_ROLE="staging"` — the server posture (docker, sshd + hardening) converges via `rig bootstrap staging` after mint. The tailnet + workload join holds a pre-auth key and therefore **stays operator-run** + (`box shell` → `sudo rig bootstrap workload`), printed as a next step — + box never sees the key. +- **`BOX_BOOTSTRAP_ROLE` template key + mint-time auto-run** (#81) — a + template names the **creds-free** rig tenant role box runs inside the guest + after cloud-init settles (`incus exec … rig bootstrap `); 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 — the roles + are convergent by contract (rig#31). `blank` names no role and auto-runs + nothing. +- **The rig pin point: `RIG_REPO` / `RIG_REF`** (#81) — the tenant seeds + preinstall rig, inverting the rig→box install edge (rig#28), and the new + edge gets the same honest treatment rig#29 gave box's unpinned install: + `@RIG_REPO@`/`@RIG_REF@` tokens in the seed resolve at mint from the + environment (default `heavy-duty/rig` @ `main` — unpinned, tracking main, + until a release flow exists, rig#32/#83). The pin covers both the installer + fetched and the tree it installs, so a rig branch under review is testable + end to end; values are allowlist-validated before touching the YAML. - **Server-posture template keys** (#81, carved from #69) — two optional `box.env` allowlist keys. `BOX_REQUIRE_VM=1` refuses both the silent container fallback (no `/dev/kvm`, exit 1) and an explicit `--container` @@ -122,6 +146,23 @@ which records not just what changed but what each drill run proved. single-version and zero-residue uninstalls, and the `INCOMPLETE` scream into *driven* tests instead of greps (154 checks). +### Changed + +- **Thin templates — box mints, rig converges** (#81, companion rig#31) — + the tenant content that lived in `claude`/`codex`/`grok`'s cloud-init (the + agent CLI installs, docker, node, the per-template agent-context heredocs) + **moves to rig's bootstrap roles**, where it is convergent, idempotent and + testable end to end instead of parse-only YAML. What remains per template + is a thin, creds-free seed: the tenant user, tmux (#65), and rig + preinstalled — nothing that joins a tailnet or admits credentials. The #80 + agent-context guard ("never run `box setup-host` or the drill inside a + box") now lives once, in rig's roles, not copy-pasted per template. The + template test sweep grew the contract's teeth: per-template seed asserts + (user matches, rig pinned via both tokens) and fail-closed **absence + greps** over effective cloud-init lines — no agent CLI, no docker, no + tailscale/authkey/ssh, no `write_files` heredocs — so tenant content + cannot quietly grow back. + ### Fixed - **`revoke --purge` re-checks the incus-user state** — the purge removed diff --git a/README.md b/README.md index 71e6f2e..8ef93b6 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,12 @@ **Headless, trust-less, throwaway dev VMs.** One command mints a fresh, network-isolated Incus box from a **template**; the coding-agent templates -ship a CLI agent on Debian 13 — `claude` (Claude Code), `codex` (OpenAI -Codex), `grok` (xAI Grok). The box is the product — you log in and work; -destroying it loses nothing you didn't push. +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) +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 +— you log in and work; destroying it loses nothing you didn't push. **Strictly creds-free.** A box ships with everything installed and **no** credentials — no agent token, no git PAT, nothing. You authenticate @@ -215,23 +218,64 @@ 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's in it | -| -------- | --------------------------------------------------------- | -| `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 | +| 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 | -A template is a directory under `templates/`: a `box.env` (image, user, -resources — parsed against a strict allowlist, never sourced) and a -`user-data.yaml` (cloud-init, passed to Incus verbatim). The coding-CLI -templates are all the same shape — install the CLI, put it on PATH, drop an -agent-context file; none of them carry credentials. +**Templates are thin seeds; rig does the becoming** +([#81](https://github.com/heavy-duty/box/issues/81)). A template is a +directory under `templates/`: a `box.env` (image, user, resources, boot +demands, tenant role — parsed against a strict allowlist, never sourced) and +a `user-data.yaml` (cloud-init, passed to Incus verbatim except the two rig +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, +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 ` → +`sudo rig bootstrap `). The agent-context file carries the +[#80](https://github.com/heavy-duty/box/issues/80) guard — never run +`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 +never sees the key ([#69](https://github.com/heavy-duty/box/issues/69)'s +split, kept). + +**The rig pin point** (`RIG_REPO` / `RIG_REF`). The seeds preinstall rig, +which inverts the rig→box install edge +([rig#28](https://github.com/heavy-duty/rig/issues/28): rig installs box on +host-class machines; box guests now install rig). The seed's install line +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 +RIG_REPO=you/rig RIG_REF=my-branch \ + box new --name trial --template claude # a rig branch under review +``` + +Both directions of that edge track `main` unpinned today — said honestly, +the same way rig documents box's unpinned install +([rig#29](https://github.com/heavy-duty/rig/issues/29)) — until the release +flow lands ([rig#32](https://github.com/heavy-duty/rig/issues/32), +[#83](https://github.com/heavy-duty/box/issues/83)). The pin covers both the +installer fetched and the tree it installs, and the values are +allowlist-validated on the host before they touch the YAML. ```sh box templates # list what this install can mint box new --name scratch # the DEFAULT template is blank: bare Debian, - # same isolation, nobody home + # same isolation, nobody home — no rig, no role ``` A template **cannot** name a network, a profile, or a `security.*` flag — diff --git a/docs/box-design.md b/docs/box-design.md index 44b0f9e..92a0bca 100644 --- a/docs/box-design.md +++ b/docs/box-design.md @@ -55,13 +55,35 @@ rides along deliberately — and because scrubbing a disk image is a promise tarball surgery cannot keep, export shouts that the file is a credential instead of pretending to sanitize it. +## Thin templates: box mints, rig converges (#81) + +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 — +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 +`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 +template, ever again. + ## The box announces itself to the agent -cloud-init installs a global agent-context file in every coding-agent box +Every coding-agent box gets a global agent-context file (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, `~/.grok/AGENTS.md`) telling the agent it is running in a box (trust-less, ephemeral, creds-free) and to treat a repo's `.box/` folder as its bootstrap runbook. No "tell it" step, no host -execution. +execution. The file is rendered by rig's tenant roles from one shared +template (#81) — including the #80 guard: never run `box setup-host`, +`box teardown-host` or the drill inside a box; a nested box stack claims the +guest's own uplink subnet and silently breaks its networking. ## `.box/` is optional, agent-facing documentation