feat(sre): enforce verified kubernetes repairs

This commit is contained in:
Your Name
2026-07-18 21:55:52 +08:00
parent 37d743295e
commit 83fe799f50
12 changed files with 3719 additions and 122 deletions

View File

@@ -594,6 +594,19 @@ class MCPBridge:
# Kubernetes: 使用真實 ActionExecutor
# =============================================
if server.name == "kubernetes":
if tool_name in {
"kubectl_delete",
"kubectl_scale",
"kubectl_restart",
"kubectl_rollout_undo",
}:
return {
"error": (
"kubernetes_controlled_executor:"
"provider_required_no_raw_mutation_fallback"
),
"runtime_write_performed": False,
}
from src.services.executor import get_executor
executor = get_executor()
@@ -609,69 +622,6 @@ class MCPBridge:
return result.k8s_response.get("stdout", "")
return {"error": result.error}
elif tool_name == "kubectl_delete":
namespace = parameters.get("namespace", settings.AWOOOI_K8S_NAMESPACE)
resource = parameters.get("resource", "pod")
name = parameters.get("name", "")
if not name:
return {"error": "Missing 'name' parameter"}
# Dry-run 驗證
if resource == "pod":
dry_run = await executor.validate_pod_exists(name, namespace)
else:
dry_run = await executor.validate_deployment_exists(name, namespace)
if not dry_run.passed:
return {"error": dry_run.message, "dry_run": False}
# 執行刪除
if resource == "pod":
result = await executor.delete_pod(name, namespace)
else:
# deployment 不支援直接刪除,改用 restart
return {"error": "Direct deployment deletion not supported, use restart"}
return {
"success": result.success,
"message": result.message,
"duration_ms": result.duration_ms,
}
elif tool_name == "kubectl_scale":
namespace = parameters.get("namespace", settings.AWOOOI_K8S_NAMESPACE)
deployment = parameters.get("deployment", "")
replicas = parameters.get("replicas", 1)
if not deployment:
return {"error": "Missing 'deployment' parameter"}
cmd = f"kubectl scale deployment/{deployment} --replicas={replicas} -n {namespace}"
result = await executor.execute_kubectl_command(cmd)
return {
"success": result.success,
"scaled": result.success,
"replicas": replicas,
"message": result.message,
}
elif tool_name == "kubectl_restart":
namespace = parameters.get("namespace", settings.AWOOOI_K8S_NAMESPACE)
deployment = parameters.get("deployment", "")
if not deployment:
return {"error": "Missing 'deployment' parameter"}
dry_run = await executor.validate_deployment_exists(deployment, namespace)
if not dry_run.passed:
return {"error": dry_run.message, "dry_run": False}
result = await executor.restart_deployment(deployment, namespace)
return {
"success": result.success,
"restarted": result.success,
"message": result.message,
"duration_ms": result.duration_ms,
}
else:
return {"error": f"Unknown kubernetes tool: {tool_name}"}

View File

