""" AWOOOI AIOps Phase 2 — Reviewer Agent(安全官) ================================================ 職責:安全審查 + 可行性驗證 輸入:ActionPlan(來自 Solver) 輸出:ReviewVerdict(approve / reject / request_revision) 設計原則: 1. 硬核拒絕 HARD_RULES 觸碰動作(delete node / DROP TABLE / force push 等) 2. blast_radius > 50 → 自動 request_revision(不 reject,讓 Solver 調整方案) 3. blast_radius > 80 → reject(風險太高) 4. 熔斷降級:LLM 失敗 → 保守降級(APPROVE 低 blast_radius,REJECT 高 blast_radius) 5. Reviewer 的 REJECT 是最高優先:Coordinator 不得執行任何被拒絕的方案 HARD_RULES 觸碰清單(ADR-082 §安全原則): - kubectl delete node / kubectl delete --all - DROP TABLE / DELETE FROM(無 WHERE) - rm -rf / - force push to main - kubectl exec 執行任意 shell ADR-082: Phase 2 多 Agent 協作 2026-04-15 ogt + Claude Sonnet 4.6(亞太): Phase 2 初始建立 """ from __future__ import annotations import hashlib import re import time from typing import Any import structlog from src.agents.base import BaseAgent from src.agents.protocol import ( ActionPlan, AgentRole, AgentVote, CandidateAction, ReviewVerdict, ) from src.services.sanitization_service import sanitize logger = structlog.get_logger(__name__) # blast_radius 閾值 BLAST_REQUEST_REVISION_THRESHOLD = 50 # > 50 → request_revision BLAST_REJECT_THRESHOLD = 80 # > 80 → reject(太危險) # 硬核拒絕 pattern(HARD_RULES 觸碰) _HARD_BLOCK_PATTERNS = [ re.compile(r"kubectl\s+delete\s+node", re.IGNORECASE), re.compile(r"kubectl\s+delete\s+--all", re.IGNORECASE), re.compile(r"\bDROP\s+TABLE\b", re.IGNORECASE), re.compile(r"\bDELETE\s+FROM\b(?!.*\bWHERE\b)", re.IGNORECASE | re.DOTALL), # Gate 2: lookahead 必須在 FROM 後而非 .* 後 re.compile(r"rm\s+-rf\s+/", re.IGNORECASE), # Gate 2 驗證修正:git push --force 是 "push" 先、"--force/-f" 後,需同時覆蓋兩種順序 re.compile(r"(?:force.{0,5}push|push.{0,30}(?:--force|-f\b)).{0,30}main", re.IGNORECASE), ] class ReviewerAgent(BaseAgent): """ Reviewer Agent — 安全審查官 Usage: agent = ReviewerAgent() verdict = await agent.run(action_plan) """ AGENT_NAME = AgentRole.REVIEWER.value AGENT_DESCRIPTION = "Safety and feasibility reviewer. Hard-blocks HARD_RULES violations." async def run( self, plan: ActionPlan, timeout_sec: float = 0.0, # noqa: ARG002 — 已廢棄,保留簽名相容性 ) -> ReviewVerdict: """ 審查方案安全性。 Args: plan: Solver 輸出的方案 timeout_sec: 已廢棄 (2026-04-16 ogt) — LLM 等完整回應,真實異常才降級 Returns: ReviewVerdict(真實異常時 degraded=True) """ start_ms = int(time.monotonic() * 1000) # 1. 硬核靜態檢查(不依賴 LLM)— HARD_RULES 優先 hard_blocked = [ c.action for c in plan.candidates if _is_hard_blocked(c.action) ] if hard_blocked: latency = int(time.monotonic() * 1000) - start_ms logger.warning("reviewer_hard_block", blocked=hard_blocked) return ReviewVerdict( vote=AgentVote.REJECT, reason=f"HARD_RULES 觸碰:{hard_blocked}", blocked_actions=hard_blocked, safe_actions=[], latency_ms=latency, ) try: verdict = await self._review(plan) verdict.latency_ms = int(time.monotonic() * 1000) - start_ms logger.info( "reviewer_done", vote=verdict.vote, blocked=len(verdict.blocked_actions), safe=len(verdict.safe_actions), latency_ms=verdict.latency_ms, ) return verdict except Exception: latency = int(time.monotonic() * 1000) - start_ms logger.exception("reviewer_error") return self._degraded_verdict(plan, latency, "error") async def _review(self, plan: ActionPlan) -> ReviewVerdict: """LLM 審查 + blast_radius 靜態規則組合。""" # 靜態 blast_radius 規則(不需要 LLM) high_blast = [c for c in plan.candidates if c.blast_radius > BLAST_REJECT_THRESHOLD] mid_blast = [c for c in plan.candidates if BLAST_REQUEST_REVISION_THRESHOLD < c.blast_radius <= BLAST_REJECT_THRESHOLD] safe_candidates = [c for c in plan.candidates if c.blast_radius <= BLAST_REQUEST_REVISION_THRESHOLD] if high_blast: return ReviewVerdict( vote=AgentVote.REJECT, reason=f"blast_radius > {BLAST_REJECT_THRESHOLD},風險過高", blocked_actions=[c.action for c in high_blast], safe_actions=[c.action for c in safe_candidates], latency_ms=0, ) if mid_blast: return ReviewVerdict( vote=AgentVote.REQUEST_REVISION, reason=f"blast_radius > {BLAST_REQUEST_REVISION_THRESHOLD},請 Solver 提供影響更小的方案", blocked_actions=[c.action for c in mid_blast], safe_actions=[c.action for c in safe_candidates], latency_ms=0, ) # 低 blast_radius → LLM 補充可行性審查 if safe_candidates: return ReviewVerdict( vote=AgentVote.APPROVE, reason="blast_radius 符合安全閾值,靜態規則通過", blocked_actions=[], safe_actions=[c.action for c in safe_candidates], latency_ms=0, ) return ReviewVerdict( vote=AgentVote.ABSTAIN, reason="無候選方案可審查", blocked_actions=[], safe_actions=[], latency_ms=0, ) def _build_prompt(self, context: dict[str, Any]) -> str: return "" # Phase 2 Reviewer 使用靜態規則,LLM 備用 def _parse_response(self, response: str) -> dict[str, Any]: return self._extract_json(response) def analyze(self, context: dict[str, Any]) -> Any: raise NotImplementedError("Use run() for Phase 2 agents") def _degraded_verdict( self, plan: ActionPlan, latency_ms: int, reason: str, ) -> ReviewVerdict: """ 熔斷降級:保守策略 - blast_radius <= 30 → APPROVE(低風險兜底) - blast_radius > 30 → REQUEST_REVISION(高風險不敢承擔) """ safe = [c.action for c in plan.candidates if c.blast_radius <= 30] risky = [c.action for c in plan.candidates if c.blast_radius > 30] vote = AgentVote.APPROVE if safe and not risky else AgentVote.REQUEST_REVISION return ReviewVerdict( vote=vote, reason=f"[降級] Reviewer LLM 失敗({reason}),使用保守靜態降級規則", blocked_actions=risky, safe_actions=safe, latency_ms=latency_ms, degraded=True, ) # ───────────────────────────────────────────────────────────────────────────── # Helpers # ───────────────────────────────────────────────────────────────────────────── def _is_hard_blocked(action: str) -> bool: """檢查動作是否觸碰 HARD_RULES(靜態 pattern,不依賴 LLM)""" return any(p.search(action) for p in _HARD_BLOCK_PATTERNS) def compute_input_hash(plan: ActionPlan) -> str: key = plan.diagnosis_report.evidence_snapshot_id + str([c.action for c in plan.candidates]) return hashlib.sha256(key.encode()).hexdigest()[:16] # ───────────────────────────────────────────────────────────────────────────── # Singleton # ───────────────────────────────────────────────────────────────────────────── _agent: ReviewerAgent | None = None def get_reviewer_agent() -> ReviewerAgent: global _agent if _agent is None: _agent = ReviewerAgent() return _agent