Files
awoooi/apps/api/src/services/trust_engine.py
OG T ce7f8a1b23 feat(api): ADR-030 Phase 4 自動執行機制
實作低風險操作自動執行策略:

1. auto_approve.py - 自動執行策略服務
   - AutoApprovePolicy: 評估是否可自動執行
   - 條件: LOW 風險 + 信任分數 >= 5 + Playbook 成功率 >= 95%
   - CRITICAL 永遠不自動執行
   - 完整審計追蹤

2. trust_engine.py - 新增 singleton
   - get_trust_manager(): 取得全域 TrustScoreManager

3. decision_manager.py - 整合自動執行 (Tier 3 紅區)
   - Step 5 加入 AutoApprovePolicy 判斷
   - 條件滿足時跳過 Telegram,直接執行
   - _auto_execute(): 自動執行邏輯
   - 失敗時 fallback 到人工審核

流程:
Incident → 分析 → AutoApprovePolicy 評估
  ├─ 可自動執行 → 直接執行 → 完成
  └─ 需人工審核 → Telegram 通知 → 等待批准

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-26 22:13:10 +08:00

423 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Trust Engine - 信任引擎與漸進自治
Phase 3.2: Progressive Autonomy
核心理念:
當某種特定操作被人類連續批准多次後,
系統自動將該操作的風險等級降級,最終達成 Zero-Touch (免授權自動執行)
信任累積規則:
- 每次 Approve: +1 分
- 每次 Reject: 歸零 (信任瞬間瓦解)
風險降級閾值:
- score >= 5: medium → low (變成自動執行)
- score >= 10: high → medium (雙簽變單簽)
- critical: 永遠不准降級 (Drop Table 等毀滅性操作)
"""
import logging
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal, Protocol, runtime_checkable
# Phase 16 R2 (2026-03-25): RiskLevel 統一改從 models/approval.py 導入
# 原因: 消除重複定義,統一風險等級來源
# 執行者: Claude Code
# 回滾: 取消註解下方 RiskLevel class 區塊,移除此 import
from src.models.approval import RiskLevel
logger = logging.getLogger(__name__)
# ==================== Types ====================
# --- 以下為舊定義,已封存 (Phase 16 R2) ---
# class RiskLevel(str, Enum):
# """風險等級"""
# LOW = "low"
# MEDIUM = "medium"
# HIGH = "high"
# CRITICAL = "critical"
# --- 封存結束 ---
@dataclass
class TrustRecord:
"""信任記錄"""
action_pattern: str
score: int = 0
total_approvals: int = 0
total_rejections: int = 0
last_approval_by: str | None = None
last_approval_at: datetime | None = None
last_rejection_by: str | None = None
last_rejection_at: datetime | None = None
created_at: datetime = field(default_factory=datetime.utcnow)
@property
def approval_rate(self) -> float:
"""批准率"""
total = self.total_approvals + self.total_rejections
if total == 0:
return 0.0
return self.total_approvals / total
@dataclass
class RiskAdjustment:
"""風險調整結果"""
original_risk: RiskLevel
adjusted_risk: RiskLevel
trust_score: int
reason: str
is_downgraded: bool
def to_dict(self) -> dict:
return {
"originalRisk": self.original_risk.value,
"adjustedRisk": self.adjusted_risk.value,
"trustScore": self.trust_score,
"reason": self.reason,
"isDowngraded": self.is_downgraded,
}
# ==================== Configuration ====================
@dataclass
class TrustThresholds:
"""信任閾值配置"""
# 降級閾值
medium_to_low: int = 5 # medium → low (自動執行)
high_to_medium: int = 10 # high → medium (雙簽→單簽)
# Reject 懲罰
rejection_penalty: int = -5 # Reject 時直接扣分 (或歸零)
reset_on_reject: bool = True # True = 歸零, False = 扣分
# 信任衰減 (可選,防止過時信任)
decay_enabled: bool = False
decay_days: int = 30 # 幾天沒操作後開始衰減
decay_rate: float = 0.1 # 每天衰減比例
# 預設閾值
DEFAULT_THRESHOLDS = TrustThresholds()
# ==================== Protocol Interface (Phase 17 P1) ====================
@runtime_checkable
class ITrustScoreManager(Protocol):
"""
TrustScoreManager 介面定義
用途:
- 依賴注入 (DI) 時的型別約束
- 測試時 Mock 的型別檢查
- 符合 leWOOOgo 積木化規範
Tier 3 紅區服務: 修改需首席架構師簽核
@see feedback_lewooogo_modular_enforcement.md
@see docs/RED_ZONES.md
"""
def record_approval(
self,
action_pattern: str,
user_role: str,
user_id: str | None = None,
) -> TrustRecord:
"""記錄人類批准"""
...
def record_rejection(
self,
action_pattern: str,
user_role: str,
user_id: str | None = None,
reason: str | None = None,
) -> TrustRecord:
"""記錄人類拒絕"""
...
def evaluate_adjusted_risk(
self,
action_pattern: str,
original_risk: str | RiskLevel,
) -> RiskAdjustment:
"""評估調整後的風險等級"""
...
def get_trust_record(self, action_pattern: str) -> TrustRecord | None:
"""取得信任記錄"""
...
# ==================== Trust Engine ====================
class TrustScoreManager:
"""
信任分數管理器
追蹤每個 action_pattern 的信任分數,
根據人類批准/拒絕歷史動態調整風險等級
"""
def __init__(self, thresholds: TrustThresholds | None = None):
self.thresholds = thresholds or DEFAULT_THRESHOLDS
# In-memory storage (Phase 4+ 換成 Redis/PostgreSQL)
self._records: dict[str, TrustRecord] = {}
def _get_or_create_record(self, action_pattern: str) -> TrustRecord:
"""取得或建立信任記錄"""
if action_pattern not in self._records:
self._records[action_pattern] = TrustRecord(action_pattern=action_pattern)
return self._records[action_pattern]
def record_approval(
self,
action_pattern: str,
user_role: str,
user_id: str | None = None,
) -> TrustRecord:
"""
記錄人類批准
每次 Approve該 pattern 的信任分數 +1
連續批准累積信任,最終達成 Zero-Touch
Args:
action_pattern: 操作模式 (例如: "delete_pod:nginx-*")
user_role: 批准者角色
user_id: 批准者 ID (可選)
Returns:
更新後的 TrustRecord
"""
record = self._get_or_create_record(action_pattern)
# 累積信任
record.score += 1
record.total_approvals += 1
record.last_approval_by = user_id or user_role
record.last_approval_at = datetime.utcnow()
logger.info(
f"[TrustEngine] Approval recorded: {action_pattern} "
f"(score: {record.score}, by: {user_role})"
)
return record
def record_rejection(
self,
action_pattern: str,
user_role: str,
user_id: str | None = None,
reason: str | None = None,
) -> TrustRecord:
"""
記錄人類拒絕
⚠️ 信任瞬間瓦解: Reject 會讓分數歸零或大幅扣分
這確保系統不會因為歷史批准而忽視人類當下的判斷
Args:
action_pattern: 操作模式
user_role: 拒絕者角色
user_id: 拒絕者 ID (可選)
reason: 拒絕原因 (可選)
Returns:
更新後的 TrustRecord
"""
record = self._get_or_create_record(action_pattern)
# 信任瓦解
old_score = record.score
if self.thresholds.reset_on_reject:
record.score = 0 # 歸零
else:
record.score = max(0, record.score + self.thresholds.rejection_penalty)
record.total_rejections += 1
record.last_rejection_by = user_id or user_role
record.last_rejection_at = datetime.utcnow()
logger.warning(
f"[TrustEngine] Rejection recorded: {action_pattern} "
f"(score: {old_score}{record.score}, by: {user_role}, reason: {reason})"
)
return record
def evaluate_adjusted_risk(
self,
action_pattern: str,
original_risk: str | RiskLevel,
) -> RiskAdjustment:
"""
評估調整後的風險等級
根據信任分數決定是否降級風險
降級規則:
- score >= 5: medium → low (自動執行)
- score >= 10: high → medium (雙簽→單簽)
- critical: 永遠不准降級
Args:
action_pattern: 操作模式
original_risk: 原始風險等級
Returns:
RiskAdjustment 包含調整後風險與原因
"""
# 標準化 risk level
if isinstance(original_risk, str):
original_risk = RiskLevel(original_risk.lower())
record = self._get_or_create_record(action_pattern)
score = record.score
# ╔════════════════════════════════════════════════════╗
# ║ CRITICAL 永遠不准降級 - 企業鐵律 ║
# ║ Drop Table, Delete Namespace 等毀滅性操作 ║
# ║ 無論多少次批准,都必須人類雙簽 ║
# ╚════════════════════════════════════════════════════╝
if original_risk == RiskLevel.CRITICAL:
return RiskAdjustment(
original_risk=original_risk,
adjusted_risk=RiskLevel.CRITICAL,
trust_score=score,
reason="CRITICAL operations never auto-downgrade (enterprise policy)",
is_downgraded=False,
)
adjusted_risk = original_risk
reason = "No adjustment"
is_downgraded = False
# HIGH → MEDIUM (score >= 10)
if original_risk == RiskLevel.HIGH and score >= self.thresholds.high_to_medium:
adjusted_risk = RiskLevel.MEDIUM
reason = f"Trust score {score} >= {self.thresholds.high_to_medium}: HIGH → MEDIUM (2-sig → 1-sig)"
is_downgraded = True
# MEDIUM → LOW (score >= 5)
elif original_risk == RiskLevel.MEDIUM and score >= self.thresholds.medium_to_low:
adjusted_risk = RiskLevel.LOW
reason = f"Trust score {score} >= {self.thresholds.medium_to_low}: MEDIUM → LOW (auto-execute)"
is_downgraded = True
# HIGH 但未達降級閾值
elif original_risk == RiskLevel.HIGH and score < self.thresholds.high_to_medium:
reason = f"Trust score {score} < {self.thresholds.high_to_medium}: HIGH maintained"
# MEDIUM 但未達降級閾值
elif original_risk == RiskLevel.MEDIUM and score < self.thresholds.medium_to_low:
reason = f"Trust score {score} < {self.thresholds.medium_to_low}: MEDIUM maintained"
# LOW 已是最低
elif original_risk == RiskLevel.LOW:
reason = "Already at lowest risk level"
if is_downgraded:
logger.info(
f"[TrustEngine] Risk downgraded: {action_pattern} "
f"({original_risk.value}{adjusted_risk.value}, score: {score})"
)
return RiskAdjustment(
original_risk=original_risk,
adjusted_risk=adjusted_risk,
trust_score=score,
reason=reason,
is_downgraded=is_downgraded,
)
def get_trust_record(self, action_pattern: str) -> TrustRecord | None:
"""取得信任記錄"""
return self._records.get(action_pattern)
def get_all_records(self) -> list[TrustRecord]:
"""取得所有信任記錄"""
return list(self._records.values())
def reset_trust(self, action_pattern: str) -> None:
"""重置特定 pattern 的信任分數"""
if action_pattern in self._records:
self._records[action_pattern].score = 0
logger.info(f"[TrustEngine] Trust reset: {action_pattern}")
def reset_all(self) -> None:
"""重置所有信任分數 (緊急用)"""
for record in self._records.values():
record.score = 0
logger.warning("[TrustEngine] All trust scores reset!")
# ==================== Pattern Matching Utilities ====================
def normalize_action_pattern(
operation: str,
parameters: dict,
granularity: Literal["exact", "resource", "operation"] = "resource",
) -> str:
"""
正規化操作為 pattern
granularity 控制信任累積粒度:
- exact: "delete_pod:nginx-frontend-7d4b8c9f5-xk2m3" (精確到實例)
- resource: "delete_pod:nginx-frontend-*" (資源類型)
- operation: "delete_pod:*" (操作類型)
Args:
operation: 操作名稱
parameters: 操作參數
granularity: 粒度
Returns:
正規化後的 pattern
"""
if granularity == "operation":
return f"{operation}:*"
# 嘗試從參數提取資源名稱
resource_name = (
parameters.get("pod_name") or
parameters.get("deployment") or
parameters.get("table_name") or
parameters.get("resource") or
parameters.get("name") or
"*"
)
if granularity == "exact":
return f"{operation}:{resource_name}"
# resource: 提取資源前綴
# nginx-frontend-7d4b8c9f5-xk2m3 → nginx-frontend-*
if isinstance(resource_name, str) and resource_name != "*":
parts = resource_name.rsplit("-", 2)
if len(parts) >= 3:
resource_name = f"{parts[0]}-*"
return f"{operation}:{resource_name}"
# 全域實例
trust_engine = TrustScoreManager()
def get_trust_manager() -> TrustScoreManager:
"""取得 TrustScoreManager singleton (ADR-030 Phase 4)"""
return trust_engine