box/CHANGELOG.md

194 lines
12 KiB
Markdown
Raw Permalink Normal View History

# 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.
## Unreleased
2026-07-18 13:39:45 +00:00
## 0.6.0 — 2026-07-18
### Added
- **The restricted tier: multi-user hosts** (#74, redesigning #72) — an admin
runs `box grant <user>` and that user gets their own boxes on the same
hardened `boxnet`, seeing nobody else's; `box revoke <user>` takes it back
(`--purge` deletes their world, and asserts the absence). The tier rides
incus-user, whose defaults miss box's contract three measured ways (Debian
2026-07-18 13:39:45 +00:00
13 / Incus 6.0.4): a private _unhardened_ NAT bridge per user, snapshots
blocked, the `box-net` profile invisible — so grant is an idempotent
convergence: project narrowed to `boxnet` **and only boxnet** (listing the
private bridge too, the obvious fix, would keep an unhardened network one
`--network` flag away), snapshots allowed, the shipped profile installed
into their project. `box_tier()` (live credentials, argless `id -nG`)
drives the tier-aware surface: `expose` refuses honestly before any daemon
call, `setup-host` and `doctor` answer at the caller's tier. Rehearsed
grant/rehearsal: the codex round — verified rollback, loud partial states, and the raw-attach guarantee measured (#75) Review 4727756972 (A2): the backout no longer trusts gpasswd — it re-reads the live group database after removal; verified-absent gets the safe message, anything else screams ROLLBACK INCOMPLETE, exits nonzero, and names the exact remediation. The concurrent-login window (a session begun between usermod and backout keeps the group) is CLOSED to the extent the database can't reach: the backout detects live processes and names loginctl terminate-user, and the success wording claims only what was verified. Review 4727641752 (A1): a failed grant for a user whose membership predates the run (the hand-added-user scenario) now fails LOUDLY — they retain socket access on part-converged policy, and the message says so with both remediations (box revoke now, or fix and re-run). Their membership is not stripped: breaking a working user over a failed re-grant is its own hazard. The default-profile eth0 removal is deliberately not restored on failure — that mutation only reduces capability, and restoring it would move the failure state AWAY from fail-closed. Injected-failure coverage is criterion (n), both flavors: fresh-user backout (fault at the LAST mutation, so the rollback runs after every earlier one) with the group's absence verified and a converging re-run; blocked narrowing staged for real with an instance-local NIC parked on the private bridge. Review A3, resolution 3 with the measurement demanded: criterion (m) launches exactly 'incus launch --network boxnet' as the restricted user and probes the raw NIC from inside — egress works, RFC1918 dropped (the ACL is the network's), sibling probes dropped BOTH directions (the nft drop is the host's), name enumeration blocked. The scoped guarantee is now stated in box-design.md and measured on every run: box-minted instances carry per-NIC port_isolation; raw attachments keep every network- and host-owned control, losing only that redundant L2 layer. Instrument lesson kept as MU-5: the probe's first cut minted the non-cloud image — no DHCP client, no lease, and a dead NIC passes every negative probe vacuously; it now requires the lease before believing its own answers. Rehearsal: 54/54 (containers). test/cli.sh: 82 checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 06:41:05 +00:00
end-to-end by `drill/multiuser.sh` (criteria an: confinement, lifecycle,
cross-user visibility, name collisions, the in-box isolation contract,
escape hatches, re-sync survival, revoke incl. the live-session case) —
grant/rehearsal: the codex round — verified rollback, loud partial states, and the raw-attach guarantee measured (#75) Review 4727756972 (A2): the backout no longer trusts gpasswd — it re-reads the live group database after removal; verified-absent gets the safe message, anything else screams ROLLBACK INCOMPLETE, exits nonzero, and names the exact remediation. The concurrent-login window (a session begun between usermod and backout keeps the group) is CLOSED to the extent the database can't reach: the backout detects live processes and names loginctl terminate-user, and the success wording claims only what was verified. Review 4727641752 (A1): a failed grant for a user whose membership predates the run (the hand-added-user scenario) now fails LOUDLY — they retain socket access on part-converged policy, and the message says so with both remediations (box revoke now, or fix and re-run). Their membership is not stripped: breaking a working user over a failed re-grant is its own hazard. The default-profile eth0 removal is deliberately not restored on failure — that mutation only reduces capability, and restoring it would move the failure state AWAY from fail-closed. Injected-failure coverage is criterion (n), both flavors: fresh-user backout (fault at the LAST mutation, so the rollback runs after every earlier one) with the group's absence verified and a converging re-run; blocked narrowing staged for real with an instance-local NIC parked on the private bridge. Review A3, resolution 3 with the measurement demanded: criterion (m) launches exactly 'incus launch --network boxnet' as the restricted user and probes the raw NIC from inside — egress works, RFC1918 dropped (the ACL is the network's), sibling probes dropped BOTH directions (the nft drop is the host's), name enumeration blocked. The scoped guarantee is now stated in box-design.md and measured on every run: box-minted instances carry per-NIC port_isolation; raw attachments keep every network- and host-owned control, losing only that redundant L2 layer. Instrument lesson kept as MU-5: the probe's first cut minted the non-cloud image — no DHCP client, no lease, and a dead NIC passes every negative probe vacuously; it now requires the lease before believing its own answers. Rehearsal: 54/54 (containers). test/cli.sh: 82 checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 06:41:05 +00:00
54/54 on the design host (container and VM mode), including the raw-attach scoped-guarantee measurement and both grant-failure injections demanded by #75's review.
- **CI runs the multi-user rehearsal on a real Incus** — a second `rehearsal`
job stands up the full stack on the runner (setup-host, doctor, then
`multiuser.sh --container`), so every PR proves the tier's semantics
against a live daemon, not a mock. The VM trust boundary itself remains a
real-hardware ritual, like the full drill.
2026-07-18 13:39:45 +00:00
- **Global / root install** (#71) — run as root, box installs _once_ to
`/opt/box` (world-readable) with the `box` symlink 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_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 (read for files, +search on dirs), guarded on root. This
unblocks "rig installs box" (rig#24's `box` role).
- **CI + a test suite** — `.github/workflows/ci.yml` (a `check` job: globstar
`shellcheck -x` over `bin/* **/*.sh`, then `bash test/cli.sh`) and `test/cli.sh`,
dependency-free and runnable by a non-root user with no Incus. It exercises the
`install.sh` DEST/BINDIR branch functionally (both tiers + `BOX_HOME`/`BOX_BIN`
overrides), 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 restore` never worked against Incus 6** — the command table
dispatched `incus restore`, a subcommand that does not exist (Incus 6
spells it `incus snapshot restore`), so every restore died on "unknown
command". Found by #74's rehearsal exercising the full lifecycle as a
restricted user; fixed for every tier, and the rehearsal + a grep-guard in
`test/cli.sh` now hold it.
- **`box tmux` works on every template** (#65) — `box tmux` runs
2026-07-18 13:39:45 +00:00
`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 (`blank`/`claude`/`codex`/`grok`).
- **`box setup-host` finishes in one run** (#63). When it had to add you to
`incus-admin` it stopped there and told you to re-login and re-run — an
`exit 0` that reported success having built none of the stack: no `boxnet`,
no ACL, no `box-net` profile, no firewall. It now re-execs itself under
`sg incus-admin` and 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 moment `usermod` returns, 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 from `incus`. Argless
`id -nG` asks the process what it actually holds.
Make setup-host privilege-aware; make the drill prove the new contract Review found two real problems, both confirmed by reproducing them. setup-host hardcoded 'sudo' for every privileged call, so install.sh's deliberate root branch — the one that proceeds when id -u is 0 even with no sudo installed — handed off to a script that died on 'sudo: command not found' before doing anything (exit 127, reproduced with env -i and a minimal PATH). The root path was nominal, not real. Privilege is now resolved once: nothing at UID 0, sudo otherwise, a clear error if neither is possible. Two things fell out of that. Root does not need incus-admin at all (UID 0 opens the socket regardless), so adding root to the group was a no-op that also missed the human — under 'sudo install.sh' that is SUDO_USER, who is now the one granted the group. And apt must not hang: install.sh runs setup-host with nobody watching, while a fresh cloud image holds the dpkg lock in apt-daily for its first minutes, so the calls are now bounded and non-interactive. The drill did not exercise any of this. It ran setup-host immediately after install.sh, so the stack existed by the drill's own hand and a run passed identically whether or not install.sh had done a thing — a fresh run converged three times while its 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 the clean or anything else mutates the host, which is the assertion that actually proves #64. setup-host then runs exactly once more, after the clean — that one is load-bearing, since the clean deliberately unsets dns.mode and something has to converge it back. DRILL_OWNS_SETUP=1 hands sequencing back to the drill. Pre-setup tripwires now read before install.sh, because install.sh is what triggers setup now; read afterwards they said nothing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 13:16:06 +00:00
- **`setup-host` works as root, with or without `sudo`** — every privileged
call was a hardcoded `sudo`, so on a minimal root image (no `sudo` package)
it died on `sudo: command not found` before doing anything. Privilege is now
resolved once: nothing at UID 0, `sudo` otherwise, and a clear error if
neither is possible. This is what made `install.sh`'s root path real rather
than nominal.
- **`setup-host` grants `incus-admin` to the human, not to root** — under
`sudo install.sh` it would have added `root` to 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 from `SUDO_USER`.
- **`box-firewall.service` now reports its state honestly** — the unit is
`Type=oneshot` and was missing `RemainAfterExit=yes`, so it went
`inactive (dead)` the instant it succeeded: a host whose isolation was
perfectly live read as one whose firewall unit had died. drill.sh sends you
to `systemctl status box-firewall` to 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 box` showed the drop live while the unit read
dead. `restart` was and remains correct either way.
Make setup-host privilege-aware; make the drill prove the new contract Review found two real problems, both confirmed by reproducing them. setup-host hardcoded 'sudo' for every privileged call, so install.sh's deliberate root branch — the one that proceeds when id -u is 0 even with no sudo installed — handed off to a script that died on 'sudo: command not found' before doing anything (exit 127, reproduced with env -i and a minimal PATH). The root path was nominal, not real. Privilege is now resolved once: nothing at UID 0, sudo otherwise, a clear error if neither is possible. Two things fell out of that. Root does not need incus-admin at all (UID 0 opens the socket regardless), so adding root to the group was a no-op that also missed the human — under 'sudo install.sh' that is SUDO_USER, who is now the one granted the group. And apt must not hang: install.sh runs setup-host with nobody watching, while a fresh cloud image holds the dpkg lock in apt-daily for its first minutes, so the calls are now bounded and non-interactive. The drill did not exercise any of this. It ran setup-host immediately after install.sh, so the stack existed by the drill's own hand and a run passed identically whether or not install.sh had done a thing — a fresh run converged three times while its 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 the clean or anything else mutates the host, which is the assertion that actually proves #64. setup-host then runs exactly once more, after the clean — that one is load-bearing, since the clean deliberately unsets dns.mode and something has to converge it back. DRILL_OWNS_SETUP=1 hands sequencing back to the drill. Pre-setup tripwires now read before install.sh, because install.sh is what triggers setup now; read afterwards they said nothing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 13:16:06 +00:00
- **`setup-host`'s apt calls can no longer hang** — a fresh cloud image has
`apt-daily`/`unattended-upgrades` holding the dpkg lock, and a plain
`apt-get install` waits on it silently and indefinitely. Now bounded
(`DPkg::Lock::Timeout=300`) and non-interactive, which matters because
`install.sh` runs it with nobody watching.
### Changed
Make setup-host privilege-aware; make the drill prove the new contract Review found two real problems, both confirmed by reproducing them. setup-host hardcoded 'sudo' for every privileged call, so install.sh's deliberate root branch — the one that proceeds when id -u is 0 even with no sudo installed — handed off to a script that died on 'sudo: command not found' before doing anything (exit 127, reproduced with env -i and a minimal PATH). The root path was nominal, not real. Privilege is now resolved once: nothing at UID 0, sudo otherwise, a clear error if neither is possible. Two things fell out of that. Root does not need incus-admin at all (UID 0 opens the socket regardless), so adding root to the group was a no-op that also missed the human — under 'sudo install.sh' that is SUDO_USER, who is now the one granted the group. And apt must not hang: install.sh runs setup-host with nobody watching, while a fresh cloud image holds the dpkg lock in apt-daily for its first minutes, so the calls are now bounded and non-interactive. The drill did not exercise any of this. It ran setup-host immediately after install.sh, so the stack existed by the drill's own hand and a run passed identically whether or not install.sh had done a thing — a fresh run converged three times while its 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 the clean or anything else mutates the host, which is the assertion that actually proves #64. setup-host then runs exactly once more, after the clean — that one is load-bearing, since the clean deliberately unsets dns.mode and something has to converge it back. DRILL_OWNS_SETUP=1 hands sequencing back to the drill. Pre-setup tripwires now read before install.sh, because install.sh is what triggers setup now; read afterwards they said nothing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 13:16:06 +00:00
- **`drill.sh` proves the new contract instead of masking it** — the drill ran
`setup-host` itself right after installing, so the stack existed by its own
hand and a run passed identically whether or not `install.sh` had 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
runs `setup-host` exactly once more — after the clean, which deliberately
unsets `dns.mode` and so has to be converged back. `DRILL_OWNS_SETUP=1`
2026-07-18 13:39:45 +00:00
hands sequencing back to the drill. Pre-setup tripwires now read _before_
Make setup-host privilege-aware; make the drill prove the new contract Review found two real problems, both confirmed by reproducing them. setup-host hardcoded 'sudo' for every privileged call, so install.sh's deliberate root branch — the one that proceeds when id -u is 0 even with no sudo installed — handed off to a script that died on 'sudo: command not found' before doing anything (exit 127, reproduced with env -i and a minimal PATH). The root path was nominal, not real. Privilege is now resolved once: nothing at UID 0, sudo otherwise, a clear error if neither is possible. Two things fell out of that. Root does not need incus-admin at all (UID 0 opens the socket regardless), so adding root to the group was a no-op that also missed the human — under 'sudo install.sh' that is SUDO_USER, who is now the one granted the group. And apt must not hang: install.sh runs setup-host with nobody watching, while a fresh cloud image holds the dpkg lock in apt-daily for its first minutes, so the calls are now bounded and non-interactive. The drill did not exercise any of this. It ran setup-host immediately after install.sh, so the stack existed by the drill's own hand and a run passed identically whether or not install.sh had done a thing — a fresh run converged three times while its 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 the clean or anything else mutates the host, which is the assertion that actually proves #64. setup-host then runs exactly once more, after the clean — that one is load-bearing, since the clean deliberately unsets dns.mode and something has to converge it back. DRILL_OWNS_SETUP=1 hands sequencing back to the drill. Pre-setup tripwires now read before install.sh, because install.sh is what triggers setup now; read afterwards they said nothing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 13:16:06 +00:00
`install.sh`, since that is what triggers setup now.
Redesign install flow: confirm, no-op if installed, opt-in host setup Implements the flow @danmt specified on #66. The installer now asks before it acts, and never overwrites itself. 1. "Install box?" — prompted before anything is downloaded. 2. If box is already installed: say so and stop. A re-run changes nothing, so curl|bash can no longer clobber a working tree or rebuild the host stack under live boxes. Upgrading is explicit — uninstall, then install fresh. 3. On a fresh host: download, install, link onto PATH. 4. "Set up this machine as a box host now?" — a separate decision, because the CLI and the host are not the same choice (you may host boxes elsewhere). This replaces the version-diff refusal from the previous round with the simpler rule Dan asked for: installed at all => no-op. It dissolves the same class of "the upgrade ate my boxes" errors without the installer having to reason about versions or enumerate boxes at all — you cannot lose boxes to an install that refuses to touch an existing one. Prompts read /dev/tty, because under curl|bash the script itself is stdin and a plain read would eat the installer's own remaining lines. With no terminal (CI, a pipe) BOX_YES=1 assumes yes and is required to proceed unattended; without it we refuse rather than invent consent. BOX_SKIP_SETUP_HOST=1 declines the second prompt. The drill uninstalls before installing (the no-op rule would otherwise refuse to re-lay the tree it re-proves each run) and sets BOX_YES=1 for the prompts; BOX_FORCE_UPGRADE is gone with the refusal it drove. Verified on a real host: cancel, fresh install, no-op re-run, and both prompts driven through a pty (y/n and y-then-n), plus the no-tty refusal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:10:34 +00:00
- **`install.sh` asks, sets up the host, and no-ops on re-run** (#64) — it now
2026-07-18 13:39:45 +00:00
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
Redesign install flow: confirm, no-op if installed, opt-in host setup Implements the flow @danmt specified on #66. The installer now asks before it acts, and never overwrites itself. 1. "Install box?" — prompted before anything is downloaded. 2. If box is already installed: say so and stop. A re-run changes nothing, so curl|bash can no longer clobber a working tree or rebuild the host stack under live boxes. Upgrading is explicit — uninstall, then install fresh. 3. On a fresh host: download, install, link onto PATH. 4. "Set up this machine as a box host now?" — a separate decision, because the CLI and the host are not the same choice (you may host boxes elsewhere). This replaces the version-diff refusal from the previous round with the simpler rule Dan asked for: installed at all => no-op. It dissolves the same class of "the upgrade ate my boxes" errors without the installer having to reason about versions or enumerate boxes at all — you cannot lose boxes to an install that refuses to touch an existing one. Prompts read /dev/tty, because under curl|bash the script itself is stdin and a plain read would eat the installer's own remaining lines. With no terminal (CI, a pipe) BOX_YES=1 assumes yes and is required to proceed unattended; without it we refuse rather than invent consent. BOX_SKIP_SETUP_HOST=1 declines the second prompt. The drill uninstalls before installing (the no-op rule would otherwise refuse to re-lay the tree it re-proves each run) and sets BOX_YES=1 for the prompts; BOX_FORCE_UPGRADE is gone with the refusal it drove. Verified on a real host: cancel, fresh install, no-op re-run, and both prompts driven through a pty (y/n and y-then-n), plus the no-tty refusal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:10:34 +00:00
isolation stack if you say yes (previously it only printed a warning and left
you a command, so the install reported success and `box new` died on a host
with no Incus). Prompts read `/dev/tty`, since under `curl | bash` the script
itself is stdin; `BOX_YES=1` answers yes unattended (required where there is
no terminal), `BOX_SKIP_SETUP_HOST=1` declines the host-setup step.
- **`install.sh` never 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 portable
`box export` so 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
- **`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>
2026-07-18 13:39:45 +00:00
--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 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).