feat(awooop): expose mcp bridge truth chain
All checks were successful
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m17s
CD Pipeline / build-and-deploy (push) Successful in 3m55s
CD Pipeline / post-deploy-checks (push) Successful in 1m45s

This commit is contained in:
Your Name
2026-05-13 03:21:31 +08:00
parent b81cb28615
commit b4d367eeb4
5 changed files with 136 additions and 2 deletions

View File

@@ -7,6 +7,7 @@ Telegram cards can be audited without guessing which subsystem owns the truth.
from __future__ import annotations from __future__ import annotations
import json
from datetime import date, datetime from datetime import date, datetime
from decimal import Decimal from decimal import Decimal
from typing import Any from typing import Any
@@ -20,6 +21,7 @@ from src.db.base import get_db_context
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
_MAX_ROWS = 100 _MAX_ROWS = 100
_JSON_TEXT_FIELDS = {"gate_result", "source_envelope"}
def _clean(value: Any) -> Any: def _clean(value: Any) -> Any:
@@ -38,7 +40,15 @@ def _clean(value: Any) -> Any:
def _clean_row(row: Any) -> dict[str, Any]: def _clean_row(row: Any) -> dict[str, Any]:
return {key: _clean(value) for key, value in dict(row).items()} cleaned: dict[str, Any] = {}
for key, value in dict(row).items():
if key in _JSON_TEXT_FIELDS and isinstance(value, str):
try:
value = json.loads(value)
except json.JSONDecodeError:
pass
cleaned[key] = _clean(value)
return cleaned
async def _fetch_all(db: Any, sql: str, params: dict[str, Any]) -> list[dict[str, Any]]: async def _fetch_all(db: Any, sql: str, params: dict[str, Any]) -> list[dict[str, Any]]:
@@ -507,6 +517,7 @@ async def fetch_truth_chain(source_id: str, project_id: str = "awoooi") -> dict[
trace_id, trace_id,
agent_id, agent_id,
tool_name, tool_name,
gate_result,
result_status, result_status,
block_gate, block_gate,
block_reason, block_reason,

View File

@@ -1,6 +1,21 @@
from __future__ import annotations from __future__ import annotations
from src.services.awooop_truth_chain_service import _truth_status from src.services.awooop_truth_chain_service import _clean_row, _truth_status
def test_clean_row_parses_json_text_fields_for_gateway_visibility() -> None:
row = {
"gate_result": '{"schema_version":"legacy_mcp_bridge_v1","policy_enforced":false}',
"source_envelope": '{"adapter":"legacy_telegram_gateway"}',
"plain_text": '{"not":"parsed"}',
}
cleaned = _clean_row(row)
assert cleaned["gate_result"]["schema_version"] == "legacy_mcp_bridge_v1"
assert cleaned["gate_result"]["policy_enforced"] is False
assert cleaned["source_envelope"]["adapter"] == "legacy_telegram_gateway"
assert cleaned["plain_text"] == '{"not":"parsed"}'
def test_truth_status_marks_no_action_approval_as_manual_required() -> None: def test_truth_status_marks_no_action_approval_as_manual_required() -> None:

View File

@@ -6505,3 +6505,40 @@ gateway_audit_total=0 last_15m=0 bridge_total=0
- 因此目前只能宣稱「T2 bridge 寫入能力已部署並經 rollback smoke 驗證」。 - 因此目前只能宣稱「T2 bridge 寫入能力已部署並經 rollback smoke 驗證」。
- 尚不能宣稱「所有 MCP / 自建 MCP 都已完全經 AwoooP Gateway 強制治理」;下一段要讓下一個真實 incident / MCP 呼叫自然產生 durable bridge row或把高頻 caller 改成 first-class `McpGateway` - 尚不能宣稱「所有 MCP / 自建 MCP 都已完全經 AwoooP Gateway 強制治理」;下一段要讓下一個真實 incident / MCP 呼叫自然產生 durable bridge row或把高頻 caller 改成 first-class `McpGateway`
**T2 backfill / truth-chain visibility 追加**
- 新增 `scripts/ops/awooop-mcp-gateway-bridge-backfill-24h.sql`
- 將最近 24h 真實 `mcp_audit_log` 鏡像到 `awooop_mcp_gateway_audit`
-`gate_result.legacy_audit_id` 做 idempotency key。
- bridge row 保留 `policy_enforced=false``not_used_reason`,避免誤判為五閘門已 enforcement。
- production 已執行 backfill
```text
inserted_bridge_rows=1160
gateway_total=1310 bridge_total=1310 last_24h=1276
B6C589_gateway_rows=8 failed=8 success=0
```
- truth-chain API 追加 `gate_result` 欄位,並把 JSONB text 解析回物件,讓 UI 能顯示 bridge reason。
```text
py_compile:
apps/api/src/services/awooop_truth_chain_service.py
apps/api/tests/test_awooop_truth_chain_service.py
# OK
ruff F,E9:
# All checks passed
pytest:
apps/api/tests/test_awooop_truth_chain_service.py
apps/api/tests/test_platform_router_order.py
apps/api/tests/test_awooop_operator_auth.py
# 11 passed
```
**效果**
- `INC-20260512-B6C589` truth-chain 現在不再是 `awooop_mcp_gateway_audit_empty`
- 仍顯示 `manual_required/blocked`,因為 8 個 SSH MCP 都失敗approval/incident 狀態仍矛盾;這是 T5 要處理,不能用 T2 粉飾成自動修復完成。

View File

@@ -1883,6 +1883,8 @@ Phase 6 完成後
- T2 bridge image `94d006ea` 已部署CD run `1921` successhealth 200。 - T2 bridge image `94d006ea` 已部署CD run `1921` successhealth 200。
- rollback smoke 證明 `record_mcp_call()` 在同一 transaction 內會同時寫 legacy `mcp_audit_log``awooop_mcp_gateway_audit` bridge row且 bridge row 標示 `policy_enforced=false` / `not_used_reason=legacy direct provider path; bridge audit only`rollback 後兩邊皆未污染 production。 - rollback smoke 證明 `record_mcp_call()` 在同一 transaction 內會同時寫 legacy `mcp_audit_log``awooop_mcp_gateway_audit` bridge row且 bridge row 標示 `policy_enforced=false` / `not_used_reason=legacy direct provider path; bridge audit only`rollback 後兩邊皆未污染 production。
- 部署後短觀察窗內沒有自然新 legacy MCP call`legacy_mcp_15m=0`),所以 live `awooop_mcp_gateway_audit` total 仍是 0。T2 bridge capability 已上線,但 T2 全退出條件仍需下一個真實 MCP 呼叫產生 durable row或把高頻 caller 改成 first-class Gateway path。 - 部署後短觀察窗內沒有自然新 legacy MCP call`legacy_mcp_15m=0`),所以 live `awooop_mcp_gateway_audit` total 仍是 0。T2 bridge capability 已上線,但 T2 全退出條件仍需下一個真實 MCP 呼叫產生 durable row或把高頻 caller 改成 first-class Gateway path。
- 已執行最近 24h 真實 legacy MCP backfill`inserted_bridge_rows=1160`,目前 `awooop_mcp_gateway_audit gateway_total=1310 / bridge_total=1310 / last_24h=1276``INC-20260512-B6C589` 現在 gateway side 可見 8 筆 MCP8 failed / 0 successtruth-chain blocker 移除 `awooop_mcp_gateway_audit_empty`,但仍是 `manual_required/blocked`,因為 evidence sensors 全失敗、NO_ACTION approval 無 execution、incident 仍 investigating。
- truth-chain API 追加回傳 `gate_result`,讓 Operator Console 可直接顯示 `policy_enforced=false``not_used_reason`,避免把 bridge row 誤認為 first-class Gateway enforcement。
**仍未宣稱完成** **仍未宣稱完成**
- 這只是 legacy bridge不是把所有呼叫強制改經 AwoooP GatewayT2 後續仍要把新 MCP caller 收斂到 first-class Gateway path。 - 這只是 legacy bridge不是把所有呼叫強制改經 AwoooP GatewayT2 後續仍要把新 MCP caller 收斂到 first-class Gateway path。

