3bf62b95bb
gates / gates (push) Successful in 33s
CI convicted ALL 174 checked tags while the pre-push hook was green. Cause, read from the run log rather than guessed at the second attempt: the search used `grep -rnE --include=…`, and the CI runner's image carries python3 and git and deliberately little else — its grep does not support `--include`, so stdout was empty and the gate read empty as "the tag is absent". That is a gate silently treating a tool failure as a finding, which is worse than no gate, and it is exactly the error-swallowing this repo forbids. A green from it would have been just as untrustworthy as the red. Fixed by removing the dependency, not by working around it: the search is now pure Python — one token index per receiving repo, built in a single pass, no subprocess. Faster too (one walk instead of ~350 greps), and unreadable-file / empty-repo cases now exit 2 INCONCLUSIVE rather than reporting absence. THE BEFORE CAPTURE WAS RE-VERIFIED, NOT RE-GENERATED — the stronger claim. All 40 fields recorded in BEFORE.md were re-tested against the new implementation: agree=40, disagree=0, i.e. exactly the four this session fixed are now present and the other 36 still absent. The number 40 stands under both implementations; only the mechanism changed. The whole-token property survives by construction — a token index treats `healed_at` and `privsep_healed_at` as distinct tokens. This is the THIRD instrument defect this gate's own controls caught before it was trusted, after the substring false negative and the dr_recipe over-opacity. The first two were caught by re-finding the known instances; this one by the CI-versus-hook disagreement the workflow's alarm mail explicitly says outranks whatever the push was for.
108 lines
6.0 KiB
Markdown
108 lines
6.0 KiB
Markdown
# The wire-contract gate, run against the tree at b7fb211 on 2026-08-08
|
||
|
||
Deliverable 1 of the G-1/R-260 session: the gate FAILING, before any field was fixed.
|
||
Captured verbatim. If this had been green, the gate would not work and that would be the finding.
|
||
|
||
```
|
||
wire-contract gate — 210 tag(s) checked across 3 declared wire(s); 51 skipped (generic / opaque / allowlisted)
|
||
|
||
agent -> hub (POST /host-report)
|
||
the RECEIVER (hub) contains no occurrence of:
|
||
applied_at (emitted at pbs_dr.applied_at)
|
||
checked_at (emitted at guest_net.checked_at)
|
||
cpu_temp_c (emitted at host.cpu_temp_c)
|
||
damped (emitted at guest_net.guests.damped)
|
||
dhclient_alive (emitted at guest_net.guests.dhclient_alive)
|
||
disk_bytes (emitted at guests.spec.disk_bytes)
|
||
guest_net (emitted at guest_net)
|
||
has_route (emitted at guest_net.guests.has_route)
|
||
heal_succeeded (emitted at guest_net.guests.heal_succeeded)
|
||
healed_at (emitted at oob.healed_at)
|
||
healed_recently (emitted at mgmt_plane.healed_recently)
|
||
heals_last_hour (emitted at guest_net.guests.heals_last_hour)
|
||
last_handshake_age_s (emitted at wireguard.last_handshake_age_s)
|
||
last_heal_at (emitted at guest_net.guests.last_heal_at)
|
||
loadavg (emitted at host.loadavg)
|
||
memory_bytes (emitted at guests.spec.memory_bytes)
|
||
memory_total_bytes (emitted at host.memory_total_bytes)
|
||
memory_used_bytes (emitted at host.memory_used_bytes)
|
||
model_name (emitted at storage_targets.smart.model_name)
|
||
mount_inventory (emitted at restore_tests.mount_inventory)
|
||
mount_parity (emitted at restore_tests.mount_parity)
|
||
operator_key_configured (emitted at oob.operator_key_configured)
|
||
selfupdate_pending (emitted at selfupdate_pending)
|
||
selfupdate_pending_version (emitted at selfupdate_pending_version)
|
||
uptime_seconds (emitted at host.uptime_seconds)
|
||
wg_handshake_age_s (emitted at oob.wg_handshake_age_s)
|
||
|
||
controller -> hub (POST /report)
|
||
the RECEIVER (hub) contains no occurrence of:
|
||
config_hash (emitted at config_hash)
|
||
last_db_dump (emitted at backup.last_db_dump)
|
||
last_integrity_check (emitted at backup.last_integrity_check)
|
||
load_avg_1 (emitted at system.load_avg_1)
|
||
load_avg_15 (emitted at system.load_avg_15)
|
||
load_avg_5 (emitted at system.load_avg_5)
|
||
memory_total_mb (emitted at system.memory_total_mb)
|
||
memory_used_mb (emitted at system.memory_used_mb)
|
||
migrated_to (emitted at storage.migrated_to)
|
||
reporting_disabled (emitted at reporting_disabled)
|
||
stacks (emitted at stacks)
|
||
temperature_celsius (emitted at system.temperature_celsius)
|
||
uptime_seconds (emitted at system.uptime_seconds)
|
||
|
||
hub -> controller (report ACK, `escrow` object)
|
||
the RECEIVER (controller) contains no occurrence of:
|
||
escrow_stale (emitted at escrow_stale)
|
||
|
||
WIRE-CONTRACT GATE FAILED: 40 emitted field(s) cannot be received.
|
||
A tag whose literal string occurs nowhere in the receiving repo cannot be decoded
|
||
by any struct, named or anonymous — encoding/json discards it on arrival.
|
||
Fix: model the field on the receiving side and say what consults it — or, if it is
|
||
deliberately not consumed, add it to ALLOWLIST WITH A REASON. Never a quiet skip.
|
||
|
||
Blind spots (a green is not full coverage — see the module docstring):
|
||
generic tag names are not checked; reachability of a NAME is not use of a VALUE;
|
||
only the declared ROOTS are covered (hub desired-state and the agent local API
|
||
are NOT).
|
||
exit=1
|
||
```
|
||
|
||
## Self-test (a planted unreachable tag on a real root)
|
||
```
|
||
planted an unreachable tag on agent HostReport -> gate exit 1, planted tag convicted: True
|
||
unplanted tree -> gate exit 1
|
||
SELFTEST OK — the gate convicts a planted unreachable tag and the plant is the only difference.
|
||
exit=0
|
||
```
|
||
|
||
---
|
||
|
||
## ⚠ The search implementation changed AFTER this capture, and the capture was re-verified
|
||
|
||
The run above used `grep -rnE --include=…` to test whether a tag occurs in the receiving repo. That
|
||
**works on a workstation and returns nothing on the CI runner**, whose image carries python3 and git
|
||
and deliberately little else — its `grep` does not support `--include`. Empty stdout was then read as
|
||
"the tag is absent", so the gate convicted **all 174** checked tags and CI went red while the
|
||
pre-push hook was green (runs 260–262).
|
||
|
||
That is the gate silently reading a tool failure as a finding, which is worse than no gate, and it is
|
||
the error-swallowing this repo's rules forbid. The search is now **pure Python**: one token index per
|
||
receiving repo, no subprocess, no external dependency.
|
||
|
||
**This capture was NOT re-generated — it is re-verified**, which is the stronger claim. Every one of
|
||
the 40 fields recorded above was re-tested against the new implementation:
|
||
|
||
```
|
||
fields recorded in BEFORE.md: 40
|
||
agree=40 disagree=0
|
||
```
|
||
|
||
— i.e. the new implementation finds exactly the four this session fixed (`operator_key_configured`,
|
||
`wg_handshake_age_s`, `healed_at`, `escrow_stale`) present, and the other 36 still absent. **The
|
||
number 40 stands under both implementations**; only the mechanism and its portability changed.
|
||
|
||
The whole-token property also survives by construction: a token index treats `healed_at` and
|
||
`privsep_healed_at` as distinct tokens, so the substring false negative that the control caught
|
||
cannot come back.
|