feat(governance): 新增 AI Agent 專業任務擴展
This commit is contained in:
@@ -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],
|
||||
|
||||
241
apps/api/src/services/ai_agent_professional_task_expansion.py
Normal file
241
apps/api/src/services/ai_agent_professional_task_expansion.py
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user