fix(mcp): tag legacy provider calls with audit context
This commit is contained in:
@@ -280,6 +280,7 @@ async def dispatch_action(
|
||||
|
||||
# MCP registry dispatch
|
||||
from src.plugins.mcp.registry import get_provider
|
||||
from src.services.mcp_audit_context import with_mcp_audit_context
|
||||
provider_name = _resolve_provider_name(spec.mcp_provider)
|
||||
provider = get_provider(provider_name)
|
||||
if not provider:
|
||||
@@ -293,8 +294,16 @@ async def dispatch_action(
|
||||
)
|
||||
|
||||
# 執行 MCP tool with timeout
|
||||
audited_params = with_mcp_audit_context(
|
||||
resolved_params,
|
||||
session_id=f"callback:{incident_id}:{action_name}",
|
||||
incident_id=incident_id,
|
||||
flywheel_node="operate",
|
||||
agent_role="telegram_callback_dispatcher",
|
||||
operator_user_id=user_id,
|
||||
)
|
||||
mcp_result = await asyncio.wait_for(
|
||||
provider.execute(spec.mcp_tool, resolved_params),
|
||||
provider.execute(spec.mcp_tool, audited_params),
|
||||
timeout=float(spec.timeout_sec),
|
||||
)
|
||||
|
||||
|
||||
@@ -317,11 +317,20 @@ class HeartbeatReportService:
|
||||
"""K8s MCP:確認 kubectl 能連到 K3s"""
|
||||
try:
|
||||
from src.plugins.mcp.providers.k8s_provider import K8sProvider
|
||||
provider = K8sProvider()
|
||||
from src.plugins.mcp.registry import AuditedMCPToolProvider
|
||||
from src.services.mcp_audit_context import with_mcp_audit_context
|
||||
provider = AuditedMCPToolProvider(K8sProvider())
|
||||
if not provider.enabled:
|
||||
return ProbeResult(False, "⚠️ K8s MCP 未啟用")
|
||||
params = with_mcp_audit_context(
|
||||
{"resource_type": "nodes"},
|
||||
session_id="heartbeat:mcp_k8s",
|
||||
flywheel_node="govern",
|
||||
agent_role="heartbeat_report_service",
|
||||
gateway_path="legacy_heartbeat_provider",
|
||||
)
|
||||
result = await asyncio.wait_for(
|
||||
provider.execute("kubectl_get", {"resource_type": "nodes"}),
|
||||
provider.execute("kubectl_get", params),
|
||||
timeout=_PROBE_TIMEOUT,
|
||||
)
|
||||
if result.success:
|
||||
@@ -389,14 +398,23 @@ class HeartbeatReportService:
|
||||
"""Velero 備份:確認最後一次備份是否在 26 小時內"""
|
||||
try:
|
||||
from src.plugins.mcp.providers.k8s_provider import K8sProvider
|
||||
provider = K8sProvider()
|
||||
from src.plugins.mcp.registry import AuditedMCPToolProvider
|
||||
from src.services.mcp_audit_context import with_mcp_audit_context
|
||||
provider = AuditedMCPToolProvider(K8sProvider())
|
||||
if not provider.enabled:
|
||||
return ProbeResult(False, "⚠️ K8s MCP 未啟用,無法查 Velero")
|
||||
result = await asyncio.wait_for(
|
||||
provider.execute("kubectl_get", {
|
||||
params = with_mcp_audit_context(
|
||||
{
|
||||
"resource_type": "backups.velero.io",
|
||||
"namespace": "velero",
|
||||
}),
|
||||
},
|
||||
session_id="heartbeat:velero",
|
||||
flywheel_node="govern",
|
||||
agent_role="heartbeat_report_service",
|
||||
gateway_path="legacy_heartbeat_provider",
|
||||
)
|
||||
result = await asyncio.wait_for(
|
||||
provider.execute("kubectl_get", params),
|
||||
timeout=_PROBE_TIMEOUT,
|
||||
)
|
||||
if not result.success:
|
||||
|
||||
49
apps/api/src/services/mcp_audit_context.py
Normal file
49
apps/api/src/services/mcp_audit_context.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""MCP audit context helpers.
|
||||
|
||||
Legacy production callers still use the provider registry directly while
|
||||
AwoooP MCP Gateway is rolled in by lane. These helpers make those calls
|
||||
observable without changing execution semantics.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_mcp_audit_context(
|
||||
*,
|
||||
session_id: str | None = None,
|
||||
incident_id: str | None = None,
|
||||
flywheel_node: str | None = None,
|
||||
agent_role: str | None = None,
|
||||
gateway_path: str = "legacy_registry_provider",
|
||||
operator_user_id: int | str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build the `_mcp_audit` metadata object carried beside tool params."""
|
||||
|
||||
context: dict[str, Any] = {
|
||||
"gateway_path": gateway_path,
|
||||
}
|
||||
optional_values = {
|
||||
"session_id": session_id,
|
||||
"incident_id": incident_id,
|
||||
"flywheel_node": flywheel_node,
|
||||
"agent_role": agent_role,
|
||||
"operator_user_id": operator_user_id,
|
||||
}
|
||||
context.update({key: value for key, value in optional_values.items() if value is not None})
|
||||
return context
|
||||
|
||||
|
||||
def with_mcp_audit_context(
|
||||
parameters: dict[str, Any],
|
||||
**audit_kwargs: Any,
|
||||
) -> dict[str, Any]:
|
||||
"""Return a shallow copy of provider parameters with merged audit context."""
|
||||
|
||||
audited = dict(parameters)
|
||||
existing = audited.get("_mcp_audit")
|
||||
merged = dict(existing) if isinstance(existing, dict) else {}
|
||||
merged.update(build_mcp_audit_context(**audit_kwargs))
|
||||
audited["_mcp_audit"] = merged
|
||||
return audited
|
||||
@@ -34,14 +34,14 @@ MASTER §3.1 L6×D1
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import structlog
|
||||
|
||||
from src.services.evidence_snapshot import EvidenceSnapshot
|
||||
from src.services.mcp_audit_context import with_mcp_audit_context
|
||||
from src.services.mcp_tool_registry import SensorDimension, get_mcp_tool_registry
|
||||
from src.services.sanitization_service import sanitize_dict_values
|
||||
from src.services.sanitization_service import sanitize, sanitize_dict_values
|
||||
# W2 PR-V1: 頂層 import 讓測試 patch 路徑固定(延遲 import 無法被 patch)
|
||||
# ENABLE_SELF_HEALING_VALIDATOR=False 時此 import 不影響效能(純 python 模組)
|
||||
from src.services import self_healing_validator as _shv_module
|
||||
@@ -216,8 +216,15 @@ class PostExecutionVerifier:
|
||||
|
||||
async def _call_one(reg) -> tuple[str, Any]:
|
||||
try:
|
||||
audited_params = with_mcp_audit_context(
|
||||
params,
|
||||
session_id=f"incident:{_get_incident_id(incident)}:post_execution",
|
||||
incident_id=_get_incident_id(incident),
|
||||
flywheel_node="verify",
|
||||
agent_role="post_execution_verifier",
|
||||
)
|
||||
result = await asyncio.wait_for(
|
||||
reg.provider.execute(reg.tool.name, params),
|
||||
reg.provider.execute(reg.tool.name, audited_params),
|
||||
timeout=TOOL_TIMEOUT_SEC,
|
||||
)
|
||||
if result.success and result.output:
|
||||
|
||||
@@ -33,6 +33,7 @@ from typing import TYPE_CHECKING, Any
|
||||
import structlog
|
||||
|
||||
from src.services.evidence_snapshot import EvidenceSnapshot
|
||||
from src.services.mcp_audit_context import with_mcp_audit_context
|
||||
from src.services.mcp_tool_registry import RegisteredTool, SensorDimension, get_mcp_tool_registry
|
||||
from src.services.sanitization_service import sanitize, sanitize_dict_values
|
||||
|
||||
@@ -323,8 +324,15 @@ class PreDecisionInvestigator:
|
||||
_mcp_status = "failed"
|
||||
_mcp_error = None
|
||||
try:
|
||||
audited_params = with_mcp_audit_context(
|
||||
params,
|
||||
session_id=f"incident:{snapshot.incident_id}:pre_decision",
|
||||
incident_id=snapshot.incident_id,
|
||||
flywheel_node="sense",
|
||||
agent_role="pre_decision_investigator",
|
||||
)
|
||||
result = await asyncio.wait_for(
|
||||
reg.provider.execute(tool_name, params),
|
||||
reg.provider.execute(tool_name, audited_params),
|
||||
timeout=MCP_TOOL_TIMEOUT_SEC,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user