The census answers no, three receipts found, and the prune was on file all along
gates / gates (push) Successful in 13s

CENSUS (read-only, hub store, tester's machine not contacted): no machine that is
not ours can be in the state that cost demo-felhom its history. The hub holds
escrow for three hosts; both demo boxes lost their pre-fix key in the same four
hours on 2026-08-04; peti-felhom and david have no host row and no escrow at all.
A control ran FIRST and had to pass -- the query returned "present (572 bytes)"
for a host known to have material and "absent (NULL)" for one known not to.
Corrected my own instrument on the way: a date-only comparison mislabelled both
losses as after the fix, so the in-force moment is now pinned from the hub's first
post-fix escrow row (11:11:37Z), which independently agrees with the register.

PART 1 ESTABLISHED. The prune is recorded inside R-267 -- the row about the
Configuration page being slow -- because pruning artifacts is what made that page
fast. Arithmetic checks (23+7=30, plus three versions that only surfaced after the
first thirty moved them onto page one = 33) and the PAGINATED listing shows both
generics at exactly ten. R-291's blocking condition is released: the operator was
being asked to establish something already written down. And my counter-argument
yesterday was wrong in exactly the way R-267 warns about -- "containers hold 19"
came from an unpaginated query; paginated they hold 270 and 169.

RECEIPTS: three restored (drives.enrol, backup.tier1, fail.lost-recovery-code),
each citing the document that walked it; the map already read PROVEN-LIVE for all
three, so this follows the map rather than raising a status in the view. NINE
HONEST GREYS. fault.selfheal's best hit argues against it -- an incident recording
self-heal's absence through a 1h15m outage.

THE DECAY RULE FIRED FOR THE FIRST TIME. backup.restore-proof has a receipt from
28 July and is superseded anyway: demo-hp's restore-test failed 5 August and the
box has since been rebuilt. A claim about a continuing behaviour cannot rest on an
old observation. The capability map still reads PROVEN-LIVE and is now the thing
out of step -- recorded, not silently rewritten.

PART 4 specified, not implemented. The orphan card promises restorability the box
rendering it cannot evaluate: the discriminator is on the hub and no wire field
carries it. A conditional promise the system cannot evaluate is the same defect as
an unconditional false one, so the copy stops promising, says what happens, and
names a route. Ships with the next controller change so one bake covers both.
This commit is contained in:
2026-08-10 11:15:37 +02:00
parent 67eced8fbf
commit c04f933d0b
8 changed files with 344 additions and 240 deletions
+129 -131
View File
@@ -1,152 +1,150 @@
# REPORT — the guards, and one thing I could not establish (2026-08-09 evening)
# REPORT — the census, the receipts, and a promise we should stop making (2026-08-10)
No machine touched. Both demo boxes stayed offline and in transit; nothing here needed them.
**Parts 1, 2 and 3 are done. Part 4 and Part 5 are NOT done** — see §6 and §8. Part 5 was marked
droppable and was dropped; **Part 4 was not marked droppable and I ran out of session before it**,
which is a shortfall rather than a decision.
Documentation and one read-only census. **No code that runs on a customer's machine, no deploy, no
bake.** Both demo boxes are online and were not changed. **The tester's machine was not contacted**
only its hub-side record was read, and it has none.
---
## 1. Part 1.1 — the deleter: **NOT ESTABLISHED**
## 1. Part 2's answer, first
**This prompt's account is not corroborated by any source I can reach**, and the prompt itself said
not to take its word for it.
> **No. A machine that is not ours cannot be in the state that cost demo-felhom its history — not
> today, and not for anything enrolled from now on.**
| claim in the prompt | what I found |
The hub holds escrow for **three** hosts. Both demo boxes lost their pre-fix key in the same four-hour
window on 2026-08-04; that is the entire affected population and it is entirely ours. `peti-felhom`
has **no host row and no escrow at all**, and neither does `david`. Anything enrolled from here escrows
under the fixed code, which has been in force since 2026-08-04.
## 2. The census, and the control that came first
**Control (run before the census, and it had to pass or the census was worthless):**
```
demo-felhom-8363b5 host_escrow -> present (572 bytes) want present OK
demo-felhom-8363b5 host_escrow_superseded -> absent (NULL) want absent OK
control PASSED: the query distinguishes both states on known cases.
```
| host | current | superseded | material | superseded_at | verdict |
|---|---|---|---|---|---|
| `demo-felhom-8363b5` | 572 B | id=4 | **ABSENT** | 2026-08-04 07:20:08 | old backups lost (before the fix) |
| `demo-hp-bb76ea` | 572 B | id=3 | **ABSENT** | 2026-08-04 07:15:36 | old backups lost (before the fix) |
| `drill-r50-0a4f9a` | none | 0 | | | no supersession has happened |
| `peti-felhom` | — | — | — | — | **no host record, no escrow** |
| `david` | — | — | — | — | **no host record, no escrow** |
An orphan check found no escrow row pointing at a host the hub does not know.
**One correction I made to my own instrument.** The first run labelled both losses *"superseded AFTER
the fix — unexpected"*, because a date-only comparison puts `2026-08-04 07:20:08` after `2026-08-04`.
The in-force moment is pinned instead from the hub's own first post-fix escrow row — **11:11:37Z**
which independently agrees with the register's *"four hours before v0.93.0 fixed the retention"*. Both
losses are then correctly *before* the fix: explained, not anomalous.
**What this does NOT claim:** that a *retained* blob has ever been unwrapped on a superseded row.
Retention is proven; recovery **from** a superseded row remains unexercised.
## 3. Part 1 — the deleter is ESTABLISHED, and it was on file all along
The locator was correct: the record is **inside the R-267 row** — *"Pruned to the newest 10 per package
on the operator's rule, with the live-vouched golden/agent/floor asserted into the KEEP set before a
single DELETE was issued; 33 deletions, all HTTP 204."*
Every corroboration checked and every one holds:
- **The arithmetic:** 23 agent + 7 golden = 30, plus three older agent versions (0.81.0/0.80.0/0.79.0)
that *"only became visible after the first 30 deletions moved them onto page one"* = **33**.
- **The live PAGINATED listing** — 14 pages, 653 package-versions — `felhom-agent` generic at **exactly
10**, `felhom-golden` generic at **exactly 10**. That is what a newest-10 prune leaves.
The midnight-cleanup candidate is retired, and **R-291's blocking condition is released**: the operator
was being asked to establish something already written down.
**My counter-argument yesterday was wrong, in precisely the way R-267 warns about.** I argued against
the prune because *"container packages hold 19 each"*. That came from an **unpaginated** query the API
caps at 50/page. Paginated, they hold **270** and **169** — they were never in the prune. R-267 records
the identical trap one paragraph above the sentence I could not find: *"An unpaginated listing is not
evidence of a total — this repo's own rule, walked into while measuring."*
## 4. Part 3 — the twelve
**Three restored. Nine honest greys.** The map already read PROVEN-LIVE for the three, so the dataset
was *behind* it — restoring follows the map rather than raising a status in the view.
| claim | outcome |
|---|---|
| "the register records … pruned to the newest ten … 33 deletions, all HTTP 204" | **No such row exists.** The only prune-adjacent row is **R-210**, which is `WAITING-ON-OPERATOR`, says in terms *"Nothing was deleted; this is a list, not an action"*, and concerns **local Docker images on DooPlex**, not the Gitea registry |
| "newest ten per package" | **Not visible in the current state.** `felhom-agent` generic holds 10, but `felhom-controller` and `felhom-hub` container packages hold **19 each** |
| `drives.enrol` | **RESTORED**`audits/SPIKE-raw-drive-enroll-2026-06-15.md`: a live drive walked scan → format → mount → PVE storage → one-click enrol, with the resulting `storage.cfg` entry and mount unit recorded |
| `backup.tier1` | **RESTORED**`audits/CAMPAIGN-8-backup-restore-2026-07-27.md`: adversarial, destructive, unattended, both boxes + ep0; A2 proven end to end |
| `fail.lost-recovery-code` | **RESTORED**`audits/REHEARSAL-byo-reinstall-2026-08-09.md`, and proven the hard way on 2026-08-10 |
| `backup.restore-proof` | **grey — THE DECAY RULE FIRED** (below) |
| `install.installer-by-tag` | grey — hits are ISO spikes; nothing walks a tag rollback |
| `use.lifecycle` | grey — two passing mentions, no walk |
| `drives.migrate` | grey — one or two mentions only |
| `backup.whole-machine` | grey — diagnostics and phase findings, no walk of the claim |
| `fault.selfheal` | grey — **the best hit argues the other way**: `INCIDENT-guest-dhclient-killed-2026-07-20.md` documents self-heal's *absence* through a 1 h 15 m outage |
| `fault.operator-email` | grey — source-verified, delivery never observed |
| `fail.drive-filling` | grey — weak hits only |
| `fail.hub-down` | grey — Campaign 11's hub-unreachable work produced a *finding* (R-224), not a pass |
Everything else I could reach is silent, and silence here is not evidence of anything:
`package_cleanup_rule` is **empty**; `package_version` has **no soft-delete column**, so a deletion
leaves no row; Gitea's `action` feed carries **no package operation at all** across 2026-08-08 →
2026-08-09 and nothing whatever on the evening of 08-08; the Gitea pod has **53 days uptime, 0
restarts**.
## 5. Did the "code moved under the proof" rule fire? **Yes — for the first time.**
**And I withdrew one of my own claims.** R-287 said *"no DELETE on the packages API appears in 48 h of
Gitea router logs"*. Re-checked: `kubectl logs --since=72h` returns nothing older than **2026-08-09
16:35** and contains **zero** `api/packages` lines even for requests I made myself. **The log never
covered the window, so its silence was never evidence.** Withdrawn in the register.
`backup.restore-proof` **has** a receipt: `architecture/_recovery-inventory-2026-07-28.md` carries live
journal lines for scheduled restore-tests on both boxes and both tiers. It is superseded anyway —
demo-hp logged `restore_test_failed` on 2026-08-05, and the box has since been wiped and reinstalled.
The claim is about a **continuing** scheduled behaviour, so a 2026-07-28 observation cannot carry it.
**What remains established, unchanged:** `v0.120.0` downloadable at 2026-08-08 14:29 UTC (run 267
printed `ok v0.120.0`), 404 by 2026-08-09 09:30 UTC (run 284), and `0.128.0` published at 14:47 UTC —
eighteen minutes after the good run. **It may be unestablishable from this side: Gitea keeps no
package-deletion trail.**
**So the rule can fire, and now has.** Two nights ago it fired zero times out of twelve because every
downgrade came from *missing* evidence, not decayed evidence — there was nothing for it to bite on.
**A consequence to note: the capability map still reads `PROVEN-LIVE (2026-08-03)` for that row, so the
map is now the thing out of step**, and it is the source. Recorded rather than silently rewritten.
Per §8 I halted that part's attribution and carried on; the rest is independent.
## 6. Part 4 — the promise, and why the wording is what it is
## 2. The number, and the two places that read it
**Surfaces:**
`felhom-agent/scripts/retention-policy.json``generic_versions_kept: 10`.
| file:line | |
|---|---|
| `controller/internal/web/templates/backups_remote.html:101` | **the false promise** |
| `controller/internal/web/templates/backups_remote.html:98` | the orphan explanation — accurate, keep |
| `controller/internal/web/templates/layout.html:143` | the 14-day abandon countdown — accurate, keep |
| `controller/internal/settings/settings.go:337-338` | `OrphanedRenamedTo` schema comment — accurate, keep |
Read by **`scripts/check-published-versions.py`** (bounds its assertion) and referenced by the prune
procedure. The file states, in its own header, that ten is an **observed state and not a located
ruling**, and that the principled bound is the hub's vouched `min_agent` floor — nothing can install
below it — which needs network the gate does not have.
*(The capability map was searched and makes no such claim — nothing to correct there.)*
**What CI no longer covers:** *a released version older than the retention window is no longer
asserted downloadable.* Its **git tag and config tree are still asserted**; only the binary's presence
is dropped. The check **prints the dropped versions every run**:
**The three cases:** set aside **before** the fix → not recoverable by construction (the restic
password lives only in the identity bundle, `escrow/identity.go:39` read by `escrow/recover.go:91`, and
those rows are NULL); set aside **after** → recoverable in principle, never demonstrated; **today's
population is entirely the first case**.
```
11 released version(s); retention policy keeps the newest 10
NOT ASSERTED (older than the retention window …): 0.120.0
^ these versions still have git TAGS … what is no longer asserted is the BINARY's presence.
```
**The deciding fact: neither the box nor the customer can tell which case they are in.** The hub holds
the discriminator (`host_escrow_superseded.identity_blob`); the box caches only
`HubEscrowIdentityPresent`, which is about the *current* escrow; and **no field on the report or ACK
wire carries superseded-blob retention**. The box renders the card. **A conditional promise the
renderer cannot evaluate is the same defect as an unconditional false one** — so the specified copy
stops promising, says plainly what happens, explains why it cannot promise, and names a route
(write to us).
Three controls: green at 10 naming what it dropped (**exit 0**); widened to 11 the evicted version
re-enters and convicts (**exit 1**, `FAIL v0.120.0`); the policy file removed → **exit 2 INCONCLUSIVE**,
naming the path it tried — never silently unbounded.
**Specification:** `documentation/design/SPEC-orphan-card-copy-2026-08-10.md` — copy, surfaces, and the
render tests that should pin it, including a regression guard that the string `visszaállítható lehet`
never returns. **Not implemented, on purpose:** it lands in the controller, and a controller release is
undelivered until a golden carries it (R-242). It should ship with the next controller change so one
bake and one approval cover both, and the spec says so.
## 3. `felhom-agent` green
## 7. Register
```
reuse-refs OK · instructions OK · published OK · release-complete OK · all agent gates OK
```
**Ceiling R-292 → R-294.** Opened **R-293** (the census) and **R-294** (the promise + spec). **R-287
turned to ESTABLISHED** with my unpaginated-count error withdrawn. **R-291's blocking condition
released.** **R-288 gained a second measured cost**, and it is a different failure mode from the first:
prose rows make claims ambiguous; rows-about-other-things make facts unfindable.
**At `main`: green**, pushed as `53d047a`, and the pre-push hook ran the same entry point.
**At a tag: not re-proved tonight, and I will not claim it.** The available evidence is that the gate
is ref-independent — it enumerates from the Gitea tags API, and runs **190** (`v0.126.0`) and **216**
(`v0.127.0`) were tag pushes that passed. Minting a tag purely to prove it would have published a
release, which this session forbids.
## 8. Observations — noticed, not acted on
## 4. Red-proofs
| # | mutation | asserted applied | outcome |
|---|---|---|---|
| 1 | **tag check removed** | `grep -c` → 1 | **scenario A FAILED, reporting `flash = "artifacts_set"`** — Friday's exact defect returned: the manifest saved with no tag |
| 2 | package check removed | `grep -c` → 1 | scenario B FAILED, `flash = "artifacts_set"` |
| 3 | inconclusive branch mapped onto success | `grep -c` → 2 | scenario E FAILED |
All reverted; `grep -c MUTATION`**0**; all seven tests green again.
**Red-proof 3, stated precisely rather than flatteringly:** with the inconclusive branch mapped onto
success the save did **not** complete — it fell through to `artifact_sha_invalid`. So the mutation
proves the guard is load-bearing for *the message the operator sees*, not for the save itself. That is
the honest reading, and it is exactly the defect scenario E exists to prevent: the operator being told
"missing or invalid" when the truth is "I could not reach the registry".
## 5. The five scenarios, as the operator sees them
| | outcome | the message |
|---|---|---|
| **A** tag missing | REFUSED | *"Refused: that version has no usable git tag. The installer fetches an agent's config files from `raw/tag/v<version>/configs/`, so a version published without its tag makes every fresh install and reinstall fail at step 5 of 8 — as root, on a virgin machine. … Fix it by pushing the tag: `git tag -a v<version> <released-commit> && git push origin v<version>`"* |
| **B** package pruned | REFUSED | *"Refused: that version's artifact is not downloadable. The version is tagged but its package is not in the registry, so a box would 404 fetching the binary itself. … Publish it — `bash scripts/release-agent.sh <version>`"* |
| **C** no checksum | REFUSED | the pre-existing *"Couldn't set the checksum …"* — see the note below |
| **D** both good | **SAVES**, `artifacts_set`, byte-identical behaviour | — |
| **E** registry unreachable | REFUSED | *"Refused: could not verify — this does not mean anything is missing. … It refuses rather than saving with a warning, because a warning beside a success reads as a success. … There is deliberately no override: the registry is on your own server, so if it is unreachable the vouch can wait."* |
**Scenario C changed shape because my first draft modelled nothing real,** and that is worth recording.
With a Gitea client configured, `resolveArtifactSHA` fetches the sha **authoritatively and ignores what
was submitted** — so "submit an empty sha" cannot produce an empty stored sha. The genuine shape is
Gitea answering with no `sha256`, and the **existing** refusal already owns it. The test now pins the
guarantee (*the manifest is unchanged*) rather than a mechanism I had invented.
**No override was built, and none is wanted.** §8's halt condition did not trigger.
## 6. Part 4 — NOT DONE
The twelve downgraded claims were not re-examined and no receipts were searched for. **The honest-grey
count is therefore still the twelve from last night, unverified in either direction**, and
`where-felhom-stands.*` is untouched. This is the session's shortfall: Parts 13 took the budget, and
splitting Part 4 in half would have produced exactly the kind of half-checked green the whole exercise
exists to prevent.
**Whether the "code moved under the proof" rule can fire at all** is therefore still open from last
night, where it fired **zero** times out of twelve — every downgrade came from missing evidence, not
from decayed evidence. My reading remains that it *can* fire but will stay rare until rows cite
evidence at all, which is R-290.
## 7. Hub deployed
**v0.102.0**, live and verified: deploy image `gitea.dooplex.hu/admin/felhom-hub:0.102.0`, rollout
complete, and the page footer reads `0.102.0`. Manifest commit **`36bcd12`**; code commit `b55fc17`.
The image was verified **served by the registry before** the manifest was bumped, not after. ArgoCD's
`felhom` app has `automated.enabled: false`, so the sync was triggered explicitly — **no
`kubectl set image` at any point.**
## 8. Part 5 — dropped, as marked
Not started. It was explicitly droppable and it is the only part that touches customer-facing wording,
which is where a rushed edit does most harm.
## 9. Register
**Ceiling R-290 → R-292.** Opened **R-291** (the narrowing, with its reason, so it can be widened
deliberately) and **R-292** (`artifact_sha_invalid` conflates three facts — found by scenario E
failing, worked around by ordering, message untouched). Closed: **R-273's owed-guards tail**, both
guards built. Corrected: **R-287**, twice.
## 10. Observations — noticed, not acted on
- **R-292 is the interesting one.** A test I wrote to check a new guard failed for a reason that had
nothing to do with the guard, and that reason was a real pre-existing defect. The five scenarios
earned their keep before the feature shipped.
- **The golden gets no tag probe.** It is fetched by version and has no config tree, so a tag probe
would assert something the installer never does. Deliberate, and stated in the code.
- **The gate is skipped entirely when no Gitea client is configured**, or a hub without registry
credentials could never vouch anything. Pinned by its own test, and worth knowing: the guard is only
as present as the client is.
- `check-release-complete` runs in `--fast`, so a missing tag is now caught by the **pre-push hook**,
earlier than CI.
- **The capability map is now out of step in two directions** — behind the dataset for three claims it
already called PROVEN-LIVE, and ahead of it for `backup.restore-proof`. Both point at R-288.
- **`fault.selfheal`'s only real document argues against it.** Worth someone deciding whether the
capability is real and unwalked, or overstated.
- **Retention is proven; recovery from a superseded row is not.** The census proves blobs are now kept;
nobody has ever unwrapped one. That is the next thing worth a drill, and it needs no customer.
- The page now shows the three restored claims as **"moved twice"** rather than once, so an unsettled
status reads as unsettled.