-
0.2.0 Stable
released this
2026-07-19 23:04:21 +00:00 | 193 commits to main since this releaseAdded
users applygrants the box tier, not just its socket (#49) — role
boxresolved to exactly one action,usermod -aG incus. That is the
socket; it is step 1 of the fivebox grantperforms, so every box-role
user still needed an admin to runbox grant <user>by hand before their
firstbox newwould do anything but refuse ("your project has no box-net
profile"), and until that admin arrived they held anincusmembership
with no converged project — incus-user would lazily hand them a stock
unhardened NAT bridge, which is worse than no grant at all. Onhost=yes
apply now callsbox grantper box-role user, afteruseradd(grant
refuses an unknown account) and with the group ADD deferred to grant, so a
grant that fails partway can take the socket back with it. Failures split
the way thehost=guard beside them already splits: a missingboxCLI
onhost=yesdies (a broken VM host), a per-user grant failure warns and
continues (one box-role user must not stop apply for the fleet).host=no
and marker-less boxes keep their existing skip-with-warning. An
incus-adminmember is warned, not fatal —box grantrefuses them today,
which heavy-duty/box#99 fixes box-side with no rig change needed.
Changed
-
BREAKING:
rig bootstraptakes the users file, and requires it (#51) —
bootstrap already knew everything else about what a box is (class, host,
join, hostname) and wrote/etc/rig/roleto say so; the users file was the
last piece of that answer it did not take, so bring-up was two commands and
the second was the forgettable one.--users <path>now runs theusers applyconvergence as bootstrap's final phase — after the traits, after
the verified tailnet join, after the role marker (apply reads that
marker), and after thehost=yesbox install (so box-role users find the
incusgroup box's ownsetup-hostbuilt). One command, and the box has
its people on it. The file is still passed per invocation and never
persisted;--users -is refused, because bootstrap's stdin belongs to
the pre-auth key prompt.Migration: every existing
rig bootstrapinvocation must add--users <path>or--no-users. Omitting both is now a usage error (exit 2)
naming both flags, and passing both is a usage error too. Scripted
bring-up that already ranrig users applyas a separate step can either
fold it in (--users ./users, and drop the separate call) or keep the old
shape verbatim by adding--no-users. Required onclass=serveras well
asclass=human: a server nobody logs into routinely is exactly where
shared-root access rots, and per-human accounts keep attribution intact
for the times someone does go in — so the complete path is the default
path, and skipping it is deliberate rather than an omission that looks
identical to forgetting. The box TENANT roles (claude|codex|grok| staging) take neither flag: a guest is minted non-interactively by box,
never joins the tailnet, and has no SSH door of its own — entry isbox shell, gated by the host'sincusgrants.A bad users file is caught up front now (the same parser apply uses,
beforeapt, the hostname change, and any spent pre-auth key), and on
host=yeswithRIG_SKIP_BOX_INSTALL=1a box-role user with noincus
group refuses immediately instead of a hundred lines later — the one case
where the outcome is already certain. rig still never installs Incus and
never callsbox setup-hoston its own account; every other way that step
can fail lands inusers apply's existing refusal, unchanged.
Fixed
-
A release no longer disarms the changelog under the PRs still in
flight (#67) — the ceremony stamps## Unreleasedto
## X.Y.Z — YYYY-MM-DDand stops. Every PR authored before that merge
wrote its entry under## Unreleased; with the heading gone, git files
the entry under whatever now occupies the position — the release that
already shipped. There is no conflict, because the stamped heading and
the incoming entry never overlap textually, so the one signal an author
relies on ("git told me to look") is absent exactly when the outcome is
wrong. It happened here: #60's #58 entry landed inside## 0.1.0at
67386b4and was repaired two minutes later by0ff520c; #54 would
have filed a BREAKING entry the same way. The published release body
is never affected —release.ymlextracts it from the tree at the tag,
before the late merges land — so the only file that drifts is the one
only maintainers read, which is why it survived a whole release batch
unnoticed. Fixed in both halves the failure has. The ceremony now
re-arms: it adds a fresh empty## Unreleasedabove the section it
just stamped, so a late merge has somewhere correct to land with no
author action. That belongs to the ceremony step in
CONTRIBUTING.md, not torelease.yml— no workflow
has ever touched the heading; the stamping was always by hand, and the
-devre-arm the workflow does perform was only ever aboutVERSION.
Andtest/release.shnow keys its guard toVERSIONrather than
demanding a literal heading: a stamped top section is legal exactly when
VERSIONis bare, and the moment it carries-dev— main, where
feature PRs merge — the top section must be## Unreleased. That
distinguishes the two states the old check collapsed into one, so it
catches a disarmed main without re-breaking the ceremony's own tree
the way the pre-#44 guard did. The rule is proven against seven
constructedVERSION+CHANGELOG.mdpairs, including a re-armed
ceremony whose top section is legitimately empty — the state the old
non-empty assert would have rejected. box and cast carry the same flow
and the same exposure (heavy-duty/box#96); cast is disarmed onmain
as of this writing and is getting the sibling fix. -
A
host=nobox with anincusgroup no longer hands out the bare
socket (#58) —users applyconsulted thehost=trait only when group
incuswas ABSENT (die onhost=yes, skip onhost=no). When the group
was PRESENT the trait was never asked, so ahost=noor marker-less box
that nonetheless carried the group —box setup-hostran, then the box was
re-bootstrapped with other traits — gave every box-role user a bare
usermod -aG incus: the socket with no tier behind it, whichincus-user
answers by lazily building an UNHARDENED project under whoever opens it
(incusbr-<uid>, NAT on v4 and v6, no ACL, nodns.mode=none, no port
isolation). The marker now decides in BOTH directions, through one new pure
gate (assert_marker_hosts_vms, testable against fixture markers non-root
likeassert_marker_human): the box role applies only where the box CLAIMS
to host VMs, so the verdict is identical whether or not the group exists.
The machine deliberately does not overrule the marker — but the skip is not
silent either: when the group exists and the trait disagrees, the warning
names the contradiction andrig bootstrapas the repair. On such a box
exact-membership convergence now strips box-role users out ofincus, on
the same reasoning: a membership inherited from a previous life is the same
half-grant as a freshly added one. -
Dropping the box role revokes through
box, not behind its back
(#50) —users applyconverged groupincuswith a baregpasswd -d,
the same move it makes forrig-adminandrig. Those two are rig's;
incusis box's, andbox revokedoes strictly more with it: it says
out loud that supplementary groups are read at LOGIN, so a session the
dropped operator already holds keeps the Incus socket until it dies, and
hands overloginctl terminate-user <user>as the remedy. rig logged
removed <user> from incusand moved on, so an operator who dropped
someone from the users file and watched apply succeed believed the VM
access was gone — and was wrong for as long as that user held a session.
Both removal paths (the per-user convergence and the dropped-user sweep)
now callbox revoke, which keeps one owner for the group. Never
--purge: that deletes the user's boxes, images and project, and
destroying someone's running machines is not a convergence step — it
stays an explicit admin act. The exit code is not trusted (#12's lesson):
a revoke that returns 0 with the membership still standing has not closed
the socket, and rig falls back to removing the group itself, as it also
does where box is not installed. Every fallback path carries the session
warning, because the silence was the bug. -
rig bootstraprefuses a users file that names no users (#57) — an
empty, comments-only or whitespace-only file is not a parse error, so it
passed pre-flight, converged nothing, and left the box root-only: the exact
outcome--no-usersexists to make explicit, reached by the flag added to
guarantee the opposite. Bootstrap's pre-flight now catches the zero-user
parse — beforeapt, the hostname change, or a spent pre-auth key — and
refuses, naming--no-usersas the way to ask for a root-only box out loud.
Scoped torig bootstrap's contract only: a standalonerig users apply
against an emptied file is a real de-provisioning operation and is
unchanged.
Downloads
-
Source code (ZIP)
1 download
-
Source code (TAR.GZ)
1 download