feat: a minted box records how it was minted, and box info reads it back #129
4 changed files with 624 additions and 5 deletions
69
CHANGELOG.md
69
CHANGELOG.md
|
|
@ -5,6 +5,75 @@ which records not just what changed but what each drill run proved.
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- **A minted box records how it was minted, and `box info` reads it back**
|
||||
(#103) — `cmd_new` knew a great deal at the moment it launched and wrote
|
||||
three `user.*` keys, dropping the rest on the floor: the box version that
|
||||
minted it, the base image (an *unpinned alias on a moving remote* — two
|
||||
boxes minted a month apart from "the same template" are not the same box,
|
||||
and nothing on either said so), 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 simply 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* (an integer, not the box version: it moves only when a key
|
||||
is removed or repurposed, never when one is added). The alias's resolved
|
||||
fingerprint is pinned in a second call after the launch, read from
|
||||
`volatile.base_image` — best-effort by construction, because a box that
|
||||
exists and boots must never be failed over a provenance field. Deliberately
|
||||
not stamped: cpu/memory (`limits.*` hold them, and a duplicate drifts the
|
||||
first time someone edits a limit by hand), disk (a VM's *is* the root
|
||||
device size and a container's does not exist — its root rides the pool),
|
||||
and the tier, which `box_tier()` derives from whoever is *asking*.
|
||||
- **A clone re-stamps its own provenance instead of inheriting a lie**
|
||||
(#103) — `incus copy` carries every `user.*` key forward (drill audit item
|
||||
B2), which is what makes a clone know its template and user for free and is
|
||||
exactly why the mint stamp could not ride along untouched: an inherited
|
||||
stamp does not go stale, it goes **false**, claiming a mint time the clone
|
||||
was not present for and a box version that never saw it. `box new --from`
|
||||
now re-stamps the four keys that describe *this* instance's coming into
|
||||
being — schema, version, created, `origin=clone` with `origin.from=<src>` —
|
||||
on the copied instance *before* it is started, so a clone is never
|
||||
observable wearing its source's provenance. The lineage keys (template,
|
||||
user, image, role, rig pin) are left alone on purpose: the clone's disk
|
||||
genuinely did come from them, and re-deriving them from the cloning
|
||||
process's own template lookup would be the actual lie. `origin.from`
|
||||
records **one hop** — a clone of a clone names its parent and forgets its
|
||||
grandparent, because the alternative is an unbounded chain in a config
|
||||
value and the parent is the box an operator can go look at.
|
||||
One key sits in **neither** column and is therefore *cleared*:
|
||||
`user.box.mode.asked`. It records whether a container was asked for or
|
||||
fallen back into, which makes it a mint-*event* fact — and the asker was
|
||||
the **source's** operator. A clone refuses `--vm`/`--container` outright,
|
||||
so nobody was asked anything about this instance, and there is no true
|
||||
value to re-stamp it with; inheriting it made `box info` print
|
||||
`MODE vm (asked: auto)` on a clone, describing a demand never made of it.
|
||||
The clear lands with the re-stamp, before the start, and the read side
|
||||
needs no special case: the `MODE` line is gated on `asked`, so absence
|
||||
renders as silence while `TYPE` still reports VM or CT off the preserved
|
||||
instance type.
|
||||
- **`box info` grew a provenance block** (#103) — it printed
|
||||
`NAME / STATE / TYPE / IPV4`, exposures and snapshots, and surfaced *none*
|
||||
of the `user.box.*` keys, including the two that already existed. A stamp
|
||||
nothing can read is not a stamp. Every key is read with absence tolerated:
|
||||
`incus config get` on an unset key prints empty and exits 0 (audit item
|
||||
B4), so "no stamp" and "the daemon said no" arrive identically, and both
|
||||
must render as a box with blanks. **Boxes minted before this stamp existed
|
||||
keep working under every verb** — the README's standing promise, and the
|
||||
same one `user.claudebox` carries — showing `MINTED (not recorded — this
|
||||
box predates the mint stamp)` rather than an invented time or an error; a
|
||||
pre-rename box still reads as the claude template. A schema this box does
|
||||
not recognise is treated as *newer than me*: it shows what it understands
|
||||
and says so, because a box outlives the release that minted it and refusing
|
||||
to describe one a later release minted perfectly well is the wrong answer.
|
||||
`box info --json` needed no code — the keys ride `incus list --format json`
|
||||
in `config`. `test/cli.sh` drives both halves against a fake incus that
|
||||
logs the arguments box builds, including the absence assertions that keep
|
||||
cpu/memory/disk/tier out of the namespace and the lineage keys out of the
|
||||
clone's re-stamp.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`state:needs-human` no longer waits on the cron to become true** (#141)
|
||||
|
|
|
|||
45
README.md
45
README.md
|
|
@ -390,7 +390,7 @@ the door is per-port, punched and removable at runtime.
|
|||
box new --name <box> [--template <t>] [--from <src>[/<snap>]] [--cpu <n>] [--memory <size>] [--disk <size>] [--vm|--container]
|
||||
box templates # list the templates this install can mint
|
||||
box list # list your boxes
|
||||
box info <box> # one box: state, IP, exposures, snapshot labels
|
||||
box info <box> # one box: state, IP, exposures, provenance, snapshots
|
||||
box shell <box> # enter as the template's user
|
||||
box exec <box> -- <cmd...> # run a command in the box
|
||||
box tmux <box> [session] # attach/create a tmux session — survives disconnects
|
||||
|
|
@ -427,6 +427,49 @@ clones an existing box or snapshot. VM mode (`--vm`, the default where
|
|||
`security.nesting=true`) is for hosts without nested virt — weaker isolation,
|
||||
dev/test only.
|
||||
|
||||
## What minted this box: `box info`
|
||||
|
||||
A box outlives the release that minted it, the template that shaped it and the
|
||||
image build it came from — and until
|
||||
[#103](https://github.com/heavy-duty/box/issues/103) it recorded none of them.
|
||||
There is no host-side per-box store; the Incus instance config _is_ the
|
||||
database, so a fact not written at mint time is simply gone. `box new` now
|
||||
stamps what it knew, and `box info` reads it back:
|
||||
|
||||
```
|
||||
NAME work
|
||||
STATE RUNNING
|
||||
TYPE VM
|
||||
IPV4 10.x.x.x
|
||||
|
||||
MINTED 2026-07-19T14:22:07Z by box 0.8.1
|
||||
TEMPLATE claude (user claude, role claude)
|
||||
IMAGE images:debian/13/cloud @ 8a2f1c9d4e5b…
|
||||
MODE vm (asked: auto)
|
||||
RIG heavy-duty/rig@main
|
||||
ORIGIN mint
|
||||
```
|
||||
|
||||
The image line carries both halves on purpose: the template names an
|
||||
_unpinned alias on a moving remote_, so what it resolved to at that mint is the
|
||||
only reproducible fact. `box info --json` carries every key verbatim — they
|
||||
ride `incus list --format json` in `config`.
|
||||
|
||||
**A clone re-stamps.** `incus copy` preserves `user.*` keys, so a clone inherits
|
||||
its source's template and user for free — but inheriting the mint stamp would
|
||||
not make it stale, it would make it **false**: the clone was not present at that
|
||||
mint. `box new --from` therefore re-stamps the four keys that describe _this_
|
||||
instance's coming into being (`ORIGIN clone of work/authed`, a fresh time, the
|
||||
box version that cloned it) and leaves the lineage keys alone, because the
|
||||
clone's disk genuinely did come from that image, template and role. `origin.from`
|
||||
records one hop: a clone of a clone names its parent, not its grandparent.
|
||||
|
||||
**Boxes minted before this stamp existed keep working**, under this verb and
|
||||
every other — they render as a box with blanks and say `MINTED (not recorded)`
|
||||
rather than erroring. `user.box.schema` names the stamp's _shape_ (an integer,
|
||||
not the box version) so a box minted by a later release reads back on an older
|
||||
box as "here is what I understand, and there is more I don't".
|
||||
|
||||
## Boxes are just Incus instances
|
||||
|
||||
A box is an ordinary Incus instance tagged `user.box=1` (pre-0.4.0 boxes
|
||||
|
|
|
|||
188
bin/box
188
bin/box
|
|
@ -12,7 +12,18 @@ inst="" # the resolved Incus instance, set by the 'box' precondition
|
|||
|
||||
die() { echo "box: $*" >&2; exit 1; } # 1 = it went wrong
|
||||
usage_error() { echo "box: $*" >&2; echo "try 'box help'." >&2; exit 2; } # 2 = you asked wrong
|
||||
version() { echo "box $(cat "$root/VERSION" 2>/dev/null || echo unknown) ($root)"; }
|
||||
# The tree's own version, read in ONE place. 'box --version' says it out loud;
|
||||
# the mint stamp (#103) writes it onto every instance box creates, so a box can
|
||||
# still name the release that made it long after that release is history.
|
||||
box_version() { cat "$root/VERSION" 2>/dev/null || echo unknown; }
|
||||
version() { echo "box $(box_version) ($root)"; }
|
||||
|
||||
# The SHAPE of the mint stamp, not the box version — an integer that changes
|
||||
# only when a key is removed or repurposed, never when one is added (a reader
|
||||
# that does not know a key simply does not print it). Absent means pre-stamp:
|
||||
# every box minted before #103 has no schema key at all, and must keep working
|
||||
# under every verb, which is the same promise 'user.claudebox' carries.
|
||||
BOX_STAMP_SCHEMA=1
|
||||
|
||||
# Which tier is THIS PROCESS? Decided from live credentials (argless 'id -nG':
|
||||
# what the kernel will present when incus opens the socket), never from the
|
||||
|
|
@ -73,7 +84,7 @@ CMDS=(
|
|||
"new^--name <box> [--template <t>] [--from <src>[/<snap>]] [--cpu <n>] [--memory <size>] [--disk <size>] [--vm|--container]^^Mint a box from a template (default: blank), or --from an existing box/snapshot^fn:cmd_new^"
|
||||
"templates^^^List the templates this install can mint^fn:cmd_templates^"
|
||||
"list^[--json]^^List your boxes^fn:cmd_list^"
|
||||
"info^<box> [--json]^box^One box: state, type, IP, and its snapshot labels^fn:cmd_info^"
|
||||
"info^<box> [--json]^box^One box: state, type, IP, what minted it, snapshot labels^fn:cmd_info^"
|
||||
"shell^<box>^box^Open a shell in a box, as its template's user^fn:cmd_shell^"
|
||||
"exec^<box> -- <cmd...>^box^Run a command inside a box^fn:cmd_exec^"
|
||||
"tmux^<box> [<session>]^box^Attach or create a tmux session in a box — survives disconnects^fn:cmd_tmux^"
|
||||
|
|
@ -1002,8 +1013,16 @@ load_template() {
|
|||
# environment must die on the host, never execute in the guest. bash's =~
|
||||
# anchors to the whole string — a multi-line value cannot sneak one clean
|
||||
# line past it the way a line-oriented grep would.
|
||||
# The rig pin, resolved from the mint environment, in ONE place: render_userdata
|
||||
# substitutes it into the seed, and the mint stamp (#103) records it onto the
|
||||
# instance. Two spellings of the same default would eventually disagree, and a
|
||||
# stamp that disagrees with the seed is worse than no stamp at all.
|
||||
rig_repo() { printf '%s\n' "${RIG_REPO:-heavy-duty/rig}"; }
|
||||
rig_ref() { printf '%s\n' "${RIG_REF:-main}"; }
|
||||
|
||||
render_userdata() {
|
||||
local f="$1" repo="${RIG_REPO:-heavy-duty/rig}" ref="${RIG_REF:-main}" data
|
||||
local f="$1" repo data ref
|
||||
repo="$(rig_repo)"; ref="$(rig_ref)"
|
||||
[[ "$repo" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] \
|
||||
|| die "RIG_REPO must look like owner/repo: $repo"
|
||||
[[ "$ref" =~ ^[A-Za-z0-9._/-]+$ ]] \
|
||||
|
|
@ -1026,6 +1045,13 @@ cmd_templates() {
|
|||
echo "mint one: box new --name <box> --template <template>"
|
||||
}
|
||||
|
||||
# When this instance came into being. A timestamp in a CONVERGENT file would be
|
||||
# churn — the same run writing a different byte every time — but a mint is not
|
||||
# convergent: it happens exactly once, to exactly one instance, and is never
|
||||
# re-run against it. UTC and ISO 8601 so it sorts as a string and means the same
|
||||
# thing on every host that reads it back.
|
||||
mint_time() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
||||
|
||||
cmd_new() {
|
||||
[ -n "$name" ] || usage_error "usage: $(synopsis_of new)"
|
||||
require_stack
|
||||
|
|
@ -1037,6 +1063,47 @@ cmd_new() {
|
|||
case "$from" in */*) snap="${from#*/}" ;; esac
|
||||
srcref="$(iname_of "$src")"; [ -n "$snap" ] && srcref="$srcref/$snap"
|
||||
incus copy "$srcref" "$instance"
|
||||
# 'incus copy' carries every user.* key forward (audit B2) — which is what
|
||||
# makes a clone know its template and user for free, and is also why the
|
||||
# mint stamp (#103) cannot simply ride along. A clone that inherited the
|
||||
# stamp verbatim would claim to have been minted at the source's mint time,
|
||||
# by the box version that minted the SOURCE, in a mint that never touched
|
||||
# this instance. That is not a stale field, it is a false one.
|
||||
#
|
||||
# So re-stamp exactly the keys that describe THIS instance's coming into
|
||||
# being, and leave the rest alone:
|
||||
# · version / created / schema — the clone was made HERE, NOW, by THIS box
|
||||
# · origin=clone, origin.from=<srcref> — how, and from what
|
||||
# Deliberately NOT re-stamped, because they are lineage and stay true: the
|
||||
# clone's disk really did come from that image, that template, that user and
|
||||
# that rig role — reading them off the source is the whole point of a clone.
|
||||
# ('incus copy' preserves the instance type too, so mode stays true as well.)
|
||||
#
|
||||
# 'mode.asked' is the one key that sits in NEITHER column, and so it is
|
||||
# CLEARED rather than re-stamped or inherited. It is a mint-event fact —
|
||||
# only the mint knew whether a container was asked for or fallen back into
|
||||
# for want of /dev/kvm — and the asker was the SOURCE's operator. A clone
|
||||
# refuses --vm/--container outright (nobody was asked anything here), so an
|
||||
# inherited 'asked' makes 'box info' print a demand that was never made of
|
||||
# this instance. There is no true value to re-stamp it with: the honest
|
||||
# answer is absence, and absence is already how the whole block renders
|
||||
# what it does not know — the MODE line simply does not print, while TYPE
|
||||
# above still says VM or CT off the preserved instance type.
|
||||
#
|
||||
# origin.from records ONE hop. A clone of a clone names its parent and
|
||||
# forgets its grandparent: the alternative is an unbounded chain in a config
|
||||
# value, and the parent is the box an operator can actually go look at.
|
||||
incus config set "$instance" \
|
||||
user.box.schema="$BOX_STAMP_SCHEMA" \
|
||||
user.box.version="$(box_version)" \
|
||||
user.box.created="$(mint_time)" \
|
||||
user.box.origin=clone \
|
||||
user.box.origin.from="$srcref"
|
||||
# Cleared, not set-to-empty: an empty value is still a key, and a reader
|
||||
# that greps the config would find it. Tolerated failure because the source
|
||||
# may predate the stamp and never have carried the key at all — a clone
|
||||
# must not die over a key that was already absent.
|
||||
incus config unset "$instance" user.box.mode.asked >/dev/null 2>&1 || true
|
||||
incus start "$instance"
|
||||
wait_agent "$instance"
|
||||
reset_identity "$instance"
|
||||
|
|
@ -1077,6 +1144,47 @@ cmd_new() {
|
|||
# which user. 'incus copy' preserves user.* keys (audit B2), so a clone
|
||||
# knows what it is without ever consulting the template again.
|
||||
#
|
||||
# And the rest of what this line knows and used to drop on the floor (#103).
|
||||
# There is no host-side per-box store — the Incus instance config IS the
|
||||
# database — so a fact not written here is simply gone the moment the mint
|
||||
# returns. The stamp describes the MINT, not the outcome: it lands before
|
||||
# cloud-init and before rig, and nothing later edits it.
|
||||
#
|
||||
# schema the stamp's shape, so a future reader knows what it is holding
|
||||
# version the box that minted it — 'box --version' is a fact about the
|
||||
# binary in front of you, never about the box you are looking at
|
||||
# image the alias asked for. It is an UNPINNED alias on a moving
|
||||
# remote: two boxes minted a month apart from "the same
|
||||
# template" are not the same box, and the alias alone cannot
|
||||
# say so. What it resolved to is pinned after the launch below.
|
||||
# mode what it minted as, and what was ASKED — a container that fell
|
||||
# back for want of /dev/kvm and one the operator asked for read
|
||||
# identically afterwards, and only the mint knew which
|
||||
# role the rig role box auto-runs at the hook below
|
||||
# rig.* WHICH rig converged it, stamped only for a seed that actually
|
||||
# installs rig from the pin ('blank' seeds none, so it gets none)
|
||||
# created when. See mint_time() for why a timestamp belongs here.
|
||||
# origin mint. A clone re-stamps it (see the --from branch above).
|
||||
#
|
||||
# NOT stamped, on purpose: cpu/memory (limits.* already hold them, and a
|
||||
# duplicate drifts the first time someone edits the limit by hand); disk
|
||||
# (a VM's is the root device size, and a container's does not exist — its
|
||||
# root rides the pool, so a stamped value would be fiction); and tier,
|
||||
# which box_tier() derives from whoever is ASKING, not from the box.
|
||||
local stamp=(
|
||||
--config user.box.schema="$BOX_STAMP_SCHEMA"
|
||||
--config user.box.version="$(box_version)"
|
||||
--config user.box.image="$T_IMAGE"
|
||||
--config user.box.mode="$m"
|
||||
--config user.box.mode.asked="$mode"
|
||||
--config user.box.created="$(mint_time)"
|
||||
--config user.box.origin=mint
|
||||
)
|
||||
[ -z "$T_BOOTSTRAP_ROLE" ] || stamp+=(--config user.box.role="$T_BOOTSTRAP_ROLE")
|
||||
if grep -q '@RIG_REPO@' "$root/templates/$t/user-data.yaml" 2>/dev/null; then
|
||||
stamp+=(--config user.box.rig.repo="$(rig_repo)" --config user.box.rig.ref="$(rig_ref)")
|
||||
fi
|
||||
#
|
||||
# The launch is narrated and TIME-BOXED (#93). Twice in the 2026-07-19
|
||||
# release drill the child 'incus launch' wedged before the create was
|
||||
# even accepted — 'incus operation list' empty, the instance never
|
||||
|
|
@ -1095,6 +1203,7 @@ cmd_new() {
|
|||
--config user.box=1 \
|
||||
--config user.box.template="$t" \
|
||||
--config user.box.user="$T_USER" \
|
||||
"${stamp[@]}" \
|
||||
--config limits.cpu="$T_CPU" \
|
||||
--config limits.memory="$T_MEMORY" \
|
||||
--config cloud-init.user-data="$(render_userdata "$root/templates/$t/user-data.yaml")" \
|
||||
|
|
@ -1132,6 +1241,20 @@ cmd_new() {
|
|||
# Not a wedge: incus refused and said why on stderr, right above.
|
||||
die "incus launch failed (exit $rc)"
|
||||
fi
|
||||
# The one field the launch line could not know: 'user.box.image' above is
|
||||
# the ALIAS, and an alias on a moving remote is not a reproducible fact.
|
||||
# Incus resolves it during the launch and records what it landed on in
|
||||
# volatile.base_image — read it back and pin it into the stamp, so an
|
||||
# incident six months from now can ask "was this box built on the image
|
||||
# that broke?" and get an answer instead of a template name.
|
||||
#
|
||||
# Best-effort BY CONSTRUCTION, and that is the whole design of this line:
|
||||
# it runs only after a launch that already succeeded, and a box that exists
|
||||
# and boots must never be failed over a provenance field. Every failure
|
||||
# here is silent and leaves the alias standing as the honest partial answer
|
||||
# — a stamp with no fingerprint, which is exactly how cmd_info renders it.
|
||||
local fp; fp="$(incus config get "$instance" volatile.base_image 2>/dev/null || true)"
|
||||
[ -z "$fp" ] || incus config set "$instance" user.box.image.fingerprint="$fp" >/dev/null 2>&1 || true
|
||||
wait_agent "$instance"
|
||||
echo "box: waiting for phase-1 (cloud-init)..."
|
||||
echo "box: (its full narration, live: incus exec $name -- tail -f /var/log/cloud-init-output.log)"
|
||||
|
|
@ -1284,6 +1407,59 @@ cmd_list() {
|
|||
fi
|
||||
}
|
||||
|
||||
# One instance config key. 'incus config get' on an UNSET key prints empty and
|
||||
# exits 0 (audit B4) — so the '|| true' here covers the daemon refusing, not the
|
||||
# key being absent, and every caller below reads absence as an empty string.
|
||||
box_cfg() { incus config get "$1" "$2" 2>/dev/null || true; }
|
||||
|
||||
# The mint stamp (#103), read back for 'box info'. Every key is optional and so
|
||||
# is the whole block: a box minted before the stamp existed carries none of it
|
||||
# and must render as a box with blanks — never as an error, and never as a box
|
||||
# wearing a mint time it does not have. Legacy boxes are not a transitional
|
||||
# case: a box outlives the release that minted it, which is exactly what the
|
||||
# legacy 'user.claudebox' tag already says out loud at resolve_box.
|
||||
box_provenance() {
|
||||
local i="$1" schema created ver img fp m asked tpl u role rrepo rref origin from
|
||||
schema="$(box_cfg "$i" user.box.schema)"
|
||||
created="$(box_cfg "$i" user.box.created)"; ver="$(box_cfg "$i" user.box.version)"
|
||||
img="$(box_cfg "$i" user.box.image)"; fp="$(box_cfg "$i" user.box.image.fingerprint)"
|
||||
m="$(box_cfg "$i" user.box.mode)"; asked="$(box_cfg "$i" user.box.mode.asked)"
|
||||
tpl="$(box_cfg "$i" user.box.template)"; u="$(box_cfg "$i" user.box.user)"
|
||||
role="$(box_cfg "$i" user.box.role)"
|
||||
rrepo="$(box_cfg "$i" user.box.rig.repo)"; rref="$(box_cfg "$i" user.box.rig.ref)"
|
||||
origin="$(box_cfg "$i" user.box.origin)"; from="$(box_cfg "$i" user.box.origin.from)"
|
||||
# A pre-rename box has no metadata at all but is always a Claude box — the
|
||||
# same mapping box_user() makes, for the same reason.
|
||||
[ -n "$tpl" ] || [ "$(box_cfg "$i" user.claudebox)" != 1 ] || { tpl=claude; u="${u:-claude}"; }
|
||||
|
||||
echo
|
||||
if [ -n "$created" ] || [ -n "$ver" ]; then
|
||||
printf '%-11s%s\n' MINTED "${created:-(time not recorded)} by box ${ver:-unknown}"
|
||||
else
|
||||
printf '%-11s%s\n' MINTED "(not recorded — this box predates the mint stamp)"
|
||||
fi
|
||||
if [ -n "$tpl" ]; then
|
||||
local paren=""
|
||||
[ -z "$u" ] || paren="user $u"
|
||||
[ -z "$role" ] || paren="${paren:+$paren, }role $role"
|
||||
printf '%-11s%s\n' TEMPLATE "$tpl${paren:+ ($paren)}"
|
||||
fi
|
||||
[ -z "$img" ] || printf '%-11s%s\n' IMAGE "$img${fp:+ @ ${fp:0:12}…}"
|
||||
# The mode is only worth a line alongside what was ASKED: TYPE above already
|
||||
# says VM or CT, but only the mint knew whether a container was chosen or
|
||||
# fallen back into for want of /dev/kvm.
|
||||
[ -z "$asked" ] || printf '%-11s%s\n' MODE "${m:-?} (asked: $asked)"
|
||||
[ -z "$rrepo" ] || printf '%-11s%s\n' RIG "$rrepo@${rref:-?}"
|
||||
[ -z "$origin" ] || printf '%-11s%s\n' ORIGIN "$origin${from:+ of $from}"
|
||||
# A schema box does not recognise is NEWER than box, not broken: show what is
|
||||
# understood and say so, rather than refusing to describe a box that a later
|
||||
# release minted perfectly well. (A non-integer lands here too, which is the
|
||||
# right side to fail on.)
|
||||
if [ -n "$schema" ] && { ! [ "$schema" -eq "$schema" ] 2>/dev/null || [ "$schema" -gt "$BOX_STAMP_SCHEMA" ]; }; then
|
||||
printf '%-11s%s\n' NOTE "stamp schema '$schema' is newer than this box ($(box_version)) reads ($BOX_STAMP_SCHEMA) — showing what it understands"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_info() {
|
||||
local box="${args[0]}" row
|
||||
if [ "$json" -eq 1 ]; then incus list "$inst" --format json; return; fi
|
||||
|
|
@ -1305,6 +1481,12 @@ cmd_info() {
|
|||
printf '%-11s%s → port %s\n' EXPOSED "${listen#tcp:}" "${d#expose-}"
|
||||
done < <(incus config device list "$inst" 2>/dev/null)
|
||||
|
||||
# What built this box, from what, when, with which box (#103). Nothing else
|
||||
# on the host records it — the instance config IS the store — so a stamp
|
||||
# nothing surfaces is a stamp nobody has. 'box info --json' carries the keys
|
||||
# for free: 'incus list --format json' includes config verbatim.
|
||||
box_provenance "$inst"
|
||||
|
||||
echo
|
||||
case "${snaps:-0}" in
|
||||
''|0)
|
||||
|
|
|
|||
327
test/cli.sh
327
test/cli.sh
|
|
@ -205,8 +205,15 @@ rm -rf "$EVILROOT"
|
|||
# grep -q failure mode).
|
||||
# ---------------------------------------------------------------------------
|
||||
RUFN="$(mktemp)"
|
||||
awk '/^render_userdata\(\) \{/,/^\}/' "$ROOT/bin/box" > "$RUFN"
|
||||
# The pin's DEFAULTS live in rig_repo/rig_ref, one definition shared with the
|
||||
# mint stamp (#103) — extract them alongside the function that reads them, or
|
||||
# the extracted copy silently renders an empty repo and every assertion below
|
||||
# goes green against nothing.
|
||||
{ grep -cE '^rig_(repo|ref)\(\)' "$ROOT/bin/box" | grep -qx 2 || echo 'die "the rig pin helpers moved — this extraction is stale"'
|
||||
grep -E '^rig_(repo|ref)\(\)' "$ROOT/bin/box"
|
||||
awk '/^render_userdata\(\) \{/,/^\}/' "$ROOT/bin/box"; } > "$RUFN"
|
||||
check "render_userdata: extracted from bin/box (guards the awk)" 0 "RIG_REPO" cat "$RUFN"
|
||||
check "render_userdata: the pin's defaults came with it (guards the grep)" 0 "heavy-duty/rig" cat "$RUFN"
|
||||
check "render_userdata: the extracted function is valid bash" 0 "" bash -n "$RUFN"
|
||||
|
||||
SEED="$(mktemp)"
|
||||
|
|
@ -1068,6 +1075,324 @@ check "import: reset_identity follows the start" 0 "" bash -c '
|
|||
check "grant: allows backups (the export workflow)" 0 "" \
|
||||
grep -qF 'restricted.backups allow' "$ROOT/host/grant-user.sh"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The mint stamp (#103) — DRIVEN on both halves, write and read.
|
||||
#
|
||||
# There is no host-side per-box store: the Incus instance config IS the
|
||||
# database, so the only proof that a fact survives the mint is the argument
|
||||
# list box hands 'incus launch'. A fake incus logs every call verbatim and
|
||||
# answers just enough for cmd_new and cmd_info to run to completion with no
|
||||
# daemon anywhere — the same trick the confirm-gate drive uses above.
|
||||
#
|
||||
# The read half matters as much as the write half, and legacy boxes most of
|
||||
# all: every box minted before this stamp existed carries none of these keys,
|
||||
# and a box outlives the release that minted it. 'incus config get' on an
|
||||
# unset key prints EMPTY and exits 0 (audit B4), so "no stamp" and "daemon
|
||||
# said no" arrive identically — 'box info' must render both as a box with
|
||||
# blanks, never as an error.
|
||||
# ---------------------------------------------------------------------------
|
||||
MSHIM="$(mktemp -d)"; MWORK="$(mktemp -d)"
|
||||
cat > "$MSHIM/incus" <<'SHIM'
|
||||
#!/usr/bin/env bash
|
||||
# Fake incus for the mint-stamp drive. Knobs, all optional:
|
||||
# FAKE_BASE_IMAGE what 'config get <i> volatile.base_image' resolves to;
|
||||
# empty = incus does not know it (the degraded mint)
|
||||
# FAKE_CFG a file of "<key> <value>" lines answering 'config get'
|
||||
# FAKE_ROW the csv row 'list --columns nstS' returns
|
||||
# The launch carries a whole cloud-init seed, so the call is logged with its
|
||||
# newlines flattened — an assertion about "the launch line" must see one line.
|
||||
printf 'incus %s\n' "$*" | tr '\n' ' ' >> "$FAKE_INCUS_LOG"
|
||||
printf '\n' >> "$FAKE_INCUS_LOG"
|
||||
case "$*" in
|
||||
*volatile.base_image) printf '%s\n' "${FAKE_BASE_IMAGE-}" ;;
|
||||
"config get "*)
|
||||
[ -n "${FAKE_CFG:-}" ] || exit 0
|
||||
key="$*"; key="${key##* }"
|
||||
awk -v k="$key" '$1 == k { $1 = ""; sub(/^ /, ""); print }' "$FAKE_CFG" ;;
|
||||
*"--columns nstS") printf '%s\n' "${FAKE_ROW-}" ;;
|
||||
*"--columns 4") echo '10.1.2.3 (enp5s0)' ;;
|
||||
esac
|
||||
exit 0
|
||||
SHIM
|
||||
chmod +x "$MSHIM/incus"
|
||||
|
||||
export FAKE_BASE_IMAGE=deadbeefcafe0123456789 # what the alias resolves to
|
||||
mintbox() { # mintbox <logfile> <args...> — the real box, shimmed, no TTY
|
||||
local log="$1"; shift
|
||||
: > "$log"
|
||||
env FAKE_INCUS_LOG="$log" PATH="$MSHIM:$PATH" "$BOX" "$@" </dev/null >"$log.out" 2>&1
|
||||
local rc=$?
|
||||
cat "$log.out"
|
||||
return "$rc"
|
||||
}
|
||||
# The launch line, isolated: every assertion below is about ONE incus call, and
|
||||
# grepping the whole log would let a key stamped by some other call pass.
|
||||
launchline() { grep -m1 '^incus launch ' "$1"; }
|
||||
# launch_has/restamp_has <log> <ere> — a matcher per surface, so the ABSENCE
|
||||
# assertions (a key that must not be stamped) are a plain non-zero exit rather
|
||||
# than a nest of quoting.
|
||||
launch_has() { launchline "$1" | grep -qE "$2"; }
|
||||
restamp_has() { grep -F 'config set' "$1" | grep -qE "$2"; }
|
||||
|
||||
# --- the write half: a fresh mint ------------------------------------------
|
||||
MLOG="$MWORK/mint.log"
|
||||
check "mint: a shimmed 'box new' runs to completion" 0 "ready" \
|
||||
mintbox "$MLOG" new --name w1 --template claude-box --container
|
||||
# Each key on its own check: a single grep for the whole block would go green
|
||||
# on a partial stamp, and "which fact was dropped" is the useful failure.
|
||||
check "mint: stamps the schema — the stamp's SHAPE, not the box version (#103)" \
|
||||
0 "user.box.schema=1" launchline "$MLOG"
|
||||
check "mint: stamps the box version that minted it (#103)" \
|
||||
0 "user.box.version=$(cat "$ROOT/VERSION")" launchline "$MLOG"
|
||||
check "mint: stamps the base image ALIAS asked for (#103)" \
|
||||
0 "user.box.image=images:debian/13/cloud" launchline "$MLOG"
|
||||
check "mint: stamps the mode it minted as (#103)" \
|
||||
0 "user.box.mode=container" launchline "$MLOG"
|
||||
# The demand, not just the outcome: TYPE already says CT afterwards, but only
|
||||
# the mint knew whether a container was ASKED for or fallen back into.
|
||||
check "mint: stamps the mode that was ASKED, not only the outcome (#103)" \
|
||||
0 "user.box.mode.asked=container" launchline "$MLOG"
|
||||
check "mint: stamps the rig role box will auto-run (#103)" \
|
||||
0 "user.box.role=claude-box" launchline "$MLOG"
|
||||
check "mint: stamps which rig converged it — repo (#103)" \
|
||||
0 "user.box.rig.repo=heavy-duty/rig" launchline "$MLOG"
|
||||
check "mint: stamps which rig converged it — ref (#103)" \
|
||||
0 "user.box.rig.ref=main" launchline "$MLOG"
|
||||
check "mint: stamps the origin (#103)" 0 "user.box.origin=mint" launchline "$MLOG"
|
||||
# The timestamp's SHAPE, so a local-time or seconds-since-epoch spelling fails
|
||||
# here: UTC ISO 8601, which is the only form that sorts and travels.
|
||||
check "mint: stamps the mint time as UTC ISO 8601 (#103)" 0 "" \
|
||||
launch_has "$MLOG" 'user\.box\.created=[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z'
|
||||
# The existing three keys are UNTOUCHED — the stamp extends the namespace, it
|
||||
# does not rewrite it, and box_user()/the login hint read two of them.
|
||||
check "mint: the pre-existing boundary tag still rides the same line" \
|
||||
0 "user.box=1" launchline "$MLOG"
|
||||
check "mint: the pre-existing template stamp is untouched" \
|
||||
0 "user.box.template=claude-box" launchline "$MLOG"
|
||||
check "mint: the pre-existing user stamp is untouched" \
|
||||
0 "user.box.user=claude" launchline "$MLOG"
|
||||
# Deliberately NOT stamped. limits.* already hold cpu/memory and a duplicate
|
||||
# drifts the first time someone edits a limit by hand; a container's disk does
|
||||
# not exist (its root rides the pool); and the tier is a fact about whoever is
|
||||
# ASKING. Absence assertions, so a well-meant addition has to argue here first.
|
||||
check "mint: does NOT duplicate cpu into the user.box namespace (#103)" 1 "" \
|
||||
launch_has "$MLOG" 'user\.box\.cpu'
|
||||
check "mint: does NOT duplicate memory into the user.box namespace (#103)" 1 "" \
|
||||
launch_has "$MLOG" 'user\.box\.memory'
|
||||
check "mint: does NOT stamp a disk — a container's does not exist (#103)" 1 "" \
|
||||
launch_has "$MLOG" 'user\.box\.disk'
|
||||
check "mint: does NOT stamp the tier — it describes the asker, not the box (#103)" 1 "" \
|
||||
launch_has "$MLOG" 'user\.box\.tier'
|
||||
|
||||
# --- the resolved fingerprint: the alias is not a reproducible fact ---------
|
||||
# $T_IMAGE is an unpinned alias on a moving remote. Incus resolves it during
|
||||
# the launch and records what it landed on; box reads that back and pins it.
|
||||
check "mint: pins the RESOLVED image fingerprint after the launch (#103)" 0 "" \
|
||||
grep -qF "config set w1 user.box.image.fingerprint=$FAKE_BASE_IMAGE" "$MLOG"
|
||||
check "mint: ...and it is a SECOND call, not something the launch could know" 1 "" \
|
||||
launch_has "$MLOG" 'image\.fingerprint'
|
||||
# The load-bearing half of that design: a box that exists and boots must never
|
||||
# be failed over a provenance field. With no fingerprint to be had, the mint
|
||||
# still succeeds and the alias stands alone as the honest partial answer.
|
||||
NOFP="$MWORK/nofp.log"
|
||||
FAKE_BASE_IMAGE="" # exported above; incus does not know what the alias resolved to
|
||||
check "mint: an unknowable fingerprint does not fail the mint (#103)" 0 "ready" \
|
||||
mintbox "$NOFP" new --name w4 --template blank --container
|
||||
check "mint: ...and it stamped no empty fingerprint key either (#103)" 1 "" \
|
||||
grep -q 'image.fingerprint' "$NOFP"
|
||||
FAKE_BASE_IMAGE=deadbeefcafe0123456789
|
||||
|
||||
# --- a seed that installs no rig is stamped no rig pin ----------------------
|
||||
# 'blank' carries no @RIG_REPO@ token, so there is no rig to name. A stamp
|
||||
# that named one anyway would be fiction, which is worse than a missing key.
|
||||
check "mint: the 'blank' seed installs no rig, so no rig pin is stamped (#103)" 1 "" \
|
||||
launch_has "$NOFP" 'user\.box\.rig\.'
|
||||
check "mint: ...and no role either — blank names none (#103)" 1 "" \
|
||||
launch_has "$NOFP" 'user\.box\.role'
|
||||
|
||||
# --- the rig pin has ONE definition, and both readers get the same answer ---
|
||||
# The seed substitutes it and the stamp records it. Two spellings of the same
|
||||
# default would eventually disagree, and a stamp that disagrees with the seed
|
||||
# it shipped alongside is worse than no stamp. Driven through the environment
|
||||
# override, so the two are compared on a value neither can have hardcoded.
|
||||
RIGLOG="$MWORK/rig.log"
|
||||
export RIG_REPO=someone/rig RIG_REF=probe-ref
|
||||
mintbox "$RIGLOG" new --name w5 --template claude-box --container >/dev/null 2>&1
|
||||
unset RIG_REPO RIG_REF
|
||||
check "mint: the rig pin override reaches the STAMP (#103)" 0 "" \
|
||||
launch_has "$RIGLOG" 'user\.box\.rig\.repo=someone/rig'
|
||||
check "mint: ...both halves of it (#103)" 0 "" \
|
||||
launch_has "$RIGLOG" 'user\.box\.rig\.ref=probe-ref'
|
||||
check "mint: ...and the SEED it shipped with carries the same pin (#103)" 0 "" \
|
||||
launch_has "$RIGLOG" 'someone/rig/probe-ref'
|
||||
|
||||
# --- the clone: the sharpest edge of the whole stamp ------------------------
|
||||
# 'incus copy' carries every user.* key forward (audit B2) — which is what
|
||||
# makes a clone know its template for free, and is exactly why the stamp
|
||||
# cannot ride along untouched. An inherited stamp does not go stale, it goes
|
||||
# FALSE: the clone would claim a mint time it was not present for, by a box
|
||||
# version that never saw it.
|
||||
CLONELOG="$MWORK/clone.log"
|
||||
check "clone: a shimmed 'box new --from' runs to completion" 0 "cloned" \
|
||||
mintbox "$CLONELOG" new --name w2 --from work/authed
|
||||
check "clone: re-stamps the origin as a clone, not a mint (#103)" 0 "" \
|
||||
grep -qF 'user.box.origin=clone' "$CLONELOG"
|
||||
check "clone: names the source it was taken from — one hop (#103)" 0 "" \
|
||||
grep -qF 'user.box.origin.from=work/authed' "$CLONELOG"
|
||||
check "clone: re-stamps the box version that made THIS instance (#103)" 0 "" \
|
||||
grep -qF "user.box.version=$(cat "$ROOT/VERSION")" "$CLONELOG"
|
||||
check "clone: re-stamps a fresh created time (#103)" 0 "" \
|
||||
grep -qE 'user\.box\.created=[0-9]{4}-[0-9]{2}-[0-9]{2}T' "$CLONELOG"
|
||||
# The other half of the decision, and the reason it is a decision at all: the
|
||||
# LINEAGE keys are left alone on purpose. The clone's disk genuinely came from
|
||||
# that image, that template, that user, that role — re-stamping them from the
|
||||
# cloning process's own template lookup would be the actual lie, and would
|
||||
# break the login hint that reads user.box.template off the instance.
|
||||
check "clone: does NOT re-stamp the template — it is inherited lineage (#103)" 1 "" \
|
||||
restamp_has "$CLONELOG" 'user\.box\.template'
|
||||
check "clone: does NOT re-stamp the user — box_user() reads the source's (#103)" 1 "" \
|
||||
restamp_has "$CLONELOG" 'user\.box\.user'
|
||||
check "clone: does NOT re-stamp the image — the disk really came from it (#103)" 1 "" \
|
||||
restamp_has "$CLONELOG" 'user\.box\.image'
|
||||
check "clone: does NOT re-stamp the role — the source's rig converged it (#103)" 1 "" \
|
||||
restamp_has "$CLONELOG" 'user\.box\.role'
|
||||
# The third column, and the one review found: 'mode.asked' is neither lineage
|
||||
# nor re-stampable. It is a mint-event fact whose asker was the SOURCE's
|
||||
# operator, and a clone refuses --vm/--container, so nobody was asked anything
|
||||
# here. It is CLEARED — there is no true value to give it.
|
||||
check "clone: clears the inherited mode.asked — nobody asked THIS box (#103)" 0 "" \
|
||||
grep -qF 'config unset w2 user.box.mode.asked' "$CLONELOG"
|
||||
check "clone: ...and does not re-stamp it with a fabricated answer (#103)" 1 "" \
|
||||
restamp_has "$CLONELOG" 'user\.box\.mode\.asked'
|
||||
# Cleared, never set-to-empty: an empty value is still a key on the instance.
|
||||
check "clone: clears it rather than setting it empty (#103)" 1 "" \
|
||||
grep -qE 'config set .*user\.box\.mode\.asked=($|[[:space:]])' "$CLONELOG"
|
||||
# Order: the re-stamp lands on the copied instance BEFORE it is started, so a
|
||||
# clone is never observable wearing its source's provenance. Fail-closed — an
|
||||
# absent line makes the arithmetic fail, not pass.
|
||||
restamp_precedes_start() {
|
||||
local set start
|
||||
set="$(grep -n 'config set .* user.box.origin=clone' "$1" | head -1 | cut -d: -f1)"
|
||||
start="$(grep -n '^incus start ' "$1" | head -1 | cut -d: -f1)"
|
||||
[ -n "$set" ] && [ -n "$start" ] && [ "$set" -lt "$start" ]
|
||||
}
|
||||
check "clone: the re-stamp precedes the start (#103)" 0 "" \
|
||||
restamp_precedes_start "$CLONELOG"
|
||||
# The clear rides the same rule for the same reason: a clone must never be
|
||||
# observable — not for one moment, not to 'box info' — wearing an 'asked' its
|
||||
# operator never gave. Fail-closed the same way.
|
||||
clear_precedes_start() {
|
||||
local unset_ln start
|
||||
unset_ln="$(grep -n 'config unset .* user.box.mode.asked' "$1" | head -1 | cut -d: -f1)"
|
||||
start="$(grep -n '^incus start ' "$1" | head -1 | cut -d: -f1)"
|
||||
[ -n "$unset_ln" ] && [ -n "$start" ] && [ "$unset_ln" -lt "$start" ]
|
||||
}
|
||||
check "clone: the mode.asked clear precedes the start too (#103)" 0 "" \
|
||||
clear_precedes_start "$CLONELOG"
|
||||
|
||||
# --- the read half: 'box info' surfaces it ---------------------------------
|
||||
# A stamp nothing can read is not done. cmd_info printed NAME/STATE/TYPE/IPV4
|
||||
# and surfaced none of the keys that already existed.
|
||||
STAMPED="$MWORK/stamped.cfg"
|
||||
cat > "$STAMPED" <<'CFG'
|
||||
user.box 1
|
||||
user.box.schema 1
|
||||
user.box.created 2026-07-19T14:22:07Z
|
||||
user.box.version 0.8.0
|
||||
user.box.image images:debian/13/cloud
|
||||
user.box.image.fingerprint 8a2f1c9d4e5b6a7c8d9e
|
||||
user.box.mode vm
|
||||
user.box.mode.asked auto
|
||||
user.box.template claude
|
||||
user.box.user claude
|
||||
user.box.role claude
|
||||
user.box.rig.repo heavy-duty/rig
|
||||
user.box.rig.ref main
|
||||
user.box.origin mint
|
||||
CFG
|
||||
infobox() { # infobox <cfg-file> — 'box info work' against a canned config
|
||||
env FAKE_INCUS_LOG=/dev/null FAKE_CFG="$1" FAKE_ROW='work,RUNNING,VIRTUAL-MACHINE,0' \
|
||||
PATH="$MSHIM:$PATH" "$BOX" info work </dev/null 2>&1
|
||||
}
|
||||
check "info: surfaces the mint time and the box that minted it (#103)" \
|
||||
0 "MINTED 2026-07-19T14:22:07Z by box 0.8.0" infobox "$STAMPED"
|
||||
check "info: surfaces the image alias AND what it resolved to (#103)" \
|
||||
0 "IMAGE images:debian/13/cloud @ 8a2f1c9d4e5b" infobox "$STAMPED"
|
||||
check "info: surfaces the template with its user and role (#103)" \
|
||||
0 "TEMPLATE claude (user claude, role claude)" infobox "$STAMPED"
|
||||
check "info: surfaces which rig converged it (#103)" \
|
||||
0 "RIG heavy-duty/rig@main" infobox "$STAMPED"
|
||||
check "info: surfaces the origin (#103)" 0 "ORIGIN mint" infobox "$STAMPED"
|
||||
# Still the box it always was: the new block is additive, above the snapshots.
|
||||
check "info: still prints the state block it always did" 0 "IPV4 10.1.2.3" infobox "$STAMPED"
|
||||
|
||||
# A clone reads back as a clone, naming its source. Modelled on what the
|
||||
# --from branch actually leaves behind: origin re-stamped, mode.asked cleared.
|
||||
CLONECFG="$MWORK/clone.cfg"
|
||||
{ grep -v -e '^user.box.origin ' -e '^user.box.mode.asked ' "$STAMPED"
|
||||
echo 'user.box.origin clone'
|
||||
echo 'user.box.origin.from work/authed'; } > "$CLONECFG"
|
||||
check "info: a clone says so, and names the box it came from (#103)" \
|
||||
0 "ORIGIN clone of work/authed" infobox "$CLONECFG"
|
||||
# ...and stays silent about a demand nobody made of it. The MODE line is gated
|
||||
# on 'asked' precisely so absence renders as silence rather than as a guess;
|
||||
# TYPE above still reports VM off the instance type, so nothing is lost.
|
||||
info_has_mode() { infobox "$1" | grep -q '^MODE'; }
|
||||
check "info: a clone prints no MODE line — nobody asked IT anything (#103)" 1 "" \
|
||||
info_has_mode "$CLONECFG"
|
||||
check "info: ...while TYPE still reports what it actually is (#103)" \
|
||||
0 "TYPE VM" infobox "$CLONECFG"
|
||||
# The mint keeps its MODE line — there, the operator really was asked.
|
||||
check "info: a MINT still surfaces what was asked for (#103)" \
|
||||
0 "MODE vm (asked: auto)" infobox "$STAMPED"
|
||||
|
||||
# --- legacy boxes: the promise that they keep working under every verb ------
|
||||
# A box carrying the boundary tag and NOTHING else — every box minted before
|
||||
# this stamp existed. It must still render, exit 0, and say plainly that the
|
||||
# mint was not recorded rather than inventing one or erroring out.
|
||||
LEGACY="$MWORK/legacy.cfg"; printf 'user.box 1\n' > "$LEGACY"
|
||||
check "info: a box with NO stamp at all still renders, exit 0 (#103)" \
|
||||
0 "NAME work" infobox "$LEGACY"
|
||||
check "info: ...and says the mint was not recorded, rather than inventing one" \
|
||||
0 "predates the mint stamp" infobox "$LEGACY"
|
||||
info_has() { infobox "$1" | grep -qE "$2"; }
|
||||
check "info: ...and prints no half-empty IMAGE/ORIGIN lines for keys it lacks" 1 "" \
|
||||
info_has "$LEGACY" '^(IMAGE|ORIGIN|RIG|MODE) '
|
||||
# A pre-rename box carries user.claudebox=1 and no metadata at all, and is
|
||||
# always a Claude box — the same mapping box_user() makes, honored forever.
|
||||
PRERENAME="$MWORK/prerename.cfg"; printf 'user.claudebox 1\n' > "$PRERENAME"
|
||||
check "info: a pre-rename box still reads as the claude template (#103)" \
|
||||
0 "TEMPLATE claude" infobox "$PRERENAME"
|
||||
|
||||
# --- a schema from the future is not a broken box --------------------------
|
||||
# A box outlives the release that minted it, so an OLDER box will one day read
|
||||
# a NEWER box's stamp. It shows what it understands and says so; refusing to
|
||||
# describe a box a later release minted perfectly well is the wrong answer.
|
||||
FUTURE="$MWORK/future.cfg"
|
||||
{ grep -v '^user.box.schema ' "$STAMPED"; echo 'user.box.schema 99'; } > "$FUTURE"
|
||||
check "info: an unrecognised (newer) schema is noted, not refused (#103)" \
|
||||
0 "NOTE" infobox "$FUTURE"
|
||||
check "info: ...and it still shows every key it DOES understand (#103)" \
|
||||
0 "MINTED 2026-07-19T14:22:07Z" infobox "$FUTURE"
|
||||
check "info: ...and it still exits 0 — a future box is not a broken box (#103)" \
|
||||
0 "ORIGIN" infobox "$FUTURE"
|
||||
# A non-integer schema lands on the same side: noted, never fatal under set -e.
|
||||
GARBAGE="$MWORK/garbage.cfg"
|
||||
{ grep -v '^user.box.schema ' "$STAMPED"; echo 'user.box.schema not-a-number'; } > "$GARBAGE"
|
||||
check "info: a non-integer schema is noted, not fatal (#103)" 0 "NOTE" infobox "$GARBAGE"
|
||||
|
||||
# VERSION has ONE reader in bin/box — box_version() — and both 'box --version'
|
||||
# and the mint stamp go through it. A second 'cat $root/VERSION' is how the two
|
||||
# would eventually disagree about what minted a box.
|
||||
# shellcheck disable=SC2016 # '$root' is bin/box's variable, matched literally
|
||||
one_version_reader() {
|
||||
[ "$(grep -cF 'cat "$root/VERSION"' "$ROOT/bin/box")" -eq 1 ]
|
||||
}
|
||||
check "the tree's VERSION has a single reader, box_version() (#103)" 0 "" \
|
||||
one_version_reader
|
||||
|
||||
rm -rf "$MSHIM" "$MWORK"
|
||||
|
||||
# The rehearsal itself stays runnable: syntax-checked here, run on real hosts.
|
||||
check "multiuser.sh is valid bash" 0 "" bash -n "$ROOT/drill/multiuser.sh"
|
||||
check "multiuser.sh refuses without the env gate" 2 "opt in" \
|
||||
|
|
|
|||
Loading…
Reference in a new issue