R-444: hub half — the System page shows each box's last disk trim

sysfacts reads the agent's top-level guest_disk_trim stanza (schedule + per-guest
last attempt: vmid, last_attempt_at, ok, bytes_trimmed, mounts, duration_seconds,
last_ok_at, error) into a field-by-field mirror. The System page's new 'Last disk
trim' column shows the last successful trim and the GiB it freed; amber when the
newest attempt failed (error shown) or last_ok_at is older than 14 days (judged on
the success time, never the attempt time); '—' when the agent sends no stanza.
wire_contract_gate: SUBTREE_MIRRORS checks guest_disk_trim field by field BOTH
ways against sysfacts.DiskTrim; decoys (ok renamed, last_ok_at dropped) in
test_gate_decoys.py. Decision 139.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-06 11:28:20 +02:00
parent 9d39faabf8
commit a411cde7c4
7 changed files with 303 additions and 13 deletions
+34
View File
@@ -105,6 +105,17 @@ MIRRORS = {
"hub -> controller (report ACK, `escrow` object)": ("internal/report", "EscrowStatus"),
}
# R-444 (2026-10-06): a SUBTREE of a name-checked root whose receiver decodes it into one named mirror type gets the
# field-by-field check in BOTH directions — every emitted path under it must be a json path of the mirror, AND every
# mirror path must be emitted (a field the hub reads and the agent never sends would leave the page on "—" for ever).
# Both directions, because some leaves here are short names (`ok`, `error`, `vmid`) the name check cannot judge:
# `ok` occurs everywhere, so a renamed `ok` would pass it. Keyed by (root label, dotted subtree path):
# (receiver package dir, receiver type). A subtree the emitter does not carry YET prints PENDING — the receiver was
# built first, against a provisional shape — and is neither a pass nor a conviction of the subtree.
SUBTREE_MIRRORS = {
("agent -> hub (POST /host-report)", "guest_disk_trim"): ("internal/sysfacts", "DiskTrim"),
}
# Tag names whose literal string carries no information in a repo-wide search. NOT CHECKED.
# Listed rather than silently skipped: each one is a hole.
GENERIC = {
@@ -630,6 +641,29 @@ def run(root_override=None, quiet=False):
convictions.append((label, receiver, missing))
continue
kinds.append((label, "name-reachability only (a tag found ANYWHERE in %s passes)" % receiver))
emitted_paths = {d for _, d in tags}
for (slabel, sub), (mdir, mtype) in sorted(SUBTREE_MIRRORS.items()):
if slabel != label:
continue
rby_dir, rby_name = indexes[receiver]
if (mdir, mtype) not in rby_dir:
die("wire-contract gate INCONCLUSIVE: declared subtree mirror %s.%s not found in %s/%s\n"
" A mirror that cannot be resolved is not a pass — fix SUBTREE_MIRRORS or the type."
% (receiver, mtype, receiver, mdir))
if sub not in emitted_paths:
kinds.append((" └ " + sub, "PENDING — the emitter does not carry `%s` yet; %s.%s is unchecked"
% (sub, mdir, mtype)))
continue
want = {sub + "." + d for _, d in walk(rby_dir, rby_name, mdir, mtype)}
got = {d for d in emitted_paths if d.startswith(sub + ".")}
for d in sorted(got - want):
checked += 1
missing.append((d.split(".")[-1], d))
for d in sorted(want - got):
checked += 1
missing.append((d.split(".")[-1] + " (read by the receiver, never emitted)", d))
kinds.append((" └ " + sub, "FIELD-BY-FIELD both ways against %s %s.%s (%d path(s))"
% (receiver, mdir, mtype, len(want | got))))
for tag, dotted in sorted(seen_tags.items()):
if tag in GENERIC:
skipped += 1