feat(agents): 新增 AI action audit ledger
Some checks failed
Code Review / ai-code-review (push) Successful in 12s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
CD Pipeline / tests (push) Successful in 1m54s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-19 00:50:43 +08:00
parent f390cddb4d
commit e13f716c00
9 changed files with 1306 additions and 136 deletions

View File

@@ -43,6 +43,12 @@ from src.services.agent_service import (
TaskState,
get_agent_service,
)
from src.services.ai_agent_12_agent_war_room import (
load_latest_ai_agent_12_agent_war_room,
)
from src.services.ai_agent_action_audit_ledger import (
load_latest_ai_agent_action_audit_ledger,
)
from src.services.ai_agent_automation_backlog_snapshot import (
load_latest_ai_agent_automation_backlog_snapshot,
)
@@ -70,6 +76,9 @@ from src.services.ai_agent_failure_receipt_no_send_replay import (
from src.services.ai_agent_gitea_pr_draft_lane import (
load_latest_ai_agent_gitea_pr_draft_lane,
)
from src.services.ai_agent_high_risk_owner_review_queue import (
load_latest_ai_agent_high_risk_owner_review_queue,
)
from src.services.ai_agent_host_stateful_version_inventory import (
load_latest_ai_agent_host_stateful_version_inventory,
)
@@ -82,33 +91,15 @@ from src.services.ai_agent_learning_writeback_approval_package import (
from src.services.ai_agent_live_read_model_gate import (
load_latest_ai_agent_live_read_model_gate,
)
from src.services.ai_agent_12_agent_war_room import (
load_latest_ai_agent_12_agent_war_room,
)
from src.services.ai_agent_professional_task_expansion import (
load_latest_ai_agent_professional_task_expansion,
)
from src.services.ai_agent_receipt_readback_owner_review import (
load_latest_ai_agent_receipt_readback_owner_review,
)
from src.services.ai_agent_report_no_write_analysis_runtime import (
load_latest_ai_agent_report_no_write_analysis_runtime,
)
from src.services.ai_agent_high_risk_owner_review_queue import (
load_latest_ai_agent_high_risk_owner_review_queue,
)
from src.services.ai_agent_report_source_health import (
build_ai_agent_report_source_health,
)
from src.services.ai_agent_low_medium_risk_whitelist import (
load_latest_ai_agent_low_medium_risk_whitelist,
)
from src.services.host_runaway_aiops_loop_readiness import (
load_latest_host_runaway_aiops_loop_readiness,
)
from src.services.ai_agent_matched_playbook_learning_gap import (
load_latest_ai_agent_matched_playbook_learning_gap,
)
from src.services.ai_agent_operation_permission_model import (
load_latest_ai_agent_operation_permission_model,
)
from src.services.ai_agent_owner_approved_fixture_dry_run import (
load_latest_ai_agent_owner_approved_fixture_dry_run,
)
@@ -127,116 +118,29 @@ from src.services.ai_agent_owner_approved_result_capture_promotion_dry_run impor
from src.services.ai_agent_owner_approved_result_capture_readback import (
load_latest_ai_agent_owner_approved_result_capture_readback,
)
from src.services.ai_agent_operation_permission_model import (
load_latest_ai_agent_operation_permission_model,
)
from src.services.ai_agent_post_write_verifier_package import (
load_latest_ai_agent_post_write_verifier_package,
)
from src.services.ai_agent_proactive_operations_contract import (
load_latest_ai_agent_proactive_operations_contract,
)
from src.services.ai_agent_professional_task_expansion import (
load_latest_ai_agent_professional_task_expansion,
)
from src.services.ai_agent_receipt_readback_owner_review import (
load_latest_ai_agent_receipt_readback_owner_review,
)
from src.services.ai_agent_redis_dry_run_gate import (
load_latest_ai_agent_redis_dry_run_gate,
)
from src.services.ai_agent_reviewer_queue_no_write_readback import (
load_latest_ai_agent_reviewer_queue_no_write_readback,
)
from src.services.ai_agent_result_capture_no_write_readback import (
load_latest_ai_agent_result_capture_no_write_readback,
)
from src.services.ai_agent_result_capture_release_decision_hold import (
load_latest_ai_agent_result_capture_release_decision_hold,
)
from src.services.ai_agent_result_capture_release_decision_readback import (
load_latest_ai_agent_result_capture_release_decision_readback,
)
from src.services.ai_agent_result_capture_release_decision_next_handoff import (
load_latest_ai_agent_result_capture_release_decision_next_handoff,
)
from src.services.ai_agent_result_capture_release_decision_input_prep import (
load_latest_ai_agent_result_capture_release_decision_input_prep,
)
from src.services.ai_agent_result_capture_release_decision_owner_response_preflight import (
load_latest_ai_agent_result_capture_release_decision_owner_response_preflight,
)
from src.services.ai_agent_result_capture_release_decision_owner_response_readback import (
load_latest_ai_agent_result_capture_release_decision_owner_response_readback,
)
from src.services.ai_agent_result_capture_release_decision_owner_response_acceptance_gate import (
load_latest_ai_agent_result_capture_release_decision_owner_response_acceptance_gate,
)
from src.services.ai_agent_result_capture_owner_promotion_review import (
load_latest_ai_agent_result_capture_owner_promotion_review,
)
from src.services.ai_agent_result_capture_owner_approved_execution_rehearsal import (
load_latest_ai_agent_result_capture_owner_approved_execution_rehearsal,
)
from src.services.ai_agent_result_capture_owner_acceptance_maintenance_gate import (
load_latest_ai_agent_result_capture_owner_acceptance_maintenance_gate,
)
from src.services.ai_agent_result_capture_owner_acceptance_readback_preflight_hold import (
load_latest_ai_agent_result_capture_owner_acceptance_readback_preflight_hold,
)
from src.services.ai_agent_result_capture_owner_approved_preflight_release_package import (
load_latest_ai_agent_result_capture_owner_approved_preflight_release_package,
)
from src.services.ai_agent_result_capture_owner_approved_release_readiness_readback import (
load_latest_ai_agent_result_capture_owner_approved_release_readiness_readback,
)
from src.services.ai_agent_result_capture_owner_release_approval_gate import (
load_latest_ai_agent_result_capture_owner_release_approval_gate,
)
from src.services.ai_agent_result_capture_final_release_candidate_readback import (
load_latest_ai_agent_result_capture_final_release_candidate_readback,
)
from src.services.ai_agent_result_capture_release_authorization_hold import (
load_latest_ai_agent_result_capture_release_authorization_hold,
)
from src.services.ai_agent_result_capture_release_authorization_readback_gate import (
load_latest_ai_agent_result_capture_release_authorization_readback_gate,
)
from src.services.ai_agent_result_capture_release_verifier_preflight_gate import (
load_latest_ai_agent_result_capture_release_verifier_preflight_gate,
)
from src.services.ai_agent_result_capture_release_verifier_owner_review_packet import (
load_latest_ai_agent_result_capture_release_verifier_owner_review_packet,
)
from src.services.ai_agent_result_capture_post_release_verifier_rollback_gate import (
load_latest_ai_agent_result_capture_post_release_verifier_rollback_gate,
)
from src.services.ai_agent_result_capture_promotion_approval_gate import (
load_latest_ai_agent_result_capture_promotion_approval_gate,
)
from src.services.ai_agent_result_capture_write_gate_review import (
load_latest_ai_agent_result_capture_write_gate_review,
)
from src.services.ai_agent_result_capture_writer_implementation_review import (
load_latest_ai_agent_result_capture_writer_implementation_review,
)
from src.services.ai_agent_result_capture_writer_dry_run_fixture import (
load_latest_ai_agent_result_capture_writer_dry_run_fixture,
)
from src.services.ai_agent_result_capture_writer_dry_run_readback import (
load_latest_ai_agent_result_capture_writer_dry_run_readback,
)
from src.services.ai_agent_runtime_readback_approval_package import (
load_latest_ai_agent_runtime_readback_approval_package,
)
from src.services.ai_agent_runtime_readback_implementation_review import (
load_latest_ai_agent_runtime_readback_implementation_review,
)
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_automation_review import (
load_latest_ai_agent_report_automation_review,
)
from src.services.ai_agent_report_live_delivery_approval_package import (
load_latest_ai_agent_report_live_delivery_approval_package,
)
from src.services.ai_agent_report_automation_review import (
load_latest_ai_agent_report_automation_review,
from src.services.ai_agent_report_no_write_analysis_runtime import (
load_latest_ai_agent_report_no_write_analysis_runtime,
)
from src.services.ai_agent_report_runtime_dry_run import (
load_latest_ai_agent_report_runtime_dry_run,
@@ -247,20 +151,119 @@ from src.services.ai_agent_report_runtime_fixture_readback import (
from src.services.ai_agent_report_runtime_readiness import (
load_latest_ai_agent_report_runtime_readiness,
)
from src.services.ai_agent_report_source_health import (
build_ai_agent_report_source_health,
)
from src.services.ai_agent_report_status_board import (
load_latest_ai_agent_report_status_board,
)
from src.services.ai_agent_report_truth_actionability_review import (
load_latest_ai_agent_report_truth_actionability_review,
)
from src.services.ai_agent_result_capture_final_release_candidate_readback import (
load_latest_ai_agent_result_capture_final_release_candidate_readback,
)
from src.services.ai_agent_result_capture_no_write_readback import (
load_latest_ai_agent_result_capture_no_write_readback,
)
from src.services.ai_agent_result_capture_owner_acceptance_maintenance_gate import (
load_latest_ai_agent_result_capture_owner_acceptance_maintenance_gate,
)
from src.services.ai_agent_result_capture_owner_acceptance_readback_preflight_hold import (
load_latest_ai_agent_result_capture_owner_acceptance_readback_preflight_hold,
)
from src.services.ai_agent_result_capture_owner_approved_execution_rehearsal import (
load_latest_ai_agent_result_capture_owner_approved_execution_rehearsal,
)
from src.services.ai_agent_result_capture_owner_approved_preflight_release_package import (
load_latest_ai_agent_result_capture_owner_approved_preflight_release_package,
)
from src.services.ai_agent_result_capture_owner_approved_release_readiness_readback import (
load_latest_ai_agent_result_capture_owner_approved_release_readiness_readback,
)
from src.services.ai_agent_result_capture_owner_promotion_review import (
load_latest_ai_agent_result_capture_owner_promotion_review,
)
from src.services.ai_agent_result_capture_owner_release_approval_gate import (
load_latest_ai_agent_result_capture_owner_release_approval_gate,
)
from src.services.ai_agent_result_capture_post_release_verifier_rollback_gate import (
load_latest_ai_agent_result_capture_post_release_verifier_rollback_gate,
)
from src.services.ai_agent_result_capture_promotion_approval_gate import (
load_latest_ai_agent_result_capture_promotion_approval_gate,
)
from src.services.ai_agent_result_capture_release_authorization_hold import (
load_latest_ai_agent_result_capture_release_authorization_hold,
)
from src.services.ai_agent_result_capture_release_authorization_readback_gate import (
load_latest_ai_agent_result_capture_release_authorization_readback_gate,
)
from src.services.ai_agent_result_capture_release_decision_hold import (
load_latest_ai_agent_result_capture_release_decision_hold,
)
from src.services.ai_agent_result_capture_release_decision_input_prep import (
load_latest_ai_agent_result_capture_release_decision_input_prep,
)
from src.services.ai_agent_result_capture_release_decision_next_handoff import (
load_latest_ai_agent_result_capture_release_decision_next_handoff,
)
from src.services.ai_agent_result_capture_release_decision_owner_response_acceptance_gate import (
load_latest_ai_agent_result_capture_release_decision_owner_response_acceptance_gate,
)
from src.services.ai_agent_result_capture_release_decision_owner_response_preflight import (
load_latest_ai_agent_result_capture_release_decision_owner_response_preflight,
)
from src.services.ai_agent_result_capture_release_decision_owner_response_readback import (
load_latest_ai_agent_result_capture_release_decision_owner_response_readback,
)
from src.services.ai_agent_result_capture_release_decision_readback import (
load_latest_ai_agent_result_capture_release_decision_readback,
)
from src.services.ai_agent_result_capture_release_verifier_owner_review_packet import (
load_latest_ai_agent_result_capture_release_verifier_owner_review_packet,
)
from src.services.ai_agent_result_capture_release_verifier_preflight_gate import (
load_latest_ai_agent_result_capture_release_verifier_preflight_gate,
)
from src.services.ai_agent_result_capture_write_gate_review import (
load_latest_ai_agent_result_capture_write_gate_review,
)
from src.services.ai_agent_result_capture_writer_dry_run_fixture import (
load_latest_ai_agent_result_capture_writer_dry_run_fixture,
)
from src.services.ai_agent_result_capture_writer_dry_run_readback import (
load_latest_ai_agent_result_capture_writer_dry_run_readback,
)
from src.services.ai_agent_result_capture_writer_implementation_review import (
load_latest_ai_agent_result_capture_writer_implementation_review,
)
from src.services.ai_agent_reviewer_queue_no_write_readback import (
load_latest_ai_agent_reviewer_queue_no_write_readback,
)
from src.services.ai_agent_runtime_readback_approval_package import (
load_latest_ai_agent_runtime_readback_approval_package,
)
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_implementation_review import (
load_latest_ai_agent_runtime_readback_implementation_review,
)
from src.services.ai_agent_runtime_readback_promotion_gate import (
load_latest_ai_agent_runtime_readback_promotion_gate,
)
from src.services.ai_agent_runtime_verifier_evidence_review import (
load_latest_ai_agent_runtime_verifier_evidence_review,
)
from src.services.ai_agent_runtime_worker_shadow_gate import (
load_latest_ai_agent_runtime_worker_shadow_gate,
)
from src.services.ai_agent_runtime_write_gate_review import (
load_latest_ai_agent_runtime_write_gate_review,
)
from src.services.ai_agent_runtime_verifier_evidence_review import (
load_latest_ai_agent_runtime_verifier_evidence_review,
from src.services.ai_agent_task_result_audit_trail import (
load_latest_ai_agent_task_result_audit_trail,
)
from src.services.ai_agent_telegram_action_required_digest_policy import (
load_latest_ai_agent_telegram_action_required_digest_policy,
@@ -268,9 +271,6 @@ from src.services.ai_agent_telegram_action_required_digest_policy import (
from src.services.ai_agent_telegram_receipt_approval_package import (
load_latest_ai_agent_telegram_receipt_approval_package,
)
from src.services.ai_agent_task_result_audit_trail import (
load_latest_ai_agent_task_result_audit_trail,
)
from src.services.ai_agent_tool_adoption_approval_package import (
load_latest_ai_agent_tool_adoption_approval_package,
)
@@ -310,6 +310,9 @@ from src.services.docker_build_surface_inventory import (
from src.services.gitea_workflow_runner_health import (
load_latest_gitea_workflow_runner_health,
)
from src.services.host_runaway_aiops_loop_readiness import (
load_latest_host_runaway_aiops_loop_readiness,
)
from src.services.javascript_package_inventory import (
load_latest_javascript_package_inventory,
)
@@ -325,10 +328,10 @@ from src.services.package_supply_chain_inventory import (
from src.services.product_code_review_gate import (
load_latest_product_code_review_gate,
)
from src.services.public_redaction import redact_public_lan_topology
from src.services.runtime_surface_inventory import (
load_latest_runtime_surface_inventory,
)
from src.services.public_redaction import redact_public_lan_topology
from src.services.service_health_failure_notification_policy import (
load_latest_service_health_failure_notification_policy,
)
@@ -939,6 +942,36 @@ async def get_agent_high_risk_owner_review_queue() -> dict[str, Any]:
) from exc
@router.get(
"/agent-action-audit-ledger",
response_model=dict[str, Any],
summary="取得 P2-410 AI Agent 行動審計帳本",
description=(
"讀取最新已提交的 P2-410 AI Agent 行動審計帳本快照;此端點只呈現 "
"immutable audit event template、source readback、verifier receipt gate、redaction contract "
"與 no-write activation boundary。它不寫 audit DB、不寫 timeline、不寫 KM、不更新 PlayBook trust、"
"不寫 Gateway queue、不送 Telegram、不呼叫 Bot API、不寫 production、不讀 secret、"
"不呼叫付費 API、不改主機、不執行 kubectl 或不可逆操作。"
),
)
async def get_agent_action_audit_ledger() -> dict[str, Any]:
"""回傳最新 P2-410 action audit ledger 只讀快照。"""
try:
payload = await asyncio.to_thread(load_latest_ai_agent_action_audit_ledger)
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_action_audit_ledger_invalid", error=str(exc))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="P2-410 AI Agent 行動審計帳本快照無效",
) from exc
@router.get(
"/agent-host-runaway-aiops-loop-readiness",
response_model=dict[str, Any],

View File

@@ -0,0 +1,323 @@
"""
P2-410 AI Agent action audit ledger snapshot.
Loads the latest committed action audit ledger. This module validates read-only
event templates and verifier receipt gates. It never writes audit DB rows,
timeline events, KM, PlayBook trust, Gateway queues, Telegram messages, secrets,
hosts, Kubernetes resources, or production state.
"""
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_action_audit_ledger_*.json"
_SCHEMA_VERSION = "ai_agent_action_audit_ledger_v1"
_RUNTIME_AUTHORITY = "agent_action_audit_ledger_no_live_write_committed_snapshot"
_EXPECTED_CURRENT_TASK = "P2-410"
_EXPECTED_NEXT_TASK = "P2-411"
_EXPECTED_SOURCE_SCHEMAS = {
"ai_agent_low_medium_risk_whitelist_v1",
"ai_agent_high_risk_owner_review_queue_v1",
"ai_agent_task_result_audit_trail_v1",
"awoooi_sre_digest_no_send_preview_v1",
"awoooi_work_items_report_source_gap_owner_review_v1",
"telegram_notification_egress_no_new_bypass_guard_v1",
"governance_automation_inventory_readback_v1",
}
_TRUE_TRUTH_FLAGS = {
"p2_408_whitelist_loaded",
"p2_409_owner_queue_loaded",
"p2_103_result_audit_loaded",
"p2_110c_sre_digest_loaded",
"p2_110e_work_items_loaded",
"telegram_no_new_bypass_loaded",
"audit_event_templates_ready",
"verifier_receipt_gates_ready",
"immutable_event_required",
"redacted_evidence_refs_required",
"read_only_mode",
}
_FALSE_TRUTH_FLAGS = {
"audit_db_write_enabled",
"timeline_write_enabled",
"km_write_enabled",
"playbook_trust_write_enabled",
"gateway_queue_write_enabled",
"telegram_send_enabled",
"bot_api_call_enabled",
"receipt_production_write_enabled",
"production_write_enabled",
"secret_read_enabled",
"paid_api_call_enabled",
"host_write_enabled",
"kubectl_action_enabled",
"destructive_operation_enabled",
}
_ZERO_TRUTH_COUNTS = {
"audit_db_write_count_24h",
"timeline_write_count_24h",
"km_write_count_24h",
"playbook_trust_write_count_24h",
"gateway_queue_write_count_24h",
"telegram_send_count_24h",
"bot_api_call_count_24h",
"receipt_production_write_count_24h",
"production_write_count_24h",
"secret_read_count_24h",
"paid_api_call_count_24h",
"host_write_count_24h",
"kubectl_action_count_24h",
"destructive_operation_count_24h",
}
_FALSE_EVENT_FLAGS = {
"audit_db_write_allowed",
"timeline_write_allowed",
"km_write_allowed",
"playbook_trust_write_allowed",
"gateway_queue_write_allowed",
"telegram_send_allowed",
"production_write_allowed",
}
_FALSE_BOUNDARY_FLAGS = _FALSE_TRUTH_FLAGS
_ZERO_ROLLUP_FIELDS = {
"audit_db_write_count",
"timeline_write_count",
"km_write_count",
"playbook_trust_write_count",
"gateway_queue_write_count",
"telegram_send_count",
"bot_api_call_count",
"receipt_production_write_count",
"production_write_count",
"secret_read_count",
"paid_api_call_count",
"host_write_count",
"kubectl_action_count",
"destructive_operation_count",
"owner_response_received_count",
"owner_response_accepted_count",
}
_FORBIDDEN_PUBLIC_TERMS = {
"批准" + "",
"In app " + "browser",
"My request for " + "Codex",
"codex_" + "delegation",
"source_" + "thread_id",
"chain_of_thought",
"private reasoning text",
"authorization_header",
"telegram token value",
"raw_payload",
"raw prompt",
"internal collaboration transcript",
}
def load_latest_ai_agent_action_audit_ledger(
evaluations_dir: Path | None = None,
) -> dict[str, Any]:
"""Load the newest committed P2-410 action audit ledger snapshot."""
directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR
candidates = sorted(directory.glob(_SNAPSHOT_PATTERN))
if not candidates:
raise FileNotFoundError(f"no AI Agent action audit ledger 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_sources(payload, label)
_require_audit_truth(payload, label)
_require_audit_event_templates(payload, label)
_require_verifier_receipt_gates(payload, label)
_require_activation_boundaries(payload, label)
_require_redaction_contract(payload, label)
_require_rollups(payload, label)
_require_no_forbidden_public_terms(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 = {
"overall_completion_percent": 100,
"current_priority": "P0",
"current_task_id": _EXPECTED_CURRENT_TASK,
"next_task_id": _EXPECTED_NEXT_TASK,
"read_only_mode": True,
"runtime_authority": _RUNTIME_AUTHORITY,
}
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_sources(payload: dict[str, Any], label: str) -> None:
if not payload.get("source_refs"):
raise ValueError(f"{label}: source_refs must not be empty")
sources = payload.get("source_readbacks") or []
schemas = {item.get("source_schema_version") for item in sources}
missing = sorted(_EXPECTED_SOURCE_SCHEMAS - schemas)
if missing:
raise ValueError(f"{label}: missing source schemas: {missing}")
for item in sources:
readback_id = item.get("readback_id") or "<missing>"
for field in ("source_ref", "endpoint", "owner_agent", "status", "key_readback", "next_action"):
if not item.get(field):
raise ValueError(f"{label}: source readback {readback_id} missing {field}")
def _require_audit_truth(payload: dict[str, Any], label: str) -> None:
truth = payload.get("audit_truth") or {}
missing_true = sorted(flag for flag in _TRUE_TRUTH_FLAGS if truth.get(flag) is not True)
if missing_true:
raise ValueError(f"{label}: audit truth flags must remain true: {missing_true}")
unsafe_false = sorted(flag for flag in _FALSE_TRUTH_FLAGS if truth.get(flag) is not False)
if unsafe_false:
raise ValueError(f"{label}: audit truth flags must remain false: {unsafe_false}")
non_zero = sorted(field for field in _ZERO_TRUTH_COUNTS if truth.get(field) != 0)
if non_zero:
raise ValueError(f"{label}: audit truth counts must remain zero: {non_zero}")
if not truth.get("truth_note"):
raise ValueError(f"{label}: audit_truth.truth_note is required")
def _require_audit_event_templates(payload: dict[str, Any], label: str) -> None:
events = payload.get("audit_event_templates") or []
if not events:
raise ValueError(f"{label}: audit_event_templates must not be empty")
source_ids = {item.get("readback_id") for item in payload.get("source_readbacks") or []}
risk_tiers = {event.get("risk_tier") for event in events}
if not {"low", "medium", "high", "critical"}.issubset(risk_tiers):
raise ValueError(f"{label}: audit event templates must cover low, medium, high, and critical")
for event in events:
event_id = event.get("audit_event_id") or "<missing>"
if event.get("immutable_event_required") is not True:
raise ValueError(f"{label}: event {event_id}.immutable_event_required must remain true")
unsafe = sorted(flag for flag in _FALSE_EVENT_FLAGS if event.get(flag) is not False)
if unsafe:
raise ValueError(f"{label}: event {event_id} write/send flags must remain false: {unsafe}")
if event.get("side_effect_count") != 0:
raise ValueError(f"{label}: event {event_id}.side_effect_count must remain zero")
for field in ("source_readback_ids", "required_audit_fields", "required_evidence_refs", "blocked_writes", "next_gate"):
if not event.get(field):
raise ValueError(f"{label}: event {event_id} missing {field}")
missing_sources = sorted(set(event.get("source_readback_ids") or []) - source_ids)
if missing_sources:
raise ValueError(f"{label}: event {event_id} references missing source readbacks: {missing_sources}")
def _require_verifier_receipt_gates(payload: dict[str, Any], label: str) -> None:
gates = payload.get("verifier_receipt_gates") or []
if len(gates) < 1:
raise ValueError(f"{label}: verifier_receipt_gates must not be empty")
for gate in gates:
gate_id = gate.get("gate_id") or "<missing>"
if not gate.get("required_checks"):
raise ValueError(f"{label}: verifier gate {gate_id} missing required_checks")
if not gate.get("failure_if_missing"):
raise ValueError(f"{label}: verifier gate {gate_id} missing failure_if_missing")
for field in ("live_verifier_allowed", "receipt_write_allowed", "runtime_action_allowed"):
if gate.get(field) is not False:
raise ValueError(f"{label}: verifier gate {gate_id}.{field} must remain false")
def _require_activation_boundaries(payload: dict[str, Any], label: str) -> None:
boundaries = payload.get("activation_boundaries") or {}
required_true = {
"committed_snapshot_read_allowed",
"audit_event_template_preview_allowed",
"verifier_receipt_gate_preview_allowed",
"governance_ui_projection_allowed",
}
missing = sorted(field for field in required_true if boundaries.get(field) is not True)
if missing:
raise ValueError(f"{label}: activation boundaries must remain true: {missing}")
unsafe = sorted(field for field in _FALSE_BOUNDARY_FLAGS if boundaries.get(field) is not False)
if unsafe:
raise ValueError(f"{label}: activation boundaries must remain false: {unsafe}")
def _require_redaction_contract(payload: dict[str, Any], label: str) -> None:
contract = payload.get("display_redaction_contract") or {}
required_false = {
"unsafe_payload_display_allowed",
"private_reasoning_display_allowed",
"secret_value_display_allowed",
"raw_prompt_display_allowed",
"work_window_transcript_display_allowed",
}
if contract.get("redaction_required") is not True:
raise ValueError(f"{label}: redaction_required must remain true")
unsafe = sorted(field for field in required_false if contract.get(field) is not False)
if unsafe:
raise ValueError(f"{label}: display redaction flags must remain false: {unsafe}")
if not contract.get("allowed_display_fields") or not contract.get("blocked_display_fields"):
raise ValueError(f"{label}: display redaction contract must list allowed and blocked fields")
def _require_rollups(payload: dict[str, Any], label: str) -> None:
rollups = payload.get("rollups") or {}
events = payload.get("audit_event_templates") or []
gates = payload.get("verifier_receipt_gates") or []
sources = payload.get("source_readbacks") or []
expected_counts = {
"source_readback_count": len(sources),
"audit_event_template_count": len(events),
"verifier_receipt_gate_count": len(gates),
"low_medium_event_count": sum(1 for event in events if event.get("risk_tier") in {"low", "medium"}),
"high_risk_event_count": sum(1 for event in events if event.get("risk_tier") == "high"),
"critical_event_count": sum(1 for event in events if event.get("risk_tier") == "critical"),
"report_gap_event_count": sum(
1 for event in events if any("p2_110" in source for source in event.get("source_readback_ids") or [])
),
"telegram_event_count": sum(
1
for event in events
if any("telegram" in source for source in event.get("source_readback_ids") or [])
),
"required_audit_field_count": sum(len(event.get("required_audit_fields") or []) for event in events),
"blocked_runtime_action_count": len(
{
blocked
for event in events
for blocked in event.get("blocked_writes") or []
}
),
}
mismatches = _mismatches(rollups, expected_counts)
if mismatches:
raise ValueError(f"{label}: rollup counts mismatch: {mismatches}")
non_zero = sorted(field for field in _ZERO_ROLLUP_FIELDS if rollups.get(field) != 0)
if non_zero:
raise ValueError(f"{label}: live write/send rollups must remain zero: {non_zero}")
def _require_no_forbidden_public_terms(payload: dict[str, Any], label: str) -> None:
haystack = json.dumps(payload, ensure_ascii=False)
hits = sorted(term for term in _FORBIDDEN_PUBLIC_TERMS if term in haystack)
if hits:
raise ValueError(f"{label}: forbidden public terms detected: {hits}")
def _mismatches(source: dict[str, Any], expected: dict[str, Any]) -> dict[str, Any]:
return {
field: {"expected": value, "actual": source.get(field)}
for field, value in expected.items()
if source.get(field) != value
}

View File

@@ -0,0 +1,133 @@
from __future__ import annotations
import copy
import json
from pathlib import Path
import pytest
from src.services.ai_agent_action_audit_ledger import (
load_latest_ai_agent_action_audit_ledger,
)
_REPO_ROOT = Path(__file__).resolve().parents[3]
_COMMITTED_SNAPSHOT = (
_REPO_ROOT
/ "docs"
/ "evaluations"
/ "ai_agent_action_audit_ledger_2026-06-19.json"
)
def test_load_latest_ai_agent_action_audit_ledger_reads_newest_file(tmp_path):
older = _snapshot(generated_at="2026-06-18T23:00:00+08:00")
newer = _snapshot(generated_at="2026-06-19T04:10:00+08:00")
(tmp_path / "ai_agent_action_audit_ledger_2026-06-18.json").write_text(
json.dumps(older),
encoding="utf-8",
)
(tmp_path / "ai_agent_action_audit_ledger_2026-06-19.json").write_text(
json.dumps(newer),
encoding="utf-8",
)
loaded = load_latest_ai_agent_action_audit_ledger(tmp_path)
assert loaded["generated_at"] == "2026-06-19T04:10:00+08:00"
assert loaded["program_status"]["current_task_id"] == "P2-410"
assert loaded["program_status"]["next_task_id"] == "P2-411"
assert loaded["rollups"]["audit_event_template_count"] == 8
assert loaded["rollups"]["verifier_receipt_gate_count"] == 5
assert loaded["rollups"]["telegram_send_count"] == 0
def test_ai_agent_action_audit_ledger_requires_read_only_mode(tmp_path):
snapshot = _snapshot()
snapshot["program_status"]["read_only_mode"] = False
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="program_status"):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def test_ai_agent_action_audit_ledger_blocks_audit_db_write(tmp_path):
snapshot = _snapshot()
snapshot["audit_truth"]["audit_db_write_enabled"] = True
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="audit_db_write_enabled"):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def test_ai_agent_action_audit_ledger_blocks_event_gateway_write(tmp_path):
snapshot = _snapshot()
snapshot["audit_event_templates"][0]["gateway_queue_write_allowed"] = True
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="gateway_queue_write_allowed"):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def test_ai_agent_action_audit_ledger_requires_all_risk_tiers(tmp_path):
snapshot = _snapshot()
for event in snapshot["audit_event_templates"]:
event["risk_tier"] = "medium"
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="low, medium, high, and critical"):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def test_ai_agent_action_audit_ledger_rejects_missing_source_ref(tmp_path):
snapshot = _snapshot()
snapshot["audit_event_templates"][0]["source_readback_ids"] = ["missing_source"]
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="missing source readbacks"):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def test_ai_agent_action_audit_ledger_blocks_verifier_live_mode(tmp_path):
snapshot = _snapshot()
snapshot["verifier_receipt_gates"][0]["live_verifier_allowed"] = True
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="live_verifier_allowed"):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def test_ai_agent_action_audit_ledger_requires_rollup_consistency(tmp_path):
snapshot = _snapshot()
snapshot["rollups"]["audit_event_template_count"] = 99
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="rollup counts"):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def test_ai_agent_action_audit_ledger_rejects_private_terms(tmp_path):
snapshot = _snapshot()
snapshot["audit_event_templates"][0]["display_name"] = "請把 " + "In app " + "browser" + " 內容放進前端"
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="forbidden public terms"):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def test_ai_agent_action_audit_ledger_fails_when_missing(tmp_path):
with pytest.raises(FileNotFoundError):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def _snapshot(*, generated_at: str = "2026-06-19T04:10:00+08:00") -> dict:
payload = json.loads(_COMMITTED_SNAPSHOT.read_text(encoding="utf-8"))
cloned = copy.deepcopy(payload)
cloned["generated_at"] = generated_at
return cloned
def _write_snapshot(tmp_path, payload: dict) -> None:
(tmp_path / "ai_agent_action_audit_ledger_2026-06-19.json").write_text(
json.dumps(payload),
encoding="utf-8",
)

View File

@@ -0,0 +1,47 @@
from __future__ import annotations
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.v1.agents import router
def test_ai_agent_action_audit_ledger_endpoint_returns_committed_snapshot():
app = FastAPI()
app.include_router(router, prefix="/api/v1")
client = TestClient(app)
response = client.get("/api/v1/agents/agent-action-audit-ledger")
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == "ai_agent_action_audit_ledger_v1"
assert data["program_status"]["current_task_id"] == "P2-410"
assert data["program_status"]["next_task_id"] == "P2-411"
assert data["program_status"]["read_only_mode"] is True
assert (
data["program_status"]["runtime_authority"]
== "agent_action_audit_ledger_no_live_write_committed_snapshot"
)
assert data["rollups"]["source_readback_count"] == len(data["source_readbacks"]) == 7
assert data["rollups"]["audit_event_template_count"] == len(data["audit_event_templates"]) == 8
assert data["rollups"]["verifier_receipt_gate_count"] == len(data["verifier_receipt_gates"]) == 5
assert data["rollups"]["low_medium_event_count"] == 4
assert data["rollups"]["high_risk_event_count"] == 3
assert data["rollups"]["critical_event_count"] == 1
assert data["rollups"]["report_gap_event_count"] == 2
assert data["rollups"]["telegram_event_count"] == 2
assert data["rollups"]["blocked_runtime_action_count"] == 23
assert data["rollups"]["audit_db_write_count"] == 0
assert data["rollups"]["timeline_write_count"] == 0
assert data["rollups"]["km_write_count"] == 0
assert data["rollups"]["playbook_trust_write_count"] == 0
assert data["rollups"]["gateway_queue_write_count"] == 0
assert data["rollups"]["telegram_send_count"] == 0
assert data["rollups"]["bot_api_call_count"] == 0
assert data["rollups"]["production_write_count"] == 0
assert data["activation_boundaries"]["committed_snapshot_read_allowed"] is True
assert data["activation_boundaries"]["audit_db_write_enabled"] is False
assert data["activation_boundaries"]["telegram_send_enabled"] is False
assert data["display_redaction_contract"]["redaction_required"] is True
assert data["display_redaction_contract"]["secret_value_display_allowed"] is False