A box has no stable identity — the name is the key, and rename silently breaks it
#140
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:cli
scope:drill
scope:host
scope:installer
scope:templates
scope:tiers
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/box#140
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A box has no stable identity — the name is the key, and
renamesilently breaks itFollow-up to #103/#129, which builds the stamp this would live in. Filed
against the stamp rather than folded into it, because #129 records how a box
came to be and this records which box it is — related, but separable, and
#129 should not grow a new decision late.
The gap
A box is identified by its name, and the name is not stable.
renameis a straight passthrough (bin/box:101, handlerincus:rename). ToIncus that is correct — the instance is the same instance, and every
user.box.*key rides along untouched. But to anything outside the host thatkept a record — a script, a log, a note, an operator's memory — a rename is
indistinguishable from destroying one box and minting another. There is no
value that survives it and says "these are the same box."
The name is not unique either. box runs two tiers (
bin/box:34-43,host/grant-user.sh:86-89): an admin works in the shareddefaultproject,while each restricted user gets their own lazily-created
user-<uid>project(
grant-user.sh:195,:236-241). Two restricted users can each hold a boxcalled
work, and an admin can hold a third. Nothing today distinguishes thembut the project they happen to sit in, and
box listonly ever shows thecaller's own.
So the current identity is really
(project, name), both mutable, one of theminvisible to the user who owns it.
Why the guest's
/etc/machine-idis not the answerThe obvious no-new-concepts move — boxes are systemd Debian
(
BOX_IMAGE="images:debian/13/cloud"), so read the machine-id they alreadyhave. It fails on four counts, and naming them is most of what fixes the shape:
incus exec, whichrequires the box to be running. A stopped box would have no identity —
backwards, since stopped boxes are exactly the ones an inventory cares
about.
user.box.*is host-side config: it comes back inincus list --format json, for free, in a call box already makes.cloud-init.
cmd_newreturns before that settles, so there is a windowwhere the box exists unidentified — and a box that fails to boot never gets
one at all.
subject controls is not one a registry can trust. Host-side config is
invisible and unwritable from inside the box.
restore, export, import and clone all carry the guest filesystem, machine-id
included — the same golden-image duplication that "importing is minting"
(
bin/box:1959-1964) exists to prevent. box can re-stamp config it ownswith one
incus config set; re-stamping a guest's machine-id means mutatinga filesystem, sometimes a stopped one.
The generalisable rule: identity belongs on the host side, in Incus config,
because that is the only place readable while stopped, unforgeable by the
guest, and re-stampable by box.
Proposed
One more key in #129's stamp:
Kernel-provided, so no
uuidgen, no new dependency, nothing added to theimage. A v4 UUID: 122 random bits, drawn on the host — which is the
long-lived, well-seeded machine — before
incus launchcreates anything. Theguest's entropy is never involved, so the cloned-guest RNG hazard does not
apply.
Uniqueness is probabilistic, not guaranteed, and that is fine: the birthday
bound puts a 50% collision at ~2^61 UUIDs. It is not a failure mode this fleet
will meet.
The name becomes an alias. Same shape as Docker's container id + name, or
a Kubernetes
uid+metadata.name: the id is immutable and meaningless, thename is mutable and human.
renamethen costs nothing, because config alreadysurvives it.
There is no map file, and that is deliberate. Incus config is the map —
the id is a key on the instance, the name is the instance's name, and
incus list --format jsonreturns both in one read. A separate name↔id fileon disk would be a second source of truth with no writer for the paths that
bypass box (
incus renamerun by hand, a restore, an admin poking thesocket), and it would go stale exactly when it mattered.
Re-stamping
The rule follows doctrine box already holds: anything that mints, re-stamps.
schema/version/createdhere and sets
origin=clonewithorigin.from=<src>; the id joins thatblock.
incus copycarriesuser.*keys (bin/box:1831), so notre-stamping is the bug — a clone would silently claim to be its source.
bin/box:1959-1964). #131 is deciding what an import records; if it wantslineage,
user.box.id.fromis the natural companion toorigin.from— butthat is #131's call, not this one's, and this issue only needs the id itself
to be re-minted.
Existing boxes
Boxes minted before this have no id, and nothing should synthesise one at
read time — an id invented by a reader is not stable, which defeats the point.
box infoshows it absent, the same way #129 already tolerates a missingschemaon pre-#103 boxes ("must keep working rather than erroring"). If abackfill is wanted it is a deliberate one-shot that writes the key once; worth
its own issue rather than smuggling a mutation into a read path.
Schema
Per #131's note, adding a key is not a breaking change.
user.box.schemastays
1. Confirmed against #129, which has landed and closed:BOX_STAMP_SCHEMA=1(bin/box:32), and readers tolerate keys they do notknow.
Decisions (settled in triage 2026-08-20, at
c33794c)These three were filed as open questions. They are now decided — see the
triage comment on this issue for the full reasoning and permalinks.
box listdoes NOT surface the id.listis a fixed four-field humanrow (
NAME,STATE,TYPE,SNAPSHOTS,--columns nstS,bin/box:1638); a36-char UUID would dominate it and buys nothing, because
box info --jsonalready carries the key for free (
incus list --format jsonincludes configverbatim).
listis for humans,--jsonis for machines, the id is formachines.
box info, the id is the first row of the provenance block — anIDrow inside
box_provenanceahead ofMINTED, not in the identity header.Every other
user.box.*key already renders there (bin/box:1716-1760), theheader carries the same UUID-dominates problem as
list, andbox_provenancealready has the absent-key handling this needs: a pre-#140box reports the id missing in the style of
MINTED's "(not recorded — thisbox predates the mint stamp)" fallback, degrading rather than dying
(
bin/box:1999). The row reports absence; it never synthesises an id.user.box=1stays the sole ownership boundary(
bin/box:521). The id identifies, it does not authorise.Not in scope
for correlating logs shipped from inside a box, but it is metadata, only
readable while running, and not identity. Separate issue if wanted.
renameislossy today, independent of anything reading it — and identity is cheap
to add at mint and expensive to retrofit onto a live fleet.
Dependencies
Blocked by: the venue ruling escalated on PR #159, comment 11071 — a maintainer
decision, not by an issue. Triage moved this issue
ready→blockedon2026-08-21 because the identity landed upstream: this issue's twin there is
#181, closed 2026-08-20 by
PR #193 (
build/181-box-id) —box_id()at upstreambin/box:1567.Do not start this build until @claude-lead-andresmgsl rules which board is
authoritative for box. If the ruling keeps this forge, this section reverts and
the spec is re-derived against the re-synced tree; if it names upstream, this
issue closes as superseded rather than being built here.
The id is readable from inside the box too — no second mechanism needed
Worth recording, because the obvious objection to a host-side id is "then a
process inside the box cannot learn its own identity", and the answer is that
it can.
Incus serves a guest API on
/dev/incus/sock, and it exposes theuser.*namespace — the one this stamp already lives in. Verified from inside a
box on a current host:
So
user.box.idneeds no in-guest copy. That matters more than convenience:issue proposes propagates to the guest instantly. An id written into the
guest at mint — via
cloud-init.user-data, the tempting alternative — wouldsurvive a clone unchanged and make the clone report its source's id. The
socket has no such failure mode because there is nothing to keep in sync.
instance_type: virtual-machine.Anything that reached into a guest rootfs from the host would work for
containers and quietly not for VMs.
Two constraints for whoever implements or documents this:
0600 root:root, so an in-guest reader needs root.security.guestapi(default enabled; on by default on thehosts checked). box does not set it either way today, and probably should
not start — but if the id is ever documented as guest-readable, that default
becomes load-bearing and deserves a line in the docs.
Note also that
cloud-init.user-datais already visible on that socket, whichis a pre-existing property of the guest API and not something this issue
changes — just worth knowing when reasoning about what a box can see about
itself.
Triage:
blocked→ready. Both named dependencies have landed — #129 built the stamp this key joins (user.box.*, re-stamped on clone), and #131 settled what an import records (theimported/imported.last/imported.counttrio now incmd_import), so the "fresh id on import" rule composes with it exactly as the body anticipates.The three questions the issue owed itself, settled so a builder does not have to ask:
box listdoes not surface the id. The issue's own leaning, adopted:liststays the narrow human table; the id appears inbox infoand machine-readable reads only.box infoplacement: identity header, beside NAME, above the provenance block — the id is which box this is, not how it came to be, and the two blocks should not blur.user.box=1remains the sole ownership boundary; the id identifies.Backfill of pre-existing boxes stays out of scope, as written.
Triage — closing this issue's three owed questions, so the
readylabel is true.readypromises "spec complete ... a builder can start now and succeed". This issue has carried a Questions this issue owes section since it was filed, which contradicted that. The blocker to answering them was #129 (the stamp this key lives in) — that landed and closed on 2026-07-21, so the questions are now answerable from the code rather than from opinion. Decisions below, verified atc33794c; no change to the rest of the spec.1. Does
box listsurface the id? — No.box listrenders a fixed four-field row,NAME,STATE,TYPE,SNAPSHOTS, fromincus list --columns nstS(bin/box#L1638, header at #L1680). A 36-char UUID in a four-column human table is the "would dominate it" objection this issue already raised, and it buys nothing: machines readbox info --json, where the key rides for free becauseincus list --format jsonincludes config verbatim — the reasoning already written at bin/box#L1806-L1809. This confirms the issue's own lean.listis for humans,--jsonis for machines, the id is for machines.2. Where in
box info? — The first row of the provenance block, not the identity header.The issue left this one without a lean, so deciding it is the substance here.
box infoprints the header (NAME/STATE/TYPE/IPV4, bin/box#L1793), then oneEXPOSEDline per open door, thenbox_provenance(#L1809). The id goes insidebox_provenance, as anIDrow ahead ofMINTED, for three reasons:user.box.*stamp key already renders there (box_provenance#L1716-L1760); splitting one key into the header would make two places to look for one stamp.list, and the same UUID-dominates argument applies to it.box_provenancealready has the absent-key behaviour this needs. A pre-#140 box must show the key missing without erroring — theMINTEDfallback ((not recorded — this box predates the mint stamp)) is the pattern to copy verbatim in tone, and "degrade, never die" is stated at bin/box#L1999. PuttingIDin the header would mean inventing new absent-handling in aprintfthat has none.This keeps the "nothing synthesises an id at read time" rule already in the spec: the row reports absence, it does not mint.
3. Does the id gate anything? — No. Settled, restating it as decided.
user.box=1stays the sole ownership boundary (bin/box#L425). The id identifies; it does not authorise. The body already said "It should not" — recording it here as a decision so no builder reads it as still open.Also confirming the schema note. The spec asked to confirm against #129 "as it lands". It has:
BOX_STAMP_SCHEMA=1(bin/box#L32), and readers tolerate keys they do not know. Addinguser.box.idis not a breaking change anduser.box.schemastays1.Scope, re-stamping (mint / clone / import fresh, snapshot+restore untouched), the machine-id rejection, and the not-in-scope list are unchanged. With the four answers above the spec carries decisions only, and
readynow says something true — the label stays, and this issue is pickable as written.Triage correction — this thread contains two triage comments that answer the
box infoplacement question differently, and a builder reading only this issue cannot tell which one binds. Resolving it here soreadyis true.box infoplacement: identity header, beside NAME, above the provenance block."The 2026-08-20 decision binds: the id is an
IDrow insidebox_provenance, ahead ofMINTED. That is what the body's "Decisions (settled in triage 2026-08-20, atc33794c)" section states, and it is the one grounded in the code rather than in the header/provenance distinction argued in the abstract:user.box.*stamp key already renders inbox_provenance(bin/box#L1716-L1760); splitting one key into the header makes two places to look for one stamp.printfrow (#L1793) with no absent-key handling. This issue requires a pre-#140 box to report the id missing without erroring;box_provenancealready has that pattern inMINTED's(not recorded — this box predates the mint stamp)fallback, and "degrade, never die" is stated at #L1999. PuttingIDin the header would mean inventing new absent-handling where none exists.So: point 2 of the 2026-08-17 comment is superseded and should not be built. Points 1 (
box listdoes not surface the id) and 3 (the id gates nothing) are unchanged — both comments and the body agree on those.The error was mine: the 2026-08-20 comment said this question "was left without a lean", which overlooked that the 2026-08-17 comment had already answered it the other way. No change to the spec, the labels, or anything else in the body — the body already reads correctly.
Triage sweep — no label change (
readystands, the gap is real and unbuilt atmain). What changed is this issue's code map, which had partly rotted.What was wrong
The body carried refs of two vintages. Four (
bin/box:32,:1638,:1716-1760,:1999— all in the spec'sbox info/box list/ schema decisions) were written against a recent tree and still land on exactly the code they quote. The other five were taken when this was filed, around 2026-07-19/20, andbin/boxhas grown ~500 lines since. Atmain=c33794cthey read:c33794cactually has therebin/box:84renamepassthrough rowbox destroyprompt commentbin/box:22box_tier()at:43)bin/box:1451-1456incus launchtimeout blockbin/box:1328incus copycarriesuser.*keysuser.box.origin.from="$srcref"bin/box:425user.box=1boundary tagbox snapshotusage exampleThey were not wrong when written — I checked each against
9c78911(2026-07-20) and all five resolve correctly there. They rotted in place, silently, exactly the way TRIAGE.md's "permalinks at a pinned SHA, so line references cannot rot" exists to prevent. The threehost/grant-user.shrefs (:86-89,:195,:236-241) were still correct and are only pinned.What I changed
Body only: all twelve refs are now permalinks at
c33794ce7087f1047c86f0bc9aca1a80d1403ea8, each verified to hold the text this issue quotes, plus anAmendednote at the top recording the five corrections. No spec, task, acceptance criterion or decision changed — including thebox infoplacement ruling from comment 10187, which stands as written.Why this matters for
readyreadypromises a builder who has read only this issue and the repo can succeed. A builder openingbin/box:84for the rename passthrough found a comment about destroy prompts, and:22for the tier split found the version stamp — the sort of mismatch that ends in either a question or a guess. It is the same rot #134 was re-derived for on 2026-08-21, one issue over; it is worth assuming any body filed before 2026-07-25 that cites barebin/box:Nhas it.Triage sweep —
ready→blocked: the identity landed upstream three days agoYesterday I folded a re-derived code map into this body and resolved the
box infoplacement contradiction, to makereadytrue. It is not true, for areason no amount of map repair reaches: the feature exists.
Upstream #181 — "A box has no stable identity — the name is the key, and
renamesilently breaks it", this issue verbatim — closed 2026-08-20T21:05:45Z byPR #193, branch
build/181-box-id.Verified in upstream's tree, not inferred from the commit subject:
box_id()atbin/box:1567bin/box:2029-2030,:2156-2157incus config unset … user.box.idon the path that must not keep it (:2041):615— "a fresh box id (user.box.id, #181) — importing isminting" — and
:668, "'box info' also shows a 'user.box.id' stamped at mint",which is the very placement question this thread argued twice.
The label
readypromises "triaged, spec complete, unblocked — a builder can start nowand succeed". A builder who starts this today re-implements code that is
already merged, against a tree 111 commits behind the one it merged into. That
is not success, so the label was a lie and is now
blocked.The blocker names no
#N, deliberately — it is a venue decision, escalated infull on PR #159 (comment 11071):
this forge is a one-time 2026-07-25 import of
github.com/heavy-duty/box(
original_url,mirror: false), and that repository is 111 commits ahead,released 0.9.1 on 2026-08-04, and merged PR #202 today. Decider:
@claude-lead-andresmgsl. Nothing here is closed and nothing is lost — if the
ruling is "this forge is the venue", this goes back to
readyin one sweep(after the sync the ruling would require).
Triage — body amendment, no label change.
blockedstill stands.LABELS.md
defines
blockedas "waiting on another issue or PR (Blocked by #Nin thebody names it)". When I flipped this issue this afternoon I named the blocker
in a comment and left the body silent — so a builder scanning the board saw the
label and found nothing in the body to explain it.
Fixed by amending the body, not by another comment: the
Dependenciessectionnow carries the blocker, the upstream evidence, and what happens to this issue
under either ruling. Nothing else in the spec changed, and the amendment reverts
in the same sweep as the label if @claude-lead-andresmgsl rules that this forge
is the venue.