@@ -67,6 +67,7 @@ class K8sProvider(MCPToolProvider):
def __init__(self) -> None:
# Lazy import to avoid circular dependency
self._executor = None
self._rollout_verifier = None
@property
def name(self) -> str:
@@ -261,6 +262,56 @@ class K8sProvider(MCPToolProvider):
executor = self._get_executor()
try:
if tool_name in {
"kubectl_delete",
"kubectl_scale",
"kubectl_restart",
"kubectl_rollout_undo",
}:
from src.services.kubernetes_controlled_executor import (
validate_kubernetes_callback_execution_capability,
)
claim = parameters.get("_controlled_execution_claim")
capability = parameters.get("_controlled_execution_capability")
claim_valid, claim_reason = (
validate_kubernetes_callback_execution_capability(
tool_name=tool_name,
parameters=parameters,
claim=claim if isinstance(claim, dict) else None,
capability=capability,
)
)
if not claim_valid:
return MCPToolResult(
success=False,
execution_id=execution_id,
error=f"kubernetes_controlled_executor:{claim_reason}",
)
if tool_name == "kubectl_restart":
from src.services.kubernetes_controlled_executor import (
execute_kubernetes_controlled_claim,
)
controlled_result = await execute_kubernetes_controlled_claim(
claim,
executor=executor,
verifier=self._rollout_verifier,
)
output = {
"success": controlled_result.success,
"restarted": controlled_result.success,
"message": controlled_result.message,
"duration_ms": controlled_result.duration_ms,
"controlled_receipt": controlled_result.k8s_response,
}
return MCPToolResult(
success=controlled_result.success,
execution_id=execution_id,
output=output,
error=controlled_result.error,
)
if tool_name == "kubectl_get":
output = await self._kubectl_get(executor, parameters)
elif tool_name == "kubectl_delete":
@@ -290,6 +341,17 @@ class K8sProvider(MCPToolProvider):
error=f"Unknown tool: {tool_name}",
)
output_error = output.get("error") if isinstance(output, dict) else None
output_failed = (
isinstance(output, dict) and output.get("success") is False
)
if output_error or output_failed:
return MCPToolResult(
success=False,
execution_id=execution_id,
output=output,
error=str(output_error or "kubernetes_tool_execution_failed"),
)
return MCPToolResult(
success=True,
execution_id=execution_id,
@@ -317,27 +379,23 @@ class K8sProvider(MCPToolProvider):
return {"error": result.error}
async def _kubectl_delete(self, executor, parameters: dict) -> dict:
namespace = parameters.get("namespace", "awoooi-prod")
namespace = _validate_namespace(
str(parameters.get("namespace", DEFAULT_NAMESPACE))
)
resource = parameters.get("resource", "pod")
name = parameters.get("name", "")
name = _validate_name(str(parameters.get("name", "")))
if not name:
return {"error": "Missing 'name' parameter"}
if resource != "pod":
return {"error": "Only exact pod deletion is supported"}
# Dry-run validation
if resource == "pod":
dry_run = await executor.validate_pod_exists(name, namespace)
else:
dry_run = await executor.validate_deployment_exists(name, namespace)
dry_run = await executor.validate_pod_exists(name, namespace)
if not dry_run.passed:
return {"error": dry_run.message, "dry_run": False}
# Execute deletion
if resource == "pod":
result = await executor.delete_pod(name, namespace)
else:
return {"error": "Direct deployment deletion not supported, use restart"}
result = await executor.delete_pod(name, namespace)
return {
"success": result.success,
@@ -346,11 +404,13 @@ class K8sProvider(MCPToolProvider):
}
async def _kubectl_scale(self, executor, parameters: dict) -> dict:
namespace = parameters.get("namespace", "awoooi-prod")
deployment = parameters.get("deployment", "")
if not deployment:
return {"error": "Missing 'deployment' parameter"}
namespace = _validate_namespace(
str(parameters.get("namespace", DEFAULT_NAMESPACE))
)
deployment = _validate_name(
str(parameters.get("deployment", "")),
"deployment",
)
# 2026-04-27 ogt + Claude Sonnet 4.6: 補 dry-run 驗證,與 _kubectl_restart 對齊
# 根因_kubectl_scale 缺 validate_deployment_exists → giteadocker-compose 服務)
@@ -381,11 +441,13 @@ class K8sProvider(MCPToolProvider):
}
async def _kubectl_restart(self, executor, parameters: dict) -> dict:
namespace = parameters.get("namespace", "awoooi-prod")
deployment = parameters.get("deployment", "")
if not deployment:
return {"error": "Missing 'deployment' parameter"}
namespace = _validate_namespace(
str(parameters.get("namespace", DEFAULT_NAMESPACE))
)
deployment = _validate_name(
str(parameters.get("deployment", "")),
"deployment",
)
dry_run = await executor.validate_deployment_exists(deployment, namespace)
if not dry_run.passed:
@@ -401,11 +463,13 @@ class K8sProvider(MCPToolProvider):
}
async def _kubectl_rollout_undo(self, executor, parameters: dict) -> dict:
namespace = parameters.get("namespace", "awoooi-prod")
deployment = parameters.get("deployment", "")
if not deployment:
return {"error": "Missing 'deployment' parameter"}
namespace = _validate_namespace(
str(parameters.get("namespace", DEFAULT_NAMESPACE))
)
deployment = _validate_name(
str(parameters.get("deployment", "")),
"deployment",
)
dry_run = await executor.validate_deployment_exists(deployment, namespace)
if not dry_run.passed:

View File

@@ -16,6 +16,10 @@ from src.plugins.mcp.interfaces import MCPTool, MCPToolProvider, MCPToolResult
logger = structlog.get_logger(__name__)
_EPHEMERAL_PROVIDER_PARAMETER_KEYS = frozenset({
"_controlled_execution_capability",
})
class AuditedMCPToolProvider(MCPToolProvider):
"""Provider wrapper that writes every MCP tool call to the audit subsystem."""
@@ -52,6 +56,11 @@ class AuditedMCPToolProvider(MCPToolProvider):
key: value for key, value in parameters.items()
if key != "_mcp_audit"
}
audit_input_parameters = {
key: value
for key, value in parameters.items()
if key not in _EPHEMERAL_PROVIDER_PARAMETER_KEYS
}
started = monotonic_ms()
result: MCPToolResult | None = None
try:
@@ -66,7 +75,7 @@ class AuditedMCPToolProvider(MCPToolProvider):
await record_mcp_call(
mcp_server=self.name,
tool_name=tool_name,
input_params=parameters,
input_params=audit_input_parameters,
output_result=result.output if result else None,
duration_ms=duration_ms,
success=bool(result.success) if result else False,