Commit graph

17 commits

Author SHA1 Message Date
claude-hdb
455fbc656e feat: host lifecycle as verbs (setup-host/teardown-host/migrate-host) + .box/ convention
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.
2026-07-14 18:01:34 +00:00
claude-hdb
4eb6b35a7b chore: finish the debrand — env vars, install dir, docs are 'box', not 'claudebox'
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.
2026-07-14 17:44:24 +00:00
claude-hdb
449d750eb5 Merge remote-tracking branch 'fork/feat/expose' into integration/0.5.0 2026-07-14 16:12:53 +00:00
claude-hdb
de6467a728 feat: 'box expose <box> <port>' — a deliberate, loopback-only door to a dev server
The 'no inbound path' contract is one notch too absolute for the tool's
own flagship workflow: coding in a box, a dev server on :3000, and no
way to open it in your browser. expose is the deliberate un-screwing.

- Loopback only, always: the host side listens on 127.0.0.1, never
  0.0.0.0 — no other machine can reach the box; only this host gets a
  door. No flag widens it (that is the escape hatch's job).
- A verb, per-port, reversible, visible: each exposure is a named proxy
  device (expose-<port>); --list and box info show it, --remove undoes
  it. A box with a hole says so.
- Mechanism (VMs): an Incus proxy device forwards host loopback to the
  box's ip:port, plus a SCOPED ingress ACL allow (this box's ip + this
  port only) so the forkproxy's connection survives the default drop —
  the drill decides whether that allow is needed or redundant. The
  in-box server must listen on 0.0.0.0 (a VM's forwarder reaches it over
  the network); inside an isolated box that is safe.

Drill phase E: start a detached listener in a box, expose it, prove the
HOST loopback reaches it, prove a NON-exposed port is still dropped (A7
survives), prove --remove shuts the door.

Closes #55
2026-07-14 16:07:17 +00:00
claude-hdb
c6bb6cb0e0 feat: codex and grok templates — the mechanic's second and third tenants
Two coding-CLI templates mirroring claude's shape: a box.env + verbatim
cloud-init, inheriting the box-net placement contract structurally, no
new design.

- codex: OpenAI Codex CLI via 'npm i -g @openai/codex' (the SCOPED
  package; needs Node 22), symlinked onto the non-interactive exec PATH
  via 'npm prefix -g' — the same PATH fix the claude template needed.
- grok: xAI Grok Build via the official 'curl x.ai/cli/install.sh',
  run AS the grok user (the installer drops into $HOME); the binary is
  found and symlinked to /usr/local/bin.

Install commands verified upstream at implementation time, per the
issue's rule (npmjs.com/package/@openai/codex, x.ai/cli). Each gets an
AGENTS.md-style context file telling the agent it lives in a
disposable, isolated, creds-free box.

Drill: templates listing now expects four; a compact per-template smoke
(mint, '<cli> --version' via box exec, remove) validates each payload
installs and lands on the exec PATH — the generic mechanic is already
proven by blank+claude and not repeated.

Closes #54
2026-07-14 15:59:39 +00:00
claude-hdb
5defd40bca feat!: rename the host stack too, default to blank, drill the templates, add wipe
Follow-up to the rename, per operator direction — the divergence is
reversed and the cut is complete:

- Host stack: boxnet (10.88.0.0/24 — a pre-rename host may still carry
  claudenet on 10.87, two bridges must not claim one subnet),
  box-isolate, nft tables 'inet box'/'bridge box', box-firewall.{sh,
  service}. teardown-host now strips BOTH name generations, so one
  script uninstalls a host of any age.

- Default template is blank: 'box new --name x' mints bare Debian;
  the claude box is '--template claude'. The login hint follows the
  EFFECTIVE template read off the instance, so clones of claude boxes
  still get it and blank boxes are not told to run a binary they lack.

- The drill validates templates: listing, unknown-template refusal,
  the allowlist rejecting BOX_NETWORK by name, and a full blank mint —
  default resolves to blank, metadata stamped, box-net placement, exec
  lands in 'dev', no claude binary, and isolation parity (egress +
  pinned DNS) on the same contract as every template.

