From f237b25453e4124b379525aeb50398c374c87407 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 2 Jul 2026 18:31:19 +0800 Subject: [PATCH] feat(telegram): expose AI automation matrix --- apps/api/src/api/v1/agents.py | 33 ++ .../awoooi_priority_work_order_readback.py | 7 +- .../telegram_alert_ai_automation_matrix.py | 414 ++++++++++++++++++ ...telegram_alert_ai_automation_matrix_api.py | 102 +++++ docs/LOGBOOK.md | 16 + .../TELEGRAM-INCIDENT-NOTIFICATION-MODEL.md | 7 +- 6 files changed, 574 insertions(+), 5 deletions(-) create mode 100644 apps/api/src/services/telegram_alert_ai_automation_matrix.py create mode 100644 apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index d12ce610f..d95b7b833 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -464,6 +464,9 @@ from src.services.stockplatform_public_api_controlled_recovery_preflight import from src.services.stockplatform_public_api_runtime_readback import ( load_latest_stockplatform_public_api_runtime_readback, ) +from src.services.telegram_alert_ai_automation_matrix import ( + load_latest_telegram_alert_ai_automation_matrix, +) router = APIRouter(prefix="/agents", tags=["Agent Teams"]) logger = get_logger("awoooi.agents") @@ -4287,6 +4290,36 @@ async def get_agent_telegram_action_required_digest_policy() -> dict[str, Any]: ) from exc +@router.get( + "/telegram-alert-ai-automation-matrix", + response_model=dict[str, Any], + summary="取得 Telegram 告警 AI 自動化矩陣", + description=( + "整合 Telegram egress inventory、readability guard、migration candidates、" + "action-required digest policy 與 AwoooP outbound mirror source contract," + "顯示每個 Telegram 告警 surface 是否具備 DB/log receipt、AI route、" + "controlled queue、work item、post verifier 與 learning writeback。" + "此端點只回 metadata,不送 Telegram、不呼叫 Bot API、不寫 Gateway queue、" + "不讀 secret、不改 receiver、不觸發 workflow。" + ), +) +async def get_telegram_alert_ai_automation_matrix() -> dict[str, Any]: + """Return the Telegram alert AI automation matrix.""" + try: + return await asyncio.to_thread(load_latest_telegram_alert_ai_automation_matrix) + 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("telegram_alert_ai_automation_matrix_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Telegram 告警 AI 自動化矩陣無效", + ) from exc + + @router.get( "/agent-gitea-pr-draft-lane", response_model=dict[str, Any], diff --git a/apps/api/src/services/awoooi_priority_work_order_readback.py b/apps/api/src/services/awoooi_priority_work_order_readback.py index 978b69295..95fccc808 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -320,9 +320,12 @@ _COMMANDER_INSERTED_REQUIREMENT_WORK_ITEMS: list[dict[str, Any]] = [ "每個 Telegram 告警都要映射到 DB/log receipt、AI route、controlled queue、work item、" "post verifier 與 learning writeback;direct send 或 manual default 都列缺口。" ), - "current_state": "本輪已把 workflow direct send 收斂到 0 並暴露 API direct gap;仍需全面 alert matrix。", + "current_state": ( + "Telegram alert AI automation matrix endpoint 已實作;目前明確讀回 workflow direct send 0、" + "ops script direct gap 4、API direct gap 1,下一步收斂 direct Bot API 旁路。" + ), "acceptance": "Telegram alert matrix 顯示 total、DB receipt、AI route、controlled queue、manual/default gap 與 direct send gap。", - "next_action": "擴充 Telegram alert inventory/readback,讓人工預設只允許 critical/break-glass 或歷史證據。", + "next_action": "先把 apps/api/src/services/channel_hub.py direct sender 改走 TelegramGateway / AwoooP outbound receipt path。", "mapped_workplan_id": "P0-006", }, { diff --git a/apps/api/src/services/telegram_alert_ai_automation_matrix.py b/apps/api/src/services/telegram_alert_ai_automation_matrix.py new file mode 100644 index 000000000..0fd1fd7a2 --- /dev/null +++ b/apps/api/src/services/telegram_alert_ai_automation_matrix.py @@ -0,0 +1,414 @@ +"""Telegram alert AI automation matrix readback. + +Builds a repo-committed, metadata-only matrix that shows whether Telegram alert +surfaces are wired into AI automation receipts instead of ending as manual +messages. This module never sends Telegram, calls Bot API, reads secrets, or +writes runtime queues. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from src.services.snapshot_paths import ( + default_evaluations_dir, + default_security_dir, + resolve_repo_root, +) + +_SCHEMA_VERSION = "telegram_alert_ai_automation_matrix_v1" +_EGRESS_INVENTORY = "telegram-notification-egress-inventory.snapshot.json" +_OWNER_ACCEPTANCE = "telegram-notification-egress-owner-response-acceptance.snapshot.json" +_MIGRATION_PLAN = "telegram-notification-egress-migration-plan-draft.snapshot.json" +_READABILITY_GUARD = "telegram-alert-readability-guard.snapshot.json" +_DIGEST_POLICY_PATTERN = "ai_agent_telegram_action_required_digest_policy_*.json" + + +def load_latest_telegram_alert_ai_automation_matrix( + security_dir: Path | None = None, + evaluations_dir: Path | None = None, +) -> dict[str, Any]: + """Return the latest Telegram alert AI automation matrix.""" + anchor = Path(__file__) + security = security_dir or default_security_dir(anchor) + evaluations = evaluations_dir or default_evaluations_dir(anchor) + repo_root = resolve_repo_root(anchor) + + egress_inventory = _load_required_json(security / _EGRESS_INVENTORY) + owner_acceptance = _load_required_json(security / _OWNER_ACCEPTANCE) + migration_plan = _load_required_json(security / _MIGRATION_PLAN) + readability_guard = _load_required_json(security / _READABILITY_GUARD) + digest_policy = _load_latest_json(evaluations, _DIGEST_POLICY_PATTERN) + + _require_schema( + egress_inventory, + "telegram_notification_egress_inventory_v1", + _EGRESS_INVENTORY, + ) + _require_schema( + owner_acceptance, + "telegram_notification_egress_owner_response_acceptance_v1", + _OWNER_ACCEPTANCE, + ) + _require_schema( + migration_plan, + "telegram_notification_egress_migration_plan_draft_v1", + _MIGRATION_PLAN, + ) + _require_schema( + readability_guard, + "telegram_alert_readability_guard_v1", + _READABILITY_GUARD, + ) + _require_schema( + digest_policy, + "ai_agent_telegram_action_required_digest_policy_v1", + _DIGEST_POLICY_PATTERN, + ) + _require_safe_boundaries( + egress_inventory, + owner_acceptance, + migration_plan, + readability_guard, + digest_policy, + ) + + egress_summary = egress_inventory["summary"] + owner_summary = owner_acceptance["summary"] + migration_summary = migration_plan["summary"] + readability_summary = readability_guard["summary"] + digest_rollups = digest_policy["rollups"] + source_proof = _inspect_source_contract(repo_root) + + direct_gap_count = int(egress_summary["direct_bot_api_call_count"]) + direct_gap_file_count = int(egress_summary["direct_bot_api_file_count"]) + direct_gap_refs = [ + { + "surface_id": item["egress_surface_id"], + "surface_kind": item["surface_kind"], + "path": item["path"], + "line": item["line"], + "line_hash": item["line_hash"], + } + for item in egress_inventory.get("direct_bot_api_calls", []) + ] + + matrix = [ + { + "surface_id": "telegram_gateway_ai_alert_cards", + "priority_work_item_id": "CIR-P0-TG-001", + "surface_kind": "gateway_final_exit", + "status": "ai_controlled_receipt_path_ready", + "known_item_count": int(readability_summary["final_exit_contract_count"]), + "db_or_log_receipt": "ready_via_awooop_outbound_message_mirror", + "ai_route": "ready_ai_automation_alert_card_v1", + "controlled_queue": "ready_controlled_playbook_queue", + "post_verifier": "ready_telegram_alert_readability_guard", + "learning_writeback": "ready_delivery_receipt_readback_required", + "manual_default_gap_count": 0, + "direct_send_gap_count": 0, + "evidence_refs": [ + "docs/security/telegram-alert-readability-guard.snapshot.json", + "apps/api/src/services/telegram_gateway.py", + "/api/v1/platform/runs/ai-alert-cards", + ], + }, + { + "surface_id": "telegram_direct_bot_api_egress_gap", + "priority_work_item_id": "CIR-P0-TG-001", + "surface_kind": "direct_bot_api_gap", + "status": "gap_open_requires_controlled_migration", + "known_item_count": direct_gap_count, + "db_or_log_receipt": "missing_or_not_proven_for_direct_path", + "ai_route": "missing_or_not_proven_for_direct_path", + "controlled_queue": "missing_or_not_proven_for_direct_path", + "post_verifier": "metadata_inventory_ready", + "learning_writeback": "migration_candidate_ready_not_applied", + "manual_default_gap_count": direct_gap_count, + "direct_send_gap_count": direct_gap_count, + "evidence_refs": [ + "docs/security/telegram-notification-egress-inventory.snapshot.json", + "docs/security/telegram-notification-egress-migration-plan-draft.snapshot.json", + ], + }, + { + "surface_id": "telegram_action_required_digest_policy", + "priority_work_item_id": "CIR-P0-TG-001", + "surface_kind": "ai_digest_policy", + "status": "no_send_policy_ready", + "known_item_count": int(digest_rollups["rule_count"]), + "db_or_log_receipt": "draft_only_no_runtime_write", + "ai_route": "ready_action_required_and_failure_only_rules", + "controlled_queue": "ready_policy_gate_no_direct_send", + "post_verifier": "ready_policy_loader_validation", + "learning_writeback": "ready_next_task_ids", + "manual_default_gap_count": 0, + "direct_send_gap_count": 0, + "evidence_refs": [ + "docs/evaluations/ai_agent_telegram_action_required_digest_policy_2026-06-11.json", + "/api/v1/agents/agent-telegram-action-required-digest-policy", + ], + }, + { + "surface_id": "telegram_egress_controlled_migration_candidates", + "priority_work_item_id": "CIR-P0-TG-001", + "surface_kind": "migration_candidate", + "status": "controlled_apply_candidates_ready", + "known_item_count": int(migration_summary["migration_candidate_count"]), + "db_or_log_receipt": "required_before_candidate_close", + "ai_route": "migration_plan_ready", + "controlled_queue": "ready_for_controlled_apply_review", + "post_verifier": "owner_acceptance_ledger_ready", + "learning_writeback": "candidate_acceptance_metadata_ready", + "manual_default_gap_count": 0, + "direct_send_gap_count": direct_gap_count, + "evidence_refs": [ + "docs/security/telegram-notification-egress-owner-response-acceptance.snapshot.json", + "docs/security/telegram-notification-egress-migration-plan-draft.snapshot.json", + ], + }, + { + "surface_id": "telegram_ai_alert_card_delivery_readback_endpoint", + "priority_work_item_id": "CIR-P0-TG-001", + "surface_kind": "production_readback_endpoint", + "status": "readback_endpoint_present", + "known_item_count": source_proof["ai_alert_card_delivery_readback_endpoint_count"], + "db_or_log_receipt": "ready_awooop_outbound_message_query", + "ai_route": "ready_event_type_lane_target_filters", + "controlled_queue": "read_only_query_no_send", + "post_verifier": "ready_source_contract_present", + "learning_writeback": "ready_delivery_receipt_summary", + "manual_default_gap_count": 0, + "direct_send_gap_count": 0, + "evidence_refs": [ + "apps/api/src/api/v1/platform/operator_runs.py", + "apps/api/src/db/awooop_models.py", + ], + }, + ] + + summary = _build_summary( + matrix=matrix, + direct_gap_count=direct_gap_count, + direct_gap_file_count=direct_gap_file_count, + egress_summary=egress_summary, + owner_summary=owner_summary, + migration_summary=migration_summary, + readability_summary=readability_summary, + digest_rollups=digest_rollups, + source_proof=source_proof, + ) + + return { + "schema_version": _SCHEMA_VERSION, + "status": ( + "telegram_alert_ai_automation_matrix_ready_with_direct_send_gaps" + if direct_gap_count + else "telegram_alert_ai_automation_matrix_ready" + ), + "mode": "metadata_only_no_secret_no_telegram_send_no_runtime_write", + "priority_work_item_id": "CIR-P0-TG-001", + "source_refs": { + "egress_inventory": f"docs/security/{_EGRESS_INVENTORY}", + "owner_acceptance": f"docs/security/{_OWNER_ACCEPTANCE}", + "migration_plan": f"docs/security/{_MIGRATION_PLAN}", + "readability_guard": f"docs/security/{_READABILITY_GUARD}", + "digest_policy": "docs/evaluations/ai_agent_telegram_action_required_digest_policy_2026-06-11.json", + }, + "operation_boundaries": { + "telegram_send_performed": False, + "bot_api_call_performed": False, + "gateway_queue_write_performed": False, + "runtime_write_performed": False, + "secret_value_read": False, + "raw_payload_stored": False, + "receiver_route_changed": False, + "workflow_dispatch_performed": False, + "production_write_performed": False, + "metadata_only": True, + }, + "summary": summary, + "matrix": matrix, + "direct_send_gap_refs": direct_gap_refs, + "next_controlled_actions": [ + { + "action_id": "migrate_api_direct_sender_to_gateway", + "scope": "apps/api/src/services/channel_hub.py", + "priority": "P0", + "why": "API direct Bot API path is the remaining high-blast-radius bypass.", + "requires_secret": False, + "requires_runtime_send": False, + "post_verifier": "telegram-notification-egress-inventory direct gap count decreases and TelegramGateway mirror tests pass.", + }, + { + "action_id": "migrate_ops_script_direct_senders_to_receipt_path", + "scope": "scripts/ops direct Bot API sendMessage surfaces", + "priority": "P0", + "why": "Ops script direct sends can bypass DB/log receipt and AI automation card metadata.", + "requires_secret": False, + "requires_runtime_send": False, + "post_verifier": "egress inventory and owner acceptance matrix show no unreviewed direct sends.", + }, + { + "action_id": "bind_ai_alert_cards_to_learning_writeback", + "scope": "ai_automation_alert_card_v1 outbound mirror metadata", + "priority": "P0", + "why": "Telegram must become an AI automation learning input, not a manual terminal state.", + "requires_secret": False, + "requires_runtime_send": False, + "post_verifier": "/api/v1/platform/runs/ai-alert-cards readback shows delivery_receipt_readback_required.", + }, + ], + "operator_interpretation": [ + "TelegramGateway AI alert cards already expose AI route, controlled queue, final-exit formatter, outbound mirror metadata, and readback endpoint support.", + "Known direct Bot API surfaces remain the concrete AI automation gap; they must not be treated as completed delivery receipts.", + "Manual/default semantics are only acceptable for critical/break-glass or historical evidence; direct send gaps remain controlled migration candidates.", + ], + } + + +def _load_required_json(path: Path) -> dict[str, Any]: + if not path.exists(): + raise FileNotFoundError(f"required Telegram matrix source missing: {path}") + with path.open(encoding="utf-8") as handle: + payload = json.load(handle) + if not isinstance(payload, dict): + raise ValueError(f"{path}: expected JSON object") + return payload + + +def _load_latest_json(directory: Path, pattern: str) -> dict[str, Any]: + candidates = sorted(directory.glob(pattern)) + if not candidates: + raise FileNotFoundError(f"no matching snapshots found in {directory}: {pattern}") + return _load_required_json(candidates[-1]) + + +def _require_schema(payload: dict[str, Any], expected: str, label: str) -> None: + actual = payload.get("schema_version") + if actual != expected: + raise ValueError(f"{label}: expected schema_version={expected}, got {actual!r}") + + +def _require_safe_boundaries(*payloads: dict[str, Any]) -> None: + forbidden_true_keys = { + "telegram_send_authorized", + "bot_api_call_authorized", + "telegram_direct_send_allowed", + "telegram_gateway_queue_write_allowed", + "runtime_execution_authorized", + "runtime_execution_allowed", + "runtime_gate_open", + "workflow_dispatch_authorized", + "production_deploy_authorized", + "production_write_authorized", + "secret_value_collection_allowed", + "secret_plaintext_allowed", + "raw_payload_storage_allowed", + "conversation_transcript_allowed", + } + for payload in payloads: + for section_key in ( + "operation_boundaries", + "execution_boundaries", + "approval_boundaries", + ): + section = payload.get(section_key) or {} + if not isinstance(section, dict): + continue + unsafe = sorted( + key + for key in forbidden_true_keys + if section.get(key) not in (None, False) + ) + if unsafe: + raise ValueError( + f"{payload.get('schema_version')}.{section_key}: unsafe true flags {unsafe}" + ) + + +def _inspect_source_contract(repo_root: Path) -> dict[str, int | bool]: + source_checks = { + "telegram_gateway_mirror_present": ( + repo_root / "apps/api/src/services/telegram_gateway.py", + "_mirror_outbound_message", + ), + "telegram_gateway_record_outbound_present": ( + repo_root / "apps/api/src/services/telegram_gateway.py", + "record_outbound_message", + ), + "ai_alert_card_metadata_present": ( + repo_root / "apps/api/src/services/telegram_gateway.py", + "ai_automation_alert_card_mirror_v1", + ), + "outbound_message_model_present": ( + repo_root / "apps/api/src/db/awooop_models.py", + "__tablename__ = \"awooop_outbound_message\"", + ), + "ai_alert_card_delivery_readback_endpoint_present": ( + repo_root / "apps/api/src/api/v1/platform/operator_runs.py", + "/runs/ai-alert-cards", + ), + } + result: dict[str, int | bool] = {} + for key, (path, marker) in source_checks.items(): + text = path.read_text(encoding="utf-8", errors="replace") if path.exists() else "" + present = marker in text + result[key] = present + if key == "ai_alert_card_delivery_readback_endpoint_present": + result["ai_alert_card_delivery_readback_endpoint_count"] = 1 if present else 0 + + required = [key for key, value in result.items() if key.endswith("_present") and value is not True] + if required: + raise ValueError(f"Telegram matrix source contract missing markers: {required}") + return result + + +def _build_summary( + *, + matrix: list[dict[str, Any]], + direct_gap_count: int, + direct_gap_file_count: int, + egress_summary: dict[str, Any], + owner_summary: dict[str, Any], + migration_summary: dict[str, Any], + readability_summary: dict[str, Any], + digest_rollups: dict[str, Any], + source_proof: dict[str, int | bool], +) -> dict[str, Any]: + ready = lambda key, prefix: sum(1 for item in matrix if str(item[key]).startswith(prefix)) + return { + "telegram_alert_surface_count": len(matrix), + "known_direct_send_gap_count": direct_gap_count, + "known_direct_send_gap_file_count": direct_gap_file_count, + "workflow_direct_send_gap_count": int(egress_summary["workflow_direct_bot_api_call_count"]), + "ops_script_direct_send_gap_count": int(egress_summary["ops_script_direct_bot_api_call_count"]), + "ci_script_direct_send_gap_count": int(egress_summary["ci_script_direct_bot_api_call_count"]), + "api_direct_send_gap_count": int(egress_summary["api_direct_bot_api_call_count"]), + "gateway_normalized_callsite_count": int(egress_summary["gateway_normalized_callsite_count"]), + "db_or_log_receipt_ready_surface_count": ready("db_or_log_receipt", "ready"), + "ai_route_ready_surface_count": ready("ai_route", "ready"), + "controlled_queue_ready_surface_count": ready("controlled_queue", "ready"), + "post_verifier_ready_surface_count": ready("post_verifier", "ready"), + "learning_writeback_ready_surface_count": ready("learning_writeback", "ready"), + "manual_default_gap_count": sum(int(item["manual_default_gap_count"]) for item in matrix), + "direct_gap_migration_candidate_count": int(migration_summary["migration_candidate_count"]), + "owner_acceptance_candidate_count": int(owner_summary["acceptance_candidate_count"]), + "owner_response_required_as_evidence_count": int(migration_summary["owner_response_required_count"]), + "readability_final_exit_contract_count": int(readability_summary["final_exit_contract_count"]), + "digest_rule_count": int(digest_rollups["rule_count"]), + "digest_action_required_rule_count": len(digest_rollups["action_required_rule_ids"]), + "digest_failure_escalation_rule_count": len(digest_rollups["failure_escalation_rule_ids"]), + "digest_suppressed_success_rule_count": len(digest_rollups["suppressed_success_rule_ids"]), + "telegram_send_performed_count": 0, + "bot_api_call_performed_count": 0, + "secret_value_read_count": 0, + "runtime_write_performed_count": 0, + "ai_alert_card_delivery_readback_endpoint_count": source_proof[ + "ai_alert_card_delivery_readback_endpoint_count" + ], + "next_priority_action_id": "migrate_api_direct_sender_to_gateway", + "next_priority_work_item_id": "CIR-P0-TG-001", + } diff --git a/apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py b/apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py new file mode 100644 index 000000000..78dc2cf1c --- /dev/null +++ b/apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import json + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.api.v1.agents import router +from src.services.telegram_alert_ai_automation_matrix import ( + load_latest_telegram_alert_ai_automation_matrix, +) + + +def test_telegram_alert_ai_automation_matrix_loader_returns_gap_matrix(): + payload = load_latest_telegram_alert_ai_automation_matrix() + + assert payload["schema_version"] == "telegram_alert_ai_automation_matrix_v1" + assert payload["priority_work_item_id"] == "CIR-P0-TG-001" + assert payload["mode"] == "metadata_only_no_secret_no_telegram_send_no_runtime_write" + assert payload["operation_boundaries"] == { + "telegram_send_performed": False, + "bot_api_call_performed": False, + "gateway_queue_write_performed": False, + "runtime_write_performed": False, + "secret_value_read": False, + "raw_payload_stored": False, + "receiver_route_changed": False, + "workflow_dispatch_performed": False, + "production_write_performed": False, + "metadata_only": True, + } + + summary = payload["summary"] + assert summary["telegram_alert_surface_count"] == 5 + assert summary["known_direct_send_gap_count"] == 5 + assert summary["known_direct_send_gap_file_count"] == 5 + assert summary["workflow_direct_send_gap_count"] == 0 + assert summary["ops_script_direct_send_gap_count"] == 4 + assert summary["api_direct_send_gap_count"] == 1 + assert summary["gateway_normalized_callsite_count"] >= 50 + assert summary["db_or_log_receipt_ready_surface_count"] >= 2 + assert summary["ai_route_ready_surface_count"] >= 3 + assert summary["controlled_queue_ready_surface_count"] >= 2 + assert summary["post_verifier_ready_surface_count"] >= 3 + assert summary["learning_writeback_ready_surface_count"] >= 3 + assert summary["manual_default_gap_count"] == 5 + assert summary["direct_gap_migration_candidate_count"] == 5 + assert summary["owner_acceptance_candidate_count"] == 5 + assert summary["telegram_send_performed_count"] == 0 + assert summary["bot_api_call_performed_count"] == 0 + assert summary["secret_value_read_count"] == 0 + assert summary["runtime_write_performed_count"] == 0 + assert summary["ai_alert_card_delivery_readback_endpoint_count"] == 1 + assert summary["next_priority_action_id"] == "migrate_api_direct_sender_to_gateway" + + matrix = {item["surface_id"]: item for item in payload["matrix"]} + assert matrix["telegram_gateway_ai_alert_cards"]["status"] == ( + "ai_controlled_receipt_path_ready" + ) + assert matrix["telegram_gateway_ai_alert_cards"]["db_or_log_receipt"] == ( + "ready_via_awooop_outbound_message_mirror" + ) + assert matrix["telegram_direct_bot_api_egress_gap"]["status"] == ( + "gap_open_requires_controlled_migration" + ) + assert matrix["telegram_direct_bot_api_egress_gap"]["direct_send_gap_count"] == 5 + assert matrix["telegram_action_required_digest_policy"]["known_item_count"] == 8 + assert matrix["telegram_ai_alert_card_delivery_readback_endpoint"][ + "db_or_log_receipt" + ] == "ready_awooop_outbound_message_query" + + assert len(payload["direct_send_gap_refs"]) == 5 + assert any( + ref["path"] == "apps/api/src/services/channel_hub.py" + for ref in payload["direct_send_gap_refs"] + ) + + serialized = json.dumps(payload, ensure_ascii=False) + for marker in [ + "TELEGRAM_BOT_TOKEN", + "authorization_header", + "raw Telegram payload", + "批准!繼續", + "My request for Codex", + ]: + assert marker not in serialized + + +def test_telegram_alert_ai_automation_matrix_endpoint_returns_readback(): + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.get("/api/v1/agents/telegram-alert-ai-automation-matrix") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "telegram_alert_ai_automation_matrix_v1" + assert data["summary"]["known_direct_send_gap_count"] == 5 + assert data["summary"]["next_priority_work_item_id"] == "CIR-P0-TG-001" + assert data["operation_boundaries"]["telegram_send_performed"] is False + assert data["operation_boundaries"]["secret_value_read"] is False diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index d62929ef3..e8850cc84 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -12,6 +12,22 @@ **仍維持**: - 未使用 GitHub / `gh` / GitHub API;未讀 secret / token / `.env` / raw sessions / SQLite / auth;未觸發 workflow;未重啟主機 / VM / Docker / Nginx / K3s / DB / firewall;未寫 production DB。 +## 2026-07-02 — 15:55 CIR-P0-TG-001 Telegram alert AI automation matrix + +**完成內容**: +- 新增 `/api/v1/agents/telegram-alert-ai-automation-matrix`,把 Telegram egress inventory、readability guard、migration candidates、AI action-required digest policy 與 AwoooP outbound mirror source contract 合併成正式 readback。 +- Matrix 現在可機器讀回 DB/log receipt、AI route、controlled queue、work item、post verifier、learning writeback、manual/default gap 與 direct send gap;已明確讀回 known direct Bot API gap `5`:workflow `0`、ops script `4`、API direct path `1`。 +- `CIR-P0-TG-001` 工作項 current_state 已更新為 endpoint 實作完成,下一個 controlled action 固定為先收斂 `apps/api/src/services/channel_hub.py` API direct sender 到 TelegramGateway / AwoooP outbound receipt path。 +- `docs/awooop/TELEGRAM-INCIDENT-NOTIFICATION-MODEL.md` 同步舊 `18/11` 旁路數字為目前 source truth `5/5`,避免文件和 API matrix 打架。 + +**本地驗證結果**: +- `DATABASE_URL=postgresql+asyncpg://test:test@localhost/test PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_telegram_alert_ai_automation_matrix_api.py -q -p no:cacheprovider`:`2 passed`。 +- `DATABASE_URL=postgresql+asyncpg://test:test@localhost/test PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_awoooi_priority_work_order_readback_api.py -q -p no:cacheprovider`:`14 passed`。 + +**仍維持**: +- 未使用 GitHub / `gh` / GitHub API;未讀 secret / token / `.env` / raw sessions / SQLite / auth。 +- 未送 Telegram、未呼叫 Bot API、未寫 Gateway queue、未改 receiver route、未 workflow_dispatch、未寫 production DB、未重啟主機 / VM / Docker / Nginx / K3s / DB / firewall。 + ## 2026-07-02 — 15:42 P0-006 Windows99 verify collector 留在 controlled-runtime CD lane **完成內容**: diff --git a/docs/awooop/TELEGRAM-INCIDENT-NOTIFICATION-MODEL.md b/docs/awooop/TELEGRAM-INCIDENT-NOTIFICATION-MODEL.md index e3affb64f..8b03d94ea 100644 --- a/docs/awooop/TELEGRAM-INCIDENT-NOTIFICATION-MODEL.md +++ b/docs/awooop/TELEGRAM-INCIDENT-NOTIFICATION-MODEL.md @@ -87,9 +87,10 @@ Host / runner 資源告警的第一版落地: 通知出口旁路清冊: -- `docs/security/telegram-notification-egress-inventory.snapshot.json` 目前固定 repo 內 `18` 個 direct Bot API `sendMessage`,分布為 Gitea workflow `13`、ops script `4`、API direct path `1`。 -- `docs/security/telegram-notification-egress-owner-request-draft.snapshot.json` 已將上述 direct egress 檔案聚成 `11` 份人工送件前草稿,request sent 仍為 `0`。 -- `docs/security/telegram-notification-egress-migration-plan-draft.snapshot.json` 已將 11 份草稿排成 workflow、ops script、API sender 三個遷移波次,migration authorized 仍為 `0`。 +- `docs/security/telegram-notification-egress-inventory.snapshot.json` 目前固定 repo 內 `5` 個 direct Bot API `sendMessage`,分布為 Gitea workflow `0`、ops script `4`、API direct path `1`。 +- `docs/security/telegram-notification-egress-owner-request-draft.snapshot.json` 已將上述 direct egress 檔案聚成 `5` 份 metadata-only candidate;owner response 欄位只能作為 evidence,不得阻擋低爆炸半徑 controlled apply。 +- `docs/security/telegram-notification-egress-migration-plan-draft.snapshot.json` 已將 5 份 candidate 排成 ops script、API sender 兩個遷移波次;migration runtime send / Bot API call / secret read 仍維持 `0`。 +- `GET /api/v1/agents/telegram-alert-ai-automation-matrix` 會把上述清冊與 readability guard、AI digest policy、AwoooP outbound mirror source contract 合併,讀回 DB/log receipt、AI route、controlled queue、work item、post verifier、learning writeback、manual/default gap 與 direct send gap。 - 這些 direct egress 可能繞過 TelegramGateway formatter、dedup、outbound mirror、KM / PlayBook / Verifier 與 no-false-green gate;因此只能視為已知待治理 surface,不能視為已完成收斂。 - 清冊階段只允許保存 path、line、redacted excerpt、owner 欄位與 reviewer gate;不送 Telegram、不讀 Bot token、不保存 chat secret、不改 workflow / script、不開 runtime gate。 - 後續每個 direct egress 要收斂前,都必須補 owner route、message shape、redaction contract、formatter convergence plan、delivery receipt、fallback mode、rollback owner 與 post-check evidence。