From b4d367eeb463eccda5aec8aa9c90f19897dbd634 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 13 May 2026 03:21:31 +0800 Subject: [PATCH] feat(awooop): expose mcp bridge truth chain --- .../services/awooop_truth_chain_service.py | 13 +++- .../tests/test_awooop_truth_chain_service.py | 17 ++++- docs/LOGBOOK.md | 37 ++++++++++ ...-04-15-MASTER-ai-autonomous-flywheel-v2.md | 2 + ...awooop-mcp-gateway-bridge-backfill-24h.sql | 69 +++++++++++++++++++ 5 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 scripts/ops/awooop-mcp-gateway-bridge-backfill-24h.sql diff --git a/apps/api/src/services/awooop_truth_chain_service.py b/apps/api/src/services/awooop_truth_chain_service.py index d1e669dd..78867133 100644 --- a/apps/api/src/services/awooop_truth_chain_service.py +++ b/apps/api/src/services/awooop_truth_chain_service.py @@ -7,6 +7,7 @@ Telegram cards can be audited without guessing which subsystem owns the truth. from __future__ import annotations +import json from datetime import date, datetime from decimal import Decimal from typing import Any @@ -20,6 +21,7 @@ from src.db.base import get_db_context logger = structlog.get_logger(__name__) _MAX_ROWS = 100 +_JSON_TEXT_FIELDS = {"gate_result", "source_envelope"} def _clean(value: Any) -> Any: @@ -38,7 +40,15 @@ def _clean(value: Any) -> 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]]: @@ -507,6 +517,7 @@ async def fetch_truth_chain(source_id: str, project_id: str = "awoooi") -> dict[ trace_id, agent_id, tool_name, + gate_result, result_status, block_gate, block_reason, diff --git a/apps/api/tests/test_awooop_truth_chain_service.py b/apps/api/tests/test_awooop_truth_chain_service.py index 4c308732..bf99813e 100644 --- a/apps/api/tests/test_awooop_truth_chain_service.py +++ b/apps/api/tests/test_awooop_truth_chain_service.py @@ -1,6 +1,21 @@ 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: diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 33522e80..3140f747 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -6505,3 +6505,40 @@ gateway_audit_total=0 last_15m=0 bridge_total=0 - 因此目前只能宣稱「T2 bridge 寫入能力已部署並經 rollback smoke 驗證」。 - 尚不能宣稱「所有 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 粉飾成自動修復完成。 diff --git a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md index 15f6a18c..966a03a5 100644 --- a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md +++ b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md @@ -1883,6 +1883,8 @@ Phase 6 完成後 - T2 bridge image `94d006ea` 已部署,CD run `1921` success,health 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。 - 部署後短觀察窗內沒有自然新 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 筆 MCP,8 failed / 0 success;truth-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 Gateway;T2 後續仍要把新 MCP caller 收斂到 first-class Gateway path。 diff --git a/scripts/ops/awooop-mcp-gateway-bridge-backfill-24h.sql b/scripts/ops/awooop-mcp-gateway-bridge-backfill-24h.sql new file mode 100644 index 00000000..509621f3 --- /dev/null +++ b/scripts/ops/awooop-mcp-gateway-bridge-backfill-24h.sql @@ -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;