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:
OG T
2026-03-23 18:40:36 +08:00
parent 6eccb45757
commit 7478dc0254
169 changed files with 24613 additions and 247 deletions

View 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",
]

View 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
View 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,
}

View 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)

View 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)