#71 — run as root, box installs once to /opt/box (world-readable) with a /usr/local/bin shim, so every operator on a shared host has `box` on PATH instead of `command not found`. Per-user ($HOME/.local) install is byte-identical to before; BOX_HOME/BOX_BIN still override. A per-user tree under /root is 0700 and unreadable to everyone else — the whole fleet got `command not found` — so the root branch lands in a system location and chmod -R a+rX's it, guarded on root. This is the path rig installs box through (rig#24's `box` role). #65 — `box tmux` runs `tmux new-session` inside the box, but the templates did not install tmux, so it failed with `tmux: command not found`. tmux is now in each template's cloud-init package list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.7 KiB
Changelog
History before 0.5.0 lives in git and in drill/RUNS.md, which records not just what changed but what each drill run proved.
Unreleased
Added
- Global / root install (#71) — run as root, box installs once to
/opt/box(world-readable) with theboxsymlink on/usr/local/bin, so every operator on a shared host runs the same tree. Per-user installs are unchanged ($HOME/.local);BOX_HOME/BOX_BINstill override. A per-user tree under/rootis0700and unreadable to everyone else — the whole fleet gotcommand not found— so the root branch lands in a system location andchmod -R a+rX's it (read for files, +search on dirs), guarded on root. This unblocks "rig installs box" (rig#24'sboxrole). - CI + a test suite —
.github/workflows/ci.yml(acheckjob: globstarshellcheck -xoverbin/* **/*.sh, thenbash test/cli.sh) andtest/cli.sh, dependency-free and runnable by a non-root user with no Incus. It exercises theinstall.shDEST/BINDIR branch functionally (both tiers +BOX_HOME/BOX_BINoverrides), the CLI contract, and grep-guards the daemon-gated invariants and tmux in every template — the box was the repo with "no tests and no CI".
Fixed
-
box tmuxworks on every template (#65) —box tmuxrunstmux new-sessioninside the box, but the templates did not install tmux, so it failed withtmux: command not found.tmuxis now in each template's cloud-init package list (blank/claude/codex/grok). -
box setup-hostfinishes in one run (#63). When it had to add you toincus-adminit stopped there and told you to re-login and re-run — anexit 0that reported success having built none of the stack: noboxnet, no ACL, nobox-netprofile, no firewall. It now re-execs itself undersg incus-adminand completes in that one invocation. The membership check was also asking the wrong question:id -nG "$USER"reads the group database, which lists the group the momentusermodreturns, so a same-session re-run passed the check with credentials that still lacked the group and died further down on a bare permission error fromincus. Arglessid -nGasks the process what it actually holds. -
setup-hostworks as root, with or withoutsudo— every privileged call was a hardcodedsudo, so on a minimal root image (nosudopackage) it died onsudo: command not foundbefore doing anything. Privilege is now resolved once: nothing at UID 0,sudootherwise, and a clear error if neither is possible. This is what madeinstall.sh's root path real rather than nominal. -
setup-hostgrantsincus-adminto the human, not to root — undersudo install.shit would have addedrootto the group: a no-op (UID 0 opens the socket regardless) that also left the actual user locked out of their own boxes. It now derives the login user fromSUDO_USER. -
box-firewall.servicenow reports its state honestly — the unit isType=oneshotand was missingRemainAfterExit=yes, so it wentinactive (dead)the instant it succeeded: a host whose isolation was perfectly live read as one whose firewall unit had died. drill.sh sends you tosystemctl status box-firewallto diagnose exactly that, and setup-host.sh's own comment already asserted the unit "is RemainAfterExit" — it was not. Found by running the drill on a real host and mistrusting the green:nft list table bridge boxshowed the drop live while the unit read dead.restartwas and remains correct either way. -
setup-host's apt calls can no longer hang — a fresh cloud image hasapt-daily/unattended-upgradesholding the dpkg lock, and a plainapt-get installwaits on it silently and indefinitely. Now bounded (DPkg::Lock::Timeout=300) and non-interactive, which matters becauseinstall.shruns it with nobody watching.
Changed
drill.shproves the new contract instead of masking it — the drill ransetup-hostitself right after installing, so the stack existed by its own hand and a run passed identically whether or notinstall.shhad done a thing; a fresh run converged the stack three times, while the messages still described the pre-#63 "first pass may only add you to the group" behaviour. It now asserts the post-install stack in-group before touching the host, and runssetup-hostexactly once more — after the clean, which deliberately unsetsdns.modeand so has to be converged back.DRILL_OWNS_SETUP=1hands sequencing back to the drill. Pre-setup tripwires now read beforeinstall.sh, since that is what triggers setup now.install.shasks, sets up the host, and no-ops on re-run (#64) — it now prompts "Install box?", then on a fresh host installs the tree and asks a second question, "Set up this machine as a box host now?", running the whole isolation stack if you say yes (previously it only printed a warning and left you a command, so the install reported success andbox newdied on a host with no Incus). Prompts read/dev/tty, since undercurl | bashthe script itself is stdin;BOX_YES=1answers yes unattended (required where there is no terminal),BOX_SKIP_SETUP_HOST=1declines the host-setup step.install.shnever overwrites an existing install — if box is already installed it says so and changes nothing, so a stray re-run can no longer clobber a working tree or rebuild the host stack under live boxes. Upgrading is explicit: uninstall (rm -rf ~/.local/share/box ~/.local/bin/box, boxes preserved first) and install fresh. This replaces the earlier version-diff refusal with a simpler rule that dissolves the same class of errors. The version-aware upgrade that migrates boxes instead is #67; a portablebox exportso a box survives its own deletion is #70.
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
codexandgroktemplates — OpenAI Codex CLI and xAI Grok CLI boxes, creds-free like every template. The template mechanic (image + user + resources, never a network or asecurity.*key) now has three tenants besideblank, 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's127.0.0.1(no flag to widen it), the door is per-port,--list/--removemanage it, andbox infoshows 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_DISKenvironment (the scripting form) > templatebox.env> defaults. Values pass to Incus verbatim; resources are all a flag can touch.--fromrefuses them — a clone carries its source's resources. - Host lifecycle as verbs —
box setup-host,box teardown-host, andbox 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 exposeactually delivers packets — a trilogy of drill-found absences: the NAT proxy needs the box's boxnet lease pinned as a staticipv4.address(Incus resolvesconnect=0.0.0.0against device config, not the lease); a loopback-sourced packet needsroute_localnetplus a masquerade on the bridge to leave the host and be answerable; and the box's replies need act state established,relatedaccept 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.shrebuilds 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.
grokinstalls 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 isheavy-duty/box(GitHub redirects the old one,BOX_REPOoverrides). - The drill grew from 47 to 84 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).