Files
felhom-agent/REPORT.md
T

99 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# REPORT — F1 (HIGH) + F2: never destroy a guest this transaction didn't create (v0.60.0)
**Date:** 2026-07-02 · **Repo:** `felhom-agent` · **Class:** Risky/supervised (destructive
rollback/destroy path) — strictly SAFER. Fixes the pool-effects campaign's F1/F2
(`felhom.eu/documentation/audits/CAMPAIGN-pool-effects-2026-07-01.md`); F3 is the companion doc in
felhom.eu (`documentation/runbooks/provisioning.md`, commit `6dae9d7`).
## 1. Baseline → target
agent **v0.59.0** (`55ade9e`) → **v0.60.0** (commit **`b9356d6`** on `main`). felhom.eu doc commit
`6dae9d7` (no bump).
## 2. Root cause + what changed
The compensating destroy ran on ASSUMED provenance (`Rollback:true`/`SameTxnCreated`), never verified
against proof-of-launch. A `RestoreLXC` that failed synchronously (no UPID — e.g. PVE refusing a
pre-existing vmid the pool-blind ListLXC couldn't see) still armed `DestroyLXC(vmid)` — destroying a
guest the transaction never created. The pool ACL's 403 only saved the non-pool subset; an in-pool
pre-existing guest would have been destroyed, in-process AND by Recover. Now **a RestoreLXC UPID is the
sole destroy authorization**, in all three destroy paths (the pool ACL is defense-in-depth again):
- **F1a `internal/reconcile/bringup.go`** — `runBringUp`'s rollback defer gated on `launched` (set only
after the restore POST is accepted); a no-launch failure closes the owning entry terminal-failed with
NO destroy. Pre-restore `OpStarted` append kept (crash-safety). `rollbackBringUp` doc updated: only
ever called launch-proven.
- **F1b `internal/reconcile/restoretest.go`** — `runScratchTest`'s `teardownScratch` defer gated the
same way.
- **F1c `internal/reconcile/recover.go`** — the no-UPID "POST never confirmed → abandon" check now runs
BEFORE the Scratch/Rollback dispatch: a no-UPID guest-creating entry is abandoned (marked failed, NO
destroy) instead of destroy-by-vmid-existence. Recover safe by DESIGN, not by the pool-blind
"already gone" accident.
- **F2 `restoretest.go`** — `RunRestoreTest` band-advance: an "already exists" refusal (new
`pveAlreadyExists`, APIError-typed like `pveConfigLock` — a real restore failure is never
misclassified) advances to the next free band vmid (`pickScratchVMID` + exclude set, bounded by the
band width); band exhausted → `Skipped` (the scheduler raises no record/alert), never FAIL.
- **Accepted residual (rule 4, by design):** a crash in the one-statement window between obtaining the
UPID and journaling it leaks a half-built guest Recover won't destroy — cleanable, preferable to
destroying an innocent guest.
## 3. Green gate
`go build ./... && go vet ./... && go test ./...` — clean, 17 packages ok (build server go1.26.0 build
of the deploy binary also clean).
## 4. Tests (incl. the red-proofs)
- **F1a red-proof `TestRunBringUp_NoLaunchNoDestroy`** (PVE "already exists" APIError + plain
synchronous error): asserts `DestroyLXC` NOT called + entry terminal. **Companion verified:** with
the `launched` gate reverted it fails `destroys=[8000]` — the exact destroy-innocent-guest bug.
- **F1b `TestRunRestoreTest_RestoreNoLaunchNoTeardown`** (replaces the old
`…RestoreFailureStillTearsDown`, which asserted the BUGGY behavior): no teardown on a no-launch
failure. Red-proof verified (`destroys=[990000]` pre-fix).
- **No-regression (Scenario B):** `TestRunRestoreTest_LaunchedTaskFailureStillTearsDown` + a new
"restore-task failure after launch" case in `TestRunBringUp_CompensatingRollback` — a LAUNCHED
restore whose task fails still destroys the half-built guest.
- **F1c `TestRecover_BringUpNoUPIDAbandoned` + `TestRecover_ScratchNoUPIDAbandoned`:** a no-UPID
Rollback/Scratch entry with a guest PRESENT at the vmid → abandoned via the fail-safe path, no
destroy. Red-proof verified. Existing leaked-guest Recover tests updated to carry the UPID (the
launch proof) their scenario implies.
- **F2 `TestRunRestoreTest_AdvancesPastOccupiedBandVMID`** (squatter at 990000 → PASS at 990001,
squatter never destroyed) + **`…BandFullOfSquattersSkips`** (Skipped, not FAIL, bounded — one try per
band vmid) + `TestPickScratchVMID` exclude-set coverage.
## 5. Live proofs (felhom-pve, v0.60.0 deployed)
- **Probe A (headline):** `--selftest=bring-up -vmid 9001` (existing non-pool guest) → PVE refused
("CT 9001 already exists"); **no gate decision, no DestroyLXC attempt of any kind** (pre-fix run had
`gate decision class=guest_destroy` + a 403'd DELETE); 9001 untouched; journal drained in-process
(next run: `recover: examined=0`).
- **Probe D (F2):** decoy parked at 990000 → restore-test logged "band VMID occupied … advancing",
**passed at 990001** (restored+booted+verified+torn-down in 28s), decoy survived, decoy removed.
- **Regression:** forced post-launch failure (`-cores 10000` → config PUT 400 after restore) →
compensating rollback destroyed the half-built 9310 ("bring-up: rolled back"); a normal provision of
9310 then passed (boot+running 22s) and tore down cleanly.
- STOP condition (any destroy attempted against 9001 or the decoy) did not trigger.
## 6. Deployed + published
- felhom-pve: `/usr/local/bin/felhom-agent` = **0.60.0** (backup `felhom-agent.bak-0.59.0`), unit
`active`, clean logs. Demo left healthy: pool `felhom` = {9201}, guests 9001/9100/9201/9999 intact.
- Gitea generic package `felhom-agent/0.60.0` published (HTTP 201), download sha256 round-trip verified:
`45d19b8799cc764333cd95bb64e0515f2246cdb08c0fb36d9b23409051725831`.
- **Operator follow-up:** bump the hub Day-0 artifact manifest (password-gated UI) to agent **0.60.0**
— until then, fresh Day-0 installs still fetch the manifest-pinned older version.
## 7. F3 doc
`felhom.eu/documentation/runbooks/provisioning.md` (commit `6dae9d7`): pick the provision `-vmid` from
the host's FULL guest list (`pct list` + `qm list`), never the pool-only hub inventory; reserved bands
documented (9999, 990000990009 fleet-wide). Campaign doc updated with the F1/F2/F3 resolution banner.
## 8. Observations (not acted on)
- The hub artifact-manifest bump remains operator-gated (hub v0.29.0 dropdowns make it one click).
- Campaign F4 (restore-test can't read a NON-pool source guest's config for bind-mount neutralization)
and F5 (pool-blind eject dependent-scan) remain open as LOW, per the campaign doc.
- The bring-up duplicate-guard (`bringup.go` ListLXC scan) is still pool-blind — acceptable now that a
collision is non-destructive and PVE refuses authoritatively; noted in the F3 runbook.