diff --git a/apps/api/src/services/approval_execution.py b/apps/api/src/services/approval_execution.py
index 809752c47..eaee13e45 100644
--- a/apps/api/src/services/approval_execution.py
+++ b/apps/api/src/services/approval_execution.py
@@ -42,7 +42,7 @@ from src.services.approval_db import get_approval_service, get_timeline_service
from src.services.awooop_deeplinks import incident_truth_chain_button_row
from src.services.executor import ExecutionResult, OperationType, get_executor
from src.services.operation_parser import parse_operation_from_action
-from src.services.operator_outcome import build_operator_outcome
+from src.services.operator_outcome import build_operator_outcome, normalize_operator_outcome
if TYPE_CHECKING:
from src.services.notifications import ExecutionStatus
@@ -1265,17 +1265,17 @@ class ApprovalExecutionService:
)
outcome = truth_chain.get("operator_outcome")
if isinstance(outcome, dict):
- return outcome
+ return normalize_operator_outcome(outcome) or outcome
quality = truth_chain.get("automation_quality")
if isinstance(quality, dict) and isinstance(quality.get("operator_outcome"), dict):
- return quality["operator_outcome"]
- return build_operator_outcome(
+ return normalize_operator_outcome(quality["operator_outcome"]) or quality["operator_outcome"]
+ return normalize_operator_outcome(build_operator_outcome(
truth_status=truth_chain.get("truth_status")
if isinstance(truth_chain.get("truth_status"), dict)
else {},
automation_quality=quality if isinstance(quality, dict) else {},
source_id=incident_id,
- )
+ )) or {}
except Exception as exc:
logger.warning(
"execution_result_outcome_fetch_failed",
@@ -1311,9 +1311,9 @@ class ApprovalExecutionService:
elif state == "completed_verified":
icon = "✅"
title = "修復完成並已驗證"
- elif state == "diagnostic_only_manual_review":
+ elif state == "diagnostic_only_ai_repair_required":
icon = "⚠️"
- title = "已記錄診斷,尚未證明修復"
+ title = "已記錄診斷,AI 修復候選補齊中"
elif needs_human:
icon = "⚠️"
title = "已執行,但需人工確認"
diff --git a/apps/api/src/services/operator_outcome.py b/apps/api/src/services/operator_outcome.py
index 1dc8b0e53..300872b31 100644
--- a/apps/api/src/services/operator_outcome.py
+++ b/apps/api/src/services/operator_outcome.py
@@ -11,6 +11,66 @@ from typing import Any
_ACTION_REQUIRED_CHANNELS = ("telegram_sre_war_room", "awooop_operator_console")
_RESULT_ONLY_CHANNELS = ("telegram_result_reply", "awooop_operator_console")
+_LEGACY_AI_CONTROLLED_STATE_REWRITE = {
+ "diagnostic_only_manual_review": (
+ "diagnostic_only_ai_repair_required",
+ "auto_generate_repair_candidate_from_diagnostic_evidence",
+ "只完成診斷/觀察;AI 已排入 PlayBook / transport / verifier 修復候選",
+ ),
+ "verification_degraded_manual_required": (
+ "verification_degraded_ai_verifier_required",
+ "run_verifier_or_rollback_candidate",
+ "驗證退化;AI 已排入 verifier / rollback 判定",
+ ),
+ "execution_unverified_manual_required": (
+ "execution_unverified_controlled_verifier_required",
+ "run_post_execution_verifier_or_rollback",
+ "已執行但缺少驗證結果;AI 進入 verifier / rollback 判定",
+ ),
+ "no_action_manual_review": (
+ "no_action_ai_candidate_required",
+ "collect_evidence_or_generate_playbook_candidate",
+ "AI 未找到可執行修復;已排入 evidence / PlayBook 補強與 controlled policy 判定",
+ ),
+ "approval_expired_manual_review": (
+ "approval_expired_ai_retry",
+ "ai_retry_or_rebuild_controlled_packet",
+ "舊審批已逾期;AI 重新建立受控處置包",
+ ),
+ "write_observed_manual_review": (
+ "write_observed_ai_verifier_or_rollback",
+ "auto_verify_or_rollback_observed_write",
+ "補救證據出現寫入旗標;AI 進入 verifier / rollback 判定",
+ ),
+ "blocked_manual_required": (
+ "blocked_ai_repair_required",
+ "auto_repair_blocker_or_connector_then_retry",
+ "自動化流程受阻;AI 已排入 blocker / connector 修復",
+ ),
+}
+_BREAK_GLASS_BLOCKER_TOKENS = (
+ "secret",
+ "token",
+ "private_key",
+ "authorization_header",
+ "drop",
+ "truncate",
+ "restore",
+ "prune",
+ "remote_delete",
+ "reboot",
+ "node_drain",
+ "firewall_cutover",
+ "credentialed_exploit",
+ "external_active_scan",
+ "paid_provider",
+ "cost_limit",
+ "force_push",
+ "repo_delete",
+ "ref_delete",
+ "raw_runtime_secret_volume",
+ "break_glass",
+)
def _safe_int(value: Any) -> int:
@@ -27,6 +87,69 @@ def _first_text(values: list[Any]) -> str | None:
return None
+def _requires_break_glass(blockers: list[str]) -> bool:
+ haystack = " ".join(str(item).lower() for item in blockers)
+ return any(token in haystack for token in _BREAK_GLASS_BLOCKER_TOKENS)
+
+
+def _canonical_operator_state(state: str) -> str:
+ rewritten = _LEGACY_AI_CONTROLLED_STATE_REWRITE.get(state)
+ return rewritten[0] if rewritten else state
+
+
+def normalize_operator_outcome(outcome: dict[str, Any] | None) -> dict[str, Any] | None:
+ """Rewrite legacy human-gate outcomes to the current controlled-AI policy.
+
+ Older truth-chain rows can contain states such as ``*_manual_required``.
+ They remain useful as history, but the operator-facing contract must not
+ present them as the live product path unless a break-glass blocker exists.
+ """
+ if not isinstance(outcome, dict):
+ return outcome
+ state = str(outcome.get("state") or "")
+ rewrite = _LEGACY_AI_CONTROLLED_STATE_REWRITE.get(state)
+ if not rewrite:
+ return outcome
+
+ normalized = dict(outcome)
+ new_state, next_action, summary = rewrite
+ blockers = [
+ str(item)
+ for item in normalized.get("blockers", [])
+ if item
+ ] if isinstance(normalized.get("blockers"), list) else []
+ break_glass = _requires_break_glass(blockers)
+ normalized["legacy_state"] = state
+ normalized["state"] = "break_glass_required" if break_glass else new_state
+ normalized["summary_zh"] = (
+ "命中硬阻擋,需 break-glass 或外部授權"
+ if break_glass
+ else summary
+ )
+ normalized["needs_human"] = break_glass
+ normalized["human_action_required"] = break_glass
+ normalized["next_action"] = (
+ "break_glass_or_external_authorization"
+ if break_glass
+ else next_action
+ )
+ normalized["human_action_reason"] = (
+ normalized.get("human_action_reason")
+ if break_glass
+ else "legacy_manual_gate_converted_to_controlled_ai_policy"
+ )
+ notification = (
+ dict(normalized.get("notification"))
+ if isinstance(normalized.get("notification"), dict)
+ else {}
+ )
+ notification["mode"] = "action_required"
+ notification["channels"] = list(_ACTION_REQUIRED_CHANNELS)
+ notification["reason"] = str(normalized["human_action_reason"])
+ normalized["notification"] = notification
+ return normalized
+
+
def _verification_blocker_code(value: Any) -> str:
status = str(value or "missing").strip().lower()
if status in {"", "none", "null", "missing", "--"}:
@@ -96,6 +219,7 @@ def _build_execution_result(
verification: str,
) -> dict[str, Any]:
"""Describe execution completion separately from remediation outcome."""
+ state = _canonical_operator_state(state)
approval_status = "unknown"
completion_status = "unknown"
command_status = "unknown"
@@ -120,29 +244,29 @@ def _build_execution_result(
failure_status = "command_failed"
summary = "已失敗:修復指令執行失敗,AI 已排入 rollback / repair 候選"
terminal = False
- elif state == "diagnostic_only_manual_review":
- approval_status = "completed"
- completion_status = "completed_no_repair"
+ elif state == "diagnostic_only_ai_repair_required":
+ approval_status = "auto_authorized"
+ completion_status = "diagnostic_completed_ai_repair_queued"
command_status = "diagnostic_completed" if has_nonrepair_operation else "skipped_no_action"
- repair_status = "not_executed"
+ repair_status = "playbook_or_transport_repair_required"
failure_status = "no_command_failed"
- summary = "流程已完成:只完成診斷/觀察,沒有修復指令成功或失敗"
- terminal = True
- elif state == "verification_degraded_manual_required":
- approval_status = "completed"
- completion_status = "completed_verification_degraded"
- command_status = "succeeded" if has_repair_execution else "diagnostic_completed"
- repair_status = "verification_degraded"
- failure_status = "no_command_failed"
- summary = "已執行,但驗證結果退化;需人工確認是否真的修復"
+ summary = "只完成診斷/觀察;AI 已排入修復候選補齊"
terminal = False
- elif state == "execution_unverified_manual_required":
- approval_status = "completed"
+ elif state == "verification_degraded_ai_verifier_required":
+ approval_status = "auto_authorized"
+ completion_status = "completed_verification_degraded_ai_recovery"
+ command_status = "succeeded" if has_repair_execution else "diagnostic_completed"
+ repair_status = "ai_verifier_or_rollback_required"
+ failure_status = "no_command_failed"
+ summary = "已執行但驗證退化;AI 已排入 verifier / rollback 判定"
+ terminal = False
+ elif state == "execution_unverified_controlled_verifier_required":
+ approval_status = "auto_authorized"
completion_status = "completed_unverified"
command_status = "succeeded"
- repair_status = "unverified"
+ repair_status = "ai_verifier_or_rollback_required"
failure_status = "no_command_failed"
- summary = "已執行成功,但缺少修復驗證結果"
+ summary = "已執行成功但缺少修復驗證結果;AI 進入 verifier / rollback 判定"
terminal = False
elif state == "apply_candidate_owner_review_ready":
approval_status = "auto_authorized_preflight"
@@ -200,7 +324,7 @@ def _build_execution_result(
failure_status = "not_applicable"
summary = "真相鏈舊人工閘已轉入 AI 受控處理"
terminal = False
- elif state == "no_action_manual_review":
+ elif state == "no_action_ai_candidate_required":
approval_status = "controlled_policy_check"
completion_status = "not_started_no_action"
command_status = "not_started"
@@ -216,21 +340,13 @@ def _build_execution_result(
failure_status = "not_applicable"
summary = "已拒絕:審批結案,未執行任何修復指令"
terminal = True
- elif state == "approval_expired_manual_review":
- approval_status = "controlled_policy_retry"
- completion_status = "expired_route_requeued"
- command_status = "not_run"
- repair_status = "controlled_apply_evaluation"
- failure_status = "not_applicable"
- summary = "舊審批逾期:已改排 AI retry / rollback / evidence 補強"
- terminal = False
elif state == "approval_expired_ai_retry":
approval_status = "controlled_policy_retry"
completion_status = "expired_route_requeued"
command_status = "not_run"
repair_status = "controlled_apply_evaluation"
failure_status = "not_applicable"
- summary = "舊審批逾期:AI 已重新排入 retry / rollback / evidence 補強"
+ summary = "舊審批逾期:已改排 AI retry / rollback / evidence 補強"
terminal = False
elif state == "approval_required":
approval_status = "auto_policy_check"
@@ -260,9 +376,9 @@ def _build_execution_result(
approval_status = "completed"
completion_status = "completed_needs_review"
command_status = "succeeded" if verification != "missing" else "completed"
- repair_status = "needs_review"
+ repair_status = "ai_verifier_or_rollback_required"
failure_status = "no_command_failed"
- summary = "已有執行紀錄,但仍需人工確認最終修復結果"
+ summary = "已有執行紀錄;AI 進入 verifier / rollback 判定"
terminal = False
elif verdict in {"received_only", "observed_not_executed"} or stage == "received":
approval_status = "not_started"
@@ -270,7 +386,7 @@ def _build_execution_result(
command_status = "not_started"
repair_status = "not_executed"
failure_status = "not_applicable"
- summary = "尚未執行修復指令"
+ summary = "尚未執行修復指令;AI 應補 evidence / PlayBook / controlled policy"
terminal = False
return {
@@ -332,13 +448,14 @@ def build_operator_outcome(
)
needs_human_from_truth = bool(truth_status.get("needs_human"))
first_blocker = _first_text(blockers)
+ break_glass = _requires_break_glass(blockers)
if fetch_error:
- state = "truth_chain_unavailable"
+ state = "truth_chain_connector_repair_required"
severity = "warning"
- needs_human = True
- next_action = "open_awooop_and_review_source_records"
- summary = "真相鏈查詢失敗,需人工確認處置結果"
+ needs_human = False
+ next_action = "auto_repair_truth_chain_connector_then_retry"
+ summary = "真相鏈查詢失敗,AI 已排入 connector / evidence 修復"
reason = str(fetch_error)[:240]
elif verdict == "auto_repaired_verified":
state = "completed_verified"
@@ -362,18 +479,18 @@ def build_operator_outcome(
summary = "執行失敗,AI 已排入 rollback / PlayBook / transport 修復候選"
reason = first_blocker or "execution_failed"
elif verdict == "manual_required_diagnostic_only" or has_nonrepair_operation:
- state = "diagnostic_only_manual_review"
+ state = "diagnostic_only_ai_repair_required"
severity = "warning"
- needs_human = True
- next_action = "manual_review_or_collect_repair_evidence"
- summary = "只完成診斷/觀察,尚未證明修復"
+ needs_human = False
+ next_action = "auto_generate_repair_candidate_from_diagnostic_evidence"
+ summary = "只完成診斷/觀察;AI 已排入 PlayBook / transport / verifier 修復候選"
reason = first_blocker or "diagnostic_or_audit_only"
elif verdict == "auto_repaired_verification_degraded":
- state = "verification_degraded_manual_required"
+ state = "verification_degraded_ai_verifier_required"
severity = "warning"
- needs_human = True
- next_action = "manual_verify_or_repair"
- summary = "已執行但驗證退化,需人工確認"
+ needs_human = False
+ next_action = "run_verifier_or_rollback_candidate"
+ summary = "已執行但驗證退化,AI 進入 verifier / rollback 判定"
reason = first_blocker or f"verification={verification}"
elif verdict == "ansible_check_mode_only" or ansible_dry_run_only:
state = "controlled_apply_queued"
@@ -392,7 +509,7 @@ def build_operator_outcome(
summary = "已執行但缺少驗證結果,AI 進入 verifier / rollback 判定"
reason = first_blocker or "execution_without_verification_result"
elif verdict == "manual_required_no_action":
- state = "no_action_manual_review"
+ state = "no_action_ai_candidate_required"
severity = "warning"
needs_human = False
next_action = "collect_evidence_or_generate_playbook_candidate"
@@ -420,7 +537,7 @@ def build_operator_outcome(
summary = "只讀試跑完成,AI 進入受控 apply 判定"
reason = first_blocker or "read_only_dry_run"
elif remediation_state == "write_observed":
- state = "write_observed_manual_review"
+ state = "write_observed_ai_verifier_or_rollback"
severity = "critical"
needs_human = False
next_action = "auto_verify_or_rollback_observed_write"
@@ -441,11 +558,19 @@ def build_operator_outcome(
summary = "已進入目前 owner policy 判定;低/中/高風險由 AI 受控執行"
reason = first_blocker or "current_owner_policy_auto_authorized"
elif needs_human_from_truth:
- state = "truth_chain_ai_recovery_required"
- severity = "warning"
- needs_human = False
- next_action = "auto_generate_repair_or_break_glass_packet"
- summary = "真相鏈舊判定需人工;已由目前政策轉入 AI 受控處理"
+ state = "break_glass_required" if break_glass else "truth_chain_ai_recovery_required"
+ severity = "critical" if break_glass else "warning"
+ needs_human = break_glass
+ next_action = (
+ "break_glass_or_external_authorization"
+ if break_glass
+ else "auto_generate_repair_or_break_glass_packet"
+ )
+ summary = (
+ "命中硬阻擋,需 break-glass 或外部授權"
+ if break_glass
+ else "真相鏈舊判定需人工;已由目前政策轉入 AI 受控處理"
+ )
reason = first_blocker or f"{stage}/{stage_status}"
elif verdict in {"observed_not_executed", "received_only"}:
state = "observed_not_executed"
@@ -457,9 +582,17 @@ def build_operator_outcome(
else:
state = "unknown_pending_observation"
severity = "warning"
- needs_human = bool(blockers)
- next_action = "review_status_chain"
- summary = "處置結果尚未形成明確結論"
+ needs_human = break_glass
+ next_action = (
+ "break_glass_or_external_authorization"
+ if break_glass
+ else "auto_collect_evidence_or_generate_controlled_candidate"
+ )
+ summary = (
+ "命中硬阻擋,需 break-glass 或外部授權"
+ if break_glass
+ else "處置結果尚未形成明確結論;AI 應補 evidence / PlayBook / verifier"
+ )
reason = first_blocker or f"{verdict}:{stage}/{stage_status}"
execution_result = _build_execution_result(
diff --git a/apps/api/src/services/platform_operator_service.py b/apps/api/src/services/platform_operator_service.py
index e69ec587d..427cba249 100644
--- a/apps/api/src/services/platform_operator_service.py
+++ b/apps/api/src/services/platform_operator_service.py
@@ -67,7 +67,11 @@ from src.services.ollama_failover_manager import (
get_ollama_failover_manager,
)
from src.services.ollama_health_monitor import HealthReport, HealthStatus
-from src.services.operator_outcome import build_operator_outcome, normalize_operator_blockers
+from src.services.operator_outcome import (
+ build_operator_outcome,
+ normalize_operator_blockers,
+ normalize_operator_outcome,
+)
from src.services.operator_summary_cache import (
get_cached_operator_summary_async,
store_operator_summary_async,
@@ -5824,19 +5828,19 @@ def _build_awooop_status_chain(
next_step = "verify_execution_result"
elif has_nonrepair_operation:
repair_state = "diagnostic_or_audit_recorded"
- next_step = "manual_review_or_collect_repair_evidence"
+ next_step = "auto_generate_repair_candidate_from_diagnostic_evidence"
elif remediation_state == "read_only":
repair_state = "read_only_dry_run"
next_step = "approve_or_escalate_from_awooop"
elif remediation_state == "write_observed":
- repair_state = "write_observed_manual_review"
- next_step = "review_write_evidence"
+ repair_state = "write_observed_ai_verifier_or_rollback"
+ next_step = "auto_verify_or_rollback_observed_write"
elif remediation_state == "blocked":
- repair_state = "blocked_manual_required"
- next_step = "manual_investigation"
+ repair_state = "blocked_ai_repair_required"
+ next_step = "auto_repair_blocker_or_connector_then_retry"
elif needs_human:
- repair_state = "manual_required"
- next_step = "manual_investigation"
+ repair_state = "truth_chain_ai_recovery_required"
+ next_step = "auto_generate_repair_or_break_glass_packet"
else:
repair_state = "no_execution_evidence"
next_step = "collect_evidence_or_wait"
@@ -5889,23 +5893,23 @@ def _build_awooop_status_chain(
outcome_quality["facts"] = outcome_facts
outcome_quality["verdict"] = "ansible_check_mode_only"
outcome_quality.pop("operator_outcome", None)
- outcome = build_operator_outcome(
+ outcome = normalize_operator_outcome(build_operator_outcome(
truth_status=truth_status,
automation_quality=outcome_quality,
remediation_state=remediation_state,
fetch_error=fetch_error,
source_id=source_id,
- )
+ )) or {}
elif isinstance(quality.get("operator_outcome"), dict):
- outcome = dict(quality["operator_outcome"])
+ outcome = normalize_operator_outcome(dict(quality["operator_outcome"])) or {}
else:
- outcome = build_operator_outcome(
+ outcome = normalize_operator_outcome(build_operator_outcome(
truth_status=truth_status,
automation_quality=quality,
remediation_state=remediation_state,
fetch_error=fetch_error,
source_id=source_id,
- )
+ )) or {}
if outcome:
needs_human = bool(outcome.get("needs_human"))
next_step = str(outcome.get("next_action") or next_step)
diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py
index c29b1f20f..a8a7aad94 100644
--- a/apps/api/src/services/telegram_gateway.py
+++ b/apps/api/src/services/telegram_gateway.py
@@ -52,7 +52,7 @@ from src.services.approval_action_classifier import (
is_no_action_approval_action,
)
from src.services.chat_manager import get_chat_manager
-from src.services.operator_outcome import build_operator_outcome
+from src.services.operator_outcome import build_operator_outcome, normalize_operator_outcome
from src.services.security_interceptor import (
NonceReplayError,
UserNotWhitelistedError,
@@ -888,13 +888,13 @@ def _operator_outcome_from_blocks(
source_id: str | None = None,
) -> dict[str, object]:
if isinstance(quality, dict) and isinstance(quality.get("operator_outcome"), dict):
- return dict(quality["operator_outcome"])
- return build_operator_outcome(
+ return normalize_operator_outcome(dict(quality["operator_outcome"])) or {}
+ return normalize_operator_outcome(build_operator_outcome(
truth_status=truth_status if isinstance(truth_status, dict) else {},
automation_quality=quality if isinstance(quality, dict) else {},
remediation_state=remediation_state,
source_id=source_id,
- )
+ )) or {}
def _format_operator_outcome_lines(outcome: dict[str, object] | None) -> list[str]:
@@ -1424,19 +1424,19 @@ def _format_awooop_status_chain_lines(
next_step = "verify_execution_result"
elif has_nonrepair_operation:
repair_state = "diagnostic_or_audit_recorded"
- next_step = "manual_review_or_collect_repair_evidence"
+ next_step = "auto_generate_repair_candidate_from_diagnostic_evidence"
elif remediation_state == "read_only":
repair_state = "read_only_dry_run"
next_step = "approve_or_escalate_from_awooop"
elif remediation_state == "write_observed":
- repair_state = "write_observed_manual_review"
- next_step = "review_write_evidence"
+ repair_state = "write_observed_ai_verifier_or_rollback"
+ next_step = "auto_verify_or_rollback_observed_write"
elif remediation_state == "blocked":
- repair_state = "blocked_manual_required"
- next_step = "manual_investigation"
+ repair_state = "blocked_ai_repair_required"
+ next_step = "auto_repair_blocker_or_connector_then_retry"
elif needs_human:
- repair_state = "manual_required"
- next_step = "manual_investigation"
+ repair_state = "truth_chain_ai_recovery_required"
+ next_step = "auto_generate_repair_or_break_glass_packet"
else:
repair_state = "no_execution_evidence"
next_step = "collect_evidence_or_wait"
@@ -2753,19 +2753,19 @@ def _callback_reply_awooop_status_chain_snapshot(
next_step = "verify_execution_result"
elif has_nonrepair_operation:
repair_state = "diagnostic_or_audit_recorded"
- next_step = "manual_review_or_collect_repair_evidence"
+ next_step = "auto_generate_repair_candidate_from_diagnostic_evidence"
elif remediation_state == "read_only":
repair_state = "read_only_dry_run"
next_step = "approve_or_escalate_from_awooop"
elif remediation_state == "write_observed":
- repair_state = "write_observed_manual_review"
- next_step = "review_write_evidence"
+ repair_state = "write_observed_ai_verifier_or_rollback"
+ next_step = "auto_verify_or_rollback_observed_write"
elif remediation_state == "blocked":
- repair_state = "blocked_manual_required"
- next_step = "manual_investigation"
+ repair_state = "blocked_ai_repair_required"
+ next_step = "auto_repair_blocker_or_connector_then_retry"
elif needs_human:
- repair_state = "manual_required"
- next_step = "manual_investigation"
+ repair_state = "truth_chain_ai_recovery_required"
+ next_step = "auto_generate_repair_or_break_glass_packet"
else:
repair_state = "no_execution_evidence"
next_step = "collect_evidence_or_wait"
@@ -3033,7 +3033,7 @@ class TelegramMessage:
# 2026-04-16 ogt + Claude Sonnet 4.6: 告警分類與修復鏈路顯示 (ADR-076)
alert_category: str = "" # host/k8s/database/service/external_site/secops 等
playbook_name: str = "" # 匹配到的 Playbook 名稱(空字串=規則匹配)
- automation_state: str = "" # diagnosis_collected_manual_required / diagnosis_failed_manual_required
+ automation_state: str = "" # legacy diagnosis_*_manual_required is displayed as AI controlled recovery
automation_quality: dict | None = None # truth-chain automation_quality 摘要
remediation_summary: dict | None = None # ADR-100 read-only dry-run history 摘要
repair_candidate_blocker_summary: str = "" # 修復候選阻擋原因摘要
@@ -3114,7 +3114,8 @@ class TelegramMessage:
else None
)
if outcome and outcome.get("summary_zh"):
- return str(outcome["summary_zh"])
+ normalized = normalize_operator_outcome(dict(outcome)) or outcome
+ return str(normalized["summary_zh"])
facts = quality.get("facts") if isinstance(quality.get("facts"), dict) else {}
verdict = str(quality.get("verdict") or "")
has_repair_execution = _has_repair_execution_evidence(facts)
@@ -3170,15 +3171,15 @@ class TelegramMessage:
def _operator_outcome(self) -> dict[str, object] | None:
quality = self.automation_quality if isinstance(self.automation_quality, dict) else {}
if isinstance(quality.get("operator_outcome"), dict):
- return dict(quality["operator_outcome"])
+ return normalize_operator_outcome(dict(quality["operator_outcome"]))
if not quality:
return None
- return build_operator_outcome(
+ return normalize_operator_outcome(build_operator_outcome(
truth_status=None,
automation_quality=quality,
remediation_state=_remediation_evidence_state(self.remediation_summary),
source_id=self.incident_id or self.approval_id,
- )
+ ))
def _format_operator_outcome_block(self) -> str:
lines = _format_operator_outcome_lines(self._operator_outcome())
diff --git a/apps/api/tests/test_awooop_operator_timeline_labels.py b/apps/api/tests/test_awooop_operator_timeline_labels.py
index 4a127dcd9..3a1648d70 100644
--- a/apps/api/tests/test_awooop_operator_timeline_labels.py
+++ b/apps/api/tests/test_awooop_operator_timeline_labels.py
@@ -2273,9 +2273,9 @@ def test_awooop_status_chain_does_not_treat_audit_ops_as_repair() -> None:
)
assert chain["repair_state"] == "diagnostic_or_audit_recorded"
- assert chain["next_step"] == "manual_review_or_collect_repair_evidence"
- assert chain["needs_human"] is True
- assert chain["operator_outcome"]["state"] == "diagnostic_only_manual_review"
+ assert chain["next_step"] == "auto_generate_repair_candidate_from_diagnostic_evidence"
+ assert chain["needs_human"] is False
+ assert chain["operator_outcome"]["state"] == "diagnostic_only_ai_repair_required"
assert chain["operator_outcome"]["notification"]["mode"] == "action_required"
assert chain["evidence"]["operation_records"] == 1
assert chain["evidence"]["auto_repair_records"] == 0
diff --git a/apps/api/tests/test_operator_outcome.py b/apps/api/tests/test_operator_outcome.py
index 80846978a..14b7db8a6 100644
--- a/apps/api/tests/test_operator_outcome.py
+++ b/apps/api/tests/test_operator_outcome.py
@@ -40,7 +40,7 @@ def test_operator_outcome_normalizes_missing_evidence_blockers() -> None:
assert "learning_recorded" not in outcome["blockers"]
-def test_operator_outcome_marks_diagnostic_only_as_manual_action_required() -> None:
+def test_operator_outcome_marks_diagnostic_only_as_ai_repair_required() -> None:
outcome = build_operator_outcome(
truth_status={
"current_stage": "execution_succeeded",
@@ -64,13 +64,17 @@ def test_operator_outcome_marks_diagnostic_only_as_manual_action_required() -> N
)
assert outcome["schema_version"] == "operator_outcome_v1"
- assert outcome["state"] == "diagnostic_only_manual_review"
- assert outcome["needs_human"] is True
+ assert outcome["state"] == "diagnostic_only_ai_repair_required"
+ assert outcome["needs_human"] is False
assert outcome["notification"]["mode"] == "action_required"
assert "telegram_sre_war_room" in outcome["notification"]["channels"]
- assert outcome["next_action"] == "manual_review_or_collect_repair_evidence"
- assert outcome["execution_result"]["completion_status"] == "completed_no_repair"
- assert outcome["execution_result"]["repair_status"] == "not_executed"
+ assert outcome["next_action"] == "auto_generate_repair_candidate_from_diagnostic_evidence"
+ assert outcome["execution_result"]["completion_status"] == (
+ "diagnostic_completed_ai_repair_queued"
+ )
+ assert outcome["execution_result"]["repair_status"] == (
+ "playbook_or_transport_repair_required"
+ )
assert outcome["execution_result"]["failure_status"] == "no_command_failed"
@@ -326,8 +330,8 @@ def test_execution_result_message_does_not_call_diagnostic_success_repair_done()
),
)
- assert "已記錄診斷,尚未證明修復" in text
- assert "completed_no_repair" in text
+ assert "只完成診斷/觀察;AI 已排入修復候選補齊" in text
+ assert "diagnostic_completed_ai_repair_queued" in text
assert "no_command_failed" in text
assert "執行成功" not in text
assert "修復完成" not in text
diff --git a/apps/api/tests/test_telegram_message_templates.py b/apps/api/tests/test_telegram_message_templates.py
index 3aa58b0d2..887f6a948 100644
--- a/apps/api/tests/test_telegram_message_templates.py
+++ b/apps/api/tests/test_telegram_message_templates.py
@@ -743,10 +743,10 @@ def test_awooop_status_chain_lines_do_not_treat_audit_ops_as_repair() -> None:
joined = "\n".join(lines)
assert "diagnostic_or_audit_recorded" in joined
- assert "manual_review_or_collect_repair_evidence" in joined
+ assert "auto_generate_repair_candidate_from_diagnostic_evidence" in joined
assert "executed_pending_verification" not in joined
assert "處置結論" in joined
- assert "只完成診斷/觀察,尚未證明修復" in joined
+ assert "只完成診斷/觀察;AI 已排入 PlayBook / transport / verifier 修復候選" in joined
assert "通知:telegram_sre_war_room,awooop_operator_console" in joined
diff --git a/apps/web/src/app/[locale]/alerts/page.tsx b/apps/web/src/app/[locale]/alerts/page.tsx
index eb7715ace..2f65a5a33 100644
--- a/apps/web/src/app/[locale]/alerts/page.tsx
+++ b/apps/web/src/app/[locale]/alerts/page.tsx
@@ -207,9 +207,14 @@ function FocusIncidentEvidencePanel({
const sourceReasonCode = String(sourceCorrelation?.missing_reason ?? '')
const outcomeStateLabels: Record = {
verification_degraded_manual_required: t('operatorFlow.stateLabels.verificationDegradedManualRequired'),
+ verification_degraded_ai_verifier_required: t('operatorFlow.stateLabels.verificationDegradedAiVerifierRequired'),
+ diagnostic_only_ai_repair_required: t('operatorFlow.stateLabels.diagnosticOnlyAiRepairRequired'),
+ no_action_ai_candidate_required: t('operatorFlow.stateLabels.noActionAiCandidateRequired'),
}
const nextActionLabels: Record = {
manual_verify_or_repair: t('operatorFlow.nextActionLabels.manualVerifyOrRepair'),
+ run_verifier_or_rollback_candidate: t('operatorFlow.nextActionLabels.runVerifierOrRollbackCandidate'),
+ auto_generate_repair_candidate_from_diagnostic_evidence: t('operatorFlow.nextActionLabels.autoGenerateRepairCandidate'),
}
const humanReasonLabels: Record = {
incident_open_after_successful_execution: t('operatorFlow.reasonLabels.incidentOpenAfterSuccessfulExecution'),
diff --git a/apps/web/src/components/awooop/status-chain.tsx b/apps/web/src/components/awooop/status-chain.tsx
index b210ed9df..57bb72716 100644
--- a/apps/web/src/components/awooop/status-chain.tsx
+++ b/apps/web/src/components/awooop/status-chain.tsx
@@ -534,6 +534,12 @@ export function AwoooPStatusChainPanel({
review_owner_release_packet_before_apply: t("nextActions.reviewOwnerReleasePacket"),
manual_review_or_collect_repair_evidence: t("nextActions.collectRepairEvidence"),
manual_investigation: t("nextActions.manualInvestigation"),
+ auto_generate_repair_candidate_from_diagnostic_evidence: t("nextActions.autoGenerateRepairCandidate"),
+ auto_verify_or_rollback_observed_write: t("nextActions.autoVerifyOrRollback"),
+ auto_repair_blocker_or_connector_then_retry: t("nextActions.autoRepairBlockerConnector"),
+ auto_generate_repair_or_break_glass_packet: t("nextActions.autoGenerateRepairOrBreakGlass"),
+ run_verifier_or_rollback_candidate: t("nextActions.autoVerifyOrRollback"),
+ run_post_execution_verifier_or_rollback: t("nextActions.autoVerifyOrRollback"),
run_or_review_post_execution_verification: t("nextActions.reviewVerifier"),
monitor_for_regression: t("nextActions.monitorRegression"),
collect_evidence_or_wait: t("nextActions.collectEvidenceOrWait"),
diff --git a/docs/HARD_RULES.md b/docs/HARD_RULES.md
index 6f968961f..85f1a0d64 100644
--- a/docs/HARD_RULES.md
+++ b/docs/HARD_RULES.md
@@ -8,11 +8,11 @@
| 欄位 | 值 |
|------|-----|
-| **版本** | v2.4 |
+| **版本** | v2.5 |
| **建立日期** | 2026-03-20 (台北) |
| **建立者** | Claude Code |
-| **最後修改** | 2026-06-26 (台北) |
-| **修改者** | Codex + ogt (新增 AI Agent 受控自動執行授權) |
+| **最後修改** | 2026-06-27 (台北) |
+| **修改者** | Codex + ogt (舊 owner gate / read-only 預設失效,改以 AI Agent 受控自動執行為基線) |
### 變更紀錄
@@ -33,6 +33,7 @@
| v2.2 | 2026-06-04 | Codex + ogt | 🔴🔴🔴 IwoooS 資安治理禁令:只讀證據、低摩擦、不可誤讀 UI / AwoooP approval / runtime gate |
| v2.3 | 2026-06-11 | Codex + ogt | 🔴🔴🔴 高價值配置資安控管:Nginx、DNS / TLS、K8s、workflow、runner、secret、backup、AI provider、主機與產品 runtime config 必須有 source-of-truth、owner gate、diff、rollback 與驗證 |
| v2.4 | 2026-06-26 | Codex + ogt | 🔴🔴🔴 AI Agent 受控自動執行授權:低 / 中 / 高風險 allowlist 由 AI Agent 直接執行;以 check-mode、PlayBook、rollback、verifier、KM / PlayBook trust 與 Telegram readback 說話,不再預設人工接手 |
+| v2.5 | 2026-06-27 | Codex + ogt | 🔴🔴🔴 統帥最新要求優先:舊規範中 owner review / read-only / manual handoff 的預設阻擋全部失效;除硬阻擋外,一律轉為 AI 補齊候選、check-mode、controlled apply、verifier、rollback 與學習回寫 |
---
@@ -73,6 +74,8 @@
## 🔴🔴🔴 AI Agent Controlled Runtime Authorization
> 2026-06-26 統帥最新指示:中 / 低風險本來就應由 AI Agent 自動處理;現在高風險也放寬為直接 AI 自動化處理。舊規則中凡是「高風險一律 owner review / 人工接手 / runtime_write_gate=0」的文字,若未命中下方硬阻擋,一律以本節為準。
+>
+> 2026-06-27 統帥再次明確要求:所有舊版規範、文件、測試、Telegram 文案、UI 狀態、Run / Work Item / Approval 顯示,只要把 low / medium / high 事件預設停在 owner gate、read-only、manual handoff、`needs_human=true`、`manual_required`、`runtime_write_gate=0`,且沒有命中硬阻擋,一律視為錯誤產品行為。正確路徑是 AI Agent 自動補齊 PlayBook / Ansible / MCP / rollback / verifier / KM,再進 controlled apply;缺自動化資產本身就是 P0/P1 缺陷,不能當成正常人工流程。
### 自動執行授權範圍
@@ -84,6 +87,38 @@
4. Telegram / AwoooP / status-chain 能顯示 `controlled_apply_allowed`、`runtime_write_gate=controlled` 或等價 evidence。
5. 失敗時不是轉人工,而是排入 AI rollback、PlayBook / transport / connector 修復候選並重試。
+### 舊 owner gate / read-only 預設失效條款
+
+以下文字只允許作為歷史證據欄位或硬阻擋說明,不得再作為 low / medium / high 事件的預設終局:
+
+```text
+owner_review_required
+manual_required
+needs_human=true
+manual_handoff_required
+read_only_only_no_live_write
+runtime_execution_authorized=false
+runtime_write_gate=0
+no_action_manual_review
+diagnostic_only_manual_review
+verification_degraded_manual_required
+execution_unverified_manual_required
+```
+
+若事件沒有命中硬阻擋,以上狀態必須在 API / Telegram / AwoooP 顯示時轉譯為:
+
+```text
+controlled_playbook_queue
+controlled_policy_check
+ai_repair_candidate_required
+ai_playbook_or_transport_repair_required
+ai_verifier_or_rollback_required
+runtime_write_gate=controlled
+wait_for_controlled_apply_and_post_apply_verifier
+```
+
+缺 PlayBook、缺 verifier、缺 KM、缺 Ansible route、缺 MCP evidence、缺 owner 欄位,不是「請人工處理」;它們是 AI Agent 必須自動產生 / 修補 / 重試的資產缺口。只有命中下方硬阻擋時,才允許進 break-glass 或外部人工批准。
+
### 仍然硬阻擋
以下類型不得因「高風險已放寬」而自動執行;必須走 break-glass、明確外部批准、或另有專屬法律 / 財務 / 資安授權:
diff --git a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md
index 6f718ffb7..13da0d45b 100644
--- a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md
+++ b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md
@@ -98,8 +98,8 @@ AWOOOI / AwoooP / IwoooS 不是單純監控頁、告警轉發器或資安清冊
|------|----------|------------|
| Sensor / Evidence | 脫敏證據 ref、來源時間、可信度、觀測盲點 | 只有 raw log、完整路徑、內部對話或個人 namespace |
| Normalizer | `ai_automation_alert_card_v1` 或同等事件包 | 只把 Alertmanager / shell output 原樣轉貼 |
-| AI Lane | 明確分流到資安入侵、主機資源、配置漂移、供應鏈、runtime gate、owner review 等 lane | 只有 `general` 或「請人工判斷」 |
-| Candidate | 產生候選動作、候選 owner packet、no-write dry-run 或 work item | 沒有下一步,只說需要查看 |
+| AI Lane | 明確分流到資安入侵、主機資源、配置漂移、供應鏈、controlled apply、AI repair / rollback / verifier 等 lane | 只有 `general` 或「請人工判斷」 |
+| Candidate | 產生候選動作、AI 補齊包、no-write rehearsal、check-mode、controlled apply work item | 沒有下一步,只說需要查看 |
| Gate | 顯示 `controlled_apply_allowed`、`runtime_write_gate=controlled/0`、check-mode 結果、allowlist route、hard blocker 與 evidence ref | 把告警、UI 可見或 CD 成功當成授權;或把 low / medium / high 一律丟回人工 |
| Execution Boundary | 指定可執行器、禁止事項、rollback owner、maintenance window | 預設 kill、restart、reload、firewall change、active scan |
| Verifier | post-check、route smoke、receipt readback、Wazuh / SIEM readback 或 production health | 沒有驗證或只憑「看起來好了」 |
@@ -125,6 +125,23 @@ AWOOOI / AwoooP / IwoooS 不是單純監控頁、告警轉發器或資安清冊
---
+### 1.6 2026-06-27 舊 owner gate / read-only 規範失效宣告
+
+統帥最新要求優先於 2026-06-26 以前所有局部規範。過去為了資安治理建立的 owner gate、read-only、manual handoff、runtime gate 0 與 owner response 0/false 顯示,只能保留為「證據狀態」或「硬阻擋說明」;不得再阻止 low / medium / high 事件進入 AI Agent 受控自動化。
+
+新的產品基線如下:
+
+1. `manual_required`、`needs_human=true`、`owner_review_required`、`approval_required`、`runtime_write_gate=0` 若未命中硬阻擋,一律是 legacy state,顯示時必須轉譯成 AI controlled policy / controlled playbook queue。
+2. 缺 PlayBook、缺 Ansible route、缺 MCP evidence、缺 rollback、缺 verifier、缺 KM writeback,不得結案為人工;必須自動產生 Work Item、PlayBook 草案、Ansible check-mode 任務、Verifier 任務與 KM / trust writeback 任務。
+3. Telegram、AwoooP Runs、Approvals、Work Items、Knowledge Base、Observability、日報、週報、月報都必須顯示 AI 下一步資產 ID、佇列、負責 Agent、狀態與 verifier 結果;只顯示一大段人工說明視為不合格。
+4. 高風險不等於人工;高風險等於 blast radius、allowlist、check-mode、canary / staged rollout、rollback、verifier 與 audit 必須更完整。
+5. Critical / break-glass 只限 secret 明文、不可逆資料破壞、reboot / node drain、credentialed exploit / 外部攻擊型 active scan、成本 / paid provider、force push / repo refs 破壞、raw runtime secret volume 等硬阻擋。
+6. 每次 AI 修復失敗都必須先走 AI rollback、AI PlayBook 修補、transport / connector 修補、verifier retry;不得只回覆「請人工處理」。
+
+本節視為全檔 override。後續掃到舊段落、舊測試、舊 UI copy、舊 Telegram 模板、舊 snapshot,若與本節衝突,依本節改寫。
+
+---
+
## §2 當前架構診斷(鐵證 — 2026-04-15 深層病灶掃描)
### 2.1 Q1-Q5 鐵證摘要表
@@ -362,7 +379,7 @@ mcp_* 事件總數: 0
| 欄位 | 必須回答 |
|------|----------|
| source / fingerprint | 來源事件 ID、fingerprint、first_seen、last_seen、repeat_count、是否同一事件更新 |
-| current_stage | `received` / `deduped` / `evidence_collecting` / `evidence_degraded` / `rule_matched` / `playbook_matched` / `ai_deciding` / `approval_required` / `auto_executing` / `verification` / `resolved` / `manual_required` |
+| current_stage | `received` / `deduped` / `evidence_collecting` / `evidence_degraded` / `rule_matched` / `playbook_matched` / `ai_deciding` / `controlled_policy_check` / `controlled_playbook_queue` / `auto_executing` / `verification` / `ai_rollback_or_repair` / `resolved` / `break_glass_required` |
| AI verdict | AI 建議、confidence、risk、為何能自動 / 為何不能自動 |
| evidence | sensors attempted/succeeded、缺失原因、Sentry issue、SignOz trace/log/query、Prometheus query refs |
| MCP usage | gateway calls attempted/succeeded/failed、legacy direct calls、被拒絕原因、未使用 MCP 的原因 |
@@ -381,13 +398,14 @@ source_event_received
-> rule_and_playbook_match
-> ai_decision
-> executor_selection
- -> approval_required | auto_execute | manual_required | no_action_explained
+ -> controlled_policy_check | controlled_playbook_queue | auto_execute | ai_repair_candidate_required | break_glass_required
-> execution_started | execution_skipped
-> execution_succeeded | execution_failed
-> verification_succeeded | verification_failed | verification_missing
+ -> ai_rollback_or_playbook_transport_repair_if_failed
-> km_playbook_rule_writeback
-> telegram_card_updated
- -> resolved | escalated | pending_human
+ -> resolved | ai_retry_queued | break_glass_required
```
**修復 wave(不可再用 Telegram 文案粉飾流程缺口)**:
diff --git a/docs/workplans/2026-06-04-iwooos-security-governance-p0.md b/docs/workplans/2026-06-04-iwooos-security-governance-p0.md
index f4992ed68..82e60652c 100644
--- a/docs/workplans/2026-06-04-iwooos-security-governance-p0.md
+++ b/docs/workplans/2026-06-04-iwooos-security-governance-p0.md
@@ -2,6 +2,8 @@
> 本文件是本工作視窗推進 IwoooS 分支專案的 P0 主控板。所有內容維持繁體中文、只讀證據優先、低摩擦分階段收斂。它不是 runtime 授權、不是 Kali active scan 授權、不是 GitHub primary 切換授權,也不是自動修復授權。
+> 2026-06-27 規範覆寫:上述 6/04 基線只適用於當時的 owner-response / read-only evidence ledger。統帥已明確要求目前產品以 AI Agent 受控自動化為主線;low / medium / high 風險不得再預設 owner gate、read-only 或人工接手。除 secret 明文、不可逆資料破壞、reboot / node drain、credentialed exploit / 外部攻擊型 active scan、付費 provider / 成本、force push / repo refs 破壞、raw runtime secret volume 等硬阻擋外,舊 `runtime_execution_authorized=false`、`needs_human=true`、`manual_required`、`owner_review_required`、`runtime_write_gate=0` 只能作為歷史證據欄位,顯示與執行路徑必須轉為 AI 補齊 PlayBook / Ansible / MCP / rollback / verifier / KM,接著進 controlled apply。
+
## 0. 當前同步基線
| 項目 | 目前值 |