hub v0.99.0 — the hub can see whether the operator can get in (R-260); G-1 gate closes, R-247 closes
oobDegraded tested five things and the sixth never arrived. The agent has emitted `operator_key_configured` on every heartbeat since v0.72.0 — the SAME version that introduced the `oob` stanza carrying it — and store.HostOOBRow mirrored five of the agent's eight OOB fields. With no field for it, encoding/json discarded the fact on arrival, so a box with felhom-sshd active, reachable, a valid config and a configured peer reported `ok` with NO OPERATOR KEY INSTALLED AT ALL. Not a wrong answer: an answer to a question nobody was asking. `operator_peer_configured`, which the hub did read, only says the peer IP is in desired-state — that OOB is MEANT to work, not that entry is possible. Now decoded: operator_key_configured, plus wg_handshake_age_s and healed_at. The last two ride the ALERT TEXT and are deliberately NOT in the predicate — widening a check beyond the fact that is now arriving is how a check stops being read. SCENARIO F, decided on a measurement rather than a preference. operator_key_configured decodes as a POINTER: nil = the agent never said, reported distinctly and never as ok. The version gate was rejected because the field and its stanza shipped in the SAME agent version (v0.72.0), so a stanza without the field cannot come from any released agent; the fleet is 0.113.0/0.127.0 and the vouched floor is 0.127.0. Handled explicitly anyway and pinned, because "cannot happen" is a claim this project has been burned by. THE MESSAGE NAMES THE FAULT. oobDegradedReason is the single source for both predicate and text, so the alert can never name a different fault from the one that fired. The old form derived it separately and had a vocabulary of two — unreachable, or config invalid — with no way to say the key is missing. The operator reads this at 07:00. TESTS DRIVE THE DECODE BOUNDARY. Every hub OOB test before this built a HostOOBRow by hand, and a test written that way CANNOT SEE A FIELD THAT NEVER DECODES — which is how this held a green suite for five weeks. The pre-existing fixture oobReport() also omitted the field, so those scenarios ran against a report shape no released agent produces (same family as R-262). Both fixed. Red-proofs, 8 expected outcomes and 0 wrong, each with the mutation asserted applied: dropping the field returns the false ok; an unconditional check alerts a healthy box; unknown-as-ok restores the silent pass. G-1 CLOSED — scripts/wire_contract_gate.py shipped as ranked, built BEFORE the fixes and seen failing on 40 fields (documentation/tests/wire-contract-gate-2026-08-08/BEFORE.md). Two instrument defects the control caught first: a substring false negative (grep -F healed_at matched privsep_healed_at) and treating dr_recipe as wholly opaque when its top-level sections ARE decoded through an allow-list that already cost offsite_restic (R-122). The prompt for this session said "465 emitted tags, eight unreachable". Checked against the repo: R-260 said "at least eight DECISION-BEARING facts", never eight tags. The real count is 40. R-260 CLOSED (class gated, sharpest instance fixed). R-247 CLOSED (controller v0.209.0). R-264 MINTED and OPEN — the 21 facts with no consumer, allowlisted with reasons so that gating the class could not be mistaken for deciding them. Still open and named: R-246, R-255..R-259, R-261..R-263, and C7's test-comment half. Capability map checked: it claims OOB access is implemented, never monitored, so no row was untrue; what was untrue sat one layer down and the row now records it. repo_gates --fast: all 8 OK. go build/vet/test green in hub, run separately from this commit.
This commit is contained in:
@@ -1,3 +1,48 @@
|
||||
## v0.99.0 — the hub can finally see whether the operator can get in (2026-08-08, R-260 / G-1)
|
||||
|
||||
**`oobDegraded` tested five things and the sixth never arrived.** The agent has emitted
|
||||
`operator_key_configured` on every heartbeat since v0.72.0 — the same version that introduced the
|
||||
`oob` stanza carrying it — and `store.HostOOBRow` mirrored five of the agent's eight OOB fields. With
|
||||
no field for it, `encoding/json` discarded the fact on arrival, so a box with felhom-sshd **active,
|
||||
reachable, a valid config and a configured peer** reported `ok` **with no operator key installed at
|
||||
all**. That is not a wrong answer; it is an answer to a question nobody was asking.
|
||||
|
||||
`operator_peer_configured`, which the hub did read, is not a substitute: it says the peer IP is in
|
||||
desired-state — that OOB is MEANT to work — not that entry is possible.
|
||||
|
||||
**Decoded now:** `operator_key_configured`, plus `wg_handshake_age_s` and `healed_at`. The last two
|
||||
are carried for the ALERT TEXT and are deliberately NOT in the predicate — a check that starts
|
||||
failing for reasons nobody asked for is how a check stops being read.
|
||||
|
||||
**`operator_key_configured` decodes as a POINTER.** nil = the agent never said, which is not the same
|
||||
as saying no. Absence must never read as "the key is installed" — that is the same defect returning
|
||||
through the version door, and this project has watched an absence read as a fact four times. A stanza
|
||||
without the field cannot come from any released agent (field and stanza shipped together in v0.72.0,
|
||||
and the vouched floor is far above it), so it is handled explicitly rather than assumed impossible,
|
||||
and reported DISTINCTLY from a known-missing key.
|
||||
|
||||
**The message names the fault.** `oobDegradedReason` is now the single source for both the predicate
|
||||
and the alert, so the text can never name a different fault from the one that fired. The old form
|
||||
derived the reason separately and had a vocabulary of exactly two — "unreachable" or "config
|
||||
invalid" — with no way to say "the key is missing". The operator reads this at 07:00 and needs to
|
||||
know which of five things is wrong.
|
||||
|
||||
**Tests drive the DECODE BOUNDARY, not a hand-built struct.** Every hub OOB test before this one
|
||||
constructed a `HostOOBRow` itself and asked what the checker did with it — a test written that way
|
||||
cannot see a field that never decodes, which is exactly why this survived five weeks under a green
|
||||
suite. `host_oob_decode_test.go` feeds raw report JSON; `TestHostOOB_MissingOperatorKey_EndToEnd` and
|
||||
`TestHostOOB_NoKeyField_IsNotSilentlyOK_EndToEnd` drive the whole path from JSON to emitted event.
|
||||
|
||||
**The existing fixture was part of the problem and was fixed too:** `oobReport()` omitted
|
||||
`operator_key_configured` entirely, so every pre-existing scenario ran against a report shape **no
|
||||
released agent produces**. Same family as R-262.
|
||||
|
||||
Red-proofs, each with the mutation asserted applied: dropping the field from the decoder returns the
|
||||
false `ok`; making the check unconditional alerts a healthy box; treating unknown as ok restores the
|
||||
silent pass.
|
||||
|
||||
**Gate:** `felhom.eu/scripts/wire_contract_gate.py` (G-1), `--fast`, registered in `repo_gates.py`.
|
||||
|
||||
## v0.98.0 — drop the retained recovery package when the box says its set-aside history is gone (2026-08-07, R-241)
|
||||
|
||||
The hub half of the controller's abandonment countdown, and the **only** reason the hub was touched
|
||||
|
||||
Reference in New Issue
Block a user