GL-6 F4 fix: v1.11.3 — --resume repopulates producer-step outputs

Live-found in the GL-6 drill: `should_skip X || step_X` fully skips a
completed step on --resume, but token/enroll/grows produce IN-MEMORY
outputs (pve token; hub host_id/api_key; volume grows) that later steps
consume — agent_config writes them into the config, provision passes the
grows as flags. A resume that had completed token/enroll but not
agent_config wrote a config missing hub.host_id/proxmox.token (daemon
crash-loop, "hub.host_id is required"); a resume past grows passed
`-rootfs-grow ""` (flag parse error). step_token even had an internal
resume-guard the `|| step_token` dispatch defeated.

Fix: token/enroll/grows now run every pass (all idempotent — token
reuses-or-rotates from the on-disk config, enroll is mint-once-reuse,
grows is a pure recompute); the guard uses _state_has (no misleading
SKIP log). golden's GOLDEN_VOLID is re-derived from the local archive in
the resume block so provision never gets an empty -archive. Harness
+GL6-F4 invariant; 27/27; shellcheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-08 13:01:33 +02:00
parent 4cb70ff88b
commit 98bf5a434f
2 changed files with 42 additions and 5 deletions
+31 -5
View File
@@ -42,6 +42,12 @@
# artifacts are world-readable by ruling; the hub-vouched sha256 stays the integrity root).
# A configured credential is still used when present.
#
# v1.11.3 (GL-6 finding F4): --resume correctness — the producer steps (token/enroll/grows) now
# run every pass so their in-memory outputs (pve token, hub host_id/api_key, volume grows) are
# repopulated for the later steps that consume them; a resumed install no longer writes a config
# missing hub.host_id/proxmox.token (daemon crash-loop) or passes an empty -rootfs-grow. golden's
# GOLDEN_VOLID is re-derived from the local archive on resume.
#
# Grounding: documentation/audits/SPIKE-day0-firstboot-handshake-2026-06-26.md
#
# Usage:
@@ -149,7 +155,7 @@
set -euo pipefail
SCRIPT_VERSION="1.11.2" # keep in sync with the header line at the top of this file
SCRIPT_VERSION="1.11.3" # keep in sync with the header line at the top of this file
# Operator signing keys pinned at day-0 (GL-4; doc 04 §3 two-key model). EMPTY by default — the pin
# CEREMONY is an operator step: generate the real keypairs OFFLINE, then fill these four constants
@@ -1448,7 +1454,10 @@ step_preflight() {
#-------------------------------------------------------------------------------
step_token() {
log_step "2/8 Proxmox API token"
if should_skip token && [[ -n "$PVE_TOKEN" ]]; then return 0; fi
# Fast-path only when the token is ALREADY in memory this process (avoids a double reuse-check if
# called twice). On --resume PVE_TOKEN starts empty, so this correctly re-derives it from the
# on-disk config (reuse) or a rotation — GL6-F4. Uses _state_has (no misleading SKIP log here).
if _state_has token && [[ -n "$PVE_TOKEN" ]]; then return 0; fi
# Pool BEFORE the ACL: /pool/felhom must exist before apply_scoped_acl grants on it (3b). Always —
# even under --skip-provision (the token exists now; a later provision-into-pool needs pool + grant).
@@ -2265,14 +2274,31 @@ if $RESUME && _state_has preflight; then
[[ -n "$AGENT_CONFIG" ]] || AGENT_CONFIG="/etc/felhom-agent/agent.json"
# Backfill display values from the already-written config so the summary is complete.
[[ -f "$AGENT_CONFIG" ]] && HOST_ID=$(python3 -c "import json;print(json.load(open('$AGENT_CONFIG')).get('hub',{}).get('host_id',''))" 2>/dev/null || true)
# GL6-F4: golden's GOLDEN_VOLID feeds provision, but the resume path skips preflight (where local
# auto-discovery sets it). If the golden step already completed, the archive is on the local
# storage from run 1 — re-derive the volid so provision doesn't get an empty -archive. (When
# golden hasn't completed yet, step_golden runs and sets it.)
if [[ -z "$GOLDEN_VOLID" ]] && _state_has golden; then
GOLDEN_VOLID=$(pvesm list "$ARCHIVE_STORAGE" --content backup 2>/dev/null | awk -v v="$GOLDEN_VMID" '$0 ~ ("vzdump-lxc-" v "-"){print $1}' | sort | tail -1)
[[ -n "$GOLDEN_VOLID" ]] && log_info " golden (resumed from local): $GOLDEN_VOLID"
fi
log_skip "pre-flight (resumed)"
else
step_preflight
fi
should_skip token || step_token
should_skip grows || step_grows
should_skip enroll || step_enroll
# GL6-F4: token/enroll/grows are PRODUCERS whose IN-MEMORY outputs (PVE_TOKEN; HOST_ID/HOST_API_KEY;
# ROOTFS_GROW/DATAVOL_GROW/SYSDATA_GROW) are consumed by later steps (agent_config writes the token +
# hub.host_id into the config; provision passes the grows as flags). They MUST run every pass — even
# on --resume — or a resumed install writes a config missing hub.host_id/proxmox.token (daemon
# crash-loops) and provision gets `-rootfs-grow ""` (flag parse error). All three are idempotent:
# token reuses the on-disk token if it still authenticates (else rotates), enroll is a mint-once-
# reuse POST (200 REUSED), grows is a pure recompute — so unconditional re-run is cheap + correct.
# (This is why they are NOT behind `should_skip … ||`; step_token's own `_state_has` fast-path still
# short-circuits the reuse check when the token is already in memory within one process.)
step_token
step_grows
step_enroll
# GL-2: break-glass is gated at the CALL SITE (not inside the step) so the byo skip is auditable in
# one place. byo = a host the operator does not own: root@pam is the OWNER's credential — never
# reset, never vaulted.
+11
View File
@@ -281,6 +281,17 @@ else
verdict FAIL "GL6-ANON empty-cred anonymous-fetch fallback (warn-not-die + conditional auth)"
fi
# GL6-F4 (v1.11.3): the producer steps token/enroll/grows must run UNCONDITIONALLY (not behind
# `should_skip … ||`) so --resume repopulates the in-memory outputs later steps consume; and the
# resume block must re-derive GOLDEN_VOLID from the local archive when golden already completed.
if grep -qE '^step_token$' "$SCRIPT" && grep -qE '^step_grows$' "$SCRIPT" && grep -qE '^step_enroll$' "$SCRIPT" \
&& ! grep -qE 'should_skip (token|grows|enroll) +\|\| +step_' "$SCRIPT" \
&& grep -q 'golden (resumed from local)' "$SCRIPT"; then
verdict PASS "GL6-F4 resume repopulates producer outputs (token/enroll/grows unconditional + golden re-derive)"
else
verdict FAIL "GL6-F4 resume repopulates producer outputs (token/enroll/grows unconditional + golden re-derive)"
fi
# GL4-INV: no forced/lazy unmount and no format op on the drives root — REAL invocations only
# (comment lines and log_* guidance strings legitimately SAY "never umount -l/-f").
if ! grep -vE '^[[:space:]]*#|log_(warn|info|dry|error|success|skip)' "$SCRIPT" | grep -E 'umount +-(l|f)' >/dev/null \