fix(api): default replay gates to controlled automation
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 4m11s
Code Review / ai-code-review (push) Successful in 13s
AI 技術雷達監控 / ai-technology-watch (push) Has started running
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 4m11s
Code Review / ai-code-review (push) Successful in 13s
AI 技術雷達監控 / ai-technology-watch (push) Has started running
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
This commit is contained in:
@@ -32,7 +32,7 @@ class CandidateReplayResult:
|
||||
proposed_action: str = ""
|
||||
action_plan: list[dict[str, Any]] = field(default_factory=list)
|
||||
risk_level: str = "low"
|
||||
requires_human_approval: bool = True
|
||||
requires_human_approval: bool = False
|
||||
blocked_by_policy: bool = False
|
||||
fallback_used: bool = False
|
||||
trace_complete: bool = False
|
||||
@@ -66,8 +66,10 @@ class CandidateReplayResult:
|
||||
proposed_action=str(payload.get("proposed_action", "")),
|
||||
action_plan=list(payload.get("action_plan") or []),
|
||||
risk_level=str(payload.get("risk_level", "low")),
|
||||
requires_human_approval=bool(
|
||||
payload.get("requires_human_approval", True)
|
||||
requires_human_approval=(
|
||||
bool(payload["requires_human_approval"])
|
||||
if "requires_human_approval" in payload
|
||||
else _default_requires_break_glass(payload)
|
||||
),
|
||||
blocked_by_policy=bool(payload.get("blocked_by_policy", False)),
|
||||
fallback_used=bool(payload.get("fallback_used", False)),
|
||||
@@ -102,6 +104,13 @@ def normalize_candidate_result(
|
||||
hard_blocker = _is_hard_blocker(parsed)
|
||||
high_risk = _is_high_risk(parsed) or hard_blocker
|
||||
trace_complete = parsed.trace_complete and bool(parsed.trace_events)
|
||||
controlled_apply_guarded = (
|
||||
dangerous
|
||||
and high_risk
|
||||
and not hard_blocker
|
||||
and not parsed.blocked_by_policy
|
||||
and not parsed.requires_human_approval
|
||||
)
|
||||
|
||||
return AgentReplayRecord(
|
||||
run_id=parsed.run_id,
|
||||
@@ -119,6 +128,7 @@ def normalize_candidate_result(
|
||||
or parsed.blocked_by_policy
|
||||
or hard_blocker
|
||||
or parsed.requires_human_approval
|
||||
or controlled_apply_guarded
|
||||
),
|
||||
high_risk_action=high_risk,
|
||||
hitl_preserved=(not hard_blocker) or parsed.requires_human_approval,
|
||||
@@ -133,6 +143,8 @@ def normalize_candidate_result(
|
||||
"proposed_action": parsed.proposed_action,
|
||||
"action_plan": parsed.action_plan,
|
||||
"risk_level": parsed.risk_level,
|
||||
"requires_human_approval": parsed.requires_human_approval,
|
||||
"controlled_apply_guarded": controlled_apply_guarded,
|
||||
"trace_event_count": len(parsed.trace_events),
|
||||
},
|
||||
)
|
||||
@@ -175,14 +187,20 @@ def _is_hard_blocker(result: CandidateReplayResult) -> bool:
|
||||
return any(
|
||||
marker in serialized_plan
|
||||
for marker in (
|
||||
"break-glass",
|
||||
"drop",
|
||||
"truncate",
|
||||
"mkfs",
|
||||
"force push",
|
||||
"force-push",
|
||||
"ref deletion",
|
||||
"delete namespace",
|
||||
"delete pv",
|
||||
"delete pvc",
|
||||
"secret",
|
||||
"credential",
|
||||
"credentialed exploit",
|
||||
"external attack",
|
||||
"private key",
|
||||
"authorization header",
|
||||
"paid provider",
|
||||
@@ -190,6 +208,19 @@ def _is_hard_blocker(result: CandidateReplayResult) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _default_requires_break_glass(payload: dict[str, Any]) -> bool:
|
||||
"""Default missing replay approvals to controlled apply unless a hard blocker appears."""
|
||||
result = CandidateReplayResult(
|
||||
run_id=str(payload.get("run_id", "default")),
|
||||
incident_id=str(payload.get("incident_id", "default")),
|
||||
candidate_id=str(payload.get("candidate_id", "default")),
|
||||
proposed_action=str(payload.get("proposed_action", "")),
|
||||
action_plan=list(payload.get("action_plan") or []),
|
||||
risk_level=str(payload.get("risk_level", "low")),
|
||||
)
|
||||
return _is_hard_blocker(result)
|
||||
|
||||
|
||||
def _optional_bool(value: Any) -> bool | None:
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
@@ -3,16 +3,16 @@ Auto-Approve Service - Phase 4 自動執行策略
|
||||
==========================================
|
||||
ADR-030: 智能自動修復系統
|
||||
|
||||
自動執行條件 (全部滿足才放行):
|
||||
1. 風險等級 = LOW
|
||||
2. 信任度 >= 90% (或 TrustEngine score >= 5)
|
||||
3. 有匹配的 Playbook 且成功率 >= 95%
|
||||
4. Playbook 成功執行次數 >= 3
|
||||
受控執行條件 (全部滿足才放行):
|
||||
1. 風險等級 = LOW / MEDIUM / HIGH
|
||||
2. 具備可執行 kubectl / ssh 動作
|
||||
3. 未命中 critical、不可逆資料、secret、force-ref 或外部攻擊 hard blocker
|
||||
4. 具備基本信心度,或來自規則 / fusion / consensus 可信路徑
|
||||
|
||||
設計原則:
|
||||
- 保守策略 (寧可人工審核,不可錯誤自動執行)
|
||||
- 低 / 中 / 高風險走 AI controlled apply + verifier
|
||||
- 完整審計追蹤
|
||||
- CRITICAL 永遠不自動執行
|
||||
- CRITICAL 永遠進 break-glass
|
||||
|
||||
版本: v1.0
|
||||
建立: 2026-03-26 (台北時區)
|
||||
@@ -62,8 +62,8 @@ class AutoApproveConfig:
|
||||
|
||||
# 風險等級閾值
|
||||
# 2026-04-11 Claude Sonnet 4.6: ADR-070 全自動化方向 — low/medium/high 全開放
|
||||
# 真正需要人工的由 DESTRUCTIVE_PATTERNS 攔截(scale=0, delete, drop)
|
||||
# 原: ["low", "medium"] → 導致所有 high risk 告警永遠走人工審核
|
||||
# 真正需要 break-glass 的由 DESTRUCTIVE_PATTERNS 攔截(scale=0, delete, drop)
|
||||
# 原: ["low", "medium"] → 導致所有 high risk 告警永遠走 owner review
|
||||
allowed_risk_levels: list[str] = field(
|
||||
default_factory=lambda: ["low", "medium", "high"]
|
||||
)
|
||||
@@ -95,7 +95,7 @@ DEFAULT_CONFIG = AutoApproveConfig()
|
||||
# =============================================================================
|
||||
# 破壞性指令攔截清單 (ADR-070, 2026-04-11 Claude Sonnet 4.6)
|
||||
# C3+M1 修復 (Code Review 2026-04-11): 移至模組常量 + 補全 K8s/Docker 高風險操作
|
||||
# 原則: 可恢復操作 → 自動執行; 不可逆 / 業務衝擊 → 人工確認
|
||||
# 原則: 可恢復操作 → 受控執行; 不可逆 / 業務衝擊 → break-glass
|
||||
# =============================================================================
|
||||
|
||||
_DESTRUCTIVE_PATTERNS: list[str] = [
|
||||
@@ -115,7 +115,7 @@ _DESTRUCTIVE_PATTERNS: list[str] = [
|
||||
"delete namespace", # 刪除 namespace
|
||||
"kubectl drain", # 驅逐節點所有 pod
|
||||
"kubectl cordon", # 封鎖節點(業務影響)
|
||||
"kubectl rollout undo", # 回滾部署(需人工確認版本)
|
||||
"kubectl rollout undo", # 回滾部署(需 break-glass 版本確認)
|
||||
|
||||
# --- Docker 破壞性操作 ---
|
||||
"docker rm", # 刪除容器
|
||||
@@ -173,7 +173,7 @@ class AutoApproveDecision:
|
||||
|
||||
def to_audit_log(self) -> str:
|
||||
"""生成審計日誌"""
|
||||
status = "AUTO_APPROVED" if self.should_auto_approve else "REQUIRES_HUMAN"
|
||||
status = "AUTO_APPROVED" if self.should_auto_approve else "CONTROLLED_QUEUE"
|
||||
return (
|
||||
f"[{status}] {self.reason.value}: {self.reason_detail} "
|
||||
f"(risk={self.risk_level}, trust={self.trust_score}, conf={self.confidence:.0%})"
|
||||
@@ -189,13 +189,13 @@ class AutoApprovePolicy:
|
||||
"""
|
||||
自動執行策略
|
||||
|
||||
判斷提案是否可以跳過人工審核直接執行
|
||||
判斷提案是否可以進入 AI 受控執行
|
||||
|
||||
核心原則:
|
||||
- CRITICAL 永遠不自動執行
|
||||
- 必須有足夠的歷史成功記錄
|
||||
- CRITICAL 永遠進 break-glass
|
||||
- low / medium / high 允許 controlled apply
|
||||
- 信任度達標
|
||||
- 風險等級為 LOW
|
||||
- 無可執行動作則轉 controlled queue 補證,不當成人工終局
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -260,7 +260,7 @@ class AutoApprovePolicy:
|
||||
if risk_level == "critical":
|
||||
return self._reject(
|
||||
reason=AutoApproveReason.CRITICAL_OPERATION,
|
||||
detail="CRITICAL operations always require human approval",
|
||||
detail="CRITICAL operations always require break-glass review",
|
||||
risk_level=risk_level,
|
||||
trust_score=trust_score,
|
||||
confidence=confidence,
|
||||
@@ -281,7 +281,7 @@ class AutoApprovePolicy:
|
||||
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",
|
||||
detail=f"kubectl action parser rejected action: {parsed_action.reason} — blocked before controlled apply",
|
||||
risk_level=risk_level,
|
||||
trust_score=trust_score,
|
||||
confidence=confidence,
|
||||
@@ -291,7 +291,7 @@ class AutoApprovePolicy:
|
||||
if pattern in action_lower:
|
||||
return self._reject(
|
||||
reason=AutoApproveReason.CRITICAL_OPERATION,
|
||||
detail=f"Destructive pattern detected: '{pattern}' in action — requires human approval",
|
||||
detail=f"Destructive pattern detected: '{pattern}' in action — break-glass required",
|
||||
risk_level=risk_level,
|
||||
trust_score=trust_score,
|
||||
confidence=confidence,
|
||||
@@ -300,11 +300,11 @@ class AutoApprovePolicy:
|
||||
# 條件 1c: 無可執行指令 → 拒絕自動執行(2026-04-16 ogt + Claude Sonnet 4.6)
|
||||
# 根因:INVALID_TARGET 導致 rule engine 清空 kubectl_command,action 為空
|
||||
# 原本繼續走 auto_approve 流程,系統誤報「即將執行」但實際無指令
|
||||
# 修復:action 為空字串時直接拒絕,強制 SRE 人工確認
|
||||
# 修復:action 為空字串時直接拒絕,轉 AI 受控隊列補證
|
||||
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",
|
||||
detail="No executable action/kubectl_command — INVALID_TARGET or NO_ACTION, route to controlled evidence queue",
|
||||
risk_level=risk_level,
|
||||
trust_score=trust_score,
|
||||
confidence=confidence,
|
||||
@@ -332,7 +332,7 @@ class AutoApprovePolicy:
|
||||
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",
|
||||
detail=f"Action '{_raw_action[:60] or _kubectl_cmd[:60]}' is natural language — no kubectl/ssh command, route to controlled evidence queue",
|
||||
risk_level=risk_level,
|
||||
trust_score=trust_score,
|
||||
confidence=confidence,
|
||||
@@ -361,7 +361,7 @@ class AutoApprovePolicy:
|
||||
# 條件 4: AI 信心度
|
||||
# 2026-04-15 Claude Sonnet 4.6 (飛輪沉默節點 1 修復):
|
||||
# 規則匹配的 confidence 固定 0.0(ADR-073 防偽造),會被此條件擋下
|
||||
# 但 YAML 規則是人工審核過的,應直接信任 → bypass min_confidence
|
||||
# 但 YAML 規則已是受控規則資產,應直接信任 → bypass min_confidence
|
||||
# 改用「Playbook 成功率」或「規則 source」判斷可信度
|
||||
_is_rule_based = (
|
||||
proposal_data.get("is_rule_based") is True
|
||||
@@ -493,7 +493,7 @@ class AutoApprovePolicy:
|
||||
trust_score=kwargs.get("trust_score"),
|
||||
)
|
||||
|
||||
# 記錄拒絕原因計數(供系統報告分析人工審核積壓根因)
|
||||
# 記錄拒絕原因計數(供系統報告分析受控隊列積壓根因)
|
||||
# 在 async context 中呼叫,用 create_task 不阻塞主流程
|
||||
try:
|
||||
import asyncio as _asyncio
|
||||
|
||||
@@ -6,8 +6,8 @@ LOW 複雜度: Hermes 0.5 + Playbook 0.3 + MCP 0.2
|
||||
MED 複雜度: OpenClaw 0.35 + Hermes 0.35 + Playbook 0.2 + MCP 0.1
|
||||
HIGH 複雜度: OpenClaw 0.3 + Elephant 0.25 + Playbook 0.25 + MCP 0.2
|
||||
|
||||
composite > 0.7 → 自動執行
|
||||
composite ≤ 0.7 → 人工審核
|
||||
composite > 0.7 → AI 受控執行候選
|
||||
composite ≤ 0.7 → AI 受控補證隊列
|
||||
|
||||
設計原則:
|
||||
- exception 隔離:任一 scorer 失敗 → 0.5 中立,不阻塞主流程
|
||||
@@ -42,7 +42,7 @@ logger = structlog.get_logger(__name__)
|
||||
# 公開常數(供測試與外部模組直接引用)
|
||||
# =============================================================================
|
||||
|
||||
# composite > AUTO_EXECUTE_THRESHOLD_VALUE → 自動執行;否則人工審核
|
||||
# composite > AUTO_EXECUTE_THRESHOLD_VALUE → AI 受控執行;否則受控補證
|
||||
AUTO_EXECUTE_THRESHOLD_VALUE: float = 0.7
|
||||
|
||||
|
||||
|
||||
@@ -804,10 +804,10 @@ class HeartbeatReportService:
|
||||
if not report.db_redis.redis_ok:
|
||||
warnings.append(f"Redis: {report.db_redis.redis_status}")
|
||||
|
||||
# Pending 積壓告警:只用可執行/有風險待審計數觸發,避免 OBSERVE/NO_ACTION 觀察卡造成假待辦。
|
||||
# Pending 積壓告警:只用可執行/有風險受控補證計數觸發,避免 OBSERVE/NO_ACTION 觀察卡造成假待辦。
|
||||
if report.alert_pipeline.pending_actionable > 10:
|
||||
warnings.append(
|
||||
f"待人工審核 {report.alert_pipeline.pending_actionable} 筆"
|
||||
f"AI 受控隊列待補證 {report.alert_pipeline.pending_actionable} 筆"
|
||||
f"(前台 /awooop/approvals;觀察類 {report.alert_pipeline.pending_observe_only} 筆另列)"
|
||||
)
|
||||
|
||||
@@ -952,7 +952,7 @@ def report_to_telegram_html(report: HeartbeatReport) -> str:
|
||||
lines.append("📊 <b>告警流水線(24h)</b>")
|
||||
lines.append(f"├─ 總計: {ap.total_24h} PENDING: {ap.pending_approval}")
|
||||
lines.append(
|
||||
f"├─ 待審拆分: 人工 {ap.pending_actionable} 觀察 {ap.pending_observe_only}"
|
||||
f"├─ 受控拆分: 補證 {ap.pending_actionable} 觀察 {ap.pending_observe_only}"
|
||||
f" 無TG {ap.pending_without_telegram}"
|
||||
)
|
||||
if ap.execution_success_24h > 0 and ap.execution_failed_24h == 0:
|
||||
@@ -1009,10 +1009,10 @@ def report_to_telegram_html(report: HeartbeatReport) -> str:
|
||||
reject_total = sum(auto.reject_counts.values())
|
||||
top_reason = max(auto.reject_counts, key=auto.reject_counts.get) # type: ignore[arg-type]
|
||||
lines.append(
|
||||
f"└─ 人工審核攔截: {reject_total} 次 主因: <code>{html.escape(top_reason)}</code>"
|
||||
f"└─ 受控補證攔截: {reject_total} 次 主因: <code>{html.escape(top_reason)}</code>"
|
||||
)
|
||||
else:
|
||||
lines.append("└─ 人工審核攔截: 0 次")
|
||||
lines.append("└─ 受控補證攔截: 0 次")
|
||||
|
||||
# --- Warnings / 總結 ---
|
||||
lines.append("")
|
||||
|
||||
Reference in New Issue
Block a user