release: box 0.5.0 — codex+grok templates, migrate-host, box expose #56
No reviewers
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:cli
scope:drill
scope:host
scope:installer
scope:templates
scope:tiers
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/box#56
Loading…
Reference in a new issue
No description provided.
Delete branch "integration/0.5.0"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The 0.5.0 integration branch: three features, each already reviewed as its own PR into
integration/0.5.0on the fork (#2 templates · #3 migrate · #4 expose), merged and version-bumped here. Do not merge tomainuntil the drill is green on this branch (commands below).What's in it
templates/codex/(npm i -g @openai/codex, Node 22) andtemplates/grok/(officialx.ai/cli/install.sh), each mirroring the claude template's shape and creds-free contract. Install commands verified upstream at authoring time.host/migrate-host.sh(#53) — the pre-0.4.0 → box transition:--box/--all-boxesre-home legacy boxes onto the new stack preserving authed state (no re-login),--retire-legacyremoves the old stack once empty. Order is load-bearing (tag first, profile last, verify the effect).box expose <box> <port>(#55) — a deliberate, loopback-only door to a box's dev server. Per-port, reversible, visible inbox info; the network-facing "no inbound path" contract stays true. ⚠️ The highest-risk change — the Incus proxy-on-VM + ACL mechanism is validated by the drill, not by reasoning; expect it may need one iteration if phase E is red.VERSION → 0.5.0. Legacy
user.claudebox=1boxes keep working under every verb, as since 0.4.0.Verify before merging — drill this branch
The drill self-installs from the ref you pass, so point it at this branch. On a throwaway host you can format:
Expect the ~59 base checks plus the new phases:
<cli> --versionanswers viabox exec(the #15 PATH bug is the risk).user.claudebox=1box), then proves: retire refuses while a legacy box exists, re-home flips the tag + reassignsbox-net+ lands a 10.88 address + resolves, retire then succeeds and leaves nothing.--removeshuts the door.Budget ~40 min (four extra cold mints: codex, grok, the legacy box, plus the existing claude/blank/clone/peer). Heartbeat dots mark every mint;
tail -f /tmp/mint-<box>.logwatches one live.If phase E (expose) is the only red, that's the known-uncertain mechanism — comment the phase-E output and it gets one iteration on the expose commit before this merges. Templates and migration I'd expect green first try.
After green
Merge to
main(autodeploys nothing — it's a CLI repo), tagv0.5.0. Existing hosts upgrade with the installer one-liner;box exposeand the new templates are immediately available, andmigrate-host.shis there for anyone still on a pre-0.4.0 stack.🤖 Generated with Claude Code
Drill status — boot-path issues found (not in the 0.5.0 code)
Drilling this branch on the staging host surfaced a chain of VM boot failures. Every one reproduced with a bare
incus launch(no box, no template), so none of them are the templates/migrate/expose features — they're the VM boot path, and the fixes belong to the release only because it's the thing being drilled. Recording them here so the next run (and the next host) has the map.Symptom
Every mint hung at the GRUB/firmware menu and timed out at
wait_agent(5 min). The VM console log (once sanitized — see below) told the real story in two layers:Wall 1 — Secure Boot rejects the kernel:
Incus defaults VMs to
security.secureboot=true; the re-downloadedimages:debian/13/cloudships a shim this host's OVMF keys don't trust. Fixed in this branch —security.secureboot=falseon box VMs (cmd_new+ the drill's legacy box). Safe for a disposable box: the VM boundary is the trust boundary, not the guest's bootloader; there's no persistence surface to protect.Wall 2 — kernel won't decompress (behind Wall 1):
Seen only after Secure Boot was off (confirmed
security.secureboot: "false"). Two candidate causes, both environmental: too little RAM for EFI-stub decompression (the bare test used the ~1 GiB default profile; real boxes get 3 GiB), or a corrupted image from the--purge-storagere-download.Two things that also came out of it (both fixed in this branch)
wait_agent's console dump was raw — it scrambled the operator's terminal, doubly so throughtail -f. Now sanitized to a file (/tmp/box-console-<box>.log), full escape sequences stripped, and it names a GRUB hang in plain English./tmp/mint-<box>.log, so a slow mint is never mistaken for a wedge again.Verification commands (run on the drill host)
Confirm the installed box actually carries the fix (a stale/CDN-lagged install was one red herring — the drill installs from the ref you pass, and
install.shdefaultsCLAUDEBOX_REPOto upstream, so the fork needs it explicit):Isolate boot from box entirely — a bare VM reproduces (or clears) the walls with no box involved:
If it still fails at 3 GiB with "Failed to decompress kernel", re-pull the image (rules out a corrupt
--purge-storagedownload):Read a stuck box's console safely at any time (raw
--show-logscrambles the terminal):Re-drill
Avoid
--purge-storageunless re-validating the pool bootstrap — it forces the image re-download that started this. The btrfs pool + resolver pin were already validated in run 14.Open: confirming Wall 2 is RAM-vs-corrupt-image. If it turns out the current
images:debian/13/cloudbuild is broken in-environment, the durable fix is pinningBOX_IMAGE(known-good build, orimages:debian/12/cloud) per template — a one-line-per-template change that would land here before this merges.⚠️ Corrected commands — the debrand moved the paths and env vars
The verification block in my earlier comment is out of date: 0.5.0 finished the rename, so
~/.local/share/claudebox→~/.local/share/box,CLAUDEBOX_*→BOX_*, and the host scripts are verbs now. Use these instead.Confirm the installed box carries the fixes
install.shdefaultsBOX_REPOto upstream, so the fork needs it explicit. (The curl URL still saysclaude-hdb/claudebox— that's the repo name, which hasn't been renamed yet.)The installer also sweeps the old
~/.local/share/claudeboxtree on upgrade, so after this only~/.local/share/boxexists.Isolate boot from box (bare VM — no box, no template)
If it still fails at 3 GiB with "Failed to decompress kernel", re-pull the image (rules out a corrupt
--purge-storagedownload):Host setup / teardown are verbs now (not script paths)
Re-drill (unchanged flags; the drill self-installs from the ref)
Avoid
--purge-storageunless re-validating the pool bootstrap — it forces the image re-download that started the boot saga.Read the safe console idiom (raw
--show-logscrambles the terminal):Still open: whether "Failed to decompress kernel" is low RAM (bare test used the ~1 GiB default profile; real boxes get 3 GiB) or a corrupt image download. The bare-at-3-GiB test above decides it.
🛑 Correction: the drill command was incomplete — and the gap was dangerous
The drill snippet I posted above was missing the mandatory first step: update the local checkout. That omission is not cosmetic, because of the 0.5.0 env-var rename:
drill.sh(pre-0.5.0) passesCLAUDEBOX_REPO/CLAUDEBOX_REFtoinstall.sh.install.shreadsBOX_REPO/BOX_REFand ignores the old names.heavy-duty/claudebox@main— while the run reports "Installing box (claude-hdb/claudebox@integration/0.5.0)" and proceeds to drill the wrong tree to a green summary.That's the same silent-wrong-install class that already burned an hour via a lagged CDN tarball. So:
The accurate sequence
And the drill now refuses to drill a lie
install.shrecords what it installed ($DEST/INSTALLED_FROM), and the drill asserts it matches the requestedrepo@refbefore touching the host. If they disagree it fails immediately and names the cause:A green run can no longer mean "drilled the wrong code."
(Everything else in the corrected-commands comment above stands:
BOX_*env vars,~/.local/share/boxpaths,box setup-host/box doctorverbs.)Boot Wall 2 — it is NOT low RAM. Next diagnostic (copy-pasteable)
Confirmed on the drill host: with
security.secureboot=falseANDlimits.memory=3GiB, a bare VM (no box, no template) still dies at:Secure Boot (Wall 1) is genuinely fixed — we're past
bad shim signature. And the install is confirmed good (box-install: done (claude-hdb/claudebox@integration/0.5.0), andgrep secureboot ~/.local/share/box/bin/boxprints the flag). So the kernel blob itself is bad, which leaves two suspects:--purge-storageforced a re-download ofimages:debian/13/cloud.--purge-storage.Run this — checks storage, re-pulls the image, then isolates image-vs-host with a Debian 12 control
What each outcome means
BOOTED after re-pull--purge-storagedownload was corruptdebian/12 BOOTSimages:debian/13/cloudbuild is broken in-environmentBOX_IMAGEper template (debian/12, or a known-good 13 build) — one line each, lands in this branchdebian/12 ALSO failsWatch the
incus storage info default/df -houtput regardless — if that pool is near-full or tiny, we have the answer before the tests even finish.Reminder that none of this is the 0.5.0 feature code: every failure reproduces with a plain
incus launch, no box involved. The release content (templates, migrate-host, expose) still has not been exercised, because nothing boots yet.E-phase failure: found, reproduced live, fixed (
edf8309)I stood up the full stack (setup-host + box-firewall + a box on boxnet) on a scratch Incus host and reproduced the exact drill failure. tcpdump on
boxnetduring the failing curl:So the route_localnet + masquerade plumbing from
32bb203works — the packet that dies is the reply, and it dies at our own hand: theinet box inputchain drops ALL boxnet input except DNS/DHCP, stateless. The box's SYN/ACK arrives at the host as input on boxnet and is eaten by the very firewall that guards A7. UFW hosts never had this hole (ufw's before.rules accept RELATED,ESTABLISHED); the nft fallback — what the drill host runs — did.Fix:
ct state established,related acceptahead of the drop, plus the chains are now rebuilt (add+flush+re-add) on every run instead of skip-if-present, so upgraded rules actually land on hosts that already have the table. Boxes still cannot initiate toward the host — a box-originated SYN is a NEW flow, which is what the drop is for.Verified end-to-end on the repro stack:
curl 127.0.0.1:18091→ HTTP 200 in 2 ms; box→host initiation still times out; DNS carve-out intact; a non-exposed port is still dropped;--removeshuts the door; re-expose answers again.Copy-pastable probe (fast — container box, no VM mint; ~1 min)
From the repo checkout on the drill host, on this branch:
Or just re-run the drill — its preamble re-runs setup-host, which restarts box-firewall with the new chain.
🤖 Generated with Claude Code
The probe ran from
~, not a repo checkout —host/box-firewall.shand./bin/boxwere not found, so the new rules were never applied and the door was never opened (the nft output shows the old chain, noct staterule; the "good" negative results are just the old behavior). It also left aprobecontainer running.Here's a self-contained version — clones the branch itself, fails fast, cleans up after (including the leftover from the last attempt):
🤖 Generated with Claude Code
Previous run was from a stale checkout — the device added (it had the static-pin fix) but the
nftoutput shows noct staterule, sobox-firewall.shwas the old version and the box's SYN/ACK was still eaten by the stateless input drop. The fix is inedf8309; pull first. From~/claudebox:🤖 Generated with Claude Code
git pullwas a no-op — local branchrel-0.5.0has no upstream, so that run used the same stale tree (the nft output still shows noct staterule). Fetch the PR branch explicitly instead; this works regardless of remote setup. From~/claudebox:If the
git logline doesn't showedf8309, stop there — nothing after it is testing the fix.🤖 Generated with Claude Code
That run is the fix working — look at how the failure changed shape. Before:
curlhung 6 s (the box's reply was dropped by the stateless input chain). Now: instantConnection refusedin 0.3 ms — that's a TCP RST from inside the box, DNAT'd out and un-NAT'd back through the door. Two-way traffic works.The RST is there because nothing was listening: stock
images:debian/13has nopython3, and the backgrounded server line swallows the error (my earlier snippet's miss — my repro container had python3 installed beforehand). Same probe with the listener actually started and verified:The full drill doesn't have this problem — its E phase uses node inside a claude box, which is always present. Expect it to pass as-is.
🤖 Generated with Claude Code
Found it — and it's the probe, not the door. Reproduced on my stack: when
incus execis run from an interactive terminal it allocates a PTY, and Incus kills the exec session's processes on return — including thesetsid'd server (verified: withincus exec -tthe listener dies instantly; with stdin redirected it survives). My snippet was missing</dev/nullon the server-start line. The drill's own E phase always had it (… sh -c 'setsid node … &' </dev/null), which is why the drill is immune — its comment even says so.Meanwhile your previous run already showed the door itself working: the old 6 s timeout became an instant RST relayed from inside the box. This run just needs a listener that survives:
(The trailing
</dev/nullon each exec is the load-bearing part.)🤖 Generated with Claude Code