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:
2026-08-08 08:47:02 +02:00
parent 560f0d4451
commit b080ecf411
13 changed files with 836 additions and 58 deletions
+68 -17
View File
@@ -5,6 +5,13 @@ import "encoding/json"
// HostOOBRow is the latest operator-access (OOB) state per host (TASK H1), parsed from the newest
// host_report. Present is false when the agent sent no oob stanza (pre-H1 / feature off) → never
// alerted.
//
// ⚠ THIS STRUCT USED TO MIRROR FIVE OF THE AGENT'S EIGHT OOB FIELDS, and the three it dropped
// included the one that decides the question the OOB checker exists to answer (R-260, G-1). The
// agent has emitted `operator_key_configured` on every heartbeat since v0.72.0 — the same version
// that introduced the stanza itself — and having no field for it here meant encoding/json discarded
// it on arrival, so `oobDegraded` reported a box with felhom-sshd active, reachable, a valid config,
// a configured peer and NO OPERATOR KEY INSTALLED as `ok`. The agent knew and said so.
type HostOOBRow struct {
HostID string
CustomerID string
@@ -14,6 +21,64 @@ type HostOOBRow struct {
Reachable bool
ConfigInvalid bool
OperatorPeerConfigured bool
// OperatorKeyConfigured — the operator's authorized_key is actually installed on the box.
// `operator_peer_configured` above is NOT a substitute: that one only says the peer IP is in
// desired-state, i.e. that OOB is MEANT to work. This says the credential that actually grants
// entry is there.
OperatorKeyConfigured bool
// OperatorKeyReported distinguishes "the agent said false" from "the agent never said".
// Absence must never read as "the key is installed" — that is this defect returning through the
// version door, and this project has watched an absence read as a fact four times.
//
// It cannot be false while Present is true for any RELEASED agent: the field and the stanza
// shipped together in v0.72.0 (2026-07-05), and the vouched floor is far above it. That is a
// claim, so it is pinned by TestOOBDecode_StanzaWithoutOperatorKey_IsNotSilentlyOK rather than
// left as a comment.
OperatorKeyReported bool
// WGHandshakeAgeS / HealedAt are carried for the ALERT MESSAGE, not for the predicate — the
// operator reads this at 07:00 and needs the context, but a check that starts failing for
// reasons nobody asked for is how a check stops being read. nil / "" when not reported.
WGHandshakeAgeS *int64
HealedAt string
}
// decodeOOBInto parses the `oob` stanza of one raw host report into r.
//
// It is a named function rather than an inline literal SO THAT TESTS CAN DRIVE THE REAL DECODE
// BOUNDARY. A test that constructs HostOOBRow by hand cannot see a field that never decodes, which
// is the entire class of defect this exists because of (R-260): every hub test asked what the
// checker did with a row, none asked whether the row could carry the fact.
func decodeOOBInto(r *HostOOBRow, reportJSON string) error {
var body struct {
OOB *struct {
FelhomSshdActive bool `json:"felhom_sshd_active"`
FelhomSshdPort int `json:"felhom_sshd_port"`
Reachable bool `json:"reachable"`
ConfigInvalid bool `json:"config_invalid"`
OperatorPeerConfigured bool `json:"operator_peer_configured"`
OperatorKeyConfigured *bool `json:"operator_key_configured"`
WGHandshakeAgeS *int64 `json:"wg_handshake_age_s"`
HealedAt string `json:"healed_at"`
} `json:"oob"`
}
err := json.Unmarshal([]byte(reportJSON), &body)
if body.OOB == nil {
return err
}
r.Present = true
r.FelhomSshdActive = body.OOB.FelhomSshdActive
r.FelhomSshdPort = body.OOB.FelhomSshdPort
r.Reachable = body.OOB.Reachable
r.ConfigInvalid = body.OOB.ConfigInvalid
r.OperatorPeerConfigured = body.OOB.OperatorPeerConfigured
// A POINTER, deliberately: nil means the agent never said, which is not the same as saying no.
if body.OOB.OperatorKeyConfigured != nil {
r.OperatorKeyReported = true
r.OperatorKeyConfigured = *body.OOB.OperatorKeyConfigured
}
r.WGHandshakeAgeS = body.OOB.WGHandshakeAgeS
r.HealedAt = body.OOB.HealedAt
return err
}
// GetHostOOBStates returns the latest oob stanza per host (mirrors GetHostMgmtPlaneStates). A report
@@ -35,23 +100,9 @@ func (s *Store) GetHostOOBStates() ([]HostOOBRow, error) {
if err := rows.Scan(&r.HostID, &r.CustomerID, &reportJSON); err != nil {
return nil, err
}
var body struct {
OOB *struct {
FelhomSshdActive bool `json:"felhom_sshd_active"`
FelhomSshdPort int `json:"felhom_sshd_port"`
Reachable bool `json:"reachable"`
ConfigInvalid bool `json:"config_invalid"`
OperatorPeerConfigured bool `json:"operator_peer_configured"`
} `json:"oob"`
}
_ = json.Unmarshal([]byte(reportJSON), &body)
if body.OOB != nil {
r.Present = true
r.FelhomSshdActive = body.OOB.FelhomSshdActive
r.FelhomSshdPort = body.OOB.FelhomSshdPort
r.Reachable = body.OOB.Reachable
r.ConfigInvalid = body.OOB.ConfigInvalid
r.OperatorPeerConfigured = body.OOB.OperatorPeerConfigured
if err := decodeOOBInto(&r, reportJSON); err != nil {
// malformed JSON degrades to zero values, never an error (documented above)
_ = err
}
out = append(out, r)
}
+100
View File
@@ -0,0 +1,100 @@
package store
import "testing"
// These tests drive decodeOOBInto — THE REAL DECODE BOUNDARY — with raw report JSON, not a
// hand-built HostOOBRow.
//
// That distinction is the entire point of R-260 and is not a style preference. Every hub test that
// touched OOB before this file constructed a HostOOBRow itself and asked what the checker did with
// it. A test written that way CANNOT SEE A FIELD THAT NEVER DECODES: it hands the struct the value
// the production path would have discarded, and passes. The defect lived under a green suite for
// five weeks because of exactly that.
const oobHealthyJSON = `{"host_id":"h1","oob":{
"felhom_sshd_active":true,"felhom_sshd_port":8822,"reachable":true,
"config_invalid":false,"operator_peer_configured":true,
"operator_key_configured":true,"wg_handshake_age_s":42,"healed_at":"2026-08-08T01:02:03Z"}}`
// The shape the agent sends for the box this whole session is about: everything up, and the key
// that actually grants entry absent.
const oobNoKeyJSON = `{"host_id":"h1","oob":{
"felhom_sshd_active":true,"felhom_sshd_port":8822,"reachable":true,
"config_invalid":false,"operator_peer_configured":true,
"operator_key_configured":false}}`
// An `oob` stanza with NO operator_key_configured key at all. Unreachable for any released agent —
// the field and the stanza shipped together in v0.72.0 — but absence must be structurally
// distinguishable from a reported false, or this defect returns through the version door.
const oobLegacyNoFieldJSON = `{"host_id":"h1","oob":{
"felhom_sshd_active":true,"felhom_sshd_port":8822,"reachable":true,
"config_invalid":false,"operator_peer_configured":true}}`
func TestOOBDecode_CarriesOperatorKeyConfigured(t *testing.T) {
var r HostOOBRow
if err := decodeOOBInto(&r, oobHealthyJSON); err != nil {
t.Fatalf("decode: %v", err)
}
if !r.Present {
t.Fatal("stanza present but Present=false")
}
// THE ASSERTION THIS FILE EXISTS FOR. Before R-260 the struct had no field, so this line does
// not compile against the old code — which is the red-proof, recorded rather than described.
if !r.OperatorKeyConfigured {
t.Error("operator_key_configured=true on the wire did not reach the row — it is being dropped at the decode boundary, which is R-260")
}
if !r.OperatorKeyReported {
t.Error("the field was on the wire; OperatorKeyReported must be true")
}
if r.WGHandshakeAgeS == nil || *r.WGHandshakeAgeS != 42 {
t.Errorf("wg_handshake_age_s did not decode: %v", r.WGHandshakeAgeS)
}
if r.HealedAt != "2026-08-08T01:02:03Z" {
t.Errorf("healed_at did not decode: %q", r.HealedAt)
}
}
func TestOOBDecode_ReportedFalseIsDistinctFromNotReported(t *testing.T) {
var no HostOOBRow
if err := decodeOOBInto(&no, oobNoKeyJSON); err != nil {
t.Fatalf("decode: %v", err)
}
if no.OperatorKeyConfigured {
t.Error("operator_key_configured=false decoded as true")
}
if !no.OperatorKeyReported {
t.Error("the agent DID say false; that must be distinguishable from never saying")
}
var legacy HostOOBRow
if err := decodeOOBInto(&legacy, oobLegacyNoFieldJSON); err != nil {
t.Fatalf("decode: %v", err)
}
if legacy.OperatorKeyReported {
t.Error("no operator_key_configured key on the wire, yet OperatorKeyReported=true — absence is being read as a statement")
}
// Both are `false`; only the Reported flag tells them apart. If a future change collapses the
// pointer to a plain bool, this is the assertion that fails.
if no.OperatorKeyConfigured != legacy.OperatorKeyConfigured {
t.Fatal("precondition of this test changed")
}
}
func TestOOBDecode_AbsentStanzaIsNotPresent(t *testing.T) {
var r HostOOBRow
_ = decodeOOBInto(&r, `{"host_id":"h1"}`)
if r.Present {
t.Error("no oob stanza, yet Present=true")
}
if r.OperatorKeyReported {
t.Error("no oob stanza, yet OperatorKeyReported=true")
}
}
func TestOOBDecode_MalformedDegradesToZeroNeverPanics(t *testing.T) {
var r HostOOBRow
_ = decodeOOBInto(&r, `{"oob":`) // truncated
if r.Present {
t.Error("malformed JSON must not yield Present=true")
}
}