feat(awooop): route post verify mcp through gateway
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
CD Pipeline / tests (push) Successful in 1m7s
CD Pipeline / build-and-deploy (push) Successful in 10m15s
CD Pipeline / post-deploy-checks (push) Successful in 1m54s

This commit is contained in:
Your Name
2026-05-13 10:30:03 +08:00
parent 15873b9e0c
commit 1a03bceb5c
2 changed files with 89 additions and 2 deletions

View File

@@ -38,9 +38,12 @@ from typing import TYPE_CHECKING, Any
import structlog 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.evidence_snapshot import EvidenceSnapshot
from src.services.mcp_audit_context import with_mcp_audit_context 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 from src.services.sanitization_service import sanitize, sanitize_dict_values
# W2 PR-V1: 頂層 import 讓測試 patch 路徑固定(延遲 import 無法被 patch # W2 PR-V1: 頂層 import 讓測試 patch 路徑固定(延遲 import 無法被 patch
# ENABLE_SELF_HEALING_VALIDATOR=False 時此 import 不影響效能(純 python 模組) # ENABLE_SELF_HEALING_VALIDATOR=False 時此 import 不影響效能(純 python 模組)
@@ -224,7 +227,12 @@ class PostExecutionVerifier:
agent_role="post_execution_verifier", agent_role="post_execution_verifier",
) )
result = await asyncio.wait_for( 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, timeout=TOOL_TIMEOUT_SEC,
) )
if result.success and result.output: if result.success and result.output:
@@ -243,6 +251,35 @@ class PostExecutionVerifier:
return state 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 串接 # W2 PR-V1: SelfHealingValidator 串接

View File

@@ -23,6 +23,8 @@ import pytest
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from src.plugins.mcp.interfaces import MCPTool, MCPToolProvider, MCPToolResult 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 ( from src.services.post_execution_verifier import (
PostExecutionVerifier, PostExecutionVerifier,
_assess_recovery, _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: class TestCollectPostStateAuditContext:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_collect_post_state_injects_mcp_audit_context(self): 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["session_id"] == "incident:INC-TEST:post_execution"
assert audit_context["flywheel_node"] == "verify" assert audit_context["flywheel_node"] == "verify"
assert audit_context["agent_role"] == "post_execution_verifier" 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"