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:
+42
@@ -17,6 +17,48 @@
|
||||
|
||||
## Standing rulings
|
||||
|
||||
**S-38 — A FACT ONE SIDE EMITS AND THE OTHER CANNOT RECEIVE IS A DEFECT, AND A CHECK NOW SAYS SO
|
||||
(2026-08-08, G-1 / R-260 / R-247).**
|
||||
|
||||
`scripts/wire_contract_gate.py`, `--fast`, registered in `repo_gates.py`. For every json tag reachable
|
||||
from a declared wire ROOT, it asserts that the literal tag occurs somewhere in the receiving repo's
|
||||
production Go or templates. **A tag occurring nowhere cannot be decoded by any struct, named or
|
||||
anonymous** — which is why it is a string test and not a struct-to-struct comparison: the hub decodes
|
||||
one report through several ad-hoc anonymous structs, and shape-pairing false-positived badly when
|
||||
Campaign 12 tried it.
|
||||
|
||||
1. **Adding a field to a wire is a TWO-REPO change**, like the DR-recipe coupling rule. Emitting it is
|
||||
half the work; the gate refuses the other half being skipped.
|
||||
2. **A field that genuinely has no receiver goes in the gate's `ALLOWLIST` WITH A REASON.** A quiet
|
||||
exclusion is a dropped field with paperwork. Two kinds of entry exist and the difference is
|
||||
deliberate: *redundant* (the hub already decodes an equivalent — the matter is closed) and *R-264*
|
||||
(no consumer today and one is arguably owed — **allowlisting is not deciding**).
|
||||
3. **The roots are declared, and an unresolvable root is INCONCLUSIVE, never a pass.** Not covered,
|
||||
and the gate says so in its own output: the hub's desired-state (served as raw stored
|
||||
`host.DesiredJSON`, no typed emitter to walk) and the agent's local API (no single root type).
|
||||
4. **`dr_recipe` is opaque BELOW depth 1, not opaque.** The hub stores each half as `json.RawMessage`
|
||||
and re-emits nested shapes verbatim, so the leaves are genuinely off the wire — but the TOP-LEVEL
|
||||
section keys are decoded by `hostHalfShape` / `appHalfShape`, and those are allow-lists that
|
||||
already silently swallowed `offsite_restic` for months (R-122). The sections are checked.
|
||||
|
||||
**Scenario F's choice, and why (the same ruling's second half).** When the receiving side gains a
|
||||
field, the absence of that field from an older emitter must not read as a value. `operator_key_configured`
|
||||
decodes as a **pointer**: nil means the agent never said, and is reported **distinctly** from a
|
||||
reported `false` — never as `ok`. Three options were on the table (unknown = degraded / unknown =
|
||||
its own state / gate on the agent version); the version gate was rejected **on a measurement, not a
|
||||
preference**: the field and the `oob` stanza that carries it shipped in the SAME agent version
|
||||
(v0.72.0, 2026-07-05), so a stanza without the field cannot come from any released agent, the live
|
||||
fleet is on 0.113.0 and 0.127.0, and the vouched floor is 0.127.0. Building version-gating machinery
|
||||
the hub does not otherwise have, to handle a case no box can be in, would have been cost without
|
||||
cover. The unreachable case is handled explicitly anyway — "cannot happen" is the kind of claim this
|
||||
project has been burned by — and is pinned by `TestHostOOB_NoKeyField_IsNotSilentlyOK_EndToEnd`.
|
||||
|
||||
**And the test-shape lesson underneath it.** Every hub OOB test before this built a `HostOOBRow` by
|
||||
hand. **A test written that way cannot see a field that never decodes**, which is how the defect 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. Tests for a wire contract drive the
|
||||
**decode boundary** with raw JSON.
|
||||
|
||||
**DooPlex's own backup, surveyed 2026-08-06 (R-232, read-only).** Five sets nightly, 14/14 in 14
|
||||
days, single-file restore proven byte-identical. Destination for **every** set is `/dev/sda1`;
|
||||
`.claude-memory` and the PostgreSQL dumps are the two same-disk cases. **Nothing leaves the box** —
|
||||
|
||||
Reference in New Issue
Block a user