Repository navigation
Releases
0.10.0
Added
box checkup <box>reports one guest's seed, disk and memory headroom,/tmp, VM or container swap posture, and OOM history without changing the guest. It distinguishes a clean kernel journal from one it could not read (#258).- Release doors now publish a verified offline box installer and checksum sidecar from the committed release tree. (#251).
BOX_INSTALLED_FROMoverrides the provenance an install records, for a caller whoseBOX_INSTALL_SOURCEis a temp directory it then deletes: a dead path names nothing, and<version>/INSTALLED_FROMexists to name the source (#250).- Add a product-neutral builder for integrity-checked, offline self-extracting installer artifacts. (#249).
box down --force—incus stop --forcefor a guest that has stopped answering the graceful request. Whatever it had not flushed to disk is lost; the box, its disk and its snapshots survive, andbox startbrings it back.box down all --forceforces the fleet (#236).operator, a cross-cutting issue-owner label, andreadyturns owner-neutral — so an issue whose evidence only a human on real hardware can produce readsready+operatorrather than a falseblocked(#219).- A return path for doctrine that blocks you: raise a discussion in ceremony quoting the rule at this repo's pin, and cite it wherever the local workaround lives.
.ceremony/README.mdnames and links that flow (#219). test/cli.shasserts the two rosters are the same set, naming who each file
is missing, so the panel cannot drift silently in either direction again
(#198).- A box now has a stable identity:
user.box.id, a kernel v4 UUID stamped
at mint.renamemoves the name and leaves the id where it was, so a
record that kept the id can still find the box (#181). - The id is re-minted wherever box mints — a fresh mint, a
--fromclone,
an import — so no two boxes share one. A snapshot or a restore is the same
box and leaves it alone (#181). box infoshows the id underNAME;box listdoes not, and nothing is
invented at read time, so a box minted before the key reads as blank
(#181).BOX_STORAGE_SOURCEplaces the host storage pool on a disk of its own (/dev/sdb) or a mounted filesystem (/data/bulk/incus), instead of the loop-backed image Incus builds under/var/lib/incus— where every box's root device is charged against/(#180).box doctorreports the storage pool's driver and source, the device a pool built on a block device was made from, and the free space under a directory source, so "what is filling my root disk" is answered without an Incus lesson (#180).box restart <box>— one Incus restart, not a stop followed by a start
(#179).- The lifecycle verbs take
allwhere a box name goes:box restart all,
box start all,box down all. It acts on exactly the boxesbox list
prints for you, so an admin never reaches a restricted user's boxes. Each
box is reported and one failure does not stop the rest (#179). - A fleet verb treats a box already in the state you asked for as a success,
not a failure:down allsays so for a box that was already down, and
restart allstarts a stopped one. The exit status stays a real signal
(#179). - A fleet verb tells "no boxes" apart from "the daemon did not answer": an
unreachable daemon fails with the diagnosis instead of reporting an empty
fleet and exiting 0 (#179). allis now a reserved box name, refused at every door that would leave a
box carrying it:box new --name all,box import --name allandbox rename <box> all(#179).- Every ordinary box provisions a 4GiB swapfile. Without swap every memory spike was a
hard OOM-kill with no grace period; swap-in is observable, where a kill is a
process that was there and then was not (#178). - A named
box root <box>path for an Incus-authorized root login shell that does not depend on guest sudo (#176). box new --fromhonours--cpu,--memoryand--disk. They ride the
copy itself — incus sizes the clone's volume as it creates it — so there is
no resize, no restart, and no step inside the guest (#171).- Every box now says what resources it came up with, clone or fresh mint, read
back off incus. That sizing was invisible until you ranbox infoand read
limits.*out of a config dump (#171). changelog-assembled,runner-isolatedandsha-pinnedguard steps, and arefs-guard.ymlcaller that re-checks a PR body on edit (#168).ci-rerun.yml, which services thererun-owedlabel by starting the one rerun no fork-PR author can start themselves (#168).- Issue events now wake the labels automation, so a queue-state change no longer waits for the next cron tick (#168).
- The emitted drill record carries the isolation audit answers in their own
## Audit answerssection. They used to be printed for a human to paste into
an issue that has since closed, in a repo since renamed (#154). - Per-phase probe counts in the drill summary, and a
DRILL_EXPECTfloor a short run fails against (#153). - Legitimate drill skips print a
SKIPline and lower the expected count by exactly their probes (#153). drill/drill.sh --emit-record <path>writes the release record itself — host, candidate refs and their SHAs, the numbers, the wall clock (#152).- A shared drill run ID:
--run-id/DRILL_RUN_ID, defaulting todrill-<version>-<date>-01and announced as soon as the install lands (#152). - The drill refuses
--emit-recordat a path that already holds a record, rather than overwriting the prose that makes it evidence (#152).
Changed
- Lead installation guidance with the checksummed, scp-able release artifact and spell out which channels need GitHub. (#252).
- README now treats
0.xupgrade compatibility per release, records that0.10.0does not import0.9.xartifacts across the placement-profile rename, and preserves export/import for host rebuilds and machine moves (#240). - Force-stopping a box no longer needs the
box incusescape hatch, so it keeps theuser.box=1boundary check the hatch bypassed:--forceskips the politeness, never the check (#236). box downitself is unchanged — still graceful, still no prompt, and still never escalating to a force stop on a timer. The operator asks for the power button or does not get it (#236).- The placement contract is now the
box-profileprofile. It wasbox-net, one hyphen from theboxnetbridge its NIC attaches to, which read as a typo in rawincusoutput.boxnetis untouched: same name, subnet, keys and ACL binding (#229). setup-hostconverges the rename on every run, indefaultand in each granteduser-<uid>project. Attached boxes keep their placement across it, so a running fleet needs no restart and no reassignment (#229).box doctorreports a survivingbox-netas drift, names every project still carrying one, and points atbox setup-host— the lever that removes it (#229).teardown-hostand the drill's wipe remove both names for one release, so a host torn down after an interrupted upgrade is left with nothing behind (#229).- Where a box is still placed on the old name,
setup-hostconverges by renaming it ontobox-profileinstead of leaving both behind; where boxes sit on each name it reports the residue and does not report the host ready (#229). box revoke --purgeremoves both profile names, so a purge on a host upgraded without asetup-hostrun no longer fails deleting the project and names probes that are all empty (#229).- A daemon that will not list projects no longer reads as a host with no granted users:
setup-hostsays which projects it could not check and withholdsHost ready, andbox doctorreports the project list as unread rather than certifying a contract it never looked at (#229). - A listing that emits a row and then fails counts as unread too — a partial answer is not the host's inventory, so neither tool reports on the projects it did not reach (#229).
- Known boundary: an artifact exported by
0.9.xno longer imports onto a host whose profile has been converged, or onto a fresh one. Incus resolves an artifact's profile list before it creates the instance and refuses a name it cannot find, sobox importcannot re-home it (#229). - The refusal is incus's own and it names the profile it could not load. Re-export the box from a source host that has been converged, or have an admin create a profile under the old name before importing (#229).
- A profile created under the old name to admit such an artifact is then reported as drift by
box doctoruntil the nextsetup-hostclears it. That report is the workaround's own residue rather than a fault (#229). drill/drill.shdrills the checkout it runs from: it installs through the tree's owninstall.shwithBOX_INSTALL_SOURCE, sogit clone && bash drill/drill.shfinally drills what you cloned (#225).--repoand--ref, and theBOX_REPO/BOX_REFpass-through, are gone. There is no flag that points the drill at a tree other than the one it runs from (#225).- An emitted record's repo, branch and SHA are measured from the working tree instead of echoing the refs that were requested (#225).
- Those fields are measured before the install and carried to the end of the run, so committing or stashing mid-drill no longer changes what the record says was drilled (#225).
- The record's repository field reduces any GitHub origin to
owner/repoand never carries a credential out of the URL; a checkout detached exactly on a tag records the tag (#225). - The drill refuses if the checkout changes between the moment it is measured and the moment
install.shfinishes copying it, so the record cannot name a tree other than the one that was installed (#225). - The drill compares every file
install.shcopied against the checkout, by content, and refuses when they differ — so a tree edited while it was being copied cannot be drilled under a r...
0.9.1
Fixed
box execpreserves newlines and command argv across its login-user boundary (#169)
Changed
- Release and repository governance now use the shared ceremony pinned at
0.1.0(heavy-duty/ceremony#14)
Added
kimi-boxtemplate — the Moonshot Kimi CLI agent seed (#158; rig#109's tenant)
0.9.0
Added
box importstamps the trip, leaving the artifact's own mint stamp intact
(#131)- A minted box records how it was minted, and
box inforeads it back (#103) - A clone re-stamps its own provenance instead of inheriting its source's
(#103) box infogrew a provenance block, blank on boxes that predate the stamp
(#103)- Every fresh mint marks a
pristinesnapshot, before rig converges anything
(#104, heavy-duty/rig#62) - A mint that converges a tenant role marks a
bootstrappedsnapshot (#130) - CI refuses a release PR with no drill record at
drills/<version>.md
Changed
state:needs-humanis set at handoff, not by the cron (#141)- PR labels split into two axes:
state:*(whose ball) andblocker:*(what
is in the way);state:needs-rebaseis retired - BREAKING: the tenant templates carry rig's family suffix —
claude→
claude-box,codex→codex-box,grok→grok-box,staging→
staging-box(#123, heavy-duty/rig#76) - Changelog entries are one line each, and the whole file now follows the rule
(#147)
Fixed
test/release.shis green on the release ceremony's own treechangelog-monotonic.shno longer lets a duplicate heading through when it
cannot see the base (#143)- An unreadable check rollup no longer reads as "nothing is failing"
state:needs-humanno longer appears on PRs a human cannot merge (#136)- CI's shellcheck sweep now lints
.github/scripts/*.sh(#116) - A PR can no longer delete or duplicate a shipped changelog section and stay
green (#122) - An upgrade over a pre-0.7.0 flat
/opt/boxno longer skips host setup (#115) - Host setup runs the version it just installed, not whatever
currentpoints
at (#115) - The pre-0.7.0 migration says what it left behind, and how to keep or reap it
(#117) teardown-host.shrefuses a terminal-less run instead of aborting mute
(#113)drill/wipe.shno longer carries #102's SIGPIPE shape, and the pin sweeps
the class (#107)- The racing-reader sweep guards the class, not one spelling, and names
incus config trust listas a second writer (#124)
0.8.0
Added
- Merging the release PR IS the release — and the release re-arms main
itself (#96) — the 0.7.0 ceremony ended in an absence: the release PR
merged with four approvals and nothing happened, correctly, because
publishing hung off a separate, manual, silent-when-forgotten tag push —
a failure shape with no error and no red X. The ship decision already
lives in the release PR (the one PR whose whole diff is "the version
leaves-dev"), sorelease.ymlnow fires on pushes to main
(fork-sourced ceremony PRs get a read-only token onpull_request
events), reading the transition from the push itself:event.beforeto
the pushed head. A decide step answers four states — release-flow work
merged under thereleaselabel (-devendstates, the post-release
window) no-ops green with a NOTICE; the two genuinely ambiguous bare
states refuse loudly; a true transition then requires a merged,
release-labeled PR behind the commit (read via the API — the label is
the operator's declared intent) before anything is created. Then, in the
same job, it tags the merge commit via the API, publishes — and bumps
main toX.Y.(Z+1)-devitself, direct push with a loud open-a-PR
fallback, so no follow-up bump PR exists on the paved road. Same-job on
purpose: aGITHUB_TOKEN-created tag triggers no workflows, which is
also what makes double-publish impossible. The tag-push path stays
unchanged as the documented manual fallback and backfill (it shipped
0.7.0 itself).test/release.shgrep-pins the gate, every decide
verdict, the singleon.pushkey, and the same-job tag+publish+re-arm
in the same daemon-free, fail-closed style.
Fixed
- The release ceremony re-arms
CHANGELOG.md, and CI refuses to let
mainsit disarmed (#108) — the ceremony stamps## Unreleasedinto
## X.Y.Z — DATEby hand, and nothing put the heading back, somain
sat with no## Unreleasedfrom the release until the next PR that
happened to re-create one. A PR authored before the release wrote its
entry under## Unreleased; with that heading gone, git lands the entry
under whatever now occupies the position — the section that just
shipped — and it merges cleanly. No conflict, no error, no red X:
the one signal an author would trust is absent exactly when the outcome
is wrong, and the changelog credits a released version with a change it
does not contain until a human reads the file. Confirmed in the sibling
repo (heavy-duty/rig#66); box has not drifted yet, and the reason is
luck rather than design — 0.6.0's ceremony (77599ab) added its heading
without removing## Unreleased, so main was never disarmed, while
0.7.0 did disarm it and left a window that nothing happened to cross.
Two halves land together. The ceremony step inCONTRIBUTING.mdis now
explicitly two edits: stamp, then put an empty## Unreleasedback
above the section just stamped — it belongs there and not in
release.yml, which only ever touchesVERSION. And
.github/scripts/changelog-armed.shenforces it in CI, keyed on
VERSIONbecause the two states are genuinely different: a-devtree
must carry## Unreleasedon top, a bare-VERSIONtree (the ceremony
PR, and the merge that publishes it) may carry either that or its own
stamped section. The keying is the whole design and not an
over-complication — box previously had no top-section guard at all,
and the obvious one, an unconditional## Unreleasedrequirement, is
false by construction on the ceremony PR's own tree, which is why rig#44
and heavy-duty/cast#108 both had to revert it. So a forgotten re-arm
does not block the release; it turnsmainred on the very next push,
the automatic-devbump the release itself makes. Leaving the bare
branch's top heading unconstrained is what keeps both ceremony shapes
legal, and a review round on the sibling fix (heavy-duty/cast#114) found
the gap that asymmetry leaves: a half-ceremony tree —VERSION
bumped,## Unreleasedstill populated on top, and the section for that
version never stamped — makes the wrong-number test false on its first
clause, short-circuits, and passes. Nothing then refuses until
release.ymlextracts the notes, which is after the merge, onmain,
with the release already half-shipped. So the bare branch now also
requires that the section it is about to publish exists and is non-empty,
and it asserts that by runningrelease-notes.sh— the very script
release.ymlruns — so the guard and the publisher cannot drift apart
over what a section is. The message is its own: a missing stamp is not a
misnumbered one, and an operator sent to correct a version number that is
already right will not find the real problem. Matches
heavy-duty/rig#67, so the three repos agree. - Ctrl-D at a confirmation prompt aborts out loud, instead of exiting
in silence (#111) —confirm()anduninstall_confirm()both took
the operator's answer with a bareread -r reply. Every answer a
human can type routes through thecasebelow it and ends at a
returnor atdie "aborted."— every answer except EOF. Ctrl-D
makesreadreturn non-zero,set -euo pipefailends the run on that
line, and thecaseis never reached: box exits 1 having printed
nothing at all after the question it just asked. It fails closed,
which is why this is a small fix and not an incident — nothing is
destroyed, the abort is real. The damage is that the tool goes mute at
the one moment it had the operator's full attention, and someone who
Ctrl-Ds out ofbox rm workcannot tell from the output whether the
box is still there. The cure is one token in each function,
read -r reply || die "aborted.", the same one heavy-duty/rig#43
applied to rig's credential prompts so the two repos read alike. The
bug predates everything it touches —rmhas carried a confirm gate
for as long as the verb has existed — but #105 took the number of
verbs reaching that line from one to two, and both are irreversible,
which is the argument for closing it now rather than the next time
someone notices. The three answers a human can actually give (y,
n, and Ctrl-D) are now driven for real on a pty via util-linux
script: they were structurally untested before, because[ -t 0 ]
sends a terminal-less suite to the refusal branch and every existing
check stopped there — which is exactly how this survived four
releases. Review caught that the first pass fixed the bug where it was
reported and stopped there, while the same defect sat at two more
destructive gates in this repo:host/revoke-user.sh:50, the prompt
guardingbox revoke --purge— the one whose own text says "this
cannot be undone" — andhost/teardown-host.sh:31, guarding a full
host teardown. Both run underset -euo pipefail, both died mute on
EOF with theirabortedline never reached; both now carry the guard
in their own script's wording. The threedrill/prompts are
deliberately left alone — they run underset -uonly, so EOF falls
through to the*)arm and already aborts out loud — and
install.sh:65was already guarded. What keeps the class closed is a
repo-wide sweep intest/cli.sh: every statement-initialreadfed
from stdin, in any file that turns on errexit, must carry a||
guard, withwhile readloops and<<<herestrings excluded because
neither is a prompt. The sweep flags all four sites when their guards
are removed and nothing else across the tree's fifteen shell files —
the absence of exactly this check is why thehost/pair was missed
in the first place. box restoreasks before it destroys — and the confirmation prompt is
now the row's, not rm's (#105) —restoreandrmboth irreversibly
discard user state, and only one of them asked. The table gaverestore
the preconditionsbox,arg2: the instance is ours, a snapshot name is
present, go. Sobox restore work stale-labelsilently threw away
everything done in the box since that snapshot, with no prompt, no
--force, and no way to take it back — a warning in--helpis not a
gate. It has been that way since the verb shipped, and it is about to
become routine rather than rare (heavy-duty/rig#62's pristine snapshot),
which is the wrong time to still be relying on the operator typing the
right label. The reason it stayed ungated is worth recording, because it
is the actual bug:confirmwas already a precondition token, but the
dispatch line hardcoded the words —confirm "delete $inst and all its snapshots"— so the one-token fix would have gated restore behind a
prompt offering to DELETE the box the operator was trying to rescue. A
gate that names the wrong act is worse than no gate; it is how people
learn to answerywithout reading. So the prompt moved into the table
as a seventh field, each row saying what it is about to do in its own
words, andrestorenow asks to "roll<box>back to snapshot
<label>and discard everything in the box since it was taken" — naming
the label, because picking the wrong one is the whole risk.rm's
wording is unchanged and pinned verbatim by a test, since rewording the
one verb that already worked would be a regression shipped as a
refactor. A row markedconfirmwith no words is now a hard internal
error rather than a blank question.--forceand the no-TTY refusal come
free —confirm()already had both. The one automated caller had to
consent explicitly:drill/multiuser.shdrives restore unattended on real
Incus and now passes--force, which is the rehearsal proving the gate
rather than working around it — the CI run of this very PR failed there
first, which is the shape a gate is supposed to have. Coverage went from two
argument-validation checks that never reache...
0.7.0
Added
-
The installer defaults to the latest release, and releases publish
themselves (#83) —curl | bashused to hand out whatevermainwas at
that second: the 0.6.0 release was a bookmark, not a package, and two
operators "on 0.6.0" could be running different trees.install.shnow
resolves the latest release tag by following GitHub'sreleases/latest
redirect (one HEAD request — no API, no token, no rate-limit pain) and
downloads that tag's tarball; a failed resolution refuses loudly, naming
BOX_REFas the way out — it never hangs and never silently falls back to
main. A setBOX_REFis tried as a tag first, then as a branch, so one
knob yields three channels: default = latest release,BOX_REF=0.6.0=
pinned,BOX_REF=main= dev. A newrelease.yml(on a bareX.Y.Ztag
push — the0.6.0tag set the no-vprecedent) asserts the tag names the
tree's ownVERSION(a mismatch fails loudly and creates nothing) and
publishes the GitHub release with that version'sCHANGELOG.mdsection as
the body (.github/scripts/release-notes.sh— the curated prose, not the
generated PR list; no assets, the source tarball for the tag IS the
package). Andmain'sVERSIONnow carries-devbetween releases
(this PR:0.6.1-dev): the versioned layout names install trees after
VERSION, so amaininstall without the bump would land in
versions/0.6.0and impersonate the released tree.test/release.sh
drives all of it offline — the extraction against fixtures and the real
changelog, the resolution and every channel against a shim curl. -
setup-hostauto-picks a free subnet — nested box-in-box with zero
flags (#80, completing its fix #1: "refuse … or automatically select a
non-colliding subnet"). A barebox setup-hostnow decides the subnet
itself, in four deliberate cases: an explicitBOX_SUBNETis honored or
refused, never silently overridden (scripted hosts keep exact semantics);
an existingboxnetbridge is converged on as-is — the bridge IS the pin —
turning the old bare-re-run agree-gate refusal into plain convergence
(unless a foreigner also claims the bridge's subnet: that is #80's
poisoned state, and converging would rebuild on it, so it still refuses and
names the bridge move); a free10.88.0.0/24stays the default; and a
claimed default — the nested case: a drill or rehearsal running inside a
box, whose own uplink owns 10.88 — scans10.89.0.0/24…10.127.0.0/24
in order, takes the first free candidate, announces the pick and the
claimant loudly, and only refuses when every candidate is claimed. The
decision happens before any mutation, and everything downstream (the
bridge,BOX_GW, the ACL's gateway carve-out, the firewall, the doctor's
expectations) derives from it. -
setup-hostrefuses a claimed subnet, andBOX_SUBNETpicks another
(#80) — run inside a box,setup-hostused to build a nestedboxneton
the exact subnet and gateway of the guest's own uplink: the guest then held
its gateway's address as a local address, carried duplicate connected
routes for its uplink subnet, and suffered intermittent, self-recovering
egress blackouts that looked like flaky internet (measured live: ~24–36 s
outages, roughly hourly, with the host clean throughout).setup-hostnow
scans the target subnet before any mutation — the default route's
gateway inside it, or any non-boxnetinterface holding an address in it —
and refuses, naming the way out. A priorboxnetowning the subnet is the
legitimate converge path and does not trip it.BOX_SUBNET=<a.b.c.0/24>
(validated, alongside the existingBOX_DNS) moves the whole stack: the
bridge address, the ACL's gateway carve-out (now converged via
network acl edit, so a bridge moved off a colliding subnet no longer
strands box DNS behind a stale/32), the firewall (box-firewallreads
the gateway off the live bridge), and every drill/migrate probe that used
to hardcode10.88. -
box doctorknows the #80 signature — a default gateway held as a
LOCAL address, and duplicate connected routes for the uplink subnet, judged
fromip route/ip addron the machine doctor runs on (both tiers, before
any daemon check — the nested daemon answering could be the impostor) and
probed inside every box it examines. The existing "egress broken but DNS
fine" split now names itself as #80's fingerprint (the impostor dnsmasq on
a captured gateway keeps resolving while IP egress dies), and the admin ACL
section verifies the gateway carve-out matchesboxnet's actual gateway.
The agent-context guard for the templates (suggested fix 4) lands in
heavy-duty/rig#31's bootstrap roles per the thin-templates split (#81). -
The
stagingtemplate (#81, the re-cut of #69's layering) — a
server-class, creds-free seed: Debian 13, userops, tmux, rig,
BOX_REQUIRE_VM=1(the VM is its trust boundary),BOX_AUTOSTART=1(a
server returns from a host reboot without an operator), and
BOX_BOOTSTRAP_ROLE="staging"— the server posture (docker, sshd
hardening) converges viarig bootstrap stagingafter mint. The tailnet
workload join holds a pre-auth key and therefore stays operator-run
(box shell→sudo rig bootstrap workload), printed as a next step —
box never sees the key. -
BOX_BOOTSTRAP_ROLEtemplate key + mint-time auto-run (#81) — a
template names the creds-free rig tenant role box runs inside the guest
after cloud-init settles (incus exec … rig bootstrap <role>); the value
is a role name by allowlist (anything shell-shaped dies at parse time, on
the host). A failed role leaves the box up and names the re-run — the roles
are convergent by contract (rig#31).blanknames no role and auto-runs
nothing. -
The rig pin point:
RIG_REPO/RIG_REF(#81) — the tenant seeds
preinstall rig, inverting the rig→box install edge (rig#28), and the new
edge gets the same honest treatment rig#29 gave box's unpinned install:
@RIG_REPO@/@RIG_REF@tokens in the seed resolve at mint from the
environment (defaultheavy-duty/rig@main— unpinned, tracking main,
until a release flow exists, rig#32/#83). The pin covers both the installer
fetched and the tree it installs, so a rig branch under review is testable
end to end; values are allowlist-validated before touching the YAML. -
Server-posture template keys (#81, carved from #69) — two optional
box.envallowlist keys.BOX_REQUIRE_VM=1refuses both the silent
container fallback (no/dev/kvm, exit 1) and an explicit--container
(exit 2): such a template's trust boundary is the VM.BOX_AUTOSTART=1
stampsboot.autostart=trueat launch, per-instance likelimits.*, so
the box returns from a host reboot without an operator; clones inherit it
viaincus copy. Still no key for a network or asecurity.*flag, on
purpose. -
Dynamic template test suite (#81, carved from #69) —
test/cli.sh
discoverstemplates/*/instead of hardcoding the list, so a new template
cannot ship unseen. Per template:box.envis driven through the real,
extractedload_template(unknown keys and missingBOX_IMAGE/BOX_USER
fail, fixtures proving both dies);user-data.yamlexists, declares
#cloud-config, parses as YAML, and installs tmux (#65). Grep guards pin
thecmd_newhalf: theREQUIRE_VMrefusal orders afterpick_mode, and
boot.autostartis stamped only under theT_AUTOSTARTguard. -
box export/box import(#70) — a box's state that survives the box
and the host, unblocking #66's humane upgrade flow (down, export, rm,
upgrade, re-import).box export <box> [<file>]wrapsincus exportinto
one portable backup tarball (default<box>-<UTC stamp>.tar.gz), snapshots
included by default (--instance-onlyopts out); the box must be stopped
first (box down) so the artifact is a settled disk, not a moving one. The
file is shouted about, not scrubbed — it carries the box's whole disk
(agent logins, git credentials, SSH keys), and scrubbing a disk image is a
promise tarball surgery cannot keep, so box says what is inside instead,
every time.box import <file> [--name <box>]mints the box back and
re-stamps what is the current host's truth, not the artifact's: the
user.box=1boundary tag (legacyuser.claudebox=1honored), the
box-netplacement (re-assigned if the artifact's differs — the
migrate-host move), and a fresh machine identity: the NIC's MAC (imports
restorevolatile.*verbatim, and a re-import beside its sibling collided
at start with "MAC address already defined on another NIC" — measured
live;incus copyregenerates it,incus importdoes not) plus
reset_identity(the clone trust boundary: no DHCP collision with the box
it was exported from).
Import refuses any name an existing instance holds — theresolve_box
boundary, seen from the other side. Works on both tiers:box grantnow
also convergesrestricted.backups allow(incus-user blocks backups by
default exactly like snapshots, and an export is a backup
create+download — measured against incus 6.0'spermissions.go); re-run
box grant <user>after upgrading, as documented. CI'srehearsaljob now
proves the round-trip on a live Incus: mint → write a file → snapshot →
down → export →rm→ import → the file and the snapshot survived, the
agent answers, the tag is present, and a colliding re-import is refused. -
Versioned installs (#66's stance, made livable) — install.sh now lands
each version side by side at<root>/versions/<v>(its ownVERSION+
INSTALLED_FROM), with acurrentsymlink tracking the default and
$BINDIR/boxriding the chain, the way plenty of CLIs manage theirs. New
verbs:box versions(lists installs, marks the current default and the
runni...
0.6.0
What's Changed
- Import claudebox: creds-free, trust-less Claude Code VMs by @dan-claude-bot in #1
- fix: install from the public canonical repo by @dan-claude-bot in #2
- feat: drop the claude- instance-name prefix by @dan-claude-bot in #4
- fix: preinstall the GitHub CLI in boxes by @dan-claude-bot in #5
- feat: add
claudebox list, with snapshot labels by @dan-claude-bot in #9 - fix: standard help, honest flags, and an
rmthat asks first by @dan-claude-bot in #10 - feat: make the command surface a table, add
renameand an escape hatch by @dan-claude-bot in #13 - test: add an end-to-end drill against a real Incus by @dan-claude-bot in #18
- fix: first-run drill defects — pipefail false FAILs, exec-pty hang, DHCP race by @dan-claude-bot in #19
- fix: run-2 cascade (per-name cleanup), and claude on exec's PATH by @dan-claude-bot in #20
- fix(drill): read eth0 from inside the box; settle the flaky dns.mode verdict by @dan-claude-bot in #21
- fix(drill): no in-box probe can hang the run again by @dan-claude-bot in #22
- fix(drill): delete every listener — refused-vs-dropped already answers it by @dan-claude-bot in #23
- fix(drill): stop going silent through host setup, and keep the run log in the repo by @dan-claude-bot in #24
- fix(drill): clean the host before setup-host, and bound it by @dan-claude-bot in #25
- fix: a clone must not inherit its source's identity (two boxes, one IP) by @dan-claude-bot in #27
- fix(drill): the NIC inside a VM is enp5s0, not eth0 — read the address by subnet by @dan-claude-bot in #28
- fix: the clone identity reset could never reboot, so it never took effect by @dan-claude-bot in #30
- fix(drill): refuse to judge #16 on a broken baseline by @dan-claude-bot in #31
- fix(drill): stop poisoning the host, and add a doctor to prove it by @dan-claude-bot in #32
- fix: a failed cold mint must say why; doctor gains DNS diagnosis and --pin-dns by @dan-claude-bot in #34
- fix(doctor): check that something is actually serving the network by @dan-claude-bot in #35
- fix: boxes could reach each other — isolate them at the bridge by @dan-claude-bot in #37
- fix: isolate boxes with the bridge's port-isolation flag by @dan-claude-bot in #38
- fix(doctor): read the isolation off the bridge, not off the config by @dan-claude-bot in #39
- docs(drill): the audit is complete — boxes are not isolated from each other by @dan-claude-bot in #36
- fix(doctor): four checks that lied, and none of them about a real fault by @dan-claude-bot in #40
- fix(drill): read curl's message — the exit code cannot tell you what happened by @dan-claude-bot in #41
- fix(doctor): pin the probes' stdin — an interactive exec cannot be timed out by @dan-claude-bot in #42
- fix(doctor): the gateway does not answer ping — by design, so stop asking by @dan-claude-bot in #43
- fix(drill): stop demanding an empty host — assert OUR boxes, not NO boxes by @dan-claude-bot in #44
- fix: pin claudenet's resolver — a box's DNS is not a function of the host's VPN by @dan-claude-bot in #45
- docs(readme): the isolation contract as it ships — and measured, not claimed by @dan-claude-bot in #47
- docs(drill): record runs 11–13 — the contract measured at zero, from a bare host by @dan-claude-bot in #48
- feat: 'claudebox tmux [session]' — a shell that survives you by @dan-claude-bot in #49
- feat: 'claudebox doctor' — the host-health checks as a first-class verb by @dan-claude-bot in #50
- fix: create the storage pool deliberately — btrfs, because cloning is the whole point by @dan-claude-bot in #51
- feat!: claudebox becomes box — the Claude box is one template among several by @dan-claude-bot in #52
- release: box 0.5.0 — codex+grok templates, migrate-host, box expose by @dan-claude-bot in #56
- feat(new): inline resource overrides — --cpu, --memory, --disk by @dan-claude-bot in #59
- docs: make the box narrative agent-agnostic, not Claude-specific by @dan-claude-bot in #58
- docs(readme): how to run the drill against the latest version by @dan-claude-bot in #60
- fix(install): the rename broke every install — stop guessing the tarball's top dir by @dan-claude-bot in #61
- fix(doctor): a fresh host is not a dirty one by @dan-claude-bot in #62
- feat: global install (#71), tmux (#65), CI + tests; folds #66 by @dan-claude-bot in #73
- Make host setup complete in one run, and let the installer run it by @claude-bot-andresmgsl in #66
- Restricted incus tier: per-user projects converged onto hardened boxnet (#74) by @dan-claude-bot in #75
New Contributors
- @dan-claude-bot made their first contribution in #1
- @claude-bot-andresmgsl made their first contribution in #66
Full Changelog: https://github.com/heavy-duty/box/commits/0.6.0