From 9effc85a4795c0d425969eade04624cf57e6945c Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Thu, 16 Jul 2026 22:59:14 +0200 Subject: [PATCH] docs: v0.89.0 REPORT + CONTEXT (self-grant R-22, escrow live-reload, poke listener; live legs operator-gated) --- CONTEXT.md | 14 +++++++ REPORT.md | 115 +++++++++++++++++++++++++++++++---------------------- 2 files changed, 82 insertions(+), 47 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 24d2be4..722d532 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -5,6 +5,20 @@ ## Current +- **v0.89.0** (2026-07-16) — **agent train: three bundled agent-plane items; built + published to + Gitea (sha256 `3969fd91…`); paired with hub 0.59.0 (LIVE).** (1) **pbsdr self-grant (R-22)** — + closes the F4 self-deadlock: a 403 on the token-auth `StorageEntry` pre-check now self-grants via + the root wrapper + re-reads instead of aborting before the grant (the demo's `felhom-offsite` + case). (2) **escrow config live-reload** — `/escrow/preflight`'s `pbs_storage_id` row now reads + the live agent.json (late-bound `CurrentPBSStorageID`) so a pbsdr-seeded id flips green with no + restart. (3) **agent-plane poke listener (Direction-2a)** — `internal/poke`: contentless UDP poke + bound to the box WG /32 (port **51822**), leading-edge debounced, fires the hub-loop out-of-band + trigger for an immediate desired-state cycle; enabled with `wg_tunnel.enabled`; first slice of + R-13. Red-proofs for all three (run-fail-revert). **OPEN: operator vouch of 0.89.0 in the Day-0 + manifest + MinAgent floor raise → the floor-driven train updates the demo (still 0.88.0) → THEN + the Scenario 1 (R-22 self-heal, rollback re-grant recorded) + Scenario 3 (poke save-to-tick) + live legs run.** The ep0 poke leg is already validated live (hub REPORT). Details: REPORT.md. + - **v0.88.0** (2026-07-13 eve) — **controller-driven escrow ceremony (agent half), LIVE on demo host + drill VM (63/63 capabilities both).** `--output=json` machine mode (text mode byte-identical; extraction into `escrowCeremony()`); the ONE fixed argv diff --git a/REPORT.md b/REPORT.md index 91ab3af..de7710b 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,55 +1,76 @@ -# REPORT — v0.88.0: controller-driven escrow ceremony (agent half) +# REPORT — felhom-agent v0.89.0 (agent train: R-22 self-grant + escrow live-reload + poke listener) -**Date:** 2026-07-13 · **Version:** felhom-agent v0.88.0 (from v0.87.0, baseline `adf7882f` confirmed) · Coupled: controller v0.127.0 gates its wizard on agent ≥ 0.88.0 (passive version header, header-absent = older). Commit `1c3a3ef` on `main`. Spike basis: felhom.eu `SPIKE-controller-escrow-2026-07-13` (all five mechanisms GO; the production argv adds `--output=json`, a sudoers-semantics-neutral delta). +**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`. -## What shipped +## Items + landmarks as found at source -- **`--output=json`** — `runSelftestEscrowCreate` extracted into the shared `escrowCeremony()` core - (`cmd/felhom-agent/main.go`); text mode byte-identical (banner/R/exit codes 0/1/2, incl. the - upload-fail-after-R print order); json mode = ONE `escrow.CeremonyOutput` (v1) on stdout, - humans → stderr, `--offline`/`--paperkey` refused (exit 2). -- **The ONE fixed argv** — `internal/escrow/ceremony.go` (`CeremonyBinary` + `CeremonyArgs()`): - shared by the localapi exec, the `escrow-ceremony` capability entry (Critical, `GatedBy: pbs_dr` - explicit), and byte-identically by the new `FELHOM_ESCROW` alias in - `configs/felhom-agent.sudoers`. `TestEscrowCeremonyArgvPinned` + `TestManifestCoveredBySudoers` - lock all three. -- **localapi endpoints** (`internal/localapi/escrow_ceremony.go`): `POST /escrow/ceremony` - (single-flight 409, detached, 60 s), `GET /escrow/ceremony/status` (non-secret; R structurally - absent), `POST /escrow/ceremony/claim` (ONE-SHOT; 410 after; 10-min TTL → `unclaimed_void`, - lazy check + AfterFunc belt), `GET /escrow/preflight` (6 items; staged-secret informational; - `sudo -n -l` grant list-probe). R in-memory only — restart loses it SAFELY (re-run supersedes). +### Item 1 — pbsdr self-grant (R-22, closes F4 from tests/VALIDATION-n100-baremetal) +- **Where the pre-check lives:** `internal/pbsdr/manager.go` `Apply` → `m.px.StorageEntry(ctx, id)` + (GET `/storage/`, token-auth). On a non-default storage id the agent token has no ACL there + yet → **403**; pre-fix the generic `if err != nil { … return }` aborted BEFORE the root-run wrapper + `grant` (only reached via `adopt`/create) that creates that very ACL — a permanent self-deadlock. +- **Fix:** on an `errors.As(err, *proxmox.APIError)` with `IsForbidden()` ONLY, run + `felhom-pbs-apply grant ` now (root, no secret, no pre-existing entry — `pveum acl modify` on a + path is unconditional; verified in `configs/felhom-pbs-apply` `grant)`), re-read once, then flow the + normal adoption/create path. Every other error stays transient. Grant-OK-but-still-403 → + `verify_failed` loudly. The pre-check is KEPT (the spec's "don't drop it" — it short-circuits the + happy path once the ACL exists). A benign extra `grant` on the adopt path is idempotent. +- **Red-proof (run-fail-revert):** `TestSelfGrant_PreCheck403DoesNotAbortBeforeGrant`. Pre-fix + (forced `if true` abort): `self-grant never ran … calls=[]` → **FAIL**. Fixed: self-grant runs, + converges (adopted), no secret consumed, 2 StorageEntry calls (403 + post-grant re-read) → **PASS**. -## Green gate + red-proofs (all run → fail → revert, outputs in the session record) +### Item 2 — escrow config live-reload +- **Two ends:** the pbsdr bridge seeds `escrow.pbs_storage_id` into agent.json on convergence + (`manager.go` `finishConverged`→`seedEscrowStorageID`); the reader is + `internal/localapi/escrow_ceremony.go` `handleEscrowPreflight`, 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 existing `DRConfigured func() bool` + seam). `cmd/felhom-agent/main.go` wires it to `config.Load(cfg.SourcePath).Escrow.PBSStorageID` — + **exactly what the ceremony subprocess itself loads** — so the row reflects the true state the bare + `--selftest=escrow-create` one-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**. -`go build ./... && go vet ./... && go test ./...` green before every commit. Test functions -668 → **678** (+9 localapi ceremony, +1 argv pin; 2 probe tests updated for the 4th gated -capability). Red-proofs: -1. one-shot wipe+latch removed → `TestEscrowCeremony_OneShotClaim` FAILED ("R holder survived the claim"). -2. TTL check neutered → `TestEscrowCeremony_TTLExpiryVoidsUnclaimedR` FAILED ("claim after TTL: got 200, want 410"). -3. R injected into the job struct → `TestEscrowCeremony_JobStructCannotCarryR` FAILED ("R leaked into serialized job struct"). -6. argv element mutated (`--upload`→`--upload-mutated`) → `TestEscrowCeremonyArgvPinned` AND - `TestManifestCoveredBySudoers` FAILED (the transitive three-way lock proven). -(4/5 are controller-side — see felhom-controller/REPORT.md.) +### Item 3 — agent-plane poke listener (Direction-2a) +- **Trigger fired:** the hub control loop's out-of-band report trigger (`internal/hub/loop.go` + `SetTrigger`), 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`** — `Listener` binds a contentless UDP socket EXCLUSIVELY to the box's WG /32 + (`wgtunnel.LoadAssignedAddr`, new helper reading `registered.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 whenever `wg_tunnel.enabled`. Wired in `main.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) and + `TestDebounceCoalescesBurst` (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. -## Deployed + live-validated (2026-07-13 evening) +## Deviations from the spec's landmark guesses (source wins) +- The 403 pre-check is `StorageEntry` (GET `/storage/`) exactly as ROADMAP R-22 states — no + deviation; `StorageActive` would 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. -| Target | Result | -|---|---| -| demo host 192.168.0.162 | v0.88.0 + refreshed sudoers (`visudo -cf` gated), restart clean, **capabilities 63/63 ok, 0 degraded, 0 inactive** (DR on → `escrow-ceremony` ok) | -| drill VM (qm 300, 192.168.0.152) | same: v0.88.0, sudoers, **63/63 ok** | -| **Live ceremony (drill, endpoint-exact with the REAL guest token):** | stage-secret (real password, exact controller call) → preflight ALL GREEN (incl. the live `sudo_grant` list-probe of FELHOM_ESCROW) → `POST /escrow/ceremony` → done in ~4 s (2 polls @ 2 s) → status: `restic_pw_sealed:true, uploaded:true, claimable, TTL 598 s`, R absent from every payload → claim 1 = 200 (R: 10 words/79 chars, captured to a shell var, never printed/persisted) → **claim 2 = 410** → status `claimed:true, claimable:false` | -| Hub-side proof | `host_escrow` row `demo-vm-felhom-2f4b00`: updated `2026-07-13 17:10:57Z`, `restic_pw_sha256 = 24a887bafce4…` = sha256 of the guest's local repo password — **the spike's hash-less blob is REPAIRED through the shipped pipeline**; demo row untouched | +## Live validation (Phase 5 — CC-ownable done; remainder operator-gated) +- **Built + published** agent 0.89.0 to Gitea (sha256 `3969fd91…2609fb1a`, round-trip verified) — the + binary the floor-driven train (Scenario 4) fetches. +- **Scenario 4 / Scenario 1 live leg / Scenario 3 latency** require the demo agent to self-update to + 0.89.0 via the floor train, which needs the operator-password-gated Day-0 manifest vouch + floor + raise (CC cannot drive that UI). Demo agent is still **0.88.0**. Direct binary replace is forbidden + by the spec (the floor train IS the validation), so these legs are the operator follow-up. +- **Scenario 1 rollback recorded FIRST (as required):** current ACLs on the demo's + `/storage/felhom-offsite` are `FelhomAgentStore` for `felhom-agent@pve` (user) and + `felhom-agent@pve!agent` (token), propagate=1. Re-grant rollback = + `pveum acl modify /storage/felhom-offsite --users felhom-agent@pve --roles FelhomAgentStore` + + the `--tokens 'felhom-agent@pve!agent'` twin; the REVOKE that arms the deadlock is the same two with + `--delete`. NOT run on 0.88.0 (pre-fix → no self-heal, would break the demo's pbsdr). The ep0 poke + leg IS validated live — see the hub REPORT. -Method note: the wizard's agent half was validated by invoking the EXACT endpoints the controller -invokes, with the real pinned-channel token (browser leg: see the controller REPORT — the drill -box's dashboard password is customer-owned/Viktor's, so the typed re-auth is operator-gated). - -**NOT yet live-validated:** behavior on a real customer box (Peti); the CLI text mode on a host -(unit-locked byte-identical; not re-run live); Day-0 publish/vouch of 0.88.0 (operator follow-up — -the deployed hosts got it via direct deploy). - -## Observations -- The v0.86.0 "3 inactive" shape on no-DR boxes becomes "4 inactive" with `escrow-ceremony` — - hub chip counts shift by one; no hub change needed (generic rendering). -- `sudo -n -l` preflight probing from the daemon works exactly as the capability prober does — - no drop-in beyond the canonical sudoers needed. +## 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.