View File

@@ -0,0 +1,69 @@
-- AwoooP T2 MCP Gateway bridge backfill (24h)
-- 2026-05-12 Codex + ogt
--
-- Purpose:
-- Mirror real legacy mcp_audit_log rows into awooop_mcp_gateway_audit so
-- truth-chain can show MCP usage for recent incidents while first-class
-- Gateway migration continues. These rows are explicitly marked as bridge
-- records and policy_enforced=false; they are not proof of five-gate
-- Gateway enforcement.
--
-- Idempotency:
-- gate_result.legacy_audit_id stores the mcp_audit_log.id source key.
-- Re-running this SQL will only insert missing rows.
WITH inserted AS (
INSERT INTO awooop_mcp_gateway_audit (
project_id,
run_id,
trace_id,
agent_id,
tool_name,
input_hash,
output_hash,
gate_result,
result_status,
block_gate,
block_reason,
latency_ms,
created_at
)
SELECT
'awoooi' AS project_id,
NULL::uuid AS run_id,
LEFT(COALESCE(src.incident_id, src.session_id), 128) AS trace_id,
LEFT(COALESCE(src.agent_role, 'legacy-mcp-provider'), 128) AS agent_id,
LEFT('legacy:' || src.mcp_server || ':' || src.tool_name, 128) AS tool_name,
encode(digest(COALESCE(src.input_params::text, 'null'), 'sha256'), 'hex') AS input_hash,
CASE
WHEN src.output_result IS NULL THEN NULL
ELSE encode(digest(src.output_result::text, 'sha256'), 'hex')
END AS output_hash,
jsonb_build_object(
'schema_version', 'legacy_mcp_bridge_v1',
'gateway_path', 'legacy_backfill',
'policy_enforced', false,
'not_used_reason', 'legacy direct provider path; bridge audit only',
'legacy_audit_id', src.id::text,
'legacy_mcp_server', src.mcp_server,
'legacy_tool_name', src.tool_name,
'flywheel_node', src.flywheel_node
) AS gate_result,
CASE WHEN src.success IS TRUE THEN 'success' ELSE 'failed' END AS result_status,
NULL::smallint AS block_gate,
CASE WHEN src.success IS TRUE THEN NULL ELSE LEFT(src.error_message, 256) END AS block_reason,
src.duration_ms AS latency_ms,
src.created_at
FROM mcp_audit_log src
WHERE src.created_at > NOW() - INTERVAL '24 hours'
AND NOT EXISTS (
SELECT 1
FROM awooop_mcp_gateway_audit dst
WHERE dst.project_id = 'awoooi'
AND dst.gate_result->>'schema_version' = 'legacy_mcp_bridge_v1'
AND dst.gate_result->>'legacy_audit_id' = src.id::text
)
RETURNING call_id
)
SELECT COUNT(*) AS inserted_bridge_rows
FROM inserted;