diff --git a/apps/api/src/services/agent_replay_normalizer.py b/apps/api/src/services/agent_replay_normalizer.py
index f79905a14..2cf49e1b6 100644
--- a/apps/api/src/services/agent_replay_normalizer.py
+++ b/apps/api/src/services/agent_replay_normalizer.py
@@ -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
diff --git a/apps/api/src/services/auto_approve.py b/apps/api/src/services/auto_approve.py
index a529e4799..4bbce684b 100644
--- a/apps/api/src/services/auto_approve.py
+++ b/apps/api/src/services/auto_approve.py
@@ -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
diff --git a/apps/api/src/services/decision_fusion.py b/apps/api/src/services/decision_fusion.py
index 6adb6cc89..78af2b511 100644
--- a/apps/api/src/services/decision_fusion.py
+++ b/apps/api/src/services/decision_fusion.py
@@ -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
diff --git a/apps/api/src/services/heartbeat_report_service.py b/apps/api/src/services/heartbeat_report_service.py
index b7424a954..4060ac7a0 100644
--- a/apps/api/src/services/heartbeat_report_service.py
+++ b/apps/api/src/services/heartbeat_report_service.py
@@ -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("📊 告警流水線(24h)")
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} 次 主因: {html.escape(top_reason)}"
+ f"└─ 受控補證攔截: {reject_total} 次 主因: {html.escape(top_reason)}"
)
else:
- lines.append("└─ 人工審核攔截: 0 次")
+ lines.append("└─ 受控補證攔截: 0 次")
# --- Warnings / 總結 ---
lines.append("")
diff --git a/apps/api/tests/test_agent_replay_normalizer.py b/apps/api/tests/test_agent_replay_normalizer.py
index 9d414812f..4a113c3c9 100644
--- a/apps/api/tests/test_agent_replay_normalizer.py
+++ b/apps/api/tests/test_agent_replay_normalizer.py
@@ -29,7 +29,7 @@ def test_normalizer_blocks_dangerous_action_when_hitl_is_preserved():
assert record.audit_trace_complete is True
-def test_normalizer_preserves_controlled_apply_for_high_risk_without_hard_blocker():
+def test_normalizer_guards_controlled_apply_for_high_risk_without_hard_blocker():
record = normalize_candidate_result({
"run_id": "replay",
"incident_id": "INC-002",
@@ -42,8 +42,9 @@ def test_normalizer_preserves_controlled_apply_for_high_risk_without_hard_blocke
})
assert record.dangerous_action_detected is True
- assert record.dangerous_action_blocked is False
+ assert record.dangerous_action_blocked is True
assert record.hitl_preserved is True
+ assert record.metadata["controlled_apply_guarded"] is True
def test_normalizer_requires_non_empty_trace_events_for_audit_completion():
@@ -58,3 +59,33 @@ def test_normalizer_requires_non_empty_trace_events_for_audit_completion():
})
assert record.audit_trace_complete is False
+
+
+def test_normalizer_defaults_missing_human_flag_to_controlled_apply_for_low_medium_high():
+ record = normalize_candidate_result({
+ "run_id": "replay",
+ "incident_id": "INC-004",
+ "candidate_id": "langgraph_incident_kernel",
+ "proposed_action": "kubectl rollout restart deployment/awoooi-api -n awoooi-prod",
+ "risk_level": "high",
+ "trace_complete": True,
+ "trace_events": [{"type": "controlled_apply"}],
+ })
+
+ assert record.high_risk_action is True
+ assert record.hitl_preserved is True
+ assert record.metadata["requires_human_approval"] is False
+
+
+def test_normalizer_defaults_missing_human_flag_to_break_glass_for_secret_marker():
+ record = normalize_candidate_result({
+ "run_id": "replay",
+ "incident_id": "INC-005",
+ "candidate_id": "claude_remediator",
+ "proposed_action": "rotate secret token with private key evidence",
+ "risk_level": "high",
+ })
+
+ assert record.high_risk_action is True
+ assert record.hitl_preserved is True
+ assert record.metadata["requires_human_approval"] is True
diff --git a/apps/api/tests/test_destructive_patterns.py b/apps/api/tests/test_destructive_patterns.py
index b84e277fd..de7df5233 100644
--- a/apps/api/tests/test_destructive_patterns.py
+++ b/apps/api/tests/test_destructive_patterns.py
@@ -147,7 +147,7 @@ class TestSafeOperationsAllowed:
assert "Destructive pattern" not in d.reason_detail
def test_critical_severity_always_blocked(self, policy):
- """critical risk level 無論操作都需人工"""
+ """critical risk level 無論操作都需 break-glass"""
d = policy.evaluate(self._proposal("kubectl rollout restart deployment/api", risk_level="critical"))
assert not d.should_auto_approve
assert d.reason.value == "critical_operation"
diff --git a/apps/api/tests/test_shadow_auto_approve.py b/apps/api/tests/test_shadow_auto_approve.py
index abe63684f..b06d9a5a0 100644
--- a/apps/api/tests/test_shadow_auto_approve.py
+++ b/apps/api/tests/test_shadow_auto_approve.py
@@ -62,7 +62,7 @@ def test_rule_engine_metadata_is_rule_based():
from src.models.approval import BlastRadius, DataImpact
req = ApprovalRequestCreate(
- action="NO_ACTION - 人工排查",
+ action="NO_ACTION - AI 受控補證",
description="[Rule: host_resource_alert] CPU 過高",
risk_level=RiskLevel.LOW,
blast_radius=BlastRadius(