feat(governance): 新增 AI Agent 專業任務擴展
All checks were successful
Code Review / ai-code-review (push) Successful in 15s
CD Pipeline / tests (push) Successful in 1m38s
CD Pipeline / build-and-deploy (push) Successful in 6m8s
CD Pipeline / post-deploy-checks (push) Successful in 1m40s

This commit is contained in:
Your Name
2026-06-15 14:19:23 +08:00
parent a923e89017
commit e101931efb
15 changed files with 1453 additions and 3 deletions

View File

@@ -85,6 +85,9 @@ from src.services.ai_agent_live_read_model_gate import (
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_matched_playbook_learning_gap import (
load_latest_ai_agent_matched_playbook_learning_gap,
)
@@ -760,6 +763,36 @@ async def get_agent_12_agent_war_room() -> dict[str, Any]:
) from exc
@router.get(
"/agent-professional-task-expansion",
response_model=dict[str, Any],
summary="取得 AI Agent 專業任務擴展與 Telegram Runtime Bridge 快照",
description=(
"讀取最新已提交的 P2-405A AI Agent 專業任務擴展與 Telegram Runtime Bridge 只讀快照;"
"此端點只呈現 OpenClaw、Hermes、NemoTron 與專責 Agent 可承接的專業任務、MCP/RAG、"
"風險分層、Telegram no-send preview 與後續 canary gate"
"不寫 Gateway queue、不送 Telegram、不呼叫 Bot API、不讀 secret、不執行 production write、"
"不改主機、不執行 kubectl。"
),
)
async def get_agent_professional_task_expansion() -> dict[str, Any]:
"""回傳最新 AI Agent 專業任務擴展與 Telegram Runtime Bridge 只讀快照。"""
try:
payload = await asyncio.to_thread(load_latest_ai_agent_professional_task_expansion)
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_professional_task_expansion_invalid", error=str(exc))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="AI Agent 專業任務擴展與 Telegram Runtime Bridge 快照無效",
) from exc
@router.get(
"/agent-communication-learning-contract",
response_model=dict[str, Any],

View File

@@ -0,0 +1,241 @@
"""
AI Agent professional task expansion and Telegram runtime bridge snapshot.
Loads the latest committed P2-405A read-only contract. The contract expands
professional AI Agent work and defines Telegram bridge stages, but it does not
write Telegram Gateway queues, send Telegram messages, call the Bot API, read
secrets, or execute production changes.
"""
from __future__ import annotations
import copy
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_professional_task_expansion_*.json"
_SCHEMA_VERSION = "ai_agent_professional_task_expansion_v1"
_RUNTIME_AUTHORITY = "professional_task_expansion_and_telegram_bridge_read_only_no_send"
_EXPECTED_TASK_COUNT = 24
_EXPECTED_DOMAIN_COUNT = 8
_EXPECTED_STAGE_COUNT = 5
_EXPECTED_MESSAGE_TYPE_COUNT = 6
_ZERO_ROLLUP_FIELDS = {
"current_live_count",
"gateway_queue_write_count",
"telegram_send_count",
"bot_api_call_count",
"delivery_receipt_write_count",
"production_write_count",
"secret_read_count",
"paid_api_call_count",
"host_write_count",
"kubectl_action_count",
}
_FORBIDDEN_PUBLIC_TERMS = {
"work_window_transcript",
"raw prompt",
"private reasoning",
"chain-of-thought",
"telegram token",
"authorization header",
"secret value",
}
def load_latest_ai_agent_professional_task_expansion(
evaluations_dir: Path | None = None,
) -> dict[str, Any]:
"""Load the newest committed AI Agent professional task expansion snapshot."""
directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR
candidates = sorted(directory.glob(_SNAPSHOT_PATTERN))
if not candidates:
raise FileNotFoundError(
f"no AI Agent professional task expansion 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_telegram_bridge(payload, label)
_require_professional_tasks(payload, label)
_require_reporting_and_redaction(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 = {
"current_priority": "P2",
"current_task_id": "P2-405A",
"next_task_id": "P2-405B",
"read_only_mode": True,
"runtime_authority": _RUNTIME_AUTHORITY,
"overall_completion_percent": 82,
}
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_telegram_bridge(payload: dict[str, Any], label: str) -> None:
bridge = payload.get("telegram_runtime_bridge") or {}
expected = {
"canonical_room": "AwoooI SRE 戰情室",
"canonical_room_env": "SRE_GROUP_CHAT_ID",
"gateway_required": True,
"no_send_preview_ready": True,
"queue_preview_readback_ready": True,
"approved_canary_required": True,
"direct_bot_api_allowed": False,
"bot_api_call_enabled": False,
"gateway_queue_write_enabled": False,
"telegram_send_enabled": False,
"delivery_receipt_write_enabled": False,
}
mismatches = _mismatches(bridge, expected)
if mismatches:
raise ValueError(f"{label}: telegram_runtime_bridge mismatch: {mismatches}")
stages = bridge.get("stages") or []
if len(stages) != _EXPECTED_STAGE_COUNT:
raise ValueError(f"{label}: expected {_EXPECTED_STAGE_COUNT} Telegram stages")
if any(stage.get("live_send_enabled") is not False for stage in stages):
raise ValueError(f"{label}: Telegram stages must keep live_send_enabled false")
message_types = bridge.get("message_types") or []
if len(message_types) != _EXPECTED_MESSAGE_TYPE_COUNT:
raise ValueError(f"{label}: expected {_EXPECTED_MESSAGE_TYPE_COUNT} message types")
def _require_professional_tasks(payload: dict[str, Any], label: str) -> None:
domains = payload.get("professional_task_domains") or []
if len(domains) != _EXPECTED_DOMAIN_COUNT:
raise ValueError(f"{label}: expected {_EXPECTED_DOMAIN_COUNT} professional task domains")
domain_ids = {domain.get("domain_id") for domain in domains}
tasks = payload.get("professional_tasks") or []
if len(tasks) != _EXPECTED_TASK_COUNT:
raise ValueError(f"{label}: expected {_EXPECTED_TASK_COUNT} professional tasks")
task_ids = [task.get("task_id") for task in tasks]
if len(set(task_ids)) != len(task_ids):
raise ValueError(f"{label}: task_id values must be unique")
owners = {task.get("owner_agent") for task in tasks}
required_owners = {
"openclaw",
"hermes",
"nemotron",
"telegram_ops_liaison",
"security_sentinel",
"sre_sentinel",
"devops_commander",
}
if not required_owners.issubset(owners):
raise ValueError(f"{label}: professional tasks must include owners {sorted(required_owners)}")
for task in tasks:
task_id = task.get("task_id")
if task.get("domain_id") not in domain_ids:
raise ValueError(f"{label}: {task_id}.domain_id must reference a known domain")
if task.get("current_live_count_24h") != 0:
raise ValueError(f"{label}: {task_id}.current_live_count_24h must remain zero")
if not task.get("required_mcp"):
raise ValueError(f"{label}: {task_id}.required_mcp must not be empty")
if not task.get("required_rag"):
raise ValueError(f"{label}: {task_id}.required_rag must not be empty")
if not task.get("blocked_actions"):
raise ValueError(f"{label}: {task_id}.blocked_actions must not be empty")
risk = task.get("risk_tier")
if risk in {"high", "critical"} and task.get("approval_required") is not True:
raise ValueError(f"{label}: {task_id} high/critical tasks must require approval")
if risk == "critical" and task.get("automation_mode") not in {
"approval_required_before_execution",
"blocked_until_owner_response",
}:
raise ValueError(f"{label}: {task_id} critical tasks must stay approval/blocker gated")
def _require_reporting_and_redaction(payload: dict[str, Any], label: str) -> None:
reporting = payload.get("reporting_contract") or {}
for cadence in ("daily", "weekly", "monthly", "action_required"):
if (reporting.get(cadence) or {}).get("required") is not True:
raise ValueError(f"{label}: reporting_contract.{cadence}.required must be true")
redaction = payload.get("redaction_contract") or {}
expected = {
"redaction_required": True,
"conversation_transcript_display_allowed": False,
"raw_prompt_display_allowed": False,
"private_reasoning_display_allowed": False,
"secret_value_display_allowed": False,
"raw_runtime_payload_display_allowed": False,
"telegram_message_must_be_sanitized": True,
}
mismatches = _mismatches(redaction, expected)
if mismatches:
raise ValueError(f"{label}: redaction_contract mismatch: {mismatches}")
def _require_rollups(payload: dict[str, Any], label: str) -> None:
rollups = payload.get("rollups") or {}
tasks = payload.get("professional_tasks") or []
domains = payload.get("professional_task_domains") or []
bridge = payload.get("telegram_runtime_bridge") or {}
expected = {
"professional_task_count": len(tasks),
"domain_count": len(domains),
"telegram_stage_count": len(bridge.get("stages") or []),
"telegram_message_type_count": len(bridge.get("message_types") or []),
"approval_required_count": sum(1 for task in tasks if task.get("approval_required") is True),
"low_risk_task_count": sum(1 for task in tasks if task.get("risk_tier") == "low"),
"medium_risk_task_count": sum(1 for task in tasks if task.get("risk_tier") == "medium"),
"high_risk_task_count": sum(1 for task in tasks if task.get("risk_tier") == "high"),
"critical_risk_task_count": sum(1 for task in tasks if task.get("risk_tier") == "critical"),
}
mismatches = _mismatches(rollups, expected)
if mismatches:
raise ValueError(f"{label}: rollups mismatch: {mismatches}")
for field in _ZERO_ROLLUP_FIELDS:
if rollups.get(field) != 0:
raise ValueError(f"{label}: rollups.{field} must remain zero")
def _require_no_forbidden_public_terms(payload: dict[str, Any], label: str) -> None:
scrubbed = copy.deepcopy(payload)
redaction = scrubbed.get("redaction_contract")
if isinstance(redaction, dict):
redaction["forbidden_terms"] = []
public_text = json.dumps(scrubbed, ensure_ascii=False).lower()
leaked = sorted(term for term in _FORBIDDEN_PUBLIC_TERMS if term.lower() in public_text)
if leaked:
raise ValueError(f"{label}: forbidden public terms leaked: {leaked}")
def _mismatches(payload: dict[str, Any], expected: dict[str, Any]) -> dict[str, dict[str, Any]]:
return {
key: {"expected": expected_value, "actual": payload.get(key)}
for key, expected_value in expected.items()
if payload.get(key) != expected_value
}