Merge pull request #59 from claude-hdb/feat/new-resource-flags
feat(new): inline resource overrides — --cpu, --memory, --disk
This commit is contained in:
commit
1cbdfb05dd
4 changed files with 117 additions and 15 deletions
72
CHANGELOG.md
Normal file
72
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
History before 0.5.0 lives in git and in [drill/RUNS.md](drill/RUNS.md),
|
||||||
|
which records not just what changed but what each drill run proved.
|
||||||
|
|
||||||
|
## 0.5.0 — 2026-07-15
|
||||||
|
|
||||||
|
The release the project was renamed in: the repo is `heavy-duty/box`, matching
|
||||||
|
the CLI it ships. Everything legacy-facing is honored forever — the
|
||||||
|
`user.claudebox=1` tag, the `.claudebox/` runbook folder, the old symlink the
|
||||||
|
installer retires — but nothing current carries the old name.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`codex` and `grok` templates** — OpenAI Codex CLI and xAI Grok CLI boxes,
|
||||||
|
creds-free like every template. The template mechanic (image + user +
|
||||||
|
resources, never a network or a `security.*` key) now has three tenants
|
||||||
|
beside `blank`, and the drill mints all of them cold.
|
||||||
|
- **`box expose <box> <port> [<host-port>]`** — a deliberate, loopback-only
|
||||||
|
door to a port inside a box, for seeing a dev server in your browser. The
|
||||||
|
listen side is always the host's `127.0.0.1` (no flag to widen it), the door
|
||||||
|
is per-port, `--list`/`--remove` manage it, and `box info` shows open
|
||||||
|
exposures — a box with a hole says so.
|
||||||
|
- **Inline resource overrides on `new`** — `--cpu <n> --memory <size>
|
||||||
|
--disk <size>` (#57). Resolution most-specific-first: flag > `BOX_CPU` /
|
||||||
|
`BOX_MEMORY` / `BOX_DISK` environment (the scripting form) > template
|
||||||
|
`box.env` > defaults. Values pass to Incus verbatim; resources are all a
|
||||||
|
flag can touch. `--from` refuses them — a clone carries its source's
|
||||||
|
resources.
|
||||||
|
- **Host lifecycle as verbs** — `box setup-host`, `box teardown-host`, and
|
||||||
|
`box migrate-host`, which re-homes pre-0.4.0 boxes onto the current stack
|
||||||
|
(`--box <n>` / `--all-boxes`, authed state preserved) and retires the legacy
|
||||||
|
bridge once empty (`--retire-legacy`).
|
||||||
|
- **The `.box/` recipe convention** — the agent-facing runbook folder a repo
|
||||||
|
can ship, renamed from `.claudebox/` (both spellings read).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **VM mints no longer hang at GRUB** — Incus defaults VMs to Secure Boot on,
|
||||||
|
and a cloud image whose shim the host's OVMF doesn't trust dies with "bad
|
||||||
|
shim signature" forever. Boxes now launch with `security.secureboot=false`;
|
||||||
|
the VM boundary, not boot attestation, is the box threat model.
|
||||||
|
- **`box expose` actually delivers packets** — a trilogy of drill-found
|
||||||
|
absences: the NAT proxy needs the box's boxnet lease pinned as a static
|
||||||
|
`ipv4.address` (Incus resolves `connect=0.0.0.0` against device config, not
|
||||||
|
the lease); a loopback-sourced packet needs `route_localnet` plus a
|
||||||
|
masquerade on the bridge to leave the host and be answerable; and the box's
|
||||||
|
replies need a `ct state established,related` accept ahead of the host
|
||||||
|
firewall's input drop, which was eating them statelessly. Boxes still
|
||||||
|
cannot initiate toward the host — a box-originated SYN is a NEW flow.
|
||||||
|
- **Firewall rules now converge on upgrade** — `box-firewall.sh` rebuilds its
|
||||||
|
chains every run (add + flush + re-add) instead of skipping when they
|
||||||
|
exist, which had pinned every host to the rule set of the release that
|
||||||
|
first ran there.
|
||||||
|
- **Failed mints tell you why** — cloud-init failures print the box's own log
|
||||||
|
excerpts and leave the box up to inspect; a mint that never boots names the
|
||||||
|
likely cause (corrupt image, Secure Boot, GRUB hang) and ships a sanitized
|
||||||
|
console dump; the installer asserts it landed the ref it was asked for.
|
||||||
|
- **`grok` installs the binary it actually ships** — the installer was read,
|
||||||
|
not guessed at, and the CLI lands on the non-interactive PATH (same fix
|
||||||
|
class as codex).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Debrand complete** — env vars, install dir, docs, template descriptions
|
||||||
|
and the README all say `box`; the install URL is
|
||||||
|
`heavy-duty/box` (GitHub redirects the old one, `BOX_REPO` overrides).
|
||||||
|
- **The drill grew from 47 to 83 checks** — the expose door opened, exercised
|
||||||
|
and shut (with the contract re-probed around it), every template minted
|
||||||
|
cold, a faithful pre-0.4.0 box re-homed through `migrate-host`, and the
|
||||||
|
inline resource flags asserted (including their precedence over the
|
||||||
|
environment).
|
||||||
11
README.md
11
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 —
|
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`
|
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
|
profile (the isolated NIC + root disk), so every template gets the identical
|
||||||
trust boundary. Resources come from the template's `box.env`;
|
trust boundary. Resources come from the template's `box.env`, overridable at
|
||||||
`BOX_CPU` / `BOX_MEMORY` / `BOX_DISK` environment variables override them at
|
mint time — inline (`--cpu 2 --memory 3GiB --disk 20GiB`) or via
|
||||||
mint time. The template's identity (name, user) is stamped onto the instance,
|
`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
|
so `shell`, `exec` and `tmux` land in the right user — and a clone still
|
||||||
knows, because `incus copy` carries the metadata.
|
knows, because `incus copy` carries the metadata.
|
||||||
|
|
||||||
|
|
@ -152,7 +153,7 @@ the door is per-port, punched and removable at runtime.
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```
|
```
|
||||||
box new --name <box> [--template <t>] [--from <src>[/<snap>]] [--vm|--container]
|
box new --name <box> [--template <t>] [--from <src>[/<snap>]] [--cpu <n>] [--memory <size>] [--disk <size>] [--vm|--container]
|
||||||
box templates # list the templates this install can mint
|
box templates # list the templates this install can mint
|
||||||
box list # list your boxes
|
box list # list your boxes
|
||||||
box info <box> # one box: state, IP, exposures, snapshot labels
|
box info <box> # one box: state, IP, exposures, snapshot labels
|
||||||
|
|
@ -246,7 +247,7 @@ drill installs the whole stack, mints every template cold, snapshots and
|
||||||
clones, probes every boundary from inside the boxes, opens and shuts the
|
clones, probes every boundary from inside the boxes, opens and shuts the
|
||||||
`expose` door (and checks the contract survives it), re-homes a faithful
|
`expose` door (and checks the contract survives it), re-homes a faithful
|
||||||
pre-0.4.0 box through `migrate-host`, and removes what it minted —
|
pre-0.4.0 box through `migrate-host`, and removes what it minted —
|
||||||
currently **81 checks, 81 passing**. [drill/RUNS.md](drill/RUNS.md) is the full
|
currently **83 checks**. [drill/RUNS.md](drill/RUNS.md) is the full
|
||||||
history, including every trap that fooled a run into a wrong verdict.
|
history, including every trap that fooled a run into a wrong verdict.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
|
||||||
36
bin/box
36
bin/box
|
|
@ -7,6 +7,7 @@ set -euo pipefail
|
||||||
|
|
||||||
root="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
|
root="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
|
||||||
remote=""; mode="auto"; name=""; from=""; template=""; force=0; json=0; want_help=0
|
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
|
inst="" # the resolved Incus instance, set by the 'box' precondition
|
||||||
|
|
||||||
die() { echo "box: $*" >&2; exit 1; } # 1 = it went wrong
|
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
|
# doesn't enforce a box invariant, it is incus's job, not ours — that is
|
||||||
# what `box incus` is for.
|
# what `box incus` is for.
|
||||||
CMDS=(
|
CMDS=(
|
||||||
"new^--name <box> [--template <t>] [--from <src>[/<snap>]] [--vm|--container]^^Mint a box from a template (default: blank), or --from an existing box/snapshot^fn:cmd_new^"
|
"new^--name <box> [--template <t>] [--from <src>[/<snap>]] [--cpu <n>] [--memory <size>] [--disk <size>] [--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^"
|
"templates^^^List the templates this install can mint^fn:cmd_templates^"
|
||||||
"list^[--json]^^List your boxes^fn:cmd_list^"
|
"list^[--json]^^List your boxes^fn:cmd_list^"
|
||||||
"info^<box> [--json]^box^One box: state, type, IP, and its snapshot labels^fn:cmd_info^"
|
"info^<box> [--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
|
A template sets image, user and resources — never
|
||||||
the network: every template gets the same isolation.
|
the network: every template gets the same isolation.
|
||||||
--from <src>[/<snap>] Clone src's live state, or its snapshot <snap>.
|
--from <src>[/<snap>] Clone src's live state, or its snapshot <snap>.
|
||||||
|
--cpu <n> CPUs for this mint (limits.cpu, verbatim to Incus).
|
||||||
|
--memory <size> RAM for this mint, e.g. 3GiB (limits.memory).
|
||||||
|
--disk <size> 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
|
--vm | --container Force the mode. VM is the trust boundary and the
|
||||||
default wherever /dev/kvm exists; container mode
|
default wherever /dev/kvm exists; container mode
|
||||||
(security.nesting=true) is the fallback for hosts
|
(security.nesting=true) is the fallback for hosts
|
||||||
without nested virt — weaker isolation, dev/test only.
|
without nested virt — weaker isolation, dev/test only.
|
||||||
|
|
||||||
Resources come from the template's box.env; BOX_CPU / BOX_MEMORY / BOX_DISK
|
Resources resolve most-specific-first: these flags, then BOX_CPU /
|
||||||
environment variables override them at mint time (a small host shrinks a box
|
BOX_MEMORY / BOX_DISK environment variables (the scripting form), then the
|
||||||
without editing a template it doesn't own).
|
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 scratch # blank, the default
|
||||||
box new --name work --template claude
|
box new --name work --template claude
|
||||||
|
box new --name lean --template claude --cpu 2 --memory 3GiB
|
||||||
box new --name feature --from work/authed
|
box new --name feature --from work/authed
|
||||||
EOF
|
EOF
|
||||||
;;
|
;;
|
||||||
|
|
@ -443,6 +451,9 @@ while [ $# -gt 0 ]; do
|
||||||
--name) [ $# -ge 2 ] || usage_error "--name needs a value"; name="$2"; shift 2 ;;
|
--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 ;;
|
--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 ;;
|
--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 ;;
|
--remote) [ $# -ge 2 ] || usage_error "--remote needs a value"; remote="$2:"; shift 2 ;;
|
||||||
--vm) mode=vm; shift ;;
|
--vm) mode=vm; shift ;;
|
||||||
--container) mode=container; shift ;;
|
--container) mode=container; shift ;;
|
||||||
|
|
@ -640,11 +651,14 @@ load_template() {
|
||||||
esac
|
esac
|
||||||
done <"$dir/box.env"
|
done <"$dir/box.env"
|
||||||
[ -n "$T_IMAGE" ] && [ -n "$T_USER" ] || die "template '$t': BOX_IMAGE and BOX_USER are required"
|
[ -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
|
# Resolution, most specific wins: inline flag (--cpu/--memory/--disk, #57)
|
||||||
# drill) shrinks a box without editing a template it doesn't own.
|
# > BOX_* environment (how a small host or the drill shrinks every box it
|
||||||
T_CPU="${BOX_CPU:-${T_CPU:-4}}"
|
# mints) > the template's file > defaults. Values pass to Incus verbatim —
|
||||||
T_MEMORY="${BOX_MEMORY:-${T_MEMORY:-8GiB}}"
|
# its units, its validation; box adds no parser of its own. Resources only:
|
||||||
T_DISK="${BOX_DISK:-${T_DISK:-60GiB}}"
|
# 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() {
|
cmd_templates() {
|
||||||
|
|
@ -664,6 +678,7 @@ cmd_new() {
|
||||||
local instance; instance="$(iname_of "$name")"
|
local instance; instance="$(iname_of "$name")"
|
||||||
if [ -n "$from" ]; then
|
if [ -n "$from" ]; then
|
||||||
[ -z "$template" ] || usage_error "--from clones an existing box; its template rides along (drop --template)"
|
[ -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
|
local src="${from%%/*}" snap="" srcref
|
||||||
case "$from" in */*) snap="${from#*/}" ;; esac
|
case "$from" in */*) snap="${from#*/}" ;; esac
|
||||||
srcref="$(iname_of "$src")"; [ -n "$snap" ] && srcref="$srcref/$snap"
|
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
|
# turning it off boots reliably across image rebuilds. Container mode has
|
||||||
# no firmware, so it does not apply there.
|
# 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
|
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,
|
# The template's identity is stamped ONTO the instance: which template,
|
||||||
# which user. 'incus copy' preserves user.* keys (audit B2), so a clone
|
# which user. 'incus copy' preserves user.* keys (audit B2), so a clone
|
||||||
# knows what it is without ever consulting the template again.
|
# knows what it is without ever consulting the template again.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|| no "a box.env key outside the allowlist was ACCEPTED — a template could weaken isolation"
|
||||||
rm -rf "$badt"
|
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'
|
printf '\n minting a blank box (the DEFAULT template — no tooling, fast)…\n'
|
||||||
t0=$SECONDS
|
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)"
|
ok "box new --name tpl, no --template ($((SECONDS - t0))s)"
|
||||||
tt="$(incus config get tpl user.box.template 2>/dev/null)"
|
tt="$(incus config get tpl user.box.template 2>/dev/null)"
|
||||||
[ "$tt" = blank ] && ok "the default template is blank (user.box.template=blank)" \
|
[ "$tt" = blank ] && ok "the default template is blank (user.box.template=blank)" \
|
||||||
|| no "default template is '${tt:-<unset>}' — expected blank"
|
|| no "default template is '${tt:-<unset>}' — 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 ] \
|
[ "$(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"
|
&& 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' \
|
incus config show tpl 2>/dev/null | grep -q '^- box-net' \
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue