2026-07-15 00:21:18 +00:00
|
|
|
# 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.
|
|
|
|
|
|
feat: merging a release-labeled PR is the release (#96)
The 0.7.0 ceremony exposed the gap: the release PR merged with four
approvals and nothing happened, correctly, because publishing hung off a
separate, manual, silent-when-forgotten tag push — the worst failure
shape, no error and no red X. The ship decision already lives in the
release PR, so the merge now IS the release.
release.yml grows a second door: pull_request closed on main, gated on
merged == true AND the hand-set release label (read from the event
payload — no extra permission). Four asserts, in order, each fail-loud
and creating nothing: VERSION at the merge commit is non--dev; VERSION
changed in this PR (merge vs first parent — the -dev interlock that
kills a mislabeled ordinary PR); the version's CHANGELOG.md section
extracts non-empty via the existing release-notes.sh; and no tag or
release exists yet. Then, in the same job, it creates the tag ref at
the merge commit via the API and publishes with gh release create
--verify-tag. Same-job on purpose: a GITHUB_TOKEN-created tag triggers
no workflows (GitHub's anti-recursion), so the tag door can never fire
off it and double-publish, and the no-existing assert covers a manual
tag racing the merge. The tag-push path stays step-for-step identical
as the documented manual fallback and backfill, gated to the push event
so a closed PR never runs it against a branch ref.
CONTRIBUTING.md's Releases section now reads "the maintainer's merge IS
the release", with the manual tag ritual kept as the fallback.
test/release.sh grep-pins the merged+labeled gate, all four asserts,
the same-job tag+publish, and that the tag-push trigger survives — in
the same daemon-free, fail-closed style.
Fixes #96
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:19:31 +00:00
|
|
|
## Unreleased
|
|
|
|
|
|
2026-07-25 17:16:00 +00:00
|
|
|
### Fixed
|
|
|
|
|
|
|
|
|
|
- `box exec` preserves newlines and command argv across its login-user boundary (#169)
|
|
|
|
|
|
2026-07-23 13:45:08 +00:00
|
|
|
### Changed
|
|
|
|
|
|
|
|
|
|
- Release and repository governance now use the shared ceremony pinned at `0.1.0` (heavy-duty/ceremony#14)
|
|
|
|
|
|
2026-07-22 15:10:35 +00:00
|
|
|
### Added
|
|
|
|
|
|
2026-08-18 07:04:33 +00:00
|
|
|
- The hardware drill can pin inputs and emit an uncoloured release-record draft with a shared run ID (#152)
|
2026-07-22 15:10:35 +00:00
|
|
|
- `kimi-box` template — the Moonshot Kimi CLI agent seed (#158; rig#109's tenant)
|
|
|
|
|
|
2026-07-21 14:49:51 +00:00
|
|
|
## 0.9.0 — 2026-07-21
|
|
|
|
|
|
feat: a minted box records how it was minted, and box info reads it back
cmd_new knew a great deal at the moment it launched and wrote three user.*
keys, dropping the rest: the box version that minted it, the base image (an
unpinned alias on a moving remote), the rig role, which rig repo and ref
converged it, the mint time, and whether a container was chosen or fallen
back into for want of /dev/kvm. There is no host-side per-box store — the
Incus instance config IS the database — so every one of those facts was gone
the moment the mint returned.
The same single write point now carries them as user.box.*, plus
user.box.schema=1 naming the stamp's shape. The alias's resolved fingerprint
is pinned in a second call after the launch, read from volatile.base_image,
best-effort by construction: a box that exists and boots must never be failed
over a provenance field.
A clone re-stamps rather than inheriting. 'incus copy' carries every user.*
key forward (audit B2), so an inherited stamp would not go stale, it would go
false. --from now re-stamps schema/version/created/origin/origin.from on the
copied instance before it starts, and leaves the lineage keys (template,
user, image, role, rig pin) alone — the clone's disk genuinely came from
them. origin.from records one hop.
cmd_info grows a provenance block, tolerating absence everywhere: boxes
minted before this stamp existed render as a box with blanks, and a schema
this box does not recognise is treated as newer than it, not as broken.
Closes #103.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 10:12:47 +00:00
|
|
|
### Added
|
|
|
|
|
|
2026-07-21 13:51:22 +00:00
|
|
|
- `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)
|
refactor: one drill record per version, in drills/
Drill records move from sections inside drill/RUNS.md to one file per
version: drills/<version>.md. drill/RUNS.md is untouched — it stays the
harness's own run log, traps table and lore. drills/ is release evidence
only, and the docs now say which is which.
The old guard parsed headings: em-dash field matching, an optional
' — DATE' tail, whole-version comparison so 0.9.0-rc1 could not satisfy
0.9.0, avoiding \x escapes because CI runs mawk not gawk, and a non-blank
body rule. Every one of those existed only because records shared one
file, and two separate defects were found in review because of that
complexity — the sed '/./,$!d' whitespace bypass, and heading-grammar
drift from the sibling repos.
One file per version makes almost all of it unrepresentable: 0.9.0.md and
0.9.0-rc1.md are simply different files, so whole-version matching is
free rather than a trap. The guard keeps only the rule that was never
about headings — a record of pure whitespace is not a record — and keeps
the failure message that names the version, the path, the unblock, and
the recorded-waiver escape hatch.
Plain drills/, not .drills/ — a dot-directory is invisible to globs
without dotglob, which is what caused #116 and #118.
Also corrects the release-drill framing: the three repos' drills are
INDEPENDENT and run in any order. What dissolves the box<->rig recursion
is that every drill pins the same fixed candidate refs, not sequencing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:26:47 +00:00
|
|
|
- CI refuses a release PR with no drill record at `drills/<version>.md`
|
feat: mark 'bootstrapped' after a rig hook box watched succeed
converged anything. This takes the checkpoint one step later: after the rig
bootstrap hook has run and box has WATCHED IT SUCCEED, the box is converged
and untouched. 'box restore <box> bootstrapped' keeps the tenant role and
undoes only what happened after it, which is the undo an operator reaches
for far more often and which otherwise costs a ~10-minute re-mint.
The policy is #128's, shared rather than copied: snapshot_pristine and the
new snapshot_bootstrapped are thin wrappers over one snapshot_mark, so the
never-fatal contract and the 'dir' skip exist in exactly one place. What
does not generalise is the prose — the two marks name different moments —
so each wrapper owns its own narration.
Deliberately conditional where 'pristine' is unconditional. 'pristine' marks
a MOMENT every fresh mint has; 'bootstrapped' marks an EVENT, and a blank
box has no such event, so it gets no mark rather than a byte-identical
duplicate of 'pristine' at twice the disk cost. A FAILED hook gets none
either: the failure message already sends the operator to 'box shell', which
is a run box does not watch, so box hands over 'box snapshot <box>
bootstrapped' instead of labelling a convergence it never saw.
The label is therefore documented as one-directional on every surface: its
presence means the hook converged untouched, its absence means nothing at
all. Same durability caveats restated everywhere it appears — it dies with
the box on 'box rm', and no filesystem rollback reaches off-box state
(heavy-duty/rig#62).
Closes #130
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:09:23 +00:00
|
|
|
|
refactor(templates): the tenant seeds carry rig's -box family suffix
rig is growing a second family of roles, and once a 'staging' role can mean
either a fleet machine or a box tenant, the bare name stops naming anything.
rig's answer is a suffix on the role (heavy-duty/rig#76): '-server' for fleet
machines, '-box' for box tenants. box's answer is that a template keeps being
named for the role it converges, so the tenant templates move with it:
claude -> claude-box codex -> codex-box
grok -> grok-box staging -> staging-box
Templates are the only surface that spells a rig role out loud
(BOX_BOOTSTRAP_ROLE, auto-run at mint since #81), so a directory that says one
thing and a role key that says another is a trap with a 15-minute fuse: it
mints clean and dies at convergence. Renamed with 'git mv' so the history of
each seed follows it.
'blank' keeps its name. It seeds no tenant role and sets no
BOX_BOOTSTRAP_ROLE, so it has nothing to agree with — renaming it would only
churn the default template's name for symmetry's sake.
Two namespaces move apart here and only one of them moved: the template name
and the role are now claude-box, while the seed USER stays 'claude' — that is
the user rig's role converges and the one 'box shell' lands in. test/cli.sh
pins the pair per tenant rather than each half alone, because a later rename
that moves one and forgets the other mints a box whose role dies looking for a
user nobody created. drill.sh keeps its bare box NAMES ('codex', 'grok' — what
the pre-flight banner announces and what teardown deletes) and only moves the
--template it passes.
The mint-time hints in cmd_new match both spellings of user.box.template, and
that is not an alias for the role: 'rig bootstrap claude' is gone and nothing
here softens the cut. The stamp is a fact about an INSTANCE, written at its own
mint time and carried forward by every clone; refusing the old spelling would
cut nothing over and only drop the login hint on boxes that predate today —
the same reason user.claudebox is honored everywhere else. migrate-host.sh
stamps re-homed legacy boxes claude-box, the name the template has today, so a
re-homed box looks like a fresh mint rather than a fossil.
Ordered AFTER rig's rename, and that is not a preference. The seeds install rig
from RIG_REPO/RIG_REF, defaulting to heavy-duty/rig@main and unpinned until
rig#32's releases, so these templates ask whatever main happens to be for
'rig bootstrap claude-box'. Against a pre-rename rig that role does not exist
and cmd_new refuses to call the box ready. Merged in the other order the window
closes instead of opening: rig's cut is hard, with no aliases, so the day it
lands every unmerged box seed naming a bare role is the broken one.
Closes #123
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 00:01:29 +00:00
|
|
|
### Changed
|
|
|
|
|
|
2026-07-21 13:51:22 +00:00
|
|
|
- `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)
|
2026-07-20 00:07:00 +00:00
|
|
|
|
fix: lint the release path — globstar does not descend into dot-directories
CI's shellcheck sweep set globstar and globbed `bin/* **/*.sh`. globstar
makes `**` descend into subdirectories, but a glob still does not MATCH a
dot-prefixed name, so `**/` never entered `.github/` and three scripts were
never linted: changelog-armed.sh (the #108/#110 guard that gates every PR),
release-notes.sh (which produces the published release body), and
labels-reconcile.sh (the label state machine). That is the entire release
path, while the step's own comment promised the opposite — that a script in
a new subdirectory is linted without anyone editing a list.
Latent, not broken: all three pass shellcheck as-is, so this is a no-op on
current code. What changes is that a regression in them would be caught.
dotglob alongside globstar closes it, measured rather than assumed: it adds
exactly those three and nothing else — a checkout's .git carries no *.sh,
its hooks shipping as *.sample, so `**/*.sh` does not wander into it.
The one-time fix is dotglob; what keeps the gap shut is the CLASS check, in
the same shape as the eof_guard_sweep of #112. The sweep now compares the
globbed set against `git ls-files '*.sh'` and fails naming any tracked
script it does not cover, so a future dot-directory or shopt subtlety
cannot silently lint a subset and pass.
eof_guard_sweep carried the identical blind spot — it rebuilds the same
glob — and is widened the same way. A no-op today: the three scripts set
errexit, so they are in that class by construction, but none of them reads.
Refs #116
2026-07-19 23:32:49 +00:00
|
|
|
### Fixed
|
|
|
|
|
|
2026-07-21 13:51:22 +00:00
|
|
|
- `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)
|
fix: the racing-reader sweep guards the class, and revoke-user captures the trust store
The sweep added in #107 matched `ufw status[^|]*\| *grep` — every historical
instance, and none of the equivalent spellings. `head -n1`, `sed -n '1p;q'`,
`awk '/x/ {print; exit}'` and `read` all close the pipe early, SIGPIPE the
writer, and yield the same 141 under pipefail. The pin guarded the instance
spelling of the very thing it existed to generalise.
Both halves of the matcher are alternations now.
Readers are deliberately not narrowed to the early-exit spellings: telling
`grep -q` from `grep -c` by regex is precision that rots, and all six
`ufw status` sites in the tree already capture first, so banning the pipe
outright costs nothing real.
Writers gain `incus config trust list`. host/revoke-user.sh:206 piped it into
`grep -q` as the --purge leftover assert, under `set -euo pipefail` — so
unlike drill/wipe.sh nothing but the writer's size was holding it, and left of
`&&` a 141 is set -e-exempt too. It would have read as "no leftover cert" on a
host that still trusts the revoked user's certificate and called the purge
complete: fail-open, on the path whose job is to prove access is gone. Now
captured into `trust_csv` and matched with `[[ ]]`, with a leading newline so
the first CSV row anchors like the `^` it replaces.
Writers are enumerated rather than generalised — ~150 legitimate `| grep`
sites exist under host/ and drill/, nearly all re-reading a captured string —
so the sweep claims only what it can check. The `id -nG | tr | grep -qx`
shapes in grant/revoke/setup are left alone: single tiny writes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 10:02:09 +00:00
|
|
|
|
2026-07-19 23:49:06 +00:00
|
|
|
## 0.8.0 — 2026-07-19
|
|
|
|
|
|
2026-07-19 18:03:46 +00:00
|
|
|
### Added
|
|
|
|
|
|
2026-07-21 13:51:22 +00:00
|
|
|
- Merging the release PR is the release, and the release re-arms main itself
|
|
|
|
|
(#96)
|
2026-07-19 18:03:46 +00:00
|
|
|
|
fix: the fresh-UFW test block no longer flakes on a missing log
It was never a test bug. box-firewall.sh decided the host's entire
firewall stance with `ufw status | grep -q "Status: active"`, and
"Status: active" is the FIRST line ufw prints: grep -q matches it and
exits immediately, closing the pipe while ufw is still writing the rest
of the table, so ufw dies of SIGPIPE. grep returned 0, but under the
script's own `set -o pipefail` the PIPELINE returns 141 (PIPESTATUS =
"141 0") — the if reads false, and a host with UFW plainly active takes
the nft-fallback branch and never builds the DNS carve-out.
`ufw status` is now read once into a variable and matched with [[ ]]:
no reader means no early exit means no race. The stale-rule scan reads
the same snapshot, so the branch decision and the converge loop cannot
disagree.
Separately, test/cli.sh now asserts that each shimmed run logged ufw
mutations at all, before the content greps, and dumps $WFW, the log and
the run's stderr when it did not — so the next occurrence reports its
own cause instead of four content-free grep failures.
Closes #102
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:44:54 +00:00
|
|
|
### Fixed
|
|
|
|
|
|
2026-07-21 13:51:22 +00:00
|
|
|
- The release ceremony re-arms `CHANGELOG.md`, and CI refuses to let main sit
|
|
|
|
|
disarmed (#108, heavy-duty/rig#67)
|
|
|
|
|
- Ctrl-D at a confirmation prompt aborts out loud instead of exiting in
|
|
|
|
|
silence (#111)
|
|
|
|
|
- `box restore` asks before it destroys, in the row's own words rather than
|
|
|
|
|
`rm`'s (#105)
|
|
|
|
|
- `box-firewall` could hand a UFW host the no-UFW firewall, ~2% of the time
|
|
|
|
|
(#102)
|
|
|
|
|
- A missing firewall log now diagnoses itself (#102)
|
|
|
|
|
- `box grant` provisions an `incus-admin` member instead of refusing them
|
|
|
|
|
(#99)
|
2026-07-19 18:03:46 +00:00
|
|
|
|
2026-07-19 13:44:32 +00:00
|
|
|
## 0.7.0 — 2026-07-19
|
Make host setup complete in one run, and let the installer run it
box setup-host stopped halfway when it had to add you to incus-admin: it
usermod'd, printed a NOTE telling you to re-login and re-run, and exited 0 —
a success-shaped no-op with no boxnet, no ACL, no box-net profile and no
firewall behind it. It now re-execs itself under 'sg incus-admin' and
finishes in that same invocation.
The membership check was also asking the wrong question. 'id -nG "$USER"'
names a user, so it reads the group database — which lists incus-admin the
instant usermod returns, while the shell's own credentials still lack it
(supplementary groups are fixed at login). A same-session re-run therefore
passed the check and died further down on a bare permission error from incus
that mentioned neither the group nor the re-login. Argless 'id -nG' asks the
process what it actually holds, which is what incus checks when it opens
/var/lib/incus/unix.socket.
With one run now sufficient, install.sh runs the setup itself instead of
printing a warning and leaving the user a command: the install reported
success and 'box new' then failed on a host with no Incus. setup-host is
idempotent, so doing this on every install is also how an upgraded host picks
up stack changes. BOX_SKIP_SETUP_HOST=1 opts out, and a failed setup leaves
the install standing and says what to re-run.
Fixes #63
Fixes #64
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 12:52:50 +00:00
|
|
|
|
2026-07-18 16:02:37 +00:00
|
|
|
### Added
|
|
|
|
|
|
2026-07-21 13:51:22 +00:00
|
|
|
- The installer defaults to the latest release, and releases publish
|
|
|
|
|
themselves (#83)
|
|
|
|
|
- `setup-host` auto-picks a free subnet — nested box-in-box with zero flags
|
|
|
|
|
(#80)
|
|
|
|
|
- `setup-host` refuses a claimed subnet, and `BOX_SUBNET` picks another (#80)
|
|
|
|
|
- `box doctor` knows the #80 signature: a gateway held as a local address, and
|
|
|
|
|
duplicate connected routes for the uplink subnet
|
|
|
|
|
- The `staging` template — a server-class, creds-free seed (#81)
|
|
|
|
|
- The `BOX_BOOTSTRAP_ROLE` template key, auto-run at mint (#81)
|
|
|
|
|
- The rig pin point: `RIG_REPO` / `RIG_REF` (#81)
|
|
|
|
|
- Server-posture template keys `BOX_REQUIRE_VM` and `BOX_AUTOSTART` (#81)
|
|
|
|
|
- The template test suite discovers `templates/*/` instead of hardcoding the
|
|
|
|
|
list (#81)
|
|
|
|
|
- `box export` / `box import` — a box's state that survives the box and the
|
|
|
|
|
host (#70)
|
|
|
|
|
- Versioned installs at `<root>/versions/<v>`, with `box versions` and
|
|
|
|
|
`box use` (#66)
|
|
|
|
|
- A real uninstall: `box uninstall [<version>] [--all] [--purge-host]`, ending
|
|
|
|
|
in an absence assert
|
|
|
|
|
- `BOX_INSTALL_SOURCE=<dir-or-tarball>` installs from a local tree, and CI's
|
|
|
|
|
rehearsal drills the uninstall to zero residue
|
|
|
|
|
- `test/cli.sh` drives real installs against throwaway roots and a fake incus
|
|
|
|
|
(154 checks)
|
2026-07-18 16:02:37 +00:00
|
|
|
|
docs: the thin-template story — box mints, rig converges (#81)
README: the templates section tells the split — thin seeds (user, tmux,
rig), the creds-free tenant role auto-run at mint, the operator-run
workload join, and the RIG_REPO/RIG_REF pin point with the honest unpinned
note (both directions of the rig<->box edge track main until rig#32/#83).
The #80 guard note is cross-referenced as living once, in rig's roles.
box-design.md: a layering section (why rig roles and not cloud-init:
convergent, re-runnable, effective-state-asserted vs a first-boot
one-shot), and the announce section now says who renders the context file.
CHANGELOG: staging template, BOX_BOOTSTRAP_ROLE + auto-run, the pin point
under Added; the tenant-content move under Changed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:03:55 +00:00
|
|
|
### Changed
|
|
|
|
|
|
2026-07-21 13:51:22 +00:00
|
|
|
- Thin templates — box mints a creds-free seed, rig's bootstrap roles converge
|
|
|
|
|
the tenant content (#81, heavy-duty/rig#31)
|
docs: the thin-template story — box mints, rig converges (#81)
README: the templates section tells the split — thin seeds (user, tmux,
rig), the creds-free tenant role auto-run at mint, the operator-run
workload join, and the RIG_REPO/RIG_REF pin point with the honest unpinned
note (both directions of the rig<->box edge track main until rig#32/#83).
The #80 guard note is cross-referenced as living once, in rig's roles.
box-design.md: a layering section (why rig roles and not cloud-init:
convergent, re-runnable, effective-state-asserted vs a first-boot
one-shot), and the announce section now says who renders the context file.
CHANGELOG: staging template, BOX_BOOTSTRAP_ROLE + auto-run, the pin point
under Added; the tenant-content move under Changed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:03:55 +00:00
|
|
|
|
2026-07-18 16:02:37 +00:00
|
|
|
### Fixed
|
|
|
|
|
|
2026-07-21 13:51:22 +00:00
|
|
|
- A wedged `incus launch` fails loudly, not forever: the launch phase is
|
|
|
|
|
narrated and time-boxed (#93)
|
|
|
|
|
- UFW's gateway carve-out converges with the bridge, and the doctor can see it
|
|
|
|
|
(#86)
|
|
|
|
|
- The boot-time gateway fallback is gone — an unaddressed bridge leaves the
|
|
|
|
|
persisted UFW rules alone (#86)
|
|
|
|
|
- `revoke --purge` re-checks the incus-user state, and stats it through
|
|
|
|
|
`$SUDO`
|
|
|
|
|
- A wedged `$BINDIR/box` no longer blocks installing
|
2026-07-18 16:02:37 +00:00
|
|
|
|
2026-07-18 13:39:45 +00:00
|
|
|
## 0.6.0 — 2026-07-18
|
|
|
|
|
|
2026-07-18 00:01:15 +00:00
|
|
|
### Added
|
|
|
|
|
|
2026-07-21 13:51:22 +00:00
|
|
|
- The restricted tier: `box grant` / `box revoke` give a user their own boxes
|
|
|
|
|
on the shared hardened `boxnet` (#74)
|
|
|
|
|
- CI runs the multi-user rehearsal on a real Incus
|
|
|
|
|
- Global / root install — one world-readable tree at `/opt/box` (#71)
|
|
|
|
|
- CI and a test suite: `.github/workflows/ci.yml` and `test/cli.sh`
|
2026-07-18 00:01:15 +00:00
|
|
|
|
Make host setup complete in one run, and let the installer run it
box setup-host stopped halfway when it had to add you to incus-admin: it
usermod'd, printed a NOTE telling you to re-login and re-run, and exited 0 —
a success-shaped no-op with no boxnet, no ACL, no box-net profile and no
firewall behind it. It now re-execs itself under 'sg incus-admin' and
finishes in that same invocation.
The membership check was also asking the wrong question. 'id -nG "$USER"'
names a user, so it reads the group database — which lists incus-admin the
instant usermod returns, while the shell's own credentials still lack it
(supplementary groups are fixed at login). A same-session re-run therefore
passed the check and died further down on a bare permission error from incus
that mentioned neither the group nor the re-login. Argless 'id -nG' asks the
process what it actually holds, which is what incus checks when it opens
/var/lib/incus/unix.socket.
With one run now sufficient, install.sh runs the setup itself instead of
printing a warning and leaving the user a command: the install reported
success and 'box new' then failed on a host with no Incus. setup-host is
idempotent, so doing this on every install is also how an upgraded host picks
up stack changes. BOX_SKIP_SETUP_HOST=1 opts out, and a failed setup leaves
the install standing and says what to re-run.
Fixes #63
Fixes #64
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 12:52:50 +00:00
|
|
|
### Fixed
|
|
|
|
|
|
2026-07-21 13:51:22 +00:00
|
|
|
- `box restore` never worked against Incus 6 — it dispatched `incus restore`,
|
|
|
|
|
which does not exist
|
|
|
|
|
- `box tmux` works on every template — tmux is in each template's package list
|
|
|
|
|
(#65)
|
|
|
|
|
- `box setup-host` finishes in one run, re-execing itself under
|
|
|
|
|
`sg incus-admin` (#63)
|
|
|
|
|
- `setup-host` works as root, with or without `sudo`
|
|
|
|
|
- `setup-host` grants `incus-admin` to the human, not to root
|
|
|
|
|
- `box-firewall.service` reports its state honestly, via `RemainAfterExit=yes`
|
|
|
|
|
- `setup-host`'s apt calls can no longer hang on the dpkg lock
|
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
|
|
|
|
Make host setup complete in one run, and let the installer run it
box setup-host stopped halfway when it had to add you to incus-admin: it
usermod'd, printed a NOTE telling you to re-login and re-run, and exited 0 —
a success-shaped no-op with no boxnet, no ACL, no box-net profile and no
firewall behind it. It now re-execs itself under 'sg incus-admin' and
finishes in that same invocation.
The membership check was also asking the wrong question. 'id -nG "$USER"'
names a user, so it reads the group database — which lists incus-admin the
instant usermod returns, while the shell's own credentials still lack it
(supplementary groups are fixed at login). A same-session re-run therefore
passed the check and died further down on a bare permission error from incus
that mentioned neither the group nor the re-login. Argless 'id -nG' asks the
process what it actually holds, which is what incus checks when it opens
/var/lib/incus/unix.socket.
With one run now sufficient, install.sh runs the setup itself instead of
printing a warning and leaving the user a command: the install reported
success and 'box new' then failed on a host with no Incus. setup-host is
idempotent, so doing this on every install is also how an upgraded host picks
up stack changes. BOX_SKIP_SETUP_HOST=1 opts out, and a failed setup leaves
the install standing and says what to re-run.
Fixes #63
Fixes #64
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 12:52:50 +00:00
|
|
|
### Changed
|
|
|
|
|
|
2026-07-21 13:51:22 +00:00
|
|
|
- `drill.sh` asserts the post-install stack instead of building it itself
|
|
|
|
|
- `install.sh` asks, sets up the host, and no-ops on re-run (#64)
|
|
|
|
|
- `install.sh` never overwrites an existing install
|
Make host setup complete in one run, and let the installer run it
box setup-host stopped halfway when it had to add you to incus-admin: it
usermod'd, printed a NOTE telling you to re-login and re-run, and exited 0 —
a success-shaped no-op with no boxnet, no ACL, no box-net profile and no
firewall behind it. It now re-execs itself under 'sg incus-admin' and
finishes in that same invocation.
The membership check was also asking the wrong question. 'id -nG "$USER"'
names a user, so it reads the group database — which lists incus-admin the
instant usermod returns, while the shell's own credentials still lack it
(supplementary groups are fixed at login). A same-session re-run therefore
passed the check and died further down on a bare permission error from incus
that mentioned neither the group nor the re-login. Argless 'id -nG' asks the
process what it actually holds, which is what incus checks when it opens
/var/lib/incus/unix.socket.
With one run now sufficient, install.sh runs the setup itself instead of
printing a warning and leaving the user a command: the install reported
success and 'box new' then failed on a host with no Incus. setup-host is
idempotent, so doing this on every install is also how an upgraded host picks
up stack changes. BOX_SKIP_SETUP_HOST=1 opts out, and a failed setup leaves
the install standing and says what to re-run.
Fixes #63
Fixes #64
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 12:52:50 +00:00
|
|
|
|
2026-07-15 00:21:18 +00:00
|
|
|
## 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
|
|
|
|
|
|
2026-07-21 13:51:22 +00:00
|
|
|
- `codex` and `grok` templates
|
|
|
|
|
- `box expose <box> <port> [<host-port>]` — a loopback-only door to a port
|
|
|
|
|
inside a box
|
|
|
|
|
- Inline resource overrides on `new`: `--cpu`, `--memory`, `--disk` (#57)
|
|
|
|
|
- Host lifecycle as verbs: `box setup-host`, `box teardown-host`,
|
|
|
|
|
`box migrate-host`
|
|
|
|
|
- The `.box/` recipe convention, renamed from `.claudebox/` (both spellings
|
|
|
|
|
read)
|
2026-07-15 00:21:18 +00:00
|
|
|
|
|
|
|
|
### Fixed
|
|
|
|
|
|
2026-07-21 13:51:22 +00:00
|
|
|
- VM mints no longer hang at GRUB — boxes launch with
|
|
|
|
|
`security.secureboot=false`
|
|
|
|
|
- `box expose` actually delivers packets
|
|
|
|
|
- Firewall rules converge on upgrade instead of pinning a host to the release
|
|
|
|
|
that first ran there
|
|
|
|
|
- Failed mints tell you why
|
|
|
|
|
- `grok` installs the binary it actually ships
|
2026-07-15 00:21:18 +00:00
|
|
|
|
|
|
|
|
### Changed
|
|
|
|
|
|
2026-07-21 13:51:22 +00:00
|
|
|
- Debrand complete — env vars, install dir, docs, template descriptions and
|
|
|
|
|
the README all say `box`; the install URL is `heavy-duty/box`
|
|
|
|
|
- The drill grew from 47 to 84 checks
|