R-224..R-228 CLOSED: registers, capability map, campaign annotation, STATUS
gates / gates (push) Successful in 7s

Five closed in controller v0.202.0 + agent v0.126.0, each with its live or
red-proof evidence in the row. Five explicitly still open and named as such
rather than left to inference: R-214, R-220, R-221, R-213, R-202 — and R-220 is
flagged as currently worked around BY HAND on the campaign venue, which is the
only reason an app could be deployed there.

The capability map's recovery row STAYS FAIL and says why: fixes are not a
re-walk, nothing walked a customer end to end, and the customer-facing messages
were NOT re-driven live because /recovery correctly retires itself once the old
data is set aside — restoring that state is the reconfiguration the task forbade.

The campaign document is ANNOTATED, not rewritten: it records what was true when
it ran, and that is its value.

workspace-CLAUDE.md gains comment-vs-code entry 9 — the escrow header said the
errors were 'DISTINCT on purpose' and named THREE situations while a fourth was
folded into one of them, and a green test named the defect and did not prevent
it because it asserted a STRING one layer below the merge.

ROADMAP needed no collapse — it carries no rows for these IDs.
This commit is contained in:
2026-08-06 08:34:04 +02:00
parent 453e4503a9
commit d30c2a51ed
5 changed files with 71 additions and 40 deletions
+35 -33
View File
@@ -1,6 +1,6 @@
# STATUS — what works, what's broken, what's next
**Updated 2026-08-05.**
**Updated 2026-08-06.**
> **A view, not a source.** `documentation/backlog/OPEN-ITEMS.md` is the authority on open work; this
> page restates part of it in plain words, and **nothing may exist only here**. **Not `CONTEXT.md`**,
@@ -24,11 +24,11 @@ code was wrong. *(CAMPAIGN 11)*
## What's broken
- **A machine installed today would tell its owner their correct recovery code is wrong.** The
recovery screen needs a newer in-house service than a new machine is given; when it asked and got
nothing, it blamed the customer's typing. **We fixed the lie today** — it now says plainly that the
*machine* cannot do this yet, and never accuses anyone. **It still needs one click from you to
actually work on new machines** (below). *(R-216, R-223)*
- **A machine installed today still gets the older in-house service, so it cannot open a recovery
package until you approve the newer one.** It is no longer *lied to* — it says plainly that the
machine cannot do this yet — but **approving the new service is one click from you**, and until then
such a machine also gets the cautious "we do not know why" wording rather than the helpful one.
*(R-216, R-223, R-224)*
- **Three things a rebuilt machine still cannot do by itself.** Its owner cannot re-attach their own
drives, so no app can be put back on its data; it cannot create a new recovery code at all; and the
screen at the machine itself never stops showing a stale pairing code. Each is understood, measured
@@ -37,40 +37,42 @@ code was wrong. *(CAMPAIGN 11)*
that encrypts its own off-site backups, and a rebuilt machine invents a new one. **The good news:
the old key really is kept now — we proved it on a real machine today, for the first time**, and a
changed key raises an alarm the same day. *(R-193, R-198)*
- **The kept older backups cannot be opened yet.** We keep the previous sealed package, and there is
no way to open it. A customer holding exactly the right code for it used to be told they had
mistyped; today the screen names the situation honestly instead — but it still cannot open it, and
it does not pretend otherwise. *(R-222, R-202)*
- **The kept older backups cannot be opened — by anyone.** We keep the previous sealed package and
there is no way to open it. The screens now say exactly that and stop. **One place still promises
otherwise**: the older-backups card says they "may be restorable later with the matching code",
which is not true today. *(R-222, R-202)*
- **The off-site copy can be erased by the machine that made it.** A daily snapshot is armed as a
stopgap. *(R-95, R-87)*
## What last night's stress test found (2026-08-05/06, unattended)
## What last night's stress test found — and what we fixed this morning
We spent the night trying to break the recovery journey with eleven deliberate faults, then left the
machine alone and watched it run on its own. **The good news is real and worth saying first: nothing
we did lost a single byte.** When the customer chose "I do not want the old data", the old backups
were **set aside and not deleted** — we checked the far end of the wire and the 12.5 MB was still
there, untouched, to the byte. A wrong code was refused three times with nothing written and no
lockout. The machine's own alarm fired when we switched it off and cleared itself when it came back.
We spent the night trying to break the recovery journey, then left the machine alone and watched it
run. **Nothing we did lost a byte.** When the customer chose "I do not want the old data", the old
backups were **set aside and not deleted** — we checked the far end of the wire and the 12.5 MB was
still there, to the byte. A wrong code was refused three times with nothing written and no lockout.
The machine's alarm fired when we switched it off and cleared itself when it came back. Overnight it
ran a full cycle on its own and made a fresh off-site copy without being asked.
**What we found is that the machine still tells people the wrong thing when something else is wrong.**
**What it found: the machine still blamed the customer for failures that were not theirs.** Pull the
plug on our own central system and the customer was told their recovery code was bad — in three
hundredths of a second, when actually checking a code takes about one. The machine had not even
tried. **All of that is fixed and deployed** *(R-224, R-226, R-225, R-227, R-228)*:
- **Pull the plug on our own central system, and the customer is told their recovery code is bad.**
Same if the machine's in-house service is stopped. In both cases the code was **perfect** — and the
machine had not even tried it (we can prove that: a real attempt takes about a second, these failed
in three hundredths). The machine knows the difference internally and throws it away before anyone
sees it. **This is the same lie we fixed yesterday, coming back through a different door.**
*(R-224)*
- **A customer who mistypes is no longer told to check their typing** — on any machine that has been
given a new recovery code, that message can no longer appear at all. *(R-226)*
- **The backups page says "0 snapshots · 0 GB" when it cannot read the store** — directly above a
paragraph saying the store contains backups. It really held one snapshot and 12.5 MB. The machine
does not know the number and shows a confident zero instead of "unknown". *(R-225)*
- **After "I do not want the old data", the set-aside backups become invisible.** They are kept, and
the machine writes down exactly where — and then shows that to nobody, ever. *(R-228)*
- **When something on our side is down, we say so** — and we say plainly that the code was **not**
used, so it is still good. Proven on the real machine: with our hub unreachable the answer changed
from "your code is wrong" to "we could not reach the central system".
- **A customer who mistypes is told to check their typing again.** That message had become
unreachable on any machine that had been given a new code — exactly the machine that just recovered.
- **When we do not know why something failed, we say that**, and never guess the customer.
- **"0 snapshots · 0 GB" is gone** where the truth is "we have not read it yet".
- **The set-aside backups are visible again** — the machine says they are kept and not deleted, and
does **not** pretend they can be reopened, because today they cannot be.
**Nothing was fixed last night, on purpose** — a campaign that fixes as it goes is measuring a moving
target. Everything above is written down and ready to work on.
**Still open, and worth knowing:** a rebuilt machine still cannot re-attach its own drives without us
*(R-220 — we are working around it by hand on the test machine right now)*, cannot create a new
recovery code *(R-221)*, and the screen at the machine still shows a stale pairing code *(R-214)*.
**The recovery journey is still recorded as FAILED** — these are fixes, not a re-walk, and it stays
failed until someone walks it end to end with no help from us.
## What shipped recently
File diff suppressed because one or more lines are too long
@@ -20,6 +20,21 @@ Evidence: `../tests/campaign11-evidence-2026-08-05/` — `journal.md` (Phases 0,
---
> **ANNOTATION 2026-08-06 — what has since been fixed. The body below is NOT rewritten.** This
> document records what was true when the campaign ran, and that is its value; the fixes are recorded
> here and in `felhom-controller/REPORT.md`.
>
> **R-224, R-226, R-225, R-227, R-228 are CLOSED** in controller **v0.202.0** + agent **v0.126.0**.
> The unlock path now classifies why it failed — from the value, never the text — and the message
> that mentions typing is reachable only after a real refusal; anything unclassifiable renders a
> neutral message rather than an accusation. Proven live on this venue: same wrong code, hub up →
> `400`, hub REJECTed → `502` naming the connection and stating the code was **not used**, hub
> restored → `400`.
>
> **Unchanged by that work:** the campaign's verdict, the RTO, and the capability map's recovery row,
> which stays **FAIL** until a re-walk passes. **R-214, R-220, R-221 remain open**, and R-220 is still
> worked around by hand on this venue.
## 1. Venue and baselines
| | |
+18 -5
View File
@@ -85,16 +85,29 @@ and `journal-phase24.md` (Phases 2/4). Campaign document:
### Phase 2 — the injected faults, 2026-08-05/06 (unattended)
> **ALL FIVE CLOSED 2026-08-06** in controller **v0.202.0** + agent **v0.126.0**. The rule they now
> enforce, stated so it outlives them: **on the unlock path the customer is blamed only after a real
> attempt REFUSED their code; every other outcome, including an unclassifiable one, says something
> else.**
>
> **STILL OPEN AND DELIBERATELY UNTOUCHED BY THAT WORK — said explicitly rather than left to
> inference:** **R-214** (the console never stops showing a stale pairing code), **R-220** (a rebuilt
> box's drives cannot be re-enrolled — *currently worked around BY HAND on the campaign venue*, which
> is the only reason an app could be deployed there at all), **R-221** (a rebuilt box cannot run the
> escrow ceremony), **R-213** (putting files back), **R-202** (the orphan card's unconditional
> promise — now the **last** place on that surface still promising recoverability, two doors from
> where R-228 removed the same promise).
Eleven faults, each judged on **the message, not the outcome**, each with a positive control proving
the fault was real. Full observables: `tests/campaign11-evidence-2026-08-05/journal-phase24.md`.
| ID | What | State |
|---|---|---|
| **R-224** | **Every non-code failure on the unlock path is reported to the customer as a statement about their code.** Two faults, one defect. **F3** — the hub REJECTed (control: `302``exit 7`) and the **correct current** code returned M4 (*„Ez a kód nem nyitja meg azt a csomagot…"*) in **0.0556 s**. **F4**`felhom-agent` stopped (control: `:8443` gone) and the same correct code returned M4 in **0.0299 s**. Neither elapsed time is compatible with a real unseal (~1.0 s of scrypt; F1's wrong-code attempts took 1.194/1.004/1.014 s), so **no unseal was attempted in either case** — the failure was upstream and was attributed to the code anyway. **The discriminator exists and is thrown away**: the agent's own `err` reads `fetching the sealed bundle: hub: transport error: … no route to host` (F3) versus `the recovery code did not unwrap the identity escrow` (F1), but both return **HTTP 400** under one merged sentence (*"…or the bundle could not be fetched"*), and the controller's failure path has **no branch for "could not ask / could not reach"**. **⚠ The vocabulary already exists and is wired to the wrong condition** — „A gép házon belüli kapcsolata most nem elérhető" fires only when `s.agentClient()` fails to *construct*, never when the RPC fails. **And R-216's gate cannot catch it**: the box's own ring reads `recovery capability gate: offsite_key_recovery=yes (source=version)` — the gate discriminates the agent's **age**, not its **reachability**, so a dead agent of the right version sails through the guard whose own comment says *"An attempt that cannot succeed must never be made, because its failure is attributed to the code."* **This is Phase 1's headline finding relocated from the version channel to the transport.** Breaches **I6** | **OPEN — NOT FIXED.** Fix shape: classify `rerr` (transport/fetch vs unseal) at the agent's HTTP boundary and give the controller a reach-failure branch. Do NOT widen the capability gate into a liveness probe on the unlock path — a probe that must succeed before an unlock adds a second thing that can fail |
| **R-225** | **The remote store reports `0 pillanatkép · 0 / 50 GB` when the box cannot read it — directly above a card stating the store holds backups.** `/backups/remote` renders both at once. **Ground truth, measured over SFTP with the box's own credential (read-only, no decryption):** `/home/felhom-repo/snapshots` holds **`f3d9cd67…`** — the Phase 0 snapshot with all three sentinels, the customer's only surviving copy — and `du -s` returns **12 535 KB**, matching the journal's `repo_size_bytes 12 611 522`. **Mechanism, from the box's own state:** after the rebuild the `offbox` block carries **no `snapshot_count` and no `repo_size_bytes` key at all** — the values are *unknown* and unknown renders as the zero value. **This is R-217's defect class in a second location**, and `OffsiteInventory.Empty` exists precisely because *"len(Apps)==0 is also what a failed read looks like"*. **I5 checked and NOT breached** — the hub's `0.0 GB` is 12.5 MB rounded, and the pool total is consistent; only the box's snapshot **count**, an integer, is false. Breaches **I6** | **OPEN — NOT FIXED.** A count the box has never successfully taken must render as unknown („—"), never as `0` |
| **R-226** | **M1 — the only message that tells a customer to check their typing — is unreachable on any box that has re-escrowed.** The failure path tests M4's condition *before* M1's (`if present, at := s.recoverySuperseded(); present { …M4…; return }`), so on every box the hub keeps an earlier package for, a **genuinely mistyped code** produces M4. **Measured**: F1's three wrong-code attempts each returned M4 after a real ~1 s unseal. M4 is hedged (*„**Ha** egy korábbi kódot adtál meg"*) and states two true facts, but it offers no hint to re-check the ten words and routes the customer to support about *older* backups. **The population that has re-escrowed is exactly the population that has just been handed a new recovery code and is most likely to be typing one.** R-222's fix removed one conflation and introduced another on the same branch | **OPEN — NOT FIXED.** Fix shape: M4 should name both possibilities (a mistype OR an earlier code), since from the engine's side they genuinely are indistinguishable — which is the honest thing to say |
| **R-228** | **After „I do not want the old data", the set-aside history becomes invisible — the box records where it is and shows it to nobody.** The move-aside itself is **correct and was verified byte-for-byte**: `/home/felhom-repo``/home/felhom-repo.orphaned-20260805` with its mtime, its `du -s` (**12 535 KB**) and snapshot **`f3d9cd67`** all unchanged, and a fresh empty repo initialised beside it. **Nothing was deleted.** But `settings.json` then carries `"orphaned_renamed_to": "/home/felhom-repo.orphaned-20260805"` and a census returns **zero** references to `OrphanedRenamedTo` in any template or web handler — the field is written and read by nobody. `/backups/remote` after the set-aside contains no occurrence of the path, „félretéve", „régi előzmény" or any equivalent (instrument controls: `felhom-repo` → 2, „letétbe helyezve" → 1). **12.5 MB of the customer's deliberately retained data sits at a path the box knows and never shows**; its only mention is a flash message on the redirect, gone on the next click. Meanwhile `GET /recovery` → 302 and `POST /recovery/unlock` → 302 with no message, so a customer who changes their mind gets **no explanation at all** (correctly, not a typing accusation — but not an explanation either). **The project's own "seam built but never wired" pattern**, landing on the one promise the set-aside screen makes | **OPEN — NOT FIXED.** Fix shape: surface `OrphanedRenamedTo` on `/backups/remote` as a plain statement that an earlier history is set aside and not deleted. **Do NOT promise it can be reopened** R-222 means it cannot be, and R-202's gate is precisely about not making that conditional promise |
| **R-227** | **A controller restart mid-unlock returns a raw English `Bad Gateway`.** F8 restarted the container at T+0.7 s, inside the unseal window (control: `StartedAt` moved). The customer got **HTTP 502 / „Bad Gateway"** from traefik — a raw upstream error, in English, naming no reason and saying nothing about whether the key was installed. **The state half is clean**: the four `/data/offbox` files stayed byte-identical with mtimes frozen, and the controller returned healthy in 40 s. Breaches **I3** | **OPEN — NOT FIXED.** Low severity — the window is ~1 s wide. Recorded rather than inflated |
| **R-224** | **Every non-code failure on the unlock path is reported to the customer as a statement about their code.** Two faults, one defect. **F3** — the hub REJECTed (control: `302``exit 7`) and the **correct current** code returned M4 (*„Ez a kód nem nyitja meg azt a csomagot…"*) in **0.0556 s**. **F4**`felhom-agent` stopped (control: `:8443` gone) and the same correct code returned M4 in **0.0299 s**. Neither elapsed time is compatible with a real unseal (~1.0 s of scrypt; F1's wrong-code attempts took 1.194/1.004/1.014 s), so **no unseal was attempted in either case** — the failure was upstream and was attributed to the code anyway. **The discriminator exists and is thrown away**: the agent's own `err` reads `fetching the sealed bundle: hub: transport error: … no route to host` (F3) versus `the recovery code did not unwrap the identity escrow` (F1), but both return **HTTP 400** under one merged sentence (*"…or the bundle could not be fetched"*), and the controller's failure path has **no branch for "could not ask / could not reach"**. **⚠ The vocabulary already exists and is wired to the wrong condition** — „A gép házon belüli kapcsolata most nem elérhető" fires only when `s.agentClient()` fails to *construct*, never when the RPC fails. **And R-216's gate cannot catch it**: the box's own ring reads `recovery capability gate: offsite_key_recovery=yes (source=version)` — the gate discriminates the agent's **age**, not its **reachability**, so a dead agent of the right version sails through the guard whose own comment says *"An attempt that cannot succeed must never be made, because its failure is attributed to the code."* **This is Phase 1's headline finding relocated from the version channel to the transport.** Breaches **I6** | **CLOSED 2026-08-06 — controller v0.202.0 + agent v0.126.0.** The discriminator is now a VALUE: `escrow.ErrBundleFetch`**HTTP 502** at the agent, `agentapi.RecoveryRefusal` carrying the status at the controller, and `ClassifyRecoveryFailure` mapping it to one of five classes **from the value, never the text**. **PROVEN LIVE on the venue**, same wrong code, only the hub's reachability changed: `hub up → 400 "…did not open the sealed bundle"` · `hub REJECTed → 502 "…could not be fetched — the recovery code was NOT used"` · `hub restored → 400`. Red-proof: deleting the agent case reproduces `got 400, want 502` with the wrong-code sentence. **Coupled `MinAgent 0.126.0`** — an older agent answers 400 for both causes, so the reading is withheld and the 400 degrades to NEUTRAL; the gate blocks nothing. **The customer-facing messages were NOT re-driven end-to-end**: `/recovery` correctly redirects since F7 set the old data aside, and restoring that state is the reconfiguration §11 forbids — they are covered by handler tests + red-proofs |
| **R-225** | **The remote store reports `0 pillanatkép · 0 / 50 GB` when the box cannot read it — directly above a card stating the store holds backups.** `/backups/remote` renders both at once. **Ground truth, measured over SFTP with the box's own credential (read-only, no decryption):** `/home/felhom-repo/snapshots` holds **`f3d9cd67…`** — the Phase 0 snapshot with all three sentinels, the customer's only surviving copy — and `du -s` returns **12 535 KB**, matching the journal's `repo_size_bytes 12 611 522`. **Mechanism, from the box's own state:** after the rebuild the `offbox` block carries **no `snapshot_count` and no `repo_size_bytes` key at all** — the values are *unknown* and unknown renders as the zero value. **This is R-217's defect class in a second location**, and `OffsiteInventory.Empty` exists precisely because *"len(Apps)==0 is also what a failed read looks like"*. **I5 checked and NOT breached** — the hub's `0.0 GB` is 12.5 MB rounded, and the pool total is consistent; only the box's snapshot **count**, an integer, is false. Breaches **I6** | **CLOSED 2026-08-06 — controller v0.202.0.** `StatsKnown` is a **named** state (the `OffsiteInventory.Empty` pattern), because zero is what an unread store and an empty one both look like and `omitempty` makes "absent" and "0" the same bytes. The fill bar renders only when the fill is known — a 0 %-wide bar is a picture of emptiness. **PROVEN LIVE both ways**: before a run the venue read „a pillanatképek száma még ismeretlen"; after one, „2 pillanatkép … / 50 GB". A measured zero still says zero |
| **R-226** | **M1 — the only message that tells a customer to check their typing — is unreachable on any box that has re-escrowed.** The failure path tests M4's condition *before* M1's (`if present, at := s.recoverySuperseded(); present { …M4…; return }`), so on every box the hub keeps an earlier package for, a **genuinely mistyped code** produces M4. **Measured**: F1's three wrong-code attempts each returned M4 after a real ~1 s unseal. M4 is hedged (*„**Ha** egy korábbi kódot adtál meg"*) and states two true facts, but it offers no hint to re-check the ten words and routes the customer to support about *older* backups. **The population that has re-escrowed is exactly the population that has just been handed a new recovery code and is most likely to be typing one.** R-222's fix removed one conflation and introduced another on the same branch | **CLOSED 2026-08-06 — controller v0.202.0.** The retained-package message now names **both** possibilities and restores the ten-words prompt, because the two are indistinguishable at the engine and saying so is the honest thing. It still does not promise the earlier package can be opened. Red-proof: removing the clause makes the prompt unreachable again |
| **R-228** | **After „I do not want the old data", the set-aside history becomes invisible — the box records where it is and shows it to nobody.** The move-aside itself is **correct and was verified byte-for-byte**: `/home/felhom-repo``/home/felhom-repo.orphaned-20260805` with its mtime, its `du -s` (**12 535 KB**) and snapshot **`f3d9cd67`** all unchanged, and a fresh empty repo initialised beside it. **Nothing was deleted.** But `settings.json` then carries `"orphaned_renamed_to": "/home/felhom-repo.orphaned-20260805"` and a census returns **zero** references to `OrphanedRenamedTo` in any template or web handler — the field is written and read by nobody. `/backups/remote` after the set-aside contains no occurrence of the path, „félretéve", „régi előzmény" or any equivalent (instrument controls: `felhom-repo` → 2, „letétbe helyezve" → 1). **12.5 MB of the customer's deliberately retained data sits at a path the box knows and never shows**; its only mention is a flash message on the redirect, gone on the next click. Meanwhile `GET /recovery` → 302 and `POST /recovery/unlock` → 302 with no message, so a customer who changes their mind gets **no explanation at all** (correctly, not a typing accusation — but not an explanation either). **The project's own "seam built but never wired" pattern**, landing on the one promise the set-aside screen makes | **CLOSED 2026-08-06 — controller v0.202.0.** `OrphanedRenamedTo` is surfaced as two facts and stops. **It does not promise the history can be reopened** — it cannot be, by anyone, today (R-199's inventory is unbuilt) — and the set-aside **confirmation copy was corrected** for the same reason: *"a helyreállítási kód nélkül többé nem lesznek megnyithatók"* implied that WITH the code they could be. The field's own comment said "recovery-code-recoverable", the same over-promise in the code. **PROVEN LIVE**: the notice renders on the venue |
| **R-227** | **A controller restart mid-unlock returns a raw English `Bad Gateway`.** F8 restarted the container at T+0.7 s, inside the unseal window (control: `StartedAt` moved). The customer got **HTTP 502 / „Bad Gateway"** from traefik — a raw upstream error, in English, naming no reason and saying nothing about whether the key was installed. **The state half is clean**: the four `/data/offbox` files stayed byte-identical with mtimes frozen, and the controller returned healthy in 40 s. Breaches **I3** | **CLOSED 2026-08-06 — controller v0.202.0, partially and stated as such.** **The layer that answers is traefik**, whose config this repo generates — but traefik v3 serves no static files, so a branded proxy page needs a **new always-up container** for every 502 on the box: **scoped, not built**. Shipped: the unlock posts via `fetch` and answers a gateway failure in Hungarian in-page. **Progressive enhancement — with no JS the plain POST still shows the proxy's error** |
**Recorded against existing rows by Phase 2:**
+2 -1
View File
@@ -178,7 +178,7 @@ turns a true alarm into one the operator dismisses.
### A comment asserting an invariant needs a test pinning it, or it is a wish
**Eight instances in this project have shipped guarantees the code did not provide** — each survived
**Nine instances in this project have shipped guarantees the code did not provide** — each survived
review because the comment read as settled:
| # | Comment | What it claimed | What the code did |
@@ -191,6 +191,7 @@ review because the comment read as settled:
| 6 | `quiesce.go` | the agent's 409 *prevents* "a spurious failure" | on the start path it produced one (F-A1) |
| 7 | `recovery_unit.go` B2 refusal (R-181) | *"the previous unit is untouched and NOTHING was deleted"* | *nothing deleted* held; **untouched was measured false** — the floor was checked ONLY in `captureAllRecoveryUnits`, while the two dump legs wrote the bulk into the same tree first and unguarded, so a 182,272 B tar became 2,147,666,432 B under a manifest that had not moved |
| 8 | `ResolveManagedFloor` (R-216) | *"never push a controller past the agent it depends on"* | it compared the box's agent against the **golden's** MinAgent while serving a **floor** that could point elsewhere. Raise a floor above the vouched golden — which the day-0 runbook recommends and a per-customer override makes trivial — and the guard checks a version it is not serving. Measured live 2026-08-05: golden 0.192.0/MinAgent 0.113.0, floor 0.200.0, agent 0.120.0 → served, and the box landed on a controller needing 0.125.0. Its customer was then told their correct recovery code was wrong. **The first entry in this table where the false invariant was a GUARD, not a comment alone.** Fixed hub v0.97.0: a floor above the vouched golden is HELD, with its own reason |
| 9 | `escrow/recover.go` header (R-224) | *"The errors below are DISTINCT on purpose"*, naming **three** situations | there were **four**. A failed FETCH was wrapped as an anonymous error and fell through the local-api handler's `default` into the wrong-code answer, so a hub that could not be reached was reported to the customer as a bad recovery code. Measured live 2026-08-05 (CAMPAIGN-11 F3/F4) with a **correct current** code: **0.0556 s** with the hub firewalled off and **0.0299 s** with the agent stopped, against ~1.0 s for a genuine unseal — the machine accused the customer of something it had not attempted. **AND A GREEN TEST NAMED IT AND DID NOT PREVENT IT:** `TestRecoverOffsiteRepoPassword_FetchErrorIsDistinct` has said since v0.125.0 that *"the operator must not be sent to re-read their recovery code because the hub was unreachable"* — it asserted this package's error **string**, one layer below where the merge happened, and a string is not something a caller can branch on. **Mechanism asserted, consequence unpinned.** Fixed agent v0.126.0 (`ErrBundleFetch` → HTTP 502) + controller v0.202.0 (classify by value; the typing message reachable from ONE class; unknown → neutral) |
Three of these (4, 5/6 and 7) were found **on live hardware**, not by review or unit tests — #4 had a
green, red-proofed test suite over a production path that was broken two independent ways, and #7