#!/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 ^ confirm prompt # # 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. The row supplies the words # (last field) — see 'confirm prompt' below. # # 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. # # confirm prompt: the words the 'confirm' precondition asks with, phrased as # the act ("delete X", "roll X back to Y") — confirm() wraps it into # "box: ? this cannot be undone." and into the no-TTY refusal # ("refusing to without --force"). It is a per-row field and not a # shared string on purpose: the prompt was hardcoded to rm's wording once, and # the whole reason 'restore' shipped ungated for four releases is that adding # the token to its row would have asked the operator to confirm DELETING the # box they were trying to rescue (#105). A gate that names the wrong act is # worse than no gate — it teaches people to answer 'y' without reading. Same # substitutions as the ok message, except {} is the RESOLVED instance: the # prompt names the thing about to be destroyed, so under --remote it should # say 'lab:work', not 'work'. # # 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^ [