From 75ef38660150c7c3d0819ef447b1db972d35606b Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sun, 19 Jul 2026 23:35:58 +0000 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20rig=20platform=20=E2=80=94=20what?= =?UTF-8?q?=20is=20this=20machine,=20computed=20not=20stored?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rig read no hardware at all. The single exception was `uname -m` in runner-install.sh, used to pick a runner tarball and then discarded — so "is this the 32GB one, or the M900?" was a question you answered by logging in and running free -h, nproc, df -h and uname -r by hand, four commands deep, on a machine you were already unsure about. `rig platform` prints hostname, OS, kernel, CPU, memory, disk and virtualization, then a provenance block: which rig, when, and the role marker's traits. It COMPUTES rather than stores, and that is the design rather than an implementation detail. Specs change without rig doing anything — RAM added, root disk resized, the unattended-upgrades bootstrap itself enables patching the kernel — so a stored spec is stale the moment the machine changes, and refreshing one on every run would collide with bootstrap's "safe to re-run; a second run changes nothing" contract. Nothing is written, so nothing can go stale. The corollary is deliberate: reading only /proc, uname, /etc/os-release, df and systemd-detect-virt means no root, no network, and it runs on a pristine Debian box rig has never bootstrapped — useful for deciding what to converge a machine into, not only for auditing it afterwards. That also makes it the rare rig command the harness can RUN for real rather than grep: the tests assert the answer describes the actual test machine (kernel and hostname compared against independently computed values), and assert it writes nothing. Both known traps are handled explicitly. /etc/os-release is sourced in a SUBSHELL — it defines VERSION, NAME and ID and would otherwise clobber same-named script variables, the form every other site in this tree uses and test/cli.sh already greps for. systemd-detect-virt exits non-zero on bare metal while printing 'none', a normal answer that set -e would otherwise turn into a failed run, so it is wrapped in `|| true`. Provenance is read, never written, and degrades per file. /etc/rig/manifest is #61 and does not exist yet, so that line reads 'not bootstrapped' on every machine today; the command ships complete without it and neither blocks the other. Named `platform` and not `status`: `users status` and `runner status` cross-check recorded against live state and print DRIFT, and a command that records nothing cannot drift, so calling it status would borrow a promise it structurally cannot make. It also leaves `rig status` free for the machine-wide roll-up it will eventually want to be. Refs #64 --- CHANGELOG.md | 33 +++++++++ README.md | 77 +++++++++++++++++++ bin/rig | 13 ++++ commands/platform.sh | 173 +++++++++++++++++++++++++++++++++++++++++++ test/cli.sh | 50 +++++++++++++ 5 files changed, 346 insertions(+) create mode 100755 commands/platform.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index cc80cc6..6923726 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,39 @@ on the way to cutting its first release, and this file starts there. ## Unreleased +### Added + +- **`rig platform` — what is this machine, calculated at run time, stored + nowhere** (#64) — rig read no hardware at all; the single exception was + `uname -m` in `runner-install.sh`, used to pick a tarball and then + discarded. So "is this the 32GB one, or the M900?" was answered by logging + in and running `free -h`, `nproc`, `df -h` and `uname -r` by hand, four + commands deep, on a machine you were already unsure about. `rig platform` + prints hostname, OS, kernel, CPU, memory, disk and virtualization, then a + provenance block (which rig, when, and the role marker's traits). It + **computes rather than stores**: specs change without rig doing anything — + RAM added, root disk resized, the unattended-upgrades bootstrap itself + enables patching the kernel — so a stored spec is stale the moment the + machine changes, and refreshing one per run would collide with bootstrap's + "a second run changes nothing" contract. Nothing is written, so nothing can + go stale. The corollary is deliberate: reading only `/proc`, `uname`, + `/etc/os-release`, `df` and `systemd-detect-virt` means it needs no root, + makes no network call, and **runs on a pristine Debian box rig has never + bootstrapped** — useful for deciding what to converge a machine into, not + only for auditing it afterwards. That also makes it the rare rig command + the harness can RUN for real instead of grepping: the tests assert the + actual answer describes the actual test machine. Provenance is read, never + written, and degrades per-file — `/etc/rig/manifest` is #61 and does not + exist yet, so that line reads `not bootstrapped` on every machine today and + nothing else depends on it. Named `platform` and not `status` on purpose: + `users status` and `runner status` cross-check recorded against live state + and print `DRIFT`, and a command that records nothing cannot drift — which + also leaves `rig status` free for the machine-wide roll-up. Known + limitation, stated rather than guessed at: `CPU`/`MEMORY` are read from + `/proc` with no cgroup awareness, and whether an `lxc` guest sees its own + limits or the host's totals depends on whether `lxcfs` is in play — it is + unverified, so those two lines are unreliable there. + ### Changed - **BREAKING: `--class human|server` is now `--root-door closed|open`** (#77) — diff --git a/README.md b/README.md index 806c0ab..9743bda 100644 --- a/README.md +++ b/README.md @@ -719,6 +719,83 @@ default, dumps it, restores into a second container whose superuser differs, and asserts the rows and an ordered checksum survived — the same proof, done against throwaway containers on every push. +### `rig platform` + +```sh +rig platform +``` + +What is this machine — computed at run time, **stored nowhere**: + +``` +PLATFORM +HOSTNAME hetzner-cp-1 +OS Debian GNU/Linux 13 (trixie) +KERNEL 6.12.95+deb13-amd64 (x86_64) +CPU AMD Ryzen 7 3700X 8-Core Processor (16 cores) +MEMORY 31Gi total, 24Gi available +DISK 456Gi total, 201Gi free on / +VIRT kvm + +PROVENANCE +RIG 0.4.0, bootstrapped 2026-07-19T14:24:51Z +ROLE dev (class=human host=yes join=authkey) +``` + +"Is this the 32GB one, or the M900?" was previously a question you answered by +logging in and running `free -h`, `nproc`, `df -h` and `uname -r` by hand — +four commands deep, on a machine you were already unsure about. + +**Why this computes instead of storing.** It would be easy to write the specs +into a file at bootstrap. That is the wrong shape: specs change without rig +doing anything — someone adds RAM, resizes the root disk, or the +unattended-upgrades that bootstrap itself enables patches the kernel. A stored +spec is stale the moment the machine changes, and refreshing it on every run +would collide with bootstrap's contract that a second run changes nothing. +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. + +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 +written**: `RIG` comes from `/etc/rig/manifest` and `ROLE` from +`/etc/rig/role`. Neither file is required — a machine missing one reads `not +bootstrapped` for that line, which is itself the useful answer. The manifest +is #61 and is not implemented yet, so today that line reads `not bootstrapped` +on every machine; nothing else in the command depends on it. + +**Known limitation — `CPU` and `MEMORY` inside a container-style guest are +unverified.** `CPU` and `MEMORY` are read straight from `/proc/cpuinfo` and +`/proc/meminfo`, with no cgroup awareness. Inside a box-minted guest (`VIRT` +says `lxc`) it is **not currently established** whether those files report the +instance's configured limits or the host's totals: neither file is namespaced +by the kernel, but `lxcfs` — when the guest has it — overmounts both with +limit-aware versions, so the answer depends on the guest's setup rather than +on anything rig controls. Until someone confirms it against a real guest, +treat those two lines as unreliable on `lxc` machines and check the instance +config if the number matters. Everything else (OS, kernel, disk, virt, +provenance) is the guest's own either way. + +Deliberately not guessed at: cgroup-aware limit detection would be the fix if +the numbers do turn out to be the host's, but writing it against a *reasoned* +answer rather than an *observed* one risks correcting a bug that isn't there +and papering over one that is. + +Deliberately **not** here: NIC names, MAC addresses, PCI inventory, mount +tables, sensors — this is a cheatsheet, not `inxi`, and the bar is "what would +I want to know before I SSH in". Nor any health judgement ("disk nearly +full"): that needs thresholds this command has no business owning. It is +called `platform` and not `status` on purpose — `rig users status` and `rig +runner status` cross-check recorded state against live state and print +`DRIFT`, and this command records nothing, so it cannot drift and must not +borrow a promise it structurally cannot make. That leaves `rig status` free +for the machine-wide roll-up it will eventually want to be. + ### `rig runner install --repo ` Runner box only, run after `rig bootstrap runner-server` (the same two-step rhythm diff --git a/bin/rig b/bin/rig index c352918..a077496 100755 --- a/bin/rig +++ b/bin/rig @@ -54,6 +54,15 @@ commands: writes a gzipped SQL artifact (--no-owner --no-acl, so it restores onto a different instance); `restore` loads one back, connecting as the container's own superuser, behind a confirm gate. Run as root. + platform + What is this machine: hostname, 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 and stored NOWHERE — a + spec goes stale the moment someone adds RAM, so there is nothing to + go stale here. Writes nothing, needs no root, makes no network call, + and therefore also runs on a pristine Debian box rig has never + bootstrapped, where the provenance block reads 'not bootstrapped'. runner install --repo [options] GitHub Actions runner as a systemd service under an unprivileged user — outbound-only, no Docker. Prompts for the short-lived @@ -383,6 +392,10 @@ case "$cmd" in ;; esac ;; + platform) + shift + exec "$ROOT/commands/platform.sh" "$@" + ;; runner) shift sub="${1:-}" diff --git a/commands/platform.sh b/commands/platform.sh new file mode 100755 index 0000000..12ccdaa --- /dev/null +++ b/commands/platform.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +# 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 +# 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 +# refreshing one on every run would collide with bootstrap's convergence +# contract ("safe to re-run; a second run changes nothing"). +# +# The corollary is worth having deliberately: this runs on a machine rig has +# never converged, and needs no root. It answers "what should I converge this +# into?", not only "what did I converge this into?". +set -euo pipefail + +HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" +# shellcheck source=SCRIPTDIR/lib/users-config.sh +. "$HERE/lib/users-config.sh" # read_role_marker — one reader of /etc/rig/role + +die() { printf 'rig-platform: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; } + +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). + +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 +the provenance block reads 'not bootstrapped'. +EOF +} + +# --- args ------------------------------------------------------------------ +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) usage; exit 0 ;; + *) die "unknown flag: $1" 2 ;; + esac +done + +# One aligned column for every line, so the output diffs cleanly across a +# fleet and reads as one table rather than a log. +field() { printf '%-10s %s\n' "$1" "$2"; } + +# --- hostname --------------------------------------------------------------- +# uname -n is the coreutils fallback: `hostname` lives in its own package and a +# minimal image may not carry it, and this command's whole point is running +# before anything has been installed. +HOSTNAME_V="$(hostname 2>/dev/null || uname -n)" + +# --- 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. +# Every site in this tree sources it in a SUBSHELL instead (bootstrap.sh:305, +# bootstrap-tenant.sh:126, runner-install.sh:88, db.sh:52, +# coolify-backup-install.sh:88), and test/cli.sh greps commands/ to keep it +# that way. Follow the form verbatim. +if [ -r /etc/os-release ]; then + OS="$(. /etc/os-release && printf '%s %s' "${NAME:-}" "${VERSION:-${VERSION_ID:-}}")" +else + OS="" +fi + +# --- kernel ----------------------------------------------------------------- +KERNEL="$(uname -r) ($(uname -m))" + +# --- CPU -------------------------------------------------------------------- +# 'model name' is x86's spelling; arm64 /proc/cpuinfo has no such field, so an +# unnamed CPU still reports its core count rather than nothing at all. +CPU_MODEL="$(awk -F': ' '/^model name/ {print $2; exit}' /proc/cpuinfo 2>/dev/null || true)" +CORES="$(nproc 2>/dev/null || true)" + +# --- memory ----------------------------------------------------------------- +# /proc/meminfo is in kB. MemAvailable is the kernel's own estimate of what a +# new workload could claim (MemFree undercounts badly, reclaimable cache being +# most of a busy box's RAM); it predates every kernel rig targets, but degrade +# rather than print a wrong number if it is missing. +mem_kb() { awk -v k="$1" '$1 == k":" {print $2; exit}' /proc/meminfo 2>/dev/null || true; } +MEM_TOTAL_KB="$(mem_kb MemTotal)" +MEM_AVAIL_KB="$(mem_kb MemAvailable)" +human_kb() { # kB -> IEC, matching df's units below + [ -n "${1:-}" ] || { printf 'unknown'; return 0; } + numfmt --to=iec-i "$(( $1 * 1024 ))" 2>/dev/null || printf '%s kB' "$1" +} + +# --- disk ------------------------------------------------------------------- +# -P is the one-line-per-filesystem guarantee (a long device name otherwise +# wraps and breaks field positions); -B1 gives bytes, so numfmt renders the +# same IEC units as memory above instead of df's own bare 'G'. +DISK_TOTAL="" DISK_FREE="" +if DF="$(df -PB1 / 2>/dev/null)"; then + DISK_TOTAL="$(printf '%s\n' "$DF" | awk 'NR==2 {print $2}')" + DISK_FREE="$(printf '%s\n' "$DF" | awk 'NR==2 {print $4}')" +fi +human_b() { [ -n "${1:-}" ] && numfmt --to=iec-i "$1" 2>/dev/null || printf 'unknown'; } + +# --- virtualization --------------------------------------------------------- +# THE set -e TRAP: systemd-detect-virt exits NON-ZERO on bare metal while +# printing 'none'. That is a normal, correct answer — without the `|| true` a +# bare-metal machine would turn this whole command into a failed run. The +# substitution also swallows the binary being absent entirely (a non-systemd +# box), which lands as 'unknown'. +VIRT="$(systemd-detect-virt 2>/dev/null || true)" + +printf '%s\n' "PLATFORM" +field HOSTNAME "$HOSTNAME_V" +field OS "${OS:-unknown}" +field KERNEL "$KERNEL" +field CPU "${CPU_MODEL:-unknown}${CORES:+ ($CORES cores)}" +field MEMORY "$(human_kb "$MEM_TOTAL_KB") total, $(human_kb "$MEM_AVAIL_KB") available" +field DISK "$(human_b "$DISK_TOTAL") total, $(human_b "$DISK_FREE") free on /" +field VIRT "${VIRT:-unknown}" +echo + +# --- provenance: READ, never written ---------------------------------------- +# The complementary half of the answer — which rig, and when — is decided +# rather than observed, so unlike everything above it IS stored. rig writes it +# during bootstrap; this command only ever reads it. +# +# /etc/rig/manifest is #61 and is NOT implemented yet, so on every machine in +# existence today this block reads 'not bootstrapped'. That degradation is the +# point: the two features are independent and neither blocks the other. The +# parse is the flat key=value shape the manifest is specified to use — the +# same jq-free shape /etc/rig/users and /etc/rig/role already use, parseable +# with `read` on a box that has no YAML parser. +# +# RIG_MANIFEST / RIG_ROLE_MARKER override the paths so the harness can drive +# both the present and the absent case against fixtures, non-root, without a +# real marker on the machine running the tests (repo precedent: the +# RIG_ROLE_MARKER gate in bin/rig, install.sh and users-close-root.sh). +MANIFEST="${RIG_MANIFEST:-/etc/rig/manifest}" +MARKER="${RIG_ROLE_MARKER:-/etc/rig/role}" + +manifest_field() { # $1 = key — empty when absent, unreadable or unset + local k v + [ -r "$MANIFEST" ] || return 0 + while IFS='=' read -r k v; do + [ "$k" = "$1" ] || continue + printf '%s\n' "$v" + return 0 + done < "$MANIFEST" + return 0 +} + +printf '%s\n' "PROVENANCE" +if [ -r "$MANIFEST" ]; then + RIG_VER="$(manifest_field version)" + RIG_WHEN="$(manifest_field bootstrapped)" + field RIG "${RIG_VER:-unknown}${RIG_WHEN:+, bootstrapped $RIG_WHEN}" +else + field RIG "not bootstrapped (no $MANIFEST)" +fi + +# The role marker is bootstrap's own line — 'role=dev class=human host=yes +# join=authkey' — printed as the role plus its traits. +MARKER_LINE="$(read_role_marker "$MARKER")" +if [ -n "$MARKER_LINE" ]; then + ROLE_NAME="" ROLE_TRAITS="" + for kv in $MARKER_LINE; do + case "$kv" in + role=*) ROLE_NAME="${kv#role=}" ;; + *) ROLE_TRAITS="${ROLE_TRAITS:+$ROLE_TRAITS }$kv" ;; + esac + done + field ROLE "${ROLE_NAME:-unknown}${ROLE_TRAITS:+ ($ROLE_TRAITS)}" +else + field ROLE "not bootstrapped (no $MARKER)" +fi diff --git a/test/cli.sh b/test/cli.sh index ec6c0f3..a05ecfb 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -977,6 +977,56 @@ else echo "skip: runner status/remove/repoint non-root refusals (running as root)" fi +# --------------------------------------------------------------------------- +# rig platform (#64). Unusually testable for this repo: it needs no root, no +# network and no fixtures, and it WRITES NOTHING — so unlike every other +# command here the harness can RUN it for real on the machine running the +# tests and assert on the actual answer, instead of proving arg-parse +# refusals and grepping the rest. +# --------------------------------------------------------------------------- +check "platform: --help exits 0" 0 "usage:" "$ROOT/commands/platform.sh" --help +check "platform: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/platform.sh" --nope +check "platform: dispatches through bin/rig" 0 "PLATFORM" "$ROOT/bin/rig" platform + +# 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 + 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 +# the hostname are the two the harness can independently compute and compare, +# which is what separates "it printed a table" from "it read the machine". +check "platform: KERNEL is this kernel" 0 "$(uname -r)" "$ROOT/bin/rig" platform +check "platform: HOSTNAME is this host" 0 "$(uname -n)" "$ROOT/bin/rig" platform +# MemAvailable/df rendered, not left as the 'unknown' fallback: a numfmt or +# /proc parse that silently broke would still print the labels above. +check "platform: MEMORY carries real numbers" 0 "total," "$ROOT/bin/rig" platform + +# Provenance degrades on a machine rig never converged — #61's manifest does +# not exist yet, so 'not bootstrapped' is the state of the world today and the +# command must ship complete without it. Both paths driven against fixtures. +PLATWORK="$(mktemp -d)" +printf 'version=9.9.9\nbootstrapped=2026-07-19T14:24:51Z\n' > "$PLATWORK/manifest" +check "platform: no manifest reads 'not bootstrapped'" 0 "RIG not bootstrapped" \ + env RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform +check "platform: no role marker reads 'not bootstrapped'" 0 "ROLE not bootstrapped" \ + env RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform +# A manifest that DOES exist is read, never written — the forward-compatible +# half, so #61 landing needs no change here. +check "platform: reads the manifest version" 0 "9.9.9" \ + env RIG_MANIFEST="$PLATWORK/manifest" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform +check "platform: reads the manifest timestamp" 0 "bootstrapped 2026-07-19T14:24:51Z" \ + env RIG_MANIFEST="$PLATWORK/manifest" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform +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 +# 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 +check "platform: writes nothing (no manifest created)" 1 "" test -e "$PLATWORK/absent" +rm -rf "$PLATWORK" + check "bare users shows usage, exit 2" 2 "usage:" "$ROOT/bin/rig" users check "users: bad subcommand exits 2" 2 "usage:" "$ROOT/bin/rig" users frobnicate -- 2.45.2 From 3cfbb8921c6e3afb642c337fe1cfb2a99145c06b Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Mon, 20 Jul 2026 00:00:57 +0000 Subject: [PATCH 2/5] fix: platform reads a manifest whose last line lacks a newline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `while IFS='=' read -r k v` drops an unterminated final line: read returns 1 at EOF even having filled k and v. A manifest ending `bootstrapped=...Z` with no trailing \n rendered `RIG 1.2.3` with the timestamp silently gone — the version read fine, so nothing looked wrong. Guarded with `|| [ -n "$k" ]`, the same shape parse_users_file already uses (lib/users-config.sh:47). #61's writer should not have to know whether this reader tolerates a missing newline. Also make human_b fall back like human_kb. With numfmt absent, memory degraded to a raw number while disk printed 'unknown' beside it, from data already in hand. Both found in review of #74. Refs #64 --- commands/platform.sh | 11 +++++++++-- test/cli.sh | 7 +++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/commands/platform.sh b/commands/platform.sh index 12ccdaa..2bdb999 100755 --- a/commands/platform.sh +++ b/commands/platform.sh @@ -97,7 +97,10 @@ if DF="$(df -PB1 / 2>/dev/null)"; then DISK_TOTAL="$(printf '%s\n' "$DF" | awk 'NR==2 {print $2}')" DISK_FREE="$(printf '%s\n' "$DF" | awk 'NR==2 {print $4}')" fi -human_b() { [ -n "${1:-}" ] && numfmt --to=iec-i "$1" 2>/dev/null || printf 'unknown'; } +human_b() { # bytes -> IEC; falls back like human_kb rather than to 'unknown' + [ -n "${1:-}" ] || { printf 'unknown'; return 0; } + numfmt --to=iec-i "$1" 2>/dev/null || printf '%s B' "$1" +} # --- virtualization --------------------------------------------------------- # THE set -e TRAP: systemd-detect-virt exits NON-ZERO on bare metal while @@ -139,7 +142,11 @@ MARKER="${RIG_ROLE_MARKER:-/etc/rig/role}" manifest_field() { # $1 = key — empty when absent, unreadable or unset local k v [ -r "$MANIFEST" ] || return 0 - while IFS='=' read -r k v; do + # `|| [ -n "$k" ]` so a manifest whose last line lacks a trailing newline + # still yields that line: read returns 1 at EOF even having filled k/v. + # Same guard parse_users_file uses (lib/users-config.sh:47) — #61's writer + # should not have to know whether this reader tolerates a missing \n. + while IFS='=' read -r k v || [ -n "$k" ]; do [ "$k" = "$1" ] || continue printf '%s\n' "$v" return 0 diff --git a/test/cli.sh b/test/cli.sh index a05ecfb..8e93a88 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -1017,6 +1017,13 @@ check "platform: reads the manifest version" 0 "9.9.9" \ env RIG_MANIFEST="$PLATWORK/manifest" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform check "platform: reads the manifest timestamp" 0 "bootstrapped 2026-07-19T14:24:51Z" \ env RIG_MANIFEST="$PLATWORK/manifest" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform +# ...including one whose last line has no trailing newline: `read` returns 1 at +# EOF even having filled the variables, so an unguarded loop drops that line +# silently — the timestamp would vanish while the version still rendered. #61's +# writer must not have to know this reader's tolerances (found in #74 review). +printf 'version=9.9.9\nbootstrapped=2026-07-19T14:24:51Z' > "$PLATWORK/manifest-nonl" +check "platform: reads a manifest with no trailing newline" 0 "bootstrapped 2026-07-19T14:24:51Z" \ + env RIG_MANIFEST="$PLATWORK/manifest-nonl" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform 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 -- 2.45.2 From 91a8905a7145dc2fbe17b35d8598bba60e12e694 Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Mon, 20 Jul 2026 00:22:01 +0000 Subject: [PATCH 3/5] fix: platform reads #61's actual manifest schema, not invented keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reader asked for `version` and `bootstrapped`. #61 specifies `schema`, `bootstrapped_by`/`bootstrapped_at` and `converged_by`/`converged_at` — so no writer would ever have produced the keys being read, and the day #61 landed this command would have rendered 'unknown' with the timestamp omitted, forever, with nothing to say why. Keyed to #61's spelling, with fixtures carrying that schema verbatim so the contract is pinned rather than assumed. Birth and latest are reported separately and neither is inferred from the other: under #61 rule 2 converged_* is written only when the version differs, so its absence is a legitimate state on a freshly bootstrapped box, printed 'not recorded' rather than backfilled from birth. A manifest whose schema this rig does not know is named as such instead of being half-read in silence. Found in review of #74. Refs #64 --- CHANGELOG.md | 7 +++++-- README.md | 22 ++++++++++++++++------ commands/platform.sh | 25 ++++++++++++++++++++++--- test/cli.sh | 32 +++++++++++++++++++++++++++----- 4 files changed, 70 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6923726..a7a2106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,8 +29,11 @@ on the way to cutting its first release, and this file starts there. the harness can RUN for real instead of grepping: the tests assert the actual answer describes the actual test machine. Provenance is read, never written, and degrades per-file — `/etc/rig/manifest` is #61 and does not - exist yet, so that line reads `not bootstrapped` on every machine today and - nothing else depends on it. Named `platform` and not `status` on purpose: + exist yet, so those lines read `not bootstrapped` on every machine today and + nothing else depends on it. The reader is keyed to #61's documented schema + (`schema`, `bootstrapped_by`/`_at`, `converged_by`/`_at`) and fixtures pin + that exact spelling, so the integration cannot land silently broken; birth + and latest stay separate rather than one being inferred from the other. Named `platform` and not `status` on purpose: `users status` and `runner status` cross-check recorded against live state and print `DRIFT`, and a command that records nothing cannot drift — which also leaves `rig status` free for the machine-wide roll-up. Known diff --git a/README.md b/README.md index 9743bda..0910a55 100644 --- a/README.md +++ b/README.md @@ -738,7 +738,8 @@ DISK 456Gi total, 201Gi free on / VIRT kvm PROVENANCE -RIG 0.4.0, bootstrapped 2026-07-19T14:24:51Z +CONVERGED 0.6.0, 2026-08-02T09:11:03Z +BOOTSTRAP 0.4.0, 2026-07-19T14:24:51Z ROLE dev (class=human host=yes join=authkey) ``` @@ -763,11 +764,20 @@ needs no root, makes no network call, and writes nothing, ever. 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 -written**: `RIG` comes from `/etc/rig/manifest` and `ROLE` from -`/etc/rig/role`. Neither file is required — a machine missing one reads `not -bootstrapped` for that line, which is itself the useful answer. The manifest -is #61 and is not implemented yet, so today that line reads `not bootstrapped` -on every machine; nothing else in the command depends on it. +written**: `CONVERGED`/`BOOTSTRAP` come from `/etc/rig/manifest` and `ROLE` +from `/etc/rig/role`. Neither file is required — a machine missing one reads +`not bootstrapped` for that line, which is itself the useful answer. The +manifest is #61 and is not implemented yet, so today those lines read `not +bootstrapped` on every machine; nothing else in the command depends on it. + +**The two dates are deliberately separate**, matching #61's schema: `BOOTSTRAP` +is birth (`bootstrapped_by`/`bootstrapped_at`, first-write-wins, pinned +forever) and `CONVERGED` is latest (`converged_by`/`converged_at`, updated only +when the converging version actually differs). That distinction is what answers +"is this machine still converged by a rig that predates the fix?" — so +`CONVERGED` reads `not recorded` rather than being backfilled from birth on a +box that has only ever been bootstrapped once. A manifest whose `schema=` this +rig does not know is named as such instead of being half-read in silence. **Known limitation — `CPU` and `MEMORY` inside a container-style guest are unverified.** `CPU` and `MEMORY` are read straight from `/proc/cpuinfo` and diff --git a/commands/platform.sh b/commands/platform.sh index 2bdb999..65c446d 100755 --- a/commands/platform.sh +++ b/commands/platform.sh @@ -155,10 +155,29 @@ manifest_field() { # $1 = key — empty when absent, unreadable or unset } printf '%s\n' "PROVENANCE" +# Keys are #61's documented schema verbatim — schema/bootstrapped_by/ +# bootstrapped_at/converged_by/converged_at — NOT invented ones. #61 keeps +# birth and latest deliberately separate ("is this machine converged by a rig +# that predates the fix?"), so both are reported and neither is inferred from +# the other: CONVERGED answers currency, BOOTSTRAPPED answers provenance. +# Every field degrades independently, so a partial manifest from a future +# schema still renders what it does carry. if [ -r "$MANIFEST" ]; then - RIG_VER="$(manifest_field version)" - RIG_WHEN="$(manifest_field bootstrapped)" - field RIG "${RIG_VER:-unknown}${RIG_WHEN:+, bootstrapped $RIG_WHEN}" + M_SCHEMA="$(manifest_field schema)" + B_BY="$(manifest_field bootstrapped_by)"; B_AT="$(manifest_field bootstrapped_at)" + C_BY="$(manifest_field converged_by)"; C_AT="$(manifest_field converged_at)" + # A manifest with no schema= line is pre-#61; say so rather than render blanks. + if [ -z "$M_SCHEMA$B_BY$B_AT$C_BY$C_AT" ]; then + field RIG "manifest present but carries no recognised fields ($MANIFEST)" + else + # 'not recorded' rather than 'unknown': under #61's rule 2 converged_* is + # written only when the version actually differs, so its absence is a + # legitimate state on a freshly bootstrapped box, not a lost value. + field CONVERGED "${C_BY:-not recorded}${C_AT:+, $C_AT}" + field BOOTSTRAP "${B_BY:-not recorded}${B_AT:+, $B_AT}" + [ -n "$M_SCHEMA" ] && [ "$M_SCHEMA" != "1" ] && \ + field NOTE "manifest schema=$M_SCHEMA is newer than this rig reads (expects 1)" + fi else field RIG "not bootstrapped (no $MANIFEST)" fi diff --git a/test/cli.sh b/test/cli.sh index 8e93a88..f9941a5 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -1006,23 +1006,45 @@ check "platform: MEMORY carries real numbers" 0 "total," "$ROOT/bin/rig" platfor # not exist yet, so 'not bootstrapped' is the state of the world today and the # command must ship complete without it. Both paths driven against fixtures. PLATWORK="$(mktemp -d)" -printf 'version=9.9.9\nbootstrapped=2026-07-19T14:24:51Z\n' > "$PLATWORK/manifest" +# THE INTEGRATION CONTRACT (#61, found in #74 review): these fixtures carry +# #61's documented schema VERBATIM — schema/bootstrapped_by/bootstrapped_at/ +# converged_by/converged_at. An earlier draft of this reader invented `version` +# and `bootstrapped`, which no writer would ever have produced: the command +# would have rendered 'unknown' forever the day #61 landed, and nothing here +# would have said so. Keep these keys in step with #61; that is the point. +printf 'schema=1\nbootstrapped_by=0.4.0\nbootstrapped_at=2026-07-19T14:24:51Z\nconverged_by=0.6.0\nconverged_at=2026-08-02T09:11:03Z\n' > "$PLATWORK/manifest" check "platform: no manifest reads 'not bootstrapped'" 0 "RIG not bootstrapped" \ env RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform check "platform: no role marker reads 'not bootstrapped'" 0 "ROLE not bootstrapped" \ env RIG_MANIFEST="$PLATWORK/absent" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform # A manifest that DOES exist is read, never written — the forward-compatible # half, so #61 landing needs no change here. -check "platform: reads the manifest version" 0 "9.9.9" \ +check "platform: reads #61's converged_by/at" 0 "CONVERGED 0.6.0, 2026-08-02T09:11:03Z" \ env RIG_MANIFEST="$PLATWORK/manifest" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform -check "platform: reads the manifest timestamp" 0 "bootstrapped 2026-07-19T14:24:51Z" \ +check "platform: reads #61's bootstrapped_by/at" 0 "BOOTSTRAP 0.4.0, 2026-07-19T14:24:51Z" \ env RIG_MANIFEST="$PLATWORK/manifest" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform +# Birth and latest stay SEPARATE: #61 rule 2 writes converged_* only when the +# version actually differs, so its absence on a freshly bootstrapped box is a +# legitimate state — it must not be silently backfilled from bootstrapped_*. +printf 'schema=1\nbootstrapped_by=0.4.0\nbootstrapped_at=2026-07-19T14:24:51Z\n' > "$PLATWORK/manifest-birth" +check "platform: unconverged box says so, never infers from birth" 0 "CONVERGED not recorded" \ + env RIG_MANIFEST="$PLATWORK/manifest-birth" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform +# A newer schema renders what it recognises and says the rest is unreadable, +# rather than pretending a partial read is the whole truth. +printf 'schema=2\nbootstrapped_by=9.9.9\nbootstrapped_at=2027-01-01T00:00:00Z\n' > "$PLATWORK/manifest-v2" +check "platform: a newer schema is named, not silently half-read" 0 "schema=2 is newer" \ + env RIG_MANIFEST="$PLATWORK/manifest-v2" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform +# A manifest carrying none of #61's keys is reported as such — the pre-#61 or +# corrupt case, distinct from both 'absent' and 'read fine'. +printf 'somethingelse=1\n' > "$PLATWORK/manifest-alien" +check "platform: an unrecognised manifest is not read as empty" 0 "no recognised fields" \ + env RIG_MANIFEST="$PLATWORK/manifest-alien" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform # ...including one whose last line has no trailing newline: `read` returns 1 at # EOF even having filled the variables, so an unguarded loop drops that line # silently — the timestamp would vanish while the version still rendered. #61's # writer must not have to know this reader's tolerances (found in #74 review). -printf 'version=9.9.9\nbootstrapped=2026-07-19T14:24:51Z' > "$PLATWORK/manifest-nonl" -check "platform: reads a manifest with no trailing newline" 0 "bootstrapped 2026-07-19T14:24:51Z" \ +printf 'schema=1\nbootstrapped_by=0.4.0\nbootstrapped_at=2026-07-19T14:24:51Z' > "$PLATWORK/manifest-nonl" +check "platform: reads a manifest with no trailing newline" 0 "BOOTSTRAP 0.4.0, 2026-07-19T14:24:51Z" \ env RIG_MANIFEST="$PLATWORK/manifest-nonl" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform 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)" \ -- 2.45.2 From d3577b99547cf0a3ad88855241f734cd5e2802e2 Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Mon, 20 Jul 2026 00:34:26 +0000 Subject: [PATCH 4/5] docs: a fresh bootstrap writes both pairs equal, not a missing converged_* The previous commit's test and README described a freshly bootstrapped machine as lacking converged_* and rendering 'not recorded'. That pins the wrong contract: #61 states plainly that "on a fresh machine both pairs are written with equal values", and rule 2 only suppresses converged_* churn on a later same-version re-run. So two identical lines mean bootstrapped-and-never-re-converged, and a manifest missing the pair is partial or hand-edited rather than fresh -- no writer produces it. The graceful degradation stays for that case, still without backfilling from birth, which would invent a convergence that never happened. The reader itself was already correct; this fixes the fixture and the prose that claimed otherwise. Found in review of #74. Refs #64 --- CHANGELOG.md | 5 ++++- README.md | 17 +++++++++++++---- test/cli.sh | 20 ++++++++++++++------ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7a2106..341e9c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,10 @@ on the way to cutting its first release, and this file starts there. nothing else depends on it. The reader is keyed to #61's documented schema (`schema`, `bootstrapped_by`/`_at`, `converged_by`/`_at`) and fixtures pin that exact spelling, so the integration cannot land silently broken; birth - and latest stay separate rather than one being inferred from the other. Named `platform` and not `status` on purpose: + and latest stay separate rather than one being inferred from the other. A + fresh machine writes both pairs equal, so two identical lines read as + "never re-converged"; a manifest missing the pair is partial rather than + fresh, and says so instead of backfilling from birth. Named `platform` and not `status` on purpose: `users status` and `runner status` cross-check recorded against live state and print `DRIFT`, and a command that records nothing cannot drift — which also leaves `rig status` free for the machine-wide roll-up. Known diff --git a/README.md b/README.md index 0910a55..1db81aa 100644 --- a/README.md +++ b/README.md @@ -774,10 +774,19 @@ bootstrapped` on every machine; nothing else in the command depends on it. is birth (`bootstrapped_by`/`bootstrapped_at`, first-write-wins, pinned forever) and `CONVERGED` is latest (`converged_by`/`converged_at`, updated only when the converging version actually differs). That distinction is what answers -"is this machine still converged by a rig that predates the fix?" — so -`CONVERGED` reads `not recorded` rather than being backfilled from birth on a -box that has only ever been bootstrapped once. A manifest whose `schema=` this -rig does not know is named as such instead of being half-read in silence. +"is this machine still converged by a rig that predates the fix?". + +A **fresh machine writes both pairs with equal values**, so two identical lines +mean "bootstrapped and never re-converged since" — not a missing record. A +later re-converge by a different rig moves `CONVERGED` and leaves `BOOTSTRAP` +untouched, which is the whole point of keeping them apart. + +`CONVERGED not recorded` therefore does **not** describe a freshly +bootstrapped box; no writer produces a manifest without the pair. It means the +file is partial or hand-edited, and the value is deliberately not backfilled +from `BOOTSTRAP` — inferring a convergence that never happened would be worse +than saying so. Likewise a manifest whose `schema=` this rig does not know is +named as such instead of being half-read in silence. **Known limitation — `CPU` and `MEMORY` inside a container-style guest are unverified.** `CPU` and `MEMORY` are read straight from `/proc/cpuinfo` and diff --git a/test/cli.sh b/test/cli.sh index f9941a5..fd54b61 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -1023,12 +1023,20 @@ check "platform: reads #61's converged_by/at" 0 "CONVERGED 0.6.0, 2026-08-02T09 env RIG_MANIFEST="$PLATWORK/manifest" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform check "platform: reads #61's bootstrapped_by/at" 0 "BOOTSTRAP 0.4.0, 2026-07-19T14:24:51Z" \ env RIG_MANIFEST="$PLATWORK/manifest" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform -# Birth and latest stay SEPARATE: #61 rule 2 writes converged_* only when the -# version actually differs, so its absence on a freshly bootstrapped box is a -# legitimate state — it must not be silently backfilled from bootstrapped_*. -printf 'schema=1\nbootstrapped_by=0.4.0\nbootstrapped_at=2026-07-19T14:24:51Z\n' > "$PLATWORK/manifest-birth" -check "platform: unconverged box says so, never infers from birth" 0 "CONVERGED not recorded" \ - env RIG_MANIFEST="$PLATWORK/manifest-birth" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform +# A FRESH bootstrap carries both pairs with EQUAL values — #61 is explicit +# ("On a fresh machine both pairs are written with equal values"); rule 2 only +# suppresses converged_* churn on a later same-version re-run. So equal dates +# are the never-re-converged case and must render as themselves, not be +# special-cased into looking unset. +printf 'schema=1\nbootstrapped_by=0.4.0\nbootstrapped_at=2026-07-19T14:24:51Z\nconverged_by=0.4.0\nconverged_at=2026-07-19T14:24:51Z\n' > "$PLATWORK/manifest-fresh" +check "platform: a fresh bootstrap shows both pairs equal (#61)" 0 "CONVERGED 0.4.0, 2026-07-19T14:24:51Z" \ + env RIG_MANIFEST="$PLATWORK/manifest-fresh" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform +# A manifest missing converged_* is therefore NOT a fresh box — no writer +# produces that — so it is partial or hand-edited. Degrade loudly rather than +# backfilling from birth, which would invent a convergence that never happened. +printf 'schema=1\nbootstrapped_by=0.4.0\nbootstrapped_at=2026-07-19T14:24:51Z\n' > "$PLATWORK/manifest-partial" +check "platform: a partial manifest says so, never infers from birth" 0 "CONVERGED not recorded" \ + env RIG_MANIFEST="$PLATWORK/manifest-partial" RIG_ROLE_MARKER="$PLATWORK/absent" "$ROOT/bin/rig" platform # A newer schema renders what it recognises and says the rest is unreadable, # rather than pretending a partial read is the whole truth. printf 'schema=2\nbootstrapped_by=9.9.9\nbootstrapped_at=2027-01-01T00:00:00Z\n' > "$PLATWORK/manifest-v2" -- 2.45.2 From e11247b467519814fef9ff5f0626a8c4360777ee Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Mon, 20 Jul 2026 12:41:30 +0000 Subject: [PATCH 5/5] docs: the provenance comment matches #61's pinned contract, not the old story Review found `commands/platform.sh` still telling future readers that a missing converged_* pair is a legitimate state on a freshly bootstrapped box. It is not, and the rest of this PR already says so: #61's writer records both pairs equally at bootstrap, so no writer produces that manifest -- its absence means partial or hand-edited. README and the fixtures were corrected in an earlier commit of this series; the comment beside the code was not, which left the one artifact a future change actually reads while editing this block still arguing the retired position. Also corrects two examples that went stale under this PR rather than in it. #76 and #77 merged while this was in review, so the marker example in the same file -- and the sample `rig platform` output in README -- still showed `role=dev class=human host=yes join=authkey`, a spelling bootstrap no longer writes. Now `role=dev-server root-door=closed host=yes join=authkey`, with a note that the renderer prints whatever fields a marker carries, so a pre-rename box still shows its own class= line unchanged. Comments and docs only; no behaviour change. Same defect class both times -- prose that contradicts the contract the tests pin -- which is why the sweep went wider than the one site reported. Co-Authored-By: Claude Opus 4.8 --- README.md | 2 +- commands/platform.sh | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1db81aa..a8b82a8 100644 --- a/README.md +++ b/README.md @@ -740,7 +740,7 @@ VIRT kvm PROVENANCE CONVERGED 0.6.0, 2026-08-02T09:11:03Z BOOTSTRAP 0.4.0, 2026-07-19T14:24:51Z -ROLE dev (class=human host=yes join=authkey) +ROLE dev-server (root-door=closed host=yes join=authkey) ``` "Is this the 32GB one, or the M900?" was previously a question you answered by diff --git a/commands/platform.sh b/commands/platform.sh index 65c446d..c72d11c 100755 --- a/commands/platform.sh +++ b/commands/platform.sh @@ -170,9 +170,12 @@ if [ -r "$MANIFEST" ]; then if [ -z "$M_SCHEMA$B_BY$B_AT$C_BY$C_AT" ]; then field RIG "manifest present but carries no recognised fields ($MANIFEST)" else - # 'not recorded' rather than 'unknown': under #61's rule 2 converged_* is - # written only when the version actually differs, so its absence is a - # legitimate state on a freshly bootstrapped box, not a lost value. + # 'not recorded' rather than 'unknown', and it does NOT describe a fresh + # box: #61's writer records both pairs equally at bootstrap, so no writer + # produces a manifest missing converged_* — its absence means the file is + # partial or hand-edited. Deliberately not backfilled from bootstrapped_*, + # because inferring a convergence that never happened is worse than saying + # the record is not there. README and the fixtures pin exactly this. field CONVERGED "${C_BY:-not recorded}${C_AT:+, $C_AT}" field BOOTSTRAP "${B_BY:-not recorded}${B_AT:+, $B_AT}" [ -n "$M_SCHEMA" ] && [ "$M_SCHEMA" != "1" ] && \ @@ -182,8 +185,11 @@ else field RIG "not bootstrapped (no $MANIFEST)" fi -# The role marker is bootstrap's own line — 'role=dev class=human host=yes -# join=authkey' — printed as the role plus its traits. +# The role marker is bootstrap's own line — 'role=dev-server +# root-door=closed host=yes join=authkey' — printed as the role plus its +# traits. The example tracks the CURRENT vocabulary (#76's -server/-box role +# suffixes, #77's root-door trait); this command renders whatever fields the +# marker carries, so a pre-rename box still prints its own class= line as-is. MARKER_LINE="$(read_role_marker "$MARKER")" if [ -n "$MARKER_LINE" ]; then ROLE_NAME="" ROLE_TRAITS="" -- 2.45.2