Skip to content

Releases

0.10.0

Choose a tag to compare

@github-actions github-actions released this 02 Sep 16:54
bb39772

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_FROM overrides the provenance an install records, for a caller whose BOX_INSTALL_SOURCE is a temp directory it then deletes: a dead path names nothing, and <version>/INSTALLED_FROM exists to name the source (#250).
  • Add a product-neutral builder for integrity-checked, offline self-extracting installer artifacts. (#249).
  • box down --force — incus stop --force for 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, and box start brings it back. box down all --force forces the fleet (#236).
  • operator, a cross-cutting issue-owner label, and ready turns owner-neutral — so an issue whose evidence only a human on real hardware can produce reads ready + operator rather than a false blocked (#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.md names and links that flow (#219).
  • test/cli.sh asserts 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. rename moves 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 --from clone,
    an import — so no two boxes share one. A snapshot or a restore is the same
    box and leaves it alone (#181).
  • box info shows the id under NAME; box list does not, and nothing is
    invented at read time, so a box minted before the key reads as blank
    (#181).
  • BOX_STORAGE_SOURCE places 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 doctor reports 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 all where a box name goes: box restart all,
    box start all, box down all. It acts on exactly the boxes box 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 all says so for a box that was already down, and
    restart all starts 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).
  • all is now a reserved box name, refused at every door that would leave a
    box carrying it: box new --name all, box import --name all and box 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 --from honours --cpu, --memory and --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 ran box info and read
    limits.* out of a config dump (#171).
  • changelog-assembled, runner-isolated and sha-pinned guard steps, and a refs-guard.yml caller that re-checks a PR body on edit (#168).
  • ci-rerun.yml, which services the rerun-owed label 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 answers section. 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_EXPECT floor a short run fails against (#153).
  • Legitimate drill skips print a SKIP line 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 to drill-<version>-<date>-01 and announced as soon as the install lands (#152).
  • The drill refuses --emit-record at 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.x upgrade compatibility per release, records that 0.10.0 does not import 0.9.x artifacts 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 incus escape hatch, so it keeps the user.box=1 boundary check the hatch bypassed: --force skips the politeness, never the check (#236).
  • box down itself 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-profile profile. It was box-net, one hyphen from the boxnet bridge its NIC attaches to, which read as a typo in raw incus output. boxnet is untouched: same name, subnet, keys and ACL binding (#229).
  • setup-host converges the rename on every run, in default and in each granted user-<uid> project. Attached boxes keep their placement across it, so a running fleet needs no restart and no reassignment (#229).
  • box doctor reports a surviving box-net as drift, names every project still carrying one, and points at box setup-host — the lever that removes it (#229).
  • teardown-host and 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-host converges by renaming it onto box-profile instead of leaving both behind; where boxes sit on each name it reports the residue and does not report the host ready (#229).
  • box revoke --purge removes both profile names, so a purge on a host upgraded without a setup-host run 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-host says which projects it could not check and withholds Host ready, and box doctor reports 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.x no 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, so box import cannot 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 doctor until the next setup-host clears it. That report is the workaround's own residue rather than a fault (#229).
  • drill/drill.sh drills the checkout it runs from: it installs through the tree's own install.sh with BOX_INSTALL_SOURCE, so git clone && bash drill/drill.sh finally drills what you cloned (#225).
  • --repo and --ref, and the BOX_REPO/BOX_REF pass-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/repo and 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.sh finishes copying it, so the record cannot name a tree other than the one that was installed (#225).
  • The drill compares every file install.sh copied 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...
Read more

0.9.1

Choose a tag to compare

@github-actions github-actions released this 04 Aug 14:45
770d2c0

Fixed

  • box exec preserves newlines and command argv across its login-user boundary (#169)

Changed

Added

  • kimi-box template — the Moonshot Kimi CLI agent seed (#158; rig#109's tenant)

0.9.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 18:06
86af660

Added

  • box import stamps the trip, leaving the artifact's own mint stamp intact
    (#131)
  • A minted box records how it was minted, and box info reads it back (#103)
  • A clone re-stamps its own provenance instead of inheriting its source's
    (#103)
  • box info grew a provenance block, blank on boxes that predate the stamp
    (#103)
  • Every fresh mint marks a pristine snapshot, before rig converges anything
    (#104, heavy-duty/rig#62)
  • A mint that converges a tenant role marks a bootstrapped snapshot (#130)
  • CI refuses a release PR with no drill record at drills/<version>.md

Changed

  • state:needs-human is set at handoff, not by the cron (#141)
  • PR labels split into two axes: state:* (whose ball) and blocker:* (what
    is in the way); state:needs-rebase is 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.sh is green on the release ceremony's own tree
  • changelog-monotonic.sh no 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-human no 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/box no longer skips host setup (#115)
  • Host setup runs the version it just installed, not whatever current points
    at (#115)
  • The pre-0.7.0 migration says what it left behind, and how to keep or reap it
    (#117)
  • teardown-host.sh refuses a terminal-less run instead of aborting mute
    (#113)
  • drill/wipe.sh no 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 list as a second writer (#124)

0.8.0

Choose a tag to compare

@github-actions github-actions released this 19 Jul 23:04
c4f62e5

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"), so release.yml now fires on pushes to main
    (fork-sourced ceremony PRs get a read-only token on pull_request
    events), reading the transition from the push itself: event.before to
    the pushed head. A decide step answers four states — release-flow work
    merged under the release label (-dev endstates, 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 to X.Y.(Z+1)-dev itself, direct push with a loud open-a-PR
    fallback, so no follow-up bump PR exists on the paved road. Same-job on
    purpose: a GITHUB_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.sh grep-pins the gate, every decide
    verdict, the single on.push key, 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
    main sit disarmed
    (#108) — the ceremony stamps ## Unreleased into
    ## X.Y.Z — DATE by hand, and nothing put the heading back, so main
    sat with no ## Unreleased from 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 in CONTRIBUTING.md is now
    explicitly two edits: stamp, then put an empty ## Unreleased back
    above the section just stamped — it belongs there and not in
    release.yml, which only ever touches VERSION. And
    .github/scripts/changelog-armed.sh enforces it in CI, keyed on
    VERSION because the two states are genuinely different: a -dev tree
    must carry ## Unreleased on top, a bare-VERSION tree (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 ## Unreleased requirement, 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 turns main red on the very next push,
    the automatic -dev bump 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, ## Unreleased still 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.yml extracts the notes, which is after the merge, on main,
    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 running release-notes.sh — the very script
    release.yml runs — 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() and uninstall_confirm() both took
    the operator's answer with a bare read -r reply. Every answer a
    human can type routes through the case below it and ends at a
    return or at die "aborted." — every answer except EOF. Ctrl-D
    makes read return non-zero, set -euo pipefail ends the run on that
    line, and the case is 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 of box rm work cannot 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 — rm has 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
    guarding box revoke --purge — the one whose own text says "this
    cannot be undone" — and host/teardown-host.sh:31, guarding a full
    host teardown. Both run under set -euo pipefail, both died mute on
    EOF with their aborted line never reached; both now carry the guard
    in their own script's wording. The three drill/ prompts are
    deliberately left alone — they run under set -u only, so EOF falls
    through to the *) arm and already aborts out loud — and
    install.sh:65 was already guarded. What keeps the class closed is a
    repo-wide sweep in test/cli.sh: every statement-initial read fed
    from stdin, in any file that turns on errexit, must carry a ||
    guard, with while read loops 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 the host/ pair was missed
    in the first place.
  • box restore asks before it destroys — and the confirmation prompt is
    now the row's, not rm's
    (#105) — restore and rm both irreversibly
    discard user state, and only one of them asked. The table gave restore
    the preconditions box,arg2: the instance is ours, a snapshot name is
    present, go. So box restore work stale-label silently 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 --help is 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: confirm was 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 answer y without 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, and restore now 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 marked confirm with no words is now a hard internal
    error rather than a blank question. --force and the no-TTY refusal come
    free — confirm() already had both. The one automated caller had to
    consent explicitly: drill/multiuser.sh drives 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...
Read more

0.7.0

Choose a tag to compare

@github-actions github-actions released this 19 Jul 15:47
fb7fc84

Added

  • The installer defaults to the latest release, and releases publish
    themselves
    (#83) — curl | bash used to hand out whatever main was 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.sh now
    resolves the latest release tag by following GitHub's releases/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_REF as the way out — it never hangs and never silently falls back to
    main. A set BOX_REF is 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 new release.yml (on a bare X.Y.Z tag
    push — the 0.6.0 tag set the no-v precedent) asserts the tag names the
    tree's own VERSION (a mismatch fails loudly and creates nothing) and
    publishes the GitHub release with that version's CHANGELOG.md section 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). And main's VERSION now carries -dev between releases
    (this PR: 0.6.1-dev): the versioned layout names install trees after
    VERSION, so a main install without the bump would land in
    versions/0.6.0 and 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-host auto-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 bare box setup-host now decides the subnet
    itself, in four deliberate cases: an explicit BOX_SUBNET is honored or
    refused, never silently overridden (scripted hosts keep exact semantics);
    an existing boxnet bridge 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 free 10.88.0.0/24 stays the default; and a
    claimed default — the nested case: a drill or rehearsal running inside a
    box, whose own uplink owns 10.88 — scans 10.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-host refuses a claimed subnet, and BOX_SUBNET picks another
    (#80) — run inside a box, setup-host used to build a nested boxnet on
    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-host now
    scans the target subnet before any mutation — the default route's
    gateway inside it, or any non-boxnet interface holding an address in it —
    and refuses, naming the way out. A prior boxnet owning the subnet is the
    legitimate converge path and does not trip it. BOX_SUBNET=<a.b.c.0/24>
    (validated, alongside the existing BOX_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-firewall reads
    the gateway off the live bridge), and every drill/migrate probe that used
    to hardcode 10.88.

  • box doctor knows the #80 signature — a default gateway held as a
    LOCAL address, and duplicate connected routes for the uplink subnet, judged
    from ip route/ip addr on 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 matches boxnet'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 staging template (#81, the re-cut of #69's layering) — a
    server-class, creds-free seed: Debian 13, user ops, 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 via rig bootstrap staging after 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_ROLE template 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). blank names 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 (default heavy-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.env allowlist keys. BOX_REQUIRE_VM=1 refuses 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
    stamps boot.autostart=true at launch, per-instance like limits.*, so
    the box returns from a host reboot without an operator; clones inherit it
    via incus copy. Still no key for a network or a security.* flag, on
    purpose.

  • Dynamic template test suite (#81, carved from #69) — test/cli.sh
    discovers templates/*/ instead of hardcoding the list, so a new template
    cannot ship unseen. Per template: box.env is driven through the real,
    extracted load_template (unknown keys and missing BOX_IMAGE/BOX_USER
    fail, fixtures proving both dies); user-data.yaml exists, declares
    #cloud-config, parses as YAML, and installs tmux (#65). Grep guards pin
    the cmd_new half: the REQUIRE_VM refusal orders after pick_mode, and
    boot.autostart is stamped only under the T_AUTOSTART guard.

  • 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>] wraps incus export into
    one portable backup tarball (default <box>-<UTC stamp>.tar.gz), snapshots
    included by default (--instance-only opts 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=1 boundary tag (legacy user.claudebox=1 honored), the
    box-net placement (re-assigned if the artifact's differs — the
    migrate-host move), and a fresh machine identity: the NIC's MAC (imports
    restore volatile.* verbatim, and a re-import beside its sibling collided
    at start with "MAC address already defined on another NIC" — measured
    live; incus copy regenerates it, incus import does 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 — the resolve_box
    boundary, seen from the other side. Works on both tiers: box grant now
    also converges restricted.backups allow (incus-user blocks backups by
    default exactly like snapshots, and an export is a backup
    create+download — measured against incus 6.0's permissions.go); re-run
    box grant <user> after upgrading, as documented. CI's rehearsal job 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 own VERSION +
    INSTALLED_FROM), with a current symlink tracking the default and
    $BINDIR/box riding the chain, the way plenty of CLIs manage theirs. New
    verbs: box versions (lists installs, marks the current default and the
    runni...

Read more

0.6.0

Choose a tag to compare

@danmt danmt released this 18 Jul 13:40

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 rm that asks first by @dan-claude-bot in #10
  • feat: make the command surface a table, add rename and 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

Full Changelog: https://github.com/heavy-duty/box/commits/0.6.0