fix(awooop): persist approved ssh gateway blocks
All checks were successful
Code Review / ai-code-review (push) Successful in 20s
CD Pipeline / tests (push) Successful in 3m58s
CD Pipeline / build-and-deploy (push) Successful in 3m47s
CD Pipeline / post-deploy-checks (push) Successful in 1m18s

This commit is contained in:
Your Name
2026-05-13 11:15:43 +08:00
parent ce83e8dc00
commit 34bfe56f53
3 changed files with 85 additions and 14 deletions

View File

@@ -34,7 +34,7 @@ from src.core.config import settings
from src.core.redis_client import get_redis
from src.db.base import get_db_context
from src.models.approval import ApprovalRequest
from src.plugins.mcp.gateway import GatewayContext, McpGateway
from src.plugins.mcp.gateway import GatewayContext, McpGateway, McpGatewayError
from src.plugins.mcp.interfaces import MCPToolResult
from src.services.approval_db import get_approval_service, get_timeline_service
from src.services.executor import ExecutionResult, OperationType, get_executor
@@ -813,19 +813,33 @@ class ApprovalExecutionService:
},
}
async with get_db_context(_SSH_GATEWAY_PROJECT_ID) as db:
return await McpGateway(db).call(
GatewayContext(
project_id=_SSH_GATEWAY_PROJECT_ID,
agent_id=_SSH_GATEWAY_AGENT_ID,
tool_name=tool_name,
run_id=run_id,
trace_id=approval.incident_id or str(approval.id),
is_shadow=False,
environment={"env": "prod"},
required_scope=required_scope,
),
params_with_audit,
ctx = GatewayContext(
project_id=_SSH_GATEWAY_PROJECT_ID,
agent_id=_SSH_GATEWAY_AGENT_ID,
tool_name=tool_name,
run_id=run_id,
trace_id=approval.incident_id or str(approval.id),
is_shadow=False,
environment={"env": "prod"},
required_scope=required_scope,
)
try:
return await McpGateway(db).call(ctx, params_with_audit)
except McpGatewayError as exc:
logger.warning(
"mcp_gateway_approved_ssh_blocked",
approval_id=str(approval.id),
incident_id=approval.incident_id,
tool=tool_name,
gate=exc.gate,
error_code=exc.error_code,
error=str(exc),
)
return MCPToolResult(
success=False,
execution_id=f"blocked:{tool_name}:{run_id}",
error=f"{exc.error_code}: {exc}",
)
async def _push_execution_result_to_alert(
self,

View File

@@ -7,6 +7,7 @@ from uuid import UUID
import pytest
from src.models.approval import ApprovalRequest, RiskLevel
from src.plugins.mcp.gateway import GateApprovalError
from src.plugins.mcp.interfaces import MCPToolResult
from src.services.approval_execution import ApprovalExecutionService
@@ -145,3 +146,58 @@ async def test_ssh_diagnose_approval_uses_gateway_without_gate5_projection(
assert ctx.tool_name == "ssh_diagnose"
assert ctx.required_scope == "read"
assert ctx.is_shadow is False
@pytest.mark.asyncio
async def test_gateway_block_returns_failed_execution_result_without_rollback(
monkeypatch: pytest.MonkeyPatch,
) -> None:
fake_redis = FakeRedis()
db_context_exits: list[str] = []
@asynccontextmanager
async def fake_db_context(project_id: str | None = None):
try:
yield {"project_id": project_id}
except Exception:
db_context_exits.append("raised")
raise
else:
db_context_exits.append("normal")
class FakeGateway:
def __init__(self, db: object) -> None:
self.db = db
async def call(self, ctx: Any, parameters: dict[str, Any]) -> MCPToolResult:
raise GateApprovalError("approval missing in smoke path")
monkeypatch.setattr("src.services.approval_execution.get_redis", lambda: fake_redis)
monkeypatch.setattr("src.services.approval_execution.get_db_context", fake_db_context)
monkeypatch.setattr("src.services.approval_execution.McpGateway", FakeGateway)
approval = ApprovalRequest(
action="docker restart sentry-worker",
description="測試 Gateway blocked audit 不被 rollback",
risk_level=RiskLevel.LOW,
requested_by="test",
required_signatures=0,
incident_id="INC-TEST-BLOCKED",
)
result = await ApprovalExecutionService()._execute_ssh_host_action(
approval=approval,
host="192.168.0.110",
)
assert result.success is False
assert result.error is not None
assert "E-MCP-GATE-005" in result.error
assert db_context_exits == ["normal"]
assert fake_redis.set_calls == [
(
f"mcp_approval:awoooi:approval_executor:ssh_docker_restart:{approval.id}",
"approved",
600,
)
]

View File

@@ -7034,6 +7034,7 @@ not_used_reason=legacy direct provider path; bridge audit only
- `ssh_diagnose` 走 read scope不投影 Gate 5 key。
- `ssh_docker_restart` / `ssh_systemctl_restart` 等 write tool 走 write scope。
- `ssh_docker_prune` 走 admin scope。
- Gateway Gate block 轉為 failed `ExecutionResult`,避免 `get_db_context` 因 exception rollback 而丟失 blocked audit。
- 新增 migration seed
- `awooop_awoooi_mcp_approval_executor_ssh_gateway_2026-05-13.sql`
- `awooop_awoooi_mcp_approval_executor_ssh_gateway_2026-05-13_down.sql`
@@ -7042,7 +7043,7 @@ not_used_reason=legacy direct provider path; bridge audit only
```text
python -m pytest tests/test_mcp_gateway_gate5.py tests/test_approval_execution_mcp_audit.py -q
5 passed
6 passed
python -m ruff check --select F821 src/services/approval_execution.py tests/test_approval_execution_mcp_audit.py
All checks passed