feat(governance): 新增 Telegram receipt approval package
All checks were successful
CD Pipeline / tests (push) Successful in 1m30s
Code Review / ai-code-review (push) Successful in 27s
CD Pipeline / build-and-deploy (push) Successful in 4m11s
CD Pipeline / post-deploy-checks (push) Successful in 1m55s

This commit is contained in:
Your Name
2026-06-11 22:11:27 +08:00
parent 7cd475581a
commit aec3657f5d
21 changed files with 982 additions and 47 deletions

View File

@@ -79,6 +79,9 @@ from src.services.ai_agent_redis_dry_run_gate import (
from src.services.ai_agent_telegram_action_required_digest_policy import (
load_latest_ai_agent_telegram_action_required_digest_policy,
)
from src.services.ai_agent_telegram_receipt_approval_package import (
load_latest_ai_agent_telegram_receipt_approval_package,
)
from src.services.ai_agent_tool_adoption_approval_package import (
load_latest_ai_agent_tool_adoption_approval_package,
)
@@ -693,6 +696,33 @@ async def get_agent_learning_writeback_approval_package() -> dict[str, Any]:
) from exc
@router.get(
"/agent-telegram-receipt-approval-package",
response_model=dict[str, Any],
summary="取得 AI Agent Telegram receipt approval package",
description=(
"讀取最新已提交的 Telegram receipt / queue / delivery / ack / failure / retry 批准包;"
"此端點不寫 Gateway queue、不呼叫 Telegram Bot API、不改 receiver route、不發送通知、"
"不啟動 runtime worker、不回傳 Telegram token、raw chat id 或未脫敏 payload。"
),
)
async def get_agent_telegram_receipt_approval_package() -> dict[str, Any]:
"""Return the latest read-only AI Agent Telegram receipt approval package."""
try:
return await asyncio.to_thread(load_latest_ai_agent_telegram_receipt_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_telegram_receipt_approval_package_invalid", error=str(exc))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="AI Agent Telegram receipt approval package 無效",
) from exc
@router.get(
"/agent-proactive-operations-contract",
response_model=dict[str, Any],

View File

@@ -0,0 +1,163 @@
"""
AI Agent Telegram receipt approval package snapshot.
Loads the latest committed P2-403E approval package for Telegram queue,
delivery, acknowledgement, failure, and retry receipts. This module never
writes gateway queues, calls Telegram Bot API, changes receiver routes, sends
notifications, 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_telegram_receipt_approval_package_*.json"
_SCHEMA_VERSION = "ai_agent_telegram_receipt_approval_package_v1"
def load_latest_ai_agent_telegram_receipt_approval_package(
evaluations_dir: Path | None = None,
) -> dict[str, Any]:
"""Load the newest committed AI Agent Telegram receipt approval package."""
directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR
candidates = sorted(directory.glob(_SNAPSHOT_PATTERN))
if not candidates:
raise FileNotFoundError(
f"no AI Agent Telegram receipt 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_telegram_send":
raise ValueError(f"{label}: runtime_authority must stay approval_package_only_no_telegram_send")
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("telegram_truth") or {}
false_flags = {
"telegram_send_allowed",
"gateway_queue_write_allowed",
"direct_bot_api_allowed",
"receiver_route_change_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}: Telegram truth flags must remain false: {unsafe}")
zero_counts = {
"live_queued_receipt_count",
"live_delivered_receipt_count",
"live_acknowledged_receipt_count",
"live_failed_receipt_count",
"live_retry_count",
}
non_zero = sorted(key for key in zero_counts if truth.get(key) != 0)
if non_zero:
raise ValueError(f"{label}: live Telegram receipt counts must remain zero: {non_zero}")
def _require_package_safety(payload: dict[str, Any], label: str) -> None:
package = payload.get("receipt_package") or {}
required_fields = set(package.get("required_fields") or [])
required_minimum = {
"receipt_event_id",
"incident_id",
"target_channel_alias",
"message_template_id",
"redacted_message_summary",
"delivery_correlation_id",
"retry_policy_ref",
"owner_review_required",
}
missing = sorted(required_minimum - required_fields)
if missing:
raise ValueError(f"{label}: receipt 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("retry_policy_required") is not True:
raise ValueError(f"{label}: retry policy must be required")
if package.get("delivery_receipt_required") is not True:
raise ValueError(f"{label}: delivery receipt 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")
retry = payload.get("retry_contract") or {}
if retry.get("retry_required") is not True:
raise ValueError(f"{label}: retry_contract.retry_required must be true")
if not retry.get("retry_steps"):
raise ValueError(f"{label}: retry steps must not be empty")
def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None:
rollups = payload.get("rollups") or {}
gates = payload.get("receipt_gates") or []
lanes = payload.get("receipt_lanes") or []
package = payload.get("receipt_package") or {}
truth = payload.get("telegram_truth") or {}
expected_counts = {
"receipt_gate_count": len(gates),
"receipt_lane_count": len(lanes),
"blocked_runtime_action_count": len({gate.get("blocked_runtime_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_queued_receipt_count",
"live_delivered_receipt_count",
"live_acknowledged_receipt_count",
"live_failed_receipt_count",
"live_retry_count",
)
)
if rollups.get("live_receipt_count_total") != live_total:
raise ValueError(f"{label}: rollups.live_receipt_count_total mismatch")

View File

@@ -13,9 +13,9 @@ def test_load_latest_ai_agent_interaction_learning_proof_reads_committed_snapsho
data = load_latest_ai_agent_interaction_learning_proof()
assert data["schema_version"] == "ai_agent_interaction_learning_proof_v1"
assert data["program_status"]["overall_completion_percent"] == 72
assert data["program_status"]["current_task_id"] == "P2-403D"
assert data["program_status"]["next_task_id"] == "P2-403E"
assert data["program_status"]["overall_completion_percent"] == 80
assert data["program_status"]["current_task_id"] == "P2-403E"
assert data["program_status"]["next_task_id"] == "P2-403F"
assert data["program_status"]["read_only_mode"] is True
assert data["program_status"]["runtime_authority"] == "proof_surface_only_no_live_worker"
assert data["live_truth"]["runtime_loop_enabled"] is False
@@ -31,9 +31,7 @@ def test_load_latest_ai_agent_interaction_learning_proof_reads_committed_snapsho
assert data["rollups"]["operator_surface_count"] == len(data["operator_surfaces"]) == 5
assert data["rollups"]["runtime_gate_count"] == len(data["runtime_gates"]) == 5
assert data["rollups"]["live_signal_count"] == 0
assert set(data["rollups"]["live_pending_level_ids"]) == {
"telegram_receipts",
}
assert data["rollups"]["live_pending_level_ids"] == []
assert {lane["agent_id"] for lane in data["agent_lanes"]} == {
"openclaw",
"hermes",

View File

@@ -16,9 +16,9 @@ def test_ai_agent_interaction_learning_proof_endpoint_returns_committed_snapshot
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == "ai_agent_interaction_learning_proof_v1"
assert data["program_status"]["overall_completion_percent"] == 72
assert data["program_status"]["current_task_id"] == "P2-403D"
assert data["program_status"]["next_task_id"] == "P2-403E"
assert data["program_status"]["overall_completion_percent"] == 80
assert data["program_status"]["current_task_id"] == "P2-403E"
assert data["program_status"]["next_task_id"] == "P2-403F"
assert data["program_status"]["read_only_mode"] is True
assert data["live_truth"]["runtime_loop_enabled"] is False
assert data["live_truth"]["active_live_agent_sessions"] == 0

View File

@@ -14,8 +14,8 @@ def test_load_latest_ai_agent_proactive_operations_contract_reads_committed_snap
assert data["schema_version"] == "ai_agent_proactive_operations_contract_v1"
assert data["program_status"]["overall_completion_percent"] == 100
assert data["program_status"]["current_task_id"] == "P2-403D"
assert data["program_status"]["next_task_id"] == "P2-403E"
assert data["program_status"]["current_task_id"] == "P2-403E"
assert data["program_status"]["next_task_id"] == "P2-403F"
assert data["program_status"]["read_only_mode"] is True
assert data["program_status"]["runtime_authority"] == "contract_only_no_version_or_runtime_update"
assert data["approval_boundaries"]["runtime_version_update_allowed"] is False
@@ -25,7 +25,7 @@ def test_load_latest_ai_agent_proactive_operations_contract_reads_committed_snap
assert data["approval_boundaries"]["telegram_direct_send_allowed"] is False
assert data["rollups"]["version_domain_count"] == len(data["version_lifecycle_domains"]) == 12
assert data["rollups"]["delegable_capability_count"] == len(data["delegable_capabilities"]) == 24
assert data["rollups"]["rollout_task_count"] == len(data["rollout_tasks"]) == 11
assert data["rollups"]["rollout_task_count"] == len(data["rollout_tasks"]) == 12
assert data["rollups"]["auto_execute_allowed_count"] == 0
assert any(domain["domain_id"] == "ai_agents_models" for domain in data["version_lifecycle_domains"])
assert any(

View File

@@ -17,8 +17,8 @@ def test_ai_agent_proactive_operations_contract_endpoint_returns_committed_snaps
data = response.json()
assert data["schema_version"] == "ai_agent_proactive_operations_contract_v1"
assert data["program_status"]["overall_completion_percent"] == 100
assert data["program_status"]["current_task_id"] == "P2-403D"
assert data["program_status"]["next_task_id"] == "P2-403E"
assert data["program_status"]["current_task_id"] == "P2-403E"
assert data["program_status"]["next_task_id"] == "P2-403F"
assert data["program_status"]["read_only_mode"] is True
assert data["approval_boundaries"]["runtime_version_update_allowed"] is False
assert data["approval_boundaries"]["package_upgrade_allowed"] is False
@@ -26,7 +26,7 @@ def test_ai_agent_proactive_operations_contract_endpoint_returns_committed_snaps
assert data["approval_boundaries"]["telegram_direct_send_allowed"] is False
assert data["rollups"]["version_domain_count"] == 12
assert data["rollups"]["delegable_capability_count"] == 24
assert data["rollups"]["rollout_task_count"] == 11
assert data["rollups"]["rollout_task_count"] == 12
assert data["rollups"]["auto_execute_allowed_count"] == 0
assert any(domain["domain_id"] == "host_os_packages" for domain in data["version_lifecycle_domains"])
assert any(

View File

@@ -0,0 +1,52 @@
import copy
import pytest
from src.services.ai_agent_telegram_receipt_approval_package import (
load_latest_ai_agent_telegram_receipt_approval_package,
)
def test_load_latest_ai_agent_telegram_receipt_approval_package():
data = load_latest_ai_agent_telegram_receipt_approval_package()
assert data["schema_version"] == "ai_agent_telegram_receipt_approval_package_v1"
assert data["program_status"]["current_task_id"] == "P2-403E"
assert data["program_status"]["next_task_id"] == "P2-403F"
assert data["telegram_truth"]["telegram_send_allowed"] is False
assert data["telegram_truth"]["gateway_queue_write_allowed"] is False
assert data["telegram_truth"]["live_delivered_receipt_count"] == 0
assert data["rollups"]["receipt_gate_count"] == len(data["receipt_gates"])
def test_rejects_telegram_send_enabled(tmp_path):
data = load_latest_ai_agent_telegram_receipt_approval_package()
bad = copy.deepcopy(data)
bad["telegram_truth"]["telegram_send_allowed"] = True
path = tmp_path / "ai_agent_telegram_receipt_approval_package_2026-06-11.json"
path.write_text(__import__("json").dumps(bad), encoding="utf-8")
with pytest.raises(ValueError, match="Telegram truth flags"):
load_latest_ai_agent_telegram_receipt_approval_package(tmp_path)
def test_rejects_missing_delivery_receipt(tmp_path):
data = load_latest_ai_agent_telegram_receipt_approval_package()
bad = copy.deepcopy(data)
bad["receipt_package"]["delivery_receipt_required"] = False
path = tmp_path / "ai_agent_telegram_receipt_approval_package_2026-06-11.json"
path.write_text(__import__("json").dumps(bad), encoding="utf-8")
with pytest.raises(ValueError, match="delivery receipt"):
load_latest_ai_agent_telegram_receipt_approval_package(tmp_path)
def test_rejects_rollup_mismatch(tmp_path):
data = load_latest_ai_agent_telegram_receipt_approval_package()
bad = copy.deepcopy(data)
bad["rollups"]["receipt_gate_count"] = 999
path = tmp_path / "ai_agent_telegram_receipt_approval_package_2026-06-11.json"
path.write_text(__import__("json").dumps(bad), encoding="utf-8")
with pytest.raises(ValueError, match="rollup counts"):
load_latest_ai_agent_telegram_receipt_approval_package(tmp_path)

View File

@@ -0,0 +1,17 @@
from fastapi.testclient import TestClient
from src.main import app
def test_get_ai_agent_telegram_receipt_approval_package_api():
client = TestClient(app)
response = client.get("/api/v1/agents/agent-telegram-receipt-approval-package")
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == "ai_agent_telegram_receipt_approval_package_v1"
assert data["program_status"]["current_task_id"] == "P2-403E"
assert data["program_status"]["next_task_id"] == "P2-403F"
assert data["telegram_truth"]["telegram_send_allowed"] is False
assert data["telegram_truth"]["gateway_queue_write_allowed"] is False
assert data["rollups"]["live_receipt_count_total"] == 0