test(issue-5): harden watcher evidence validation and add codex pass review report
- Enforce evidence-root confinement for capture_ref and 1:1 capture citation uniqueness - Add pane identity and backend identity cross-validation - Add bidirectional agreement checks between watcher/herdr manifests and scenario fixtures - Expand contract test suite to 41 tests (100% pass) - Document evidence confinement rules in herdr-runtime.md and update task.md - Archive Codex 4th review report (Job 1bd561e6: [VERDICT: PASS])
This commit is contained in:
1 parent
19d798bf8f
commit
fc0bd96d8a
6 files changed
+286
-15
No files matched your search
@@ -13,6 +13,11 @@ Validates:
|
||||
accepted as 'observed' when an evidence schema exists for that source kind (currently Herdr only).
|
||||
Event / filesystem / harness items have no independent evidence schema yet and are fail-closed when
|
||||
labeled 'observed'; they must remain 'simulated' until such schemas exist.
|
||||
8. Evidence identity (Rev.5 self-review hardening): observed capture_refs must be repo-relative paths confined
|
||||
to the native Herdr evidence root; one capture may substantiate at most one timeline observation; the
|
||||
capture's session UUID and pane id must match the scenario's 4-fold attribution; a backend-specific
|
||||
scenario may only cite captures of that backend; and both fixture manifests must agree with the files
|
||||
they index (no drift, no orphan captures).
|
||||
"""
|
||||
|
||||
import copy
|
||||
@@ -48,6 +53,12 @@ OBSERVED_EVIDENCE_SCHEMAS = {"herdr"}
|
||||
# against the native capture cannot be skipped.
|
||||
HERDR_MANDATORY_ITEM_FIELDS = ("agent_status",)
|
||||
|
||||
# Native agent_status values a Herdr manifest slot may be named after (slot name == recorded agent_status).
|
||||
HERDR_AGENT_STATUS_SLOTS = {"idle", "working", "blocked", "done", "unknown"}
|
||||
|
||||
# Scenario backend marker meaning "any backend's capture may be cited".
|
||||
BACKEND_AGNOSTIC = "backend_agnostic"
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Validation Engine Functions (Used for both real fixtures & negative tests)
|
||||
@@ -103,10 +114,24 @@ def _load_observed_capture_ref(index: int, item: dict) -> "tuple[Path, dict]":
|
||||
assert "capture_ref" in item and item["capture_ref"], (
|
||||
f"Timeline item [{index}] is marked observed but lacks non-empty 'capture_ref'"
|
||||
)
|
||||
ref_path = REPO_ROOT / item["capture_ref"]
|
||||
raw_ref = str(item["capture_ref"])
|
||||
assert not Path(raw_ref).is_absolute(), (
|
||||
f"Observed timeline item [{index}] capture_ref must be a repo-relative path, got absolute '{raw_ref}'"
|
||||
)
|
||||
ref_path = REPO_ROOT / raw_ref
|
||||
assert ref_path.exists(), f"Observed timeline item [{index}] points to nonexistent capture_ref: {ref_path}"
|
||||
assert ref_path.suffix == ".json", f"capture_ref must point to a JSON evidence record, got {ref_path}"
|
||||
|
||||
# Confinement: evidence may only come from the native Herdr evidence root (no traversal, no foreign files).
|
||||
resolved = ref_path.resolve()
|
||||
try:
|
||||
resolved.relative_to(HERDR_ROOT.resolve())
|
||||
except ValueError:
|
||||
raise AssertionError(
|
||||
f"Observed timeline item [{index}] capture_ref '{raw_ref}' resolves outside the native Herdr "
|
||||
f"evidence root {HERDR_ROOT}; foreign or traversed paths are not admissible evidence"
|
||||
)
|
||||
|
||||
try:
|
||||
ref_data = json.loads(ref_path.read_text(encoding="utf-8"))
|
||||
except Exception as exc:
|
||||
@@ -133,12 +158,12 @@ def _extract_native_agent_object(ref_data: dict):
|
||||
return agent
|
||||
|
||||
|
||||
def _validate_observed_herdr_item(index: int, item: dict, dctx: dict) -> None:
|
||||
"""Strict evidence correspondence for an observed Herdr timeline item.
|
||||
def _validate_observed_herdr_item(index: int, item: dict, dctx: dict, scenario_backend: str = None) -> str:
|
||||
"""Strict evidence correspondence for an observed Herdr timeline item. Returns the capture_id.
|
||||
|
||||
Every declared payload field must be a native field of the referenced capture's agent object
|
||||
and must match its value exactly. Mandatory fields cannot be omitted to dodge the comparison,
|
||||
and the capture's agent session identity must match the scenario's dispatch attribution.
|
||||
and the capture's identity (session UUID, pane id, backend) must match the scenario's attribution.
|
||||
"""
|
||||
ref_path, ref_data = _load_observed_capture_ref(index, item)
|
||||
|
||||
@@ -164,7 +189,7 @@ def _validate_observed_herdr_item(index: int, item: dict, dctx: dict) -> None:
|
||||
f"it cannot substantiate observed herdr timeline item [{index}]"
|
||||
)
|
||||
|
||||
# Identity correspondence: the capture must be about the session this scenario attributes.
|
||||
# Identity correspondence (session): the capture must be about the session this scenario attributes.
|
||||
session_obj = agent_obj.get("agent_session")
|
||||
capture_uuid = session_obj.get("value") if isinstance(session_obj, dict) else None
|
||||
assert capture_uuid == dctx["agent_session_uuid"], (
|
||||
@@ -172,6 +197,25 @@ def _validate_observed_herdr_item(index: int, item: dict, dctx: dict) -> None:
|
||||
f"'{dctx['agent_session_uuid']}' but capture {ref_path} records agent_session.value='{capture_uuid}'"
|
||||
)
|
||||
|
||||
# Identity correspondence (pane): pane_process_generation is '<pane_id>:<pid>:<start>'; the capture's
|
||||
# native pane_id must be its leading component, otherwise the capture is about a different pane/generation.
|
||||
capture_pane = agent_obj.get("pane_id")
|
||||
assert isinstance(capture_pane, str) and capture_pane, (
|
||||
f"capture {ref_path} lacks a native pane_id; pane identity for timeline item [{index}] cannot be established"
|
||||
)
|
||||
ppg = str(dctx["pane_process_generation"])
|
||||
assert ppg.startswith(capture_pane + ":"), (
|
||||
f"Identity correspondence mismatch: dispatch_context.pane_process_generation='{ppg}' does not belong to "
|
||||
f"pane_id='{capture_pane}' recorded by capture {ref_path}"
|
||||
)
|
||||
|
||||
# Backend correspondence: a backend-specific scenario may only cite captures of that backend.
|
||||
if scenario_backend and scenario_backend != BACKEND_AGNOSTIC:
|
||||
assert ref_data.get("backend") == scenario_backend, (
|
||||
f"Backend correspondence mismatch: scenario declares backend='{scenario_backend}' but capture "
|
||||
f"{ref_path} records backend='{ref_data.get('backend')}'"
|
||||
)
|
||||
|
||||
# Payload correspondence: every declared field must exist natively and match exactly.
|
||||
for key, declared in item_payload.items():
|
||||
assert key in agent_obj, (
|
||||
@@ -183,6 +227,55 @@ def _validate_observed_herdr_item(index: int, item: dict, dctx: dict) -> None:
|
||||
f"but capture {ref_path} records {key}='{agent_obj[key]}'"
|
||||
)
|
||||
|
||||
return str(ref_data["capture_id"])
|
||||
|
||||
|
||||
def validate_watcher_manifest(manifest: dict) -> None:
|
||||
"""The watcher manifest must not drift from the scenario files it indexes."""
|
||||
for sid, sinfo in manifest["scenarios"].items():
|
||||
data = json.loads((WATCHER_ROOT / sinfo["file"]).read_text(encoding="utf-8"))
|
||||
assert data["scenario_id"] == sid, f"{sinfo['file']} scenario_id '{data['scenario_id']}' != manifest key '{sid}'"
|
||||
assert data["provenance"] == sinfo["provenance"], (
|
||||
f"{sid}: manifest provenance '{sinfo['provenance']}' != file provenance '{data['provenance']}'"
|
||||
)
|
||||
assert data["expected_policy"]["decision"] == sinfo["expected_decision"], (
|
||||
f"{sid}: manifest expected_decision '{sinfo['expected_decision']}' != "
|
||||
f"file decision '{data['expected_policy']['decision']}'"
|
||||
)
|
||||
|
||||
|
||||
def validate_herdr_manifest(manifest: dict) -> None:
|
||||
"""Every observed slot must be backed by a capture of the same backend recording the slot's state,
|
||||
and every capture on disk must be indexed by exactly one observed slot (no drift, no orphans)."""
|
||||
referenced = {}
|
||||
for backend, bdata in manifest["backends"].items():
|
||||
for slot_name, slot in bdata["slots"].items():
|
||||
if slot["status"] != "observed":
|
||||
continue
|
||||
rel = slot["file"]
|
||||
assert rel not in referenced, f"capture {rel} indexed by two slots: {referenced[rel]} and {backend}/{slot_name}"
|
||||
referenced[rel] = f"{backend}/{slot_name}"
|
||||
cap_path = HERDR_ROOT / rel
|
||||
assert cap_path.exists(), f"Observed slot {backend}/{slot_name} references missing capture {rel}"
|
||||
cap = json.loads(cap_path.read_text(encoding="utf-8"))
|
||||
assert cap["backend"] == backend, f"{rel}: capture backend '{cap['backend']}' != manifest backend '{backend}'"
|
||||
agent_obj = _extract_native_agent_object(cap)
|
||||
if slot_name in HERDR_AGENT_STATUS_SLOTS:
|
||||
assert agent_obj is not None, f"{rel}: status slot '{slot_name}' requires an agent object"
|
||||
assert agent_obj.get("agent_status") == slot_name, (
|
||||
f"{rel}: manifest slot '{slot_name}' but capture records agent_status='{agent_obj.get('agent_status')}'"
|
||||
)
|
||||
elif slot_name == "wait_timeout":
|
||||
assert cap["exit_code"] != 0 and agent_obj is None, f"{rel}: wait_timeout slot must be a nonzero-exit capture"
|
||||
err = json.loads(cap["stderr"]).get("error", {})
|
||||
assert err.get("code") == "timeout", f"{rel}: wait_timeout slot must record error.code='timeout'"
|
||||
else:
|
||||
raise AssertionError(f"Unknown observed slot kind '{slot_name}' for backend '{backend}'")
|
||||
|
||||
on_disk = {str(pth.relative_to(HERDR_ROOT)) for pth in HERDR_ROOT.glob("*/*.json")}
|
||||
orphans = on_disk - set(referenced)
|
||||
assert not orphans, f"Captures on disk not indexed by any observed manifest slot: {sorted(orphans)}"
|
||||
|
||||
|
||||
def validate_watcher_scenario(data: dict, file_path: Path = None) -> None:
|
||||
"""Strictly validates a Watcher composite scenario, attribution, timeline authenticity, and correspondence."""
|
||||
@@ -222,6 +315,8 @@ def validate_watcher_scenario(data: dict, file_path: Path = None) -> None:
|
||||
simulated_count = 0
|
||||
total_items = len(data["timeline"])
|
||||
assert total_items > 0, "timeline must contain at least one item"
|
||||
scenario_backend = data.get("backend")
|
||||
cited_capture_ids: dict = {} # capture_id -> first timeline index that cited it
|
||||
|
||||
for i, item in enumerate(data["timeline"]):
|
||||
source_kind = item.get("source_kind")
|
||||
@@ -247,10 +342,17 @@ def validate_watcher_scenario(data: dict, file_path: Path = None) -> None:
|
||||
)
|
||||
|
||||
if source_kind == "herdr":
|
||||
_validate_observed_herdr_item(i, item, dctx)
|
||||
capture_id = _validate_observed_herdr_item(i, item, dctx, scenario_backend)
|
||||
else: # pragma: no cover - guarded by the assertion above
|
||||
raise AssertionError(f"Unhandled observed source_kind '{source_kind}' at timeline item [{i}]")
|
||||
|
||||
# One capture is one point-in-time observation; it cannot substantiate two timeline points.
|
||||
assert capture_id not in cited_capture_ids, (
|
||||
f"Timeline item [{i}] re-cites capture_id '{capture_id}' already used by item "
|
||||
f"[{cited_capture_ids[capture_id]}]; a single capture cannot substantiate multiple observations"
|
||||
)
|
||||
cited_capture_ids[capture_id] = i
|
||||
|
||||
# Provenance honesty invariant
|
||||
if observed_count > 0 and simulated_count > 0:
|
||||
assert overall_prov == "mixed", (
|
||||
@@ -636,11 +738,11 @@ def test_negative_observed_herdr_item_session_identity_mismatch_fails():
|
||||
|
||||
|
||||
def test_negative_observed_herdr_item_referencing_non_native_json_record_fails():
|
||||
"""A JSON file that is not a native Herdr capture (e.g. a Watcher scenario) is not Herdr evidence."""
|
||||
"""A JSON file inside the evidence root that is not a native capture (e.g. the manifest) is not evidence."""
|
||||
sc = _load_scenario("initial_idle.json")
|
||||
sc["provenance"] = "mixed"
|
||||
sc["timeline"][1]["source_provenance"] = "observed"
|
||||
sc["timeline"][1]["capture_ref"] = "tests/fixtures/issue_5/watcher/finalized_candidate.json"
|
||||
sc["timeline"][1]["capture_ref"] = "tests/fixtures/issue_5/herdr/manifest.json"
|
||||
sc["timeline"][1]["payload"] = {"agent_status": "idle"}
|
||||
with pytest.raises(AssertionError, match="must have provenance 'observed'"):
|
||||
validate_watcher_scenario(sc)
|
||||
@@ -665,3 +767,124 @@ def test_positive_observed_herdr_item_with_full_native_correspondence_passes():
|
||||
sc["provenance"] = "observed_composite"
|
||||
with pytest.raises(AssertionError, match="overall provenance must be 'mixed'"):
|
||||
validate_watcher_scenario(sc)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 6. Evidence Identity & Manifest Drift Tests (Rev.5 self-review hardening)
|
||||
# ===========================================================================
|
||||
|
||||
def _observed_herdr_scenario(payload: dict, capture_ref: str = HERDR_CLAUDE_IDLE) -> dict:
|
||||
"""initial_idle with its herdr item (index 1) relabeled observed against `capture_ref`."""
|
||||
sc = _load_scenario("initial_idle.json")
|
||||
sc["provenance"] = "mixed"
|
||||
sc["timeline"][1]["source_provenance"] = "observed"
|
||||
sc["timeline"][1]["capture_ref"] = capture_ref
|
||||
sc["timeline"][1]["payload"] = payload
|
||||
return sc
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_ref, expected", [
|
||||
# Escapes the evidence root via traversal even though the target is a well-formed JSON file.
|
||||
("tests/fixtures/issue_5/herdr/../watcher/finalized_candidate.json", "resolves outside the native Herdr evidence root"),
|
||||
# Well-formed JSON in the repo but not under the evidence root.
|
||||
("tests/fixtures/issue_5/watcher/finalized_candidate.json", "resolves outside the native Herdr evidence root"),
|
||||
# Absolute paths are never admissible, even when they point at a real capture.
|
||||
(str(HERDR_ROOT / "claude" / "idle.json"), "must be a repo-relative path"),
|
||||
])
|
||||
def test_negative_observed_capture_ref_outside_evidence_root_fails(bad_ref, expected):
|
||||
sc = _observed_herdr_scenario({"agent_status": "idle"}, capture_ref=bad_ref)
|
||||
with pytest.raises(AssertionError, match=expected):
|
||||
validate_watcher_scenario(sc)
|
||||
|
||||
|
||||
def test_negative_single_capture_cited_by_two_observed_items_fails():
|
||||
"""One point-in-time capture cannot substantiate two distinct timeline observations."""
|
||||
sc = _load_scenario("initial_idle.json")
|
||||
sc["provenance"] = "observed_composite"
|
||||
# Turn the event item into a second herdr observation citing the very same capture.
|
||||
for item in sc["timeline"]:
|
||||
item["source_kind"] = "herdr"
|
||||
item["source_provenance"] = "observed"
|
||||
item["capture_ref"] = HERDR_CLAUDE_IDLE
|
||||
item["payload"] = {"agent_status": "idle"}
|
||||
with pytest.raises(AssertionError, match="re-cites capture_id 'cap-claude-idle-01'"):
|
||||
validate_watcher_scenario(sc)
|
||||
|
||||
|
||||
def test_negative_observed_herdr_item_pane_identity_mismatch_fails():
|
||||
"""Session UUID may match while the pane generation belongs to a different pane; must be rejected."""
|
||||
sc = _observed_herdr_scenario({"agent_status": "idle"})
|
||||
sc["dispatch_context"]["pane_process_generation"] = "w2X:p9:1234:2026-09-06T00:00:00Z" # capture says w2X:p1
|
||||
with pytest.raises(AssertionError, match="does not belong to pane_id='w2X:p1'"):
|
||||
validate_watcher_scenario(sc)
|
||||
|
||||
# A pane id that merely shares a prefix (w2X:p1 vs w2X:p10) must not be accepted either.
|
||||
sc["dispatch_context"]["pane_process_generation"] = "w2X:p10:1234:2026-09-06T00:00:00Z"
|
||||
with pytest.raises(AssertionError, match="does not belong to pane_id='w2X:p1'"):
|
||||
validate_watcher_scenario(sc)
|
||||
|
||||
|
||||
def test_negative_backend_specific_scenario_citing_other_backend_capture_fails():
|
||||
"""A scenario pinned to one backend may not borrow another backend's capture as evidence."""
|
||||
sc = _observed_herdr_scenario({"agent_status": "idle"})
|
||||
sc["backend"] = "grok" # capture is claude
|
||||
with pytest.raises(AssertionError, match="Backend correspondence mismatch"):
|
||||
validate_watcher_scenario(sc)
|
||||
|
||||
sc["backend"] = "claude"
|
||||
validate_watcher_scenario(sc) # matching backend is accepted
|
||||
|
||||
sc["backend"] = BACKEND_AGNOSTIC
|
||||
validate_watcher_scenario(sc) # agnostic scenarios may cite any backend
|
||||
|
||||
|
||||
def test_watcher_manifest_agrees_with_scenario_files():
|
||||
validate_watcher_manifest(json.loads((WATCHER_ROOT / "manifest.json").read_text(encoding="utf-8")))
|
||||
|
||||
|
||||
def test_negative_watcher_manifest_drift_is_detected():
|
||||
manifest = json.loads((WATCHER_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||
manifest["scenarios"]["initial_idle"]["expected_decision"] = "candidate_approved" # file says hold
|
||||
with pytest.raises(AssertionError, match="manifest expected_decision 'candidate_approved' != file decision 'hold'"):
|
||||
validate_watcher_manifest(manifest)
|
||||
|
||||
manifest = json.loads((WATCHER_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||
manifest["scenarios"]["finalized_candidate"]["provenance"] = "observed_composite" # file says simulated
|
||||
with pytest.raises(AssertionError, match="manifest provenance 'observed_composite' != file provenance 'simulated'"):
|
||||
validate_watcher_manifest(manifest)
|
||||
|
||||
|
||||
def test_herdr_manifest_observed_slots_agree_with_captures_and_no_orphan_captures():
|
||||
validate_herdr_manifest(json.loads((HERDR_ROOT / "manifest.json").read_text(encoding="utf-8")))
|
||||
|
||||
|
||||
def test_negative_herdr_manifest_slot_status_drift_is_detected():
|
||||
"""Re-indexing claude/idle.json (records idle) under the 'done' slot must be rejected."""
|
||||
manifest = json.loads((HERDR_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||
slots = manifest["backends"]["claude"]["slots"]
|
||||
slots["done"] = {"status": "observed", "file": "claude/idle.json"}
|
||||
slots["idle"] = {"status": "unobserved", "reason": "mutated"}
|
||||
with pytest.raises(AssertionError, match="manifest slot 'done' but capture records agent_status='idle'"):
|
||||
validate_herdr_manifest(manifest)
|
||||
|
||||
|
||||
def test_negative_herdr_manifest_backend_or_orphan_drift_is_detected():
|
||||
manifest = json.loads((HERDR_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||
# Borrow another backend's capture: grok's idle slot pointed at claude's capture (claude's own slot
|
||||
# de-indexed so the duplicate-index guard does not fire first and the backend check is what rejects).
|
||||
manifest["backends"]["claude"]["slots"]["idle"] = {"status": "unobserved", "reason": "mutated"}
|
||||
manifest["backends"]["grok"]["slots"]["idle"]["file"] = "claude/idle.json"
|
||||
with pytest.raises(AssertionError, match="capture backend 'claude' != manifest backend 'grok'"):
|
||||
validate_herdr_manifest(manifest)
|
||||
|
||||
manifest = json.loads((HERDR_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||
# Same capture indexed by two slots must be rejected.
|
||||
manifest["backends"]["grok"]["slots"]["idle"]["file"] = "claude/idle.json"
|
||||
with pytest.raises(AssertionError, match="indexed by two slots"):
|
||||
validate_herdr_manifest(manifest)
|
||||
|
||||
manifest = json.loads((HERDR_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||
# De-index a real capture: it becomes an orphan on disk and must be reported.
|
||||
manifest["backends"]["codex"]["slots"]["done"] = {"status": "unobserved", "reason": "mutated"}
|
||||
with pytest.raises(AssertionError, match=r"not indexed by any observed manifest slot: \['codex/done.json'\]"):
|
||||
validate_herdr_manifest(manifest)
|
||||
Reference in new issue
Block a user