From 551227f3bbaa7e19a49b94d0719ac9ad8a7aba11 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 27 Jun 2026 19:35:41 +0800 Subject: [PATCH] fix(ai): convert legacy manual gates to controlled automation --- apps/api/src/services/approval_execution.py | 14 +- apps/api/src/services/operator_outcome.py | 237 ++++++++++++++---- .../src/services/platform_operator_service.py | 30 ++- apps/api/src/services/telegram_gateway.py | 47 ++-- .../test_awooop_operator_timeline_labels.py | 6 +- apps/api/tests/test_operator_outcome.py | 20 +- .../tests/test_telegram_message_templates.py | 4 +- apps/web/src/app/[locale]/alerts/page.tsx | 5 + .../src/components/awooop/status-chain.tsx | 6 + docs/HARD_RULES.md | 41 ++- ...-04-15-MASTER-ai-autonomous-flywheel-v2.md | 28 ++- ...026-06-04-iwooos-security-governance-p0.md | 2 + 12 files changed, 324 insertions(+), 116 deletions(-) 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. 當前同步基線 | 項目 | 目前值 |