diff --git a/apps/api/src/services/post_execution_verifier.py b/apps/api/src/services/post_execution_verifier.py index 25661759..0ca0f4a9 100644 --- a/apps/api/src/services/post_execution_verifier.py +++ b/apps/api/src/services/post_execution_verifier.py @@ -38,9 +38,12 @@ from typing import TYPE_CHECKING, Any import structlog +from src.db.base import get_db_context +from src.plugins.mcp.gateway import GatewayContext, McpGateway +from src.plugins.mcp.registry import AuditedMCPToolProvider 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.mcp_tool_registry import RegisteredTool, SensorDimension, get_mcp_tool_registry from src.services.sanitization_service import sanitize, sanitize_dict_values # W2 PR-V1: 頂層 import 讓測試 patch 路徑固定(延遲 import 無法被 patch) # ENABLE_SELF_HEALING_VALIDATOR=False 時此 import 不影響效能(純 python 模組) @@ -224,7 +227,12 @@ class PostExecutionVerifier: agent_role="post_execution_verifier", ) result = await asyncio.wait_for( - reg.provider.execute(reg.tool.name, audited_params), + self._execute_tool( + reg=reg, + tool_name=reg.tool.name, + audited_params=audited_params, + incident_id=_get_incident_id(incident), + ), timeout=TOOL_TIMEOUT_SEC, ) if result.success and result.output: @@ -243,6 +251,35 @@ class PostExecutionVerifier: return state + async def _execute_tool( + self, + reg: RegisteredTool, + tool_name: str, + audited_params: dict[str, Any], + incident_id: str, + ): + """Route production post-execution sensors through AwoooP MCP Gateway. + + Raw providers are still used by unit tests and manual injections. In + production the registry wraps providers in `AuditedMCPToolProvider`, and + those calls must leave first-class gateway audit rows just like the + pre-decision sense path. + """ + if not isinstance(reg.provider, AuditedMCPToolProvider): + return await reg.provider.execute(tool_name, audited_params) + + async with get_db_context("awoooi") as db: + ctx = GatewayContext( + project_id="awoooi", + agent_id="post_execution_verifier", + tool_name=tool_name, + trace_id=incident_id, + is_shadow=True, + environment={"env": "prod"}, + required_scope="read", + ) + return await McpGateway(db).call(ctx, audited_params) + # ───────────────────────────────────────────────────────────────────────────── # W2 PR-V1: SelfHealingValidator 串接 diff --git a/apps/api/tests/test_post_execution_verifier.py b/apps/api/tests/test_post_execution_verifier.py index 56bcd0e3..bada10d8 100644 --- a/apps/api/tests/test_post_execution_verifier.py +++ b/apps/api/tests/test_post_execution_verifier.py @@ -23,6 +23,8 @@ import pytest from unittest.mock import AsyncMock, patch from src.plugins.mcp.interfaces import MCPTool, MCPToolProvider, MCPToolResult +from src.plugins.mcp.registry import AuditedMCPToolProvider +from src.services import post_execution_verifier as pev_module from src.services.post_execution_verifier import ( PostExecutionVerifier, _assess_recovery, @@ -341,6 +343,14 @@ class _FakeRegistry: ] +class _DbContext: + async def __aenter__(self) -> object: + return object() + + async def __aexit__(self, *_args: object) -> None: + return None + + class TestCollectPostStateAuditContext: @pytest.mark.asyncio async def test_collect_post_state_injects_mcp_audit_context(self): @@ -357,3 +367,43 @@ class TestCollectPostStateAuditContext: assert audit_context["session_id"] == "incident:INC-TEST:post_execution" assert audit_context["flywheel_node"] == "verify" assert audit_context["agent_role"] == "post_execution_verifier" + + @pytest.mark.asyncio + async def test_collect_post_state_routes_audited_provider_through_gateway( + self, + monkeypatch: pytest.MonkeyPatch, + ): + provider = _CaptureProvider() + verifier = PostExecutionVerifier() + audited_provider = AuditedMCPToolProvider(provider) + verifier._registry = _FakeRegistry(audited_provider) + calls: list[dict] = [] + + class FakeGateway: + def __init__(self, db: object) -> None: + self.db = db + + async def call(self, ctx, parameters: dict) -> MCPToolResult: + calls.append({"ctx": ctx, "parameters": parameters, "db": self.db}) + return MCPToolResult( + success=True, + execution_id="gw", + output={"status": "Running"}, + ) + + monkeypatch.setattr(pev_module, "get_db_context", lambda _project_id: _DbContext()) + monkeypatch.setattr(pev_module, "McpGateway", FakeGateway) + + state = await verifier._collect_post_state(_stub_incident()) + + assert state["kubectl_get"]["status"] == "Running" + assert provider.seen_parameters is None + assert calls + ctx = calls[0]["ctx"] + assert ctx.project_id == "awoooi" + assert ctx.agent_id == "post_execution_verifier" + assert ctx.tool_name == "kubectl_get" + assert ctx.trace_id == "INC-TEST" + assert ctx.required_scope == "read" + assert calls[0]["parameters"]["_mcp_audit"]["incident_id"] == "INC-TEST" + assert calls[0]["parameters"]["_mcp_audit"]["flywheel_node"] == "verify"