From 244a8f69a284b22b6edab2fb00e8e2113ff8ea39 Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Wed, 15 Jul 2026 00:15:45 +0000 Subject: [PATCH 1/7] docs(readme): make the narrative agent-agnostic, not Claude-specific MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README told the story around Claude Code even though the tool ships `codex` and `grok` templates and treats every coding agent the same. Reframe the generic prose — the intro, creds-free line, `.box/` runbook, quick start, snapshot copy, isolation boundary, recipes, and non-goals — to speak of "the coding agent" while keeping `claude` as a named, concrete example (it's still where the project started). No behavior or command changes. Co-Authored-By: Claude Opus 4.8 --- README.md | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 4909cc7..8322b43 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # box **Headless, trust-less, throwaway dev VMs.** One command mints a fresh, -network-isolated Incus box from a **template**; the flagship template is -`claude` — Debian 13 with Claude Code installed. The box is the product — -you log in and work; destroying it loses nothing you didn't push. +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. **Strictly creds-free.** A box ships with everything installed and **no** -credentials — no Claude token, no git PAT, nothing. You authenticate +credentials — no agent token, no git PAT, nothing. You authenticate interactively *inside* the box. The tool never stores or injects a secret. That means there's nothing shared or committed, so it's safe for multiple operators out of the box. @@ -18,8 +19,8 @@ nobody home — not a box with the safety off. **The tool knows nothing about your projects.** You just `git clone` inside a box. A repo can ship an optional [`.box/`](docs/box-recipe.md) -runbook that Claude Code reads and acts on — there is no `install` step and no -host-run setup. See [docs/box-design.md](docs/box-design.md) for the +runbook that the box's coding agent reads and acts on — there is no `install` +step and no host-run setup. See [docs/box-design.md](docs/box-design.md) for the design rationale. > **0.5.0**: two new templates (`codex`, `grok`), `box expose` — a @@ -66,23 +67,26 @@ legacy box remains. ## Quick start ```sh -box new --name work --template claude # a creds-free Claude box (~10 min cold) +box new --name work --template claude # a creds-free coding-agent box (~10 min cold) box shell work # enter as the template's user ``` -Inside the box, authenticate as needed: +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: ```sh claude # then run /login — copy the URL (press c), open it # in YOUR browser, paste the code back. No host CLI needed. gh auth login # or drop a PAT in — your git credentials, your call git clone https://github.com/you/project && cd project -claude # if the repo has .box/, Claude reads it and sets up +claude # if the repo has .box/, the agent reads it and sets up ``` ## Templates -The claude box is one template among several. What ships today: +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 | | --- | --- | @@ -122,7 +126,7 @@ box snapshot work authed # checkpoint after you've logged in box new --name feature --from work/authed # clone the authed state into a new box ``` -`--from` copies the whole box (Claude login, git creds, clones and all) while +`--from` copies the whole box (agent login, git creds, clones and all) while preserving isolation. You can also `box new --name x --from work` to clone a box's live state, or roll a box back with `box restore work authed`. @@ -233,8 +237,9 @@ enforces it, layer by layer: that door only ever opens onto the host's own loopback (`127.0.0.1`), never the network. -The VM is the trust boundary: whatever runs inside — Claude, or anything a -template ships — can run arbitrary code and touch nothing you care about. +The VM is the trust boundary: whatever runs inside — the coding agent, or +anything a template ships — can run arbitrary code and touch nothing you care +about. ### Measured, not claimed @@ -265,9 +270,10 @@ inherit. ## Recipes: the `.box/` convention A repo that wants to be easy to stand up in a box ships an optional `.box/` -folder — a runbook Claude reads and follows (install deps, start services, -template env, seed data, smoke-test). It is agent-facing documentation, not a -host-executed script. See [docs/box-recipe.md](docs/box-recipe.md). +folder — a runbook the box's coding agent reads and follows (install deps, +start services, template env, seed data, smoke-test). It is agent-facing +documentation, not a host-executed script. See +[docs/box-recipe.md](docs/box-recipe.md). ## Uninstall @@ -280,6 +286,6 @@ rm -rf ~/.local/share/box ~/.local/bin/box # the CLI itself ## Non-goals - **No unattended/CI bring-up.** The flow is interactive (log in, clone, ask - Claude). Reproducible-by-construction provisioning is out of scope. + the agent). Reproducible-by-construction provisioning is out of scope. - **No credential storage or injection by the tool.** Boxes are creds-free; snapshots are the reuse mechanism, not a secrets store. -- 2.45.2 From b90372da625621b7270d7755b5c3bbbf509dc455 Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Wed, 15 Jul 2026 00:17:33 +0000 Subject: [PATCH 2/7] =?UTF-8?q?feat(new):=20inline=20resource=20overrides?= =?UTF-8?q?=20=E2=80=94=20--cpu,=20--memory,=20--disk=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolution is most-specific-first: flag > BOX_* env (kept — it is the scripting form and how the drill shrinks boxes on small hosts) > the template's box.env > defaults. Values pass to Incus verbatim (limits.cpu, limits.memory, root size=) — its units, its validation; box adds no parser. Resources are all a flag can touch: there is still no flag for a network or a security.* key, on purpose. Flags shape a fresh mint only — --from refuses them, a clone carries its source's resources. An explicit --disk on a container mint gets a note instead of a silent drop (a container's root rides the pool). The drill's blank mint now carries --cpu 1 --memory 1GiB and asserts the limits landed — which is also the precedence proof, since the drill exports BOX_CPU/BOX_MEMORY on small hosts — plus a negative check that --from refuses resource flags. Verified live (container mint, image cached): BOX_CPU=3 + --cpu 1 --memory 1GiB → limits.cpu=1, limits.memory=1GiB; --from + --cpu exits 2 before touching anything; container --disk prints the note. Closes #57 Co-Authored-By: Claude Fable 5 --- README.md | 9 +++++---- bin/box | 36 +++++++++++++++++++++++++++--------- drill/drill.sh | 13 ++++++++++++- 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4909cc7..e925275 100644 --- a/README.md +++ b/README.md @@ -106,9 +106,10 @@ box new --name scratch # the DEFAULT template is blank: bare Debian, A template **cannot** name a network, a profile, or a `security.*` flag — there is no key for them. Every box launches with the shared `box-net` profile (the isolated NIC + root disk), so every template gets the identical -trust boundary. Resources come from the template's `box.env`; -`BOX_CPU` / `BOX_MEMORY` / `BOX_DISK` environment variables override them at -mint time. The template's identity (name, user) is stamped onto the instance, +trust boundary. Resources come from the template's `box.env`, overridable at +mint time — inline (`--cpu 2 --memory 3GiB --disk 20GiB`) or via +`BOX_CPU` / `BOX_MEMORY` / `BOX_DISK` environment variables (the scripting +form; flags win). The template's identity (name, user) is stamped onto the instance, so `shell`, `exec` and `tmux` land in the right user — and a clone still knows, because `incus copy` carries the metadata. @@ -152,7 +153,7 @@ the door is per-port, punched and removable at runtime. ## Commands ``` -box new --name [--template ] [--from [/]] [--vm|--container] +box new --name [--template ] [--from [/]] [--cpu ] [--memory ] [--disk ] [--vm|--container] box templates # list the templates this install can mint box list # list your boxes box info # one box: state, IP, exposures, snapshot labels diff --git a/bin/box b/bin/box index b55fd68..63be4de 100755 --- a/bin/box +++ b/bin/box @@ -7,6 +7,7 @@ set -euo pipefail root="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)" remote=""; mode="auto"; name=""; from=""; template=""; force=0; json=0; want_help=0 +cpu=""; memory=""; disk="" inst="" # the resolved Incus instance, set by the 'box' precondition die() { echo "box: $*" >&2; exit 1; } # 1 = it went wrong @@ -37,7 +38,7 @@ version() { echo "box $(cat "$root/VERSION" 2>/dev/null || echo unknown) ($root) # doesn't enforce a box invariant, it is incus's job, not ours — that is # what `box incus` is for. CMDS=( - "new^--name [--template ] [--from [/]] [--vm|--container]^^Mint a box from a template (default: blank), or --from an existing box/snapshot^fn:cmd_new^" + "new^--name [--template ] [--from [/]] [--cpu ] [--memory ] [--disk ] [--vm|--container]^^Mint a box from a template (default: blank), or --from an existing box/snapshot^fn:cmd_new^" "templates^^^List the templates this install can mint^fn:cmd_templates^" "list^[--json]^^List your boxes^fn:cmd_list^" "info^ [--json]^box^One box: state, type, IP, and its snapshot labels^fn:cmd_info^" @@ -202,17 +203,24 @@ being told. A template sets image, user and resources — never the network: every template gets the same isolation. --from [/] Clone src's live state, or its snapshot . + --cpu CPUs for this mint (limits.cpu, verbatim to Incus). + --memory RAM for this mint, e.g. 3GiB (limits.memory). + --disk Root disk size, e.g. 20GiB. VM mode only — a + container's root rides the storage pool. --vm | --container Force the mode. VM is the trust boundary and the default wherever /dev/kvm exists; container mode (security.nesting=true) is the fallback for hosts without nested virt — weaker isolation, dev/test only. -Resources come from the template's box.env; BOX_CPU / BOX_MEMORY / BOX_DISK -environment variables override them at mint time (a small host shrinks a box -without editing a template it doesn't own). +Resources resolve most-specific-first: these flags, then BOX_CPU / +BOX_MEMORY / BOX_DISK environment variables (the scripting form), then the +template's box.env, then defaults. Flags shape a fresh mint only — a --from +clone carries its source's resources. Resources are all a flag can touch: +there is no flag for a network or a security key, on purpose. 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 feature --from work/authed EOF ;; @@ -443,6 +451,9 @@ while [ $# -gt 0 ]; do --name) [ $# -ge 2 ] || usage_error "--name needs a value"; name="$2"; shift 2 ;; --from) [ $# -ge 2 ] || usage_error "--from needs a value"; from="$2"; shift 2 ;; --template) [ $# -ge 2 ] || usage_error "--template needs a value"; template="$2"; shift 2 ;; + --cpu) [ $# -ge 2 ] || usage_error "--cpu needs a value"; cpu="$2"; shift 2 ;; + --memory) [ $# -ge 2 ] || usage_error "--memory needs a value"; memory="$2"; shift 2 ;; + --disk) [ $# -ge 2 ] || usage_error "--disk needs a value"; disk="$2"; shift 2 ;; --remote) [ $# -ge 2 ] || usage_error "--remote needs a value"; remote="$2:"; shift 2 ;; --vm) mode=vm; shift ;; --container) mode=container; shift ;; @@ -640,11 +651,14 @@ load_template() { esac done <"$dir/box.env" [ -n "$T_IMAGE" ] && [ -n "$T_USER" ] || die "template '$t': BOX_IMAGE and BOX_USER are required" - # Environment overrides beat the file — this is how a small host (or the - # drill) shrinks a box without editing a template it doesn't own. - T_CPU="${BOX_CPU:-${T_CPU:-4}}" - T_MEMORY="${BOX_MEMORY:-${T_MEMORY:-8GiB}}" - T_DISK="${BOX_DISK:-${T_DISK:-60GiB}}" + # Resolution, most specific wins: inline flag (--cpu/--memory/--disk, #57) + # > BOX_* environment (how a small host or the drill shrinks every box it + # mints) > the template's file > defaults. Values pass to Incus verbatim — + # its units, its validation; box adds no parser of its own. Resources only: + # there is still no flag for a network or a security.* key, on purpose. + T_CPU="${cpu:-${BOX_CPU:-${T_CPU:-4}}}" + T_MEMORY="${memory:-${BOX_MEMORY:-${T_MEMORY:-8GiB}}}" + T_DISK="${disk:-${BOX_DISK:-${T_DISK:-60GiB}}}" } cmd_templates() { @@ -664,6 +678,7 @@ cmd_new() { local instance; instance="$(iname_of "$name")" if [ -n "$from" ]; then [ -z "$template" ] || usage_error "--from clones an existing box; its template rides along (drop --template)" + [ -z "$cpu$memory$disk" ] || usage_error "--cpu/--memory/--disk shape a fresh mint; a clone carries its source's resources ('box incus' can change them afterwards)" local src="${from%%/*}" snap="" srcref case "$from" in */*) snap="${from#*/}" ;; esac srcref="$(iname_of "$src")"; [ -n "$snap" ] && srcref="$srcref/$snap" @@ -685,6 +700,9 @@ cmd_new() { # turning it off boots reliably across image rebuilds. Container mode has # no firmware, so it does not apply there. if [ "$m" = vm ]; then extra+=(--vm --device "root,size=$T_DISK" --config security.secureboot=false); else extra+=(--config security.nesting=true); fi + # Root size is a VM launch concern; a container's root rides the pool. Say + # so instead of silently dropping an explicit --disk. + [ "$m" = vm ] || [ -z "$disk" ] || echo "box: note — --disk applies to VM mode only; this container's root rides the pool" >&2 # The template's identity is stamped ONTO the instance: which template, # which user. 'incus copy' preserves user.* keys (audit B2), so a clone # knows what it is without ever consulting the template again. diff --git a/drill/drill.sh b/drill/drill.sh index 77c8be5..9d60bae 100755 --- a/drill/drill.sh +++ b/drill/drill.sh @@ -422,13 +422,24 @@ box new --name tpl --template cbdrill-bad 2>&1 | grep -q "unknown key 'BOX_NETWO || no "a box.env key outside the allowlist was ACCEPTED — a template could weaken isolation" rm -rf "$badt" +# Inline resource flags (#57): refused on a clone, honored on a mint. The +# mint proof rides the blank box below — and because this drill exports +# BOX_CPU/BOX_MEMORY on small hosts, it is also the precedence proof +# (flag > env > template > default). +box new --name tpl --from nowhere --cpu 2 2>&1 | grep -q 'carries its source' \ + && ok "resource flags refused on --from — a clone carries its source's resources" \ + || no "--from accepted a resource flag (should refuse: clone resources come from the source)" + printf '\n minting a blank box (the DEFAULT template — no tooling, fast)…\n' t0=$SECONDS -if mint_box /tmp/mint-tpl.log --name tpl; then +if mint_box /tmp/mint-tpl.log --name tpl --cpu 1 --memory 1GiB; then ok "box new --name tpl, no --template ($((SECONDS - t0))s)" tt="$(incus config get tpl user.box.template 2>/dev/null)" [ "$tt" = blank ] && ok "the default template is blank (user.box.template=blank)" \ || no "default template is '${tt:-}' — expected blank" + rc="$(incus config get tpl limits.cpu 2>/dev/null)/$(incus config get tpl limits.memory 2>/dev/null)" + [ "$rc" = "1/1GiB" ] && ok "inline --cpu/--memory landed (limits = $rc, beating BOX_* env)" \ + || no "inline resource flags did not land — limits are $rc, expected 1/1GiB" [ "$(incus config get tpl user.box.user 2>/dev/null)" = dev ] \ && ok "template user stamped on the instance (user.box.user=dev)" || no "user.box.user not stamped" incus config show tpl 2>/dev/null | grep -q '^- box-net' \ -- 2.45.2 From 85e70a326a3d7ec50914bb5a56843643951a5674 Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Wed, 15 Jul 2026 00:21:18 +0000 Subject: [PATCH 3/7] docs: add CHANGELOG, starting at 0.5.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One entry for now — 0.5.0 as merged plus the inline resource flags that fold into it. Pre-0.5.0 history stays in git and drill/RUNS.md, which this points at. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f9cb045 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,72 @@ +# Changelog + +History before 0.5.0 lives in git and in [drill/RUNS.md](drill/RUNS.md), +which records not just what changed but what each drill run proved. + +## 0.5.0 — 2026-07-15 + +The release the project was renamed in: the repo is `heavy-duty/box`, matching +the CLI it ships. Everything legacy-facing is honored forever — the +`user.claudebox=1` tag, the `.claudebox/` runbook folder, the old symlink the +installer retires — but nothing current carries the old name. + +### Added + +- **`codex` and `grok` templates** — OpenAI Codex CLI and xAI Grok CLI boxes, + creds-free like every template. The template mechanic (image + user + + resources, never a network or a `security.*` key) now has three tenants + beside `blank`, and the drill mints all of them cold. +- **`box expose []`** — a deliberate, loopback-only + door to a port inside a box, for seeing a dev server in your browser. The + listen side is always the host's `127.0.0.1` (no flag to widen it), the door + is per-port, `--list`/`--remove` manage it, and `box info` shows open + exposures — a box with a hole says so. +- **Inline resource overrides on `new`** — `--cpu --memory + --disk ` (#57). Resolution most-specific-first: flag > `BOX_CPU` / + `BOX_MEMORY` / `BOX_DISK` environment (the scripting form) > template + `box.env` > defaults. Values pass to Incus verbatim; resources are all a + flag can touch. `--from` refuses them — a clone carries its source's + resources. +- **Host lifecycle as verbs** — `box setup-host`, `box teardown-host`, and + `box migrate-host`, which re-homes pre-0.4.0 boxes onto the current stack + (`--box ` / `--all-boxes`, authed state preserved) and retires the legacy + bridge once empty (`--retire-legacy`). +- **The `.box/` recipe convention** — the agent-facing runbook folder a repo + can ship, renamed from `.claudebox/` (both spellings read). + +### Fixed + +- **VM mints no longer hang at GRUB** — Incus defaults VMs to Secure Boot on, + and a cloud image whose shim the host's OVMF doesn't trust dies with "bad + shim signature" forever. Boxes now launch with `security.secureboot=false`; + the VM boundary, not boot attestation, is the box threat model. +- **`box expose` actually delivers packets** — a trilogy of drill-found + absences: the NAT proxy needs the box's boxnet lease pinned as a static + `ipv4.address` (Incus resolves `connect=0.0.0.0` against device config, not + the lease); a loopback-sourced packet needs `route_localnet` plus a + masquerade on the bridge to leave the host and be answerable; and the box's + replies need a `ct state established,related` accept ahead of the host + firewall's input drop, which was eating them statelessly. Boxes still + cannot initiate toward the host — a box-originated SYN is a NEW flow. +- **Firewall rules now converge on upgrade** — `box-firewall.sh` rebuilds its + chains every run (add + flush + re-add) instead of skipping when they + exist, which had pinned every host to the rule set of the release that + first ran there. +- **Failed mints tell you why** — cloud-init failures print the box's own log + excerpts and leave the box up to inspect; a mint that never boots names the + likely cause (corrupt image, Secure Boot, GRUB hang) and ships a sanitized + console dump; the installer asserts it landed the ref it was asked for. +- **`grok` installs the binary it actually ships** — the installer was read, + not guessed at, and the CLI lands on the non-interactive PATH (same fix + class as codex). + +### Changed + +- **Debrand complete** — env vars, install dir, docs, template descriptions + and the README all say `box`; the install URL is + `heavy-duty/box` (GitHub redirects the old one, `BOX_REPO` overrides). +- **The drill grew from 47 to 83 checks** — the expose door opened, exercised + and shut (with the contract re-probed around it), every template minted + cold, a faithful pre-0.4.0 box re-homed through `migrate-host`, and the + inline resource flags asserted (including their precedence over the + environment). -- 2.45.2 From 9fb2b8147c7ab74341b1b70f738cd48d3fddc14d Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Wed, 15 Jul 2026 00:21:22 +0000 Subject: [PATCH 4/7] docs: extend the agent-agnostic reframe to design, recipe, and drill docs Same treatment as the README, applied to the prose that stood in "Claude" for "the coding agent": box-recipe.md and box-design.md now describe the `.box/` runbook and creds-free flow around whichever agent the box was minted with, and name the per-template context file (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, `~/.grok/AGENTS.md`) instead of hardcoding Claude's. drill/README.md's "does not check" note says the drill confirms each template's CLI, not just Claude Code. `claude` stays as the concrete login example throughout. Left untouched (out of scope, literal identifiers): the legacy isolation-stack names (claudenet/claude-dev/claude-isolate, user.claudebox), the claude template files themselves, and drill/RUNS.md history. Co-Authored-By: Claude Opus 4.8 --- docs/box-design.md | 25 ++++++++++++++----------- docs/box-recipe.md | 33 ++++++++++++++++++--------------- drill/README.md | 6 +++--- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/docs/box-design.md b/docs/box-design.md index efc41ad..d55e7ca 100644 --- a/docs/box-design.md +++ b/docs/box-design.md @@ -1,28 +1,30 @@ # box design `box` is a CLI that mints and manages **trust-less, network-isolated VMs -with Claude Code installed**. It is infrastructure, not a project provisioner. +with a coding agent installed** (`claude`, `codex`, `grok`, 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. ## Principle: separate the tool from the agent -- **The tool** mints isolated boxes with Claude installed but **unauthenticated**. +- **The tool** mints isolated boxes with the agent installed but **unauthenticated**. It knows nothing about projects, secrets, recipes, or memory. -- **The agent** (Claude Code, inside the box) reads an optional `.box/` - runbook in a cloned repo and acts on it. The recipe's consumer is the - reasoning agent, not host machinery. +- **The agent** (Claude Code, Codex, Grok — whichever template, inside the box) + reads an optional `.box/` runbook in a cloned repo and acts on it. The recipe's + consumer is the reasoning agent, not host machinery. ## Boxes are strictly creds-free `box new --name ` launches a blank box: everything installed, **no** -git credentials and **no** Claude credentials. The operator authenticates +git credentials and **no** agent credentials. The operator authenticates interactively *inside* the box: -- **Claude** — `claude` → `/login` (paste-a-code OAuth: copy the URL, open it in - your own browser, paste the code back). Works because the box is outbound-only; - the tool never handles a token. +- **The coding agent** — e.g. `claude` → `/login` (paste-a-code OAuth: copy the + URL, open it in your own browser, paste the code back); `codex` and `grok` + have their own login step. Works because the box is outbound-only; the tool + never handles a token. - **Git** — the operator adds their own PAT / `gh auth login` inside the box. The tool stores and injects **no** credentials, ever. This dissolves the @@ -43,8 +45,9 @@ Log in once → snapshot → spin up authed boxes from it. ## The box announces itself to the agent -cloud-init installs a global `~/.claude/CLAUDE.md` in every box telling Claude it -is running in a box (trust-less, ephemeral, creds-free) and to treat a +cloud-init installs a global agent-context file in every coding-agent box +(`~/.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. diff --git a/docs/box-recipe.md b/docs/box-recipe.md index 6d4f345..6544c93 100644 --- a/docs/box-recipe.md +++ b/docs/box-recipe.md @@ -1,14 +1,15 @@ # The `.box/` convention -`box` mints trust-less, creds-free, isolated VMs with Claude Code already -installed (`box new/shell/snapshot/restore/exec/down/start/rm/status`). The -tool knows **nothing** about your project. There is no `install` step and no -host-run setup script. +`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. 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 -acted on by Claude Code (the reasoning agent) running inside the box. It is not -shell that the host executes. +acted on by the box's coding agent (the reasoning agent) running inside the +box, whichever template you minted. It is not shell that the host executes. > The folder was named `.claudebox/` before the 0.5.0 rename. Repos that still > ship `.claudebox/` keep working — the agent is told to read either — but new @@ -26,15 +27,17 @@ shell that the host executes. ## How it's consumed -Every box ships a global `~/.claude/CLAUDE.md` telling Claude it is inside a -box and to treat a repo's `.box/` folder as its bootstrap runbook. -So the whole flow is: +Every coding-agent box ships a global agent-context file — `~/.claude/CLAUDE.md` +for `claude`, `~/.codex/AGENTS.md` for `codex`, `~/.grok/AGENTS.md` for `grok` — +telling the agent it is inside a box and to treat a repo's `.box/` folder as its +bootstrap runbook. So the whole flow is (shown with `claude`; the other agents +follow the same shape): ``` box new # get a box box shell # get in git clone && cd -claude # Claude reads .box/ and brings the project up +claude # the agent reads .box/ and brings the project up ``` The operator can also just say: *"set this project up per .box"*. @@ -45,9 +48,9 @@ Author everything here for a reasoning agent. - **`.box/SETUP.md`** — the prose runbook. Prerequisites, how to install deps, how to start services, how to template the env, how to seed data, and - how to smoke-test. Written as instructions to Claude. -- **Helper scripts** (e.g. `.box/dev-up.sh`) that the runbook tells Claude - to run. Claude decides to run them; the host never does. + how to smoke-test. Written as instructions to the agent. +- **Helper scripts** (e.g. `.box/dev-up.sh`) that the runbook tells the agent + to run. The agent decides to run them; the host never does. - **`.box/env.template`** — example env the runbook explains how to fill. Staging values the operator pastes in. **Never commit real secrets.** - **`.box/compose.yml`** — optional services the runbook starts. @@ -70,7 +73,7 @@ This is a Node service backed by Postgres. 6. Smoke-test: `curl -sf localhost:3000/health` should return `{"ok":true}`. ``` -That's it — Claude reads it top to bottom and adapts if reality differs. +That's it — the agent reads it top to bottom and adapts if reality differs. ## Guidance @@ -79,4 +82,4 @@ That's it — Claude reads it top to bottom and adapts if reality differs. - **Never put real credentials in `.box/`.** Templates and staging placeholders only. The operator pastes real values at runtime. - **No `.box/` is fine.** The operator can stand the project up by hand, - or let Claude infer the steps from the repo's `README` / `CLAUDE.md`. + or let the agent infer the steps from the repo's `README` / `CLAUDE.md`. diff --git a/drill/README.md b/drill/README.md index cdb41e9..d7a06cf 100644 --- a/drill/README.md +++ b/drill/README.md @@ -84,9 +84,9 @@ a **design veto** for #16, caught before the code is written. ## What it does not check -`claude /login` — it's interactive by design, and the box is creds-free by -design. The drill confirms Claude Code is installed and runnable; authenticating -is yours. +agent login (e.g. `claude /login`) — it's interactive by design, and the box is +creds-free by design. The drill confirms each coding-agent template's CLI is +installed and runnable; authenticating is yours. If the host has no `/dev/kvm`, box falls back to container mode. The drill still runs, but it says loudly that **the VM trust boundary was not validated** -- 2.45.2 From 2c567f054924d522dde5e58ad719c1aadc1df11c Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Wed, 15 Jul 2026 00:21:44 +0000 Subject: [PATCH 5/7] docs(readme): drill count follows the two checks this branch adds 83 checks once the resource-flag assertions land; the '81 passing' claim belongs to the last green run and the next one re-earns it. Co-Authored-By: Claude Fable 5 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e925275..4191e25 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,7 @@ drill installs the whole stack, mints every template cold, snapshots and clones, probes every boundary from inside the boxes, opens and shuts the `expose` door (and checks the contract survives it), re-homes a faithful pre-0.4.0 box through `migrate-host`, and removes what it minted — -currently **81 checks, 81 passing**. [drill/RUNS.md](drill/RUNS.md) is the full +currently **83 checks**. [drill/RUNS.md](drill/RUNS.md) is the full history, including every trap that fooled a run into a wrong verdict. ```sh -- 2.45.2 From aff68f5492fd1e3e689ddd016444b3a2c12a96c6 Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Wed, 15 Jul 2026 00:23:58 +0000 Subject: [PATCH 6/7] docs(box-design): update isolation-stack names to the 0.4.0 rename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Snapshots, ownership-rule, and Isolation sections still named the pre-0.4.0 stack (claudenet/claude-dev/claude-isolate) while the rest of the repo — README, host/setup-host.sh, host/box-firewall.sh — uses boxnet/box-net/ box-isolate. Rename the doc references to match ground truth: claudenet→boxnet, claude-dev→box-net (profile), claude-isolate→box-isolate (ACL). Stale wording only; the mechanism described is unchanged. Co-Authored-By: Claude Opus 4.8 --- docs/box-design.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/box-design.md b/docs/box-design.md index d55e7ca..834165a 100644 --- a/docs/box-design.md +++ b/docs/box-design.md @@ -38,7 +38,7 @@ snapshots, not a secrets store: - `box snapshot [label]` — checkpoint after login + clone. - `box new --name --from [/]` — clone an existing box or snapshot (authed state and all). Isolation is preserved: the clone keeps - the `claude-dev` profile + `claudenet` + ACL. + the `box-net` profile + `boxnet` + ACL. - `box restore ` — roll a box back to a checkpoint. Log in once → snapshot → spin up authed boxes from it. @@ -65,7 +65,7 @@ forever — and wrapping them one at a time grows a worse `incus`. The rule: > **box owns a command when it must enforce an invariant Incus cannot see:** > the `user.box=1` boundary (never touch an instance we didn't mint), the -> isolation stack (`claude-dev` profile + `claudenet` + ACL), or the creds-free +> isolation stack (`box-net` profile + `boxnet` + ACL), or the creds-free > snapshot→clone workflow. Everything else is Incus's job. The rule cuts both ways, and that's the point: @@ -95,7 +95,7 @@ warns and proceeds — from there the trust boundary is yours to keep. ## Isolation -Dedicated NAT bridge `claudenet` + Incus `claude-isolate` ACL dropping all +Dedicated NAT bridge `boxnet` + Incus `box-isolate` ACL dropping all RFC1918/CGNAT/link-local egress, plus host-firewall rules blocking instance → host. Entry is `incus exec` over the local socket — no inbound path. The VM is the trust boundary. @@ -104,12 +104,12 @@ the trust boundary. That last clause is the one that was assumed and turned out to be false, so it is spelled out here with the mechanism, and `drill/` tests it on every run. -- **Box → host, LAN, RFC1918, CGNAT, link-local:** the `claude-isolate` ACL. +- **Box → host, LAN, RFC1918, CGNAT, link-local:** the `box-isolate` ACL. - **Box → box: an nftables *bridge-family* rule** (`host/box-firewall.sh`). It cannot be an ACL rule. Two boxes on one bridge share an L2 segment, so their frames are *switched* between bridge ports and never traverse the netfilter path an L3 ACL lives on — the ACL looked airtight (it drops - `10.0.0.0/8`, which contains `claudenet`) while box→box was in fact wide open. + `10.0.0.0/8`, which contains `boxnet`) while box→box was in fact wide open. A live probe found box A's SYN arriving at box B. The bridge family's forward hook fires exactly on port-to-port frames, which on this bridge means box→box and nothing else: gateway traffic and routed egress are delivered locally, not -- 2.45.2 From 342b533f4f9b573120c9876a63fd4e638aa3c255 Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Wed, 15 Jul 2026 00:28:15 +0000 Subject: [PATCH 7/7] docs(readme): how to run the drill against the latest version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The drill section showed the commands but not the step that actually bites: making sure the checkout you run is the code you mean to judge. Two versions are in play — the drill script itself, and the (repo, ref) the drill installs from GitHub and asserts before any verdict. Spell both out, plus --repo/--ref for drilling a release or a PR branch. Also catch drill.sh's REPO default up with the rename — it still said heavy-duty/claudebox (GitHub redirects it, but the default should name the repo that exists). Co-Authored-By: Claude Fable 5 --- README.md | 20 ++++++++++++++++++++ drill/drill.sh | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4909cc7..b0d5cd6 100644 --- a/README.md +++ b/README.md @@ -249,13 +249,33 @@ pre-0.4.0 box through `migrate-host`, and removes what it minted — currently **81 checks, 81 passing**. [drill/RUNS.md](drill/RUNS.md) is the full history, including every trap that fooled a run into a wrong verdict. +### Run the drill yourself + +The drill ships in the repo, not the installed tree — run it from a checkout. +Two versions are in play and both must be current: **the drill script you +run** (a stale checkout judges the past), and **the code under test** — the +drill does not test your working tree; it installs box from GitHub +(default: `heavy-duty/box@main`) and asserts the installed tree is exactly +the ref it asked for before issuing any verdict. + ```sh +git clone https://github.com/heavy-duty/box && cd box # or refresh an existing +git log --oneline -1 # checkout — this commit is + # the drill that will judge bash drill/doctor.sh # read-only: is this host healthy and the stack live? bash drill/drill.sh # FULL end-to-end — mutates the host; use a machine you own bash drill/wipe.sh # scorched earth: strip BOTH name generations, images and # (--purge-storage) the pool, so a run starts from bare ``` +To drill something other than latest `main` — a release ref, or a PR branch +on a fork: + +```sh +bash drill/drill.sh --ref +bash drill/drill.sh --repo / --ref # a PR under review +``` + The doctor reads ground truth, not config claims — the kernel's `isolated on` flag per bridge port, the process table, the resolver actually in use — and diagnoses the host faults that have actually happened: a wedged Incus daemon, diff --git a/drill/drill.sh b/drill/drill.sh index 77c8be5..8e56575 100755 --- a/drill/drill.sh +++ b/drill/drill.sh @@ -32,7 +32,7 @@ # false FAILs on the first live run. The pipeline verdict must be grep's alone. set -u -REPO="${BOX_REPO:-heavy-duty/claudebox}" +REPO="${BOX_REPO:-heavy-duty/box}" REF="${BOX_REF:-main}" YES=0; KEEP=0 SELF="$(readlink -f "$0")" -- 2.45.2