From de1428b4ae300469ed65d0b1c8f5cd9ebb57fdd2 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 12 Sep 2026 18:42:36 +0000 Subject: [PATCH] no-mistakes(review): Harden GC gate key aliases, fail closed, fix baseline --- hermes-agent-baseline.prose.md | 3 ++- scripts/disk-gc-plan.py | 24 ++++++++++++++++++------ tests/test_disk_gc_report_only.py | 12 +++++++++++- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/hermes-agent-baseline.prose.md b/hermes-agent-baseline.prose.md index 71ab79f..9158303 100644 --- a/hermes-agent-baseline.prose.md +++ b/hermes-agent-baseline.prose.md @@ -24,11 +24,12 @@ done | Agent | CT | Node | IP | LiteLLM Alias | Key Source | Platform | |-------|-----|------|-----|---------------|------------|----------| -| Koby | 111 | amdpve | .129 | `koby` | Infisical vault | **Hermes** | +| Koby | 111 | storepve | .129 | `koby` | Infisical vault | **Hermes** | | Koonimo | 113 | amdpve | .114 | `koonimo` | Infisical vault | Hermes | | Shumba | — | 192.168.68.119 | N/A | N/A (DeepSeek) | Hermes (RETIRED — CT119 now Infisical vault) | > **Note**: CT hostnames (tdunna→CT111, baggy→CT113) differ from agent identities (koby, koonimo). +> CT 111 (tdunna, 192.168.68.129, storepve) is report-only — Theo's box; alert only, never garbage-collect. Access: `pct-run ` — no IPs needed. GPU hosts (.8, .110, .15) use SSH. Keys are stored in Infisical vault (project=agents, env=production) and injected at diff --git a/scripts/disk-gc-plan.py b/scripts/disk-gc-plan.py index f251268..3d5228c 100755 --- a/scripts/disk-gc-plan.py +++ b/scripts/disk-gc-plan.py @@ -22,7 +22,8 @@ Usage: cat scan.json | disk-gc-plan.py # same, via stdin disk-gc-plan.py --scan scan.json --json # machine-readable plan -Scan entries may carry any of: id / guest / vmid, hostname / name, ip. +Scan entries may carry any of: id / guest / vmid / ct / ctid, hostname / name, ip. +A threshold-crossing entry with no recognizable identity is reported, never GC'd. Exit codes: 0 ok, 1 usage/parse error. """ from __future__ import annotations @@ -40,6 +41,9 @@ DEFAULT_CONTRACT = REPO / "disk-gc-threat-response.prose.md" AMBER, RED, CRITICAL = 75, 85, 95 +IDENTITY_FIELDS = ("id", "guest", "vmid", "ct", "ctid", "hostname", "name", "ip") +UNIDENTIFIED_REASON = "unidentified target - refusing to schedule GC" + def load_report_only_guests(contract_path: pathlib.Path) -> list[dict]: """Read the authoritative report_only_guests block out of the contract. @@ -64,7 +68,7 @@ def _keys(entry: dict) -> set[str]: """Guest/host identity keys, shared by exclusions and scan entries so the two sides of the gate can never key on different fields.""" out: set[str] = set() - for field in ("guest", "id", "vmid", "hostname", "name", "ip"): + for field in IDENTITY_FIELDS: value = entry.get(field) if value is not None and str(value).strip(): out.add(str(value).strip().lower()) @@ -92,12 +96,20 @@ def build_plan(scan: list[dict], report_only: list[dict]) -> list[dict]: if level is None: continue # GREEN: log only, no action scan_keys = _keys(entry) - match = next((e for e, keys in excluded if keys & scan_keys), None) target = next( - (entry.get(k) for k in ("id", "guest", "vmid", "hostname", "name", "ip") - if entry.get(k) not in (None, "")), + (entry.get(k) for k in IDENTITY_FIELDS if entry.get(k) not in (None, "")), "?", ) + if not scan_keys: + plan.append({ + "target": target, + "level": level, + "pct": float(pct), + "action": "report-only", + "reason": UNIDENTIFIED_REASON, + }) + continue + match = next((e for e, keys in excluded if keys & scan_keys), None) if match is not None: plan.append({ "target": target, @@ -119,7 +131,7 @@ def build_plan(scan: list[dict], report_only: list[dict]) -> list[dict]: def main() -> int: ap = argparse.ArgumentParser(description="Plan disk GC actions with the report-only gate.") - ap.add_argument("--scan", help="JSON file: list of {id|hostname|ip, usage_pct}") + ap.add_argument("--scan", help="JSON file: list of {id|ct|hostname|ip, usage_pct}") ap.add_argument("--contract", default=str(DEFAULT_CONTRACT)) ap.add_argument("--json", action="store_true", help="emit the plan as JSON") args = ap.parse_args() diff --git a/tests/test_disk_gc_report_only.py b/tests/test_disk_gc_report_only.py index 5f1279b..208d763 100644 --- a/tests/test_disk_gc_report_only.py +++ b/tests/test_disk_gc_report_only.py @@ -51,8 +51,10 @@ def test_excluded_guest_never_gets_gc_at_any_level(tmp_path): def test_exclusion_matches_on_any_identity_key(tmp_path): - """The gate is keyed on guest/host, so id, hostname or IP all match.""" + """The gate is keyed on guest/host, so id, ct, ctid, hostname or IP all match.""" for entry in ({"id": 111, "usage_pct": 95}, + {"ct": 111, "usage_pct": 95}, + {"ctid": 111, "usage_pct": 95}, {"hostname": "tdunna", "usage_pct": 95}, {"ip": "192.168.68.129", "usage_pct": 95}): plan = _plan([entry], tmp_path) @@ -78,6 +80,14 @@ def test_agent_name_alone_does_not_gate_a_guest(tmp_path): assert plan and plan[0]["action"] == "gc-executor" +def test_unidentified_threat_fails_closed(tmp_path): + """A threshold-crossing entry with no recognized identity must not schedule GC.""" + plan = _plan([{"usage_pct": 97}], tmp_path) + assert plan, "an unidentified threat must still be reported" + assert plan[0]["action"] == "report-only", plan + assert plan[0]["reason"], plan + + def _plan_with_contract(scan, contract_text, tmp_path, name): contract = tmp_path / name contract.write_text(contract_text)