# REPORT — R-196 / R-204 item 2 (hub v0.95.0), 2026-08-05 **A re-issue no longer marks a healthy escrow stale.** One behaviour change, one register closed, and the coverage claim proved rather than assumed. The controller's half of R-204 (items 1 and 3) is `felhom-controller` v0.198.0. ## 1. Baselines, re-read on arrival | Repo | Expected | Found | |---|---|---| | `felhom.eu` | `2a7ac03c4726` / hub v0.94.0 (deployed `felhom-hub:0.94.0`) | **exact match**, tree clean, `HEAD == origin/main` | **§3.2's landmark had DRIFTED, and the drift changed the work.** The task described `offsite.go:222-231` under a known-consequence comment saying the mark was made on a false premise. That comment had already been rewritten by the R-196 comment-correction commit, and the version on `main` gave a **non-false** ground for the mark: *"the box's re-apply may mint a fresh repository password (it does exactly that whenever `/offbox/repo_password` is absent — the guest-rebuild shape)"*. So the question was no longer "delete a comment's lie" but "is the shape it guards actually covered elsewhere?" — which is Scenario D, and §8.2 says to stop and report if it is not. It is; §3 below is the evidence. ## 2. What changed `offsite.ReissueCredentials` no longer calls `MarkEscrowStale` and no longer emits the `escrow_stale` event. **`offsite_reissued` is untouched** and still fires on every re-issue. The known-consequence comment is rewritten to record what was done, when, and why — with the disagreement below stated in it rather than absorbed. **What the mark actually cost, established mechanically rather than asserted** (this is why it was a blocker and not a nit): 1. `stale_at` set → `GetEscrowStatusForCustomer` **withholds** `restic_pw_sha256` from the report ACK. 2. With no hash, the controller's SLICE-3 auto-confirm returns early and cannot flip `pending → escrowed`. 3. `OffboxRunnable() = OffboxConfigured() && EscrowState == "escrowed"` → **every off-site backup refused**, indefinitely, on a box whose repository key was never in doubt. 4. The customer is told to re-run the recovery ceremony — which mints a new recovery code and supersedes the sealed blob. **During a recovery that is the one act that would have destroyed the key just recovered.** ## 3. Scenario D — the evidence that the removed marking is covered The mark was precautionary and aimed at ONE shape: a re-issue followed by a box that mints a fresh repository password (the guest-rebuild shape, where `offbox/repo_password` is absent). That shape is measured in two independent places, and **the mark was blinding one of them**: - **Continuous, box-side — the real coverage.** `report.EscrowAutoConfirmer.reconcileEscrowed` (controller) compares the ACK's sealed `restic_pw_sha256` against the box's CURRENT local repo password on **every report ACK**, raising the stale flag, the customer card and the „create a new recovery code" CTA on a mismatch. That is a **measurement**, not a guess, and it is continuous rather than edge-triggered. Pinned by the controller's `TestEscrowStale_MismatchWarnsOnceAndFlags` — re-run green this session. **And step 1 above was blinding it:** a stale flag empties the very hash that comparison needs, so the box could only reach the hash-LESS branch and report *"the hub's current blob carries NO password hash"* — which is false. Removing the mark restores the true signal. - **Edge-triggered, hub-side.** R-197's `offsite_repo_key_changed` fires from `api.maybeEmitRepoKeyChanged` on a proven hash difference across a supersession and pages the operator. **Red-proved:** removing the `maybeEmitRepoKeyChanged` call from `handleHostEscrowPut` made `TestEscrowPut_ChangedRepoKey_RaisesSignal` fail with *"the repository key demonstrably changed and NO signal was raised"*, while the two silence tests stayed green. **Disagreement recorded, per the R-96 standing rule.** Scenario D as written asks that a real key change also *"mark the escrow stale"*. **It must not, and nothing was changed to make it.** The hub learns of a real change at the instant a supersession **seals the new password** — i.e. when the escrow is at its freshest. Marking it stale there would ask the customer for a ceremony to fix the ceremony that just ran. The correct consequence at that instant is the operator alarm, which is exactly what R-197 already does. This is recorded in the code comment, the CHANGELOG and OPEN-ITEMS, not only here. ## 4. `MarkEscrowStale` is kept with no caller — deliberately Per task §5 it was not to be modified, and it is not deleted either. The `stale_at` flag remains live and correct — read by the ACK, the operator config card and the PBS-DR view — and the right way to set it is a **future EVIDENTIAL caller** that has measured a key change rather than guessed at one. Its doc comment now says so plainly instead of naming a caller that no longer exists, and `TestEscrowStaleMechanism_StillWithholdsAndClears` keeps the mechanism from decaying to inert while nothing writes it (the seam-built-but-never-wired shape, in reverse). The schema comment and `EscrowStatus.Stale`'s comment were corrected the same way — each of the three previously asserted a writer that is now gone. ## 5. Files modified | File | Change | |---|---| | `hub/internal/offsite/offsite.go` | the pessimistic `MarkEscrowStale` + `escrow_stale` event removed; comment rewritten to record the change, the coverage and the disagreement | | `hub/internal/offsite/offsite_test.go` | `TestReissue_InvalidatesEscrow` **replaced in place by its exact inverse** `TestReissue_DoesNotMarkAHealthyEscrowStale`; new `TestEscrowStaleMechanism_StillWithholdsAndClears` | | `hub/internal/store/store.go` | three comments corrected (`MarkEscrowStale`, the `stale_at` schema note, `EscrowStatus.Stale`) — each had named a writer that no longer exists | | `manifests/hub.yaml` | image tag `0.94.0` → `0.95.0` | | `hub/CHANGELOG.md`, `CONTEXT.md`, `STATUS.md`, `documentation/…` | v0.95.0 entry; ruling **S-32**; the register and architecture updates below | **Commits on `main`:** `d1a8edb` (behaviour + tests + comments) · `5c7d671` (CHANGELOG) · `975a690` (manifest bump). **Deploy:** built + pushed `felhom-hub:0.95.0`, bumped `manifests/hub.yaml`, pushed, then a **deliberate ArgoCD hard-refresh + sync** (auto-sync stays off; no `kubectl set image` anywhere). Result: app `felhom` **Synced / Healthy**, `deploy/hub` rolled out, running image `gitea.dooplex.hu/admin/felhom-hub:0.95.0`, startup log clean (offsite provisioning, pool-box checker, PBS-DR reconciler and all six host checkers initialised; `Listening on :8080`). ## 6. Tests and red-proofs Green gate: `cd hub && go build ./... && go vet ./... && go test ./...` — **rc=0**. `python3 scripts/repo_gates.py --fast` — **all five gates OK**. | Test | Result | Red-proof — what was mutated | Outcome | |---|---|---|---| | `TestReissue_DoesNotMarkAHealthyEscrowStale` (C) | PASS | restored the pessimistic `MarkEscrowStale` block in `ReissueCredentials`, exactly as it was | **FAILED** — *"a re-issue marked a HEALTHY escrow stale…"* | | `TestEscrowStaleMechanism_StillWithholdsAndClears` | PASS | same mutation | **stayed GREEN** — correctly: the mutation restores a *caller*, not a break in the mechanism. That split is the evidence Scenario C's assertion is about the caller and not the flag. | | `TestEscrowPut_ChangedRepoKey_RaisesSignal` (D) | PASS | removed the `maybeEmitRepoKeyChanged` call from `handleHostEscrowPut` | **FAILED** — *"the repository key demonstrably changed and NO signal was raised"* | | `TestEscrowPut_UnchangedRepoKey_Silent`, `TestEscrowPut_HashlessSupersession_NoSignal` | PASS | same | stayed green — the detector's silence branches are independent | | controller `TestEscrowStale_MismatchWarnsOnceAndFlags` | PASS | — (cited as the continuous-coverage pin) | — | Scenario C asserts the **consequence** (the ACK still carries the hash, so auto-confirm can proceed) rather than the mechanism (that a function was not called), because the hash is what the drill's blockage actually turned on. It also asserts that `offsite_reissued` still fires — removing a false alarm must not remove the true notice. ## 7. Live validation **Per task §12 point 5, a live re-issue was NOT run, and must not have been on demo-hp** — it would have been a credential rotation on the box holding the drill's evidence. Part 2 is proved by test and by the deployment being live and healthy. The controller-side halves of R-204 were validated live and are reported in `felhom-controller/REPORT.md`. ## 8. Register and documentation - **`OPEN-ITEMS.md`** — **R-196 → CLOSED (hub v0.95.0)**; **R-204 → items 1–3 CLOSED, item 4 OPEN (→ R-193)** with its dependency named. The header block is updated and states explicitly that **R-202**, **the ~1.2 GB orphaned-ciphertext deletion** and **R-198's retention (still UNIT-PROVEN ONLY — the second deliberate wipe is the next item)** all **remain open**, so nothing is presumed closed by association. R-201 is recorded as PASSED. **R-204 is still the highest ID; nothing new was minted.** - **`architecture/00-capability-map.md`** — the recovery row now says three of the four crutches are gone, names the fixes and their evidence, and states that **item 4 (R-193) is the one that remains** and is why the row **keeps its "with a person present" qualifier**. The **R-199 back-pointer was already present** on the adjacent key-recovery row (added when that row was last corrected), so it needed no further action — verified, not assumed. - **`architecture/07-backup-architecture.md`** — **new §7.0, "What a customer can and cannot do ALONE"**: the four steps in a table with what each cost and its status, plus the honest current answer. This is the section a future reader will use to answer the question. - **`documentation/backlog/ROADMAP.md`** — R-196 and R-204 collapsed per the coupling rule. - **`CONTEXT.md`** — new standing ruling **S-32**, which supersedes S-31's steps 2–5 and carries the blinding mechanism, the fail-closed rule and the "no TTL" reasoning forward. - **`STATUS.md`** — rewritten to **one screen** (191 → ~90 lines) per its own header. It also had a corrupted, half-overwritten "What we're working on" section left from the drill session, which is now gone. Next item stated as the retention drill. **CI:** felhom.eu runs **154** (`5c7d671`, code) and **155** (`975a690`, manifest) — both success. `--no-verify` was **not** used; the pre-push gate ran and passed on every push. ## 9. Observations — noticed, NOT acted on - **`allowedEventTypes` still lists `escrow_stale`**, which after this change has **no producer** in either repo. It is inert rather than harmful; removing an allowlist entry is a behaviour change and is out of this session's scope. - `MarkEscrowStale` is now dead code by call-graph. Kept on purpose (§4 above) — but if a future session's linter or cleanup pass proposes deleting it, the reason it exists is in its doc comment and in the test that exercises it. - `/` on DooPlex is at **86%** used — under the 90% abort line, but worth watching before large builds.