A box has no stable identity — the name is the key, and rename silently breaks it #140

Open
opened 2026-07-20 19:15:53 +00:00 by claude-bot-andresmgsl · 7 comments
claude-bot-andresmgsl commented 2026-07-20 19:15:53 +00:00 (Migrated from github.com)

A box has no stable identity — the name is the key, and rename silently breaks it

Follow-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.

Amended 2026-08-21 by triage — code map re-pinned, no change in substance.
This body carried line references of two different vintages. Four of them
(:32, :1638, :1716-1760, :1999) were written against a recent tree
and still resolve at main. The other five were taken around 2026-07-19/20
— verified: they all resolve correctly at 9c78911 (2026-07-20) — and bin/box has
grown since, so at main they landed on unrelated code: bin/box:84
(rename row) → :101, bin/box:22 (the two tiers) → :34-43,
bin/box:1451-1456 ("importing is minting") → :1959-1964,
bin/box:1328 (incus copy carries user.*) → :1831, and
bin/box:425 (the user.box=1 boundary) → :521. All twelve refs are
now permalinks pinned at main = c33794c, each verified to hold
the text this issue quotes. host/grant-user.sh:86-89, :195 and
:236-241 were already correct and are only pinned. No spec, task or
acceptance criterion changed.

The gap

A box is identified by its name, and the name is not stable.

rename is a straight passthrough (bin/box:101, handler incus:rename). To
Incus that is correct — the instance is the same instance, and every
user.box.* key rides along untouched. But to anything outside the host that
kept 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 shared default project,
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 box
called work, and an admin can hold a third. Nothing today distinguishes them
but the project they happen to sit in, and box list only ever shows the
caller's own.

So the current identity is really (project, name), both mutable, one of them
invisible to the user who owns it.

Why the guest's /etc/machine-id is not the answer

The obvious no-new-concepts move — boxes are systemd Debian
(BOX_IMAGE="images:debian/13/cloud"), so read the machine-id they already
have. It fails on four counts, and naming them is most of what fixes the shape:

  1. It lives in the guest filesystem. Reading it means incus exec, which
    requires 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 in
    incus list --format json, for free, in a call box already makes.
  2. It does not exist at mint. systemd writes it on first boot, after
    cloud-init. cmd_new returns before that settles, so there is a window
    where the box exists unidentified — and a box that fails to boot never gets
    one at all.
  3. The guest can rewrite it. These boxes run coding agents. An identity the
    subject controls is not one a registry can trust. Host-side config is
    invisible and unwritable from inside the box.
  4. It duplicates on exactly the operations box already guards. Snapshot,
    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 owns
    with one incus config set; re-stamping a guest's machine-id means mutating
    a 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:

--config user.box.id="$(cat /proc/sys/kernel/random/uuid)"

Kernel-provided, so no uuidgen, no new dependency, nothing added to the
image. A v4 UUID: 122 random bits, drawn on the host — which is the
long-lived, well-seeded machine — before incus launch creates anything. The
guest'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, the
name is mutable and human. rename then costs nothing, because config already
survives 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 json returns both in one read. A separate name↔id file
on disk would be a second source of truth with no writer for the paths that
bypass box (incus rename run by hand, a restore, an admin poking the
socket), and it would go stale exactly when it mattered.

Re-stamping

The rule follows doctrine box already holds: anything that mints, re-stamps.

  • mint — fresh id.
  • clone — fresh id. #129 already re-stamps schema/version/created
    here and sets origin=clone with origin.from=<src>; the id joins that
    block. incus copy carries user.* keys (bin/box:1831), so not
    re-stamping is the bug — a clone would silently claim to be its source.
  • import — fresh id, on the same grounds ("importing is minting",
    bin/box:1959-1964). #131 is deciding what an import records; if it wants
    lineage, user.box.id.from is the natural companion to origin.from — but
    that is #131's call, not this one's, and this issue only needs the id itself
    to be re-minted.
  • snapshot/restore — untouched. A restore is the same box.

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 info shows it absent, the same way #129 already tolerates a missing
schema on pre-#103 boxes ("must keep working rather than erroring"). If a
backfill 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.schema
stays 1.
Confirmed against #129, which has landed and closed:
BOX_STAMP_SCHEMA=1 (bin/box:32), and readers tolerate keys they do not
know.

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 list does NOT surface the id. list is a fixed four-field human
    row (NAME,STATE,TYPE,SNAPSHOTS, --columns nstS, bin/box:1638); a
    36-char UUID would dominate it and buys nothing, because box info --json
    already carries the key for free (incus list --format json includes config
    verbatim). list is for humans, --json is for machines, the id is for
    machines.
  • In box info, the id is the first row of the provenance block — an ID
    row inside box_provenance ahead of MINTED, not in the identity header.
    Every other user.box.* key already renders there (bin/box:1716-1760), the
    header carries the same UUID-dominates problem as list, and
    box_provenance already has the absent-key handling this needs: a pre-#140
    box reports the id missing in the style of MINTED's "(not recorded — this
    box predates the mint stamp)" fallback, degrading rather than dying
    (bin/box:1999). The row reports absence; it never synthesises an id.
  • The id gates nothing. user.box=1 stays the sole ownership boundary
    (bin/box:521). The id identifies, it does not authorise.

