fix(mcp): normalize audit session ids
All checks were successful
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Successful in 58s
CD Pipeline / build-and-deploy (push) Successful in 4m39s
CD Pipeline / post-deploy-checks (push) Successful in 1m17s

This commit is contained in:
Your Name
2026-05-06 17:40:42 +08:00
parent 9b0f55fd90
commit 0e2e856f12
5 changed files with 174 additions and 2 deletions

View File

@@ -7,8 +7,61 @@ observable without changing execution semantics.
from __future__ import annotations
import hashlib
from typing import Any
MAX_MCP_AUDIT_SESSION_ID_LENGTH = 36
_STAGE_ALIASES = {
"pre_decision": "pre",
"post_execution": "post",
}
def _digest(value: str) -> str:
return hashlib.sha1(value.encode("utf-8", errors="ignore")).hexdigest()[:8]
def _compact_with_hash(prefix: str, stable_part: str, raw: str) -> str:
safe_prefix = "".join(
char for char in prefix if char.isalnum() or char in "-_"
)[:8] or "mcp"
digest = _digest(raw)
head_limit = (
MAX_MCP_AUDIT_SESSION_ID_LENGTH
- len(safe_prefix)
- len(digest)
- 2
)
head = str(stable_part)[:max(1, head_limit)]
compacted = f"{safe_prefix}:{head}:{digest}"
return compacted[:MAX_MCP_AUDIT_SESSION_ID_LENGTH]
def normalize_mcp_audit_session_id(session_id: Any | None) -> str | None:
"""Normalize MCP audit session IDs to the legacy DB column length."""
if session_id is None:
return None
raw = str(session_id)
if len(raw) <= MAX_MCP_AUDIT_SESSION_ID_LENGTH:
return raw
parts = raw.split(":")
if len(parts) >= 3 and parts[0] == "incident":
stage = _STAGE_ALIASES.get(parts[-1], parts[-1][:6])
candidate = f"inc:{parts[1]}:{stage}"
if len(candidate) <= MAX_MCP_AUDIT_SESSION_ID_LENGTH:
return candidate
if len(parts) >= 3 and parts[0] == "callback":
return _compact_with_hash("cb", parts[1], raw)
if len(parts) >= 2 and parts[0] == "approval":
return _compact_with_hash("apr", parts[1], raw)
if len(parts) >= 2 and parts[0] == "mcp_bridge":
return _compact_with_hash("bridge", parts[1], raw)
return _compact_with_hash(parts[0] if parts else "mcp", raw, raw)
def build_mcp_audit_context(
*,
@@ -25,7 +78,7 @@ def build_mcp_audit_context(
"gateway_path": gateway_path,
}
optional_values = {
"session_id": session_id,
"session_id": normalize_mcp_audit_session_id(session_id),
"incident_id": incident_id,
"flywheel_node": flywheel_node,
"agent_role": agent_role,

View File

@@ -17,6 +17,7 @@ import structlog
from sqlalchemy import text
from src.db.base import get_db_context
from src.services.mcp_audit_context import normalize_mcp_audit_session_id
logger = structlog.get_logger(__name__)
@@ -95,7 +96,9 @@ async def record_mcp_call(
"""Persist one MCP tool call and update daily aggregate stats."""
audit_context = _extract_audit_context(input_params)
session_id = session_id or audit_context.get("session_id") or str(uuid.uuid4())
session_id = normalize_mcp_audit_session_id(
session_id or audit_context.get("session_id") or str(uuid.uuid4())
)
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")