feat(governance): 新增 Agent 日週月報風險自動化審查
This commit is contained in:
154
apps/api/src/services/ai_agent_report_automation_review.py
Normal file
154
apps/api/src/services/ai_agent_report_automation_review.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
AI Agent report automation review snapshot.
|
||||
|
||||
Loads the latest committed P2-403J daily / weekly / monthly report, workload,
|
||||
chart, and risk-tier automation policy review. This module never schedules a
|
||||
live report, sends Telegram, writes optimization changes, or starts an
|
||||
automation worker.
|
||||
"""
|
||||
|
||||
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_report_automation_review_*.json"
|
||||
_SCHEMA_VERSION = "ai_agent_report_automation_review_v1"
|
||||
|
||||
|
||||
def load_latest_ai_agent_report_automation_review(
|
||||
evaluations_dir: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Load the newest committed AI Agent report automation review snapshot."""
|
||||
directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR
|
||||
candidates = sorted(directory.glob(_SNAPSHOT_PATTERN))
|
||||
if not candidates:
|
||||
raise FileNotFoundError(f"no AI Agent report automation review 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_report_contract(payload, str(latest))
|
||||
_require_runtime_boundaries(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") != "reporting_and_risk_policy_review_only_no_live_execution":
|
||||
raise ValueError(
|
||||
f"{label}: runtime_authority must remain reporting_and_risk_policy_review_only_no_live_execution"
|
||||
)
|
||||
|
||||
|
||||
def _require_report_contract(payload: dict[str, Any], label: str) -> None:
|
||||
cadences = payload.get("report_cadences") or []
|
||||
cadence_ids = {cadence.get("cadence_id") for cadence in cadences}
|
||||
if cadence_ids != {"daily", "weekly", "monthly"}:
|
||||
raise ValueError(f"{label}: report cadences must include daily, weekly, monthly")
|
||||
|
||||
agent_ids = {agent.get("agent_id") for agent in payload.get("agent_workload_metrics") or []}
|
||||
if agent_ids != {"openclaw", "hermes", "nemotron"}:
|
||||
raise ValueError(f"{label}: workload metrics must include OpenClaw, Hermes, NemoTron")
|
||||
|
||||
if not payload.get("report_charts"):
|
||||
raise ValueError(f"{label}: report charts must not be empty")
|
||||
if not payload.get("analysis_recommendations"):
|
||||
raise ValueError(f"{label}: analysis recommendations must not be empty")
|
||||
|
||||
risk_ids = {tier.get("risk_id") for tier in (payload.get("risk_tier_policy") or {}).get("risk_tiers") or []}
|
||||
if not {"low", "medium", "high", "critical"}.issubset(risk_ids):
|
||||
raise ValueError(f"{label}: risk tier policy must include low, medium, high, critical")
|
||||
|
||||
|
||||
def _require_runtime_boundaries(payload: dict[str, Any], label: str) -> None:
|
||||
truth = payload.get("report_truth") or {}
|
||||
if truth.get("high_risk_requires_approval") is not True:
|
||||
raise ValueError(f"{label}: high risk approval gate must remain true")
|
||||
if truth.get("medium_low_auto_policy_defined") is not True:
|
||||
raise ValueError(f"{label}: medium / low auto policy must be defined")
|
||||
|
||||
zero_counts = {
|
||||
"report_delivery_count_24h",
|
||||
"report_read_receipt_count_24h",
|
||||
"live_auto_optimization_count_24h",
|
||||
"live_medium_low_auto_execution_count_24h",
|
||||
}
|
||||
non_zero = sorted(key for key in zero_counts if truth.get(key) != 0)
|
||||
if non_zero:
|
||||
raise ValueError(f"{label}: live report / automation counts must remain zero: {non_zero}")
|
||||
|
||||
false_flags = {
|
||||
"report_delivery_enabled",
|
||||
"ai_analysis_after_report_enabled",
|
||||
"medium_low_auto_execution_enabled",
|
||||
}
|
||||
unsafe_truth = sorted(flag for flag in false_flags if truth.get(flag) is not False)
|
||||
if unsafe_truth:
|
||||
raise ValueError(f"{label}: live report automation flags must remain false: {unsafe_truth}")
|
||||
|
||||
boundaries = payload.get("approval_boundaries") or {}
|
||||
unsafe_boundaries = sorted(
|
||||
key
|
||||
for key, value in boundaries.items()
|
||||
if key != "high_risk_requires_human_approval" and value is not False
|
||||
)
|
||||
if unsafe_boundaries:
|
||||
raise ValueError(f"{label}: approval boundaries must remain false: {unsafe_boundaries}")
|
||||
if boundaries.get("high_risk_requires_human_approval") is not True:
|
||||
raise ValueError(f"{label}: high_risk_requires_human_approval must remain true")
|
||||
|
||||
|
||||
def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None:
|
||||
rollups = payload.get("rollups") or {}
|
||||
workload = payload.get("agent_workload_metrics") or []
|
||||
recommendations = payload.get("analysis_recommendations") or []
|
||||
charts = payload.get("report_charts") or []
|
||||
cadences = payload.get("report_cadences") or []
|
||||
|
||||
expected = {
|
||||
"report_cadence_count": len(cadences),
|
||||
"agent_count": len(workload),
|
||||
"chart_count": len(charts),
|
||||
"recommendation_count": len(recommendations),
|
||||
"workload_unit_total": sum(item.get("work_units_total", 0) for item in workload),
|
||||
"workload_done_total": sum(item.get("work_units_done", 0) for item in workload),
|
||||
"workload_waiting_approval_total": sum(item.get("work_units_waiting_approval", 0) for item in workload),
|
||||
"live_report_delivery_count": 0,
|
||||
"live_auto_optimization_count": 0,
|
||||
}
|
||||
for risk in ("low", "medium", "high", "critical"):
|
||||
expected[f"{risk}_risk_recommendation_count"] = len(
|
||||
[item for item in recommendations if item.get("risk_tier") == risk]
|
||||
)
|
||||
|
||||
mismatched = {
|
||||
key: {"expected": value, "actual": rollups.get(key)}
|
||||
for key, value in expected.items()
|
||||
if rollups.get(key) != value
|
||||
}
|
||||
if mismatched:
|
||||
raise ValueError(f"{label}: rollup counts must match payload sections: {mismatched}")
|
||||
|
||||
approval_required = sorted(
|
||||
item.get("recommendation_id")
|
||||
for item in recommendations
|
||||
if item.get("approval_required") is True
|
||||
)
|
||||
if sorted(rollups.get("approval_required_recommendation_ids") or []) != approval_required:
|
||||
raise ValueError(f"{label}: approval_required_recommendation_ids mismatch")
|
||||
if rollups.get("current_auto_execution_enabled_count") != 0:
|
||||
raise ValueError(f"{label}: current auto execution count must remain zero")
|
||||
Reference in New Issue
Block a user