feat(phase6-9): Complete modular architecture and Agent Teams
Phase 6.4 - Modular Architecture: - Add lewooogo-brain adapters for LLM providers - Add lewooogo-data dual memory (Redis + PostgreSQL) - Implement consensus engine for multi-agent decisions - Add incident memory service for historical context Phase 9 - Agent Teams (Claude Agent SDK): - Add base agent class with Claude Sonnet 4 integration - Implement action planner, blast radius, and security agents - Add agent API endpoints and proposal workflow - Integrate ADR-009 OpenClaw Agent Teams architecture DevOps & CI/CD: - Add GitHub Actions CI/CD workflows (ci.yaml, cd.yaml) - Add pre-commit hooks and secrets baseline - Add docker-compose for local development - Update Kubernetes network policies Frontend Improvements: - Add auto-healing error boundary component - Update i18n messages for agent features - Enhance dual-state incident card with execution feedback Documentation: - Add 7 ADRs covering MCP, design system, architecture decisions - Update ARCHITECTURE_MEMORY.md with modular design - Add GLOBAL_RULES.md and SOUL.md for project identity Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
29
apps/api/src/agents/__init__.py
Normal file
29
apps/api/src/agents/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
AWOOOI Agent Teams - Phase 9.3
|
||||
==============================
|
||||
|
||||
三個專家 Agent 實作,使用 Claude Agent SDK (ADR-009)
|
||||
|
||||
Agents:
|
||||
- SecurityAgent: 安全風險評估 (Risk Score 0-10)
|
||||
- BlastRadiusAgent: 影響範圍分析 (low/medium/high/critical)
|
||||
- ActionPlannerAgent: 執行計畫生成 (ActionPlan + Rollback)
|
||||
|
||||
符合 leWOOOgo BRAIN 積木介面
|
||||
"""
|
||||
|
||||
from src.agents.base import BaseAgent, AgentResult
|
||||
from src.agents.security import SecurityAgent, SecurityResult
|
||||
from src.agents.blast_radius import BlastRadiusAgent, BlastRadiusResult
|
||||
from src.agents.action_planner import ActionPlannerAgent, ActionPlan
|
||||
|
||||
__all__ = [
|
||||
"BaseAgent",
|
||||
"AgentResult",
|
||||
"SecurityAgent",
|
||||
"SecurityResult",
|
||||
"BlastRadiusAgent",
|
||||
"BlastRadiusResult",
|
||||
"ActionPlannerAgent",
|
||||
"ActionPlan",
|
||||
]
|
||||
570
apps/api/src/agents/action_planner.py
Normal file
570
apps/api/src/agents/action_planner.py
Normal file
@@ -0,0 +1,570 @@
|
||||
"""
|
||||
Action Planner Agent - 執行計畫生成專家
|
||||
========================================
|
||||
|
||||
職責:
|
||||
- 生成結構化執行計畫
|
||||
- 定義 rollback 策略
|
||||
- 設定驗證步驟
|
||||
- 回傳完整 ActionPlan
|
||||
|
||||
符合 ADR-009 ActionPlannerAgent 規範
|
||||
"""
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
|
||||
from src.agents.base import AgentResult, AgentStatus, BaseAgent
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Action Plan Types
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ActionType(str, Enum):
|
||||
"""執行動作類型"""
|
||||
RESTART = "restart" # 重啟服務
|
||||
SCALE = "scale" # 擴縮容
|
||||
ROLLBACK = "rollback" # 回滾版本
|
||||
DELETE = "delete" # 刪除資源
|
||||
PATCH = "patch" # 修補配置
|
||||
EXEC = "exec" # 執行指令
|
||||
APPLY = "apply" # 應用變更
|
||||
CUSTOM = "custom" # 自訂
|
||||
|
||||
|
||||
class ActionPhase(str, Enum):
|
||||
"""執行階段"""
|
||||
PRE_CHECK = "pre_check" # 前置檢查
|
||||
EXECUTE = "execute" # 主要執行
|
||||
VERIFY = "verify" # 驗證結果
|
||||
ROLLBACK = "rollback" # 回滾 (如果失敗)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionStep:
|
||||
"""
|
||||
單一執行步驟
|
||||
|
||||
包含:
|
||||
- command: 要執行的指令
|
||||
- description: 步驟說明
|
||||
- phase: 執行階段
|
||||
- timeout_sec: 超時時間
|
||||
- can_fail: 是否允許失敗
|
||||
"""
|
||||
command: str
|
||||
description: str
|
||||
phase: ActionPhase
|
||||
timeout_sec: int = 60
|
||||
can_fail: bool = False
|
||||
order: int = 0
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"command": self.command,
|
||||
"description": self.description,
|
||||
"phase": self.phase.value,
|
||||
"timeout_sec": self.timeout_sec,
|
||||
"can_fail": self.can_fail,
|
||||
"order": self.order,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionPlan(AgentResult):
|
||||
"""
|
||||
ActionPlannerAgent 分析結果
|
||||
|
||||
完整的執行計畫,包含:
|
||||
- action_type: 動作類型
|
||||
- pre_check_steps: 前置檢查
|
||||
- execute_steps: 主要執行步驟
|
||||
- verify_steps: 驗證步驟
|
||||
- rollback_steps: 回滾步驟
|
||||
- estimated_duration: 預估執行時間
|
||||
"""
|
||||
action_type: ActionType = ActionType.CUSTOM
|
||||
pre_check_steps: list[ActionStep] = field(default_factory=list)
|
||||
execute_steps: list[ActionStep] = field(default_factory=list)
|
||||
verify_steps: list[ActionStep] = field(default_factory=list)
|
||||
rollback_steps: list[ActionStep] = field(default_factory=list)
|
||||
estimated_duration_sec: int = 0
|
||||
requires_approval: bool = True
|
||||
kubectl_commands: list[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""轉換為 dict"""
|
||||
base = super().to_dict()
|
||||
base.update({
|
||||
"action_type": self.action_type.value,
|
||||
"pre_check_steps": [s.to_dict() for s in self.pre_check_steps],
|
||||
"execute_steps": [s.to_dict() for s in self.execute_steps],
|
||||
"verify_steps": [s.to_dict() for s in self.verify_steps],
|
||||
"rollback_steps": [s.to_dict() for s in self.rollback_steps],
|
||||
"estimated_duration_sec": self.estimated_duration_sec,
|
||||
"requires_approval": self.requires_approval,
|
||||
"kubectl_commands": self.kubectl_commands,
|
||||
})
|
||||
return base
|
||||
|
||||
def get_all_steps(self) -> list[ActionStep]:
|
||||
"""取得所有步驟 (按順序)"""
|
||||
all_steps = (
|
||||
self.pre_check_steps
|
||||
+ self.execute_steps
|
||||
+ self.verify_steps
|
||||
)
|
||||
return sorted(all_steps, key=lambda s: s.order)
|
||||
|
||||
def get_primary_command(self) -> str | None:
|
||||
"""取得主要執行指令"""
|
||||
if self.execute_steps:
|
||||
return self.execute_steps[0].command
|
||||
return None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Action Templates
|
||||
# =============================================================================
|
||||
|
||||
|
||||
# 預定義的執行計畫模板
|
||||
ACTION_TEMPLATES: dict[str, dict[str, Any]] = {
|
||||
"restart": {
|
||||
"action_type": ActionType.RESTART,
|
||||
"requires_approval": False, # 重啟相對安全
|
||||
"pre_check": [
|
||||
{
|
||||
"command": "kubectl get deployment {target} -n {namespace} -o wide",
|
||||
"description": "確認目標 Deployment 存在且健康",
|
||||
},
|
||||
{
|
||||
"command": "kubectl get pods -l app={target} -n {namespace} --no-headers | wc -l",
|
||||
"description": "確認目前 Pod 數量",
|
||||
},
|
||||
],
|
||||
"execute": [
|
||||
{
|
||||
"command": "kubectl rollout restart deployment/{target} -n {namespace}",
|
||||
"description": "執行滾動重啟",
|
||||
},
|
||||
],
|
||||
"verify": [
|
||||
{
|
||||
"command": "kubectl rollout status deployment/{target} -n {namespace} --timeout=120s",
|
||||
"description": "等待滾動更新完成",
|
||||
"timeout_sec": 120,
|
||||
},
|
||||
{
|
||||
"command": "kubectl get pods -l app={target} -n {namespace} -o wide",
|
||||
"description": "確認新 Pod 狀態",
|
||||
},
|
||||
],
|
||||
"rollback": [
|
||||
{
|
||||
"command": "kubectl rollout undo deployment/{target} -n {namespace}",
|
||||
"description": "回滾到上一個版本",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
"scale": {
|
||||
"action_type": ActionType.SCALE,
|
||||
"requires_approval": False,
|
||||
"pre_check": [
|
||||
{
|
||||
"command": "kubectl get deployment {target} -n {namespace} -o jsonpath='{.spec.replicas}'",
|
||||
"description": "記錄目前副本數",
|
||||
},
|
||||
],
|
||||
"execute": [
|
||||
{
|
||||
"command": "kubectl scale deployment/{target} --replicas={replicas} -n {namespace}",
|
||||
"description": "調整副本數至 {replicas}",
|
||||
},
|
||||
],
|
||||
"verify": [
|
||||
{
|
||||
"command": "kubectl rollout status deployment/{target} -n {namespace} --timeout=60s",
|
||||
"description": "等待擴縮容完成",
|
||||
"timeout_sec": 60,
|
||||
},
|
||||
],
|
||||
"rollback": [
|
||||
{
|
||||
"command": "kubectl scale deployment/{target} --replicas={original_replicas} -n {namespace}",
|
||||
"description": "恢復原始副本數",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
"rollback": {
|
||||
"action_type": ActionType.ROLLBACK,
|
||||
"requires_approval": True, # 回滾需要審核
|
||||
"pre_check": [
|
||||
{
|
||||
"command": "kubectl rollout history deployment/{target} -n {namespace}",
|
||||
"description": "查看版本歷史",
|
||||
},
|
||||
],
|
||||
"execute": [
|
||||
{
|
||||
"command": "kubectl rollout undo deployment/{target} -n {namespace}",
|
||||
"description": "回滾到上一個版本",
|
||||
},
|
||||
],
|
||||
"verify": [
|
||||
{
|
||||
"command": "kubectl rollout status deployment/{target} -n {namespace} --timeout=120s",
|
||||
"description": "等待回滾完成",
|
||||
"timeout_sec": 120,
|
||||
},
|
||||
{
|
||||
"command": "kubectl get pods -l app={target} -n {namespace} -o wide",
|
||||
"description": "確認 Pod 狀態",
|
||||
},
|
||||
],
|
||||
"rollback": [
|
||||
{
|
||||
"command": "kubectl rollout undo deployment/{target} -n {namespace}",
|
||||
"description": "再次回滾 (恢復原版本)",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
"delete_pod": {
|
||||
"action_type": ActionType.DELETE,
|
||||
"requires_approval": True, # 刪除需要審核
|
||||
"pre_check": [
|
||||
{
|
||||
"command": "kubectl get pod {target} -n {namespace} -o wide",
|
||||
"description": "確認目標 Pod 存在",
|
||||
},
|
||||
],
|
||||
"execute": [
|
||||
{
|
||||
"command": "kubectl delete pod {target} -n {namespace}",
|
||||
"description": "刪除異常 Pod (觸發重建)",
|
||||
},
|
||||
],
|
||||
"verify": [
|
||||
{
|
||||
"command": "kubectl get pods -n {namespace} | grep -v Completed | grep -v Terminating",
|
||||
"description": "確認新 Pod 已建立",
|
||||
"can_fail": True,
|
||||
},
|
||||
],
|
||||
"rollback": [], # 刪除 Pod 無法回滾,但 Deployment 會自動重建
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ActionPlannerAgent(BaseAgent[ActionPlan]):
|
||||
"""
|
||||
執行計畫生成專家 Agent
|
||||
|
||||
分析流程:
|
||||
1. 解析輸入的問題/指令
|
||||
2. 匹配最佳執行模板
|
||||
3. 填充參數生成完整計畫
|
||||
4. 計算預估執行時間
|
||||
|
||||
使用方式:
|
||||
```python
|
||||
agent = ActionPlannerAgent()
|
||||
result = await agent.analyze({
|
||||
"problem": "Pod 頻繁重啟",
|
||||
"target_service": "api",
|
||||
"namespace": "awoooi-prod",
|
||||
})
|
||||
print(result.execute_steps) # [ActionStep(...), ...]
|
||||
```
|
||||
"""
|
||||
|
||||
AGENT_NAME = "action-planner"
|
||||
AGENT_DESCRIPTION = "行動規劃師,制定修復步驟與回滾方案"
|
||||
AGENT_TOOLS = ["Read", "Glob"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
timeout_sec: float = 30.0,
|
||||
default_namespace: str = "awoooi-prod",
|
||||
):
|
||||
"""
|
||||
初始化 ActionPlannerAgent
|
||||
|
||||
Args:
|
||||
timeout_sec: 執行超時時間
|
||||
default_namespace: 預設命名空間
|
||||
"""
|
||||
super().__init__(timeout_sec)
|
||||
self.default_namespace = default_namespace
|
||||
|
||||
async def analyze(self, context: dict[str, Any]) -> ActionPlan:
|
||||
"""
|
||||
生成執行計畫
|
||||
|
||||
Args:
|
||||
context: 分析上下文
|
||||
- problem: 問題描述
|
||||
- suggested_action: 建議的動作 (restart/scale/rollback)
|
||||
- target_service: 目標服務
|
||||
- namespace: 命名空間
|
||||
- replicas: 副本數 (scale 用)
|
||||
|
||||
Returns:
|
||||
ActionPlan 包含完整執行計畫
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
self.logger.info(
|
||||
"action_planning_start",
|
||||
problem=context.get("problem", "")[:100],
|
||||
target=context.get("target_service"),
|
||||
)
|
||||
|
||||
try:
|
||||
# 1. 決定動作類型
|
||||
action_type = self._determine_action_type(context)
|
||||
|
||||
# 2. 取得模板
|
||||
template = ACTION_TEMPLATES.get(action_type, ACTION_TEMPLATES["restart"])
|
||||
|
||||
# 3. 準備參數
|
||||
params = self._prepare_params(context)
|
||||
|
||||
# 4. 生成步驟
|
||||
pre_check_steps = self._generate_steps(
|
||||
template.get("pre_check", []),
|
||||
params,
|
||||
ActionPhase.PRE_CHECK,
|
||||
)
|
||||
|
||||
execute_steps = self._generate_steps(
|
||||
template.get("execute", []),
|
||||
params,
|
||||
ActionPhase.EXECUTE,
|
||||
)
|
||||
|
||||
verify_steps = self._generate_steps(
|
||||
template.get("verify", []),
|
||||
params,
|
||||
ActionPhase.VERIFY,
|
||||
)
|
||||
|
||||
rollback_steps = self._generate_steps(
|
||||
template.get("rollback", []),
|
||||
params,
|
||||
ActionPhase.ROLLBACK,
|
||||
)
|
||||
|
||||
# 5. 計算預估時間
|
||||
estimated_duration = self._estimate_duration(
|
||||
pre_check_steps + execute_steps + verify_steps
|
||||
)
|
||||
|
||||
# 6. 提取主要 kubectl 指令
|
||||
kubectl_commands = [
|
||||
step.command for step in execute_steps
|
||||
if step.command.startswith("kubectl")
|
||||
]
|
||||
|
||||
latency_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
# 7. 生成分析摘要
|
||||
analysis = self._generate_analysis(
|
||||
template["action_type"],
|
||||
params.get("target", "unknown"),
|
||||
len(execute_steps),
|
||||
)
|
||||
|
||||
result = ActionPlan(
|
||||
agent_name=self.AGENT_NAME,
|
||||
status=AgentStatus.SUCCESS,
|
||||
confidence=0.9,
|
||||
analysis=analysis,
|
||||
latency_ms=latency_ms,
|
||||
action_type=template["action_type"],
|
||||
pre_check_steps=pre_check_steps,
|
||||
execute_steps=execute_steps,
|
||||
verify_steps=verify_steps,
|
||||
rollback_steps=rollback_steps,
|
||||
estimated_duration_sec=estimated_duration,
|
||||
requires_approval=template.get("requires_approval", True),
|
||||
kubectl_commands=kubectl_commands,
|
||||
)
|
||||
|
||||
self.logger.info(
|
||||
"action_planning_complete",
|
||||
action_type=result.action_type.value,
|
||||
step_count=len(execute_steps),
|
||||
latency_ms=latency_ms,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
latency_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
self.logger.exception(
|
||||
"action_planning_error",
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
return ActionPlan(
|
||||
agent_name=self.AGENT_NAME,
|
||||
status=AgentStatus.FAILED,
|
||||
confidence=0.0,
|
||||
analysis=f"計畫生成失敗: {str(e)}",
|
||||
latency_ms=latency_ms,
|
||||
error=str(e),
|
||||
requires_approval=True,
|
||||
)
|
||||
|
||||
def _determine_action_type(self, context: dict[str, Any]) -> str:
|
||||
"""
|
||||
根據上下文決定最佳動作類型
|
||||
|
||||
解析 problem 或 suggested_action 來決定
|
||||
"""
|
||||
# 如果有明確指定
|
||||
suggested = context.get("suggested_action", "").lower()
|
||||
if suggested in ACTION_TEMPLATES:
|
||||
return suggested
|
||||
|
||||
# 從 problem 推斷
|
||||
problem = context.get("problem", "").lower()
|
||||
|
||||
# 關鍵字匹配
|
||||
if any(kw in problem for kw in ["crash", "restart", "oom", "killed"]):
|
||||
return "restart"
|
||||
|
||||
if any(kw in problem for kw in ["slow", "latency", "capacity", "scale"]):
|
||||
return "scale"
|
||||
|
||||
if any(kw in problem for kw in ["error", "failed", "rollback", "undo"]):
|
||||
return "rollback"
|
||||
|
||||
if any(kw in problem for kw in ["stuck", "pending", "delete pod"]):
|
||||
return "delete_pod"
|
||||
|
||||
# 預設: 重啟 (最安全)
|
||||
return "restart"
|
||||
|
||||
def _prepare_params(self, context: dict[str, Any]) -> dict[str, str]:
|
||||
"""準備模板參數"""
|
||||
target = context.get("target_service", "unknown")
|
||||
namespace = context.get("namespace", self.default_namespace)
|
||||
|
||||
# 處理 target 可能是列表的情況
|
||||
if isinstance(target, list):
|
||||
target = target[0] if target else "unknown"
|
||||
|
||||
return {
|
||||
"target": target,
|
||||
"namespace": namespace,
|
||||
"replicas": str(context.get("replicas", 3)),
|
||||
"original_replicas": str(context.get("original_replicas", 1)),
|
||||
}
|
||||
|
||||
def _generate_steps(
|
||||
self,
|
||||
template_steps: list[dict[str, Any]],
|
||||
params: dict[str, str],
|
||||
phase: ActionPhase,
|
||||
) -> list[ActionStep]:
|
||||
"""從模板生成實際步驟"""
|
||||
steps: list[ActionStep] = []
|
||||
|
||||
for i, tmpl in enumerate(template_steps):
|
||||
command = tmpl["command"].format(**params)
|
||||
description = tmpl["description"].format(**params)
|
||||
|
||||
steps.append(ActionStep(
|
||||
command=command,
|
||||
description=description,
|
||||
phase=phase,
|
||||
timeout_sec=tmpl.get("timeout_sec", 60),
|
||||
can_fail=tmpl.get("can_fail", False),
|
||||
order=i,
|
||||
))
|
||||
|
||||
return steps
|
||||
|
||||
def _estimate_duration(self, steps: list[ActionStep]) -> int:
|
||||
"""估計執行時間 (秒)"""
|
||||
total = 0
|
||||
for step in steps:
|
||||
# 假設每個步驟平均執行時間為 timeout 的 1/3
|
||||
total += step.timeout_sec // 3
|
||||
return max(total, 30) # 最少 30 秒
|
||||
|
||||
def _generate_analysis(
|
||||
self,
|
||||
action_type: ActionType,
|
||||
target: str,
|
||||
step_count: int,
|
||||
) -> str:
|
||||
"""生成分析摘要"""
|
||||
action_desc = {
|
||||
ActionType.RESTART: "滾動重啟",
|
||||
ActionType.SCALE: "擴縮容",
|
||||
ActionType.ROLLBACK: "版本回滾",
|
||||
ActionType.DELETE: "資源清理",
|
||||
ActionType.PATCH: "配置修補",
|
||||
ActionType.APPLY: "配置應用",
|
||||
ActionType.EXEC: "指令執行",
|
||||
ActionType.CUSTOM: "自訂操作",
|
||||
}
|
||||
|
||||
return (
|
||||
f"建議執行 {action_desc.get(action_type, '操作')} "
|
||||
f"於 {target},共 {step_count} 個步驟"
|
||||
)
|
||||
|
||||
def _build_prompt(self, context: dict[str, Any]) -> str:
|
||||
"""建構 LLM Prompt (Phase 9.4 擴展)"""
|
||||
return f"""你是 AWOOOI 的行動規劃師。
|
||||
根據以下問題制定修復計畫:
|
||||
|
||||
問題描述: {context.get("problem", "N/A")}
|
||||
目標服務: {context.get("target_service", "N/A")}
|
||||
命名空間: {context.get("namespace", "awoooi-prod")}
|
||||
|
||||
注意:
|
||||
- 所有 kubectl 必須帶 -n {{namespace}}
|
||||
- 必須包含前置檢查、執行步驟、驗證步驟、回滾方案
|
||||
|
||||
輸出 JSON:
|
||||
```json
|
||||
{{
|
||||
"action_type": "restart|scale|rollback|delete",
|
||||
"pre_check_steps": [
|
||||
{{"command": "kubectl ...", "description": "..."}}
|
||||
],
|
||||
"execute_steps": [
|
||||
{{"command": "kubectl ...", "description": "..."}}
|
||||
],
|
||||
"verify_steps": [
|
||||
{{"command": "kubectl ...", "description": "..."}}
|
||||
],
|
||||
"rollback_steps": [
|
||||
{{"command": "kubectl ...", "description": "..."}}
|
||||
],
|
||||
"estimated_duration_sec": 60,
|
||||
"analysis": "一句話摘要",
|
||||
"confidence": 0-1
|
||||
}}
|
||||
```"""
|
||||
|
||||
def _parse_response(self, response: str) -> dict[str, Any]:
|
||||
"""解析 LLM 回應"""
|
||||
return self._extract_json(response)
|
||||
192
apps/api/src/agents/base.py
Normal file
192
apps/api/src/agents/base.py
Normal file
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
Base Agent - 專家 Agent 基礎類別
|
||||
================================
|
||||
|
||||
定義所有專家 Agent 的共用介面和工具
|
||||
|
||||
使用 claude-agent-sdk 的 AgentDefinition
|
||||
符合 ADR-009 架構規範
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Agent Result Base
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class AgentStatus(str, Enum):
|
||||
"""Agent 執行狀態"""
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
SUCCESS = "success"
|
||||
FAILED = "failed"
|
||||
TIMEOUT = "timeout"
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentResult:
|
||||
"""
|
||||
Agent 執行結果基類
|
||||
|
||||
所有專家 Agent 的輸出都必須包含:
|
||||
- agent_name: 識別哪個 Agent
|
||||
- status: 執行狀態
|
||||
- confidence: 信心分數 (0-1)
|
||||
- analysis: 分析摘要
|
||||
- latency_ms: 執行時間
|
||||
"""
|
||||
agent_name: str
|
||||
status: AgentStatus
|
||||
confidence: float
|
||||
analysis: str
|
||||
latency_ms: int
|
||||
error: str | None = None
|
||||
raw_response: dict[str, Any] = field(default_factory=dict)
|
||||
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""轉換為 dict (API 回傳用)"""
|
||||
return {
|
||||
"agent_name": self.agent_name,
|
||||
"status": self.status.value,
|
||||
"confidence": self.confidence,
|
||||
"analysis": self.analysis,
|
||||
"latency_ms": self.latency_ms,
|
||||
"error": self.error,
|
||||
"timestamp": self.timestamp.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Base Agent
|
||||
# =============================================================================
|
||||
|
||||
T = TypeVar("T", bound=AgentResult)
|
||||
|
||||
|
||||
class BaseAgent(ABC, Generic[T]):
|
||||
"""
|
||||
專家 Agent 基礎類別
|
||||
|
||||
所有專家 Agent 都繼承此類別,並實作:
|
||||
- analyze(): 核心分析邏輯
|
||||
- _build_prompt(): 建構 Prompt
|
||||
- _parse_response(): 解析回應
|
||||
|
||||
使用方式:
|
||||
```python
|
||||
agent = SecurityAgent()
|
||||
result = await agent.analyze(incident_context)
|
||||
```
|
||||
"""
|
||||
|
||||
# Agent 識別資訊 (子類別覆寫)
|
||||
AGENT_NAME: str = "base"
|
||||
AGENT_DESCRIPTION: str = "Base Agent"
|
||||
AGENT_TOOLS: list[str] = ["Read", "Grep"]
|
||||
|
||||
def __init__(self, timeout_sec: float = 30.0):
|
||||
"""
|
||||
初始化 Agent
|
||||
|
||||
Args:
|
||||
timeout_sec: 執行超時時間 (秒)
|
||||
"""
|
||||
self.timeout_sec = timeout_sec
|
||||
self.logger = logger.bind(agent=self.AGENT_NAME)
|
||||
|
||||
@abstractmethod
|
||||
async def analyze(self, context: dict[str, Any]) -> T:
|
||||
"""
|
||||
執行分析 (子類別必須實作)
|
||||
|
||||
Args:
|
||||
context: 分析上下文 (incident 資訊)
|
||||
|
||||
Returns:
|
||||
AgentResult 子類別實例
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _build_prompt(self, context: dict[str, Any]) -> str:
|
||||
"""
|
||||
建構 Prompt (子類別必須實作)
|
||||
|
||||
Args:
|
||||
context: 分析上下文
|
||||
|
||||
Returns:
|
||||
給 LLM 的 Prompt
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _parse_response(self, response: str) -> dict[str, Any]:
|
||||
"""
|
||||
解析 LLM 回應 (子類別必須實作)
|
||||
|
||||
Args:
|
||||
response: LLM 原始回應
|
||||
|
||||
Returns:
|
||||
解析後的結構化資料
|
||||
"""
|
||||
pass
|
||||
|
||||
def _extract_json(self, text: str) -> dict[str, Any]:
|
||||
"""
|
||||
從 LLM 回應中提取 JSON
|
||||
|
||||
支援:
|
||||
- ```json ... ``` 區塊
|
||||
- 純 JSON 文字
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
|
||||
# 嘗試 ```json ... ``` 格式
|
||||
match = re.search(r"```json\s*(.*?)\s*```", text, re.DOTALL)
|
||||
if match:
|
||||
try:
|
||||
return json.loads(match.group(1))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# 嘗試 { ... } 格式
|
||||
match = re.search(r"\{[^{}]*\}", text, re.DOTALL)
|
||||
if match:
|
||||
try:
|
||||
return json.loads(match.group(0))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# 嘗試整段解析
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
self.logger.warning("json_parse_failed", text=text[:200])
|
||||
return {}
|
||||
|
||||
def _get_agent_definition(self) -> dict[str, Any]:
|
||||
"""
|
||||
取得 Claude Agent SDK 的 AgentDefinition
|
||||
|
||||
Returns:
|
||||
符合 SDK 規範的 AgentDefinition dict
|
||||
"""
|
||||
return {
|
||||
"name": self.AGENT_NAME,
|
||||
"description": self.AGENT_DESCRIPTION,
|
||||
"tools": self.AGENT_TOOLS,
|
||||
}
|
||||
525
apps/api/src/agents/blast_radius.py
Normal file
525
apps/api/src/agents/blast_radius.py
Normal file
@@ -0,0 +1,525 @@
|
||||
"""
|
||||
Blast Radius Agent - 影響範圍分析專家
|
||||
======================================
|
||||
|
||||
職責:
|
||||
- 評估操作的影響範圍
|
||||
- 識別受影響的服務和依賴
|
||||
- 估計使用者影響人數
|
||||
- 回傳影響等級 (low/medium/high/critical)
|
||||
|
||||
符合 ADR-009 BlastRadiusAgent 規範
|
||||
"""
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
|
||||
from src.agents.base import AgentResult, AgentStatus, BaseAgent
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Blast Radius Types
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ImpactLevel(str, Enum):
|
||||
"""影響等級"""
|
||||
LOW = "low" # 單一服務,<100 用戶
|
||||
MEDIUM = "medium" # 2-5 服務,100-1000 用戶
|
||||
HIGH = "high" # 5-10 服務,1000-10000 用戶
|
||||
CRITICAL = "critical" # >10 服務,>10000 用戶或核心服務
|
||||
|
||||
|
||||
@dataclass
|
||||
class AffectedService:
|
||||
"""受影響服務"""
|
||||
name: str
|
||||
impact_type: str # direct, indirect, transitive
|
||||
confidence: float
|
||||
reason: str
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"impact_type": self.impact_type,
|
||||
"confidence": self.confidence,
|
||||
"reason": self.reason,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class BlastRadiusResult(AgentResult):
|
||||
"""
|
||||
BlastRadiusAgent 分析結果
|
||||
|
||||
額外欄位:
|
||||
- impact_level: 影響等級 (low/medium/high/critical)
|
||||
- affected_services: 受影響服務列表
|
||||
- estimated_users: 估計影響用戶數
|
||||
- dependency_chain: 依賴鏈
|
||||
- recovery_time_estimate: 預估恢復時間 (分鐘)
|
||||
"""
|
||||
impact_level: ImpactLevel = ImpactLevel.LOW
|
||||
affected_services: list[AffectedService] = field(default_factory=list)
|
||||
estimated_users: int = 0
|
||||
dependency_chain: list[str] = field(default_factory=list)
|
||||
recovery_time_estimate: int = 0
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""轉換為 dict"""
|
||||
base = super().to_dict()
|
||||
base.update({
|
||||
"impact_level": self.impact_level.value,
|
||||
"affected_services": [s.to_dict() for s in self.affected_services],
|
||||
"estimated_users": self.estimated_users,
|
||||
"dependency_chain": self.dependency_chain,
|
||||
"recovery_time_estimate": self.recovery_time_estimate,
|
||||
})
|
||||
return base
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Service Dependency Graph (簡化版)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
# AWOOOI 服務依賴圖 (簡化版,實際應從 GraphRAG 讀取)
|
||||
SERVICE_DEPENDENCIES: dict[str, dict[str, Any]] = {
|
||||
# === Core Services ===
|
||||
"api": {
|
||||
"dependencies": ["postgres", "redis", "openclaw"],
|
||||
"dependents": ["web", "telegram-gateway"],
|
||||
"criticality": "critical",
|
||||
"estimated_users": 5000,
|
||||
},
|
||||
"web": {
|
||||
"dependencies": ["api"],
|
||||
"dependents": [],
|
||||
"criticality": "high",
|
||||
"estimated_users": 3000,
|
||||
},
|
||||
"openclaw": {
|
||||
"dependencies": ["redis", "ollama"],
|
||||
"dependents": ["api"],
|
||||
"criticality": "critical",
|
||||
"estimated_users": 5000,
|
||||
},
|
||||
|
||||
# === Infrastructure ===
|
||||
"postgres": {
|
||||
"dependencies": [],
|
||||
"dependents": ["api", "signoz"],
|
||||
"criticality": "critical",
|
||||
"estimated_users": 10000,
|
||||
},
|
||||
"redis": {
|
||||
"dependencies": [],
|
||||
"dependents": ["api", "openclaw", "signal-worker"],
|
||||
"criticality": "critical",
|
||||
"estimated_users": 8000,
|
||||
},
|
||||
"ollama": {
|
||||
"dependencies": [],
|
||||
"dependents": ["openclaw"],
|
||||
"criticality": "high",
|
||||
"estimated_users": 2000,
|
||||
},
|
||||
|
||||
# === Workers ===
|
||||
"signal-worker": {
|
||||
"dependencies": ["redis", "api"],
|
||||
"dependents": [],
|
||||
"criticality": "medium",
|
||||
"estimated_users": 500,
|
||||
},
|
||||
"telegram-gateway": {
|
||||
"dependencies": ["api"],
|
||||
"dependents": [],
|
||||
"criticality": "medium",
|
||||
"estimated_users": 1000,
|
||||
},
|
||||
|
||||
# === Observability ===
|
||||
"signoz": {
|
||||
"dependencies": ["postgres"],
|
||||
"dependents": [],
|
||||
"criticality": "low",
|
||||
"estimated_users": 100,
|
||||
},
|
||||
"prometheus": {
|
||||
"dependencies": [],
|
||||
"dependents": [],
|
||||
"criticality": "low",
|
||||
"estimated_users": 50,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class BlastRadiusAgent(BaseAgent[BlastRadiusResult]):
|
||||
"""
|
||||
影響範圍分析專家 Agent
|
||||
|
||||
分析流程:
|
||||
1. 識別直接影響的服務
|
||||
2. 遍歷依賴圖找出間接影響
|
||||
3. 計算總影響用戶數
|
||||
4. 判定影響等級
|
||||
|
||||
使用方式:
|
||||
```python
|
||||
agent = BlastRadiusAgent()
|
||||
result = await agent.analyze({
|
||||
"target_service": "api",
|
||||
"action": "kubectl rollout restart",
|
||||
"namespace": "awoooi-prod",
|
||||
})
|
||||
print(result.impact_level) # ImpactLevel.CRITICAL
|
||||
```
|
||||
"""
|
||||
|
||||
AGENT_NAME = "blast-radius"
|
||||
AGENT_DESCRIPTION = "影響範圍分析師,評估相依服務與影響範圍"
|
||||
AGENT_TOOLS = ["Read", "Glob", "Grep"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
timeout_sec: float = 30.0,
|
||||
dependency_graph: dict[str, dict[str, Any]] | None = None,
|
||||
):
|
||||
"""
|
||||
初始化 BlastRadiusAgent
|
||||
|
||||
Args:
|
||||
timeout_sec: 執行超時時間
|
||||
dependency_graph: 自訂依賴圖 (測試用)
|
||||
"""
|
||||
super().__init__(timeout_sec)
|
||||
self.dependency_graph = dependency_graph or SERVICE_DEPENDENCIES
|
||||
|
||||
async def analyze(self, context: dict[str, Any]) -> BlastRadiusResult:
|
||||
"""
|
||||
執行影響範圍分析
|
||||
|
||||
Args:
|
||||
context: 分析上下文
|
||||
- target_service: 目標服務 (可以是列表)
|
||||
- action: 執行的操作
|
||||
- namespace: 命名空間
|
||||
|
||||
Returns:
|
||||
BlastRadiusResult 包含影響等級和詳細分析
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
self.logger.info(
|
||||
"blast_radius_analysis_start",
|
||||
target=context.get("target_service"),
|
||||
action=context.get("action", "")[:50],
|
||||
)
|
||||
|
||||
try:
|
||||
# 取得目標服務列表
|
||||
target_services = context.get("target_service", [])
|
||||
if isinstance(target_services, str):
|
||||
target_services = [target_services]
|
||||
|
||||
# 分析每個目標服務的影響
|
||||
all_affected: list[AffectedService] = []
|
||||
total_users = 0
|
||||
dependency_chain: list[str] = []
|
||||
|
||||
for target in target_services:
|
||||
affected, users, chain = self._analyze_service_impact(target)
|
||||
all_affected.extend(affected)
|
||||
total_users = max(total_users, users) # 取最大值避免重複計算
|
||||
dependency_chain.extend(chain)
|
||||
|
||||
# 去重
|
||||
seen_services = set()
|
||||
unique_affected: list[AffectedService] = []
|
||||
for svc in all_affected:
|
||||
if svc.name not in seen_services:
|
||||
seen_services.add(svc.name)
|
||||
unique_affected.append(svc)
|
||||
|
||||
# 判定影響等級
|
||||
impact_level = self._calculate_impact_level(
|
||||
len(unique_affected),
|
||||
total_users,
|
||||
unique_affected,
|
||||
)
|
||||
|
||||
# 估計恢復時間
|
||||
recovery_time = self._estimate_recovery_time(impact_level, len(unique_affected))
|
||||
|
||||
latency_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
# 生成分析摘要
|
||||
analysis = self._generate_analysis(
|
||||
impact_level,
|
||||
len(unique_affected),
|
||||
total_users,
|
||||
)
|
||||
|
||||
result = BlastRadiusResult(
|
||||
agent_name=self.AGENT_NAME,
|
||||
status=AgentStatus.SUCCESS,
|
||||
confidence=0.85, # 基於依賴圖的信心分數
|
||||
analysis=analysis,
|
||||
latency_ms=latency_ms,
|
||||
impact_level=impact_level,
|
||||
affected_services=unique_affected,
|
||||
estimated_users=total_users,
|
||||
dependency_chain=list(set(dependency_chain)),
|
||||
recovery_time_estimate=recovery_time,
|
||||
)
|
||||
|
||||
self.logger.info(
|
||||
"blast_radius_analysis_complete",
|
||||
impact_level=impact_level.value,
|
||||
affected_count=len(unique_affected),
|
||||
estimated_users=total_users,
|
||||
latency_ms=latency_ms,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
latency_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
self.logger.exception(
|
||||
"blast_radius_analysis_error",
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
return BlastRadiusResult(
|
||||
agent_name=self.AGENT_NAME,
|
||||
status=AgentStatus.FAILED,
|
||||
confidence=0.0,
|
||||
analysis=f"分析失敗: {str(e)}",
|
||||
latency_ms=latency_ms,
|
||||
error=str(e),
|
||||
impact_level=ImpactLevel.CRITICAL, # 失敗時假設最大影響
|
||||
)
|
||||
|
||||
def _analyze_service_impact(
|
||||
self,
|
||||
target_service: str,
|
||||
) -> tuple[list[AffectedService], int, list[str]]:
|
||||
"""
|
||||
分析單一服務的影響
|
||||
|
||||
Returns:
|
||||
(受影響服務列表, 估計用戶數, 依賴鏈)
|
||||
"""
|
||||
affected: list[AffectedService] = []
|
||||
visited: set[str] = set()
|
||||
dependency_chain: list[str] = []
|
||||
total_users = 0
|
||||
|
||||
# 標準化服務名稱
|
||||
target_key = self._normalize_service_name(target_service)
|
||||
|
||||
if target_key not in self.dependency_graph:
|
||||
# 未知服務,假設中等影響
|
||||
affected.append(AffectedService(
|
||||
name=target_service,
|
||||
impact_type="direct",
|
||||
confidence=0.5,
|
||||
reason="未知服務,無法確定依賴關係",
|
||||
))
|
||||
return affected, 1000, [target_service]
|
||||
|
||||
# 1. 直接影響 (目標服務本身)
|
||||
target_info = self.dependency_graph[target_key]
|
||||
affected.append(AffectedService(
|
||||
name=target_key,
|
||||
impact_type="direct",
|
||||
confidence=1.0,
|
||||
reason="目標服務",
|
||||
))
|
||||
total_users += target_info.get("estimated_users", 0)
|
||||
dependency_chain.append(target_key)
|
||||
visited.add(target_key)
|
||||
|
||||
# 2. 依賴此服務的上游 (dependents)
|
||||
self._find_dependents(
|
||||
target_key,
|
||||
affected,
|
||||
visited,
|
||||
dependency_chain,
|
||||
depth=0,
|
||||
max_depth=3,
|
||||
)
|
||||
|
||||
# 計算總用戶數
|
||||
for svc in affected:
|
||||
if svc.name in self.dependency_graph:
|
||||
total_users += self.dependency_graph[svc.name].get("estimated_users", 0)
|
||||
|
||||
return affected, total_users, dependency_chain
|
||||
|
||||
def _find_dependents(
|
||||
self,
|
||||
service: str,
|
||||
affected: list[AffectedService],
|
||||
visited: set[str],
|
||||
chain: list[str],
|
||||
depth: int,
|
||||
max_depth: int,
|
||||
) -> None:
|
||||
"""遞迴查找依賴此服務的上游"""
|
||||
if depth >= max_depth:
|
||||
return
|
||||
|
||||
if service not in self.dependency_graph:
|
||||
return
|
||||
|
||||
dependents = self.dependency_graph[service].get("dependents", [])
|
||||
|
||||
for dep in dependents:
|
||||
if dep in visited:
|
||||
continue
|
||||
|
||||
visited.add(dep)
|
||||
chain.append(dep)
|
||||
|
||||
impact_type = "indirect" if depth == 0 else "transitive"
|
||||
confidence = 0.9 - (depth * 0.1)
|
||||
|
||||
affected.append(AffectedService(
|
||||
name=dep,
|
||||
impact_type=impact_type,
|
||||
confidence=confidence,
|
||||
reason=f"依賴 {service}",
|
||||
))
|
||||
|
||||
# 遞迴查找
|
||||
self._find_dependents(
|
||||
dep,
|
||||
affected,
|
||||
visited,
|
||||
chain,
|
||||
depth + 1,
|
||||
max_depth,
|
||||
)
|
||||
|
||||
def _normalize_service_name(self, service: str) -> str:
|
||||
"""標準化服務名稱"""
|
||||
# 移除常見後綴
|
||||
service = service.lower()
|
||||
for suffix in ["-deployment", "-svc", "-service", "-pod"]:
|
||||
if service.endswith(suffix):
|
||||
service = service[: -len(suffix)]
|
||||
|
||||
# 處理常見別名
|
||||
aliases = {
|
||||
"awoooi-api": "api",
|
||||
"awoooi-web": "web",
|
||||
"nginx": "web",
|
||||
"frontend": "web",
|
||||
"backend": "api",
|
||||
"database": "postgres",
|
||||
"db": "postgres",
|
||||
"cache": "redis",
|
||||
}
|
||||
|
||||
return aliases.get(service, service)
|
||||
|
||||
def _calculate_impact_level(
|
||||
self,
|
||||
service_count: int,
|
||||
user_count: int,
|
||||
affected: list[AffectedService],
|
||||
) -> ImpactLevel:
|
||||
"""計算影響等級"""
|
||||
# 檢查是否有 critical 服務
|
||||
has_critical = any(
|
||||
svc.name in self.dependency_graph
|
||||
and self.dependency_graph[svc.name].get("criticality") == "critical"
|
||||
for svc in affected
|
||||
)
|
||||
|
||||
if has_critical or service_count > 10 or user_count > 10000:
|
||||
return ImpactLevel.CRITICAL
|
||||
|
||||
if service_count > 5 or user_count > 1000:
|
||||
return ImpactLevel.HIGH
|
||||
|
||||
if service_count > 2 or user_count > 100:
|
||||
return ImpactLevel.MEDIUM
|
||||
|
||||
return ImpactLevel.LOW
|
||||
|
||||
def _estimate_recovery_time(
|
||||
self,
|
||||
impact_level: ImpactLevel,
|
||||
service_count: int,
|
||||
) -> int:
|
||||
"""估計恢復時間 (分鐘)"""
|
||||
base_time = {
|
||||
ImpactLevel.LOW: 5,
|
||||
ImpactLevel.MEDIUM: 15,
|
||||
ImpactLevel.HIGH: 30,
|
||||
ImpactLevel.CRITICAL: 60,
|
||||
}
|
||||
|
||||
# 每多一個服務增加 5 分鐘
|
||||
return base_time[impact_level] + (service_count * 5)
|
||||
|
||||
def _generate_analysis(
|
||||
self,
|
||||
impact_level: ImpactLevel,
|
||||
service_count: int,
|
||||
user_count: int,
|
||||
) -> str:
|
||||
"""生成分析摘要"""
|
||||
level_desc = {
|
||||
ImpactLevel.LOW: "低影響",
|
||||
ImpactLevel.MEDIUM: "中等影響",
|
||||
ImpactLevel.HIGH: "高影響",
|
||||
ImpactLevel.CRITICAL: "嚴重影響",
|
||||
}
|
||||
|
||||
return (
|
||||
f"{level_desc[impact_level]}: "
|
||||
f"影響 {service_count} 個服務,預估 {user_count:,} 用戶受影響"
|
||||
)
|
||||
|
||||
def _build_prompt(self, context: dict[str, Any]) -> str:
|
||||
"""建構 LLM Prompt (Phase 9.4 擴展)"""
|
||||
return f"""你是 AWOOOI 的影響範圍分析師。
|
||||
分析以下操作的影響範圍:
|
||||
|
||||
目標服務: {context.get("target_service", "N/A")}
|
||||
操作: {context.get("action", "N/A")}
|
||||
命名空間: {context.get("namespace", "N/A")}
|
||||
|
||||
評估:
|
||||
1. 直接影響的服務
|
||||
2. 間接相依的服務
|
||||
3. 使用者影響人數估計
|
||||
|
||||
輸出 JSON:
|
||||
```json
|
||||
{{
|
||||
"impact_level": "low|medium|high|critical",
|
||||
"affected_services": [
|
||||
{{"name": "...", "impact_type": "direct|indirect", "reason": "..."}}
|
||||
],
|
||||
"estimated_users": 0,
|
||||
"dependency_chain": ["service1", "service2"],
|
||||
"analysis": "一句話摘要",
|
||||
"confidence": 0-1
|
||||
}}
|
||||
```"""
|
||||
|
||||
def _parse_response(self, response: str) -> dict[str, Any]:
|
||||
"""解析 LLM 回應"""
|
||||
return self._extract_json(response)
|
||||
332
apps/api/src/agents/security.py
Normal file
332
apps/api/src/agents/security.py
Normal file
@@ -0,0 +1,332 @@
|
||||
"""
|
||||
Security Agent - 安全風險評估專家
|
||||
=================================
|
||||
|
||||
職責:
|
||||
- 分析提案的安全風險
|
||||
- 檢查權限邊界
|
||||
- 評估潛在漏洞
|
||||
- 回傳風險評分 (0-10)
|
||||
|
||||
符合 ADR-009 SecurityAgent 規範
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
|
||||
from src.agents.base import AgentResult, AgentStatus, BaseAgent
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Security Result
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class SecurityResult(AgentResult):
|
||||
"""
|
||||
SecurityAgent 分析結果
|
||||
|
||||
額外欄位:
|
||||
- risk_score: 風險評分 (0-10, 10 最高風險)
|
||||
- risk_factors: 風險因素列表
|
||||
- permission_issues: 權限問題
|
||||
- recommendations: 安全建議
|
||||
"""
|
||||
risk_score: float = 0.0
|
||||
risk_factors: list[str] = field(default_factory=list)
|
||||
permission_issues: list[str] = field(default_factory=list)
|
||||
recommendations: list[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""轉換為 dict"""
|
||||
base = super().to_dict()
|
||||
base.update({
|
||||
"risk_score": self.risk_score,
|
||||
"risk_factors": self.risk_factors,
|
||||
"permission_issues": self.permission_issues,
|
||||
"recommendations": self.recommendations,
|
||||
})
|
||||
return base
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Security Agent
|
||||
# =============================================================================
|
||||
|
||||
|
||||
# 安全規則引擎 (本地快速檢查)
|
||||
SECURITY_RULES: dict[str, dict[str, Any]] = {
|
||||
"delete_operation": {
|
||||
"patterns": ["delete", "rm", "remove", "destroy", "drop"],
|
||||
"risk_score": 8.0,
|
||||
"factor": "破壞性操作: 涉及刪除資源",
|
||||
"recommendation": "確保有備份,並考慮使用 --dry-run 先行測試",
|
||||
},
|
||||
"force_operation": {
|
||||
"patterns": ["--force", "-f", "--no-wait", "--grace-period=0"],
|
||||
"risk_score": 7.0,
|
||||
"factor": "強制操作: 跳過安全確認",
|
||||
"recommendation": "移除 --force 參數,使用標準流程",
|
||||
},
|
||||
"privileged_namespace": {
|
||||
"patterns": ["kube-system", "kube-public", "default"],
|
||||
"risk_score": 9.0,
|
||||
"factor": "敏感命名空間: 操作影響 K8s 核心組件",
|
||||
"recommendation": "確認是否真的需要操作系統命名空間",
|
||||
},
|
||||
"secret_operation": {
|
||||
"patterns": ["secret", "configmap", "credential", "password", "token"],
|
||||
"risk_score": 8.5,
|
||||
"factor": "敏感資料: 操作涉及機密資訊",
|
||||
"recommendation": "確保日誌不會記錄機密內容",
|
||||
},
|
||||
"network_policy": {
|
||||
"patterns": ["networkpolicy", "ingress", "egress", "firewall"],
|
||||
"risk_score": 7.5,
|
||||
"factor": "網路變更: 可能影響服務連通性",
|
||||
"recommendation": "變更前確認流量影響範圍",
|
||||
},
|
||||
"rbac_operation": {
|
||||
"patterns": ["role", "rolebinding", "clusterrole", "serviceaccount"],
|
||||
"risk_score": 9.0,
|
||||
"factor": "權限變更: 操作涉及 RBAC 設定",
|
||||
"recommendation": "最小權限原則,避免過度授權",
|
||||
},
|
||||
"scale_to_zero": {
|
||||
"patterns": ["replicas=0", "replicas 0", "scale --replicas=0"],
|
||||
"risk_score": 8.0,
|
||||
"factor": "服務中斷: 副本數設為 0",
|
||||
"recommendation": "確認是否為計畫性維護",
|
||||
},
|
||||
"rollback": {
|
||||
"patterns": ["rollout undo", "rollback"],
|
||||
"risk_score": 5.0,
|
||||
"factor": "回滾操作: 相對安全但需確認目標版本",
|
||||
"recommendation": "確認回滾目標版本是穩定的",
|
||||
},
|
||||
"restart": {
|
||||
"patterns": ["rollout restart", "restart"],
|
||||
"risk_score": 3.0,
|
||||
"factor": "重啟操作: 低風險但可能造成短暫中斷",
|
||||
"recommendation": "確認服務有足夠副本處理滾動重啟",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class SecurityAgent(BaseAgent[SecurityResult]):
|
||||
"""
|
||||
安全風險評估專家 Agent
|
||||
|
||||
分析流程:
|
||||
1. 本地規則引擎快速掃描 (毫秒級)
|
||||
2. LLM 深度分析 (可選,複雜場景)
|
||||
3. 綜合評分
|
||||
|
||||
使用方式:
|
||||
```python
|
||||
agent = SecurityAgent()
|
||||
result = await agent.analyze({
|
||||
"action": "kubectl delete pod nginx-xxx",
|
||||
"namespace": "awoooi-prod",
|
||||
"affected_services": ["nginx", "frontend"],
|
||||
})
|
||||
print(result.risk_score) # 0-10
|
||||
```
|
||||
"""
|
||||
|
||||
AGENT_NAME = "security-expert"
|
||||
AGENT_DESCRIPTION = "資安專家,評估安全風險與權限影響"
|
||||
AGENT_TOOLS = ["Read", "Grep"] # 只讀權限
|
||||
|
||||
def __init__(self, timeout_sec: float = 30.0, use_llm: bool = False):
|
||||
"""
|
||||
初始化 SecurityAgent
|
||||
|
||||
Args:
|
||||
timeout_sec: 執行超時時間
|
||||
use_llm: 是否啟用 LLM 深度分析 (Phase 9.4 擴展)
|
||||
"""
|
||||
super().__init__(timeout_sec)
|
||||
self.use_llm = use_llm
|
||||
|
||||
async def analyze(self, context: dict[str, Any]) -> SecurityResult:
|
||||
"""
|
||||
執行安全風險分析
|
||||
|
||||
Args:
|
||||
context: 分析上下文
|
||||
- action: 要執行的指令
|
||||
- namespace: 目標命名空間
|
||||
- affected_services: 受影響服務列表
|
||||
- incident_id: 事件 ID (可選)
|
||||
|
||||
Returns:
|
||||
SecurityResult 包含風險評分和詳細分析
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
self.logger.info(
|
||||
"security_analysis_start",
|
||||
action=context.get("action", "")[:100],
|
||||
namespace=context.get("namespace"),
|
||||
)
|
||||
|
||||
try:
|
||||
# Phase 1: 本地規則引擎 (同步、快速)
|
||||
rule_result = self._rule_engine_analyze(context)
|
||||
|
||||
# Phase 2: LLM 深度分析 (可選,未來擴展)
|
||||
if self.use_llm and rule_result["risk_score"] >= 7.0:
|
||||
# 高風險場景啟用 LLM 二次確認
|
||||
# TODO: Phase 9.4 實作 LLM 分析
|
||||
pass
|
||||
|
||||
latency_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
result = SecurityResult(
|
||||
agent_name=self.AGENT_NAME,
|
||||
status=AgentStatus.SUCCESS,
|
||||
confidence=rule_result["confidence"],
|
||||
analysis=rule_result["analysis"],
|
||||
latency_ms=latency_ms,
|
||||
risk_score=rule_result["risk_score"],
|
||||
risk_factors=rule_result["risk_factors"],
|
||||
permission_issues=rule_result["permission_issues"],
|
||||
recommendations=rule_result["recommendations"],
|
||||
raw_response=rule_result,
|
||||
)
|
||||
|
||||
self.logger.info(
|
||||
"security_analysis_complete",
|
||||
risk_score=result.risk_score,
|
||||
latency_ms=latency_ms,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
latency_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
self.logger.exception(
|
||||
"security_analysis_error",
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
return SecurityResult(
|
||||
agent_name=self.AGENT_NAME,
|
||||
status=AgentStatus.FAILED,
|
||||
confidence=0.0,
|
||||
analysis=f"分析失敗: {str(e)}",
|
||||
latency_ms=latency_ms,
|
||||
error=str(e),
|
||||
risk_score=10.0, # 失敗時預設最高風險
|
||||
risk_factors=["分析過程發生錯誤"],
|
||||
recommendations=["請人工審核此操作"],
|
||||
)
|
||||
|
||||
def _rule_engine_analyze(self, context: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
本地規則引擎分析
|
||||
|
||||
快速檢查常見安全模式,毫秒級回應
|
||||
"""
|
||||
action = context.get("action", "").lower()
|
||||
namespace = context.get("namespace", "").lower()
|
||||
affected_services = context.get("affected_services", [])
|
||||
|
||||
risk_factors: list[str] = []
|
||||
recommendations: list[str] = []
|
||||
permission_issues: list[str] = []
|
||||
max_risk_score: float = 0.0
|
||||
|
||||
# 掃描所有安全規則
|
||||
for rule_name, rule in SECURITY_RULES.items():
|
||||
patterns = rule["patterns"]
|
||||
|
||||
# 檢查 action
|
||||
if any(pattern in action for pattern in patterns):
|
||||
risk_factors.append(rule["factor"])
|
||||
recommendations.append(rule["recommendation"])
|
||||
max_risk_score = max(max_risk_score, rule["risk_score"])
|
||||
|
||||
# 檢查 namespace
|
||||
if rule_name == "privileged_namespace":
|
||||
if any(pattern in namespace for pattern in patterns):
|
||||
risk_factors.append(rule["factor"])
|
||||
recommendations.append(rule["recommendation"])
|
||||
max_risk_score = max(max_risk_score, rule["risk_score"])
|
||||
|
||||
# 檢查受影響服務數量
|
||||
if len(affected_services) > 5:
|
||||
risk_factors.append(f"大範圍影響: 涉及 {len(affected_services)} 個服務")
|
||||
max_risk_score = max(max_risk_score, 6.0)
|
||||
recommendations.append("考慮分批執行,降低爆炸半徑")
|
||||
|
||||
# 檢查是否涉及生產環境
|
||||
if "prod" in namespace:
|
||||
if max_risk_score < 5.0:
|
||||
max_risk_score = 5.0 # 生產環境最低風險 5
|
||||
permission_issues.append("操作目標為生產環境")
|
||||
|
||||
# 如果沒有匹配任何規則,給予基礎評分
|
||||
if not risk_factors:
|
||||
risk_factors.append("未偵測到明顯風險因素")
|
||||
max_risk_score = 2.0 # 基礎低風險
|
||||
|
||||
# 計算信心分數 (規則匹配越多,信心越高)
|
||||
confidence = min(0.95, 0.7 + len(risk_factors) * 0.05)
|
||||
|
||||
# 生成分析摘要
|
||||
if max_risk_score >= 8.0:
|
||||
analysis = f"高風險操作 (Score: {max_risk_score}/10): 建議人工審核"
|
||||
elif max_risk_score >= 5.0:
|
||||
analysis = f"中等風險 (Score: {max_risk_score}/10): 確認影響範圍後執行"
|
||||
else:
|
||||
analysis = f"低風險操作 (Score: {max_risk_score}/10): 可安全執行"
|
||||
|
||||
return {
|
||||
"risk_score": max_risk_score,
|
||||
"risk_factors": risk_factors,
|
||||
"recommendations": list(set(recommendations)), # 去重
|
||||
"permission_issues": permission_issues,
|
||||
"confidence": confidence,
|
||||
"analysis": analysis,
|
||||
"rules_matched": len(risk_factors),
|
||||
}
|
||||
|
||||
def _build_prompt(self, context: dict[str, Any]) -> str:
|
||||
"""建構 LLM Prompt (Phase 9.4 擴展)"""
|
||||
return f"""你是 AWOOOI 的資安專家。
|
||||
分析以下操作的安全風險:
|
||||
|
||||
操作指令: {context.get("action", "N/A")}
|
||||
目標命名空間: {context.get("namespace", "N/A")}
|
||||
受影響服務: {", ".join(context.get("affected_services", []))}
|
||||
|
||||
評估:
|
||||
1. 是否涉及敏感資料
|
||||
2. 是否可能被利用
|
||||
3. 權限邊界是否被突破
|
||||
|
||||
輸出 JSON:
|
||||
```json
|
||||
{{
|
||||
"risk_score": 0-10,
|
||||
"risk_factors": ["...", "..."],
|
||||
"permission_issues": ["...", "..."],
|
||||
"recommendations": ["...", "..."],
|
||||
"analysis": "一句話摘要",
|
||||
"confidence": 0-1
|
||||
}}
|
||||
```"""
|
||||
|
||||
def _parse_response(self, response: str) -> dict[str, Any]:
|
||||
"""解析 LLM 回應"""
|
||||
return self._extract_json(response)
|
||||
Reference in New Issue
Block a user