feat: /etc/rig/manifest — which rig converged this machine, and when
A rig-managed machine recorded nothing about its own provenance. The entire durable output of a bootstrap run was one line in /etc/rig/role, and that line says what the box IS, never what built it. VERSION was read in exactly one place (bin/rig:9, for --version) and reports the currently INSTALLED tree, not the one that ran; there was no timestamp anywhere in the codebase. bootstrap now stamps a second file beside the marker: schema=1, a birth pair (bootstrapped_by/_at, pinned forever) and a latest pair (converged_by/_at). key=value, one per line, 0644 — the one file that must stay readable on the most broken machine in the fleet, where there is no YAML parser and no jq. `rig manifest [<key>]` reads it back. Only DECIDED facts go in, which is what keeps bootstrap.sh:3's convergence contract intact: bootstrapped_* is first-write-wins, and converged_* updates only when the version actually differs — it is the time the converging version last changed, not the time of the last run. The renderer is pure, so a re-run by the same rig is byte-identical no matter where the clock is, and the cmp-guard stays silent. OBSERVED facts (cores, RAM, disk, kernel) stay out: they go stale on their own and belong to `rig platform` (#64). /etc/rig/role is untouched. Closes #61 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d6e8cdda06
commit
5c40e906a1
8 changed files with 644 additions and 0 deletions
31
CHANGELOG.md
31
CHANGELOG.md
|
|
@ -45,6 +45,37 @@ on the way to cutting its first release, and this file starts there.
|
|||
limits or the host's totals depends on whether `lxcfs` is in play — it is
|
||||
unverified, so those two lines are unreliable there.
|
||||
|
||||
- **`/etc/rig/manifest` records which rig converged a machine, and when**
|
||||
(#61) — the entire durable output of a bootstrap run was one line in
|
||||
`/etc/rig/role`, and that line is about what the box *is*, never about what
|
||||
built it. `VERSION` was read in exactly one place (`bin/rig:9`, for
|
||||
`--version`) and that reports the *currently installed* tree, not the one
|
||||
that ran; there was no timestamp anywhere in the codebase. SSH into a
|
||||
control plane six months on and a machine converged by `0.1.0-dev` was
|
||||
indistinguishable from one converged by `0.4.0`. Bootstrap — both the
|
||||
machine roles and the box tenant roles — now stamps a second file beside
|
||||
the marker: `schema=1`, `bootstrapped_by`/`bootstrapped_at` (the rig that
|
||||
*first* converged this machine, pinned forever) and
|
||||
`converged_by`/`converged_at` (the newest rig to have converged it), read
|
||||
back with a new `rig manifest [<key>]`. `key=value`, one per line, `0644` —
|
||||
never JSON or YAML, because this is the one file that must stay readable on
|
||||
the most broken machine in the fleet and a rig-bootstrapped box has no YAML
|
||||
parser and no `jq`.
|
||||
|
||||
Only **decided** facts go in, which is what keeps `bootstrap.sh:3`'s
|
||||
contract ("a second run changes nothing", enforced by cmp-guards at nine
|
||||
sites) intact: `bootstrapped_*` is first-write-wins, and `converged_*`
|
||||
updates **only when the version actually differs** — it is the time the
|
||||
converging version last changed, not the time of the last run. A naive
|
||||
timestamp would have made every re-run a diff and had rig report a change it
|
||||
did not make. **Observed** facts — cores, RAM, disk, kernel — are
|
||||
deliberately absent: they go stale without rig doing anything, so they
|
||||
belong to `rig platform` (#64), which computes them fresh and stores
|
||||
nothing. `/etc/rig/role` is untouched — the marker holds traits and has six
|
||||
readers; the manifest holds provenance. Readers must ignore keys they do not
|
||||
know, and the writer preserves lines it does not own, so a manifest written
|
||||
by a newer rig stays readable to (and survives a rewrite by) an older one.
|
||||
|
||||
### Changed
|
||||
|
||||
- **BREAKING: `--class human|server` is now `--root-door closed|open`** (#77) —
|
||||
|
|
|
|||
62
README.md
62
README.md
|
|
@ -247,6 +247,13 @@ the marker later (`rig users` keys root policy off `root-door=`). Written
|
|||
post-join and cmp-guarded, so a marker never describes a box that failed to
|
||||
become what it claims.
|
||||
|
||||
Immediately after it, bootstrap stamps `/etc/rig/manifest` — **provenance**:
|
||||
which rig converged this box and when (see [`rig
|
||||
manifest`](#rig-manifest)). Same discipline, same guarantee, and the two files
|
||||
stay consistent because they land together. The marker says what the box *is*;
|
||||
the manifest says what *built* it. The tenant roles stamp it too — a
|
||||
box-minted guest is a machine rig converged.
|
||||
|
||||
**`join=login` inverts the tag assertion.** A workstation joins as a
|
||||
user-owned device: there is no pre-auth key — a set `TS_AUTHKEY` is a loud
|
||||
usage error (exit 2; unset it, or pass `--join authkey`) — `tailscale up`
|
||||
|
|
@ -1109,6 +1116,61 @@ them back, fresh keys and all. And the sudoers rules land in
|
|||
bad file under `/etc/sudoers.d` can take down *all* of sudo, locking every
|
||||
admin out of the very escalation path apply just granted.
|
||||
|
||||
### `rig manifest`
|
||||
|
||||
```sh
|
||||
rig manifest # the whole provenance record
|
||||
rig manifest converged_by # one value, for a shell caller
|
||||
```
|
||||
|
||||
Prints `/etc/rig/manifest` — **which rig converged this machine, and when**.
|
||||
`rig bootstrap` writes it as its last durable act, beside the role marker;
|
||||
this command only reads, needs no root (the file is `0644`), and works on a
|
||||
machine whose rig has since been upgraded or removed.
|
||||
|
||||
```
|
||||
schema=1
|
||||
bootstrapped_by=0.4.0
|
||||
bootstrapped_at=2026-07-19T14:24:51Z
|
||||
converged_by=0.6.0
|
||||
converged_at=2026-08-02T09:11:03Z
|
||||
```
|
||||
|
||||
Two pairs: **birth** — the rig that *first* converged this machine, pinned
|
||||
forever — and **latest** — the newest rig to have converged it. On a fresh
|
||||
machine they are equal. The version recorded is the one that **ran**, captured
|
||||
at run time; `rig --version` reports the tree installed *now*, which after an
|
||||
upgrade answers a different question, because a machine outlives the rig that
|
||||
built it.
|
||||
|
||||
Only **decided** facts live here, and that is what keeps bootstrap's
|
||||
convergence contract intact. `bootstrapped_*` is first-write-wins;
|
||||
`converged_*` moves **only when the version actually differs** — it records
|
||||
the time the converging version last changed, not the time of the last run. A
|
||||
re-run by the same rig therefore renders a byte-identical file and the
|
||||
cmp-guard stays silent; a re-converge by a *different* rig is a real change and
|
||||
the guard firing there is correct.
|
||||
|
||||
**Observed** facts — cores, RAM, disk, kernel — are deliberately absent: they
|
||||
go stale on their own (someone adds RAM; unattended-upgrades patches the
|
||||
kernel), so storing them would either lie or force a rewrite on every run. They
|
||||
belong to `rig platform`, which computes them fresh and stores nothing.
|
||||
|
||||
`key=value`, one per line — never JSON, never YAML. This is the one file that
|
||||
must stay readable on the most broken machine in the fleet, and a
|
||||
rig-bootstrapped box has no YAML parser and no `jq`. Readers must ignore keys
|
||||
they do not know, so `schema=` is bumped only when a key is removed or
|
||||
repurposed; a manifest written by a newer rig stays readable to an older one,
|
||||
and the writer preserves lines it does not own rather than eating them. Nothing
|
||||
here is ever a credential.
|
||||
|
||||
Exits 1 when there is no manifest — a machine converged before rig wrote one,
|
||||
or never converged at all. `RIG_MANIFEST` overrides the path.
|
||||
|
||||
The manifest does **not** replace `/etc/rig/role`. The marker holds *traits*
|
||||
(what this box is) and has six readers; the manifest holds *provenance* (what
|
||||
built it). Two files, two jobs.
|
||||
|
||||
### `rig users status`
|
||||
|
||||
```sh
|
||||
|
|
|
|||
11
bin/rig
11
bin/rig
|
|
@ -91,6 +91,13 @@ commands:
|
|||
Shut root SSH on a class=human box once an admin key works. Refuses
|
||||
on class=server — root there is the control plane's automation door —
|
||||
and while no admin holds a key. Run as root.
|
||||
manifest [<key>]
|
||||
Print /etc/rig/manifest — which rig converged this machine and when
|
||||
(bootstrapped_by/_at pin the FIRST convergence forever; converged_by/_at
|
||||
name the latest). With a key, print that value alone for shell callers.
|
||||
Written by 'rig bootstrap' only; read-only here, and no root needed.
|
||||
Provenance is DECIDED, so it is stored; the machine's specs are
|
||||
OBSERVED, so they are not — those are 'rig platform'.
|
||||
versions
|
||||
List the installed rig versions — install.sh lands each one side by
|
||||
side at <root>/versions/<v>, a 'current' symlink tracks the default
|
||||
|
|
@ -444,6 +451,10 @@ case "$cmd" in
|
|||
;;
|
||||
esac
|
||||
;;
|
||||
manifest)
|
||||
shift
|
||||
exec "$ROOT/commands/manifest.sh" "$@"
|
||||
;;
|
||||
versions)
|
||||
shift
|
||||
cmd_versions "$@"
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
|||
. "$HERE/lib/users-config.sh" # read_role_marker / root_door_of
|
||||
# shellcheck source=SCRIPTDIR/lib/sshd.sh
|
||||
. "$HERE/lib/sshd.sh" # harden_sshd (the staging-box tenant)
|
||||
# shellcheck source=SCRIPTDIR/lib/manifest.sh
|
||||
. "$HERE/lib/manifest.sh" # manifest_stamp — provenance, written beside the marker
|
||||
|
||||
log() { printf 'rig-bootstrap: %s\n' "$*"; }
|
||||
warn() { printf 'rig-bootstrap: WARNING: %s\n' "$*" >&2; }
|
||||
|
|
@ -405,6 +407,31 @@ else
|
|||
log "machine role marker present (${EXISTING_MARKER}); leaving it alone"
|
||||
fi
|
||||
|
||||
# --- provenance manifest ------------------------------------------------------
|
||||
# A tenant gets a manifest, in the SAME /etc/rig/manifest, through the same
|
||||
# writer — and UNCONDITIONALLY, outside the marker gate above (#61's open
|
||||
# question, answered here).
|
||||
#
|
||||
# The reason the marker needs that gate is that it holds TRAITS, and a guest's
|
||||
# traits and the traits it earns after an operator-run `rig bootstrap workload`
|
||||
# join are two different, competing statements about one box — so the marker
|
||||
# has to pick, and it picks the truer one. Provenance has no such conflict.
|
||||
# "Which rig converged this guest, and when" is a fact whichever bootstrap ran,
|
||||
# and the two-pair shape composes across them exactly as designed: a staging
|
||||
# guest later joined as a workload keeps the TENANT bootstrap as its birth —
|
||||
# that genuinely is when this machine was first converged — and the machine
|
||||
# bootstrap moves converged_* forward. Skipping the write on a joined guest
|
||||
# would lose the birth stamp that only this run knows.
|
||||
#
|
||||
# One file, not a tenant-shaped second one: the manifest answers a question
|
||||
# about the MACHINE, and a guest is a machine. The marker already carries
|
||||
# `tenant=yes` for anyone who needs to know which kind.
|
||||
if manifest_stamp "$(manifest_running_version "$HERE/..")"; then
|
||||
log "provenance manifest written: $(manifest_path)"
|
||||
else
|
||||
log "provenance manifest already current"
|
||||
fi
|
||||
|
||||
log "done — tenant ${ROLE}, user ${TENANT_USER}"
|
||||
if [ "$ROLE" = "staging-box" ]; then
|
||||
log "next (operator-run, holds a credential): box shell → sudo rig bootstrap workload-server --hostname <name> with a tagged pre-auth key"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
|||
. "$HERE/lib/sshd.sh" # harden_sshd — shared with the staging tenant
|
||||
# shellcheck source=SCRIPTDIR/lib/users-config.sh
|
||||
. "$HERE/lib/users-config.sh" # parse_users_file — the --users PRE-FLIGHT only
|
||||
# shellcheck source=SCRIPTDIR/lib/manifest.sh
|
||||
. "$HERE/lib/manifest.sh" # manifest_stamp — provenance, written beside the marker
|
||||
# The users lib is sourced for validation, never for convergence: `users apply`
|
||||
# stays the single owner of what a users file DOES to a box (#51). Bootstrap
|
||||
# borrows the parser so a typo'd users file is caught in the same breath as a
|
||||
|
|
@ -619,6 +621,31 @@ else
|
|||
fi
|
||||
rm -f "$MARKER_TMP"
|
||||
|
||||
# --- provenance manifest ------------------------------------------------------
|
||||
# /etc/rig/manifest records WHICH rig converged this machine and WHEN (#61).
|
||||
# The marker above says what the box IS; this says what BUILT it — two files,
|
||||
# two jobs, and the marker is deliberately untouched (it has six readers,
|
||||
# install.sh:82-90 among them).
|
||||
#
|
||||
# Placed HERE, immediately after the marker, so the two agree by construction
|
||||
# and both inherit the marker's discipline verbatim: written only AFTER the tag
|
||||
# verification, so neither ever describes a box that failed to become what it
|
||||
# claims. A manifest that survives a failed run is worse than no manifest — it
|
||||
# is a confident wrong answer. It deliberately does NOT trail the box install
|
||||
# and the users phase below: those are the host EXTRA and the operator phase,
|
||||
# and a box whose people failed to converge was still converged BY this rig at
|
||||
# this time. Stamping provenance is not a claim that everything after it
|
||||
# succeeded — the marker beside it makes exactly the same claim, and the two
|
||||
# landing together is what keeps them readable as one statement.
|
||||
#
|
||||
# The version stamped is the one that RAN, captured now — not what `rig
|
||||
# --version` would answer after a later upgrade.
|
||||
if manifest_stamp "$(manifest_running_version "$HERE/..")"; then
|
||||
log "provenance manifest written: $(manifest_path)"
|
||||
else
|
||||
log "provenance manifest already current"
|
||||
fi
|
||||
|
||||
# --- box install (host=yes only) -------------------------------------------
|
||||
# A host=yes box exists to run guest boxes, so bootstrap finishes the job rather
|
||||
# than printing a to-do: it installs the box CLI globally and lets box's OWN
|
||||
|
|
|
|||
185
commands/lib/manifest.sh
Normal file
185
commands/lib/manifest.sh
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/env bash
|
||||
# /etc/rig/manifest — PROVENANCE: which rig converged this machine, and when
|
||||
# (#61). Sourced by bootstrap.sh, bootstrap-tenant.sh and the test harness.
|
||||
#
|
||||
# The file is `key=value`, one per line — not JSON, not YAML. Same constraint
|
||||
# stated three times in the tree already (lib/users-config.sh:6-12,
|
||||
# lib/runner-config.sh:6 and :24): a rig-bootstrapped box has no YAML parser
|
||||
# and no jq, which is why json_field() is grep-and-sed. This is the one file
|
||||
# that must stay readable on the most broken machine in the fleet, so `read`
|
||||
# parses it for free.
|
||||
#
|
||||
# WHAT GOES IN HERE: facts that are DECIDED. Which rig ran, and when it ran.
|
||||
# Facts that are OBSERVED — cores, RAM, disk, kernel — belong to `rig platform`
|
||||
# (#64), which computes them fresh and stores nothing. That split is not
|
||||
# tidiness: a stored spec goes stale on its own (someone adds RAM; the
|
||||
# unattended-upgrades bootstrap.sh itself enables patches the kernel), and
|
||||
# refreshing it on every run collides head-on with bootstrap.sh:3 —
|
||||
# "Convergent: safe to re-run; a second run changes nothing." Keeping only
|
||||
# immutable content removes that problem instead of managing it.
|
||||
#
|
||||
# NEVER A CREDENTIAL. This file is 0644 and world-readable by design — it is
|
||||
# an audit record, and an audit record nobody can read is not one. Later
|
||||
# commands may append their own provenance (runner_installed_at,
|
||||
# coolify_installed_at, box_version), subject to the same two rules: the EVENT
|
||||
# of installing something, never its current state, and never a secret. Same
|
||||
# law runner-install.sh:190 already states for `.rig-labels` — "box-local
|
||||
# metadata, never a credential."
|
||||
|
||||
# The schema version, an INTEGER, independent of the rig versions recorded in
|
||||
# the file. Bumped only when a key is REMOVED or REPURPOSED — adding a key is
|
||||
# not a bump, and readers must ignore keys they do not know, so a newer rig's
|
||||
# manifest stays readable to an older one. No `schema=` line means pre-manifest.
|
||||
MANIFEST_SCHEMA=1
|
||||
|
||||
# The five keys rig's provenance block owns. Everything else in the file is a
|
||||
# later command's line and is preserved verbatim (see manifest_foreign).
|
||||
MANIFEST_KEYS='schema bootstrapped_by bootstrapped_at converged_by converged_at'
|
||||
|
||||
# manifest_path — where the manifest lives. RIG_MANIFEST overrides it so tests
|
||||
# point at fixtures (repo precedent: RIG_ROLE_MARKER, bin/rig:148).
|
||||
manifest_path() {
|
||||
printf '%s' "${RIG_MANIFEST:-/etc/rig/manifest}"
|
||||
}
|
||||
|
||||
# manifest_value <path> <key> — the value of one key, or nothing when the file
|
||||
# or the key is absent. First occurrence wins. NO policy here: what a missing
|
||||
# key MEANS is each caller's call (this reader only reads — repo precedent:
|
||||
# read_role_marker).
|
||||
manifest_value() {
|
||||
[ -r "$1" ] || return 0
|
||||
local k v
|
||||
while IFS='=' read -r k v; do
|
||||
[ "$k" = "$2" ] || continue
|
||||
printf '%s' "$v"
|
||||
return 0
|
||||
done < "$1"
|
||||
}
|
||||
|
||||
# manifest_has <path> <key> — is the key PRESENT, regardless of its value.
|
||||
# Separate from manifest_value because "absent" and "present but empty" are
|
||||
# different answers and command substitution collapses both to the empty
|
||||
# string. Key comparison is a string equality, never a pattern: `rig manifest`
|
||||
# passes operator input straight in, and a key of `.*` must find nothing rather
|
||||
# than match the first line.
|
||||
manifest_has() {
|
||||
[ -r "$1" ] || return 1
|
||||
local k
|
||||
while IFS='=' read -r k _; do
|
||||
[ "$k" = "$2" ] && return 0
|
||||
done < "$1"
|
||||
return 1
|
||||
}
|
||||
|
||||
# manifest_foreign <path> — every line rig's provenance block does NOT own,
|
||||
# in file order. A newer rig (or a later command) may have written keys this
|
||||
# one has never heard of; rewriting the file must not eat them, or the schema's
|
||||
# "readers ignore keys they do not know" promise would only hold for readers
|
||||
# and not for the writer.
|
||||
manifest_foreign() {
|
||||
[ -r "$1" ] || return 0
|
||||
local line key
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] || continue
|
||||
key="${line%%=*}"
|
||||
case " $MANIFEST_KEYS " in
|
||||
*" $key "*) continue ;;
|
||||
esac
|
||||
printf '%s\n' "$line"
|
||||
done < "$1"
|
||||
}
|
||||
|
||||
# manifest_render <path> <version> <now> — the WHOLE convergence contract, as
|
||||
# a pure text→text function: existing file + running version + a clock reading
|
||||
# in, the file's desired content out. No side effects, so the harness proves
|
||||
# the rules non-root against fixtures (repo precedent: parse_users_file,
|
||||
# assert_marker_human).
|
||||
#
|
||||
# Purity is what makes convergence testable rather than asserted. The rendered
|
||||
# content is a function of (existing file, running version) ALONE — <now> is
|
||||
# consulted only on the paths that were going to change anyway — so calling
|
||||
# this twice with two DIFFERENT clock readings must produce byte-identical
|
||||
# output. test/cli.sh pins exactly that, which is stronger than re-running the
|
||||
# writer fast enough that the second matches by luck.
|
||||
#
|
||||
# Rule 1 — bootstrapped_* is FIRST-WRITE-WINS. Birth is pinned forever. If the
|
||||
# file already carries a bootstrapped_at, both birth fields are preserved
|
||||
# verbatim. Regenerating it as now() on every run would make every re-run a
|
||||
# diff, which is the exact trap that keeping specs out of this file closed.
|
||||
#
|
||||
# Rule 2 — converged_* updates ONLY when the version actually differs.
|
||||
# converged_at is "the time the converging version last changed", NOT the time
|
||||
# of the last run. If it tracked every run it would be a clock, and a clock in
|
||||
# a cmp-guarded file makes every re-run a fake change. So: compare the running
|
||||
# version against the recorded converged_by; equal means the pair is already
|
||||
# true and is copied through untouched.
|
||||
#
|
||||
# Under those two rules a re-run by the SAME rig renders byte-identical content
|
||||
# and the cmp-guard stays silent, while a re-converge by a DIFFERENT rig
|
||||
# renders a real diff — and the guard firing there is correct, not spurious. It
|
||||
# was only ever the clock that was the fake change, never the version.
|
||||
manifest_render() {
|
||||
local path="$1" ver="$2" now="$3"
|
||||
local b_by b_at c_by c_at
|
||||
b_by="$(manifest_value "$path" bootstrapped_by)"
|
||||
b_at="$(manifest_value "$path" bootstrapped_at)"
|
||||
c_by="$(manifest_value "$path" converged_by)"
|
||||
c_at="$(manifest_value "$path" converged_at)"
|
||||
|
||||
# Rule 1. bootstrapped_at is the field that decides, because it is the one
|
||||
# that can never be reconstructed: a machine's birth version can at least be
|
||||
# guessed at, its birth INSTANT cannot. So an existing at-stamp pins the
|
||||
# pair, and a birth-stamp with no birth version records `unknown` rather
|
||||
# than backfilling today's version as if it had always been there — a
|
||||
# manifest that lies about which rig built the box is worse than one that
|
||||
# admits it does not know.
|
||||
if [ -z "$b_at" ]; then
|
||||
b_by="$ver"; b_at="$now"
|
||||
elif [ -z "$b_by" ]; then
|
||||
b_by=unknown
|
||||
fi
|
||||
|
||||
# Rule 2. The empty-at case is a one-time repair of a truncated file, not a
|
||||
# clock: once written it satisfies the equality on every later run.
|
||||
if [ "$c_by" != "$ver" ] || [ -z "$c_at" ]; then
|
||||
c_by="$ver"; c_at="$now"
|
||||
fi
|
||||
|
||||
printf 'schema=%s\n' "$MANIFEST_SCHEMA"
|
||||
printf 'bootstrapped_by=%s\n' "$b_by"
|
||||
printf 'bootstrapped_at=%s\n' "$b_at"
|
||||
printf 'converged_by=%s\n' "$c_by"
|
||||
printf 'converged_at=%s\n' "$c_at"
|
||||
manifest_foreign "$path"
|
||||
}
|
||||
|
||||
# manifest_running_version <rig root> — the version that IS RUNNING, captured
|
||||
# at run time from the tree's own VERSION file. NOT `rig --version` read back
|
||||
# later: a machine outlives the rig that built it, so what is installed today
|
||||
# answers a different question than what converged it. First line only — a
|
||||
# stray second line would inject a bogus key into a key=value file.
|
||||
manifest_running_version() {
|
||||
local v=""
|
||||
[ -r "$1/VERSION" ] && v="$(head -n1 "$1/VERSION")"
|
||||
printf '%s' "${v:-unknown}"
|
||||
}
|
||||
|
||||
# manifest_stamp <version> — the writer. Renders, cmp-guards like every file
|
||||
# rig converges, installs 0644 beside the role marker and the users ledger.
|
||||
# Returns 0 when it CHANGED the file and 1 when the file was already current,
|
||||
# so the caller owns the log line (and, under set -e, must call it in an `if`).
|
||||
manifest_stamp() {
|
||||
local path tmp rc
|
||||
path="$(manifest_path)"
|
||||
tmp="$(mktemp)"
|
||||
manifest_render "$path" "$1" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$tmp"
|
||||
if cmp -s "$tmp" "$path" 2>/dev/null; then
|
||||
rc=1
|
||||
else
|
||||
mkdir -p "$(dirname "$path")"
|
||||
install -m 0644 "$tmp" "$path"
|
||||
rc=0
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
return "$rc"
|
||||
}
|
||||
88
commands/manifest.sh
Executable file
88
commands/manifest.sh
Executable file
|
|
@ -0,0 +1,88 @@
|
|||
#!/usr/bin/env bash
|
||||
# rig manifest — print this machine's provenance record, /etc/rig/manifest
|
||||
# (#61). Reads only: this command NEVER writes the file. `rig bootstrap` is its
|
||||
# single writer, and it stamps it as its own last durable act.
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
||||
# shellcheck source=SCRIPTDIR/lib/manifest.sh
|
||||
. "$HERE/lib/manifest.sh" # manifest_path / manifest_value
|
||||
|
||||
die() { printf 'rig-manifest: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
usage: rig manifest [<key>]
|
||||
|
||||
Print /etc/rig/manifest — which rig converged this machine, and when. With a
|
||||
key, print that key's value alone, unquoted and newline-terminated, so a shell
|
||||
caller does not re-parse the file:
|
||||
|
||||
rig manifest schema=1
|
||||
bootstrapped_by=0.2.0
|
||||
bootstrapped_at=2026-07-19T14:24:51Z
|
||||
converged_by=0.4.0
|
||||
converged_at=2026-08-02T09:11:03Z
|
||||
|
||||
rig manifest converged_by 0.4.0
|
||||
|
||||
Both pairs are provenance, both immutable in the sense that matters: BIRTH —
|
||||
the rig that first converged this machine, pinned forever — and LATEST — the
|
||||
newest rig to have converged it. On a fresh machine the two are equal. Ask the
|
||||
second pair "is this machine converged by something ancient?"; ask the first
|
||||
"what built it".
|
||||
|
||||
The version recorded is the one that RAN. `rig --version` reports the tree
|
||||
installed NOW, which after an upgrade is a different question — a machine
|
||||
outlives the rig that built it.
|
||||
|
||||
Reads only. Needs no root (the file is 0644), no network, and works on a
|
||||
machine whose rig has since been upgraded or removed. Specs — cores, RAM,
|
||||
disk, kernel — are NOT here: they are observed rather than decided, so they go
|
||||
stale on their own and belong to `rig platform`, which computes them fresh and
|
||||
stores nothing.
|
||||
|
||||
Exit 1 when there is no manifest — a machine converged before rig wrote one,
|
||||
or never converged at all.
|
||||
|
||||
RIG_MANIFEST override the path (default /etc/rig/manifest)
|
||||
EOF
|
||||
}
|
||||
|
||||
KEY=""
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-*)
|
||||
printf 'rig-manifest: unknown option: %s\n' "$a" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
if [ -n "$KEY" ]; then
|
||||
printf 'rig-manifest: manifest takes at most one key\n' >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
KEY="$a"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
MPATH="$(manifest_path)"
|
||||
[ -r "$MPATH" ] || die "no manifest at $MPATH — this machine has not been converged by a rig that writes one (rig bootstrap writes it)"
|
||||
|
||||
if [ -z "$KEY" ]; then
|
||||
cat "$MPATH"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# A key that is absent and a key whose value is empty are different answers to
|
||||
# a shell caller, and $(...) collapses both to "". So the ABSENCE is the exit
|
||||
# code, and only a present key ever prints — a caller reading `rig manifest
|
||||
# converged_by` into a variable can trust that an empty result it accepted was
|
||||
# a real empty value, not a missing key.
|
||||
manifest_has "$MPATH" "$KEY" \
|
||||
|| die "no such key: $KEY (keys present: $(cut -d= -f1 "$MPATH" | tr '\n' ' '))"
|
||||
manifest_value "$MPATH" "$KEY"
|
||||
echo
|
||||
213
test/cli.sh
213
test/cli.sh
|
|
@ -1907,6 +1907,219 @@ rm -f "$DUMP_TMP"
|
|||
check "no main-shell os-release sourcing" 1 "" \
|
||||
grep -rnE '^[[:space:]]*\.[[:space:]]+/etc/os-release' "$ROOT/commands"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /etc/rig/manifest — provenance (#61)
|
||||
#
|
||||
# The writer is a pure text→text renderer behind a cmp-guard, for the same
|
||||
# reason assert_marker_human and parse_users_file are pure: the write itself
|
||||
# sits behind bootstrap's root check, so every RULE is proven here, non-root,
|
||||
# against fixtures — and the rules are the whole feature.
|
||||
# ---------------------------------------------------------------------------
|
||||
# Every helper sources the lib in a SUBSHELL, so the harness's own $PASS/$FAIL
|
||||
# and the lib's globals never meet (repo precedent: the bash -c gates above,
|
||||
# same isolation, one less quoting layer).
|
||||
render() { # render <path> <version> <now>
|
||||
( set -euo pipefail
|
||||
. "$ROOT/commands/lib/manifest.sh"
|
||||
manifest_render "$1" "$2" "$3" )
|
||||
}
|
||||
stamp() { # stamp <path> <version> — the side-effecting writer
|
||||
( set -euo pipefail
|
||||
. "$ROOT/commands/lib/manifest.sh"
|
||||
RIG_MANIFEST="$1" manifest_stamp "$2" )
|
||||
}
|
||||
running_version() { # running_version <rig tree root>
|
||||
( set -euo pipefail
|
||||
. "$ROOT/commands/lib/manifest.sh"
|
||||
manifest_running_version "$1" )
|
||||
}
|
||||
mode_of() { stat -c %a "$1"; }
|
||||
mtime_of() { stat -c %Y "$1"; }
|
||||
text_is() { [ "$(cat "$1")" = "$2" ]; }
|
||||
MF="$(mktemp -d)"
|
||||
|
||||
# -- the shape on a virgin machine ------------------------------------------
|
||||
check "manifest: a fresh render carries schema=1" 0 "schema=1" \
|
||||
render "$MF/absent" 0.4.0 2026-07-19T14:24:51Z
|
||||
check "manifest: a fresh render pins birth to the running version" 0 "bootstrapped_by=0.4.0" \
|
||||
render "$MF/absent" 0.4.0 2026-07-19T14:24:51Z
|
||||
check "manifest: a fresh render stamps birth with now" 0 "bootstrapped_at=2026-07-19T14:24:51Z" \
|
||||
render "$MF/absent" 0.4.0 2026-07-19T14:24:51Z
|
||||
# Both pairs equal on a fresh machine: mild redundancy, in exchange for a file
|
||||
# no reader ever has to infer a missing field from.
|
||||
check "manifest: a fresh render writes latest equal to birth" 0 "converged_by=0.4.0" \
|
||||
render "$MF/absent" 0.4.0 2026-07-19T14:24:51Z
|
||||
check "manifest: a fresh render stamps latest with now" 0 "converged_at=2026-07-19T14:24:51Z" \
|
||||
render "$MF/absent" 0.4.0 2026-07-19T14:24:51Z
|
||||
# key=value, one per line, nothing else — the shape a machine with no jq and no
|
||||
# YAML parser can read with `read`. Five lines, no quoting, no nesting.
|
||||
render_is_flat_kv() { # render_is_flat_kv <path> <version>
|
||||
local out
|
||||
out="$(render "$1" "$2" 2026-07-19T14:24:51Z)" || return 1
|
||||
[ "$(printf '%s\n' "$out" | wc -l)" -eq 5 ] || return 1
|
||||
! printf '%s\n' "$out" | grep -qvE '^[a-z_]+=[^ ]*$'
|
||||
}
|
||||
check "manifest: the render is bare key=value, one per line" 0 "" \
|
||||
render_is_flat_kv "$MF/absent" 0.4.0
|
||||
|
||||
# -- THE CRUX: convergence ---------------------------------------------------
|
||||
# bootstrap.sh:3 promises "a second run changes nothing", enforced by a
|
||||
# cmp-guard before every file install. A naive `converged_at=$(now)` would
|
||||
# break that promise on every single re-run — the file would differ by a
|
||||
# timestamp, the guard would fire, and rig would report a change it did not
|
||||
# make. Rules 1 and 2 make the rendered content a function of (existing file,
|
||||
# running version) alone.
|
||||
#
|
||||
# Proven the strong way: render the SAME fixture twice with two clock readings
|
||||
# a year apart and diff. Byte-identical output means the clock cannot reach the
|
||||
# file at all — a stronger claim than re-running the writer quickly enough that
|
||||
# the two stamps happen to match by luck.
|
||||
clock_cannot_reach() { # clock_cannot_reach <path> <version>
|
||||
diff <(render "$1" "$2" 2027-01-01T00:00:00Z) <(render "$1" "$2" 2028-06-06T06:06:06Z)
|
||||
}
|
||||
reproduces_itself() { # reproduces_itself <path> <version>
|
||||
diff <(render "$1" "$2" 2029-09-09T09:09:09Z) "$1"
|
||||
}
|
||||
BORN="$MF/born"
|
||||
render "$MF/absent" 0.4.0 2026-07-19T14:24:51Z > "$BORN"
|
||||
check "manifest: re-render by the SAME rig is byte-identical across a year of clock" 0 "" \
|
||||
clock_cannot_reach "$BORN" 0.4.0
|
||||
check "manifest: re-render by the same rig equals the file it read" 0 "" \
|
||||
reproduces_itself "$BORN" 0.4.0
|
||||
# The same property through the WRITER, which is what bootstrap actually calls:
|
||||
# a first stamp writes (exit 0), a second by the same rig reports the file
|
||||
# already current (exit 1) and touches nothing.
|
||||
STAMPED="$MF/stamped"
|
||||
check "manifest: the first stamp writes the file" 0 "" stamp "$STAMPED" 0.4.0
|
||||
check "manifest: the file landed 0644 — an audit record nobody can read is not one" \
|
||||
0 "644" mode_of "$STAMPED"
|
||||
BEFORE="$(cat "$STAMPED")"
|
||||
check "manifest: a second stamp by the same rig reports already-current" 1 "" stamp "$STAMPED" 0.4.0
|
||||
check "manifest: a second stamp by the same rig changed no byte" 0 "" \
|
||||
text_is "$STAMPED" "$BEFORE"
|
||||
|
||||
# -- a DIFFERENT rig: a real diff, and only where it belongs ----------------
|
||||
# The cmp-guard firing here is CORRECT, not spurious. It was only ever the
|
||||
# clock that was the fake change, never the version.
|
||||
check "manifest: a re-converge by a newer rig moves converged_by" 0 "converged_by=0.6.0" \
|
||||
render "$BORN" 0.6.0 2026-08-02T09:11:03Z
|
||||
check "manifest: a re-converge by a newer rig moves converged_at" 0 "converged_at=2026-08-02T09:11:03Z" \
|
||||
render "$BORN" 0.6.0 2026-08-02T09:11:03Z
|
||||
# Rule 1, the load-bearing half: birth is FIRST-WRITE-WINS. bootstrapped_by
|
||||
# must survive every later convergence — "what built this box" is unanswerable
|
||||
# by any other means once the run is over.
|
||||
check "manifest: a re-converge leaves the birth version pinned" 0 "bootstrapped_by=0.4.0" \
|
||||
render "$BORN" 0.6.0 2026-08-02T09:11:03Z
|
||||
check "manifest: a re-converge leaves the birth stamp pinned" 0 "bootstrapped_at=2026-07-19T14:24:51Z" \
|
||||
render "$BORN" 0.6.0 2026-08-02T09:11:03Z
|
||||
check "manifest: the writer sees a version change as a real change" 0 "" stamp "$STAMPED" 0.6.0
|
||||
check "manifest: and settles again on the new version" 1 "" stamp "$STAMPED" 0.6.0
|
||||
|
||||
# -- a DOWNGRADE is a change too --------------------------------------------
|
||||
# converged_by is "the rig that last converged this", not "the highest one ever
|
||||
# seen". Rolling back with `rig use` and re-converging must be recorded, or the
|
||||
# file would name a version that is no longer what runs here.
|
||||
check "manifest: re-converging with an OLDER rig is recorded, not ignored" 0 "converged_by=0.1.0" \
|
||||
render "$BORN" 0.1.0 2026-09-09T09:09:09Z
|
||||
|
||||
# -- forward compatibility: unknown keys survive the rewrite ----------------
|
||||
# The schema promises readers ignore keys they do not know. That promise is
|
||||
# worthless if the WRITER eats them: a manifest touched by a newer rig, or
|
||||
# carrying a later command's own provenance line, must come back whole.
|
||||
FOREIGN="$MF/foreign"
|
||||
{ cat "$BORN"; printf 'runner_installed_at=2026-07-19T16:10:00Z\n'; printf 'schema_future_key=x\n'; } > "$FOREIGN"
|
||||
check "manifest: a later command's provenance line survives a rewrite" 0 "runner_installed_at=2026-07-19T16:10:00Z" \
|
||||
render "$FOREIGN" 0.6.0 2026-08-02T09:11:03Z
|
||||
check "manifest: a key from a newer schema survives a rewrite" 0 "schema_future_key=x" \
|
||||
render "$FOREIGN" 0.6.0 2026-08-02T09:11:03Z
|
||||
# And preserving them must not cost convergence: a file carrying foreign keys is
|
||||
# still byte-stable under a same-version re-render.
|
||||
check "manifest: foreign keys do not break convergence" 0 "" \
|
||||
reproduces_itself "$FOREIGN" 0.4.0
|
||||
|
||||
# -- damaged files: repair once, then settle --------------------------------
|
||||
# A truncated or hand-edited manifest must converge back to a whole one and
|
||||
# then STAY PUT — a repair that re-fires on every run is a clock by another
|
||||
# name, and would break convergence exactly where it is hardest to notice.
|
||||
printf 'bootstrapped_at=2020-01-01T00:00:00Z\n' > "$MF/noby"
|
||||
check "manifest: a birth stamp with no birth version records unknown, never today's" \
|
||||
0 "bootstrapped_by=unknown" render "$MF/noby" 0.4.0 2026-07-19T14:24:51Z
|
||||
check "manifest: ...and still keeps the birth stamp it does have" \
|
||||
0 "bootstrapped_at=2020-01-01T00:00:00Z" render "$MF/noby" 0.4.0 2026-07-19T14:24:51Z
|
||||
printf 'schema=1\nbootstrapped_by=0.4.0\nbootstrapped_at=2020-01-01T00:00:00Z\nconverged_by=0.4.0\n' > "$MF/noat"
|
||||
REPAIRED="$MF/repaired"
|
||||
render "$MF/noat" 0.4.0 2026-07-19T14:24:51Z > "$REPAIRED"
|
||||
check "manifest: a converged_by with no converged_at is repaired once" 0 "converged_at=2026-07-19T14:24:51Z" \
|
||||
cat "$REPAIRED"
|
||||
check "manifest: the repair settles — it does not re-fire on the next run" 0 "" \
|
||||
reproduces_itself "$REPAIRED" 0.4.0
|
||||
|
||||
# -- the version that RAN, not the one installed now ------------------------
|
||||
# The whole point: a machine outlives the rig that built it, so this is read
|
||||
# from the tree at run time and never re-derived from `rig --version` later.
|
||||
check "manifest: the running version comes from the tree's own VERSION" 0 "$(cat "$ROOT/VERSION")" \
|
||||
running_version "$ROOT"
|
||||
check "manifest: a tree with no VERSION records unknown, not an empty key" 0 "unknown" \
|
||||
running_version "$MF"
|
||||
|
||||
# -- the marker is NOT touched ----------------------------------------------
|
||||
# /etc/rig/role has six readers, install.sh:82-90 among them; the manifest is a
|
||||
# second file beside it, never a replacement. Assert the writer cannot reach it.
|
||||
check "manifest: no CODE in the manifest lib reaches the role marker" 1 "" \
|
||||
grep -nE '^[^#]*(/etc/rig/role|RIG_ROLE_MARKER)' "$ROOT/commands/lib/manifest.sh"
|
||||
check "bootstrap: the role marker write is still its own cmp-guarded block" 0 "" \
|
||||
grep -qE '^MARKER=/etc/rig/role$' "$ROOT/commands/bootstrap.sh"
|
||||
|
||||
# -- ordering: provenance is written after the tag verification -------------
|
||||
# The marker's discipline, inherited verbatim — a manifest that survives a run
|
||||
# which failed to become what it claims is a confident wrong answer.
|
||||
mfstamp_at="$(grep -nE '^if manifest_stamp ' "$ROOT/commands/bootstrap.sh" | head -n1 | cut -d: -f1)"
|
||||
verify_at="$(grep -nE '^[[:space:]]*verify_effective_tag back-out$' "$ROOT/commands/bootstrap.sh" | head -n1 | cut -d: -f1)"
|
||||
check "bootstrap: both ordering anchors were found (guards the greps above)" 0 "" \
|
||||
test -n "${mfstamp_at:-}" -a -n "${verify_at:-}"
|
||||
check "bootstrap: the manifest stamp follows the tag verification" 0 "" \
|
||||
test "${mfstamp_at:-0}" -gt "${verify_at:-999999}"
|
||||
# Both bootstrap paths stamp it: a box-minted guest is a machine rig converged,
|
||||
# and "which rig, when" is a fact whichever bootstrap ran.
|
||||
check "bootstrap-tenant: a tenant gets a manifest too" 0 "" \
|
||||
grep -q 'manifest_stamp' "$ROOT/commands/bootstrap-tenant.sh"
|
||||
|
||||
# -- `rig manifest`, the reader ---------------------------------------------
|
||||
key_prints_exactly() { # key_prints_exactly <path> <key> <want>
|
||||
[ "$(RIG_MANIFEST="$1" "$ROOT/commands/manifest.sh" "$2")" = "$3" ]
|
||||
}
|
||||
check "manifest: --help exits 0" 0 "usage: rig manifest" "$ROOT/commands/manifest.sh" --help
|
||||
check "manifest: dispatches through bin/rig" 0 "usage: rig manifest" "$ROOT/bin/rig" manifest --help
|
||||
check "manifest: rig --help lists the command" 0 "manifest [<key>]" "$ROOT/bin/rig" --help
|
||||
check "manifest: unknown flag exits 2" 2 "unknown option" \
|
||||
env RIG_MANIFEST="$STAMPED" "$ROOT/commands/manifest.sh" --nope
|
||||
check "manifest: two keys is a usage error" 2 "at most one key" \
|
||||
env RIG_MANIFEST="$STAMPED" "$ROOT/commands/manifest.sh" converged_by schema
|
||||
check "manifest: an absent manifest exits 1 by name" 1 "no manifest at" \
|
||||
env RIG_MANIFEST="$MF/absent" "$ROOT/commands/manifest.sh"
|
||||
check "manifest: bare prints the file" 0 "converged_by=0.6.0" \
|
||||
env RIG_MANIFEST="$STAMPED" "$ROOT/commands/manifest.sh"
|
||||
check "manifest: a key prints the value ALONE, for shell callers" 0 "" \
|
||||
key_prints_exactly "$STAMPED" converged_by 0.6.0
|
||||
check "manifest: an unknown key exits 1 and names the keys present" 1 "keys present" \
|
||||
env RIG_MANIFEST="$STAMPED" "$ROOT/commands/manifest.sh" nosuchkey
|
||||
# Operator input reaches the key lookup, so the lookup is a string equality and
|
||||
# never a pattern — a key of '.*' must find nothing rather than match line one.
|
||||
check "manifest: a regex-shaped key matches nothing" 1 "no such key" \
|
||||
env RIG_MANIFEST="$STAMPED" "$ROOT/commands/manifest.sh" '.*'
|
||||
# The reader writes NOTHING — bootstrap is the manifest's single writer.
|
||||
MTIME_BEFORE="$(mtime_of "$STAMPED")"
|
||||
RIG_MANIFEST="$STAMPED" "$ROOT/commands/manifest.sh" >/dev/null 2>&1 || true
|
||||
check "manifest: reading it does not write it" 0 "$MTIME_BEFORE" mtime_of "$STAMPED"
|
||||
|
||||
# Secrets: this file is 0644 by design, so the rule has to be stated where the
|
||||
# next command that appends a line will read it (repo precedent:
|
||||
# runner-install.sh:190's ".rig-labels — box-local metadata, never a credential").
|
||||
check "manifest: the never-a-credential rule is stated in the writer" 0 "never a credential" \
|
||||
cat "$ROOT/commands/lib/manifest.sh"
|
||||
|
||||
rm -rf "$MF"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The versioned install (box#79's layout, ported — #35). RIG_INSTALL_SOURCE
|
||||
# bypasses the network, so these are REAL runs of install.sh against throwaway
|
||||
|
|
|
|||
Loading…
Reference in a new issue