diff --git a/README.md b/README.md index 6fee60e..65eaaa2 100644 --- a/README.md +++ b/README.md @@ -82,9 +82,11 @@ claudebox shell # enter as the claude user claudebox exec -- # run a command in the box claudebox snapshot [label] # checkpoint (label defaults to manual-) claudebox restore # roll back to a snapshot +claudebox rename # rename a box (stop it first) claudebox down # stop (state kept; `start` resumes) claudebox start # start a stopped box claudebox rm [--force] # delete the box + its snapshots (asks first) +claudebox incus -- # escape hatch: any incus command, box resolved claudebox status # deprecated alias for `list` claudebox help [] # full help, or one command's page ``` @@ -98,6 +100,26 @@ snapshot. VM mode (`--vm`, the default where `/dev/kvm` exists) is the trust-les target; container mode (auto-fallback, `security.nesting=true`) is for hosts without nested virt — weaker isolation, dev/test only. +## Boxes are just Incus instances + +A box is an ordinary Incus instance tagged `user.claudebox=1`. claudebox 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: + +```sh +claudebox incus work -- config show # instance name appended +claudebox incus work -- file push x.tar {}/tmp/ # or placed with {} +``` + +The box is resolved and tag-checked; the rest is passed to `incus` verbatim, and +the command is echoed before it runs. If it can move the box off the isolation +stack (profile, network, device, `security.*`), claudebox warns and proceeds — +the trust boundary is then yours to keep. See +[docs/claudebox-design.md](docs/claudebox-design.md) for the rule and why the +command surface is a table. + ## Isolation Dedicated NAT bridge `claudenet` + Incus `claude-isolate` ACL dropping all diff --git a/VERSION b/VERSION index 0ea3a94..0d91a54 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.0 +0.3.0 diff --git a/bin/claudebox b/bin/claudebox index 1e1017d..9da33b7 100755 --- a/bin/claudebox +++ b/bin/claudebox @@ -1,38 +1,77 @@ #!/usr/bin/env bash # claudebox — trust-less, isolated Incus VMs with Claude Code, creds-free. -# The help text lives in usage()/help_cmd(), not in this comment. +# The command surface is the CMDS table below: it is the single source of truth +# for what exists, what it looks like, what the help says, and what runs. The +# help cannot drift from the code, because it is rendered from the same rows. set -euo pipefail root="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)" remote=""; mode="auto"; name=""; from=""; force=0; json=0; want_help=0 +inst="" # the resolved Incus instance, set by the 'box' precondition die() { echo "claudebox: $*" >&2; exit 1; } # 1 = it went wrong usage_error() { echo "claudebox: $*" >&2; echo "try 'claudebox help'." >&2; exit 2; } # 2 = you asked wrong version() { echo "claudebox $(cat "$root/VERSION" 2>/dev/null || echo unknown) ($root)"; } -COMMANDS="new list info shell exec snapshot restore down start rm status help" -is_command() { case " $COMMANDS " in *" $1 "*) return 0 ;; *) return 1 ;; esac; } +# --------------------------------------------------------------------------- +# The command table. +# +# verb ^ synopsis args ^ preconditions ^ one-line summary ^ action ^ ok message +# +# Fields are ^-separated because a synopsis may contain '|' ([--vm|--container]). +# +# preconditions (comma-separated): +# box first positional is a box: resolve it, and REFUSE if the instance +# isn't tagged user.claudebox=1 — the boundary, enforced, not assumed +# arg2 a second positional is required +# stopped the box must not be running +# confirm destructive: prompt unless --force +# +# action: +# incus: run `incus [rest...]` +# fn: call a shell function (it has real work to do) +# +# ok message: printed on success; {} = the box, {1} = the second positional. +# +# Adding a thin verb is one row. If a request can't be expressed as a row and +# doesn't enforce a claudebox invariant, it is incus's job, not ours — that is +# what `claudebox incus` is for. +CMDS=( + "new^--name [--from [/]] [--vm|--container]^^Mint a box: fresh from cloud-init, or --from an existing box/snapshot^fn:cmd_new^" + "list^[--json]^^List your boxes^fn:cmd_list^" + "info^ [--json]^box^One box: state, type, IP, and its snapshot labels^fn:cmd_info^" + "shell^^box^Open a shell in a box, as the claude user^fn:cmd_shell^" + "exec^ -- ^box^Run a command inside a box^fn:cmd_exec^" + "snapshot^ [