fix(mcp): normalize audit session ids
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user