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:
commit
243deb1a4c
4 changed files with 163 additions and 15 deletions
|
|
@ -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)
|
||||
- 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)
|
||||
- The `changelog-armed` guard returns, version-keyed (#112, ceremony#13)
|
||||
- The `.ceremony/` doctrine mirror, verified by `docs-sync` on every PR (#112, ceremony#19)
|
||||
|
|
|
|||
32
README.md
32
README.md
|
|
@ -728,6 +728,7 @@ What is this machine — computed at run time, **stored nowhere**:
|
|||
```
|
||||
PLATFORM
|
||||
HOSTNAME hetzner-cp-1
|
||||
ID cd9fb802-1493-2336-d027-7955f328bcd8
|
||||
OS Debian GNU/Linux 13 (trixie)
|
||||
KERNEL 6.12.95+deb13-amd64 (x86_64)
|
||||
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.
|
||||
|
||||
The corollary is deliberate: **`rig platform` works on a machine rig has never
|
||||
converged.** It reads only `/proc`, `uname`, `/etc/os-release`, `df` and
|
||||
`systemd-detect-virt`, so it runs on bare Debian before bootstrap — useful for
|
||||
deciding *what to converge this into*, not just for auditing afterwards. It
|
||||
needs no root, makes no network call, and writes nothing, ever.
|
||||
converged.** It reads only `/proc`, `uname`, `/etc/os-release`,
|
||||
`/etc/machine-id`, `df` and `systemd-detect-virt`, so it runs on bare Debian
|
||||
before bootstrap — useful for deciding *what to converge this into*, not just
|
||||
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
|
||||
is *decided* rather than observed, so it is stored. It is **read, never
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
# rig platform — what is this machine? Calculated at run time, stored nowhere.
|
||||
#
|
||||
# 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
|
||||
# anything (RAM added, root disk resized, unattended-upgrades patching the
|
||||
# kernel), so a stored spec is stale the moment the machine changes, and
|
||||
|
|
@ -24,13 +25,19 @@ usage() {
|
|||
cat <<'EOF'
|
||||
usage: rig platform
|
||||
|
||||
Describes the machine you are on: hostname, OS, kernel, CPU, memory, disk
|
||||
and virtualization, then rig's own provenance (which rig, when, and the role
|
||||
marker bootstrap wrote).
|
||||
Describes the machine you are on: hostname, a stable machine ID, OS, kernel,
|
||||
CPU, memory, disk and virtualization, then rig's own provenance (which rig,
|
||||
when, and the role marker bootstrap wrote).
|
||||
|
||||
Computed at run time from /proc, uname, /etc/os-release, 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
|
||||
ID names the machine where HOSTNAME names the slot: it is derived from
|
||||
/etc/machine-id (a namespaced sha256, never the raw value, which machine-id(5)
|
||||
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'.
|
||||
EOF
|
||||
}
|
||||
|
|
@ -53,6 +60,61 @@ field() { printf '%-10s %s\n' "$1" "$2"; }
|
|||
# before anything has been installed.
|
||||
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 ---------------------------------------------------------------------
|
||||
# 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.
|
||||
|
|
@ -112,6 +174,7 @@ VIRT="$(systemd-detect-virt 2>/dev/null || true)"
|
|||
|
||||
printf '%s\n' "PLATFORM"
|
||||
field HOSTNAME "$HOSTNAME_V"
|
||||
field ID "$ID_V"
|
||||
field OS "${OS:-unknown}"
|
||||
field KERNEL "$KERNEL"
|
||||
field CPU "${CPU_MODEL:-unknown}${CORES:+ ($CORES cores)}"
|
||||
|
|
|
|||
68
test/cli.sh
68
test/cli.sh
|
|
@ -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.
|
||||
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
|
||||
done
|
||||
# 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"
|
||||
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
|
||||
|
||||
# --- 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
|
||||
# missing, not the marker, not anything else in the fixture directory — the
|
||||
# whole design rests on this, so assert it rather than trust it.
|
||||
env RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform >/dev/null 2>&1
|
||||
# missing, not the marker, not a cached id (#95's Option A stores nothing),
|
||||
# not anything else in the fixture directory — the whole design rests on
|
||||
# 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"
|
||||
rm -rf "$PLATWORK"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue