feat(governance): 新增 runtime worker shadow gate
This commit is contained in:
266
apps/api/src/services/ai_agent_runtime_worker_shadow_gate.py
Normal file
266
apps/api/src/services/ai_agent_runtime_worker_shadow_gate.py
Normal file
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
AI Agent runtime worker shadow gate snapshot.
|
||||
|
||||
Loads the latest committed P2-404 shadow/no-write execution evidence gate.
|
||||
This module validates repo-committed evidence only; it never starts workers,
|
||||
writes Telegram Gateway queues, sends Telegram messages, reads secrets, or
|
||||
writes production targets.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from src.services.snapshot_paths import default_evaluations_dir
|
||||
|
||||
_DEFAULT_EVALUATIONS_DIR = default_evaluations_dir(Path(__file__))
|
||||
_SNAPSHOT_PATTERN = "ai_agent_runtime_worker_shadow_gate_*.json"
|
||||
_SCHEMA_VERSION = "ai_agent_runtime_worker_shadow_gate_v1"
|
||||
_RUNTIME_AUTHORITY = "runtime_worker_shadow_no_write_execution_evidence_gate_only_no_live_send_or_write"
|
||||
|
||||
|
||||
def load_latest_ai_agent_runtime_worker_shadow_gate(
|
||||
evaluations_dir: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Load the newest committed AI Agent runtime worker shadow gate."""
|
||||
directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR
|
||||
candidates = sorted(directory.glob(_SNAPSHOT_PATTERN))
|
||||
if not candidates:
|
||||
raise FileNotFoundError(f"no AI Agent runtime worker shadow gate snapshots found in {directory}")
|
||||
|
||||
latest = candidates[-1]
|
||||
with latest.open(encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError(f"{latest}: expected JSON object")
|
||||
_require_schema(payload, str(latest))
|
||||
_require_no_write_boundaries(payload, str(latest))
|
||||
_require_shadow_candidates(payload, str(latest))
|
||||
_require_replay_contract(payload, str(latest))
|
||||
_require_verifier_contract(payload, str(latest))
|
||||
_require_agent_roles(payload, str(latest))
|
||||
_require_redaction_contract(payload, str(latest))
|
||||
_require_rollup_consistency(payload, str(latest))
|
||||
return payload
|
||||
|
||||
|
||||
def _require_schema(payload: dict[str, Any], label: str) -> None:
|
||||
if payload.get("schema_version") != _SCHEMA_VERSION:
|
||||
raise ValueError(f"{label}: expected schema_version={_SCHEMA_VERSION}")
|
||||
status = payload.get("program_status") or {}
|
||||
if status.get("read_only_mode") is not True:
|
||||
raise ValueError(f"{label}: program_status.read_only_mode must be true")
|
||||
if status.get("runtime_authority") != _RUNTIME_AUTHORITY:
|
||||
raise ValueError(f"{label}: runtime_authority must remain {_RUNTIME_AUTHORITY}")
|
||||
if status.get("current_task_id") != "P2-404":
|
||||
raise ValueError(f"{label}: current_task_id must be P2-404")
|
||||
if status.get("next_task_id") != "P2-101":
|
||||
raise ValueError(f"{label}: next_task_id must be P2-101")
|
||||
|
||||
|
||||
def _require_no_write_boundaries(payload: dict[str, Any], label: str) -> None:
|
||||
truth = payload.get("shadow_gate_truth") or {}
|
||||
required_true = {
|
||||
"shadow_worker_evidence_gate_ready",
|
||||
"promotion_from_fixture_readback_ready",
|
||||
"no_write_replay_plan_ready",
|
||||
"action_candidate_selection_ready",
|
||||
"mcp_evidence_reuse_ready",
|
||||
"verifier_dry_run_binding_ready",
|
||||
"failure_lane_ready",
|
||||
"operator_review_packet_ready",
|
||||
}
|
||||
missing = sorted(field for field in required_true if truth.get(field) is not True)
|
||||
if missing:
|
||||
raise ValueError(f"{label}: shadow readiness flags must remain true: {missing}")
|
||||
|
||||
required_false = {
|
||||
"production_delivery_enabled",
|
||||
"gateway_queue_write_enabled",
|
||||
"telegram_send_enabled",
|
||||
"telegram_bot_api_call_enabled",
|
||||
"delivery_receipt_write_enabled",
|
||||
"shadow_worker_live_enabled",
|
||||
"ai_runtime_worker_enabled",
|
||||
"medium_low_auto_worker_enabled",
|
||||
"post_action_verifier_live_readback_enabled",
|
||||
"production_write_enabled",
|
||||
"secret_value_read_enabled",
|
||||
"work_window_transcript_display_allowed",
|
||||
}
|
||||
unsafe = sorted(field for field in required_false if truth.get(field) is not False)
|
||||
if unsafe:
|
||||
raise ValueError(f"{label}: live write/send/runtime flags must remain false: {unsafe}")
|
||||
|
||||
zero_counts = {
|
||||
"shadow_worker_live_run_count_24h",
|
||||
"gateway_queue_write_count_24h",
|
||||
"telegram_send_count_24h",
|
||||
"telegram_bot_api_call_count_24h",
|
||||
"delivery_receipt_write_count_24h",
|
||||
"ai_runtime_worker_run_count_24h",
|
||||
"medium_low_auto_execution_count_24h",
|
||||
"post_action_verifier_live_readback_count_24h",
|
||||
"production_write_count_24h",
|
||||
}
|
||||
non_zero = sorted(field for field in zero_counts if truth.get(field) != 0)
|
||||
if non_zero:
|
||||
raise ValueError(f"{label}: live write/send/runtime counts must remain zero: {non_zero}")
|
||||
|
||||
|
||||
def _require_shadow_candidates(payload: dict[str, Any], label: str) -> None:
|
||||
candidates = payload.get("shadow_worker_candidates") or []
|
||||
candidate_ids = {candidate.get("candidate_id") for candidate in candidates}
|
||||
required = {
|
||||
"shadow_report_daily_digest_candidate",
|
||||
"shadow_report_weekly_digest_candidate",
|
||||
"shadow_report_monthly_digest_candidate",
|
||||
"shadow_medium_low_noop_repair_candidate",
|
||||
"shadow_post_action_verifier_candidate",
|
||||
}
|
||||
if candidate_ids != required:
|
||||
raise ValueError(f"{label}: shadow candidates must match {sorted(required)}")
|
||||
|
||||
for candidate in candidates:
|
||||
candidate_id = candidate.get("candidate_id")
|
||||
if not _is_redacted_sha256(candidate.get("promotion_hash")):
|
||||
raise ValueError(f"{label}: candidate {candidate_id} must expose a redacted sha256 promotion_hash")
|
||||
if candidate.get("writes_production") is not False:
|
||||
raise ValueError(f"{label}: candidate {candidate_id} must not write production")
|
||||
if candidate.get("sends_telegram") is not False:
|
||||
raise ValueError(f"{label}: candidate {candidate_id} must not send Telegram")
|
||||
if candidate.get("reads_secret_value") is not False:
|
||||
raise ValueError(f"{label}: candidate {candidate_id} must not read secret value")
|
||||
if candidate.get("live_shadow_run_count_24h") != 0:
|
||||
raise ValueError(f"{label}: candidate {candidate_id} live_shadow_run_count_24h must remain zero")
|
||||
|
||||
|
||||
def _require_replay_contract(payload: dict[str, Any], label: str) -> None:
|
||||
replays = payload.get("no_write_execution_replays") or []
|
||||
replay_ids = {replay.get("replay_id") for replay in replays}
|
||||
required = {
|
||||
"daily_report_shadow_replay",
|
||||
"weekly_report_shadow_replay",
|
||||
"medium_low_noop_shadow_replay",
|
||||
"verifier_shadow_replay",
|
||||
}
|
||||
if replay_ids != required:
|
||||
raise ValueError(f"{label}: no-write replays must match {sorted(required)}")
|
||||
|
||||
for replay in replays:
|
||||
replay_id = replay.get("replay_id")
|
||||
if not _is_redacted_sha256(replay.get("evidence_hash")):
|
||||
raise ValueError(f"{label}: replay {replay_id} must expose a redacted sha256 evidence_hash")
|
||||
if replay.get("verifier_bound") is not True:
|
||||
raise ValueError(f"{label}: replay {replay_id} must remain verifier bound")
|
||||
if replay.get("writes_result") is not False:
|
||||
raise ValueError(f"{label}: replay {replay_id} must not write result")
|
||||
if replay.get("production_side_effect_count") != 0:
|
||||
raise ValueError(f"{label}: replay {replay_id} production_side_effect_count must remain zero")
|
||||
|
||||
|
||||
def _require_verifier_contract(payload: dict[str, Any], label: str) -> None:
|
||||
cases = payload.get("verifier_shadow_cases") or []
|
||||
case_ids = {case.get("case_id") for case in cases}
|
||||
required = {
|
||||
"shadow_promotion_verifier_case",
|
||||
"queue_write_block_verifier_case",
|
||||
"telegram_send_block_verifier_case",
|
||||
"production_write_block_verifier_case",
|
||||
}
|
||||
if case_ids != required:
|
||||
raise ValueError(f"{label}: verifier shadow cases must match {sorted(required)}")
|
||||
|
||||
for case in cases:
|
||||
case_id = case.get("case_id")
|
||||
if not _is_redacted_sha256(case.get("evidence_hash")):
|
||||
raise ValueError(f"{label}: verifier case {case_id} must expose a redacted sha256 evidence_hash")
|
||||
if case.get("live_readback_enabled") is not False:
|
||||
raise ValueError(f"{label}: verifier case {case_id} must not run live readback")
|
||||
if case.get("writes_result") is not False:
|
||||
raise ValueError(f"{label}: verifier case {case_id} must not write result")
|
||||
if case.get("requires_secret_value") is not False:
|
||||
raise ValueError(f"{label}: verifier case {case_id} must not require secret value")
|
||||
|
||||
|
||||
def _require_agent_roles(payload: dict[str, Any], label: str) -> None:
|
||||
roles = payload.get("agent_shadow_roles") or []
|
||||
agents = {role.get("agent_id") for role in roles}
|
||||
if agents != {"openclaw", "hermes", "nemotron"}:
|
||||
raise ValueError(f"{label}: shadow roles must include OpenClaw, Hermes, and NemoTron")
|
||||
for role in roles:
|
||||
if role.get("live_action_count_24h") != 0:
|
||||
raise ValueError(f"{label}: agent {role.get('agent_id')} live_action_count_24h must remain zero")
|
||||
|
||||
|
||||
def _require_redaction_contract(payload: dict[str, Any], label: str) -> None:
|
||||
contract = payload.get("display_redaction_contract") or {}
|
||||
required_false = {
|
||||
"raw_report_payload_display_allowed",
|
||||
"raw_telegram_payload_display_allowed",
|
||||
"raw_shadow_payload_display_allowed",
|
||||
"private_reasoning_display_allowed",
|
||||
"secret_value_display_allowed",
|
||||
"work_window_transcript_display_allowed",
|
||||
}
|
||||
if contract.get("redaction_required") is not True:
|
||||
raise ValueError(f"{label}: display redaction must remain required")
|
||||
unsafe = sorted(field for field in required_false if contract.get(field) is not False)
|
||||
if unsafe:
|
||||
raise ValueError(f"{label}: display redaction fields must remain false: {unsafe}")
|
||||
|
||||
|
||||
def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None:
|
||||
rollups = payload.get("rollups") or {}
|
||||
truth = payload.get("shadow_gate_truth") or {}
|
||||
candidates = payload.get("shadow_worker_candidates") or []
|
||||
replays = payload.get("no_write_execution_replays") or []
|
||||
cases = payload.get("verifier_shadow_cases") or []
|
||||
roles = payload.get("agent_shadow_roles") or []
|
||||
checkpoints = payload.get("operator_checkpoints") or []
|
||||
|
||||
expected = {
|
||||
"shadow_candidate_count": len(candidates),
|
||||
"passed_no_write_candidate_count": sum(1 for item in candidates if item.get("shadow_status") == "passed_no_write_replay"),
|
||||
"blocked_candidate_count": sum(1 for item in candidates if item.get("shadow_status") == "blocked_by_runtime_gate"),
|
||||
"needs_owner_review_candidate_count": sum(1 for item in candidates if item.get("shadow_status") == "needs_owner_review"),
|
||||
"no_write_replay_count": len(replays),
|
||||
"passed_no_write_replay_count": sum(1 for item in replays if item.get("replay_status") == "passed_no_write"),
|
||||
"verifier_shadow_case_count": len(cases),
|
||||
"agent_role_count": len(roles),
|
||||
"operator_checkpoint_count": len(checkpoints),
|
||||
}
|
||||
mismatches = sorted(field for field, value in expected.items() if rollups.get(field) != value)
|
||||
if mismatches:
|
||||
raise ValueError(f"{label}: rollup counts must match source arrays: {mismatches}")
|
||||
|
||||
approval_ids = sorted(item.get("checkpoint_id") for item in checkpoints if item.get("approval_required") is True)
|
||||
if sorted(rollups.get("approval_required_checkpoint_ids") or []) != approval_ids:
|
||||
raise ValueError(f"{label}: approval_required_checkpoint_ids must match operator checkpoints")
|
||||
|
||||
zero_pairs = {
|
||||
"shadow_worker_live_run_count": truth.get("shadow_worker_live_run_count_24h"),
|
||||
"gateway_queue_write_count": truth.get("gateway_queue_write_count_24h"),
|
||||
"telegram_send_count": truth.get("telegram_send_count_24h"),
|
||||
"telegram_bot_api_call_count": truth.get("telegram_bot_api_call_count_24h"),
|
||||
"delivery_receipt_write_count": truth.get("delivery_receipt_write_count_24h"),
|
||||
"ai_runtime_worker_run_count": truth.get("ai_runtime_worker_run_count_24h"),
|
||||
"medium_low_auto_execution_count": truth.get("medium_low_auto_execution_count_24h"),
|
||||
"post_action_verifier_live_readback_count": truth.get("post_action_verifier_live_readback_count_24h"),
|
||||
"production_write_count": truth.get("production_write_count_24h"),
|
||||
}
|
||||
non_zero = sorted(field for field, value in zero_pairs.items() if rollups.get(field) != 0 or value != 0)
|
||||
if non_zero:
|
||||
raise ValueError(f"{label}: rollup live counts must remain zero: {non_zero}")
|
||||
|
||||
|
||||
def _is_redacted_sha256(value: Any) -> bool:
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
if not value.startswith("sha256:"):
|
||||
return False
|
||||
digest = value.removeprefix("sha256:")
|
||||
return len(digest) == 64 and all(char in "0123456789abcdef" for char in digest)
|
||||
Reference in New Issue
Block a user