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
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user