From 93520df44cc3b2c3aacb5d6d861d472fb19574b8 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Thu, 11 Jun 2026 20:29:47 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20policy-gated=20action=20loop=20v0=20?= =?UTF-8?q?=E2=80=94=20intervention=20fixtures=20and=20enhanced=20checker?= =?UTF-8?q?=20(#162)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds valid.blocked-intervention.json: blocked action loop with trace_status=blocked and outcome result=blocked, demonstrating intervention record emitted when policy blocks a high-risk action. Adds valid.modified-action.json: modified action loop with trace_status=modified and outcome result=modified, demonstrating intervention record emitted when policy constrains scope of a moderate-risk action. Updates check_bounded_action_loop.py: - Validates all valid.*.json fixtures (not just one) - Enforces trace_status and outcome result must match (runtime trace always consistent) - Enforces intervention outcomes (blocked/modified/escalated) require audit_ref - Preserves existing recorded-trace-requires-low-risk constraint All 4 runtime rules now verified: no action without policy decision, every action emits trace, intervention recorded for blocked/modified, trait drift is observational only. Closes #162 --- .../valid.blocked-intervention.json | 30 ++++++++++++++ .../valid.modified-action.json | 40 +++++++++++++++++++ tools/check_bounded_action_loop.py | 15 ++++++- 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/bounded-action-loop/valid.blocked-intervention.json create mode 100644 tests/fixtures/bounded-action-loop/valid.modified-action.json diff --git a/tests/fixtures/bounded-action-loop/valid.blocked-intervention.json b/tests/fixtures/bounded-action-loop/valid.blocked-intervention.json new file mode 100644 index 00000000..0c3b8269 --- /dev/null +++ b/tests/fixtures/bounded-action-loop/valid.blocked-intervention.json @@ -0,0 +1,30 @@ +{ + "schema_version": "0.1", + "kind": "bounded_action_loop", + "loop_id": "loop-blocked-intervention-001", + "action_proposal": { + "proposal_id": "proposal-blocked-intervention-001", + "action_type": "emit_audit_packet", + "risk_class": "high", + "evidence_refs": [ + "evidence://agentplane/run/blocked-intervention-001/anomaly-signal" + ], + "requested_result": "emit audit packet for high-risk anomalous action" + }, + "policy_decision_ref": "SocioProphet/policy-fabric#85:pd-block-high-risk-001", + "runtime_trace": { + "trace_id": "trace-blocked-intervention-001", + "proposal_ref": "proposal-blocked-intervention-001", + "policy_decision_ref": "SocioProphet/policy-fabric#85:pd-block-high-risk-001", + "trace_status": "blocked", + "no_external_side_effects": true, + "audit_ref": "SocioProphet/model-governance-ledger#20:audit-block-001" + }, + "outcome_record": { + "outcome_id": "outcome-blocked-intervention-001", + "proposal_ref": "proposal-blocked-intervention-001", + "policy_decision_ref": "SocioProphet/policy-fabric#85:pd-block-high-risk-001", + "result": "blocked", + "audit_ref": "SocioProphet/model-governance-ledger#20:audit-block-001" + } +} diff --git a/tests/fixtures/bounded-action-loop/valid.modified-action.json b/tests/fixtures/bounded-action-loop/valid.modified-action.json new file mode 100644 index 00000000..a5f3e716 --- /dev/null +++ b/tests/fixtures/bounded-action-loop/valid.modified-action.json @@ -0,0 +1,40 @@ +{ + "schema_version": "0.1", + "kind": "bounded_action_loop", + "loop_id": "loop-modified-action-001", + "action_proposal": { + "proposal_id": "proposal-modified-action-001", + "action_type": "record_diagnostic_finding", + "risk_class": "moderate", + "evidence_refs": [ + "evidence://agentplane/run/modified-action-001/diagnostic-signal", + "evidence://agentplane/run/modified-action-001/policy-constraint" + ], + "requested_result": "record diagnostic finding with scope modification applied" + }, + "policy_decision_ref": "SocioProphet/policy-fabric#85:pd-modify-scope-001", + "runtime_trace": { + "trace_id": "trace-modified-action-001", + "proposal_ref": "proposal-modified-action-001", + "policy_decision_ref": "SocioProphet/policy-fabric#85:pd-modify-scope-001", + "trace_status": "modified", + "no_external_side_effects": true, + "audit_ref": "SocioProphet/model-governance-ledger#20:audit-modified-001" + }, + "outcome_record": { + "outcome_id": "outcome-modified-action-001", + "proposal_ref": "proposal-modified-action-001", + "policy_decision_ref": "SocioProphet/policy-fabric#85:pd-modify-scope-001", + "result": "modified", + "audit_ref": "SocioProphet/model-governance-ledger#20:audit-modified-001" + }, + "trait_baseline": { + "baseline_id": "baseline-modified-observational-001", + "observational_only": true + }, + "trait_drift_metric": { + "metric_id": "trait-drift-modified-001", + "observational_only": true, + "value": 0.15 + } +} diff --git a/tools/check_bounded_action_loop.py b/tools/check_bounded_action_loop.py index 6a8b2c9c..da59ff9f 100644 --- a/tools/check_bounded_action_loop.py +++ b/tools/check_bounded_action_loop.py @@ -13,7 +13,8 @@ ROOT = Path(__file__).resolve().parents[1] SCHEMA = ROOT / "schemas" / "bounded-action-loop.v0.schema.json" FIXTURES = ROOT / "tests" / "fixtures" / "bounded-action-loop" -VALID = FIXTURES / "valid.record-event-instance.json" + +INTERVENTION_STATUSES = {"blocked", "modified", "escalated"} def load_json(path: Path) -> dict[str, Any]: @@ -41,6 +42,12 @@ def check_data(data: dict[str, Any]) -> None: raise ValueError("proposal evidence refs required") if proposal["risk_class"] != "low" and trace["trace_status"] == "recorded": raise ValueError("recorded trace requires low risk in v0") + # Runtime trace must always be present and consistent with outcome + if trace["trace_status"] != outcome["result"]: + raise ValueError("trace_status and outcome result must match") + # Intervention outcomes (blocked/modified/escalated) must have audit_ref + if outcome["result"] in INTERVENTION_STATUSES and not outcome.get("audit_ref"): + raise ValueError(f"intervention outcome result={outcome['result']} requires audit_ref") def validate_file(path: Path, schema: dict[str, Any]) -> None: @@ -51,7 +58,11 @@ def validate_file(path: Path, schema: dict[str, Any]) -> None: def main() -> int: schema = load_json(SCHEMA) - validate_file(VALID, schema) + valids = sorted(FIXTURES.glob("valid.*.json")) + if not valids: + raise SystemExit("missing valid bounded-action-loop fixtures") + for path in valids: + validate_file(path, schema) invalids = sorted(FIXTURES.glob("invalid.*.json")) if not invalids: raise SystemExit("missing invalid bounded-action-loop fixtures")