Merge pull request #129 from claude-bot-andresmgsl/build/95-machine-id

feat(platform): stable machine ID — derived from /etc/machine-id, stored nowhere
This commit is contained in:
Daniel Marin 2026-07-24 09:12:56 +01:00 committed by GitHub
commit 243deb1a4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 163 additions and 15 deletions

View file

@ -10,6 +10,7 @@ on the way to cutting its first release, and this file starts there.
- `drill/drill.sh` — the drill has an instrument: pinned-ref assertion, a mechanical idempotence diff, and a `drills/<version>.md` record emitter (#105) - `drill/drill.sh` — the drill has an instrument: pinned-ref assertion, a mechanical idempotence diff, and a `drills/<version>.md` record emitter (#105)
- GitHub entry templates route humans to Discussions and prefill triage work orders and pull requests (#123) - GitHub entry templates route humans to Discussions and prefill triage work orders and pull requests (#123)
- `rig platform` prints a stable machine `ID`, derived from `/etc/machine-id`, never the raw value (#95)
- `kimi-box` joins the box tenant roles — the Kimi CLI agent guest (#109) - `kimi-box` joins the box tenant roles — the Kimi CLI agent guest (#109)
- The `changelog-armed` guard returns, version-keyed (#112, ceremony#13) - The `changelog-armed` guard returns, version-keyed (#112, ceremony#13)
- The `.ceremony/` doctrine mirror, verified by `docs-sync` on every PR (#112, ceremony#19) - The `.ceremony/` doctrine mirror, verified by `docs-sync` on every PR (#112, ceremony#19)

View file

@ -728,6 +728,7 @@ What is this machine — computed at run time, **stored nowhere**:
``` ```
PLATFORM PLATFORM
HOSTNAME hetzner-cp-1 HOSTNAME hetzner-cp-1
ID cd9fb802-1493-2336-d027-7955f328bcd8
OS Debian GNU/Linux 13 (trixie) OS Debian GNU/Linux 13 (trixie)
KERNEL 6.12.95+deb13-amd64 (x86_64) KERNEL 6.12.95+deb13-amd64 (x86_64)
CPU AMD Ryzen 7 3700X 8-Core Processor (16 cores) CPU AMD Ryzen 7 3700X 8-Core Processor (16 cores)
@ -755,10 +756,33 @@ Computing at run time removes the problem instead of managing it: the answer
is correct by construction because there is nothing to go stale. is correct by construction because there is nothing to go stale.
The corollary is deliberate: **`rig platform` works on a machine rig has never The corollary is deliberate: **`rig platform` works on a machine rig has never
converged.** It reads only `/proc`, `uname`, `/etc/os-release`, `df` and converged.** It reads only `/proc`, `uname`, `/etc/os-release`,
`systemd-detect-virt`, so it runs on bare Debian before bootstrap — useful for `/etc/machine-id`, `df` and `systemd-detect-virt`, so it runs on bare Debian
deciding *what to converge this into*, not just for auditing afterwards. It before bootstrap — useful for deciding *what to converge this into*, not just
needs no root, makes no network call, and writes nothing, ever. for auditing afterwards. It needs no root, makes no network call, and writes
nothing, ever.
**`ID` names the machine where `HOSTNAME` names the slot** — that contrast is
why they sit together. rig sets the hostname itself during bootstrap and
reuses it across rebuilds (`hetzner-cp-1` is a role, not hardware), so the
hostname cannot answer "is this the same machine I converged in June, or its
replacement?". `ID` can: it is derived from `/etc/machine-id` as
`sha256("rig-machine-id:<machine-id>")`, first 32 hex chars rendered
8-4-4-4-12 — computed at run time and stored nowhere, like every other fact in
the block, so it exists before bootstrap too. It is deliberately **not** the
raw machine-id: `machine-id(5)` asks that the value not be exposed, and the
namespaced hash is its documented remedy — a reader of `rig platform` output
cannot recover `/etc/machine-id`, nor correlate the id with any other tool's
derivation of it. A missing, empty or `uninitialized` machine-id renders
`ID unavailable (reason)` while every other field still reports; it is never
an empty string and never a hash of nothing, which would hand every such
machine the same identity.
**Two machines reporting the same `ID` means a cloned image** — actionable
information, not a coincidence. A host cloned from a golden image carries the
image's `/etc/machine-id`, and no identity that lives in the filesystem
survives the filesystem being copied. If you hit it, regenerate the clone's
machine-id (`systemd-machine-id-setup`) rather than doubting the field.
The `PROVENANCE` block is the complementary half — which rig, and when, which The `PROVENANCE` block is the complementary half — which rig, and when, which
is *decided* rather than observed, so it is stored. It is **read, never is *decided* rather than observed, so it is stored. It is **read, never

View file

@ -2,7 +2,8 @@
# rig platform — what is this machine? Calculated at run time, stored nowhere. # rig platform — what is this machine? Calculated at run time, stored nowhere.
# #
# Read-only in the strongest sense rig has: it reads /proc, uname, # Read-only in the strongest sense rig has: it reads /proc, uname,
# /etc/os-release, df and systemd-detect-virt, and writes NOTHING, ever. That # /etc/os-release, /etc/machine-id, df and systemd-detect-virt, and writes
# NOTHING, ever. That
# is the design, not an implementation detail — specs change without rig doing # is the design, not an implementation detail — specs change without rig doing
# anything (RAM added, root disk resized, unattended-upgrades patching the # anything (RAM added, root disk resized, unattended-upgrades patching the
# kernel), so a stored spec is stale the moment the machine changes, and # kernel), so a stored spec is stale the moment the machine changes, and
@ -24,13 +25,19 @@ usage() {
cat <<'EOF' cat <<'EOF'
usage: rig platform usage: rig platform
Describes the machine you are on: hostname, OS, kernel, CPU, memory, disk Describes the machine you are on: hostname, a stable machine ID, OS, kernel,
and virtualization, then rig's own provenance (which rig, when, and the role CPU, memory, disk and virtualization, then rig's own provenance (which rig,
marker bootstrap wrote). when, and the role marker bootstrap wrote).
Computed at run time from /proc, uname, /etc/os-release, df and ID names the machine where HOSTNAME names the slot: it is derived from
systemd-detect-virt. Writes nothing, needs no root, makes no network call — /etc/machine-id (a namespaced sha256, never the raw value, which machine-id(5)
so it also works on a pristine Debian box rig has never bootstrapped, where asks tools not to expose). Two machines reporting the same ID were cloned
from one image — actionable information, not a coincidence: no identity that
lives in the filesystem survives the filesystem being copied.
Computed at run time from /proc, uname, /etc/os-release, /etc/machine-id, df
and systemd-detect-virt. Writes nothing, needs no root, makes no network call
— so it also works on a pristine Debian box rig has never bootstrapped, where
the provenance block reads 'not bootstrapped'. the provenance block reads 'not bootstrapped'.
EOF EOF
} }
@ -53,6 +60,61 @@ field() { printf '%-10s %s\n' "$1" "$2"; }
# before anything has been installed. # before anything has been installed.
HOSTNAME_V="$(hostname 2>/dev/null || uname -n)" HOSTNAME_V="$(hostname 2>/dev/null || uname -n)"
# --- identity (#95) -----------------------------------------------------------
# HOSTNAME names the slot; ID names the machine. rig itself sets the hostname
# during bootstrap and reuses it across rebuilds ('hetzner-cp-1' is a role, not
# hardware), so nothing above answers "is this the same machine I converged in
# June, or its replacement?". /etc/machine-id does — but machine-id(5) asks
# that the raw value not be exposed (it is a stable correlator across every
# tool that leaks it), and its documented remedy is an application-specific
# derivation. So: THE PINNED DERIVATION, fixed by #95 so two implementations
# can never disagree —
#
# printf 'rig-machine-id:%s' "$(cat /etc/machine-id)" | sha256sum
# → first 32 hex chars, rendered 8-4-4-4-12
#
# The 'rig-machine-id:' prefix is the contract, not decoration: it is what
# keeps this id uncorrelatable with any other tool's derivation of the same
# machine-id. sha256sum is coreutils, which this command is restricted to.
# Derived, computed here, stored nowhere — #64's thesis — so it exists before
# bootstrap and needs no write path.
#
# What this deliberately does NOT fix: a host cloned from a golden image
# carries the clone's /etc/machine-id, so two machines reporting the same ID
# means a cloned image. That is surfaced (help text, README) rather than
# defended against — no identity that lives in the filesystem survives the
# filesystem being copied.
#
# RIG_MACHINE_ID overrides the path so the harness can drive the present,
# absent, empty and uninitialized cases against fixtures (repo precedent:
# RIG_MANIFEST / RIG_ROLE_MARKER below).
MID_FILE="${RIG_MACHINE_ID:-/etc/machine-id}"
ID_V=""
if [ ! -r "$MID_FILE" ]; then
# Never an empty string: an ID field that renders blank looks like a bug,
# and a missing file is a fact worth naming.
ID_V="unavailable (no $MID_FILE)"
else
# $(...) strips the trailing newline — that is part of the pinned derivation
# above, not an accident of shell.
MID="$(cat "$MID_FILE")"
if [ -z "$MID" ]; then
# NEVER a hash of nothing: hashing the empty string would hand every such
# machine the SAME id — the worst possible failure for an identity field.
# Images do ship the file empty (that is first-boot semantics per
# machine-id(5)), so this path is real, not defensive.
ID_V="unavailable ($MID_FILE is empty)"
elif [ "$MID" = "uninitialized" ]; then
# machine-id(5)'s other not-yet-set sentinel — same collision failure as
# empty if hashed, so same loud degradation.
ID_V="unavailable ($MID_FILE is uninitialized)"
else
MID_HASH="$(printf 'rig-machine-id:%s' "$MID" | sha256sum)"
MID_HASH="${MID_HASH%% *}"
ID_V="${MID_HASH:0:8}-${MID_HASH:8:4}-${MID_HASH:12:4}-${MID_HASH:16:4}-${MID_HASH:20:12}"
fi
fi
# --- OS --------------------------------------------------------------------- # --- OS ---------------------------------------------------------------------
# THE os-release TRAP: /etc/os-release defines VERSION, NAME and ID, so # THE os-release TRAP: /etc/os-release defines VERSION, NAME and ID, so
# sourcing it in the MAIN shell silently clobbers same-named script variables. # sourcing it in the MAIN shell silently clobbers same-named script variables.
@ -112,6 +174,7 @@ VIRT="$(systemd-detect-virt 2>/dev/null || true)"
printf '%s\n' "PLATFORM" printf '%s\n' "PLATFORM"
field HOSTNAME "$HOSTNAME_V" field HOSTNAME "$HOSTNAME_V"
field ID "$ID_V"
field OS "${OS:-unknown}" field OS "${OS:-unknown}"
field KERNEL "$KERNEL" field KERNEL "$KERNEL"
field CPU "${CPU_MODEL:-unknown}${CORES:+ ($CORES cores)}" field CPU "${CPU_MODEL:-unknown}${CORES:+ ($CORES cores)}"

View file

@ -1031,7 +1031,7 @@ check "platform: dispatches through bin/rig" 0 "PLATFORM" "$ROOT/bin/rig" platf
# The real run: exit 0 and every field present, as the running user. # The real run: exit 0 and every field present, as the running user.
check "platform: runs as this user, exit 0" 0 "PLATFORM" "$ROOT/bin/rig" platform check "platform: runs as this user, exit 0" 0 "PLATFORM" "$ROOT/bin/rig" platform
for f in HOSTNAME OS KERNEL CPU MEMORY DISK VIRT; do for f in HOSTNAME ID OS KERNEL CPU MEMORY DISK VIRT; do
check "platform: reports $f" 0 "$f" "$ROOT/bin/rig" platform check "platform: reports $f" 0 "$f" "$ROOT/bin/rig" platform
done done
# Not just the labels — the VALUES have to describe THIS machine. uname -r and # Not just the labels — the VALUES have to describe THIS machine. uname -r and
@ -1098,10 +1098,70 @@ check "platform: reads a manifest with no trailing newline" 0 "BOOTSTRAP 0.4.0,
printf 'role=dev class=human host=yes join=authkey\n' > "$PLATWORK/role" printf 'role=dev class=human host=yes join=authkey\n' > "$PLATWORK/role"
check "platform: renders the role marker's traits" 0 "dev (class=human host=yes join=authkey)" \ check "platform: renders the role marker's traits" 0 "dev (class=human host=yes join=authkey)" \
env RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/role" "$ROOT/bin/rig" platform env RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/role" "$ROOT/bin/rig" platform
# --- identity (#95): ID names the machine, HOSTNAME names the slot ----------
# Everything below drives RIG_MACHINE_ID fixtures, so the suite neither
# depends on nor leaks the machine-id of whatever box runs it.
# THE PINNED DERIVATION: sha256("rig-machine-id:<machine-id>") → first 32 hex
# rendered 8-4-4-4-12. The literal below is that digest computed OUTSIDE the
# implementation. This exact-match is what keeps every machine's identity
# stable: a refactor that changes the prefix, the hash or the slicing renames
# the whole fleet at once, and nothing but this line would notice.
# RIG_MANIFEST/RIG_ROLE_MARKER point at the absent fixture on purpose — this
# doubles as the unconverged-machine case: ID must render with no manifest
# and no role marker, because a minted-at-bootstrap id was #95's rejected
# Option B and pre-bootstrap usefulness is the property that rejected it.
printf '0123456789abcdef0123456789abcdef\n' > "$PLATWORK/machine-id"
check "platform: ID is the pinned derivation, manifest-free (#95)" 0 "ID cd9fb802-1493-2336-d027-7955f328bcd8" \
env RIG_MACHINE_ID="$PLATWORK/machine-id" RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform
# Determinism asserted, not assumed: two runs over the same input agree.
# (Reboot-stability follows — the id is a pure function of the file content.)
ID_A="$(env RIG_MACHINE_ID="$PLATWORK/machine-id" RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform | awk '$1=="ID" {print $2}')"
ID_B="$(env RIG_MACHINE_ID="$PLATWORK/machine-id" RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform | awk '$1=="ID" {print $2}')"
check "platform: ID is deterministic across runs" 0 "" test "$ID_A" = "$ID_B"
printf '%s\n' "$ID_A" > "$PLATWORK/idval"
check "platform: ID is UUID-shaped (8-4-4-4-12 hex)" 0 "" \
grep -qE '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' "$PLATWORK/idval"
# A different machine-id yields a different id — pinned exactly rather than
# asserted merely unequal, so a broken extraction cannot pass as "different".
printf 'ffffffffffffffffffffffffffffffff\n' > "$PLATWORK/machine-id-2"
check "platform: ID changes when the machine-id changes" 0 "ID 65441a65-bf82-8c75-b610-26e68a768bd3" \
env RIG_MACHINE_ID="$PLATWORK/machine-id-2" RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform
# THE CONFIDENTIALITY PROPERTY — the whole reason the derivation exists, and
# the one a future refactor is most likely to lose: the raw machine-id never
# appears anywhere in the output. machine-id(5) asks exactly this.
env RIG_MACHINE_ID="$PLATWORK/machine-id" RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" \
"$ROOT/bin/rig" platform > "$PLATWORK/platout" 2>&1
check "platform: the raw machine-id never appears in the output" 1 "" \
grep -qF '0123456789abcdef0123456789abcdef' "$PLATWORK/platout"
# An EMPTY machine-id must take the unavailable path, never be hashed:
# sha256("rig-machine-id:") renders as the literal below, and hashing nothing
# would hand every such machine the SAME id — the worst possible failure for
# an identity field. Images do ship the file empty (machine-id(5) first-boot
# semantics), so this is a real path, not a defensive one.
: > "$PLATWORK/machine-id-empty"
check "platform: an empty machine-id says why, exit 0" 0 "ID unavailable" \
env RIG_MACHINE_ID="$PLATWORK/machine-id-empty" RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform
env RIG_MACHINE_ID="$PLATWORK/machine-id-empty" RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" \
"$ROOT/bin/rig" platform > "$PLATWORK/platout-empty" 2>&1
check "platform: empty machine-id is never hashed (no collision id)" 1 "" \
grep -qF 'ddb56c2f-0df1-0ab0-1c12-371b1d32e34e' "$PLATWORK/platout-empty"
check "platform: empty machine-id — every other field still renders" 0 "HOSTNAME" \
env RIG_MACHINE_ID="$PLATWORK/machine-id-empty" RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform
# Missing file: same degradation, named reason, never an empty field.
check "platform: a missing machine-id says why, exit 0" 0 "ID unavailable (no " \
env RIG_MACHINE_ID="$PLATWORK/absent" RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform
# 'uninitialized' is machine-id(5)'s other not-yet-set sentinel — hashing it
# would collide every first-boot image exactly like the empty case.
printf 'uninitialized\n' > "$PLATWORK/machine-id-uninit"
check "platform: an 'uninitialized' machine-id is not hashed" 0 "ID unavailable ($PLATWORK/machine-id-uninit is uninitialized)" \
env RIG_MACHINE_ID="$PLATWORK/machine-id-uninit" RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform
# The defining property: it writes NOTHING. Not the manifest it just reported # The defining property: it writes NOTHING. Not the manifest it just reported
# missing, not the marker, not anything else in the fixture directory — the # missing, not the marker, not a cached id (#95's Option A stores nothing),
# whole design rests on this, so assert it rather than trust it. # not anything else in the fixture directory — the whole design rests on
env RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform >/dev/null 2>&1 # this, so assert it rather than trust it.
env RIG_MACHINE_ID="$PLATWORK/absent" RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform >/dev/null 2>&1
check "platform: writes nothing (no manifest created)" 1 "" test -e "$PLATWORK/absent" check "platform: writes nothing (no manifest created)" 1 "" test -e "$PLATWORK/absent"
rm -rf "$PLATWORK" rm -rf "$PLATWORK"