From b90372da625621b7270d7755b5c3bbbf509dc455 Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Wed, 15 Jul 2026 00:17:33 +0000 Subject: [PATCH] =?UTF-8?q?feat(new):=20inline=20resource=20overrides=20?= =?UTF-8?q?=E2=80=94=20--cpu,=20--memory,=20--disk=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolution is most-specific-first: flag > BOX_* env (kept — it is the scripting form and how the drill shrinks boxes on small hosts) > the template's box.env > defaults. Values pass to Incus verbatim (limits.cpu, limits.memory, root size=) — its units, its validation; box adds no parser. Resources are all a flag can touch: there is still no flag for a network or a security.* key, on purpose. Flags shape a fresh mint only — --from refuses them, a clone carries its source's resources. An explicit --disk on a container mint gets a note instead of a silent drop (a container's root rides the pool). The drill's blank mint now carries --cpu 1 --memory 1GiB and asserts the limits landed — which is also the precedence proof, since the drill exports BOX_CPU/BOX_MEMORY on small hosts — plus a negative check that --from refuses resource flags. Verified live (container mint, image cached): BOX_CPU=3 + --cpu 1 --memory 1GiB → limits.cpu=1, limits.memory=1GiB; --from + --cpu exits 2 before touching anything; container --disk prints the note. Closes #57 Co-Authored-By: Claude Fable 5 --- README.md | 9 +++++---- bin/box | 36 +++++++++++++++++++++++++++--------- drill/drill.sh | 13 ++++++++++++- 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4909cc7..e925275 100644 --- a/README.md +++ b/README.md @@ -106,9 +106,10 @@ box new --name scratch # the DEFAULT template is blank: bare Debian, A template **cannot** name a network, a profile, or a `security.*` flag — there is no key for them. Every box launches with the shared `box-net` profile (the isolated NIC + root disk), so every template gets the identical -trust boundary. Resources come from the template's `box.env`; -`BOX_CPU` / `BOX_MEMORY` / `BOX_DISK` environment variables override them at -mint time. The template's identity (name, user) is stamped onto the instance, +trust boundary. Resources come from the template's `box.env`, overridable at +mint time — inline (`--cpu 2 --memory 3GiB --disk 20GiB`) or via +`BOX_CPU` / `BOX_MEMORY` / `BOX_DISK` environment variables (the scripting +form; flags win). The template's identity (name, user) is stamped onto the instance, so `shell`, `exec` and `tmux` land in the right user — and a clone still knows, because `incus copy` carries the metadata. @@ -152,7 +153,7 @@ the door is per-port, punched and removable at runtime. ## Commands ``` -box new --name [--template ] [--from [/]] [--vm|--container] +box new --name [--template ] [--from [/]] [--cpu ] [--memory ] [--disk ] [--vm|--container] box templates # list the templates this install can mint box list # list your boxes box info # one box: state, IP, exposures, snapshot labels diff --git a/bin/box b/bin/box index b55fd68..63be4de 100755 --- a/bin/box +++ b/bin/box @@ -7,6 +7,7 @@ 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="" inst="" # the resolved Incus instance, set by the 'box' precondition die() { echo "box: $*" >&2; exit 1; } # 1 = it went wrong @@ -37,7 +38,7 @@ version() { echo "box $(cat "$root/VERSION" 2>/dev/null || echo unknown) ($root) # 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 [/]] [--vm|--container]^^Mint a box from a template (default: blank), or --from an existing box/snapshot^fn:cmd_new^" + "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^" @@ -202,17 +203,24 @@ being told. A template sets image, user and resources — never the network: every template gets the same isolation. --from [/] Clone src's live state, or its snapshot . + --cpu CPUs for this mint (limits.cpu, verbatim to Incus). + --memory RAM for this mint, e.g. 3GiB (limits.memory). + --disk Root disk size, e.g. 20GiB. VM mode only — a + container's root rides the storage pool. --vm | --container Force the mode. VM is the trust boundary and the default wherever /dev/kvm exists; container mode (security.nesting=true) is the fallback for hosts without nested virt — weaker isolation, dev/test only. -Resources come from the template's box.env; BOX_CPU / BOX_MEMORY / BOX_DISK -environment variables override them at mint time (a small host shrinks a box -without editing a template it doesn't own). +Resources resolve most-specific-first: these flags, then BOX_CPU / +BOX_MEMORY / BOX_DISK environment variables (the scripting form), then the +template's box.env, then defaults. Flags shape a fresh mint only — a --from +clone carries its source's resources. Resources are all a flag can touch: +there is no flag for a network or a security key, on purpose. box new --name scratch # blank, the default box new --name work --template claude + box new --name lean --template claude --cpu 2 --memory 3GiB box new --name feature --from work/authed EOF ;; @@ -443,6 +451,9 @@ while [ $# -gt 0 ]; do --name) [ $# -ge 2 ] || usage_error "--name needs a value"; name="$2"; shift 2 ;; --from) [ $# -ge 2 ] || usage_error "--from needs a value"; from="$2"; shift 2 ;; --template) [ $# -ge 2 ] || usage_error "--template needs a value"; template="$2"; shift 2 ;; + --cpu) [ $# -ge 2 ] || usage_error "--cpu needs a value"; cpu="$2"; shift 2 ;; + --memory) [ $# -ge 2 ] || usage_error "--memory needs a value"; memory="$2"; shift 2 ;; + --disk) [ $# -ge 2 ] || usage_error "--disk needs a value"; disk="$2"; shift 2 ;; --remote) [ $# -ge 2 ] || usage_error "--remote needs a value"; remote="$2:"; shift 2 ;; --vm) mode=vm; shift ;; --container) mode=container; shift ;; @@ -640,11 +651,14 @@ load_template() { esac done <"$dir/box.env" [ -n "$T_IMAGE" ] && [ -n "$T_USER" ] || die "template '$t': BOX_IMAGE and BOX_USER are required" - # Environment overrides beat the file — this is how a small host (or the - # drill) shrinks a box without editing a template it doesn't own. - T_CPU="${BOX_CPU:-${T_CPU:-4}}" - T_MEMORY="${BOX_MEMORY:-${T_MEMORY:-8GiB}}" - T_DISK="${BOX_DISK:-${T_DISK:-60GiB}}" + # Resolution, most specific wins: inline flag (--cpu/--memory/--disk, #57) + # > BOX_* environment (how a small host or the drill shrinks every box it + # mints) > the template's file > defaults. Values pass to Incus verbatim — + # its units, its validation; box adds no parser of its own. Resources only: + # there is still no flag for a network or a security.* key, on purpose. + T_CPU="${cpu:-${BOX_CPU:-${T_CPU:-4}}}" + T_MEMORY="${memory:-${BOX_MEMORY:-${T_MEMORY:-8GiB}}}" + T_DISK="${disk:-${BOX_DISK:-${T_DISK:-60GiB}}}" } cmd_templates() { @@ -664,6 +678,7 @@ cmd_new() { local instance; instance="$(iname_of "$name")" if [ -n "$from" ]; then [ -z "$template" ] || usage_error "--from clones an existing box; its template rides along (drop --template)" + [ -z "$cpu$memory$disk" ] || usage_error "--cpu/--memory/--disk shape a fresh mint; a clone carries its source's resources ('box incus' can change them afterwards)" local src="${from%%/*}" snap="" srcref case "$from" in */*) snap="${from#*/}" ;; esac srcref="$(iname_of "$src")"; [ -n "$snap" ] && srcref="$srcref/$snap" @@ -685,6 +700,9 @@ cmd_new() { # turning it off boots reliably across image rebuilds. Container mode has # no firmware, so it does not apply there. if [ "$m" = vm ]; then extra+=(--vm --device "root,size=$T_DISK" --config security.secureboot=false); else extra+=(--config security.nesting=true); fi + # Root size is a VM launch concern; a container's root rides the pool. Say + # so instead of silently dropping an explicit --disk. + [ "$m" = vm ] || [ -z "$disk" ] || echo "box: note — --disk applies to VM mode only; this container's root rides the pool" >&2 # The template's identity is stamped ONTO the instance: which template, # which user. 'incus copy' preserves user.* keys (audit B2), so a clone # knows what it is without ever consulting the template again. diff --git a/drill/drill.sh b/drill/drill.sh index 77c8be5..9d60bae 100755 --- a/drill/drill.sh +++ b/drill/drill.sh @@ -422,13 +422,24 @@ box new --name tpl --template cbdrill-bad 2>&1 | grep -q "unknown key 'BOX_NETWO || no "a box.env key outside the allowlist was ACCEPTED — a template could weaken isolation" rm -rf "$badt" +# Inline resource flags (#57): refused on a clone, honored on a mint. The +# mint proof rides the blank box below — and because this drill exports +# BOX_CPU/BOX_MEMORY on small hosts, it is also the precedence proof +# (flag > env > template > default). +box new --name tpl --from nowhere --cpu 2 2>&1 | grep -q 'carries its source' \ + && ok "resource flags refused on --from — a clone carries its source's resources" \ + || no "--from accepted a resource flag (should refuse: clone resources come from the source)" + printf '\n minting a blank box (the DEFAULT template — no tooling, fast)…\n' t0=$SECONDS -if mint_box /tmp/mint-tpl.log --name tpl; then +if mint_box /tmp/mint-tpl.log --name tpl --cpu 1 --memory 1GiB; then ok "box new --name tpl, no --template ($((SECONDS - t0))s)" tt="$(incus config get tpl user.box.template 2>/dev/null)" [ "$tt" = blank ] && ok "the default template is blank (user.box.template=blank)" \ || no "default template is '${tt:-}' — expected blank" + rc="$(incus config get tpl limits.cpu 2>/dev/null)/$(incus config get tpl limits.memory 2>/dev/null)" + [ "$rc" = "1/1GiB" ] && ok "inline --cpu/--memory landed (limits = $rc, beating BOX_* env)" \ + || no "inline resource flags did not land — limits are $rc, expected 1/1GiB" [ "$(incus config get tpl user.box.user 2>/dev/null)" = dev ] \ && ok "template user stamped on the instance (user.box.user=dev)" || no "user.box.user not stamped" incus config show tpl 2>/dev/null | grep -q '^- box-net' \