7.1 KiB
7.1 KiB
REPORT — felhom-agent v0.89.0 (agent train: R-22 self-grant + escrow live-reload + poke listener)
Date: 2026-07-16 · Baseline: v0.88.0 (c040c18) → v0.89.0 (a659e5d, pushed)
Green gate: go build ./... && go vet ./... && go test ./... — all pass (full suite, local go1.26).
Design contract for item 3: felhom.eu/documentation/audits/SPIKE-immediate-sync-transport-2026-07-16.md.
Items + landmarks as found at source
Item 1 — pbsdr self-grant (R-22, closes F4 from tests/VALIDATION-n100-baremetal)
- Where the pre-check lives:
internal/pbsdr/manager.goApply→m.px.StorageEntry(ctx, id)(GET/storage/<id>, token-auth). On a non-default storage id the agent token has no ACL there yet → 403; pre-fix the genericif err != nil { … return }aborted BEFORE the root-run wrappergrant(only reached viaadopt/create) that creates that very ACL — a permanent self-deadlock. - Fix: on an
errors.As(err, *proxmox.APIError)withIsForbidden()ONLY, runfelhom-pbs-apply grant <id>now (root, no secret, no pre-existing entry —pveum acl modifyon a path is unconditional; verified inconfigs/felhom-pbs-applygrant)), re-read once, then flow the normal adoption/create path. Every other error stays transient. Grant-OK-but-still-403 →verify_failedloudly. The pre-check is KEPT (the spec's "don't drop it" — it short-circuits the happy path once the ACL exists). A benign extragranton the adopt path is idempotent. - Red-proof (run-fail-revert):
TestSelfGrant_PreCheck403DoesNotAbortBeforeGrant. Pre-fix (forcedif trueabort):self-grant never ran … calls=[]→ FAIL. Fixed: self-grant runs, converges (adopted), no secret consumed, 2 StorageEntry calls (403 + post-grant re-read) → PASS.
Item 2 — escrow config live-reload
- Two ends: the pbsdr bridge seeds
escrow.pbs_storage_idinto agent.json on convergence (manager.gofinishConverged→seedEscrowStorageID); the reader isinternal/localapi/escrow_ceremony.gohandleEscrowPreflight, which read a daemon-start snapshot (EscrowCeremonyConfig.PBSStorageID) → stayed red until a service restart. - Reload strategy chosen (stated per source shape): the preflight reads current disk state, via
a new late-bound
CurrentPBSStorageID func() string(mirrors the existingDRConfigured func() boolseam).cmd/felhom-agent/main.gowires it toconfig.Load(cfg.SourcePath).Escrow.PBSStorageID— exactly what the ceremony subprocess itself loads — so the row reflects the true state the bare--selftest=escrow-createone-liner will see. Falls back to the boot snapshot on read error / all-env config. Preferred over an in-memory push (no cross-component coupling, single source of truth = disk). - Red-proof:
TestEscrowPreflight_PBSStorageIDLiveReload— seed after boot, same process; pre-fix (static field) stays red → FAIL; fixed flips green + reports the seeded id → PASS.
Item 3 — agent-plane poke listener (Direction-2a)
- Trigger fired: the hub control loop's out-of-band report trigger (
internal/hub/loop.goSetTrigger), the SAME cap-1 non-blocking channel the storage watchdog uses — fan-in, coalescing. An immediate collect→report cycle carries the advanced envelope generation → the desired-state syncer fetches → pbsdr/wg consumers apply. - New
internal/poke—Listenerbinds a contentless UDP socket EXCLUSIVELY to the box's WG /32 (wgtunnel.LoadAssignedAddr, new helper readingregistered.json; never 0.0.0.0, never LAN) on the fixed port 51822; payload ignored; leading-edge debounce (DebounceWindow) coalesces a burst into ≤1 tick; enabled wheneverwg_tunnel.enabled. Wired inmain.go(goroutine + errc 7→8). - Red-proofs (run-fail-revert):
TestBindConfinement(wildcard bind → bound to::/IsUnspecified→ FAIL; fixed → the specific WG addr, datagram fires the trigger) andTestDebounceCoalescesBurst(guard removed → 10 fires for 10 pokes → FAIL; fixed → 1, then fires again after the window). Port registered in REUSE.md as a shared cross-repo constant.
Deviations from the spec's landmark guesses (source wins)
- The 403 pre-check is
StorageEntry(GET/storage/<id>) exactly as ROADMAP R-22 states — no deviation;StorageActivewould also 403 but the self-grant at the top fixes both. - (Hub side, cross-repo) the MinAgent-floor save does not itself bump per-host desired generation — noted in the hub REPORT.
Live validation (Phase 5 — ALL THREE LEGS PROVEN LIVE on the demo, 2026-07-17)
- Built + published agent 0.89.0 to Gitea (sha256
3969fd91…2609fb1a, round-trip verified). - Scenario 4 — floor-driven A/B train (0.88→0.89), PROVEN LIVE. Operator vouched 0.89.0 + raised
MinAgent to 0.89.0, then signed+enqueued the
agent_updateop (the self-update trigger — a per-box operator-signed op; the vouch+floor alone does NOT queue it; CC cannot sign, per DRILL-GL6). The box fetched it on its next report and self-updated:gate decision class=agent_update allowed=true reason=signed key_id=felhom-op-1→ download (sha3969fd91…verified) →felhom-selfupdate-guarded apply(root A/B flip) → new daemonversion=0.89.0→ 60 s dwell →selfupdate: update committed(no rollback). Poke listener bound clean:poke: listening … addr=10.77.0.2:51822; 63/63 caps. - Scenario 1 — R-22 self-heal, PROVEN LIVE. Rollback re-grant recorded FIRST (
pveum acl modify /storage/felhom-offsite --users felhom-agent@pve --roles FelhomAgentStore+ the--tokens 'felhom-agent@pve!agent'twin — delete syntax ispveum acl delete). Faithful F4 reproduction: moved the pbsdr marker aside (the reinstall's fresh-state — revoke-alone is a no-op because theappliedmarker short-circuits Apply before any storage read; an important reproduction finding) +pveum acl deleteboth felhom-offsite ACLs. Next reconcile tick:08:24:16 pbsdr: pre-check 403 … self-granting via the root wrapper … (R-22) storage_id=felhom-offsite→08:24:19 pbsdr: converged state=adopted— ~3 s, no operator action. Verified: ACLs self-restored (user+token), marker rewritten (adopted),pvesm status felhom-offsite= active. Pre-fix = permanent 403→abort loop. - Scenario 3 — poke → tick latency, MEASURED LIVE. ep0 send
06:28:55.765UTC → box08:28:55.796poke received → triggering an immediate desired-state cycle from=10.77.0.1→ same instantout-of-band report triggered (watchdog/poke). ~31 ms ep0→box over the tunnel (spike ~29 ms), sub-ms to the report cycle; WG-confined (from10.77.0.1, to the10.77.0.2-bound socket). The hub-save→poke fire is separately live-proven (hub REPORT: real manifest save →sync-poke delivered to 10.77.0.2); full save→tick ≈ ~0.45 s (SSH-dominated), well under the ≤2–3 s target.
R-13
Item 3 is the FIRST concrete slice of the OOB/mutual-repair arc (the listener + trigger only). R-13 stays open; nothing beyond the poke channel was built.