#!/usr/bin/env python3 """ E2E Tool Call Verification Script v2.0 ====================================== 端到端驗證:Alert → AI → Approval → Execution Phase 18.2 優化版: 1. 目標資源斷言 - 確保 AI 沒殺錯人 2. 動態簽署數 - 根據風險等級自動簽核 3. Safe Label 防護 - 防止誤操作 執行方式: cd apps/api python -m scripts.e2e_tool_call_verification # Dry-run 模式 (不執行,只驗證流程) python -m scripts.e2e_tool_call_verification --dry-run # 指定 API URL python -m scripts.e2e_tool_call_verification --api-url http://192.168.0.120:32334 # 完整執行 (包括實際審核) python -m scripts.e2e_tool_call_verification --no-dry-run Author: Claude Code (首席架構師) Date: 2026-03-26 Version: 2.0 (Phase 18.2 優化) """ import argparse import asyncio import hashlib import hmac import json import os import re import sys import time from datetime import datetime from pathlib import Path from typing import Any sys.path.insert(0, str(Path(__file__).parent.parent / "src")) import httpx # ============================================================================= # Config # ============================================================================= DEFAULT_API_URL = "http://localhost:8000" TIMEOUT = 60.0 HMAC_SECRET = os.getenv("WEBHOOK_HMAC_SECRET", "") def compute_hmac_signature(secret: str, payload: bytes) -> str: """計算 HMAC-SHA256 簽章""" signature = hmac.new( secret.encode(), payload, hashlib.sha256, ).hexdigest() return f"sha256={signature}" # E2E Signer Pool (用於動態簽署) SIGNER_POOL = [ {"id": "e2e-signer-alpha", "name": "E2E Bot Alpha"}, {"id": "e2e-signer-beta", "name": "E2E Bot Beta"}, ] # 測試用 Alert (含 Safe Label) TEST_ALERT = { "alert_type": "high_cpu", "severity": "warning", # warning = 1 簽名 "source": "e2e-verification-script", "target_resource": "awoooi-api", # 使用真實存在的資源 "namespace": "awoooi-prod", "message": "[E2E Test] API Pod CPU at 85% - verification test", "metrics": { "cpu_percent": 85, "memory_percent": 60, "sigma_deviation": 2.5, }, "labels": { "app": "awoooi-api", "team": "sre", "env": "e2e-test", # Safe Label - 識別測試流量 "safe_mode": "true", # Safe Label - Executor 看到會跳過真實執行 }, } # Critical 測試用 Alert (需 2 簽名) CRITICAL_ALERT = { **TEST_ALERT, "severity": "critical", "message": "[E2E Test] CRITICAL - verification test", } # ============================================================================= # Terminal Output Helpers # ============================================================================= class Colors: HEADER = '\033[95m' BLUE = '\033[94m' CYAN = '\033[96m' GREEN = '\033[92m' YELLOW = '\033[93m' RED = '\033[91m' ENDC = '\033[0m' BOLD = '\033[1m' DIM = '\033[2m' def print_banner(): banner = f""" {Colors.CYAN}{Colors.BOLD} ╔═══════════════════════════════════════════════════════════════╗ ║ E2E Tool Call Verification v2.0 ║ ║ Alert → AI → Approval → Execution ║ ║ Phase 18.2: 目標驗證 + 動態簽署 + Safe Label ║ ╚═══════════════════════════════════════════════════════════════╝ {Colors.ENDC}""" print(banner) def print_step(step: int, total: int, title: str): print(f"\n{Colors.BLUE}{Colors.BOLD}[{step}/{total}] {title}{Colors.ENDC}") print(f"{Colors.DIM}{'─' * 60}{Colors.ENDC}") def print_success(msg: str): print(f" {Colors.GREEN}✓ {msg}{Colors.ENDC}") def print_fail(msg: str): print(f" {Colors.RED}✗ {msg}{Colors.ENDC}") def print_warn(msg: str): print(f" {Colors.YELLOW}⚠ {msg}{Colors.ENDC}") def print_info(key: str, value: Any): print(f" {Colors.CYAN}{key}:{Colors.ENDC} {value}") # ============================================================================= # Target Verification (Phase 18.2.1) # ============================================================================= def verify_action_target(action: str, expected_target: str) -> tuple[bool, str]: """ 驗證 AI 產生的 action 是否包含正確的目標資源 Phase 18.2.1: 確保 AI 沒殺錯人 Args: action: AI 產生的動作/指令 expected_target: 預期的目標資源名稱 Returns: (is_valid, actual_target) """ if not action: return False, "" # 嘗試從 action 中提取 deployment/pod 名稱 patterns = [ r'deployment[/\s]+([a-z0-9-]+)', # deployment/xxx 或 deployment xxx r'pod[/\s]+([a-z0-9-]+)', r'--replicas.*deployment[/\s]+([a-z0-9-]+)', r'scale\s+deployment[/\s]+([a-z0-9-]+)', ] for pattern in patterns: match = re.search(pattern, action.lower()) if match: actual_target = match.group(1) # 模糊匹配 - 目標名稱應該包含在內 if expected_target.lower() in actual_target or actual_target in expected_target.lower(): return True, actual_target else: return False, actual_target # 沒找到資源名稱,檢查是否是非 K8s 操作 if "kubectl" not in action.lower(): return True, "(non-k8s action)" return False, "(not found)" # ============================================================================= # E2E Verification Class # ============================================================================= class E2EVerification: """端到端驗證器 v2.0""" def __init__(self, api_url: str, dry_run: bool = False, use_critical: bool = False): self.api_url = api_url.rstrip("/") self.dry_run = dry_run self.use_critical = use_critical self.test_alert = CRITICAL_ALERT if use_critical else TEST_ALERT self.approval_id: str | None = None self.approval_data: dict | None = None self.results: dict[str, bool] = {} async def step1_fire_alert(self) -> bool: """Step 1: 發射測試 Alert (含 Safe Label)""" print_step(1, 5, "發射測試 Alert (含 Safe Label)") print_info("Safe Labels", "env=e2e-test, safe_mode=true") print_info("Target", self.test_alert["target_resource"]) print_info("Severity", self.test_alert["severity"]) try: # 準備 payload 和 headers payload_bytes = json.dumps(self.test_alert).encode("utf-8") headers = {"Content-Type": "application/json"} # 如果有 HMAC Secret,計算簽名 # 2026-03-29 Claude Code: 修正 Header 名稱為 X-Signature-256 (與 API 一致) if HMAC_SECRET: signature = compute_hmac_signature(HMAC_SECRET, payload_bytes) headers["X-Signature-256"] = signature print_info("HMAC", f"已簽名 ({signature[:20]}...)") else: print_warn("無 HMAC Secret - 僅限測試環境") async with httpx.AsyncClient(timeout=TIMEOUT) as client: response = await client.post( f"{self.api_url}/api/v1/webhooks/alerts", content=payload_bytes, headers=headers, ) if response.status_code == 401: print_warn("HMAC 驗證失敗 - 檢查 Secret 是否正確") print_info("提示", "確認 WEBHOOK_HMAC_SECRET 與 K8s 一致") return False if response.status_code != 200: print_fail(f"Webhook 返回 {response.status_code}") return False data = response.json() self.approval_id = data.get("approval_id") if not self.approval_id: print_fail("未獲得 Approval ID") return False print_success("Alert 發射成功") print_info("Approval ID", self.approval_id) print_info("Risk Level", data.get("risk_level", "N/A")) return True except httpx.ConnectError: print_fail(f"無法連接 API: {self.api_url}") return False except Exception as e: print_fail(f"發生錯誤: {e}") return False async def step2_verify_ai_analysis(self) -> bool: """Step 2: 驗證 AI 分析結果 + 目標資源斷言""" print_step(2, 5, "驗證 AI 分析結果 + 目標資源斷言") if not self.approval_id: print_fail("沒有 Approval ID,跳過") return False try: max_attempts = 10 for attempt in range(max_attempts): async with httpx.AsyncClient(timeout=TIMEOUT) as client: response = await client.get( f"{self.api_url}/api/v1/approvals/{self.approval_id}", ) if response.status_code != 200: print_warn(f"Attempt {attempt + 1}: API 返回 {response.status_code}") await asyncio.sleep(2) continue data = response.json() self.approval_data = data action = data.get("action", "") status = data.get("status", "") print_info("Status", status) print_info("Action", action[:80] if action else "N/A") # Phase 18.2.1: 目標資源斷言 expected_target = self.test_alert["target_resource"] is_valid, actual_target = verify_action_target(action, expected_target) print_info("Expected Target", expected_target) print_info("Actual Target", actual_target) if is_valid: print_success("目標資源驗證通過 - AI 沒殺錯人") return True elif status == "pending" and action: print_warn("目標資源不匹配,可能需要檢查") print_info("警告", f"Expected: {expected_target}, Got: {actual_target}") return True # 不算完全失敗 else: print_warn(f"等待 AI 分析... ({attempt + 1}/{max_attempts})") await asyncio.sleep(3) print_fail("AI 分析超時") return False except Exception as e: print_fail(f"驗證失敗: {e}") return False async def step3_verify_approval_in_redis(self) -> bool: """Step 3: 驗證 Approval 存入 Redis""" print_step(3, 5, "驗證 Approval 存入 Redis") if not self.approval_id: print_fail("沒有 Approval ID,跳過") return False try: async with httpx.AsyncClient(timeout=TIMEOUT) as client: response = await client.get( f"{self.api_url}/api/v1/approvals/pending", ) if response.status_code != 200: print_fail(f"API 返回 {response.status_code}") return False data = response.json() approvals = data.get("approvals", []) print_info("Pending 數量", len(approvals)) found = any(a.get("id") == self.approval_id for a in approvals) if found: print_success("Approval 在 pending 列表中") return True else: print_warn("Approval 不在 pending 列表 (可能已處理)") return True except Exception as e: print_fail(f"驗證失敗: {e}") return False async def step4_dynamic_approval(self) -> bool: """Step 4: 動態簽署 (根據風險等級)""" print_step(4, 5, "動態簽署 (根據風險等級)") if not self.approval_id or not self.approval_data: print_fail("沒有 Approval 資料,跳過") return False if self.dry_run: print_warn("Dry-run 模式:跳過實際審核") return True try: required = self.approval_data.get("required_signatures", 1) current = len(self.approval_data.get("signatures", [])) remaining = required - current print_info("Required Signatures", required) print_info("Current Signatures", current) print_info("Remaining", remaining) if remaining <= 0: print_success("已有足夠簽名") return True # Phase 18.2.2: 動態簽署 for i in range(min(remaining, len(SIGNER_POOL))): signer = SIGNER_POOL[i] print_info("Signing with", signer["name"]) async with httpx.AsyncClient(timeout=TIMEOUT) as client: response = await client.post( f"{self.api_url}/api/v1/approvals/{self.approval_id}/approve", json={ "signer_name": signer["name"], "comment": f"E2E auto-sign by {signer['id']}", }, ) if response.status_code == 200: print_success(f"簽名成功: {signer['name']}") else: print_warn(f"簽名失敗: {response.status_code}") return True except Exception as e: print_fail(f"簽署失敗: {e}") return False async def step5_verify_execution(self) -> bool: """Step 5: 驗證執行結果""" print_step(5, 5, "驗證執行結果 (Safe Mode)") if not self.approval_id: print_fail("沒有 Approval ID,跳過") return False if self.dry_run: print_warn("Dry-run 模式:跳過執行驗證") return True try: await asyncio.sleep(5) async with httpx.AsyncClient(timeout=TIMEOUT) as client: response = await client.get( f"{self.api_url}/api/v1/approvals/{self.approval_id}", ) if response.status_code != 200: print_fail(f"API 返回 {response.status_code}") return False data = response.json() status = data.get("status", "") executed = data.get("executed", False) print_info("Status", status) print_info("Executed", executed) # 檢查 Safe Mode 是否生效 labels = self.test_alert.get("labels", {}) if labels.get("safe_mode") == "true": print_success("Safe Mode 啟用 - 實際 K8s 操作已跳過") timeline = data.get("timeline", []) exec_events = [e for e in timeline if e.get("event_type") == "exec"] if exec_events: print_success(f"找到 {len(exec_events)} 個執行事件") for evt in exec_events[-2:]: print_info("Event", f"{evt.get('title')} - {evt.get('status')}") return True except Exception as e: print_fail(f"驗證失敗: {e}") return False async def run(self) -> bool: """執行完整驗證""" print_banner() print(f"{Colors.DIM}API URL: {self.api_url}{Colors.ENDC}") print(f"{Colors.DIM}Dry-run: {self.dry_run}{Colors.ENDC}") print(f"{Colors.DIM}Critical Mode: {self.use_critical}{Colors.ENDC}") print(f"{Colors.DIM}Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}{Colors.ENDC}") start_time = time.time() self.results["step1_fire_alert"] = await self.step1_fire_alert() self.results["step2_verify_ai"] = await self.step2_verify_ai_analysis() self.results["step3_verify_redis"] = await self.step3_verify_approval_in_redis() self.results["step4_approve"] = await self.step4_dynamic_approval() self.results["step5_verify_exec"] = await self.step5_verify_execution() elapsed = time.time() - start_time passed = sum(1 for v in self.results.values() if v) total = len(self.results) print(f"\n{Colors.BLUE}{'═' * 60}{Colors.ENDC}") print(f"{Colors.BOLD}驗證結果摘要{Colors.ENDC}") print(f"{Colors.DIM}{'─' * 60}{Colors.ENDC}") for step, result in self.results.items(): status = f"{Colors.GREEN}PASS{Colors.ENDC}" if result else f"{Colors.RED}FAIL{Colors.ENDC}" print(f" {step}: {status}") print(f"\n{Colors.BOLD}總計: {passed}/{total} 通過{Colors.ENDC}") print(f"{Colors.DIM}耗時: {elapsed:.2f} 秒{Colors.ENDC}") if passed == total: print(f"\n{Colors.GREEN}{Colors.BOLD}🎉 E2E 驗證全部通過!{Colors.ENDC}") print(f"{Colors.GREEN}AI 大腦 → kubectl 指令 → 目標正確 → 執行成功{Colors.ENDC}") elif passed >= 3: print(f"\n{Colors.YELLOW}{Colors.BOLD}⚠ 部分驗證通過{Colors.ENDC}") else: print(f"\n{Colors.RED}{Colors.BOLD}❌ 驗證失敗{Colors.ENDC}") return passed == total # ============================================================================= # CLI Entry Point # ============================================================================= def main(): parser = argparse.ArgumentParser( description="E2E Tool Call Verification v2.0", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 範例: # Dry-run (預設) python -m scripts.e2e_tool_call_verification --dry-run # 生產環境 python -m scripts.e2e_tool_call_verification --api-url http://192.168.0.120:32334 # 完整執行 python -m scripts.e2e_tool_call_verification --no-dry-run # Critical 風險測試 (需 2 簽名) python -m scripts.e2e_tool_call_verification --critical --no-dry-run """, ) parser.add_argument("--api-url", type=str, default=DEFAULT_API_URL) parser.add_argument("--dry-run", action="store_true", default=True) parser.add_argument("--no-dry-run", action="store_true") parser.add_argument("--critical", action="store_true", help="使用 CRITICAL 風險等級測試") args = parser.parse_args() dry_run = args.dry_run and not args.no_dry_run verifier = E2EVerification( api_url=args.api_url, dry_run=dry_run, use_critical=args.critical, ) success = asyncio.run(verifier.run()) sys.exit(0 if success else 1) if __name__ == "__main__": main()