fix(mcp): tag legacy provider calls with audit context
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
CD Pipeline / tests (push) Successful in 59s
CD Pipeline / build-and-deploy (push) Successful in 3m22s
CD Pipeline / post-deploy-checks (push) Successful in 1m19s

This commit is contained in:
Your Name
2026-05-06 17:18:52 +08:00
parent 76aaaf480c
commit fa0e956c0e
11 changed files with 300 additions and 11 deletions

View File

@@ -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),
)

View File

@@ -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:

View 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

View File

@@ -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:

View File

@@ -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,
)