hub v0.104.0: the guest network gets a reader (R-319), and the hub half of the naming (R-295)
gates / gates (push) Successful in 14s

Four paper debts and one fact given a reader. Hub-only — nothing to bake.

A4 — the entry about "the tester's machine" named a risk correctly and labelled it
in a way that invited deleting it. Established from the hub's own store: `peti-felhom`
is a REAL machine (482 reports, 2026-02-27 → 2026-07-15, a named person's own box) and
the 3.6 GB with no key and no backup is real. `david` → `tester-1` is a DIFFERENT record
with no host, no escrow and no report, ever — deleted 07:55:49 and re-created 07:56:47
this morning. The prompt's premise conflated the two; the register now says which is which.

A1 — R-312/R-313/R-303 recorded as DECIDED with their re-open triggers, and moved out
of STATUS's "Waiting on you", which is now empty.

A3 — day0-install §C.1 said pushing the installer publishes it. It has not since
R-110. Corrected, with the two manifest pins named and an outside-verification command;
the one copy that repeated it (a dated audit, true when written) carries a superseded note.

A5 — standing rule 5: evidence comes off the machine at the end of the phase that
produced it, before any revert. Earned twice in three days on the same box at the same
point (R-320). Four homes, plus what to do when it is already gone.

R-295 hub half — „Beállító kód" everywhere; „Visszaállító kód" retired. New `reenroll`
mail kind so the mail names the page a REBUILT box actually shows („A szerver
beállítása"), not the „Elfelejtett jelszó" page it has no login screen to reach.
Naming only; the acceptance pin proves the secret is untouched.

R-319 — the hub models `guest_net` after 23 days of receiving and discarding it. The
signal is `heals_last_hour`, not `state`: a guest the watchdog keeps repairing reads
healthy between repairs. `heal_succeeded` decoded too (R-260's lesson). Unknown is never
drawn as healthy — three absences, three sentences. No alarm, deliberately.
Three red-proofs, mutations asserted applied. Wire-gate checked tags 182 → 190.

B1 — the operator's 2026-08-12 dispositions were NOT in the register; they are now.
Third allowlist kind for the five ruled "no reader wanted"; `reporting_disabled`
reclassified redundant. 8 read · 5 deliberately unread · 1 redundant · 6 still owed.

Also filed: R-321 (a deliberately-silent box still alarms stale/down — the checker is
age-only, and decoding the flag would not have fixed it), R-322 (the claim guard has
never scanned the hub; a hand scan returns zero, so it is a scope gap, not a defect).
This commit is contained in:
2026-08-13 10:50:12 +02:00
parent 2d05b29b82
commit 4d6ec7c7bb
22 changed files with 1253 additions and 115 deletions
+107
View File
@@ -1,3 +1,110 @@
## v0.104.0 — the hub can see whether a guest's networking works, and one name per secret (2026-08-13, R-319 + R-295 hub half)
Two things, both hub-only. **No wire change, no agent change, no controller change, nothing to bake.**
### R-319 — the guest-network watchdog finally has a reader
The agent has reported `guest_net` on every heartbeat since **v0.92.0** (R-54, 2026-07-21). The string
`guest_net` occurred **nowhere** in this repository: the stanza was stored as raw text inside
`report_json` and read by nothing — no check, no alarm, no screen. This is the first of R-264's
twenty-one unconsumed facts to be given a reader, and it was chosen because it has a live incident
behind it: a killed `dhclient` in a guest took a tunnel down for 1 h 15 m with nobody told
(`audits/INCIDENT-guest-dhclient-killed-2026-07-20.md`).
**Established before anything was written.** The facts arrive and are persisted:
`demo-felhom-8363b5`'s newest row carries `guest_net.checked_at` plus per-guest
`vmid/state/mode/ip/has_route/dhclient_alive/checked_at/message`, and the agent's wire type
(`felhom-agent/internal/hub/report.go:139-160`) additionally carries `healed`, `heal_succeeded`,
`last_heal_at`, `heals_last_hour`, `damped` — absent from the live rows only because they are
`omitempty` on a box that has never needed a repair.
**THE SIGNAL IS THE REPAIR COUNT, NOT THE STATE.** A guest whose network the watchdog keeps repairing
is healthy at every instant anyone looks and is nevertheless failing. Rendering `state` alone would
give that machine a green tick — the exact shape of the defect that drew a failed disk as a healthy
empty disk. `heals_last_hour` is therefore surfaced *beside* the state, not behind it, and drives the
card's summary badge.
`heal_succeeded` is decoded too, deliberately. **R-260 is this project's standing warning that a
decoder three fields short of the agent dropped the one that decided the question the checker
existed to answer.** Six failed repairs is a guest that is down; six successful ones is a nuisance.
**An unknown is never drawn as healthy.** Three absences are kept apart, each with its own sentence,
following the companion-flag convention (`hostNetworkView` is the sibling):
- the agent predates v0.92.0 — absence is expected, and still tells us nothing;
- a capable agent sent no stanza — the watchdog is default-on, so this means it was switched off;
- a guest whose own `state` is empty or unrecognised — the state switch is an ALLOW-LIST, so adding a
state to the agent can never silently paint it green here.
A malformed stanza degrades to unknown and **must not 500 the page** — one box's bad field cannot
break the host page for the rest of the fleet.
**No alarm was added, and that is a judgement rather than an omission.** The incident behind this was
about nobody being able to *see* the condition, not about nobody being paged; and a new alarm on a
fleet of two demo machines is untested noise on a dispatcher whose severity contract is exact-match
lowercase. Revisit when a third machine exists, or when a repair count is observed climbing on real
hardware — the visible count is what will supply that evidence.
**Red-proofs, each mutation asserted applied by grep before its run and reverted after:**
1. the unknown branches replaced by the healthy badge → **both** scenario-C sub-cases go red, a silent
machine seen rendering as healthy. *(the one that matters)*
2. `RepairCount: 0` in the decoder → B goes red on *"a guest repaired 6 times in an hour is reported
as fine"*.
3. `Degraded()` forced true → A goes red on *"a machine that is fine is being alarmed on"* — the guard
is reachable in both directions, not just the alarming one.
**Positive control that the wiring is real and not merely written:** `wire_contract_gate.py`'s
checked-tag count rose **182 → 190** and its skipped count fell **88 → 80**, because the eight
`guest_net` allowlist entries were REMOVED. An allowlisted tag is *skipped*, so leaving them would
have meant the new reader's own fields were never checked for reachability at all.
### R-295 hub half — one name per secret, and the mail names the page the machine shows
The box side shipped 2026-08-10; the hub side was dropped twice and was the last place the retired
name survived — in the **mails**, the one surface a customer reads *before* they see any screen.
- the three-word dashboard code is **„Beállító kód"** everywhere: the mail bodies, the mail subjects,
the operator button, and the lockout notice;
- the ten-word escrow code stays **„Helyreállítási kód"**, and no dashboard-code mail names it —
naming both secrets in one message is how a customer comes to believe they are the same thing;
- **„Visszaállító kód" is retired.**
**New email kind `reenroll`, because the mail must name the page the machine is actually showing.**
`ReissueForReenroll` sent the *reset* mail, which directs the customer to an „Elfelejtett jelszó"
page — but a rebuilt box has no password, so the controller renders „A szerver beállítása"
(`web/claim.go:279`, `reset := s.authEnabled()`) and serves **no login page at all**. The route named
was not on their screen. Only the hub can tell the two situations apart, because it is the hub that
chose which call site fired. Same secret, same name, different sentence — which is exactly what the
ruling asks for. The re-enrol mail deliberately says **nothing** about apps or backups: a clean-slate
reinstall is precisely where such a reassurance could be false.
**This is naming, not function.** No acceptance logic moved.
`TestReenrollSplit_ChangesTheMailNotTheSecret` pins it: the generation still advances, the stored
bcrypt hash still verifies the emailed code, no plaintext is persisted. On the box side the companion
pin is the controller's `TestResetCode_StillAcceptedOnTheSetupPage`.
**Two red-proofs:** routing re-enrolment back to `EmailReset` reddens both the new pin and the
pre-existing `TestReissueForReenroll`; putting „Visszaállító kód" back in the reset body reddens
`TestFormatClaimEmail_OneNamePerSecret`.
**The claim guard has never scanned the hub.** `retrieval_promise_gate.py` lives in
`felhom-controller/controller/scripts/` and its declared surfaces are that repo's templates plus one
Go handler file. A scan of `felhom.eu/hub/` with the gate's own four stems returns **zero** — so
nothing was hiding, and the new strings carry no stem and need no registration. That the gate's scope
excludes a customer-facing surface is recorded as R-322 rather than fixed here.
### Also
- `wire_contract_gate.py` grows a **third** entry kind — `not consumed, deliberately` — carrying the
operator's 2026-08-12 rulings with their date. Five facts move to it; `reporting_disabled` moves to
`redundant`. **Those rulings were not in the register before today**, which is why R-264 now records
them. Where the twenty stand: **8 read · 5 deliberately unread · 1 redundant · 6 still owed**.
- **R-321 filed, not fixed:** the staleness checker is age-only, so a box on which reporting is
deliberately switched off still alarms `node_stale` then `node_down`. Decoding `reporting_disabled`
would have felt like progress and left the alarm firing; the fix belongs in the checker, which
already holds the `health.status = "disabled"` it needs.
## v0.103.0 — a host can read the packages we kept for it (2026-08-12, R-311)
**`ListSupersededEscrow` had zero production callers for nineteen days.** It is the only reader of a