release: box 0.5.0 — codex+grok templates, migrate-host, box expose #56
8 changed files with 90 additions and 36 deletions
19
README.md
19
README.md
|
|
@ -18,7 +18,7 @@ live in a shared profile no template can touch, so `blank` is a box with
|
|||
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 [`.claudebox/`](docs/box-recipe.md)
|
||||
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
|
||||
design rationale.
|
||||
|
|
@ -44,7 +44,7 @@ retires the old `claudebox` symlink. (No `git clone` needed.)
|
|||
## One-time host setup (Ubuntu 24.04 / Debian 13)
|
||||
|
||||
```sh
|
||||
~/.local/share/box/host/setup-host.sh # run twice if it adds you to incus-admin (re-login between)
|
||||
box setup-host # run twice if it adds you to incus-admin (re-login between)
|
||||
```
|
||||
|
||||
Idempotent. Installs Incus and creates the isolation stack: the `boxnet` NAT
|
||||
|
|
@ -70,7 +70,7 @@ claude # then run /login — copy the URL (press c), o
|
|||
# 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 .claudebox/, Claude reads it and sets up
|
||||
claude # if the repo has .box/, Claude reads it and sets up
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
|
@ -158,7 +158,8 @@ dev/test only.
|
|||
## Boxes are just Incus instances
|
||||
|
||||
A box is an ordinary Incus instance tagged `user.box=1` (pre-0.4.0 boxes
|
||||
carry `user.claudebox=1`, honored forever). box wraps the box lifecycle and the isolation model — not all of Incus. It owns a command
|
||||
carry `user.claudebox=1`, honored forever). box wraps the box lifecycle and
|
||||
the isolation model — not all of Incus. It owns a command
|
||||
when it must enforce something Incus can't see: that tag (it will not stop,
|
||||
rename or delete an instance it didn't mint), the isolation stack, or the
|
||||
creds-free snapshot workflow. For everything else, there's the door:
|
||||
|
|
@ -227,9 +228,9 @@ diagnoses the host faults that have actually happened: a wedged Incus daemon,
|
|||
a dnsmasq that silently isn't serving, a VPN resolver that boxes would
|
||||
inherit.
|
||||
|
||||
## Recipes: the `.claudebox/` convention
|
||||
## Recipes: the `.box/` convention
|
||||
|
||||
A repo that wants to be easy to stand up in a box ships an optional `.claudebox/`
|
||||
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).
|
||||
|
|
@ -237,9 +238,9 @@ host-executed script. See [docs/box-recipe.md](docs/box-recipe.md).
|
|||
## Uninstall
|
||||
|
||||
```sh
|
||||
~/.local/share/box/host/teardown-host.sh # boxes, network, ACL, profile, firewall
|
||||
~/.local/share/box/host/teardown-host.sh --purge-incus # ...and Incus itself
|
||||
rm -rf ~/.local/share/box ~/.local/bin/box # the CLI
|
||||
box teardown-host # boxes, network, ACL, profile, firewall
|
||||
box teardown-host --purge-incus # ...and Incus itself
|
||||
rm -rf ~/.local/share/box ~/.local/bin/box # the CLI itself
|
||||
```
|
||||
|
||||
## Non-goals
|
||||
|
|
|
|||
51
bin/box
51
bin/box
|
|
@ -53,6 +53,9 @@ CMDS=(
|
|||
"expose^<box> <port> [<host-port>] | --list | --remove <port>^box^Forward a box port to the host's loopback — see a dev server^fn:cmd_expose^"
|
||||
"incus^<box> -- <args...>^box^Escape hatch: run any incus command against a box^fn:cmd_incus^"
|
||||
"doctor^[--fix | --pin-dns]^^Is this host fit to mint boxes? Diagnose the daemon, network, DNS, isolation^fn:cmd_doctor^"
|
||||
"setup-host^^^One-time host setup: Incus, the boxnet stack, the profile, the firewall^fn:cmd_setup_host^"
|
||||
"teardown-host^[--purge-incus]^^Remove the box host stack (both name generations)^fn:cmd_teardown_host^"
|
||||
"migrate-host^--box <n> | --all-boxes | --retire-legacy^^Move a host from the pre-0.4.0 stack onto box^fn:cmd_migrate_host^"
|
||||
"status^^^Deprecated alias for 'list'^fn:cmd_status^"
|
||||
"help^[<command>]^^This help, or 'box help <command>' for one command^fn:cmd_help^"
|
||||
)
|
||||
|
|
@ -119,7 +122,7 @@ EOF
|
|||
local r v sum
|
||||
for r in "${CMDS[@]}"; do
|
||||
IFS='^' read -r v _ _ sum _ _ <<<"$r"
|
||||
printf ' %-9s %s\n' "$v" "$sum"
|
||||
printf ' %-13s %s\n' "$v" "$sum"
|
||||
done
|
||||
cat <<'EOF'
|
||||
|
||||
|
|
@ -371,6 +374,37 @@ cloud-init error that names none of them.
|
|||
Exit 0 = clean; 1 = problems found (each printed with its fix). Read-only
|
||||
unless --fix or --pin-dns is given.
|
||||
EOF
|
||||
;;
|
||||
setup-host) cat <<'EOF'
|
||||
Prepare this host to mint boxes — one time. Installs Incus and builds the
|
||||
isolation stack: the boxnet NAT bridge (resolver pinned), the box-isolate
|
||||
ACL, the box-net profile, and the firewall rules, all re-applied at boot.
|
||||
Idempotent — safe to re-run after a box upgrade to pick up stack changes.
|
||||
|
||||
If it has to add you to the incus-admin group it will say so and exit; log
|
||||
back in (or 'sg incus-admin') and run it again.
|
||||
|
||||
box setup-host
|
||||
EOF
|
||||
;;
|
||||
teardown-host) cat <<'EOF'
|
||||
Remove the box host stack — all boxes, the boxnet/claudenet networks, the
|
||||
ACLs, the profiles, and the firewall rules of BOTH name generations (current
|
||||
and pre-0.4.0). Asks first.
|
||||
|
||||
box teardown-host # the stack; leaves Incus installed
|
||||
box teardown-host --purge-incus # ...and uninstall Incus too
|
||||
EOF
|
||||
;;
|
||||
migrate-host) cat <<'EOF'
|
||||
Move a host from the pre-0.4.0 'claudebox' stack onto 'box'. Re-homing
|
||||
preserves a box's authed state (no re-login) — it only re-tags and reassigns
|
||||
the profile, then verifies the box works on its new network leg.
|
||||
|
||||
box migrate-host --box <name> # re-home one legacy box
|
||||
box migrate-host --all-boxes # re-home every legacy box
|
||||
box migrate-host --retire-legacy # remove the old stack (once no legacy box remains)
|
||||
EOF
|
||||
;;
|
||||
status) cat <<'EOF'
|
||||
Deprecated alias for 'box list'. It ignored the <box> argument it
|
||||
|
|
@ -424,6 +458,8 @@ while [ $# -gt 0 ]; do
|
|||
if [ "$cmd" = doctor ]; then args+=("$1"); shift; continue; fi
|
||||
# expose's own flags (--list, --remove) are positional to it, not box's
|
||||
if [ "$cmd" = expose ]; then args+=("$1"); shift; continue; fi
|
||||
# the host verbs delegate their flags to the scripts they wrap
|
||||
case "$cmd" in setup-host|teardown-host|migrate-host) args+=("$1"); shift; continue ;; esac
|
||||
if [ "$cmd" = exec ] || [ "$cmd" = incus ]; then
|
||||
usage_error "unknown option: $1 — a command's own flags go after --, as in '$(synopsis_of "$cmd")'"
|
||||
fi
|
||||
|
|
@ -843,6 +879,19 @@ cmd_doctor() {
|
|||
exec bash "$script" "${args[@]}"
|
||||
}
|
||||
|
||||
# The host lifecycle scripts, as first-class verbs — nobody should have to know
|
||||
# where the install tree keeps its scripts. Each execs the installed script
|
||||
# with its flags passed through; the script owns its own behavior (setup-host's
|
||||
# incus-admin re-login dance, teardown's confirmation, migrate's per-box work).
|
||||
host_script() { # $1 = script basename under host/
|
||||
local script="$root/host/$1"
|
||||
[ -f "$script" ] || die "$1 not found at $script — re-run install.sh"
|
||||
exec bash "$script" "${args[@]}"
|
||||
}
|
||||
cmd_setup_host() { host_script setup-host.sh; }
|
||||
cmd_teardown_host() { host_script teardown-host.sh; }
|
||||
cmd_migrate_host() { host_script migrate-host.sh; }
|
||||
|
||||
cmd_help() { show_help "${args[0]:-}"; }
|
||||
|
||||
# The escape hatch. The box is resolved and tag-checked; everything else is
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ design decisions.
|
|||
|
||||
- **The tool** mints isolated boxes with Claude installed but **unauthenticated**.
|
||||
It knows nothing about projects, secrets, recipes, or memory.
|
||||
- **The agent** (Claude Code, inside the box) reads an optional `.claudebox/`
|
||||
- **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.
|
||||
|
||||
|
|
@ -45,10 +45,10 @@ Log in once → snapshot → spin up authed boxes from it.
|
|||
|
||||
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
|
||||
repo's `.claudebox/` folder as its bootstrap runbook. No "tell it" step, no host
|
||||
repo's `.box/` folder as its bootstrap runbook. No "tell it" step, no host
|
||||
execution.
|
||||
|
||||
## `.claudebox/` is optional, agent-facing documentation
|
||||
## `.box/` is optional, agent-facing documentation
|
||||
|
||||
Not host-executed shell. A repo that wants to be easy to stand up in a sandbox
|
||||
ships a runbook (prose + optional scripts the agent may run). A repo that does
|
||||
|
|
@ -56,12 +56,12 @@ not, you set up by hand. The tool enforces no contract; there is no `install`.
|
|||
|
||||
## What box owns, and what it doesn't
|
||||
|
||||
Boxes are ordinary Incus instances, tagged `user.claudebox=1`. That makes every
|
||||
Boxes are ordinary Incus instances, tagged `user.box=1`. That makes every
|
||||
Incus verb a candidate feature request — `rename`, `info`, `file push`, on
|
||||
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.claudebox=1` boundary (never touch an instance we didn't mint), the
|
||||
> 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
|
||||
> snapshot→clone workflow. Everything else is Incus's job.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# The `.claudebox/` convention
|
||||
# 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
|
||||
|
|
@ -6,13 +6,17 @@ 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
|
||||
`.claudebox/` folder. This folder is **agent-facing documentation** — read and
|
||||
`.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.
|
||||
|
||||
> 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
|
||||
> projects should use `.box/`.
|
||||
|
||||
## What it is / what it is not
|
||||
|
||||
- **Optional.** No `.claudebox/` is a perfectly valid state.
|
||||
- **Optional.** No `.box/` is a perfectly valid state.
|
||||
- **Agent-facing.** You are writing instructions to a reasoning agent, not a
|
||||
machine. Prose is fine; the agent adapts.
|
||||
- **Not a host contract.** The host never parses, sources, or runs anything in
|
||||
|
|
@ -23,34 +27,34 @@ 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 `.claudebox/` folder as its bootstrap runbook.
|
||||
box and to treat a repo's `.box/` folder as its bootstrap runbook.
|
||||
So the whole flow is:
|
||||
|
||||
```
|
||||
box new # get a box
|
||||
box shell # get in
|
||||
git clone <repo> && cd <repo>
|
||||
claude # Claude reads .claudebox/ and brings the project up
|
||||
claude # Claude reads .box/ and brings the project up
|
||||
```
|
||||
|
||||
The operator can also just say: *"set this project up per .claudebox"*.
|
||||
The operator can also just say: *"set this project up per .box"*.
|
||||
|
||||
## Suggested contents (all optional)
|
||||
|
||||
Author everything here for a reasoning agent.
|
||||
|
||||
- **`.claudebox/SETUP.md`** — the prose runbook. Prerequisites, how to install
|
||||
- **`.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. `.claudebox/dev-up.sh`) that the runbook tells 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.
|
||||
- **`.claudebox/env.template`** — example env the runbook explains how to fill.
|
||||
- **`.box/env.template`** — example env the runbook explains how to fill.
|
||||
Staging values the operator pastes in. **Never commit real secrets.**
|
||||
- **`.claudebox/compose.yml`** — optional services the runbook starts.
|
||||
- **`.box/compose.yml`** — optional services the runbook starts.
|
||||
|
||||
## Worked example
|
||||
|
||||
A minimal `.claudebox/SETUP.md` for a Node + Postgres app:
|
||||
A minimal `.box/SETUP.md` for a Node + Postgres app:
|
||||
|
||||
```markdown
|
||||
# Setup
|
||||
|
|
@ -58,8 +62,8 @@ A minimal `.claudebox/SETUP.md` for a Node + Postgres app:
|
|||
This is a Node service backed by Postgres.
|
||||
|
||||
1. Install deps: `npm ci`
|
||||
2. Start Postgres: `docker compose -f .claudebox/compose.yml up -d`
|
||||
3. Create the env file: copy `.claudebox/env.template` to `.env` and ask the
|
||||
2. Start Postgres: `docker compose -f .box/compose.yml up -d`
|
||||
3. Create the env file: copy `.box/env.template` to `.env` and ask the
|
||||
operator to fill in `DATABASE_URL` and `API_KEY` (staging values).
|
||||
4. Run migrations: `npm run migrate`
|
||||
5. Start the app: `npm run dev`
|
||||
|
|
@ -72,7 +76,7 @@ That's it — Claude reads it top to bottom and adapts if reality differs.
|
|||
|
||||
- **Keep it declarative and resilient.** State intent and steps; let the agent
|
||||
adapt when the repo has drifted. Don't hard-code brittle assumptions.
|
||||
- **Never put real credentials in `.claudebox/`.** Templates and staging
|
||||
- **Never put real credentials in `.box/`.** Templates and staging
|
||||
placeholders only. The operator pastes real values at runtime.
|
||||
- **No `.claudebox/` is fine.** The operator can stand the project up by hand,
|
||||
- **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`.
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ if incus network show boxnet >/dev/null 2>&1; then
|
|||
ok "dns.mode = none — a box cannot enumerate its siblings by name"
|
||||
else
|
||||
no "dns.mode = ${dns:-<unset>} — a box can RESOLVE its siblings' names and addresses"
|
||||
inf "fix: re-run ~/.local/share/box/host/setup-host.sh"
|
||||
inf "fix: re-run: box setup-host"
|
||||
[ "$FIX" = 1 ] && { incus network set boxnet dns.mode=none && inf "set: dns.mode=none"; }
|
||||
fi
|
||||
inf "ipv4.address = $(incus network get boxnet ipv4.address 2>/dev/null)"
|
||||
|
|
@ -110,7 +110,7 @@ if [ -n "$PROFILES" ]; then
|
|||
no "$p: security.port_isolation is NOT set — BOXES CAN REACH EACH OTHER"
|
||||
inf "an L3 ACL cannot do this: two boxes on one bridge are on the same L2"
|
||||
inf "segment, so their frames are switched, never routed past the ACL."
|
||||
inf "fix: re-run ~/.local/share/box/host/setup-host.sh"
|
||||
inf "fix: re-run: box setup-host"
|
||||
fi
|
||||
for k in security.mac_filtering security.ipv4_filtering; do
|
||||
v="$(incus profile device get "$p" eth0 "$k" 2>/dev/null)"
|
||||
|
|
@ -192,7 +192,7 @@ else
|
|||
no "the host resolves via a CGNAT/Tailscale resolver ($hostns), and boxes INHERIT it — see issue #33"
|
||||
inf "· box DNS breaks whenever the tailnet's resolver does (this is what kills cold mints)"
|
||||
inf "· and tailnet names RESOLVE from inside a box, though its ACL blocks connecting to them"
|
||||
inf "fix: re-run ~/.local/share/box/host/setup-host.sh (it pins the resolver)"
|
||||
inf "fix: re-run: box setup-host (it pins the resolver)"
|
||||
inf " or quick-test the pin alone: bash drill/doctor.sh --pin-dns"
|
||||
else
|
||||
inf "boxes inherit the host's resolver (unpinned — setup-host.sh pins this now; re-run it)"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ write_files:
|
|||
- **Disposable.** Nothing here is backed up. State is discarded when the box is
|
||||
removed; the operator persists work via git push and via `box snapshot`.
|
||||
- **Bootstrap runbook.** If the repository you are working in contains a
|
||||
`.claudebox/` folder, read it as your setup runbook — how to install
|
||||
`.box/` folder (older repos may use `.claudebox/`), read it as your setup runbook — how to install
|
||||
dependencies, start services, template environment files, seed data, and
|
||||
smoke-test — and follow it. It is documentation for you, not a script the
|
||||
host runs.
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ write_files:
|
|||
- **Disposable.** Nothing here is backed up. State is discarded when the box
|
||||
is removed; the operator persists work via git push and via `box snapshot`.
|
||||
- **Bootstrap runbook.** If the repository you are working in contains a
|
||||
`.claudebox/` folder, read it as your setup runbook — how to install
|
||||
`.box/` folder (older repos may use `.claudebox/`), read it as your setup runbook — how to install
|
||||
dependencies, start services, template environment files, seed data, and
|
||||
smoke-test — and follow it. It is documentation for you, not a script the
|
||||
host runs.
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ write_files:
|
|||
- **Disposable.** Nothing here is backed up. State is discarded when the box
|
||||
is removed; the operator persists work via git push and via `box snapshot`.
|
||||
- **Bootstrap runbook.** If the repository you are working in contains a
|
||||
`.claudebox/` folder, read it as your setup runbook — how to install
|
||||
`.box/` folder (older repos may use `.claudebox/`), read it as your setup runbook — how to install
|
||||
dependencies, start services, template environment files, seed data, and
|
||||
smoke-test — and follow it. It is documentation for you, not a script the
|
||||
host runs.
|
||||
|
|
|
|||
Loading…
Reference in a new issue