563 lines
23 KiB
Python
563 lines
23 KiB
Python
"""
|
||
Auto-Approve Service - Phase 4 自動執行策略
|
||
==========================================
|
||
ADR-030: 智能自動修復系統
|
||
|
||
自動執行條件 (全部滿足才放行):
|
||
1. 風險等級 = LOW
|
||
2. 信任度 >= 90% (或 TrustEngine score >= 5)
|
||
3. 有匹配的 Playbook 且成功率 >= 95%
|
||
4. Playbook 成功執行次數 >= 3
|
||
|
||
設計原則:
|
||
- 保守策略 (寧可人工審核,不可錯誤自動執行)
|
||
- 完整審計追蹤
|
||
- CRITICAL 永遠不自動執行
|
||
|
||
版本: v1.0
|
||
建立: 2026-03-26 (台北時區)
|
||
"""
|
||
|
||
from dataclasses import dataclass, field
|
||
from datetime import UTC, datetime
|
||
from enum import Enum
|
||
from typing import Any
|
||
|
||
import structlog
|
||
|
||
from src.models.playbook import Playbook
|
||
from src.services.action_parser import parse_kubectl_action
|
||
from src.services.playbook_rag import PlaybookMatch
|
||
from src.services.trust_engine import TrustScoreManager, get_trust_manager
|
||
|
||
logger = structlog.get_logger(__name__)
|
||
|
||
|
||
# =============================================================================
|
||
# Configuration
|
||
# =============================================================================
|
||
|
||
|
||
class AutoApproveReason(str, Enum):
|
||
"""自動執行/拒絕原因"""
|
||
|
||
# 自動執行
|
||
PLAYBOOK_MATCH = "playbook_match" # Playbook 匹配成功
|
||
TRUST_SCORE = "trust_score" # 信任分數達標
|
||
LOW_RISK = "low_risk" # 低風險操作
|
||
|
||
# 拒絕自動執行
|
||
HIGH_RISK = "high_risk" # 風險過高
|
||
CRITICAL_OPERATION = "critical_operation" # 關鍵操作
|
||
LOW_TRUST = "low_trust" # 信任不足
|
||
NO_PLAYBOOK = "no_playbook" # 無匹配 Playbook
|
||
NO_EXECUTABLE_ACTION = "no_executable_action" # action 為自然語言,無法執行
|
||
LOW_SUCCESS_RATE = "low_success_rate" # Playbook 成功率不足
|
||
INSUFFICIENT_HISTORY = "insufficient_history" # 執行歷史不足
|
||
|
||
|
||
@dataclass
|
||
class AutoApproveConfig:
|
||
"""自動執行配置"""
|
||
|
||
# 風險等級閾值
|
||
# 2026-04-11 Claude Sonnet 4.6: ADR-070 全自動化方向 — low/medium/high 全開放
|
||
# 真正需要人工的由 DESTRUCTIVE_PATTERNS 攔截(scale=0, delete, drop)
|
||
# 原: ["low", "medium"] → 導致所有 high risk 告警永遠走人工審核
|
||
allowed_risk_levels: list[str] = field(
|
||
default_factory=lambda: ["low", "medium", "high"]
|
||
)
|
||
|
||
# 信任度閾值
|
||
# 2026-04-10 Claude Sonnet 4.6: trust_score 是 in-memory,Pod 重啟歸零
|
||
# → 改為 0,讓 medium risk + confidence >= 0.65 的操作直接自動執行
|
||
# 歷史原因: min_trust_score=1 導致所有告警永遠走審批,從未自動修復
|
||
min_trust_score: int = 0 # 不要求執行歷史 (原: 1)
|
||
# 2026-04-11 Claude Sonnet 4.6: ADR-070 全自動化 — 0.5 即可執行
|
||
# 真正風險由 DESTRUCTIVE_PATTERNS + risk_level=critical 把關
|
||
min_confidence: float = 0.50 # AI 有基本把握即可 (原: 0.90, 後: 0.65)
|
||
|
||
# Playbook 閾值
|
||
# 2026-04-01 ogt: 降低啟動門檻,1次成功記錄即可
|
||
min_playbook_success_rate: float = 0.80 # 成功率 >= 80% (原: 95%)
|
||
min_playbook_success_count: int = 1 # 成功次數 >= 1 (原: 3)
|
||
|
||
# 功能開關
|
||
enabled: bool = True # 總開關
|
||
require_playbook: bool = False # 不強制要求 Playbook (原: True),Gemini 分析直接可執行
|
||
audit_all: bool = True # 是否記錄所有判斷
|
||
|
||
|
||
# 預設配置 (實用策略 - 2026-04-01 ogt Phase 22 P1)
|
||
DEFAULT_CONFIG = AutoApproveConfig()
|
||
|
||
|
||
# =============================================================================
|
||
# 破壞性指令攔截清單 (ADR-070, 2026-04-11 Claude Sonnet 4.6)
|
||
# C3+M1 修復 (Code Review 2026-04-11): 移至模組常量 + 補全 K8s/Docker 高風險操作
|
||
# 原則: 可恢復操作 → 自動執行; 不可逆 / 業務衝擊 → 人工確認
|
||
# =============================================================================
|
||
|
||
_DESTRUCTIVE_PATTERNS: list[str] = [
|
||
# --- Scale to zero (停機) ---
|
||
"--replicas=0", # kubectl scale --replicas=0
|
||
'"replicas": 0', # kubectl patch JSON patch
|
||
"'replicas': 0", # kubectl patch 單引號變體
|
||
"replicas=0", # 任何形式的 replicas=0
|
||
|
||
# --- K8s 刪除操作 ---
|
||
"delete pod --all", # 批次刪除 pod
|
||
"delete pod -A", # 跨 namespace 刪除 pod
|
||
"delete pod --all-namespaces",
|
||
"delete pods", # 複數形式
|
||
"delete deployment", # 刪除 deployment
|
||
"delete pvc", # 刪除 PVC (資料丟失)
|
||
"delete namespace", # 刪除 namespace
|
||
"kubectl drain", # 驅逐節點所有 pod
|
||
"kubectl cordon", # 封鎖節點(業務影響)
|
||
"kubectl rollout undo", # 回滾部署(需人工確認版本)
|
||
|
||
# --- Docker 破壞性操作 ---
|
||
"docker rm", # 刪除容器
|
||
"docker stop", # 停止容器(不同於 restart)
|
||
"docker kill", # 強制殺死容器
|
||
|
||
# --- DB DDL (不可逆) ---
|
||
"drop table",
|
||
"drop database",
|
||
"truncate table",
|
||
|
||
# --- SSH 危險指令 ---
|
||
"rm -rf", # 遞迴刪除
|
||
"rm -f /", # 刪除根目錄
|
||
]
|
||
|
||
|
||
# =============================================================================
|
||
# Data Models
|
||
# =============================================================================
|
||
|
||
|
||
@dataclass
|
||
class AutoApproveDecision:
|
||
"""自動執行決策結果"""
|
||
|
||
should_auto_approve: bool
|
||
reason: AutoApproveReason
|
||
reason_detail: str
|
||
|
||
# 判斷依據
|
||
risk_level: str
|
||
trust_score: int
|
||
confidence: float
|
||
playbook_match: PlaybookMatch | None = None
|
||
playbook_success_rate: float | None = None
|
||
playbook_success_count: int | None = None
|
||
|
||
# 時間戳
|
||
decided_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||
|
||
def to_dict(self) -> dict[str, Any]:
|
||
return {
|
||
"should_auto_approve": self.should_auto_approve,
|
||
"reason": self.reason.value,
|
||
"reason_detail": self.reason_detail,
|
||
"risk_level": self.risk_level,
|
||
"trust_score": self.trust_score,
|
||
"confidence": self.confidence,
|
||
"playbook_match": self.playbook_match.to_dict() if self.playbook_match else None,
|
||
"playbook_success_rate": self.playbook_success_rate,
|
||
"playbook_success_count": self.playbook_success_count,
|
||
"decided_at": self.decided_at.isoformat(),
|
||
}
|
||
|
||
def to_audit_log(self) -> str:
|
||
"""生成審計日誌"""
|
||
status = "AUTO_APPROVED" if self.should_auto_approve else "REQUIRES_HUMAN"
|
||
return (
|
||
f"[{status}] {self.reason.value}: {self.reason_detail} "
|
||
f"(risk={self.risk_level}, trust={self.trust_score}, conf={self.confidence:.0%})"
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# Auto-Approve Policy
|
||
# =============================================================================
|
||
|
||
|
||
class AutoApprovePolicy:
|
||
"""
|
||
自動執行策略
|
||
|
||
判斷提案是否可以跳過人工審核直接執行
|
||
|
||
核心原則:
|
||
- CRITICAL 永遠不自動執行
|
||
- 必須有足夠的歷史成功記錄
|
||
- 信任度達標
|
||
- 風險等級為 LOW
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
config: AutoApproveConfig | None = None,
|
||
trust_manager: TrustScoreManager | None = None,
|
||
):
|
||
self.config = config or DEFAULT_CONFIG
|
||
self._trust_manager = trust_manager
|
||
|
||
@property
|
||
def trust_manager(self) -> TrustScoreManager:
|
||
"""Lazy load trust manager"""
|
||
if self._trust_manager is None:
|
||
self._trust_manager = get_trust_manager()
|
||
return self._trust_manager
|
||
|
||
def evaluate(
|
||
self,
|
||
proposal_data: dict[str, Any],
|
||
playbook: Playbook | None = None,
|
||
playbook_match: PlaybookMatch | None = None,
|
||
) -> AutoApproveDecision:
|
||
"""
|
||
評估提案是否可自動執行
|
||
|
||
Args:
|
||
proposal_data: 提案資料 (含 risk_level, confidence, action 等)
|
||
playbook: 匹配的 Playbook (可選)
|
||
playbook_match: RAG 匹配結果 (可選)
|
||
|
||
Returns:
|
||
AutoApproveDecision 包含決策結果和原因
|
||
"""
|
||
# 基本資訊
|
||
risk_level = proposal_data.get("risk_level", "medium").lower()
|
||
confidence = proposal_data.get("confidence", 0.0) # 🔴 無信心度=規則匹配
|
||
action = proposal_data.get("action", "") or proposal_data.get("kubectl_command", "")
|
||
action_pattern = self._extract_action_pattern(action)
|
||
|
||
# 取得信任分數
|
||
trust_record = self.trust_manager.get_trust_record(action_pattern)
|
||
trust_score = trust_record.score if trust_record else 0
|
||
|
||
# Playbook 資訊
|
||
playbook_success_rate = playbook.success_rate if playbook else None
|
||
playbook_success_count = playbook.success_count if playbook else None
|
||
|
||
# ========== 檢查條件 ==========
|
||
|
||
# 條件 0: 功能是否啟用
|
||
if not self.config.enabled:
|
||
return self._reject(
|
||
reason=AutoApproveReason.LOW_TRUST,
|
||
detail="Auto-approve is disabled",
|
||
risk_level=risk_level,
|
||
trust_score=trust_score,
|
||
confidence=confidence,
|
||
)
|
||
|
||
# 條件 1: CRITICAL 永遠不自動執行
|
||
if risk_level == "critical":
|
||
return self._reject(
|
||
reason=AutoApproveReason.CRITICAL_OPERATION,
|
||
detail="CRITICAL operations always require human approval",
|
||
risk_level=risk_level,
|
||
trust_score=trust_score,
|
||
confidence=confidence,
|
||
)
|
||
|
||
# 條件 1b: structured action parser 安全閘 (SPF-2, 2026-04-30)
|
||
# kubectl 指令以 token grammar 判斷,避免 substring regex 誤殺
|
||
# `kubectl delete pod <one-pod>`,同時攔截 delete deployment /
|
||
# delete --all / rollout undo / replicas=0 / shell injection。
|
||
action_stripped = action.strip()
|
||
action_lower = action_stripped.lower()
|
||
kubectl_cmd_raw = str(proposal_data.get("kubectl_command", "") or "").strip()
|
||
kubectl_candidate = kubectl_cmd_raw
|
||
if not kubectl_candidate and "kubectl" in action_lower:
|
||
kubectl_candidate = action_stripped[action_lower.index("kubectl"):].strip()
|
||
if kubectl_candidate.lower().startswith("kubectl"):
|
||
parsed_action = parse_kubectl_action(kubectl_candidate)
|
||
if not parsed_action.ok:
|
||
return self._reject(
|
||
reason=AutoApproveReason.CRITICAL_OPERATION,
|
||
detail=f"kubectl action parser rejected action: {parsed_action.reason} — requires human approval",
|
||
risk_level=risk_level,
|
||
trust_score=trust_score,
|
||
confidence=confidence,
|
||
)
|
||
else:
|
||
for pattern in _DESTRUCTIVE_PATTERNS:
|
||
if pattern in action_lower:
|
||
return self._reject(
|
||
reason=AutoApproveReason.CRITICAL_OPERATION,
|
||
detail=f"Destructive pattern detected: '{pattern}' in action — requires human approval",
|
||
risk_level=risk_level,
|
||
trust_score=trust_score,
|
||
confidence=confidence,
|
||
)
|
||
|
||
# 條件 1c: 無可執行指令 → 拒絕自動執行(2026-04-16 ogt + Claude Sonnet 4.6)
|
||
# 根因:INVALID_TARGET 導致 rule engine 清空 kubectl_command,action 為空
|
||
# 原本繼續走 auto_approve 流程,系統誤報「即將執行」但實際無指令
|
||
# 修復:action 為空字串時直接拒絕,強制 SRE 人工確認
|
||
if not action.strip():
|
||
return self._reject(
|
||
reason=AutoApproveReason.NO_PLAYBOOK,
|
||
detail="No executable action/kubectl_command — INVALID_TARGET or NO_ACTION, requires human review",
|
||
risk_level=risk_level,
|
||
trust_score=trust_score,
|
||
confidence=confidence,
|
||
)
|
||
|
||
# 條件 1d: 自然語言描述不可自動執行(2026-04-17 ogt + Claude Sonnet 4.6)
|
||
# 根因:Solver 經 OpenClaw Nemo 路徑輸出「重啟 Crash Looping Pod」等自然語言
|
||
# action 非空 → 條件 1c 通過 → auto_approved=True
|
||
# 但 kubectl_command 為空 → 實際無法執行 → incident 卡在 investigating
|
||
# 修復:直接讀 proposal_data["action"] 原始值(非 fallback 後的 action 變數)
|
||
# 避免「action 空 → fallback 成 kubectl_command → action 含 kubectl → 誤放行」
|
||
# Code Review 2026-04-17: P0-1 修正 action fallback 語意混淆
|
||
# P1-2 改用 NO_EXECUTABLE_ACTION(避免污染 KM 飛輪學習資料)
|
||
_raw_action = proposal_data.get("action", "") or ""
|
||
_kubectl_cmd = proposal_data.get("kubectl_command", "") or ""
|
||
# 2026-04-27 Claude Sonnet 4.6: 擴充可執行指令識別,加入 SSH 診斷路徑
|
||
# 根因:_has_kubectl 只認 kubectl,SSH 診斷指令(主機告警)被全部攔截 → 飛輪停轉
|
||
# 修復:ssh {host} '...' 格式也是可執行指令,允許走 _ssh_execute() 路徑
|
||
_has_executable = (
|
||
"kubectl" in _raw_action.lower()
|
||
or "kubectl" in _kubectl_cmd.lower()
|
||
or _raw_action.lower().strip().startswith("ssh ")
|
||
or _kubectl_cmd.lower().strip().startswith("ssh ")
|
||
)
|
||
if not _has_executable:
|
||
return self._reject(
|
||
reason=AutoApproveReason.NO_EXECUTABLE_ACTION,
|
||
detail=f"Action '{_raw_action[:60] or _kubectl_cmd[:60]}' is natural language — no kubectl/ssh command, requires human review",
|
||
risk_level=risk_level,
|
||
trust_score=trust_score,
|
||
confidence=confidence,
|
||
)
|
||
|
||
# 條件 2: 風險等級必須在允許列表中
|
||
if risk_level not in self.config.allowed_risk_levels:
|
||
return self._reject(
|
||
reason=AutoApproveReason.HIGH_RISK,
|
||
detail=f"Risk level '{risk_level}' not in allowed list {self.config.allowed_risk_levels}",
|
||
risk_level=risk_level,
|
||
trust_score=trust_score,
|
||
confidence=confidence,
|
||
)
|
||
|
||
# 條件 3: 信任分數
|
||
if trust_score < self.config.min_trust_score:
|
||
return self._reject(
|
||
reason=AutoApproveReason.LOW_TRUST,
|
||
detail=f"Trust score {trust_score} < {self.config.min_trust_score}",
|
||
risk_level=risk_level,
|
||
trust_score=trust_score,
|
||
confidence=confidence,
|
||
)
|
||
|
||
# 條件 4: AI 信心度
|
||
# 2026-04-15 Claude Sonnet 4.6 (飛輪沉默節點 1 修復):
|
||
# 規則匹配的 confidence 固定 0.0(ADR-073 防偽造),會被此條件擋下
|
||
# 但 YAML 規則是人工審核過的,應直接信任 → bypass min_confidence
|
||
# 改用「Playbook 成功率」或「規則 source」判斷可信度
|
||
_is_rule_based = (
|
||
proposal_data.get("is_rule_based") is True
|
||
or proposal_data.get("source") == "expert_system"
|
||
or (proposal_data.get("rule_id") or "") != ""
|
||
or (proposal_data.get("matched_rule") or "") != ""
|
||
# 2026-04-24 ogt + Claude Sonnet 4.6: Phase 2 五 agent 協作輸出 bypass confidence 閾值
|
||
# 根因:phase2_agent_debate 的 is_rule_based=False + confidence 低 → 被誤攔截
|
||
# 修法:識別 phase2_agent_debate source,視同規則可信路徑
|
||
or (proposal_data.get("source") or "").startswith("phase2_agent_debate")
|
||
# 2026-04-27 Wave8-B3 by Claude — fusion 三斷鏈修復:
|
||
# P2.1 fusion composite > 0.7 → auto_execute_eligible,bypass min_confidence 閾值
|
||
# auto_execute_eligible 是 FusionScore.to_dict() 的 bool 欄位
|
||
or (
|
||
proposal_data.get("decision_fusion", {}).get("auto_execute_eligible") is True
|
||
)
|
||
# 2026-04-27 Wave8-B5 by Claude — Consensus auto_approve 不認修復:
|
||
# source=consensus_engine + consensus_score >= 0.6 → 視同規則可信路徑
|
||
or (
|
||
proposal_data.get("source") == "consensus_engine"
|
||
and float(proposal_data.get("consensus_score", 0)) >= 0.6
|
||
)
|
||
)
|
||
if not _is_rule_based and confidence < self.config.min_confidence:
|
||
return self._reject(
|
||
reason=AutoApproveReason.LOW_TRUST,
|
||
detail=f"Confidence {confidence:.0%} < {self.config.min_confidence:.0%}",
|
||
risk_level=risk_level,
|
||
trust_score=trust_score,
|
||
confidence=confidence,
|
||
)
|
||
|
||
# 條件 5: Playbook 匹配 (如果要求)
|
||
if self.config.require_playbook:
|
||
if playbook is None:
|
||
return self._reject(
|
||
reason=AutoApproveReason.NO_PLAYBOOK,
|
||
detail="No matching Playbook found",
|
||
risk_level=risk_level,
|
||
trust_score=trust_score,
|
||
confidence=confidence,
|
||
)
|
||
|
||
# 條件 6: Playbook 成功率
|
||
if playbook_success_rate is not None:
|
||
if playbook_success_rate < self.config.min_playbook_success_rate:
|
||
return self._reject(
|
||
reason=AutoApproveReason.LOW_SUCCESS_RATE,
|
||
detail=f"Playbook success rate {playbook_success_rate:.0%} < {self.config.min_playbook_success_rate:.0%}",
|
||
risk_level=risk_level,
|
||
trust_score=trust_score,
|
||
confidence=confidence,
|
||
playbook_match=playbook_match,
|
||
playbook_success_rate=playbook_success_rate,
|
||
playbook_success_count=playbook_success_count,
|
||
)
|
||
|
||
# 條件 7: Playbook 成功次數
|
||
if playbook_success_count is not None:
|
||
if playbook_success_count < self.config.min_playbook_success_count:
|
||
return self._reject(
|
||
reason=AutoApproveReason.INSUFFICIENT_HISTORY,
|
||
detail=f"Playbook success count {playbook_success_count} < {self.config.min_playbook_success_count}",
|
||
risk_level=risk_level,
|
||
trust_score=trust_score,
|
||
confidence=confidence,
|
||
playbook_match=playbook_match,
|
||
playbook_success_rate=playbook_success_rate,
|
||
playbook_success_count=playbook_success_count,
|
||
)
|
||
|
||
# ========== 所有條件通過 ==========
|
||
return self._approve(
|
||
reason=AutoApproveReason.PLAYBOOK_MATCH if playbook else AutoApproveReason.TRUST_SCORE,
|
||
detail=f"All conditions met: risk={risk_level}, trust={trust_score}, confidence={confidence:.0%}",
|
||
risk_level=risk_level,
|
||
trust_score=trust_score,
|
||
confidence=confidence,
|
||
playbook_match=playbook_match,
|
||
playbook_success_rate=playbook_success_rate,
|
||
playbook_success_count=playbook_success_count,
|
||
)
|
||
|
||
def _approve(
|
||
self,
|
||
reason: AutoApproveReason,
|
||
detail: str,
|
||
**kwargs,
|
||
) -> AutoApproveDecision:
|
||
"""建立自動執行決策"""
|
||
decision = AutoApproveDecision(
|
||
should_auto_approve=True,
|
||
reason=reason,
|
||
reason_detail=detail,
|
||
**kwargs,
|
||
)
|
||
|
||
if self.config.audit_all:
|
||
logger.info(
|
||
"auto_approve_decision",
|
||
approved=True,
|
||
reason=reason.value,
|
||
detail=detail,
|
||
trust_score=kwargs.get("trust_score"),
|
||
)
|
||
|
||
return decision
|
||
|
||
def _reject(
|
||
self,
|
||
reason: AutoApproveReason,
|
||
detail: str,
|
||
**kwargs,
|
||
) -> AutoApproveDecision:
|
||
"""建立拒絕自動執行決策"""
|
||
decision = AutoApproveDecision(
|
||
should_auto_approve=False,
|
||
reason=reason,
|
||
reason_detail=detail,
|
||
**kwargs,
|
||
)
|
||
|
||
if self.config.audit_all:
|
||
logger.debug(
|
||
"auto_approve_decision",
|
||
approved=False,
|
||
reason=reason.value,
|
||
detail=detail,
|
||
trust_score=kwargs.get("trust_score"),
|
||
)
|
||
|
||
# 記錄拒絕原因計數(供系統報告分析人工審核積壓根因)
|
||
# 在 async context 中呼叫,用 create_task 不阻塞主流程
|
||
try:
|
||
import asyncio as _asyncio
|
||
from datetime import datetime as _dt
|
||
_today = _dt.now().strftime("%Y%m%d")
|
||
_reject_key = f"stats:auto_approve_rejected:{reason.value}:{_today}"
|
||
|
||
async def _incr_reject_stat() -> None:
|
||
try:
|
||
from src.core.redis_client import get_redis as _get_redis
|
||
_r = _get_redis()
|
||
await _r.incr(_reject_key)
|
||
await _r.expire(_reject_key, 86400 * 7)
|
||
except Exception:
|
||
pass # Redis 不可用時靜默降級,不影響核心流程
|
||
|
||
loop = _asyncio.get_running_loop()
|
||
loop.create_task(_incr_reject_stat())
|
||
except RuntimeError:
|
||
pass # 非 async context(如單元測試),靜默跳過
|
||
|
||
return decision
|
||
|
||
def _extract_action_pattern(self, action: str) -> str:
|
||
"""
|
||
從 action 字串提取 pattern
|
||
|
||
例如:
|
||
- "kubectl rollout restart deployment/awoooi-api" → "rollout_restart:awoooi-api"
|
||
- "kubectl scale deployment/nginx --replicas=3" → "scale:nginx"
|
||
"""
|
||
if not action:
|
||
return "unknown"
|
||
|
||
parts = action.split()
|
||
if len(parts) < 3:
|
||
return "unknown"
|
||
|
||
# kubectl <verb> <resource>/<name>
|
||
verb = parts[1] if len(parts) > 1 else "unknown"
|
||
resource_part = parts[2] if len(parts) > 2 else ""
|
||
|
||
if "/" in resource_part:
|
||
resource_name = resource_part.split("/")[-1]
|
||
else:
|
||
resource_name = resource_part
|
||
|
||
# 移除可能的選項
|
||
resource_name = resource_name.split()[0] if " " in resource_name else resource_name
|
||
|
||
return f"{verb}:{resource_name}"
|
||
|
||
|
||
# =============================================================================
|
||
# Singleton
|
||
# =============================================================================
|
||
|
||
_auto_approve_policy: AutoApprovePolicy | None = None
|
||
|
||
|
||
def get_auto_approve_policy() -> AutoApprovePolicy:
|
||
"""取得自動執行策略 singleton"""
|
||
global _auto_approve_policy
|
||
if _auto_approve_policy is None:
|
||
_auto_approve_policy = AutoApprovePolicy()
|
||
return _auto_approve_policy
|