Files
felhom.eu/REPORT.md
T
admin f456835bbc
gates / gates (push) Successful in 8s
REPORT/R-190: the alert pair closed — one loss, one email, one recovery
The 20-minute latch expired at 10:20:29 and the hub logged degraded -> ok
(agent_capability_recovered) at 10:30:40. Final state on both boxes: agent
0.124.1, two ACL rows on /storage/felhom-backup.
2026-08-04 10:31:46 +02:00

264 lines
15 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 — R-191 (a backup that worked must not report failure) + R-190 (a permission must repair itself)
**Date:** 2026-08-04 · **Repos:** `felhom-agent` **v0.123.0 → v0.124.0 → v0.124.1** (`257c4d8`, and
the v0.124.1 fix) · `felhom.eu` installer **1.24.0 → 1.25.0** (tag `installer-v1.25.0`) ·
**no hub change and no hub bump**.
---
## 1. Baselines
| Repo | cited in §1 | found on arrival | matched? |
|---|---|---|---|
| `felhom-agent` | `72161f6cf010` / `v0.123.0` | same | **yes** |
| `felhom.eu` | `d692c46db08f` / installer `1.24.0` | **`33f0ab2f33f7`** / installer `1.24.0` | **version yes, commit NO** |
`felhom.eu` had moved four commits forward since the brief was written — this morning's alert
diagnosis (R-190/191/192/193 rows, the demo-hp offsite re-issue), all pushed. Recorded rather than
glossed. Both `--ref=installer-v1.24.0` lines confirmed at 327 and 372.
## 2. Part 0 — the gate, and the three broken instruments that nearly failed it
**VERDICT: PASS.** ep0 prunes **both** namespaces on schedule.
```
prune-demo-felhom store=felhom-offsite ns=demo-felhom schedule=03:30 keep-last 2
prune-demo-hp store=felhom-offsite ns=demo-hp schedule=03:30 keep-last 2
```
**18 prunejob tasks, all `status=OK`, every day from 2026-07-27 to 2026-08-04**, two per day, one per
namespace. The newest task log, read in full:
```
prune job 'prune-demo-felhom' / task triggered by schedule '03:30'
retention options: --ns demo-felhom --max-depth 0 --keep-last 2
Pruning group demo-felhom:"ct/9201"
keep ct/9201/2026-07-27T19:55:41Z
keep ct/9201/2026-07-28T04:49:43Z
TASK OK
```
Read-only throughout: no job created, edited or run; nothing deleted. ep0 is Tier 2.
### The part that matters more than the verdict
**Three of my own queries said the endpoint prunes NOTHING, and all three were broken instruments:**
| query | why it was wrong |
|---|---|
| `'prune' in t['worker-type']`*0 prune tasks* | the field is **`worker_type`**; `worker-type` does not exist, so every task read `None` |
| `worker_type == 'prune'`*0* | the worker type is **`prunejob`**, not `prune` |
| `journalctl -u proxmox-backup \| grep -c prune`*0* | the unit is **`proxmox-backup-proxy`** |
A fourth reading was mine to mis-frame: *"3 snapshots under keep-last 2 proves nothing is pruning"*
the third snapshot landed at 04:49, **after** that day's 03:30 window, so it proved nothing. I caught
that one myself; the other three were caught only by checking the field names when the answer looked
too categorical.
**Had I acted on them, I would have disabled the only pruning ATTEMPT while filing a finding that
nothing prunes anywhere** — a weekly false alarm traded for unbounded growth on the protected
endpoint, invisible for months. The gate did its job precisely because it demanded evidence rather
than a verdict.
## 3. R-191 — the off-site tier stops asking to prune
**The defect:** `keep_last: 2` on the offsite tier, so vzdump uploaded and then failed the whole job
on `missing Datastore.Modify|Datastore.Prune``TASK ERROR: job errors`
`whole_guest_backup_failed`. R-89 moved offsite pruning server-side; the 2026-07-26 "two weeks" ruling
was not reversed — **where it is enforced moved and this value did not follow.**
- **Installer 1.25.0**: `keep_last: 0`, and the justifying paragraph rewritten to say where retention
now lives, citing R-89 and the Part 0 evidence, plus the warning that if ep0 ever stops pruning this
zero is unbounded growth. The agent's guard `allowPBSPrune = !primary && keep_last > 0` already
reads 0 as *never prune***no agent change**.
- **A gate asserts it** (`hostinstall_gates.py`), red-proved: pinning `keep_last: 2` back →
`FAIL: the offsite backup_target arms a CLIENT-SIDE prune (keep_last=2)`.
- **Both live boxes**, quoted:
```
demo-felhom backup tier armed target=felhom-backup … keep_last=3 … prune_pbs_allowed=false primary=true
demo-felhom backup tier armed target=felhom-pbs … keep_last=0 … prune_pbs_allowed=false primary=false
felhom-host backup tier armed target=felhom-backup … keep_last=3 … prune_pbs_allowed=false primary=true
felhom-host backup tier armed target=felhom-pbs … keep_last=0 … prune_pbs_allowed=false primary=false
```
The local tier is untouched (Scenario C).
**Not yet observed:** a weekly offsite run completing OK end to end. The failing step is removed and
the boxes are armed correctly; the *schedule* proving it is next week's event. The row says so rather
than implying the run has happened.
## 4. R-190 — the grant repairs itself, and the repair is visible
**Mitigation.** On a missing grant the agent runs the existing root wrapper
`felhom-backup-target-apply grant <id>` and **re-reads once** — the pbsdr R-22 shape and its
restraint. **No new privileged surface:** `grant *` was already in the sudoers allowlist for any
storage id (confirmed in `configs/felhom-agent.sudoers`, not assumed), and the verb already grants
both user and token. It existed, was permitted, and had only ever been called at storage **creation**
— the *built but never wired* shape in a verb rather than a seam. Bounded: one attempt per tier per
hour.
### The record — and how v0.124.0 got it wrong in production with a green test suite
§8.5 asked whether the hub's existing degraded↔ok edge sufficed **before building anything new**. It
does, as a *channel* — but only if the agent deliberately reports one degraded cycle, because a silent
repair produces no edge at all. So **nothing new was built**: no wire change, no hub change, no new
event type. The explanation rides `Feature`, because that is the field the hub interpolates into the
operator's e-mail (`monitor/host_capability.go` builds its message from names and features; `Reason`
does not travel).
**v0.124.0 implemented "one cycle" as one probe CALL, and that failed live:**
```
09:39:34 agent: GRANT WAS MISSING AND HAS BEEN SELF-REPAIRED … confirmed_by=re-read
09:39:34 agent: capabilities self-check ok=69 total=70 degraded=1
09:39:37 hub: host-report stored → pve:store-grant:felhom-backup = ok ← the record never arrived
```
`probeAll` is invoked **independently** by the self-check log and by the collector building a report.
The repairing call was the log's; the report three seconds later found the grant present and sent
`ok`. The agent's journal had the record, the hub had nothing, and the operator would have learned
nothing — the exact silence R-190 exists for, re-created inside its own mitigation, **while every unit
test passed**. **v0.124.1** replaces it with a latch on **time** (20 min > the 900 s report interval).
### Live proof, twice, on demo-felhom
Grant deleted by hand (both rows — user and token), which reproduces R-190 and the R-185 symptom
(`{"data":[]}` through the token):
```
10:00:29 agent: store-grant: GRANT WAS MISSING AND HAS BEEN SELF-REPAIRED — investigate the loss (R-190)
target=felhom-backup privilege=Datastore.AllocateSpace
action="felhom-backup-target-apply grant felhom-backup" confirmed_by=re-read
10:00:30 hub: host-report → pve:store-grant:felhom-backup = DEGRADED
feature: "…the agent's storage grant was MISSING and has been AUTOMATICALLY RESTORED…"
10:00:40 hub: Host capability: demo-felhom-8363b5 ok → degraded (agent_capability_degraded)
10:00:40 hub: Operator email sent for demo-felhom/agent_capability_degraded
pveum acl list | grep -c felhom-backup → 2 (both rows restored)
```
**And the pair CLOSED**, which is the other half of "exactly one alert per loss":
```
10:20:29 the 20-minute latch expires
10:30:40 hub: Host capability: demo-felhom-8363b5 degraded → ok (agent_capability_recovered)
```
So one lost grant produced **one degraded event + one operator e-mail + one recovery**, and nothing
further. Final state on both boxes: agent `0.124.1`, two ACL rows on `/storage/felhom-backup`.
Scenario E is unchanged from v0.123.0 — a failed repair, or one that does not survive the re-read,
leaves the capability degraded with the failure in its reason.
## 5. Part 3 — one bounded pass at the mechanism, negatives named
**The lead is real as a CLASS and is documented in our own installer:** *"`pveum user token remove`
purges the token's ACL, so re-applying post-rotate is mandatory"* — a rotation would take every grant
and re-apply only the hardcoded `PVE_STORAGES` set, which is exactly the observed end state.
**It does not fit this box.** A rotation purges **all** the token's ACLs and mints a **new secret**;
demo-felhom's token still authenticates with the same secret (`--selftest` OK), it kept its other
three storage grants throughout, and only `felhom-backup` was refused. No installer run is evidenced
(no 2026-08-03 install log; host uptime 12 days at the time).
**Ruled out, by name:** a host reinstall; any `pveum`/ACL/`user.cfg` activity in syslog 04:0010:00;
any cluster-log ACL entry; a token rotation **on this box**. **Not ruled out fleet-wide** — any
installer run still purges and re-grants only `PVE_STORAGES`, though installer 1.24.0's reuse-arm fix
now re-grants the backup target on that path.
**The mechanism remains open**, and the row says which half it reflects: mitigation shipped, cause
unexplained. No reproduction was attempted and the session was not extended for it.
## 6. Tests and red-proofs
Green gate both repos: `go build ./... && go vet ./... && go test ./...` rc=0; `agent_gates.py` and
`repo_gates.py` all OK. Test runs and commits always separate commands.
| # | Test | Mutation | Observed |
|---|---|---|---|
| G | `hostinstall_gates.py` offsite assertion | pin `keep_last: 2` back | **FAIL**`arms a CLIENT-SIDE prune (keep_last=2)` |
| F | `TestGrantRepair_IsBounded` | drop the interval check | **FAIL**`5 cycles produced 5 attempt(s)` |
| D(record) | `TestGrantRepair_ARepairedGrantIsReportedAsATransition` | report `ok` after a repair | **FAIL**`got "ok" — the loss would be invisible` |
| D(latch) | `TestGrantRepair_TransitionSurvivesALaterProbe` | ignore the latch in the healthy path | **FAIL**`got "ok" — the host-report would carry ok` |
| H | `TestMainWiresTheGrantRepair` | pass `nil` instead of the repairer | **FAIL**`not passed the repairer` |
| — | `TestGrantRepair_CallsTheExistingWrapperVerb` | — | pass (argv is exactly `grant <id>`; anything else is a policy change) |
| — | `TestGrantRepair_FailureIsReturned`, `_NilIsSafe` | — | pass |
**Two red-proofs did not fail on the first attempt, and both are reported rather than quietly fixed:**
1. **The seam mutation (`grantRepairer` → `nil`) failed to COMPILE** (`declared and not used`), so the
test never ran. The guard holds — you cannot ship it — but it was not demonstrated. The mutation
that does fail it keeps the variable used (`_ = grantRepairer`) and passes `nil`.
2. **The latch red-proof passed**, because the test asserted the latch *helper* rather than the path
consuming it. Fixed by extracting `storeGrantHealthyVerdict` and pointing the test at it — after
which the mutation fails as it should. **This is the second hollow test caught in this file**; the
first asserted a `Status` it had constructed itself. Both are now production functions under test.
Timestamps in the repair tests carry jitter (`09:39:34`, `+13m07s`, `+27m51s`, …) rather than round
intervals, per §10.
## 7. Files, commits, tag
`felhom-agent`: `cmd/felhom-agent/main.go` (repairer, latch, `storeGrantHealthyVerdict`,
`storeGrantRepairedVerdict`, wiring), `cmd/felhom-agent/storegrant_test.go`, `CHANGELOG.md`.
Commits `257c4d8` (v0.124.0) and the v0.124.1 fix. Released shas: **`5e417938…`** (0.124.0),
**`5c279bda…`** (0.124.1), both round-trip verified.
`felhom.eu`: `scripts/felhom-host-install.sh`, `scripts/hostinstall_gates.py`, `scripts/CHANGELOG.md`,
`manifests/webpage.yaml`, `documentation/backlog/OPEN-ITEMS.md`,
`documentation/architecture/07-backup-architecture.md`, `CONTEXT.md`, `STATUS.md`, `REPORT.md`.
**Installer tag: `installer-v1.25.0`.**
## 8. The served installer
```
$ curl -fsSL https://felhom.eu/scripts/felhom-host-install.sh | grep -m1 SCRIPT_VERSION
SCRIPT_VERSION="1.25.0"
$ … | grep -o '"keep_last":[0-9]*'
"keep_last":0
```
Both `--ref=` lines bumped to `installer-v1.25.0` (327, 372); ArgoCD Synced/Healthy; webpage rollout
complete. The served bytes carry the change, not just the version.
## 9. Deployed versions
Both demo hosts run **`felhom-agent 0.124.1`**, deployed from the **published** artifact (downloaded
from Gitea; deployed sha matches the published sha on both). `systemctl is-active` → active on both.
**NOT VOUCHED** — that stays the operator's act.
## 10. Registers
- **R-191 → CLOSED** (installer 1.25.0 + both boxes), carrying the Part 0 evidence *and* the
broken-instrument warning, because the latter is the more reusable lesson.
- **R-190 → MITIGATION SHIPPED, MECHANISM STILL OPEN** — stated in the status field itself so the row
cannot be misread as solved.
- **R-194 → filed** (grep established it free): PVE caches permissions — ~40 s in one run and
~16 minutes in another between deleting both ACL rows and the permission read reflecting it. It puts
a floor under detection speed, makes a single permission read a lagging indicator, and is a
candidate explanation for *when* R-190's refusal surfaced (not for what removed the grant).
- `07-backup-architecture.md`: the offsite retention row was **already correct** — the configuration
disagreed with it. Annotated, because a doc that states a contract does not enforce it; the gate does.
- `ROADMAP.md` holds none of these rows.
## 11. Teardown
The hand-removed grant was restored **by the agent**, which is the test, and confirmed present (2 ACL
rows). Nothing else was provisioned: no scratch storage, no fixture, no prune job, no probe tag.
Nothing was deleted from ep0 or from any tier.
## 12. Observations — noticed, NOT acted on
- **PVE's permission cache is the reason detection is not instant** (R-194). Notably, the storage
*content* listing reflected the loss immediately (`{"data":[]}`) while the *permission* read still
said present — two signals disagreeing, and today only one is read. Worth deciding whether the probe
should consult both.
- **`logCapabilities` runs `probeAll` independently of the report path.** The latch makes that safe
for this capability, but any future probe with one-shot semantics will hit the same trap. The
general fix would be one probe per report cycle, shared — deliberately not attempted here.
- **demo-hp's offsite tier is still `escrow_state: escrowed, snapshot_count: 0`** after this morning's
re-issue; whether the pre-rebuild 15 snapshots reattach is answered by its next offbox run (04:15),
and belongs to R-193 rather than to this session.
- **The `felhom-pbs` tier is armed on both boxes with `cadence=168h`**, so the R-191 fix cannot be
observed end-to-end until that cadence fires. Nothing was triggered manually to shortcut it.