feat(awooop): gate approved ssh execution
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
run-migration / migrate (push) Successful in 9s
CD Pipeline / tests (push) Successful in 1m22s
CD Pipeline / build-and-deploy (push) Successful in 6m36s
CD Pipeline / post-deploy-checks (push) Successful in 1m42s

This commit is contained in:
Your Name
2026-05-13 11:02:24 +08:00
parent 85a1bcef52
commit a0a2a5b1f0
5 changed files with 452 additions and 67 deletions

View File

@@ -25,14 +25,19 @@ Approval Execution Service - Phase 16 R4.2 瘦身 Router 抽取
import asyncio
import time
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from uuid import UUID
import structlog
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.interfaces import MCPToolResult
from src.services.approval_db import get_approval_service, get_timeline_service
from src.services.executor import OperationType, get_executor
from src.services.executor import ExecutionResult, OperationType, get_executor
from src.services.operation_parser import parse_operation_from_action
if TYPE_CHECKING:
@@ -45,6 +50,23 @@ logger = structlog.get_logger(__name__)
# 上限 60s 涵蓋 verifier warmup(10s) + collect(30s) + 緩衝 20s.
_VERIFIER_AWAIT_TIMEOUT_SEC = 60.0
# T9: approved SSH execution must go through AwoooP MCP Gateway.
# ApprovalRequest itself is the human/multi-sig decision artifact; for write/admin
# tools we project it into the short-lived Gate 5 Redis key expected by Gateway.
_SSH_GATEWAY_AGENT_ID = "approval_executor"
_SSH_GATEWAY_PROJECT_ID = "awoooi"
_SSH_GATEWAY_APPROVAL_TTL_SECONDS = 600
_SSH_GATEWAY_TOOL_SCOPES: dict[str, str] = {
"ssh_diagnose": "read",
"ssh_docker_restart": "write",
"ssh_docker_compose_restart": "write",
"ssh_systemctl_restart": "write",
"ssh_clear_docker_logs": "write",
"ssh_renew_ssl": "write",
"ssh_reload_nginx": "write",
"ssh_docker_prune": "admin",
}
class ApprovalExecutionService:
"""
@@ -638,7 +660,7 @@ class ApprovalExecutionService:
self,
approval: ApprovalRequest,
host: str,
) -> "ExecutionResult":
) -> ExecutionResult:
"""
執行 SSH 主機 action手動批准路徑專用
@@ -653,8 +675,6 @@ class ApprovalExecutionService:
- "ps aux" / "df -h" / "free -h" / "top" / "uptime" / 'echo' / 'ls -lah' → ssh_diagnose
- 其他:回傳失敗,提示 LLM 改寫 action
"""
from src.services.executor import ExecutionResult
start = time.time()
action = approval.action or ""
action_lower = action.lower().strip()
@@ -708,36 +728,19 @@ class ApprovalExecutionService:
error=err,
)
# 呼叫 SSH MCP Provider
# 2026-05-06 Codex: approved execution 是高風險「實際執行」路徑。
# 在 AwoooP MCP Gateway 完全接管前,至少必須經過 AuditedMCPToolProvider
# 寫入 durable mcp_audit_log並標記這仍是 legacy direct provider path。
from src.plugins.mcp.providers.ssh_provider import SSHProvider
from src.plugins.mcp.registry import AuditedMCPToolProvider
provider = AuditedMCPToolProvider(SSHProvider())
params_with_audit = {
**params,
"_mcp_audit": {
"session_id": f"approval:{approval.id}",
"incident_id": approval.incident_id,
"agent_role": "approval_executor",
"flywheel_node": "execute",
"gateway_path": "legacy_direct_provider",
},
}
try:
logger.warning(
"mcp_gateway_legacy_direct_provider_path",
"mcp_gateway_approved_ssh_execution_path",
approval_id=str(approval.id),
incident_id=approval.incident_id,
tool=tool_name,
host=host,
reason="awooop_gateway_not_enforced_for_legacy_approval_execution",
agent_id=_SSH_GATEWAY_AGENT_ID,
)
mcp_result = await provider.execute(
mcp_result = await self._execute_ssh_tool_via_gateway(
approval=approval,
tool_name=tool_name,
parameters=params_with_audit,
params=params,
)
duration_ms = int((time.time() - start) * 1000)
success = bool(mcp_result.success)
@@ -769,6 +772,61 @@ class ApprovalExecutionService:
error=str(e),
)
async def _execute_ssh_tool_via_gateway(
self,
approval: ApprovalRequest,
tool_name: str,
params: dict[str, Any],
) -> MCPToolResult:
required_scope = _SSH_GATEWAY_TOOL_SCOPES.get(tool_name, "read")
run_id = approval.id if isinstance(approval.id, UUID) else UUID(str(approval.id))
if required_scope != "read":
approval_key = (
f"mcp_approval:{_SSH_GATEWAY_PROJECT_ID}:{_SSH_GATEWAY_AGENT_ID}:"
f"{tool_name}:{run_id}"
)
try:
redis = get_redis()
await redis.set(
approval_key,
"approved",
ex=_SSH_GATEWAY_APPROVAL_TTL_SECONDS,
)
except Exception as exc:
logger.warning(
"mcp_gateway_approval_projection_failed",
approval_id=str(approval.id),
tool=tool_name,
approval_key=approval_key,
error=str(exc),
)
params_with_audit = {
**params,
"_mcp_audit": {
"session_id": f"approval:{approval.id}",
"incident_id": approval.incident_id,
"agent_role": _SSH_GATEWAY_AGENT_ID,
"flywheel_node": "execute",
"approval_id": str(approval.id),
},
}
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,
)
async def _push_execution_result_to_alert(
self,
approval: ApprovalRequest,