From f26ed519380ca74ea4a0af65b4c6f0ff508bc209 Mon Sep 17 00:00:00 2001 From: claude-bot-andresmgsl Date: Fri, 24 Jul 2026 01:59:45 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat(platform):=20ID=20names=20the=20machin?= =?UTF-8?q?e=20=E2=80=94=20a=20namespaced=20sha256=20of=20/etc/machine-id,?= =?UTF-8?q?=20computed=20at=20run=20time,=20stored=20nowhere=20(#95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Option A per triage's normalization on #95: derive, never mint. The derivation is pinned — sha256("rig-machine-id:"), first 32 hex as 8-4-4-4-12 — and the prefix is the contract that keeps the id uncorrelatable with other tools' derivations, per machine-id(5)'s own guidance not to expose the raw value. Missing, empty and 'uninitialized' machine-id files degrade loudly to an 'unavailable (reason)' line: hashing nothing would hand every such machine the same identity, the worst possible failure for an identity field. Co-Authored-By: Claude Fable 5 --- commands/platform.sh | 77 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/commands/platform.sh b/commands/platform.sh index c72d11c..8c17f52 100755 --- a/commands/platform.sh +++ b/commands/platform.sh @@ -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)}" From bd2918d0c512d67606a413c1383634e439fcaced Mon Sep 17 00:00:00 2001 From: claude-bot-andresmgsl Date: Fri, 24 Jul 2026 02:02:08 +0000 Subject: [PATCH 2/3] =?UTF-8?q?test(platform):=20the=20identity=20contract?= =?UTF-8?q?,=20pinned=20=E2=80=94=20derivation,=20determinism,=20shape,=20?= =?UTF-8?q?confidentiality,=20loud=20degradation=20(#95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both fixture digests are computed outside the implementation, so a refactor that changes the prefix, the hash or the slicing renames the whole fleet and fails here. The negative half is the spec's: empty and 'uninitialized' machine-ids must never be hashed (the collision id is asserted absent, not just the unavailable line present), and the raw machine-id must never appear in the output. Co-Authored-By: Claude Fable 5 --- test/cli.sh | 68 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/test/cli.sh b/test/cli.sh index 5c61692..f51a921 100644 --- a/test/cli.sh +++ b/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:") → 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" From 506b7506a5d2d6ab68c3f522b407808278556a4f Mon Sep 17 00:00:00 2001 From: claude-bot-andresmgsl Date: Fri, 24 Jul 2026 02:02:59 +0000 Subject: [PATCH 3/3] =?UTF-8?q?docs(platform):=20ID=20beside=20HOSTNAME=20?= =?UTF-8?q?=E2=80=94=20the=20slot=20vs=20the=20machine,=20the=20derivation?= =?UTF-8?q?,=20and=20the=20cloned-image=20caveat=20(#95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 1 + README.md | 32 ++++++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8d9b81..119bf88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/.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) diff --git a/README.md b/README.md index abe895a..0e0f029 100644 --- a/README.md +++ b/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:")`, 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