- drill/wipe.sh: scorched earth for drill hosts. Both tag generations,
  every drill-named instance, networks/ACLs/profiles/firewall of both
  generations, cached images, and (--purge-storage) the default pool.
  Ends by asserting the ABSENCE of every artifact rather than trusting
  the removals' exit codes.
2026-07-14 14:36:39 +00:00
claude-hdb
c11f3d7552 feat!: claudebox becomes box — the Claude box is one template among several
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
2026-07-14 14:22:50 +00:00
Daniel Marin
154f10e336
Merge pull request #50 from claude-hdb/feat/doctor-verb
feat: 'claudebox doctor' — the host-health checks as a first-class verb
2026-07-14 14:24:17 +01:00
claude-hdb
16a4c129fc feat: 'claudebox doctor' — the host-health checks as a first-class verb
Every fault the drill's doctor diagnoses is a user's fault first: a
wedged Incus daemon, a dnsmasq that silently isn't serving, a VPN
resolver boxes inherit, isolation claimed by config but off in the
kernel — each has killed a cold mint or weakened a boundary, with a
cloud-init error that names none of them. The CLI half-admitted it:
cmd_new's failure path hand-pointed at issue #33, doing one special
case of a doctor's job inline.

The verb delegates to the installed drill/doctor.sh (the tree ships
whole) — one hardened script, two audiences. Its flags pass through;
the verdict now reads 'fit to mint boxes (and to drill)'; the mint-
failure hint ends with 'claudebox doctor' instead of the hand-rolled
diagnosis.

Closes #46
2026-07-14 13:13:18 +00:00
claude-hdb
eacb1a9e28 feat: 'claudebox tmux <box> [session]' — a shell that survives you
'shell' is a child of the exec connection: drop the terminal and a long
Claude run inside it is SIGHUP'd — which has already cost one run. tmux
is installed in every box but relying on remembering to start it is how
that run was lost.

New verb, not a changed default: 'tmux new-session -A -s <session>'
attaches when the session exists and creates it when it doesn't, so
starting work and reattaching after a disconnect are the same command.
The session name (default: main) gives parallel streams in one box.
'shell' stays bare on purpose — two verbs, two contracts — so tmux's
costs (nesting, scrollback semantics, exit-vs-detach) are only paid
when asked for.

Closes #6
2026-07-14 13:10:59 +00:00
claude-hdb
15fe15b23f docs(readme): the isolation contract as it ships — and measured, not claimed
The Isolation section predated the #16 arc: it described the ACL and the
host firewall, and none of what that audit shipped — port-isolated NICs
(the actual sibling boundary), dns.mode=none, the pinned resolver. It
also never mentioned that the contract is tested at all.

State the contract layer by layer as it exists on main, and add a
'Measured, not claimed' section: the drill and doctor, what each does,
which one mutates the host, and the standing score — 47/47 from a bare
host (run 13, full teardown → rebuild → drill).
2026-07-14 12:47:09 +00:00
7d33ad8a00 feat: make the command surface a table, add rename and an escape hatch
Every incus verb is a candidate feature request, and wrapping them one at a
time grows a worse incus. This lands the rule instead: claudebox owns a command
when it must enforce an invariant incus cannot see — the user.claudebox=1
boundary, the isolation stack, or the creds-free snapshot workflow. Everything
else is incus's job, and now has a door.

- CMDS table: one row per command, carrying synopsis, preconditions, summary,
  action and success message. Dispatch AND help are rendered from it, so the
  help can no longer describe a command that doesn't exist — the drift that
  produced #8 is now impossible, not merely fixed.
- rename, via a table row: it needs the box stopped (incus won't rename a
  running instance), so it says so instead of leaking an incus error.
- `claudebox incus <box> -- <args...>`: the escape hatch. Box resolved and
  tag-checked, rest passed to incus verbatim, {} substituted, command echoed.
  Warns when it can move a box off the isolation stack.
- The boundary is now ENFORCED, not assumed: every box-taking command resolves
  through the user.claudebox=1 tag, so claudebox will not stop, rename or delete
  an instance it didn't mint.
- The rule, written into docs/claudebox-design.md.

Closes #11
2026-07-13 20:49:46 +00:00
8636d1abcb fix: standard help, honest flags, and an rm that asks first
The help was `sed -n '2,9p' "$0"` — the script scraping its own header comment
by line number — with no sections, no per-command help, no `-h`, and no
`--version`. Worse, parts of the interface it described weren't real: `--force`
was parsed and never read, so `claudebox rm work` force-deleted a running box
with no confirmation while advertising a flag that implied the bare form was
the careful one.

- usage()/help_cmd(): NAME/USAGE/COMMANDS/OPTIONS/EXAMPLES/EXIT STATUS, plus a
  page per command. `claudebox help <cmd>` and `claudebox <cmd> --help`.
- `-h`/`--help` anywhere; `--version`/`-V` (VERSION file).
- Unknown command → stderr, exit 2, and a did-you-mean by edit distance.
- Unknown options are rejected, not swallowed as positionals: `snapshot work
  --labl x` no longer names the snapshot "--labl". `--` still passes everything
  through for exec.
- `--force` is real: `rm` confirms on a TTY, refuses without one unless forced.
  BEHAVIOR CHANGE — a scripted `claudebox rm` now needs `--force`.
- Exit 2 for usage errors, 1 for runtime failures; accurate per-command usage
  strings (`exec` no longer omits `-- <cmd...>`).
- shellcheck is clean: SC2034 (dead `force`) and SC2015 both gone.

Closes #8
2026-07-13 20:41:21 +00:00
5ce2a0ae8d feat: add claudebox list and claudebox info
`status` was the only lister: misnamed, silently ignoring the `<box>` its own
help advertised, and unable to show a snapshot label — the one thing the
README's snapshot→clone flow requires you to know. Typing the obvious
`claudebox list` printed the help and exited 0.

- `list` — a table of your boxes (name, state, type, snapshot count).
- `info <box>` — detail, the snapshot labels, and the `--from` line to clone one.
- `list <box>` is a wrong guess we can answer: it points at `info`.
- `--json` passthrough; `--remote` honored; a real error on an unknown box.
- `status` stays as a deprecated alias for `list` so muscle memory keeps working.

Reads are filtered by the `user.claudebox=1` tag, so an Incus instance
claudebox didn't mint is never reported on or touched.

Closes #7
2026-07-13 20:40:17 +00:00
e175c416c3 fix: install from the public canonical heavy-duty/claudebox
The fork claude-hdb/claudebox is private, so an anonymous 'curl | bash' against
it 401s. Point the installer default and the README one-liner at the public
canonical repo so the install flow works without auth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:08:18 +00:00
0c9971ecf8 Import claudebox: creds-free, trust-less Claude Code VMs
A CLI that mints trust-less, network-isolated Incus VMs with Claude Code
installed. Boxes are strictly creds-free — the operator logs into Claude
interactively inside; authenticated state is reused via snapshots. The tool
knows nothing about projects; a repo ships an optional agent-facing .claudebox/
runbook that Claude reads.

- bin/claudebox: new/shell/exec/snapshot/restore/down/start/rm/status; creds-free
  'new' (fresh launch or clone via --from <src>[/<snap>]).
- cloud-init: global ~/.claude/CLAUDE.md self-describing the box + .claudebox/ runbook.
- install.sh: curl-pipe-bash installer.
- host/: Incus isolation stack (claudenet + claude-isolate ACL + claude-dev
  profile + firewall).
- docs/: design + .claudebox/ convention.

Initial canonical import (prototyped separately; re-homed onto the heavy-duty fork).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:00:36 +00:00
Daniel Marin
ec41ade3fc
Create README.md 2026-07-10 15:53:40 +01:00