#!/usr/bin/env bash # box — trust-less, isolated Incus VMs with Claude Code, creds-free. # 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=""; template=""; force=0; json=0; want_help=0 cpu=""; memory=""; disk=""; instance_only=0 inst="" # the resolved Incus instance, set by the 'box' precondition die() { echo "box: $*" >&2; exit 1; } # 1 = it went wrong usage_error() { echo "box: $*" >&2; echo "try 'box help'." >&2; exit 2; } # 2 = you asked wrong version() { echo "box $(cat "$root/VERSION" 2>/dev/null || echo unknown) ($root)"; } # Which tier is THIS PROCESS? Decided from live credentials (argless 'id -nG': # what the kernel will present when incus opens the socket), never from the # group database — the two disagree for exactly as long as a re-login is # pending, and that window is where every wrong answer lives. # UID 0 / incus-admin -> admin (the full daemon socket) # incus (only) -> restricted (incus-user: your own project, nothing else) # neither -> none (no socket at all) # host/setup-host.sh carries a byte-identical copy (it runs before any install # tree exists); test/cli.sh diffs the two so they cannot drift. box_tier() { [ "$(id -u)" -eq 0 ] && { printf 'admin\n'; return; } local groups; groups="$(id -nG 2>/dev/null | tr ' ' '\n')" if printf '%s\n' "$groups" | grep -qx incus-admin; then printf 'admin\n' elif printf '%s\n' "$groups" | grep -qx incus; then printf 'restricted\n' else printf 'none\n' fi } # --------------------------------------------------------------------------- # 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.box=1 (or the legacy 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 box invariant, it is incus's job, not ours — that is # what `box incus` is for. CMDS=( "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^" "shell^^box^Open a shell in a box, as its template's user^fn:cmd_shell^" "exec^ -- ^box^Run a command inside a box^fn:cmd_exec^" "tmux^ []^box^Attach or create a tmux session in a box — survives disconnects^fn:cmd_tmux^" "snapshot^ [