Not in scope

  • Backfilling existing boxes (above).
  • Recording the guest's machine-id as a descriptive field. Potentially useful
    for correlating logs shipped from inside a box, but it is metadata, only
    readable while running, and not identity. Separate issue if wanted.
  • Any consumer of the id. This is worth having on its own merits — rename is
    lossy 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 readyblocked on
2026-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 upstream bin/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.

# A box has no stable identity — the name is the key, and `rename` silently breaks it Follow-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. > **Amended 2026-08-21 by triage — code map re-pinned, no change in substance.** > This body carried line references of two different vintages. Four of them > (`:32`, `:1638`, `:1716-1760`, `:1999`) were written against a recent tree > and still resolve at `main`. The other five were taken around 2026-07-19/20 > — verified: they all resolve correctly at `9c78911` (2026-07-20) — and `bin/box` has > grown since, so at `main` they landed on unrelated code: `bin/box:84` > (`rename` row) → **:101**, `bin/box:22` (the two tiers) → **:34-43**, > `bin/box:1451-1456` ("importing is minting") → **:1959-1964**, > `bin/box:1328` (`incus copy` carries `user.*`) → **:1831**, and > `bin/box:425` (the `user.box=1` boundary) → **:521**. All twelve refs are > now permalinks pinned at `main` = [`c33794c`](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8), each verified to hold > the text this issue quotes. `host/grant-user.sh:86-89`, `:195` and > `:236-241` were already correct and are only pinned. No spec, task or > acceptance criterion changed. ## The gap A box is identified by its name, and the name is not stable. `rename` is a straight passthrough ([`bin/box:101`](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L101), handler `incus:rename`). To Incus that is correct — the instance is the same instance, and every `user.box.*` key rides along untouched. But to anything *outside* the host that kept 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`](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L34-L43), [`host/grant-user.sh:86-89`](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/host/grant-user.sh#L86-L89)): an admin works in the shared `default` project, while each restricted user gets their own lazily-created `user-<uid>` project ([`grant-user.sh:195`](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/host/grant-user.sh#L195), [`:236-241`](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/host/grant-user.sh#L236-L241)). Two restricted users can each hold a box called `work`, and an admin can hold a third. Nothing today distinguishes them but the project they happen to sit in, and `box list` only ever shows the caller's own. So the current identity is really `(project, name)`, both mutable, one of them invisible to the user who owns it. ## Why the guest's `/etc/machine-id` is not the answer The obvious no-new-concepts move — boxes are systemd Debian (`BOX_IMAGE="images:debian/13/cloud"`), so read the machine-id they already have. It fails on four counts, and naming them is most of what fixes the shape: 1. **It lives in the guest filesystem.** Reading it means `incus exec`, which requires 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 in `incus list --format json`, for free, in a call box already makes. 2. **It does not exist at mint.** systemd writes it on first boot, after cloud-init. `cmd_new` returns before that settles, so there is a window where the box exists unidentified — and a box that fails to boot never gets one at all. 3. **The guest can rewrite it.** These boxes run coding agents. An identity the subject controls is not one a registry can trust. Host-side config is invisible and unwritable from inside the box. 4. **It duplicates on exactly the operations box already guards.** Snapshot, 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`](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L1959-L1964)) exists to prevent. box can re-stamp config it owns with one `incus config set`; re-stamping a guest's machine-id means mutating a 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: ```sh --config user.box.id="$(cat /proc/sys/kernel/random/uuid)" ``` Kernel-provided, so no `uuidgen`, no new dependency, nothing added to the image. A v4 UUID: 122 random bits, drawn on the **host** — which is the long-lived, well-seeded machine — before `incus launch` creates anything. The guest'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, the name is mutable and human. `rename` then costs nothing, because config already survives 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 json` returns both in one read. A separate name↔id file on disk would be a second source of truth with no writer for the paths that bypass box (`incus rename` run by hand, a restore, an admin poking the socket), and it would go stale exactly when it mattered. ## Re-stamping The rule follows doctrine box already holds: **anything that mints, re-stamps.** - **mint** — fresh id. - **clone** — fresh id. #129 already re-stamps `schema`/`version`/`created` here and sets `origin=clone` with `origin.from=<src>`; the id joins that block. `incus copy` carries `user.*` keys ([`bin/box:1831`](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L1831)), so *not* re-stamping is the bug — a clone would silently claim to be its source. - **import** — fresh id, on the same grounds ("importing is minting", [`bin/box:1959-1964`](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L1959-L1964)). #131 is deciding what an import records; if it wants lineage, `user.box.id.from` is the natural companion to `origin.from` — but that is #131's call, not this one's, and this issue only needs the id itself to be re-minted. - **snapshot/restore** — untouched. A restore is the same box. ## 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 info` shows it absent, the same way #129 already tolerates a missing `schema` on pre-#103 boxes ("must keep working rather than erroring"). If a backfill 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.schema` stays `1`.** Confirmed against #129, which has landed and closed: `BOX_STAMP_SCHEMA=1` ([`bin/box:32`](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L32)), and readers tolerate keys they do not know. ## 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 list` does NOT surface the id.** `list` is a fixed four-field human row (`NAME,STATE,TYPE,SNAPSHOTS`, `--columns nstS`, [`bin/box:1638`](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L1638)); a 36-char UUID would dominate it and buys nothing, because `box info --json` already carries the key for free (`incus list --format json` includes config verbatim). `list` is for humans, `--json` is for machines, the id is for machines. - **In `box info`, the id is the first row of the provenance block** — an `ID` row inside `box_provenance` ahead of `MINTED`, not in the identity header. Every other `user.box.*` key already renders there ([`bin/box:1716-1760`](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L1716-L1760)), the header carries the same UUID-dominates problem as `list`, and `box_provenance` already has the absent-key handling this needs: a pre-#140 box reports the id missing in the style of `MINTED`'s "(not recorded — this box predates the mint stamp)" fallback, degrading rather than dying ([`bin/box:1999`](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L1999)). The row reports absence; it never synthesises an id. - **The id gates nothing.** `user.box=1` stays the sole ownership boundary ([`bin/box:521`](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L521)). The id identifies, it does not authorise. ## Not in scope - Backfilling existing boxes (above). - Recording the *guest's* machine-id as a descriptive field. Potentially useful for correlating logs shipped from inside a box, but it is metadata, only readable while running, and not identity. Separate issue if wanted. - Any consumer of the id. This is worth having on its own merits — `rename` is lossy **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](https://forgejo.heavyduty.builders/heavy-duty/box/pulls/159#issuecomment-11071) — a maintainer decision, not by an issue.** Triage moved this issue `ready` → `blocked` on 2026-08-21 because the identity landed upstream: this issue's twin there is [#181](https://github.com/heavy-duty/box/issues/181), closed 2026-08-20 by [PR #193](https://github.com/heavy-duty/box/pull/193) (`build/181-box-id`) — `box_id()` at upstream `bin/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.
claude-bot-andresmgsl commented 2026-07-20 19:22:49 +00:00 (Migrated from github.com)

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 the user.*
namespace — the one this stamp already lives in. Verified from inside a
box on a current host:

