Merge remote-tracking branch 'origin/main' into codex/mcp-federation-runtime-20260715

This commit is contained in:
ogt
2026-07-15 08:37:37 +08:00
14 changed files with 565 additions and 32 deletions

View File

@@ -454,6 +454,9 @@ from src.services.host_runaway_aiops_loop_readiness import (
from src.services.javascript_package_inventory import (
load_latest_javascript_package_inventory,
)
from src.services.notification_matrix import (
build_canonical_telegram_routing_runtime_readback,
)
from src.services.observability_contract_matrix import (
load_latest_observability_contract_matrix,
)
@@ -4977,6 +4980,38 @@ async def get_telegram_alert_ai_automation_matrix() -> dict[str, Any]:
) from exc
@router.get(
"/telegram-canonical-routing-runtime-readback",
response_model=dict[str, Any],
summary="取得 Telegram 產品告警 canonical routing runtime readback",
description=(
"以 production gateway 使用的 fail-closed resolver 逐條解析 11 個產品與 20 條 "
"symbolic route分開顯示歷史 source snapshot 與目前 runtime enforcement。"
"此端點不讀 Telegram ID/token、不呼叫 Bot API、不送訊息、不改 runtime route。"
),
)
async def get_telegram_canonical_routing_runtime_readback() -> dict[str, Any]:
"""Return the no-secret, no-send canonical Telegram runtime projection."""
try:
return await asyncio.to_thread(
build_canonical_telegram_routing_runtime_readback
)
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_canonical_routing_runtime_readback_invalid",
error=str(exc),
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Telegram canonical routing runtime readback 無效",
) from exc
@router.get(
"/agent-gitea-pr-draft-lane",
response_model=dict[str, Any],

View File

@@ -13,6 +13,7 @@ import json
import re
from collections.abc import Mapping
from dataclasses import dataclass
from datetime import UTC, datetime
from enum import Enum
from pathlib import Path
from typing import Any
@@ -337,6 +338,92 @@ def load_canonical_telegram_routing_registry(
return payload
def build_canonical_telegram_routing_runtime_readback(
security_dir: Path | None = None,
) -> dict[str, Any]:
"""Project the committed registry through the live fail-closed resolver.
The source snapshot deliberately records the no-send boundaries of the run
that created it. Once the gateway guard is deployed, that historical
``source_status`` must not be presented as the current runtime state. This
readback resolves every route with the same code path used before Telegram
provider calls and returns symbolic aliases only.
"""
registry = load_canonical_telegram_routing_registry(security_dir)
resolved_routes: list[dict[str, Any]] = []
for route in registry["routes"]:
decision = resolve_canonical_telegram_route(
route["product_id"],
route["signal_family"],
route["severity"][0],
requested_destination=route["allowed_destination"],
registry=registry,
)
resolved_routes.append(
{
**route,
"decision": decision["decision"],
"decision_reason": decision["reason"],
}
)
allowed_route_count = sum(
route["decision"] == "allow" for route in resolved_routes
)
blocked_route_count = len(resolved_routes) - allowed_route_count
unknown_probe = resolve_canonical_telegram_route(
"unknown-product",
"unknown-signal",
"P1",
registry=registry,
)
return {
"schema_version": "telegram_canonical_routing_runtime_readback_v1",
"status": "runtime_policy_enforced_product_delivery_receipts_partial",
"source_status": registry["status"],
"source_mode": registry["mode"],
"source_generated_at": registry["generated_at"],
"runtime_generated_at": datetime.now(UTC).isoformat(),
"runtime_enforcement": {
"canonical_resolver_active": True,
"gateway_pre_send_gate_active": True,
"trusted_ingress_context_required": True,
"unknown_route_decision": unknown_probe["decision"],
"unknown_route_reason": unknown_probe["reason"],
"raw_numeric_destination_count": registry["rollups"][
"raw_numeric_destination_count"
],
"destination_identity_exposed": False,
"all_product_delivery_receipts_ready": blocked_route_count == 0,
},
"rollups": {
**registry["rollups"],
"runtime_allowed_route_count": allowed_route_count,
"runtime_blocked_route_count": blocked_route_count,
},
"policy": registry["policy"],
"destination_roles": registry["destination_roles"],
"products": registry["products"],
"routes": resolved_routes,
"operation_boundaries": {
"read_only": True,
"telegram_send_performed": False,
"bot_api_call_performed": False,
"runtime_route_changed": False,
"secret_value_read": False,
"raw_identifier_included": False,
},
"source_refs": {
"registry": "docs/security/telegram-canonical-routing-registry.snapshot.json",
"resolver": "apps/api/src/services/notification_matrix.py",
"gateway_gate": "apps/api/src/services/telegram_gateway.py",
"verifier": "apps/api/tests/test_telegram_canonical_routing_registry.py",
},
}
def resolve_canonical_telegram_route(
product_id: str,
signal_family: str,

View File

@@ -0,0 +1,58 @@
from __future__ import annotations
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.v1 import agents
from src.api.v1.agents import router
def _client() -> TestClient:
app = FastAPI()
app.include_router(router, prefix="/api/v1")
return TestClient(app)
def test_canonical_routing_runtime_readback_endpoint(monkeypatch) -> None:
payload = {
"schema_version": "telegram_canonical_routing_runtime_readback_v1",
"status": "runtime_policy_enforced_product_delivery_receipts_partial",
"operation_boundaries": {
"telegram_send_performed": False,
"secret_value_read": False,
},
}
monkeypatch.setattr(
agents,
"build_canonical_telegram_routing_runtime_readback",
lambda: payload,
)
response = _client().get(
"/api/v1/agents/telegram-canonical-routing-runtime-readback"
)
assert response.status_code == 200
assert response.json() == payload
def test_canonical_routing_runtime_readback_rejects_invalid_source(
monkeypatch,
) -> None:
def invalid_source() -> dict:
raise ValueError("invalid registry")
monkeypatch.setattr(
agents,
"build_canonical_telegram_routing_runtime_readback",
invalid_source,
)
response = _client().get(
"/api/v1/agents/telegram-canonical-routing-runtime-readback"
)
assert response.status_code == 500
assert response.json() == {
"detail": "Telegram canonical routing runtime readback 無效"
}

View File

@@ -9,6 +9,7 @@ import pytest
import yaml
from src.services.notification_matrix import (
build_canonical_telegram_routing_runtime_readback,
get_trusted_alert_canonical_route_context,
load_canonical_telegram_routing_registry,
resolve_canonical_telegram_route,
@@ -30,6 +31,42 @@ EXPECTED_PRODUCTS = {
REPO_ROOT = Path(__file__).resolve().parents[3]
def test_runtime_readback_separates_historical_source_state_from_live_gate() -> None:
readback = build_canonical_telegram_routing_runtime_readback()
assert readback["status"] == (
"runtime_policy_enforced_product_delivery_receipts_partial"
)
assert readback["source_status"] == "source_policy_ready_runtime_not_applied"
assert readback["source_generated_at"] == "2026-07-14T15:50:04+08:00"
assert readback["runtime_generated_at"].endswith("+00:00")
assert readback["runtime_enforcement"] == {
"canonical_resolver_active": True,
"gateway_pre_send_gate_active": True,
"trusted_ingress_context_required": True,
"unknown_route_decision": "block",
"unknown_route_reason": "unknown_product_signal_or_severity",
"raw_numeric_destination_count": 0,
"destination_identity_exposed": False,
"all_product_delivery_receipts_ready": False,
}
assert readback["rollups"]["runtime_allowed_route_count"] == 4
assert readback["rollups"]["runtime_blocked_route_count"] == 16
assert len(readback["products"]) == 11
assert len(readback["routes"]) == 20
assert readback["operation_boundaries"] == {
"read_only": True,
"telegram_send_performed": False,
"bot_api_call_performed": False,
"runtime_route_changed": False,
"secret_value_read": False,
"raw_identifier_included": False,
}
serialized = json.dumps(readback, ensure_ascii=False)
assert '"chat_id"' not in serialized
assert '"bot_token"' not in serialized
def test_namespaced_product_ingress_route_is_explicit_and_policy_blocked() -> None:
context = get_trusted_alert_canonical_route_context(
{