feat(awooop): bridge legacy mcp audit into gateway timeline
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
CD Pipeline / tests (push) Successful in 1m4s
CD Pipeline / build-and-deploy (push) Successful in 3m22s
CD Pipeline / post-deploy-checks (push) Successful in 1m14s

This commit is contained in:
Your Name
2026-05-12 23:44:19 +08:00
parent 96a8cf3ad5
commit 94d006eac8
5 changed files with 286 additions and 2 deletions

View File

@@ -9,6 +9,7 @@ before every runtime model has been refreshed.
from __future__ import annotations
import json
import hashlib
import time
import uuid
from typing import Any
@@ -22,6 +23,8 @@ from src.services.mcp_audit_context import normalize_mcp_audit_session_id
logger = structlog.get_logger(__name__)
_REDACT_KEYS = {"token", "password", "secret", "api_key", "authorization", "key"}
_LEGACY_PROJECT_ID = "awoooi"
_REAL_GATEWAY_PATH = "awooop_mcp_gateway"
def infer_flywheel_node(mcp_server: str, tool_name: str) -> str:
@@ -79,6 +82,109 @@ def _extract_audit_context(parameters: dict[str, Any]) -> dict[str, Any]:
return audit_context if isinstance(audit_context, dict) else {}
def _extract_project_id(
parameters: dict[str, Any],
audit_context: dict[str, Any],
) -> str:
"""Legacy MCP calls predate AwoooP tenancy; keep the bridge explicit."""
for value in (audit_context.get("project_id"), parameters.get("project_id")):
if value:
return str(value)
return _LEGACY_PROJECT_ID
def _extract_run_id(audit_context: dict[str, Any]) -> uuid.UUID | None:
value = audit_context.get("run_id")
if not value:
return None
try:
return uuid.UUID(str(value))
except (TypeError, ValueError):
return None
def _compact(value: Any, limit: int) -> str | None:
if value is None:
return None
return str(value)[:limit]
def _stable_hash(value: Any) -> str:
canonical = json.dumps(value, sort_keys=True, ensure_ascii=False, default=str)
return hashlib.sha256(canonical.encode()).hexdigest()
def _bridge_tool_name(mcp_server: str, tool_name: str) -> str:
combined = f"legacy:{mcp_server}:{tool_name}"
if len(combined) <= 128:
return combined
return str(tool_name)[:128]
def _should_bridge_to_awooop(audit_context: dict[str, Any]) -> bool:
"""Skip real Gateway calls; they already write first-class audit rows."""
return audit_context.get("gateway_path") != _REAL_GATEWAY_PATH
async def _record_awooop_gateway_bridge(
db: Any,
*,
project_id: str,
run_id: uuid.UUID | None,
trace_id: str | None,
agent_id: str | None,
mcp_server: str,
tool_name: str,
input_params: dict[str, Any],
output_result: Any | None,
duration_ms: int,
success: bool,
error_message: str | None,
flywheel_node: str | None,
audit_context: dict[str, Any],
) -> None:
"""Mirror legacy direct provider calls into AwoooP audit as bridge rows."""
result_status = "success" if success else "failed"
gate_result = {
"schema_version": "legacy_mcp_bridge_v1",
"gateway_path": audit_context.get("gateway_path") or "legacy_registry_provider",
"policy_enforced": False,
"not_used_reason": "legacy direct provider path; bridge audit only",
"legacy_mcp_server": mcp_server,
"legacy_tool_name": tool_name,
"flywheel_node": flywheel_node,
}
await db.execute(
text(
"""
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
)
VALUES (
:project_id, :run_id, :trace_id, :agent_id,
:tool_name, :input_hash, :output_hash, CAST(:gate_result AS jsonb),
:result_status, NULL, :block_reason, :latency_ms
)
"""
),
{
"project_id": project_id,
"run_id": run_id,
"trace_id": _compact(trace_id, 128),
"agent_id": _compact(agent_id or "legacy-mcp-provider", 128),
"tool_name": _bridge_tool_name(mcp_server, tool_name),
"input_hash": _stable_hash(input_params),
"output_hash": _stable_hash(output_result) if output_result is not None else None,
"gate_result": json.dumps(gate_result, ensure_ascii=False, default=str),
"result_status": result_status,
"block_reason": _compact(error_message, 256) if not success else None,
"latency_ms": duration_ms,
},
)
async def record_mcp_call(
*,
mcp_server: str,
@@ -102,9 +208,13 @@ async def record_mcp_call(
flywheel_node = flywheel_node or infer_flywheel_node(mcp_server, tool_name)
incident_id = incident_id or _extract_incident_id(input_params)
agent_role = agent_role or audit_context.get("agent_role")
project_id = _extract_project_id(input_params, audit_context)
run_id = _extract_run_id(audit_context)
trace_id = audit_context.get("trace_id") or incident_id or session_id
agent_id = audit_context.get("agent_id") or agent_role
try:
async with get_db_context() as db:
async with get_db_context(project_id) as db:
await db.execute(
text(
"""
@@ -165,6 +275,23 @@ async def record_mcp_call(
"duration_ms": duration_ms,
},
)
if _should_bridge_to_awooop(audit_context):
await _record_awooop_gateway_bridge(
db,
project_id=project_id,
run_id=run_id,
trace_id=trace_id,
agent_id=agent_id,
mcp_server=mcp_server,
tool_name=tool_name,
input_params=input_params,
output_result=output_result,
duration_ms=duration_ms,
success=success,
error_message=error_message,
flywheel_node=flywheel_node,
audit_context=audit_context,
)
except Exception as exc:
logger.warning(
"mcp_audit_write_failed",