feat(governance): 新增 runtime readback promotion gate
All checks were successful
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m27s
CD Pipeline / build-and-deploy (push) Successful in 3m43s
CD Pipeline / post-deploy-checks (push) Successful in 1m29s

This commit is contained in:
Your Name
2026-06-13 18:01:34 +08:00
parent 1a1db336c8
commit ea1c825b16
10 changed files with 2198 additions and 1 deletions

View File

@@ -112,6 +112,9 @@ from src.services.ai_agent_runtime_readback_implementation_review import (
from src.services.ai_agent_runtime_readback_fixture_approval import (
load_latest_ai_agent_runtime_readback_fixture_approval,
)
from src.services.ai_agent_runtime_readback_promotion_gate import (
load_latest_ai_agent_runtime_readback_promotion_gate,
)
from src.services.ai_agent_report_live_delivery_approval_package import (
load_latest_ai_agent_report_live_delivery_approval_package,
)
@@ -1425,6 +1428,36 @@ async def get_agent_runtime_readback_fixture_approval() -> dict[str, Any]:
) from exc
@router.get(
"/agent-runtime-readback-promotion-gate",
response_model=dict[str, Any],
summary="取得 AI Agent runtime readback promotion gate",
description=(
"讀取最新已提交的 P2-113 runtime readback promotion gate"
"此端點只回傳 failure receipt、reviewer queue、result capture 的 no-write promotion "
"lane、preview、verifier 與 blocker不讀 canonical runtime target、不做 live query、"
"不寫 reviewer queue、不寫 Gateway queue、不送 Telegram、不呼叫 Bot API、"
"不寫 report receipt、不寫 result capture、不寫 learning / PlayBook trust、不讀 secret。"
),
)
async def get_agent_runtime_readback_promotion_gate() -> dict[str, Any]:
"""Return the latest read-only runtime readback promotion gate."""
try:
payload = await asyncio.to_thread(load_latest_ai_agent_runtime_readback_promotion_gate)
return redact_public_lan_topology(payload)
except FileNotFoundError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(exc),
) from exc
except (json.JSONDecodeError, ValueError) as exc:
logger.error("ai_agent_runtime_readback_promotion_gate_invalid", error=str(exc))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="AI Agent runtime readback promotion gate 無效",
) from exc
@router.get(
"/agent-owner-approved-fixture-dry-run",
response_model=dict[str, Any],

View File

@@ -0,0 +1,404 @@
"""
AI Agent runtime readback promotion gate snapshot.
Loads the latest committed P2-113 no-write promotion gate. This module validates
committed evidence only; it never reads canonical runtime targets, performs live
queries, writes reviewer queues, writes result captures, writes Gateway queues,
sends Telegram messages, calls Bot API, reads secrets, or performs destructive
operations.
"""
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_readback_promotion_gate_*.json"
_SCHEMA_VERSION = "ai_agent_runtime_readback_promotion_gate_v1"
_RUNTIME_AUTHORITY = "runtime_readback_promotion_gate_only_no_live_queue_or_result_write"
def load_latest_ai_agent_runtime_readback_promotion_gate(
evaluations_dir: Path | None = None,
) -> dict[str, Any]:
"""Load the newest committed runtime readback promotion gate."""
directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR
candidates = sorted(directory.glob(_SNAPSHOT_PATTERN))
if not candidates:
raise FileNotFoundError(f"no AI Agent runtime readback promotion 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")
label = str(latest)
_require_schema(payload, label)
_require_prior(payload, label)
_require_truth(payload, label)
_require_lanes(payload, label)
_require_contracts(payload, label)
_require_queue_previews(payload, label)
_require_result_previews(payload, label)
_require_checks(payload, label)
_require_blockers(payload, label)
_require_actions(payload, label)
_require_display_redaction(payload, label)
_require_no_forbidden_display_terms(payload, label)
_require_rollup_consistency(payload, label)
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 {}
expected = {
"current_priority": "P2",
"current_task_id": "P2-113",
"next_task_id": "P2-114",
"read_only_mode": True,
"runtime_authority": _RUNTIME_AUTHORITY,
"overall_completion_percent": 100,
}
mismatches = _mismatches(status, expected)
if mismatches:
raise ValueError(f"{label}: program_status mismatch: {mismatches}")
if not status.get("status_note"):
raise ValueError(f"{label}: program_status.status_note is required")
def _require_prior(payload: dict[str, Any], label: str) -> None:
prior = payload.get("prior_fixture_approval") or {}
expected = {
"fixture_approval_schema_version": "ai_agent_runtime_readback_fixture_approval_v1",
"fixture_approval_card_count": 5,
"adapter_contract_count": 4,
"verifier_fixture_check_count": 5,
"blocker_mapping_count": 5,
"operator_action_count": 5,
"owner_approval_received_count": 0,
"fixture_readback_execution_count": 0,
"live_query_count": 0,
"production_write_count": 0,
}
mismatches = _mismatches(prior, expected)
if mismatches:
raise ValueError(f"{label}: prior_fixture_approval mismatch: {mismatches}")
if not prior.get("readiness_note"):
raise ValueError(f"{label}: prior_fixture_approval.readiness_note is required")
def _require_truth(payload: dict[str, Any], label: str) -> None:
truth = payload.get("promotion_truth") or {}
required_true = {
"p2_112_fixture_approval_loaded",
"promotion_gate_ready",
"failure_receipt_fixture_ready",
"reviewer_queue_preview_ready",
"result_capture_preview_ready",
"owner_acceptance_required_before_promotion",
}
missing = sorted(field for field in required_true if truth.get(field) is not True)
if missing:
raise ValueError(f"{label}: promotion ready flags must remain true: {missing}")
required_false = {
"canonical_runtime_target_read_enabled",
"live_query_enabled",
"failure_receipt_send_enabled",
"reviewer_queue_write_enabled",
"gateway_queue_write_enabled",
"telegram_send_enabled",
"bot_api_call_enabled",
"report_receipt_write_enabled",
"result_capture_write_enabled",
"learning_write_enabled",
"playbook_trust_write_enabled",
"production_write_enabled",
"secret_read_enabled",
"destructive_operation_enabled",
}
unsafe = sorted(field for field in required_false if truth.get(field) is not False)
if unsafe:
raise ValueError(f"{label}: live read/send/write flags must remain false: {unsafe}")
zero_counts = {
"owner_approval_received_count",
"promotion_execution_count",
"canonical_runtime_target_read_count",
"live_query_count",
"failure_receipt_send_count",
"reviewer_queue_write_count",
"gateway_queue_write_count",
"telegram_send_count",
"bot_api_call_count",
"report_receipt_write_count",
"result_capture_write_count",
"learning_write_count",
"playbook_trust_write_count",
"production_write_count",
}
non_zero = sorted(field for field in zero_counts if truth.get(field) != 0)
if non_zero:
raise ValueError(f"{label}: promotion live counters must remain zero: {non_zero}")
if not truth.get("truth_note"):
raise ValueError(f"{label}: promotion_truth.truth_note is required")
def _require_lanes(payload: dict[str, Any], label: str) -> None:
lanes = payload.get("promotion_lanes") or []
required = {
"telegram_failure_receipt_promotion",
"reviewer_queue_preview_promotion",
"result_capture_no_write_promotion",
"report_delivery_receipt_link",
"p2_114_handoff_scope",
}
lane_ids = {lane.get("lane_id") for lane in lanes}
if lane_ids != required:
raise ValueError(f"{label}: promotion lanes must match {sorted(required)}")
for lane in lanes:
lane_id = lane.get("lane_id")
if lane.get("owner_approval_required") is not True or lane.get("promotion_only") is not True:
raise ValueError(f"{label}: lane {lane_id} must require owner approval and promotion_only")
if lane.get("status") not in {"ready_for_owner_review", "approval_required", "blocked_by_policy"}:
raise ValueError(f"{label}: lane {lane_id} status is invalid")
if lane.get("risk_tier") not in {"medium", "high", "critical"}:
raise ValueError(f"{label}: lane {lane_id} risk_tier is invalid")
if not lane.get("allowed_artifact") or not lane.get("blocked_runtime_actions"):
raise ValueError(f"{label}: lane {lane_id} must list artifact and blocked actions")
if not _is_redacted_sha256(lane.get("evidence_hash")):
raise ValueError(f"{label}: lane {lane_id} must expose redacted evidence_hash")
def _require_contracts(payload: dict[str, Any], label: str) -> None:
contracts = payload.get("receipt_contracts") or []
required = {
"no_send_failure_receipt_contract",
"sre_war_room_route_lock_contract",
"redacted_receipt_payload_contract",
"no_bot_api_call_contract",
}
contract_ids = {contract.get("contract_id") for contract in contracts}
if contract_ids != required:
raise ValueError(f"{label}: receipt contracts must match {sorted(required)}")
for contract in contracts:
contract_id = contract.get("contract_id")
if contract.get("status") not in {"ready", "approval_required", "blocked_by_policy"}:
raise ValueError(f"{label}: contract {contract_id} status is invalid")
if contract.get("live_send_enabled") is not False or contract.get("receipt_write_enabled") is not False:
raise ValueError(f"{label}: contract {contract_id} must not enable live send or receipt write")
if not contract.get("required_evidence"):
raise ValueError(f"{label}: contract {contract_id} required_evidence is required")
if not _is_redacted_sha256(contract.get("evidence_hash")):
raise ValueError(f"{label}: contract {contract_id} must expose redacted evidence_hash")
def _require_queue_previews(payload: dict[str, Any], label: str) -> None:
previews = payload.get("reviewer_queue_previews") or []
if len(previews) != 4:
raise ValueError(f"{label}: reviewer_queue_previews must contain 4 items")
for preview in previews:
preview_id = preview.get("preview_id")
if preview.get("queue_write_enabled") is not False or preview.get("audit_write_enabled") is not False:
raise ValueError(f"{label}: reviewer preview {preview_id} must not enable queue/audit writes")
if not preview.get("required_fields"):
raise ValueError(f"{label}: reviewer preview {preview_id} required_fields is required")
if not _is_redacted_sha256(preview.get("evidence_hash")):
raise ValueError(f"{label}: reviewer preview {preview_id} must expose redacted evidence_hash")
def _require_result_previews(payload: dict[str, Any], label: str) -> None:
previews = payload.get("result_capture_previews") or []
if len(previews) != 4:
raise ValueError(f"{label}: result_capture_previews must contain 4 items")
for preview in previews:
preview_id = preview.get("preview_id")
if preview.get("result_capture_write_enabled") is not False or preview.get("learning_write_enabled") is not False:
raise ValueError(f"{label}: result preview {preview_id} must not enable result/learning writes")
if not preview.get("required_fields"):
raise ValueError(f"{label}: result preview {preview_id} required_fields is required")
if not _is_redacted_sha256(preview.get("evidence_hash")):
raise ValueError(f"{label}: result preview {preview_id} must expose redacted evidence_hash")
def _require_checks(payload: dict[str, Any], label: str) -> None:
checks = payload.get("no_write_verifier_checks") or []
required = {
"no_live_query_promotion_check",
"no_gateway_queue_write_check",
"no_telegram_bot_send_check",
"no_reviewer_queue_write_check",
"no_result_capture_write_check",
}
check_ids = {check.get("check_id") for check in checks}
if check_ids != required:
raise ValueError(f"{label}: no-write verifier checks must match {sorted(required)}")
for check in checks:
check_id = check.get("check_id")
if check.get("live_verifier_enabled") is not False:
raise ValueError(f"{label}: verifier check {check_id} must not enable live verifier")
if not check.get("required_fixture") or not check.get("failure_if_missing"):
raise ValueError(f"{label}: verifier check {check_id} must include fixture and failure text")
if not _is_redacted_sha256(check.get("evidence_hash")):
raise ValueError(f"{label}: verifier check {check_id} must expose redacted evidence_hash")
def _require_blockers(payload: dict[str, Any], label: str) -> None:
blockers = payload.get("blocker_mappings") or []
required = {
"owner_acceptance_missing",
"canonical_readback_not_approved",
"telegram_send_not_approved",
"reviewer_queue_write_not_approved",
"result_capture_write_not_approved",
}
blocker_ids = {blocker.get("blocker_id") for blocker in blockers}
if blocker_ids != required:
raise ValueError(f"{label}: blocker mappings must match {sorted(required)}")
for blocker in blockers:
blocker_id = blocker.get("blocker_id")
if blocker.get("severity") not in {"medium", "high", "critical"}:
raise ValueError(f"{label}: blocker {blocker_id} severity is invalid")
if blocker.get("status") not in {"mapped", "approval_required", "blocked_by_policy"}:
raise ValueError(f"{label}: blocker {blocker_id} status is invalid")
if not blocker.get("blocked_action") or not blocker.get("blocked_until"):
raise ValueError(f"{label}: blocker {blocker_id} blocked action/until is required")
if not _is_redacted_sha256(blocker.get("evidence_hash")):
raise ValueError(f"{label}: blocker {blocker_id} must expose redacted evidence_hash")
def _require_actions(payload: dict[str, Any], label: str) -> None:
actions = payload.get("operator_actions") or []
required = {
"review_promotion_lanes",
"verify_failure_receipt_fixture",
"confirm_queue_no_write",
"compare_result_capture_preview",
"promote_to_p2_114",
}
action_ids = {action.get("action_id") for action in actions}
if action_ids != required:
raise ValueError(f"{label}: operator actions must match {sorted(required)}")
for action in actions:
action_id = action.get("action_id")
if action.get("runtime_promotion_allowed") is not False:
raise ValueError(f"{label}: action {action_id} must not allow runtime promotion")
if not action.get("operator_instruction"):
raise ValueError(f"{label}: action {action_id} operator_instruction is required")
def _require_display_redaction(payload: dict[str, Any], label: str) -> None:
contract = payload.get("display_redaction_contract") or {}
if contract.get("redaction_required") is not True:
raise ValueError(f"{label}: display redaction must be required")
false_fields = {
"raw_prompt_display_allowed",
"private_reasoning_display_allowed",
"secret_value_display_allowed",
"raw_runtime_payload_display_allowed",
"internal_collaboration_content_display_allowed",
}
unsafe = sorted(field for field in false_fields if contract.get(field) is not False)
if unsafe:
raise ValueError(f"{label}: display redaction flags must remain false: {unsafe}")
if not contract.get("frontend_display_policy"):
raise ValueError(f"{label}: frontend_display_policy is required")
def _require_no_forbidden_display_terms(payload: dict[str, Any], label: str) -> None:
serialized = json.dumps(payload, ensure_ascii=False).lower()
forbidden = {
"work_window_transcript",
"session_id",
"browser_context",
"authorization_header",
"raw telegram payload",
"private reasoning",
"raw prompt",
"chain-of-thought",
}
hits = sorted(term for term in forbidden if term in serialized)
if hits:
raise ValueError(f"{label}: forbidden display terms leaked: {hits}")
def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None:
rollups = payload.get("rollups") or {}
expected_counts = {
"promotion_lane_count": len(payload.get("promotion_lanes") or []),
"receipt_contract_count": len(payload.get("receipt_contracts") or []),
"reviewer_queue_preview_count": len(payload.get("reviewer_queue_previews") or []),
"result_capture_preview_count": len(payload.get("result_capture_previews") or []),
"no_write_verifier_check_count": len(payload.get("no_write_verifier_checks") or []),
"blocker_mapping_count": len(payload.get("blocker_mappings") or []),
"operator_action_count": len(payload.get("operator_actions") or []),
"approval_required_lane_count": sum(
1 for lane in payload.get("promotion_lanes") or [] if lane.get("status") == "approval_required"
),
"blocked_lane_count": sum(
1 for lane in payload.get("promotion_lanes") or [] if lane.get("status") == "blocked_by_policy"
),
"blocked_receipt_contract_count": sum(
1 for contract in payload.get("receipt_contracts") or [] if contract.get("status") == "blocked_by_policy"
),
"approval_required_reviewer_preview_count": sum(
1
for preview in payload.get("reviewer_queue_previews") or []
if preview.get("status") == "approval_required"
),
"blocked_result_preview_count": sum(
1
for preview in payload.get("result_capture_previews") or []
if preview.get("status") == "blocked_by_policy"
),
}
mismatches = _mismatches(rollups, expected_counts)
if mismatches:
raise ValueError(f"{label}: rollup counts mismatch: {mismatches}")
zero_rollups = {
"owner_approval_received_count",
"promotion_execution_count",
"canonical_runtime_target_read_count",
"live_query_count",
"failure_receipt_send_count",
"reviewer_queue_write_count",
"gateway_queue_write_count",
"telegram_send_count",
"bot_api_call_count",
"report_receipt_write_count",
"result_capture_write_count",
"learning_write_count",
"playbook_trust_write_count",
"production_write_count",
"secret_read_count",
"destructive_operation_count",
}
non_zero = sorted(field for field in zero_rollups if rollups.get(field) != 0)
if non_zero:
raise ValueError(f"{label}: live/send/write rollups must remain zero: {non_zero}")
def _mismatches(actual: dict[str, Any], expected: dict[str, Any]) -> dict[str, dict[str, Any]]:
return {
key: {"expected": expected_value, "actual": actual.get(key)}
for key, expected_value in expected.items()
if actual.get(key) != expected_value
}
def _is_redacted_sha256(value: Any) -> bool:
if not isinstance(value, str):
return False
if not value.startswith("sha256:") or len(value) != len("sha256:") + 64:
return False
digest = value.split(":", 1)[1]
return all(char in "0123456789abcdef" for char in digest)

View File

@@ -0,0 +1,88 @@
import copy
import json
from pathlib import Path
import pytest
from src.services.ai_agent_runtime_readback_promotion_gate import (
load_latest_ai_agent_runtime_readback_promotion_gate,
)
REPO_ROOT = Path(__file__).resolve().parents[3]
FIXTURE = REPO_ROOT / "docs/evaluations/ai_agent_runtime_readback_promotion_gate_2026-06-13.json"
def test_load_latest_ai_agent_runtime_readback_promotion_gate_snapshot() -> None:
data = load_latest_ai_agent_runtime_readback_promotion_gate()
assert data["schema_version"] == "ai_agent_runtime_readback_promotion_gate_v1"
assert data["program_status"]["current_task_id"] == "P2-113"
assert data["program_status"]["next_task_id"] == "P2-114"
assert data["program_status"]["overall_completion_percent"] == 100
assert data["program_status"]["read_only_mode"] is True
rollups = data["rollups"]
assert rollups["promotion_lane_count"] == 5
assert rollups["receipt_contract_count"] == 4
assert rollups["reviewer_queue_preview_count"] == 4
assert rollups["result_capture_preview_count"] == 4
assert rollups["no_write_verifier_check_count"] == 5
assert rollups["blocker_mapping_count"] == 5
assert rollups["operator_action_count"] == 5
assert rollups["approval_required_lane_count"] == 2
assert rollups["blocked_lane_count"] == 1
assert rollups["blocked_receipt_contract_count"] == 1
assert rollups["approval_required_reviewer_preview_count"] == 1
assert rollups["blocked_result_preview_count"] == 1
zero_fields = [
"owner_approval_received_count",
"promotion_execution_count",
"canonical_runtime_target_read_count",
"live_query_count",
"failure_receipt_send_count",
"reviewer_queue_write_count",
"gateway_queue_write_count",
"telegram_send_count",
"bot_api_call_count",
"report_receipt_write_count",
"result_capture_write_count",
"learning_write_count",
"playbook_trust_write_count",
"production_write_count",
"secret_read_count",
"destructive_operation_count",
]
for field in zero_fields:
assert rollups[field] == 0
def test_runtime_readback_promotion_gate_rejects_reviewer_queue_write(tmp_path: Path) -> None:
source = json.loads(FIXTURE.read_text(encoding="utf-8"))
source["promotion_truth"]["reviewer_queue_write_enabled"] = True
target = tmp_path / "ai_agent_runtime_readback_promotion_gate_2026-06-13.json"
target.write_text(json.dumps(source), encoding="utf-8")
with pytest.raises(ValueError, match="live read/send/write flags"):
load_latest_ai_agent_runtime_readback_promotion_gate(tmp_path)
def test_runtime_readback_promotion_gate_rejects_rollup_drift(tmp_path: Path) -> None:
source = json.loads(FIXTURE.read_text(encoding="utf-8"))
source["rollups"]["promotion_lane_count"] = 4
target = tmp_path / "ai_agent_runtime_readback_promotion_gate_2026-06-13.json"
target.write_text(json.dumps(source), encoding="utf-8")
with pytest.raises(ValueError, match="rollup counts mismatch"):
load_latest_ai_agent_runtime_readback_promotion_gate(tmp_path)
def test_runtime_readback_promotion_gate_rejects_forbidden_display_terms(tmp_path: Path) -> None:
source = copy.deepcopy(json.loads(FIXTURE.read_text(encoding="utf-8")))
source["operator_actions"][0]["operator_instruction"] = "do not expose browser_context"
target = tmp_path / "ai_agent_runtime_readback_promotion_gate_2026-06-13.json"
target.write_text(json.dumps(source), encoding="utf-8")
with pytest.raises(ValueError, match="forbidden display terms"):
load_latest_ai_agent_runtime_readback_promotion_gate(tmp_path)

View File

@@ -0,0 +1,41 @@
import pytest
from httpx import ASGITransport, AsyncClient
from src.main import app
@pytest.mark.asyncio
async def test_get_agent_runtime_readback_promotion_gate_api() -> None:
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/api/v1/agents/agent-runtime-readback-promotion-gate")
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == "ai_agent_runtime_readback_promotion_gate_v1"
assert data["program_status"]["current_task_id"] == "P2-113"
assert data["program_status"]["next_task_id"] == "P2-114"
assert data["program_status"]["overall_completion_percent"] == 100
rollups = data["rollups"]
assert rollups["promotion_lane_count"] == 5
assert rollups["receipt_contract_count"] == 4
assert rollups["reviewer_queue_preview_count"] == 4
assert rollups["result_capture_preview_count"] == 4
assert rollups["no_write_verifier_check_count"] == 5
assert rollups["blocker_mapping_count"] == 5
assert rollups["operator_action_count"] == 5
assert rollups["owner_approval_received_count"] == 0
assert rollups["promotion_execution_count"] == 0
assert rollups["canonical_runtime_target_read_count"] == 0
assert rollups["live_query_count"] == 0
assert rollups["failure_receipt_send_count"] == 0
assert rollups["reviewer_queue_write_count"] == 0
assert rollups["gateway_queue_write_count"] == 0
assert rollups["telegram_send_count"] == 0
assert rollups["bot_api_call_count"] == 0
assert rollups["report_receipt_write_count"] == 0
assert rollups["result_capture_write_count"] == 0
assert rollups["learning_write_count"] == 0
assert rollups["playbook_trust_write_count"] == 0
assert rollups["production_write_count"] == 0