feat(governance): 新增 learning writeback approval package
All checks were successful
CD Pipeline / tests (push) Successful in 1m27s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 6m51s
CD Pipeline / post-deploy-checks (push) Successful in 1m36s

This commit is contained in:
Your Name
2026-06-11 21:43:09 +08:00
parent 118967cabc
commit 6e17051b4d
21 changed files with 970 additions and 52 deletions

View File

@@ -64,6 +64,9 @@ from src.services.ai_agent_host_stateful_version_inventory import (
from src.services.ai_agent_interaction_learning_proof import (
load_latest_ai_agent_interaction_learning_proof,
)
from src.services.ai_agent_learning_writeback_approval_package import (
load_latest_ai_agent_learning_writeback_approval_package,
)
from src.services.ai_agent_live_read_model_gate import (
load_latest_ai_agent_live_read_model_gate,
)
@@ -663,6 +666,33 @@ async def get_agent_redis_dry_run_gate() -> dict[str, Any]:
) from exc
@router.get(
"/agent-learning-writeback-approval-package",
response_model=dict[str, Any],
summary="取得 AI Agent learning writeback approval package",
description=(
"讀取最新已提交的 KM / PlayBook trust / timeline learning / replay score 回寫批准包;"
"此端點不寫 KM、不更新 PlayBook trust、不寫 timeline、不寫 replay score、不送 Telegram、"
"不啟動 runtime worker、不回傳未核准內部細節。"
),
)
async def get_agent_learning_writeback_approval_package() -> dict[str, Any]:
"""Return the latest read-only AI Agent learning writeback approval package."""
try:
return await asyncio.to_thread(load_latest_ai_agent_learning_writeback_approval_package)
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_learning_writeback_approval_package_invalid", error=str(exc))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="AI Agent learning writeback approval package 無效",
) from exc
@router.get(
"/agent-proactive-operations-contract",
response_model=dict[str, Any],

View File

@@ -0,0 +1,157 @@
"""
AI Agent learning writeback approval package snapshot.
Loads the latest committed P2-403D approval package for KM, PlayBook trust,
timeline learning, and replay score writeback. This module never writes KM,
updates PlayBook trust, writes timeline events, sends Telegram messages, or
starts runtime workers.
"""
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_learning_writeback_approval_package_*.json"
_SCHEMA_VERSION = "ai_agent_learning_writeback_approval_package_v1"
def load_latest_ai_agent_learning_writeback_approval_package(
evaluations_dir: Path | None = None,
) -> dict[str, Any]:
"""Load the newest committed AI Agent learning writeback approval package."""
directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR
candidates = sorted(directory.glob(_SNAPSHOT_PATTERN))
if not candidates:
raise FileNotFoundError(
f"no AI Agent learning writeback approval package 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_read_only_boundaries(payload, str(latest))
_require_package_safety(payload, str(latest))
_require_rollup_consistency(payload, str(latest))
return payload
def _require_schema(payload: dict[str, Any], label: str) -> None:
actual = payload.get("schema_version")
if actual != _SCHEMA_VERSION:
raise ValueError(f"{label}: expected schema_version={_SCHEMA_VERSION}, got {actual!r}")
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") != "approval_package_only_no_learning_writeback":
raise ValueError(f"{label}: runtime_authority must stay approval_package_only_no_learning_writeback")
def _require_read_only_boundaries(payload: dict[str, Any], label: str) -> None:
boundaries = payload.get("approval_boundaries") or {}
enabled = sorted(key for key, value in boundaries.items() if value is not False)
if enabled:
raise ValueError(f"{label}: approval boundaries must remain false: {enabled}")
truth = payload.get("learning_truth") or {}
false_flags = {
"km_write_allowed",
"playbook_trust_write_allowed",
"timeline_learning_write_allowed",
"agent_replay_score_write_allowed",
"telegram_send_allowed",
"runtime_worker_allowed",
}
unsafe = sorted(flag for flag in false_flags if truth.get(flag) is not False)
if unsafe:
raise ValueError(f"{label}: learning truth flags must remain false: {unsafe}")
zero_counts = {
"live_learning_write_count",
"live_playbook_trust_update_count",
"live_km_update_count",
}
non_zero = sorted(key for key in zero_counts if truth.get(key) != 0)
if non_zero:
raise ValueError(f"{label}: live learning write counts must remain zero: {non_zero}")
def _require_package_safety(payload: dict[str, Any], label: str) -> None:
package = payload.get("writeback_package") or {}
required_fields = set(package.get("required_fields") or [])
required_minimum = {
"learning_event_id",
"incident_id",
"target_surface",
"proposed_delta_summary",
"redacted_evidence_ref",
"owner_review_required",
"rollback_plan_ref",
}
missing = sorted(required_minimum - required_fields)
if missing:
raise ValueError(f"{label}: writeback package missing required fields: {missing}")
if package.get("owner_review_required") is not True:
raise ValueError(f"{label}: owner review must be required")
if package.get("rollback_required") is not True:
raise ValueError(f"{label}: rollback must be required")
redaction = payload.get("display_redaction_contract") or {}
if redaction.get("redaction_required") is not True:
raise ValueError(f"{label}: frontend redaction must be required")
for flag in ("raw_payload_display_allowed", "private_reasoning_display_allowed", "secret_value_display_allowed"):
if redaction.get(flag) is not False:
raise ValueError(f"{label}: {flag} must remain false")
rollback = payload.get("rollback_contract") or {}
if rollback.get("rollback_required") is not True:
raise ValueError(f"{label}: rollback_contract.rollback_required must be true")
if not rollback.get("rollback_steps"):
raise ValueError(f"{label}: rollback steps must not be empty")
def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None:
rollups = payload.get("rollups") or {}
gates = payload.get("review_gates") or []
lanes = payload.get("learning_lanes") or []
package = payload.get("writeback_package") or {}
truth = payload.get("learning_truth") or {}
expected_counts = {
"review_gate_count": len(gates),
"learning_lane_count": len(lanes),
"blocked_write_action_count": len({gate.get("blocked_write_action") for gate in gates}),
"required_field_count": len(package.get("required_fields") or []),
"forbidden_field_count": len(package.get("forbidden_fields") or []),
}
mismatched = {
key: {"expected": expected, "actual": rollups.get(key)}
for key, expected in expected_counts.items()
if rollups.get(key) != expected
}
if mismatched:
raise ValueError(f"{label}: rollup counts must match payload sections: {mismatched}")
approval_required = sorted(
gate.get("gate_id") for gate in gates if gate.get("status") == "approval_required"
)
if sorted(rollups.get("approval_required_gate_ids") or []) != approval_required:
raise ValueError(f"{label}: rollups.approval_required_gate_ids mismatch")
live_total = sum(
int(truth.get(key) or 0)
for key in (
"live_learning_write_count",
"live_playbook_trust_update_count",
"live_km_update_count",
)
)
if rollups.get("live_write_count_total") != live_total:
raise ValueError(f"{label}: rollups.live_write_count_total mismatch")