$ curl -s --unix-socket /dev/incus/sock http://x/1.0
{"state":"Started","api_version":"1.0","instance_type":"virtual-machine","location":"dev-server"}

$ curl -s --unix-socket /dev/incus/sock http://x/1.0/config
["/1.0/config/user.box.user","/1.0/config/user.box.template","/1.0/config/user.box","/1.0/config/cloud-init.user-data"]

$ curl -s --unix-socket /dev/incus/sock http://x/1.0/config/user.box.template
claude

So user.box.id needs no in-guest copy. That matters more than convenience:

  • It is a live read of host config, not a copy. The clone re-stamp this
    issue proposes propagates to the guest instantly. An id written into the
    guest at mint — via cloud-init.user-data, the tempting alternative — would
    survive 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.
  • It works for VMs. The sample above is 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:

  • The socket is 0600 root:root, so an in-guest reader needs root.
  • It depends on security.guestapi (default enabled; on by default on the
    hosts 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-data is already visible on that socket, which
is 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.

## 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 the `user.*` namespace — the one this stamp already lives in. Verified from inside a box on a current host: ``` $ curl -s --unix-socket /dev/incus/sock http://x/1.0 {"state":"Started","api_version":"1.0","instance_type":"virtual-machine","location":"dev-server"} $ curl -s --unix-socket /dev/incus/sock http://x/1.0/config ["/1.0/config/user.box.user","/1.0/config/user.box.template","/1.0/config/user.box","/1.0/config/cloud-init.user-data"] $ curl -s --unix-socket /dev/incus/sock http://x/1.0/config/user.box.template claude ``` So `user.box.id` needs no in-guest copy. That matters more than convenience: - **It is a live read of host config, not a copy.** The clone re-stamp this issue proposes propagates to the guest instantly. An id written into the guest at mint — via `cloud-init.user-data`, the tempting alternative — would survive 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. - **It works for VMs.** The sample above is `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: - The socket is `0600 root:root`, so an in-guest reader needs root. - It depends on `security.guestapi` (default enabled; on by default on the hosts 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-data` is already visible on that socket, which is 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.
claude-bot-andresmgsl added
ready
and removed
blocked
labels 2026-08-17 22:30:36 +00:00

Triage: blockedready. 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 (the imported/imported.last/imported.count trio now in cmd_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:

  1. box list does not surface the id. The issue's own leaning, adopted: list stays the narrow human table; the id appears in box info and machine-readable reads only.
  2. box info placement: 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.
  3. The id gates nothing. Confirmed: user.box=1 remains the sole ownership boundary; the id identifies.

Backfill of pre-existing boxes stays out of scope, as written.

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 (the `imported`/`imported.last`/`imported.count` trio now in `cmd_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: 1. **`box list` does not surface the id.** The issue's own leaning, adopted: `list` stays the narrow human table; the id appears in `box info` and machine-readable reads only. 2. **`box info` placement: 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. 3. **The id gates nothing.** Confirmed: `user.box=1` remains 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 ready label is true.

ready promises "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 at c33794c; no change to the rest of the spec.

1. Does box list surface the id? — No.

box list renders a fixed four-field row, NAME,STATE,TYPE,SNAPSHOTS, from incus 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 read box info --json, where the key rides for free because incus list --format json includes config verbatim — the reasoning already written at bin/box#L1806-L1809. This confirms the issue's own lean. list is for humans, --json is 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 info prints the header (NAME/STATE/TYPE/IPV4, bin/box#L1793), then one EXPOSED line per open door, then box_provenance (#L1809). The id goes inside box_provenance, as an ID row ahead of MINTED, for three reasons:

  • Every other 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.
  • The header is the same fixed-width human row as list, and the same UUID-dominates argument applies to it.
  • box_provenance already has the absent-key behaviour this needs. A pre-#140 box must show the key missing without erroring — the MINTED fallback ((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. Putting ID in the header would mean inventing new absent-handling in a printf that 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=1 stays 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. Adding user.box.id is not a breaking change and user.box.schema stays 1.

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 ready now says something true — the label stays, and this issue is pickable as written.

Triage — closing this issue's three owed questions, so the `ready` label is true. `ready` promises "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 at `c33794c`; no change to the rest of the spec. **1. Does `box list` surface the id? — No.** `box list` renders a fixed four-field row, `NAME,STATE,TYPE,SNAPSHOTS`, from `incus list --columns nstS` ([bin/box#L1638](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L1638), header at [#L1680](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#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 read `box info --json`, where the key rides for free because `incus list --format json` includes config verbatim — the reasoning already written at [bin/box#L1806-L1809](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L1806-L1809). This confirms the issue's own lean. `list` is for humans, `--json` is 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 info` prints the header (`NAME`/`STATE`/`TYPE`/`IPV4`, [bin/box#L1793](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L1793)), then one `EXPOSED` line per open door, then `box_provenance` ([#L1809](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L1809)). The id goes **inside `box_provenance`, as an `ID` row ahead of `MINTED`**, for three reasons: - Every other `user.box.*` stamp key already renders there ([box_provenance#L1716-L1760](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L1716-L1760)); splitting one key into the header would make two places to look for one stamp. - The header is the same fixed-width human row as `list`, and the same UUID-dominates argument applies to it. - `box_provenance` already has the absent-key behaviour this needs. A pre-#140 box must show the key missing without erroring — the `MINTED` fallback (`(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](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L1999). Putting `ID` in the header would mean inventing new absent-handling in a `printf` that 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=1` stays the sole ownership boundary ([bin/box#L425](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/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](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L32)), and readers tolerate keys they do not know. Adding `user.box.id` is not a breaking change and **`user.box.schema` stays `1`**. 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 `ready` now 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 info placement question differently, and a builder reading only this issue cannot tell which one binds. Resolving it here so ready is true.

  • Comment of 2026-08-17, point 2: "box info placement: identity header, beside NAME, above the provenance block."
  • Comment of 2026-08-20, point 2: "The first row of the provenance block, not the identity header."

The 2026-08-20 decision binds: the id is an ID row inside box_provenance, ahead of MINTED. That is what the body's "Decisions (settled in triage 2026-08-20, at c33794c)" section states, and it is the one grounded in the code rather than in the header/provenance distinction argued in the abstract:

  • Every other user.box.* stamp key already renders in box_provenance (bin/box#L1716-L1760); splitting one key into the header makes two places to look for one stamp.
  • The header is a fixed-width printf row (#L1793) with no absent-key handling. This issue requires a pre-#140 box to report the id missing without erroring; box_provenance already has that pattern in MINTED's (not recorded — this box predates the mint stamp) fallback, and "degrade, never die" is stated at #L1999. Putting ID in 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 list does 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 correction — this thread contains two triage comments that answer the `box info` placement question **differently**, and a builder reading only this issue cannot tell which one binds. Resolving it here so `ready` is true. - [Comment of 2026-08-17](https://forgejo.heavyduty.builders/heavy-duty/box/issues/140#issuecomment-8476), point 2: *"`box info` placement: **identity header, beside NAME**, above the provenance block."* - [Comment of 2026-08-20](https://forgejo.heavyduty.builders/heavy-duty/box/issues/140#issuecomment-10181), point 2: *"**The first row of the provenance block**, not the identity header."* **The 2026-08-20 decision binds: the id is an `ID` row inside `box_provenance`, ahead of `MINTED`.** That is what the body's "Decisions (settled in triage 2026-08-20, at `c33794c`)" section states, and it is the one grounded in the code rather than in the header/provenance distinction argued in the abstract: - Every other `user.box.*` stamp key already renders in `box_provenance` ([bin/box#L1716-L1760](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L1716-L1760)); splitting one key into the header makes two places to look for one stamp. - The header is a fixed-width `printf` row ([#L1793](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L1793)) with **no absent-key handling**. This issue requires a pre-#140 box to report the id missing without erroring; `box_provenance` already has that pattern in `MINTED`'s `(not recorded — this box predates the mint stamp)` fallback, and "degrade, never die" is stated at [#L1999](https://forgejo.heavyduty.builders/heavy-duty/box/src/commit/c33794ce7087f1047c86f0bc9aca1a80d1403ea8/bin/box#L1999). Putting `ID` in 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 list` does 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 (ready stands, the gap is real and unbuilt at main). 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's box 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, and bin/box has grown ~500 lines since. At main = c33794c they read:

ref in the body what it claimed what c33794c actually has there correct now
bin/box:84 the rename passthrough row the box destroy prompt comment :101
bin/box:22 box runs two tiers the mint-stamp version comment :34-43 (box_tier() at :43)
bin/box:1451-1456 "importing is minting" the incus launch timeout block :1959-1964
bin/box:1328 incus copy carries user.* keys user.box.origin.from="$srcref" :1831
bin/box:425 the user.box=1 boundary tag a box snapshot usage example :521

They 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 three host/grant-user.sh refs (: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 an Amended note at the top recording the five corrections. No spec, task, acceptance criterion or decision changed — including the box info placement ruling from comment 10187, which stands as written.

Why this matters for ready

ready promises a builder who has read only this issue and the repo can succeed. A builder opening bin/box:84 for the rename passthrough found a comment about destroy prompts, and :22 for 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 bare bin/box:N has it.

Triage sweep — no label change (`ready` stands, the gap is real and unbuilt at `main`). 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's `box 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, and `bin/box` has grown ~500 lines since. At `main` = `c33794c` they read: | ref in the body | what it claimed | what `c33794c` actually has there | correct now | |---|---|---|---| | `bin/box:84` | the `rename` passthrough row | the `box destroy` prompt comment | **:101** | | `bin/box:22` | box runs two tiers | the mint-stamp version comment | **:34-43** (`box_tier()` at `:43`) | | `bin/box:1451-1456` | "importing is minting" | the `incus launch` timeout block | **:1959-1964** | | `bin/box:1328` | `incus copy` carries `user.*` keys | `user.box.origin.from="$srcref"` | **:1831** | | `bin/box:425` | the `user.box=1` boundary tag | a `box snapshot` usage example | **:521** | They 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](https://forgejo.heavyduty.builders/heavy-duty/box/src/branch/main/.ceremony/TRIAGE.md)'s "permalinks at a pinned SHA, so line references cannot rot" exists to prevent. The three `host/grant-user.sh` refs (`: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 an `Amended` note at the top recording the five corrections. **No spec, task, acceptance criterion or decision changed** — including the `box info` placement ruling from comment 10187, which stands as written. ## Why this matters for `ready` `ready` promises a builder who has read only this issue and the repo can succeed. A builder opening `bin/box:84` for the rename passthrough found a comment about destroy prompts, and `:22` for 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 bare `bin/box:N` has it.

Triage sweep — readyblocked: the identity landed upstream three days ago

Yesterday I folded a re-derived code map into this body and resolved the box info placement contradiction, to make ready true. It is not true, for a
reason no amount of map repair reaches: the feature exists.

Upstream #181 — "A box has no stable identity — the name is the key, and rename silently breaks it", this issue verbatim — closed 2026-08-20T21:05:45Z by
PR #193, branch build/181-box-id.
Verified in upstream's tree, not inferred from the commit subject:

  • box_id() at bin/box:1567
  • the clone/mint stamp and the re-stamp at bin/box:2029-2030, :2156-2157
  • incus config unset … user.box.id on the path that must not keep it (:2041)
  • the help text at :615 — "a fresh box id (user.box.id, #181) — importing is
    minting" — 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

ready promises "triaged, spec complete, unblocked — a builder can start now
and 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 in
full 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 ready in one sweep
(after the sync the ruling would require).

## Triage sweep — `ready` → `blocked`: the identity landed upstream three days ago Yesterday I folded a re-derived code map into this body and resolved the `box info` placement contradiction, to make `ready` true. It is not true, for a reason no amount of map repair reaches: **the feature exists.** **Upstream [#181](https://github.com/heavy-duty/box/issues/181) — "A box has no stable identity — the name is the key, and `rename` silently breaks it", this issue verbatim — closed 2026-08-20T21:05:45Z** by [PR #193](https://github.com/heavy-duty/box/pull/193), branch `build/181-box-id`. Verified in upstream's tree, not inferred from the commit subject: - `box_id()` at `bin/box:1567` - the clone/mint stamp and the re-stamp at `bin/box:2029-2030`, `:2156-2157` - `incus config unset … user.box.id` on the path that must *not* keep it (`:2041`) - the help text at `:615` — "a fresh box id (`user.box.id`, #181) — importing is minting" — 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 `ready` promises "triaged, spec complete, unblocked — **a builder can start now and 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 in full on [PR #159 (comment 11071)](https://forgejo.heavyduty.builders/heavy-duty/box/pulls/159#issuecomment-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 `ready` in one sweep (after the sync the ruling would require).
claude-bot-andresmgsl added
blocked
and removed
ready
labels 2026-08-21 14:43:37 +00:00

Triage — body amendment, no label change. blocked still stands.

LABELS.md
defines blocked as "waiting on another issue or PR (Blocked by #N in the
body
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 Dependencies section
now 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.

Triage — body amendment, no label change. `blocked` still stands. [LABELS.md](https://forgejo.heavyduty.builders/heavy-duty/box/src/branch/main/.ceremony/LABELS.md) defines `blocked` as "waiting on another issue or PR (`Blocked by #N` **in the body** 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 `Dependencies` section now 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.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/box#140
No description provided.