docs: REPORT/CONTEXT — v0.63.0 shipped + live-verified (B3 reload-on-miss, B2 snippets dir)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
+10
@@ -5,6 +5,16 @@
|
||||
|
||||
## Current
|
||||
|
||||
- **v0.63.0** (2026-07-03, live on felhom-pve + Gitea-published sha256 b4a89c81…) — **drill findings
|
||||
B3 + B2 FIXED** (`DRILL-day0-cleanroom-2026-07-03.md`): `TokenStore.Lookup` reloads the append-only
|
||||
store once on a miss (cross-process coherence with the one-shot provisioner — no more fresh-install
|
||||
`/controller/swap` 401 / manual restart; size short-circuit bounds the cost; behind the
|
||||
`TokenAuthority` seam) + `guesthook.InstallSnippet` issues a fenced `mkdir -p /var/lib/vz/snippets`
|
||||
first (fresh boxes lacked the dir → the self-heal hook silently never installed). Sudoers gained
|
||||
exactly that one grant — **ship sudoers WITH the binary** (done on felhom-pve). Red-proofed both;
|
||||
Scenario-E method: compiled test suite run ON felhom-pve + live channel-health hit-path.
|
||||
**OPERATOR FOLLOW-UP: bump the hub Day-0 manifest to agent 0.63.0** — until then fresh installs get
|
||||
0.62.0 and the guide's D.1b restart-first step still applies (narrowed to "< v0.63.0" in the guide).
|
||||
- **v0.62.0** (2026-07-03) — **audit A1 RESOLVED**: the stale-lock reaper's scan is now
|
||||
pool-intersected (`staleLockController.Guests()` = `ListLXC` ∩ `Client.Pool("felhom")` members),
|
||||
fail-safe skip on pool-read failure; `pve:pool-read` capability (non-critical) + `--selftest`
|
||||
|
||||
@@ -1,98 +1,82 @@
|
||||
# REPORT — v0.62.0: A1 pool-membership ownership check for the stale-lock reaper
|
||||
# REPORT — v0.63.0: B3 + B2 fresh-install fixes (token reload-on-miss + guesthook dir)
|
||||
|
||||
**Date:** 2026-07-03 · **Class:** implementation (agent v0.61.0 → **v0.62.0**, live on felhom-pve)
|
||||
**Spec basis:** audit A1 (`AUDIT-blast-radius-hostroot-localapi-2026-07-02` §A) via the spike verdict
|
||||
(`felhom.eu/documentation/audits/SPIKE-a1-pool-membership-read-2026-07-03.md` Q3/Q4/Q5).
|
||||
**Companion:** felhom.eu `felhom-host-install.sh` v1.8.0 → **v1.9.0** (`Pool.Audit` in FelhomAgentGuest).
|
||||
**Date:** 2026-07-03 · **Class:** implementation, correctness+security discipline (agent v0.62.0 →
|
||||
**v0.63.0**, live on felhom-pve) · **Source findings:**
|
||||
`felhom.eu/documentation/audits/DRILL-day0-cleanroom-2026-07-03.md` B3/B2.
|
||||
|
||||
## 1. Baselines & commits
|
||||
## Baselines & commits
|
||||
|
||||
| Repo | Baseline | Shipped commit |
|
||||
|---|---|---|
|
||||
| felhom-agent | `3e760a559a` (v0.61.0) | **`3f37c5f`** — v0.62.0 code + tests + REUSE/CHANGELOG/CONTEXT |
|
||||
| felhom.eu | `5e3dfcb714` (script v1.8.0) | **`639a57e`** — script v1.9.0 + scripts/CHANGELOG + REUSE §2 errata + CONTEXT |
|
||||
|
||||
## 2. What changed (agent)
|
||||
|
||||
- `Client.Pool(ctx, name)` (proxmox/query.go): `GET /pools/{name}` → `PoolInfo{PoolID, Members[]{VMID,Type}}`.
|
||||
API-side read — the `Client`-never-shells / `Privileged`-never-HTTPs fence is untouched.
|
||||
- `staleLockController.Guests()` (localapi/stalelock.go): **`ListLXC ∩ pool members`** (nonzero-vmid,
|
||||
non-`storage` entries). Pool-read failure → wrapped error `pool membership read (pool=felhom): …`
|
||||
→ rides the pre-existing "guest list unavailable — skipping recovery" guard (`stalelock.go`
|
||||
Server path). NEVER falls back to the unfiltered list. One INFO line per scan:
|
||||
`stale-lock: scanning pool guests` (pool, listed, scanned). **Note (spec deviation, by
|
||||
necessity):** the spec located this line in `RecoverStaleLockedGuests`; it is emitted by the
|
||||
production controller instead, because the unchanged `StaleLockController` seam cannot carry the
|
||||
pool name + pre-intersect count to the Server without breaking the 9 existing fake-based tests.
|
||||
Net startup-log effect is identical (Scenario D line observed live, §5).
|
||||
- `NewStaleLockController(px, runner, pool, logger)`; main.go threads `reconcile.DefaultPool`.
|
||||
- Capability: composed closure in main.go appends `pve:pool-read` (non-critical) to the sudo
|
||||
prober's snapshot; `internal/capability/` untouched. `--selftest` gained a "pool read" line.
|
||||
|
||||
## 3. Test results (all green: `go build ./... && go vet ./... && go test ./...`, 17 pkgs ok)
|
||||
|
||||
| Test | Result |
|
||||
| What | Value |
|
||||
|---|---|
|
||||
| `TestStaleLock_ForeignGuestNotReaped` (Scenario A) | PASS — foreign 5000 never unlocked/snap-deleted/started |
|
||||
| `TestStaleLock_PoolGuestStillReaped` (Scenario B) | PASS — owned 9201 fully recovered (unlock+delsnap+start) |
|
||||
| `TestStaleLock_PoolReadFails_SkipsAll` (Scenario C) | PASS — zero mutations on pool-read error; error text names the pool read |
|
||||
| `TestStaleLockController_GuestsIntersect` (§8 edges) | PASS — storage-type/vmid-0 members excluded; empty pool = empty scan, no error |
|
||||
| 9 pre-existing stalelock tests | PASS **unmodified** (Server-level `fakeStaleLock` seam untouched by design) |
|
||||
| Base | `84f3f7d` (v0.62.0, as spec'd) |
|
||||
| Fix commit | `f31a76f` — v0.63.0: B3+B2 (tokenstore.go, guesthook/install.go, sudoers, tests, CHANGELOG, REUSE) |
|
||||
| Docs commit | (this one — REPORT/CONTEXT) |
|
||||
|
||||
**Red-proofs (both run and reverted):**
|
||||
- **A:** intersect replaced with raw-ListLXC passthrough → `TestStaleLock_ForeignGuestNotReaped`
|
||||
**FAILED** with `runner calls=[[pct unlock 5000]]` (the exact A1 exploit) and
|
||||
`TestStaleLockController_GuestsIntersect` failed with both vmids returned. Restored.
|
||||
- **C:** pool-read error changed to fall back to the unfiltered list →
|
||||
`TestStaleLock_PoolReadFails_SkipsAll` **FAILED** with `runner=[[pct unlock 9201]]
|
||||
delsnap=[9201] started=[9201]`. Restored.
|
||||
## What changed
|
||||
|
||||
## 4. Live rescope (felhom-pve, PUSHED v1.9.0 script fetched from Gitea raw, `--rescope-acl --vmid 9201`)
|
||||
- **B3 — `TokenStore.Lookup` reload-on-miss** (`internal/localapi/tokenstore.go`): the one-shot
|
||||
provisioner Mints into the shared append-only JSONL while the daemon serves Lookup from an index
|
||||
built once at open — the daemon 401'd any token minted after it started (the drill's
|
||||
`/controller/swap: HTTP 401` until a manual restart). Lookup now re-reads the file once on a miss
|
||||
(`reloadLocked()`, factored from `load()`; idempotent under `apply`'s last-write-wins) and
|
||||
re-checks with the same constant-time compare, all under the existing single mutex. Append-only
|
||||
size short-circuit: an unknown token against an unchanged file costs one `stat`, no re-read
|
||||
(`loadedSize` kept accurate by in-process `Mint`). Fail-closed on an unreadable store; missing
|
||||
file = empty. Entirely behind the `TokenAuthority` seam — `server.go` untouched. No token/hash
|
||||
material logged (nothing new is logged at all).
|
||||
- **B2 — `guesthook.InstallSnippet` ensures the snippets dir** (`internal/guesthook/install.go`):
|
||||
fenced `mkdir -p /var/lib/vz/snippets` before the `install` op (fresh PVE lacks the dir; `install`
|
||||
without `-D` fails ENOENT — the drill's silent no-hook finding).
|
||||
`configs/felhom-agent.sudoers` FELHOM_GUESTHOOK gains exactly the one grant
|
||||
`/usr/bin/mkdir -p /var/lib/vz/snippets` (comment ties it to B2). Kept the separate-mkdir shape —
|
||||
`install -D` would have changed the allowlisted install argv.
|
||||
|
||||
Roles after (only Guest changed; Base/Store byte-identical):
|
||||
## Tests & red-proofs (both run, shown failing, restored; `git diff` clean)
|
||||
|
||||
```
|
||||
FelhomAgentStore → Datastore.Allocate,Datastore.AllocateSpace
|
||||
FelhomAgentGuest → Pool.Allocate,Pool.Audit,VM.Allocate,VM.Audit,VM.Backup,VM.Config.CPU,
|
||||
VM.Config.Disk,VM.Config.Memory,VM.Config.Network,VM.Config.Options,
|
||||
VM.PowerMgmt,VM.Snapshot,VM.Snapshot.Rollback
|
||||
FelhomAgentBase → Datastore.Audit,SDN.Use,Sys.Audit
|
||||
```
|
||||
| Test | Asserts | Result |
|
||||
|---|---|---|
|
||||
| `TestTokenStore_ReloadOnMiss_CrossProcessMint` (Scenario A) | token minted by a SECOND store handle after the daemon handle opened resolves to (120,true) | PASS. **Red-proof:** pre-fix Lookup shape (plain miss, no reload) → FAIL with `got (0,false), want (120,true)` — the exact drill 401 |
|
||||
| `TestTokenStore_ReloadOnMiss_BoundedReloads` (Scenario B) | unknown token → (0,false); exactly 1 reload after an external append; 0 further reloads on an unchanged file (size short-circuit) — no loop, no per-candidate re-read | PASS |
|
||||
| `TestTokenStore_ReloadOnMiss_RemintCoherence` (Scenario C) | cross-process rotation: new hash → (120,true), rotated-out hash 401s, `byHash`/`byVMID` coherent after full reload | PASS |
|
||||
| `TestTokenStore_ReloadOnMiss_MissingFile` (§8 edge) | store deleted → fail closed, no crash | PASS on linux (felhom-pve run); skipped on Windows (can't unlink an open handle) |
|
||||
| `TestInstallSnippet_EnsuresSnippetsDirFirst` (Scenario D) | a `mkdir -p /var/lib/vz/snippets` op with exactly the sudoers-fenced argv PRECEDES the install op | PASS. **Red-proof:** mkdir call removed → FAIL with `no mkdir -p /var/lib/vz/snippets op issued … calls: [[install …]]` |
|
||||
| Pre-existing suites | tokenstore_test.go originals, `TestInstallSnippet_RandomTempName` (only adapted to filter install calls from the recorded vector — assertions unchanged), full repo | all PASS; green gate `go build && go vet && go test ./...` exit 0 |
|
||||
|
||||
Production-token `GET /pools/felhom` (403 in the spike) → **HTTP 200, members [9201]**. The running
|
||||
v0.61.0 agent stayed `active` through the rescope (order: rescope BEFORE agent deploy, per spec).
|
||||
## Deploy verification (felhom-pve)
|
||||
|
||||
## 5. Agent deploy verification (felhom-pve)
|
||||
- Built on 180 from `f31a76f`; binary backed up as `felhom-agent.bak-0.62.0`; **sudoers shipped WITH
|
||||
the binary** (CRLF-stripped, `visudo -cf` on the file AND the live /etc/sudoers context: parsed OK;
|
||||
the new mkdir grant present).
|
||||
- Service active; `--selftest` OK (incl. `pool read pool "felhom", 1 member`); daemon hub report:
|
||||
**agent 0.63.0, 47/47 capabilities ok, `pve:pool-read` ok** (host_reports id 2412). No
|
||||
reassert/rebind or error lines in the journal after restart.
|
||||
- Published to Gitea: `felhom-agent/0.63.0`, HTTP 201, sha256 `b4a89c81c33d51ea…`.
|
||||
**OPERATOR FOLLOW-UP: bump the hub Day-0 artifact manifest to agent 0.63.0** (password-gated UI —
|
||||
CC cannot). Until then, fresh installs fetch 0.62.0 and still need the D.1b restart.
|
||||
|
||||
- Binary built on 180 from `3f37c5f`, deployed with backup `felhom-agent.bak-0.61.0`;
|
||||
`felhom-agent --version` → **`felhom-agent 0.62.0`**; service `active`.
|
||||
- Startup log (live excerpt): `msg="capabilities self-check" ok=47 total=47 degraded=0` (46 → 47:
|
||||
the new `pve:pool-read`, OK) and
|
||||
`msg="stale-lock: scanning pool guests" pool=felhom listed=1 scanned=1` (listed=1 because the
|
||||
scoped token's ListLXC is already pool-filtered — spike T1, expected). ReassertGuestBinds clean
|
||||
(logs only on action/failure; none emitted). No new warnings — the pre-existing PBS
|
||||
token-secret WARN (BUNDLE leftover, CONTEXT open thread) is unchanged.
|
||||
- `--selftest`: `[ ok ] pool read pool "felhom", 1 member(s)` / `- 9201 type=lxc`; suite `OK`.
|
||||
- Hub-side: latest stored host report (agent_version 0.62.0) carries
|
||||
`{"name":"pve:pool-read","feature":"stale-lock recovery scoping (pool ownership check)",
|
||||
"critical":false,"status":"ok"}` — 47 capabilities, 0 degraded. Verified against a snapshot of
|
||||
the hub DB (`host_reports.report_json`), since the operator UI is password-gated.
|
||||
- Published: Gitea generic package `felhom-agent/0.62.0`, round-trip sha256 verified:
|
||||
`ed1192c9ae8f355dee19128cf4b01984ba7398e860b4a16a8009655b84b32d81`.
|
||||
## Scenario E method + result (stated per §7-E; live store NOT polluted)
|
||||
|
||||
## 6. Operator follow-ups
|
||||
felhom-pve's guest 9201 already has its token in the daemon index, so a live swap cannot reproduce
|
||||
the fresh-install miss, and minting into the production store is forbidden. Method used (the spec's
|
||||
sanctioned alternative): (1) **compiled the localapi test binary on 180 and ran the full TokenStore
|
||||
suite ON felhom-pve** — all 4 reload-on-miss tests PASS on the real platform, including the
|
||||
linux-only deleted-file case; (2) **live hit-path/no-regression:** the in-guest controller's
|
||||
`agent-channel-health` scheduler job completed cleanly every minute after the restart (no 401, no
|
||||
channel event) — the deployed daemon authorizes the existing token exactly as before.
|
||||
|
||||
- **Hub Day-0 artifact manifest still vouches 0.60.0 → bump to 0.62.0**
|
||||
(sha256 above; password-gated UI — CC cannot).
|
||||
- Rotation + de-git of the 3 committed secrets in `felhom.eu/manifests/felhom.secret.yaml`
|
||||
(spike appendix; REUSE §2 errata now records that `gitea-creds` is NOT out-of-band).
|
||||
## Guide follow-through (felhom.eu)
|
||||
|
||||
## 7. Observations (not acted on)
|
||||
D.1b's unconditional "restart the agent first" step was **narrowed, not dropped**: the fix is
|
||||
deployed and platform-proven, but the Day-0 manifest still vouches agent 0.62.0, so a box installed
|
||||
today still gets a pre-fix agent. The guide now says: restart first only when the installed agent is
|
||||
< v0.63.0; the troubleshooting row records the fix version. Drill ledger B2/B3 marked FIXED
|
||||
v0.63.0. (The step drops entirely once the manifest vouches ≥ 0.63.0.)
|
||||
|
||||
- Hub capability-name handling confirmed generic: the checker keys on `Critical && status=="degraded"`
|
||||
only (`hub/internal/monitor/host_capability.go`), names are data — the new non-critical entry
|
||||
rides the report/UI without paging and needed no hub change.
|
||||
- The spike Q6 consumer observations (hub collect / disks / reconcile `ListLXC` users) remain
|
||||
observations; no other consumer touched, per spec.
|
||||
- `_state_get provisioned_vmid` was empty on felhom-pve (state.json holds only `completed`), so
|
||||
`--rescope-acl` needed the explicit `--vmid 9201` — worth knowing for other boxes.
|
||||
## Observations (not acted on)
|
||||
|
||||
- The one-shot `--selftest=hub` report carries an EMPTY `capabilities` list (the probe is composed
|
||||
only into the daemon loop) — a hub-side capability checker reading the newest report could
|
||||
transiently see 0 capabilities after an operator runs the one-shot. Cosmetic today; worth a look
|
||||
when touching the capability checker.
|
||||
- `OpenTokenStore` calls `load()` before `MkdirAll(dir)` — harmless (read tolerates ENOENT) but the
|
||||
ordering reads oddly; left as-is (out of scope).
|
||||
|
||||
Reference in New Issue
Block a user