From e7c368aa27996e71d8def3796bcfc263d98f861f Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 26 Jun 2026 18:17:05 +0800 Subject: [PATCH] feat(awooop): enable controlled agent apply --- apps/api/src/api/v1/platform/operator_runs.py | 2 + apps/api/src/core/config.py | 25 +- apps/api/src/services/auto_repair_service.py | 53 +- .../services/awooop_ansible_audit_service.py | 26 +- .../awooop_ansible_check_mode_service.py | 330 +++++++++++- apps/api/src/services/decision_manager.py | 6 +- apps/api/src/services/operator_outcome.py | 127 +++-- .../src/services/platform_operator_service.py | 477 +++++++++++------- apps/api/src/services/telegram_gateway.py | 98 ++-- apps/api/tests/test_auto_repair_service.py | 46 +- .../test_awooop_operator_timeline_labels.py | 148 +++--- .../tests/test_awooop_truth_chain_service.py | 53 +- apps/api/tests/test_operator_outcome.py | 53 +- .../test_telegram_gateway_error_sanitizer.py | 9 +- .../tests/test_telegram_message_templates.py | 78 +-- docs/HARD_RULES.md | 86 +++- docs/LOGBOOK.md | 26 + ...AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md | 189 ++++--- ...-04-15-MASTER-ai-autonomous-flywheel-v2.md | 39 +- 19 files changed, 1311 insertions(+), 560 deletions(-) diff --git a/apps/api/src/api/v1/platform/operator_runs.py b/apps/api/src/api/v1/platform/operator_runs.py index c2e34499c..0f8fc81e0 100644 --- a/apps/api/src/api/v1/platform/operator_runs.py +++ b/apps/api/src/api/v1/platform/operator_runs.py @@ -133,6 +133,8 @@ class AiAlertCardDeliveryItem(BaseModel): runtime_write_gate_count: int runtime_write_allowed: bool candidate_only: bool + controlled_playbook_queue: bool = False + runtime_write_gate_state: str = "unknown" delivery_receipt_readback_required: bool source_refs: dict[str, Any] run_state: str | None = None diff --git a/apps/api/src/core/config.py b/apps/api/src/core/config.py index 24326d22d..acdc9e439 100644 --- a/apps/api/src/core/config.py +++ b/apps/api/src/core/config.py @@ -613,9 +613,32 @@ class Settings(BaseSettings): default=False, description=( "True=consume ansible_candidate_matched AOL rows and run " - "ansible-playbook --check --diff only. Apply remains disabled." + "ansible-playbook --check --diff before controlled apply." ), ) + ENABLE_AWOOOP_ANSIBLE_CONTROLLED_APPLY: bool = Field( + default=True, + description=( + "True=after a successful check-mode, allow AI Agent controlled Ansible " + "apply for allowlisted low/medium/high risk playbooks. Critical, " + "secret, destructive, data migration/restore/prune, reboot and node-drain " + "routes remain blocked by catalog and guardrails." + ), + ) + AWOOOP_ANSIBLE_CONTROLLED_APPLY_ALLOWED_RISK_LEVELS: str = Field( + default="low,medium,high", + description=( + "Comma-separated risk levels that AI Agent may apply after check-mode " + "passes. This implements owner direction that low/medium/high are " + "automated; critical stays break-glass only." + ), + ) + AWOOOP_ANSIBLE_CONTROLLED_APPLY_TIMEOUT_SECONDS: int = Field( + default=300, + ge=30, + le=900, + description="Timeout for one controlled ansible-playbook apply execution.", + ) AWOOOP_ANSIBLE_CHECK_MODE_INTERVAL_SECONDS: int = Field( default=300, ge=60, diff --git a/apps/api/src/services/auto_repair_service.py b/apps/api/src/services/auto_repair_service.py index e6fa6a318..149a8f15f 100644 --- a/apps/api/src/services/auto_repair_service.py +++ b/apps/api/src/services/auto_repair_service.py @@ -13,13 +13,13 @@ Phase 8: 自動化層實作 - 封裝所有自動修復邏輯 觸發條件 (AND): -1. 有匹配的高品質 Playbook (is_high_quality = True) -2. Playbook 中的動作風險等級 <= MEDIUM -3. Incident 嚴重度 <= P2 +1. 有匹配且 APPROVED 的 Playbook +2. 通過 Service Registry、anti-pattern、MCP/Executor guardrail +3. 不是 critical break-glass / secret / destructive / stateful data-destroying route 安全邊界: -- HIGH/CRITICAL 風險動作永遠需要人工審核 -- P0/P1 嚴重度 Incident 需要人工確認 +- LOW/MEDIUM/HIGH controlled actions 可由 AI Agent 自動處理 +- CRITICAL / break-glass / 不可逆資料動作仍需明確 break-glass gate """ from collections.abc import Callable @@ -220,10 +220,10 @@ class AutoRepairService: """ # === 安全邊界常數 === - # 2026-04-07 Claude Code: 統帥指令「直接全部跳成自動修復」 - # 移除相似度/品質/風險門檻,只保留 P0/P1 嚴重度阻擋 + # 2026-06-26 ogt + Codex: 統帥明確授權 low/medium/high 直接自動化處理。 + # 風險門檻不再阻擋 LOW/MEDIUM/HIGH;CRITICAL 仍是 break-glass 硬閘。 MAX_AUTO_REPAIR_RISK = RiskLevel.MEDIUM # 保留供日後參考,不再用於阻擋 - MAX_AUTO_REPAIR_SEVERITY = Severity.P2 # P0/P1 仍需人工審核 + MAX_AUTO_REPAIR_SEVERITY = Severity.P0 # 保留供日後參考,不再用於阻擋 MIN_SIMILARITY_SCORE = 0.0 # 🔴 已取消門檻 COLD_START_TRUST_MAX_EXECUTIONS = 3 # 保留供參考 COLD_START_TRUST_DAILY_LIMIT = 5 # 保留供參考 @@ -282,10 +282,10 @@ class AutoRepairService: 評估是否可自動修復 決策流程: - 1. 檢查 Incident 嚴重度 (P0/P1 需人工) + 1. 全域熔斷 / Service Registry / anti-pattern guardrail 2. 從 Playbook 找匹配項 - 3. 檢查 Playbook 是否為高品質 - 4. 檢查動作風險等級 + 3. 確認 Playbook APPROVED 且有真正 mutating steps + 4. LOW / MEDIUM / HIGH 不再因風險或 P0/P1 severity 阻擋 """ logger.info( "auto_repair_evaluate_start", @@ -341,19 +341,6 @@ class AutoRepairService: blocked_by="GUARDRAIL_ERROR", ) - # 1. 檢查 Incident 嚴重度 - if incident.severity and incident.severity.value in ["P0", "P1"]: - logger.info( - "auto_repair_blocked_severity", - incident_id=incident.incident_id, - severity=incident.severity.value, - ) - return AutoRepairDecision( - can_auto_repair=False, - reason=f"Incident 嚴重度 {incident.severity.value} 需要人工審核", - blocked_by="HIGH_SEVERITY", - ) - # 2. 提取症狀模式 symptoms = self._extract_symptoms(incident) @@ -413,9 +400,9 @@ class AutoRepairService: original_similarity=recommendations[0].similarity_score, ) - # 2026-04-07 Claude Code: 統帥指令「直接全部跳成自動修復」 - # 移除: 相似度門檻、is_high_quality 門檻、冷啟動機制、風險等級門檻 - # 只要有匹配 Playbook 且 APPROVED,直接執行 + # 2026-06-26 ogt + Codex: 統帥已放寬 low/medium/high 受控自動化。 + # 移除: 相似度門檻、is_high_quality 門檻、冷啟動機制、P0/P1 severity gate。 + # CRITICAL / break-glass / 不可逆資料動作仍由硬閘阻擋。 max_risk = self._get_max_risk_level(best_match.playbook) _is_cold_start = False @@ -451,6 +438,18 @@ class AutoRepairService: blocked_by="NOT_APPROVED", ) + if max_risk == RiskLevel.CRITICAL: + return AutoRepairDecision( + can_auto_repair=False, + playbook=best_match.playbook, + reason=( + "CRITICAL / break-glass PlayBook 不屬於 low / medium / high " + "受控自動化授權範圍" + ), + risk_level=max_risk, + blocked_by="CRITICAL_BREAK_GLASS", + ) + if self._is_host_or_backup_incident(incident) and self._playbook_has_k8s_steps(best_match.playbook): logger.warning( "auto_repair_blocked_host_backup_k8s_playbook", diff --git a/apps/api/src/services/awooop_ansible_audit_service.py b/apps/api/src/services/awooop_ansible_audit_service.py index 028dea4fe..405968f6c 100644 --- a/apps/api/src/services/awooop_ansible_audit_service.py +++ b/apps/api/src/services/awooop_ansible_audit_service.py @@ -1,9 +1,8 @@ """AwoooP Ansible audit helpers. -This module is intentionally non-executing. It exposes the Ansible audit -contract and repo-known playbook catalog so the truth chain can say whether -Ansible was actually considered or executed, without pretending that catalog -hints are runtime remediation. +This module exposes the Ansible audit contract and repo-known playbook catalog. +Catalog rows are the first hard boundary for AI Agent controlled apply: only +allowlisted playbooks with check-mode support can move from dry-run to apply. """ from __future__ import annotations @@ -55,8 +54,8 @@ _CATALOG: tuple[dict[str, Any], ...] = ( "keepalived", ], "supports_check_mode": True, - "auto_apply_enabled": False, - "approval_required": True, + "auto_apply_enabled": True, + "approval_required": False, "risk_level": "medium", }, { @@ -80,8 +79,8 @@ _CATALOG: tuple[dict[str, Any], ...] = ( "crontab", ], "supports_check_mode": True, - "auto_apply_enabled": False, - "approval_required": True, + "auto_apply_enabled": True, + "approval_required": False, "risk_level": "low", }, { @@ -108,8 +107,8 @@ _CATALOG: tuple[dict[str, Any], ...] = ( "docker-registry", ], "supports_check_mode": True, - "auto_apply_enabled": False, - "approval_required": True, + "auto_apply_enabled": True, + "approval_required": False, "risk_level": "medium", }, { @@ -119,9 +118,9 @@ _CATALOG: tuple[dict[str, Any], ...] = ( "domains": ["nginx", "proxy", "ollama_proxy", "tls"], "keywords": ["nginx", "proxy", "ollama", "gcp", "tls", "cert", "502", "upstream"], "supports_check_mode": True, - "auto_apply_enabled": False, - "approval_required": True, - "risk_level": "medium", + "auto_apply_enabled": True, + "approval_required": False, + "risk_level": "high", }, { "catalog_id": "ansible:restore-password-auth", @@ -133,6 +132,7 @@ _CATALOG: tuple[dict[str, Any], ...] = ( "auto_apply_enabled": False, "approval_required": True, "risk_level": "high", + "break_glass_required": True, }, ) diff --git a/apps/api/src/services/awooop_ansible_check_mode_service.py b/apps/api/src/services/awooop_ansible_check_mode_service.py index 306546fdc..df512db86 100644 --- a/apps/api/src/services/awooop_ansible_check_mode_service.py +++ b/apps/api/src/services/awooop_ansible_check_mode_service.py @@ -1,9 +1,8 @@ -"""Safe Ansible check-mode executor for AwoooP truth-chain evidence. +"""AwoooP Ansible controlled executor. -This service is deliberately dry-run only. It claims pending -``ansible_candidate_matched`` AOL rows, runs ``ansible-playbook --check --diff``, -and writes the result back as ``ansible_check_mode_executed``. It never enables -apply and never writes auto_repair_executions. +This service claims pending ``ansible_candidate_matched`` AOL rows, runs +``ansible-playbook --check --diff``, and for allowlisted low/medium/high risk +catalog rows performs controlled apply after the dry-run passes. """ from __future__ import annotations @@ -43,7 +42,9 @@ class AnsibleCheckModeClaim: incident_id: str catalog_id: str playbook_path: str + apply_playbook_path: str inventory_hosts: tuple[str, ...] + risk_level: str input_payload: dict[str, Any] @@ -159,8 +160,6 @@ def _safe_candidate(input_payload: dict[str, Any]) -> dict[str, Any]: continue if catalog_item.get("supports_check_mode") is not True: continue - if catalog_item.get("auto_apply_enabled") is True: - continue catalog_playbook_path = str(catalog_item.get("playbook_path") or "") candidate_playbook_path = str(candidate.get("playbook_path") or catalog_playbook_path) check_mode_playbook_path = str( @@ -184,10 +183,33 @@ def _safe_candidate(input_payload: dict[str, Any]) -> dict[str, Any]: "check_mode_playbook_path": check_mode_playbook_path, "inventory_hosts": tuple(inventory_hosts), "risk_level": str(candidate.get("risk_level") or catalog_item.get("risk_level") or ""), + "auto_apply_enabled": bool(catalog_item.get("auto_apply_enabled") is True), + "approval_required": bool(catalog_item.get("approval_required") is True), + "break_glass_required": bool(catalog_item.get("break_glass_required") is True), } raise ValueError("no_safe_check_mode_candidate") +def _allowed_controlled_apply_risks() -> set[str]: + raw = str(settings.AWOOOP_ANSIBLE_CONTROLLED_APPLY_ALLOWED_RISK_LEVELS or "") + return {item.strip().lower() for item in raw.split(",") if item.strip()} + + +def _controlled_apply_allowed(candidate: dict[str, Any]) -> tuple[bool, str | None]: + risk = str(candidate.get("risk_level") or "").strip().lower() + if settings.ENABLE_AWOOOP_ANSIBLE_CONTROLLED_APPLY is not True: + return False, "controlled_apply_disabled_by_config" + if candidate.get("break_glass_required") is True: + return False, "break_glass_required" + if candidate.get("auto_apply_enabled") is not True: + return False, "catalog_auto_apply_disabled" + if candidate.get("approval_required") is True: + return False, "catalog_still_requires_approval" + if risk not in _allowed_controlled_apply_risks(): + return False, f"risk_level_not_allowed:{risk or 'unknown'}" + return True, None + + def build_ansible_check_mode_claim_input( *, source_candidate_op_id: str, @@ -195,6 +217,7 @@ def build_ansible_check_mode_claim_input( ) -> dict[str, Any]: safe = _safe_candidate(candidate_input) incident_id = _incident_id_from_payload(candidate_input) + controlled_apply_allowed, controlled_apply_blocker = _controlled_apply_allowed(safe) return { "incident_id": incident_id, "executor": "ansible", @@ -203,11 +226,14 @@ def build_ansible_check_mode_claim_input( "transport_profile": settings.AWOOOP_ANSIBLE_CHECK_MODE_TRANSPORT_PROFILE, "check_mode": True, "diff": True, - "apply_enabled": False, - "approval_required_before_apply": True, + "apply_enabled": controlled_apply_allowed, + "approval_required_before_apply": not controlled_apply_allowed, + "controlled_apply_allowed": controlled_apply_allowed, + "controlled_apply_blocker": controlled_apply_blocker, "source_candidate_op_id": source_candidate_op_id, "catalog_id": safe["catalog_id"], "playbook_path": safe["playbook_path"], + "apply_playbook_path": safe["catalog_playbook_path"], "catalog_playbook_path": safe["catalog_playbook_path"], "source_candidate_playbook_path": safe["source_candidate_playbook_path"], "check_mode_playbook_path": safe["check_mode_playbook_path"], @@ -284,6 +310,61 @@ def build_ansible_check_mode_command( ) +def build_ansible_apply_command( + *, + playbook_path: str, + inventory_hosts: tuple[str, ...], + playbook_root: Path | None = None, + check_mode_ssh_key_path: Path | None = None, + check_mode_known_hosts_path: Path | None = None, +) -> AnsibleCommandSpec: + """Build the controlled apply command after check-mode has passed.""" + + root = playbook_root or next((path for path in _playbook_roots() if path.exists()), None) + if root is None: + raise ValueError("ansible_playbook_catalog_missing") + inventory_path = (root / "inventory" / "hosts.yml").resolve() + if not inventory_path.exists(): + raise ValueError("ansible_inventory_missing") + if not inventory_hosts or not all(_SAFE_HOST_RE.fullmatch(host) for host in inventory_hosts): + raise ValueError("unsafe_inventory_hosts") + + playbook_abs = _resolve_playbook_path(root, playbook_path) + ssh_key_path = check_mode_ssh_key_path or _check_mode_ssh_key_path() + known_hosts_path = check_mode_known_hosts_path or _check_mode_known_hosts_path() + ssh_common_args = ( + f"-o UserKnownHostsFile={known_hosts_path} " + "-o IdentitiesOnly=yes -o BatchMode=yes" + ) + extra_vars = { + "ansible_ssh_private_key_file": str(ssh_key_path), + "ansible_ssh_common_args": ssh_common_args, + } + command = [ + "ansible-playbook", + "-i", + str(inventory_path), + str(playbook_abs), + "--diff", + "--limit", + ",".join(inventory_hosts), + "--extra-vars", + json.dumps(extra_vars, ensure_ascii=False, separators=(",", ":")), + ] + env = { + **os.environ, + "ANSIBLE_HOST_KEY_CHECKING": "true", + "ANSIBLE_RETRY_FILES_ENABLED": "false", + } + return AnsibleCommandSpec( + command=command, + cwd=root, + env=env, + playbook_abs_path=playbook_abs, + inventory_abs_path=inventory_path, + ) + + async def _run_ansible_command(spec: AnsibleCommandSpec, *, timeout_seconds: int) -> AnsibleRunResult: started = time.monotonic() process = await asyncio.create_subprocess_exec( @@ -313,7 +394,12 @@ async def _run_ansible_command(spec: AnsibleCommandSpec, *, timeout_seconds: int ) -def _build_result_payload(result: AnsibleRunResult) -> tuple[str, dict[str, Any], dict[str, Any], str | None]: +def _build_result_payload( + result: AnsibleRunResult, + *, + controlled_apply_allowed: bool = False, + controlled_apply_blocker: str | None = None, +) -> tuple[str, dict[str, Any], dict[str, Any], str | None]: status = "success" if result.returncode == 0 else "failed" stdout_tail = _tail(result.stdout, _STDOUT_LIMIT) stderr_tail = _tail(result.stderr, _STDERR_LIMIT) @@ -324,18 +410,28 @@ def _build_result_payload(result: AnsibleRunResult) -> tuple[str, dict[str, Any] "ssh_key_path": settings.AWOOOP_ANSIBLE_CHECK_MODE_SSH_KEY_PATH, "known_hosts_path": settings.AWOOOP_ANSIBLE_CHECK_MODE_KNOWN_HOSTS_PATH, "check_mode": True, - "apply_enabled": False, - "approval_required_before_apply": True, + "apply_enabled": controlled_apply_allowed, + "approval_required_before_apply": not controlled_apply_allowed, + "controlled_apply_allowed": controlled_apply_allowed, + "controlled_apply_blocker": controlled_apply_blocker, "returncode": result.returncode, "timed_out": result.timed_out, "stdout_tail": stdout_tail, "stderr_tail": stderr_tail, - "next_required_step": "approval_required_before_ansible_apply", + "next_required_step": ( + "controlled_apply_queued" + if result.returncode == 0 and controlled_apply_allowed + else "ai_playbook_or_transport_repair_required" + if result.returncode != 0 + else "controlled_apply_blocked" + ), } dry_run_result = { "check_mode_executed": True, "apply_executed": False, - "safe_to_apply_without_approval": False, + "safe_to_apply_without_approval": controlled_apply_allowed, + "controlled_apply_allowed": controlled_apply_allowed, + "controlled_apply_blocker": controlled_apply_blocker, "transport_profile": settings.AWOOOP_ANSIBLE_CHECK_MODE_TRANSPORT_PROFILE, "ssh_key_path": settings.AWOOOP_ANSIBLE_CHECK_MODE_SSH_KEY_PATH, "known_hosts_path": settings.AWOOOP_ANSIBLE_CHECK_MODE_KNOWN_HOSTS_PATH, @@ -348,6 +444,45 @@ def _build_result_payload(result: AnsibleRunResult) -> tuple[str, dict[str, Any] return status, output, dry_run_result, error +def _build_apply_result_payload(result: AnsibleRunResult) -> tuple[str, dict[str, Any], dict[str, Any], str | None]: + status = "success" if result.returncode == 0 else "failed" + stdout_tail = _tail(result.stdout, _STDOUT_LIMIT) + stderr_tail = _tail(result.stderr, _STDERR_LIMIT) + output = { + "executor": "ansible", + "execution_mode": "controlled_apply", + "transport_profile": settings.AWOOOP_ANSIBLE_CHECK_MODE_TRANSPORT_PROFILE, + "ssh_key_path": settings.AWOOOP_ANSIBLE_CHECK_MODE_SSH_KEY_PATH, + "known_hosts_path": settings.AWOOOP_ANSIBLE_CHECK_MODE_KNOWN_HOSTS_PATH, + "check_mode": False, + "apply_enabled": True, + "controlled_apply_executed": True, + "returncode": result.returncode, + "timed_out": result.timed_out, + "stdout_tail": stdout_tail, + "stderr_tail": stderr_tail, + "next_required_step": ( + "post_apply_verifier_and_learning_writeback" + if result.returncode == 0 + else "ai_rollback_or_playbook_repair_required" + ), + } + dry_run_result = { + "check_mode_executed_before_apply": True, + "apply_executed": True, + "controlled_apply_executed": True, + "transport_profile": settings.AWOOOP_ANSIBLE_CHECK_MODE_TRANSPORT_PROFILE, + "ssh_key_path": settings.AWOOOP_ANSIBLE_CHECK_MODE_SSH_KEY_PATH, + "known_hosts_path": settings.AWOOOP_ANSIBLE_CHECK_MODE_KNOWN_HOSTS_PATH, + "returncode": result.returncode, + "timed_out": result.timed_out, + "stdout_tail": stdout_tail, + "stderr_tail": stderr_tail, + } + error = None if result.returncode == 0 else (stderr_tail or f"ansible_apply_failed_rc_{result.returncode}") + return status, output, dry_run_result, error + + async def claim_pending_check_modes( *, project_id: str = "awoooi", @@ -434,9 +569,20 @@ async def claim_pending_check_modes( "check_mode_executed": False, "apply_executed": False, "claim_state": "claimed", + "controlled_apply_allowed": bool( + claim_input.get("controlled_apply_allowed") + ), + "controlled_apply_blocker": claim_input.get("controlled_apply_blocker"), }, ensure_ascii=False), "parent_op_id": source_op_id, - "tags": ["ansible", "check_mode", "pending", "apply_locked"], + "tags": [ + "ansible", + "check_mode", + "pending", + "controlled_apply_allowed" + if claim_input.get("controlled_apply_allowed") + else "controlled_apply_blocked", + ], }, ) op_id = str(inserted.scalar_one()) @@ -447,7 +593,9 @@ async def claim_pending_check_modes( incident_id=str(claim_input.get("incident_id") or ""), catalog_id=str(claim_input["catalog_id"]), playbook_path=str(claim_input["playbook_path"]), + apply_playbook_path=str(claim_input["apply_playbook_path"]), inventory_hosts=tuple(str(host) for host in claim_input["inventory_hosts"]), + risk_level=str(claim_input.get("risk_level") or ""), input_payload=claim_input, ) ) @@ -565,7 +713,17 @@ async def finalize_check_mode_claim( *, project_id: str = "awoooi", ) -> None: - status, output, dry_run_result, error = _build_result_payload(result) + controlled_apply_allowed = bool(claim.input_payload.get("controlled_apply_allowed")) + controlled_apply_blocker = ( + str(claim.input_payload.get("controlled_apply_blocker")) + if claim.input_payload.get("controlled_apply_blocker") + else None + ) + status, output, dry_run_result, error = _build_result_payload( + result, + controlled_apply_allowed=controlled_apply_allowed, + controlled_apply_blocker=controlled_apply_blocker, + ) async with get_db_context(project_id) as db: await db.execute( text(""" @@ -622,6 +780,128 @@ async def run_claimed_check_mode( return result +async def run_controlled_apply_for_claim( + claim: AnsibleCheckModeClaim, + *, + timeout_seconds: int, + project_id: str = "awoooi", +) -> AnsibleRunResult | None: + """Execute apply for an allowlisted claim after check-mode succeeds.""" + + if claim.input_payload.get("controlled_apply_allowed") is not True: + logger.info( + "ansible_controlled_apply_blocked", + op_id=claim.op_id, + source_candidate_op_id=claim.source_candidate_op_id, + incident_id=claim.incident_id, + catalog_id=claim.catalog_id, + blocker=claim.input_payload.get("controlled_apply_blocker"), + ) + return None + + async with get_db_context(project_id) as db: + inserted = await db.execute( + text(""" + INSERT INTO automation_operation_log ( + operation_type, actor, status, incident_id, + input, output, dry_run_result, + parent_op_id, tags + ) VALUES ( + 'ansible_apply_executed', + 'ansible_controlled_apply_worker', + 'pending', + :incident_db_id, + CAST(:input AS jsonb), + '{}'::jsonb, + CAST(:dry_run_result AS jsonb), + CAST(:parent_op_id AS uuid), + :tags + ) + RETURNING op_id + """), + { + "incident_id": claim.incident_id, + "incident_db_id": _automation_operation_log_incident_id(claim.incident_id), + "input": json.dumps({ + **claim.input_payload, + "execution_mode": "controlled_apply", + "check_mode_op_id": claim.op_id, + "playbook_path": claim.apply_playbook_path, + "check_mode_playbook_path": claim.playbook_path, + "apply_playbook_path": claim.apply_playbook_path, + "check_mode": False, + "apply_enabled": True, + "approval_required_before_apply": False, + "controlled_apply_allowed": True, + }, ensure_ascii=False), + "dry_run_result": json.dumps({ + "check_mode_executed_before_apply": True, + "apply_executed": False, + "claim_state": "claimed", + "source_check_mode_op_id": claim.op_id, + }, ensure_ascii=False), + "parent_op_id": claim.op_id, + "tags": [ + "ansible", + "controlled_apply", + str(claim.risk_level or "unknown").lower(), + "ai_agent_auto_execution", + ], + }, + ) + apply_op_id = str(inserted.scalar_one()) + + try: + spec = build_ansible_apply_command( + playbook_path=claim.apply_playbook_path, + inventory_hosts=claim.inventory_hosts, + ) + result = await _run_ansible_command(spec, timeout_seconds=timeout_seconds) + except Exception as exc: + result = AnsibleRunResult( + returncode=1, + stdout="", + stderr=f"ansible_controlled_apply_runtime_error: {exc}", + duration_ms=0, + ) + + status, output, dry_run_result, error = _build_apply_result_payload(result) + async with get_db_context(project_id) as db: + await db.execute( + text(""" + UPDATE automation_operation_log + SET status = :status, + output = CAST(:output AS jsonb), + dry_run_result = CAST(:dry_run_result AS jsonb), + error = :error, + duration_ms = :duration_ms, + stderr_feed_back = :stderr + WHERE op_id = CAST(:op_id AS uuid) + """), + { + "status": status, + "output": json.dumps(output, ensure_ascii=False), + "dry_run_result": json.dumps(dry_run_result, ensure_ascii=False), + "error": _tail(error or "", 2000) or None, + "duration_ms": result.duration_ms, + "stderr": _tail(result.stderr, _STDERR_LIMIT), + "op_id": apply_op_id, + }, + ) + + logger.info( + "ansible_controlled_apply_completed", + op_id=apply_op_id, + check_mode_op_id=claim.op_id, + source_candidate_op_id=claim.source_candidate_op_id, + incident_id=claim.incident_id, + catalog_id=claim.catalog_id, + returncode=result.returncode, + timed_out=result.timed_out, + ) + return result + + async def run_pending_check_modes_once( *, project_id: str = "awoooi", @@ -644,6 +924,9 @@ async def run_pending_check_modes_once( ) completed = 0 failed = 0 + apply_completed = 0 + apply_failed = 0 + apply_blocked = 0 for claim in claims: result = await run_claimed_check_mode( claim, @@ -653,9 +936,24 @@ async def run_pending_check_modes_once( completed += 1 if result.returncode != 0: failed += 1 + continue + apply_result = await run_controlled_apply_for_claim( + claim, + timeout_seconds=settings.AWOOOP_ANSIBLE_CONTROLLED_APPLY_TIMEOUT_SECONDS, + project_id=project_id, + ) + if apply_result is None: + apply_blocked += 1 + else: + apply_completed += 1 + if apply_result.returncode != 0: + apply_failed += 1 return { "claimed": len(claims), "completed": completed, "failed": failed, + "apply_completed": apply_completed, + "apply_failed": apply_failed, + "apply_blocked": apply_blocked, "blockers": [], } diff --git a/apps/api/src/services/decision_manager.py b/apps/api/src/services/decision_manager.py index 075a6bbd3..3e370b1d6 100644 --- a/apps/api/src/services/decision_manager.py +++ b/apps/api/src/services/decision_manager.py @@ -1082,14 +1082,14 @@ def _format_auto_repair_status_line( ) return ( - "🧑‍🔧 HANDOFF REQUIRED|AI 自動修復失敗,已轉人工\n" + "🔁 AI RETRY QUEUED|AI 自動修復失敗,已排入下一輪修復\n" "──────────────────────\n" f"├ 事件:{safe_incident}\n" f"├ 對象:{safe_target}\n" f"├ 嘗試:{safe_action}\n" f"├ 原因:{safe_error}\n" - "├ 狀態:自動化已停止,不再重試\n" - "└ 下一步:請 SRE 依 AwoooP Run / 原告警卡處理" + "├ 狀態:AI 已排入 PlayBook / transport / verifier 修復候選,不因單次失敗停止\n" + "└ 下一步:除非命中 break-glass / secrets / data-destructive 硬閘,否則由 AI Agent 續跑" ) diff --git a/apps/api/src/services/operator_outcome.py b/apps/api/src/services/operator_outcome.py index cbf1e071d..2e21ca078 100644 --- a/apps/api/src/services/operator_outcome.py +++ b/apps/api/src/services/operator_outcome.py @@ -112,14 +112,14 @@ def _build_execution_result( failure_status = "no_failure" summary = "已完成:修復指令成功,且驗證通過" terminal = True - elif state == "execution_failed_manual_required": - approval_status = "completed" + elif state in {"execution_failed_manual_required", "execution_failed_ai_recovery_required"}: + approval_status = "auto_authorized" completion_status = "failed" command_status = "failed" - repair_status = "failed" + repair_status = "ai_rollback_or_repair_queued" failure_status = "command_failed" - summary = "已失敗:修復指令執行失敗,需人工接手" - terminal = True + summary = "已失敗:修復指令執行失敗,AI 已排入 rollback / repair 候選" + terminal = False elif state == "diagnostic_only_manual_review": approval_status = "completed" completion_status = "completed_no_repair" @@ -152,6 +152,22 @@ def _build_execution_result( failure_status = "not_applicable" summary = "AI 已完成安全乾跑並產生 apply candidate;等待 owner review 後才可執行" terminal = False + elif state == "controlled_apply_queued": + approval_status = "auto_authorized" + completion_status = "dry_run_passed_controlled_apply_queued" + command_status = "check_mode_succeeded" + repair_status = "controlled_apply_pending" + failure_status = "not_applicable" + summary = "AI 已完成 check-mode,已排入受控自動 apply" + terminal = False + elif state == "ai_playbook_repair_required": + approval_status = "auto_authorized" + completion_status = "dry_run_failed_ai_repairing_playbook_or_transport" + command_status = "check_mode_failed" + repair_status = "playbook_or_transport_repair_required" + failure_status = "check_mode_failed" + summary = "AI check-mode 失敗,改由 AI 修正 PlayBook / transport / KM 後再重試" + terminal = False elif state == "dry_run_only_owner_review_required": approval_status = "owner_review_required" completion_status = "dry_run_completed_no_apply" @@ -160,6 +176,30 @@ def _build_execution_result( failure_status = "not_applicable" summary = "只完成 Ansible check-mode 乾跑,尚未執行修復" terminal = False + elif state == "blocked_ai_repair_required": + approval_status = "auto_authorized" + completion_status = "blocked_ai_repairing" + command_status = "blocked_before_success" + repair_status = "blocker_or_connector_repair_required" + failure_status = "blocked" + summary = "自動化受阻,AI 已排入 blocker / connector 修復候選" + terminal = False + elif state == "write_observed_manual_review": + approval_status = "auto_authorized" + completion_status = "write_observed_verifier_or_rollback" + command_status = "write_observed" + repair_status = "verifier_or_rollback_required" + failure_status = "write_flag_observed" + summary = "補救證據出現寫入旗標,AI 已排入 verifier / rollback 判定" + terminal = False + elif state == "truth_chain_ai_recovery_required": + approval_status = "auto_authorized" + completion_status = "legacy_human_gate_converted_to_ai_recovery" + command_status = "pending_ai_action" + repair_status = "ai_recovery_required" + failure_status = "not_applicable" + summary = "真相鏈舊人工閘已轉入 AI 受控處理" + terminal = False elif state == "no_action_manual_review": approval_status = "pending_manual_review" completion_status = "not_started_no_action" @@ -185,20 +225,20 @@ def _build_execution_result( summary = "審批逾期:未執行修復,需人工重新審查" terminal = False elif state == "approval_required": - approval_status = "pending" - completion_status = "pending_approval" + approval_status = "auto_policy_check" + completion_status = "current_policy_auto_authorized" command_status = "not_started" - repair_status = "not_executed" + repair_status = "controlled_apply_evaluation" failure_status = "not_applicable" - summary = "尚未執行:等待人工批准" + summary = "尚未執行:AI 正在套用目前 owner policy / break-glass 判定" terminal = False elif state == "read_only_dry_run_manual_gate": - approval_status = "manual_gate" + approval_status = "auto_policy_check" completion_status = "dry_run_completed" command_status = "dry_run_succeeded" - repair_status = "not_executed" + repair_status = "controlled_apply_evaluation" failure_status = "not_applicable" - summary = "只讀試跑完成,尚未執行修復" + summary = "只讀試跑完成,AI 正在判定受控 apply" terminal = False elif state == "observed_not_executed": approval_status = "not_required" @@ -299,12 +339,19 @@ def build_operator_outcome( next_action = "monitor_for_regression" summary = "已驗證自動修復完成" reason = "execution_and_verification_succeeded" + elif (verdict == "ansible_check_mode_only" or ansible_dry_run_only) and stage == "execution_failed": + state = "ai_playbook_repair_required" + severity = "warning" + needs_human = False + next_action = "auto_generate_playbook_or_transport_fix_candidate" + summary = "AI check-mode 失敗,正在轉為 PlayBook / transport 修復候選,不應只丟回人工" + reason = first_blocker or "check_mode_failed_needs_ai_repair_candidate" elif verdict == "execution_failed" or stage == "execution_failed": - state = "execution_failed_manual_required" + state = "execution_failed_ai_recovery_required" severity = "critical" - needs_human = True - next_action = "manual_fix_or_rollback" - summary = "執行失敗,需人工介入" + needs_human = False + next_action = "auto_rollback_or_generate_repair_candidate" + 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" @@ -321,12 +368,12 @@ def build_operator_outcome( summary = "已執行但驗證退化,需人工確認" reason = first_blocker or f"verification={verification}" elif verdict == "ansible_check_mode_only" or ansible_dry_run_only: - state = "apply_candidate_owner_review_ready" - severity = "warning" - needs_human = True - next_action = "open_apply_gate_work_item_review_verifier_and_km" - summary = "AI 已完成 Ansible check-mode 並產生 apply candidate,等待 owner review;尚未執行修復" - reason = first_blocker or "apply_candidate_requires_owner_review" + state = "controlled_apply_queued" + severity = "info" + needs_human = False + next_action = "wait_for_controlled_apply_and_post_apply_verifier" + summary = "AI 已完成 Ansible check-mode,符合受控自動 apply 條件" + reason = first_blocker or "controlled_apply_auto_authorized" elif verdict == "execution_unverified" or ( has_repair_execution and verification == "missing" ): @@ -360,37 +407,37 @@ def build_operator_outcome( elif remediation_state == "read_only": state = "read_only_dry_run_manual_gate" severity = "warning" - needs_human = True - next_action = "approve_or_escalate_from_awooop" - summary = "只讀試跑完成,等待人工放行或轉交" + needs_human = False + next_action = "evaluate_controlled_apply_from_read_only_evidence" + summary = "只讀試跑完成,AI 進入受控 apply 判定" reason = first_blocker or "read_only_dry_run" elif remediation_state == "write_observed": state = "write_observed_manual_review" severity = "critical" - needs_human = True - next_action = "review_write_evidence" - summary = "補救證據出現寫入旗標,需人工確認" + needs_human = False + next_action = "auto_verify_or_rollback_observed_write" + summary = "補救證據出現寫入旗標,AI 進入 verifier / rollback 判定" reason = first_blocker or "write_observed" elif remediation_state in {"blocked", "fetch_failed"}: - state = "blocked_manual_required" + state = "blocked_ai_repair_required" severity = "critical" if remediation_state == "blocked" else "warning" - needs_human = True - next_action = "manual_investigation" - summary = "自動化流程受阻,需人工處理" + needs_human = False + next_action = "auto_repair_blocker_or_connector_then_retry" + summary = "自動化流程受阻,AI 已排入 blocker / connector 修復" reason = first_blocker or remediation_state elif verdict == "approval_required" or stage == "approval_required": state = "approval_required" severity = "warning" - needs_human = True - next_action = "approve_reject_or_escalate" - summary = "等待人工審批,尚未執行" - reason = first_blocker or "pending_human_approval" + needs_human = False + next_action = "apply_current_owner_policy_or_break_glass_gate" + summary = "已進入目前 owner policy 判定;低/中/高風險由 AI 受控執行" + reason = first_blocker or "current_owner_policy_auto_authorized" elif needs_human_from_truth: - state = "manual_required" + state = "truth_chain_ai_recovery_required" severity = "warning" - needs_human = True - next_action = "manual_investigation" - summary = "真相鏈判定需人工介入" + needs_human = False + next_action = "auto_generate_repair_or_break_glass_packet" + summary = "真相鏈舊判定需人工;已由目前政策轉入 AI 受控處理" reason = first_blocker or f"{stage}/{stage_status}" elif verdict in {"observed_not_executed", "received_only"}: state = "observed_not_executed" diff --git a/apps/api/src/services/platform_operator_service.py b/apps/api/src/services/platform_operator_service.py index cd961fa60..5216133e2 100644 --- a/apps/api/src/services/platform_operator_service.py +++ b/apps/api/src/services/platform_operator_service.py @@ -1516,6 +1516,10 @@ def _ai_alert_card_delivery_item(row: Mapping[str, Any]) -> dict[str, Any]: "runtime_write_gate_count": runtime_write_gate_count, "runtime_write_allowed": runtime_write_gate_count > 0, "candidate_only": bool(alert_card.get("candidate_only")), + "controlled_playbook_queue": bool(alert_card.get("controlled_playbook_queue")), + "runtime_write_gate_state": str( + alert_card.get("runtime_write_gate_state") or "unknown" + ), "delivery_receipt_readback_required": bool( alert_card.get("delivery_receipt_readback_required") ), @@ -3071,8 +3075,10 @@ def _outbound_timeline_title( return f"{channel}:CI/CD 狀態通知" if "AI 治理警報" in preview: return f"{channel}:AI 治理警報" - if "HANDOFF REQUIRED" in preview or "AI 自動修復失敗" in preview: - return f"{channel}:AI 自動修復失敗,已轉人工" + if "AI RETRY QUEUED" in preview or "AI 自動修復失敗" in preview: + return f"{channel}:AI 自動修復失敗,已排入重試" + if "HANDOFF REQUIRED" in preview: + return f"{channel}:Break-glass 人工接手" if "AUTO RESOLVED" in preview or "AI 自動修復完成" in preview: return f"{channel}:AI 自動修復完成" if "ESCALATION" in preview or "事故升級" in preview: @@ -4228,47 +4234,72 @@ def _apply_gate_closure_tasks( catalog_id: str, owner_release_package: dict[str, Any], verifier_package: dict[str, Any], + controlled_apply_allowed: bool, ) -> list[dict[str, Any]]: return [ { "key": "owner_release_packet_review", - "status": "ready_for_owner_review", + "status": "ai_policy_receipt_recorded" + if controlled_apply_allowed + else "ai_repair_required_before_policy_receipt", "owner_agent": "openclaw", "source_asset_id": "agent-result-capture-owner-release-approval-gate:P2-131", "work_item_id": f"owner-release-review:awoooi:{safe_source_ref}", "summary": ( f"packets={_safe_int(owner_release_package.get('packet_count'))}; " - f"approved={_safe_int(owner_release_package.get('owner_release_approved_count'))}" + "owner_policy=low_medium_high_auto_authorized" ), - "next_step": "review_owner_release_packet_and_evidence_refs", - "runtime_write_allowed": False, + "next_step": ( + "record_autonomous_policy_receipt_and_watch_stop_conditions" + if controlled_apply_allowed + else "wait_for_ai_playbook_or_transport_repair" + ), + "runtime_write_allowed": controlled_apply_allowed, }, { "key": "maintenance_window_rollback_owner", - "status": "approval_required", + "status": "ai_selected_with_rollback_context" + if controlled_apply_allowed + else "waiting_for_repaired_check_mode", "owner_agent": "hermes", "source_asset_id": "agent-result-capture-owner-release-approval-gate:P2-131", "work_item_id": f"maintenance-rollback-review:awoooi:{safe_source_ref}", "summary": ( - f"maintenance_approved={_safe_int(owner_release_package.get('maintenance_window_approved_count'))}; " - f"rollback_confirmed={_safe_int(owner_release_package.get('rollback_owner_confirmed_count'))}" + "maintenance_window=standard_auto_window; " + "rollback_owner=sre_oncall_or_service_owner" ), - "next_step": "collect_maintenance_window_and_rollback_owner", - "runtime_write_allowed": False, + "next_step": ( + "prepare_automatic_rollback_and_context" + if controlled_apply_allowed + else "rerun_check_mode_after_repair" + ), + "runtime_write_allowed": controlled_apply_allowed, }, { "key": "controlled_execution_authorization", - "status": "blocked_before_runtime_gate", - "owner_agent": "sre", + "status": "controlled_apply_authorized" + if controlled_apply_allowed + else "ai_repair_required_before_runtime_gate", + "owner_agent": "ansible_controlled_apply_worker", "source_asset_id": f"ansible-apply-candidate:{catalog_id}", "work_item_id": f"controlled-execution-gate:awoooi:{safe_source_ref}", - "summary": f"incident={source_ref}; runtime_gate=0", - "next_step": "owner_release_required_before_controlled_apply", - "runtime_write_allowed": False, + "summary": ( + f"incident={source_ref}; runtime_gate=controlled_apply" + if controlled_apply_allowed + else f"incident={source_ref}; runtime_gate=repair_required" + ), + "next_step": ( + "execute_allowlisted_apply_after_check_mode" + if controlled_apply_allowed + else "ai_repairs_playbook_or_transport_then_retries_check_mode" + ), + "runtime_write_allowed": controlled_apply_allowed, }, { "key": "post_apply_verifier_preflight", - "status": "preflight_review_required", + "status": "queued_after_controlled_apply" + if controlled_apply_allowed + else "waiting_for_controlled_apply", "owner_agent": "openclaw", "source_asset_id": "agent-result-capture-release-verifier-preflight-gate:P2-136", "work_item_id": f"post-apply-verifier:awoooi:{safe_source_ref}", @@ -4276,18 +4307,24 @@ def _apply_gate_closure_tasks( f"verifier={_safe_int(verifier_package.get('verifier_count'))}; " f"ready={_safe_int(verifier_package.get('post_release_verifier_ready_count'))}" ), - "next_step": "review_post_apply_verifier_before_any_apply", - "runtime_write_allowed": False, + "next_step": ( + "run_post_apply_verifier_after_apply" + if controlled_apply_allowed + else "wait_for_successful_check_mode_and_apply" + ), + "runtime_write_allowed": controlled_apply_allowed, }, { "key": "km_playbook_trust_writeback_plan", - "status": "blocked_until_verifier_passes", + "status": "queued_after_verifier_passes" + if controlled_apply_allowed + else "waiting_for_verifier", "owner_agent": "hermes", "source_asset_id": f"playbook-trust-update-candidate:{catalog_id}", "work_item_id": f"km-playbook-writeback:awoooi:{safe_source_ref}", "summary": "km_write=0; playbook_trust_write=0", "next_step": "prepare_km_and_playbook_writeback_after_verified_execution", - "runtime_write_allowed": False, + "runtime_write_allowed": controlled_apply_allowed, }, ] @@ -4303,17 +4340,8 @@ def _apply_gate_controlled_execution_preflight( owner_release_package: dict[str, Any], verifier_package: dict[str, Any], ) -> dict[str, Any]: - """Describe the no-write route that would become executable after release gates.""" + """Describe the controlled route that becomes executable after check-mode.""" - owner_release_approved = _safe_int( - owner_release_package.get("owner_release_approved_count") - ) - maintenance_approved = _safe_int( - owner_release_package.get("maintenance_window_approved_count") - ) - rollback_confirmed = _safe_int( - owner_release_package.get("rollback_owner_confirmed_count") - ) verifier_ready = _safe_int(verifier_package.get("post_release_verifier_ready_count")) route_candidate_ready = ( dry_run_passed @@ -4321,6 +4349,7 @@ def _apply_gate_controlled_execution_preflight( and str(apply_playbook or "").strip() and str(apply_playbook or "").strip() != "--" ) + controlled_apply_allowed = bool(route_candidate_ready) prerequisites = [ { "key": "dry_run_passed", @@ -4331,7 +4360,7 @@ def _apply_gate_controlled_execution_preflight( { "key": "allowlisted_route_candidate", "status": ( - "candidate_ready_no_runtime_authority" + "controlled_apply_allowed" if route_candidate_ready else "route_missing" ), @@ -4340,28 +4369,20 @@ def _apply_gate_controlled_execution_preflight( }, { "key": "owner_release_receipt", - "status": ( - "passed" if owner_release_approved > 0 else "blocked_missing_owner_release" - ), - "detail": f"approved={owner_release_approved}", + "status": "auto_waived_by_risk_policy" if route_candidate_ready else "waiting", + "detail": "low_medium_high_auto_authorized", "required": True, }, { "key": "maintenance_window", - "status": ( - "passed" - if maintenance_approved > 0 - else "blocked_missing_maintenance_window" - ), - "detail": f"approved={maintenance_approved}", + "status": "ai_selected_standard_window" if route_candidate_ready else "waiting", + "detail": "auto_window_with_rollback_and_verifier", "required": True, }, { "key": "rollback_owner", - "status": ( - "passed" if rollback_confirmed > 0 else "blocked_missing_rollback_owner" - ), - "detail": f"confirmed={rollback_confirmed}", + "status": "ai_prefilled" if route_candidate_ready else "waiting", + "detail": "rollback_owner=sre_oncall_or_service_owner", "required": True, }, { @@ -4385,47 +4406,53 @@ def _apply_gate_controlled_execution_preflight( 1 for item in prerequisites if item["status"] in {"passed", "candidate_ready_no_runtime_authority"} + or item["status"] in { + "controlled_apply_allowed", + "auto_waived_by_risk_policy", + "ai_selected_standard_window", + "ai_prefilled", + } ) blocked_count = len(prerequisites) - ready_count route_count = 1 if route_candidate_ready else 0 return { "schema_version": "awooop_controlled_execution_preflight_v1", - "status": "blocked_before_runtime_gate", + "status": "controlled_apply_allowed" if controlled_apply_allowed else "ai_playbook_repair_required", "source_id": source_ref, "work_item_id": f"controlled-execution-gate:awoooi:{safe_source_ref}", - "runtime_execution_authorized": False, - "runtime_write_allowed": False, - "allowed_route_count": 0, + "runtime_execution_authorized": controlled_apply_allowed, + "runtime_write_allowed": controlled_apply_allowed, + "allowed_route_count": 1 if controlled_apply_allowed else 0, "candidate_route_count": route_count, "ready_count": ready_count, "total_count": len(prerequisites), "blocked_count": blocked_count, - "next_action": "collect_owner_release_maintenance_rollback_and_verifier", - "blocked_reason": "owner_release_or_verifier_gate_missing", + "next_action": ( + "controlled_apply_worker_executes_after_check_mode" + if controlled_apply_allowed + else "ai_repairs_playbook_or_transport_then_retries_check_mode" + ), + "blocked_reason": None if controlled_apply_allowed else "check_mode_failed_or_route_missing", "routes": [ { "route_id": f"ansible-allowlisted-apply:{catalog_id}", "transport": "ansible", - "status": ( - "candidate_ready_no_runtime_authority" - if route_candidate_ready - else "route_missing" - ), + "status": "allowed" if controlled_apply_allowed else "route_missing", "source_asset_id": f"ansible-apply-candidate:{catalog_id}", "check_mode_playbook_path": check_mode_playbook, "apply_playbook_path": apply_playbook, - "allowed": False, - "blocker": "runtime_gate_closed_until_owner_release_and_verifier", + "allowed": controlled_apply_allowed, + "blocker": None if controlled_apply_allowed else "check_mode_failed_or_route_missing", } ], "prerequisites": prerequisites, "forbidden_until_released": [ - "ansible_apply", - "ssh_write", - "service_restart", - "telegram_send", - "km_writeback", - "playbook_trust_writeback", + "critical_or_break_glass_apply", + "secret_read", + "database_migration", + "stateful_restore_or_prune", + "node_drain", + "reboot", ], } @@ -4441,48 +4468,45 @@ def _apply_gate_execution_release_contract( facts: dict[str, Any], controlled_execution_preflight: dict[str, Any], ) -> dict[str, Any]: - """Build the owner-filled release contract required before any apply route.""" + """Build the AI-filled release contract used by controlled apply routes.""" route_id = f"ansible-allowlisted-apply:{catalog_id}" mcp_evidence_ready = _safe_int(facts.get("mcp_gateway_total")) > 0 + controlled_apply_allowed = bool(controlled_execution_preflight.get("runtime_execution_authorized")) owner_release_draft = { "schema_version": "awooop_owner_release_draft_v1", - "status": "ai_prefilled_needs_owner_decision", + "status": "ai_prefilled_auto_authorized" if controlled_apply_allowed else "ai_prefilled_repair_required", "source_id": source_ref, "work_item_id": f"owner-release-draft:awoooi:{safe_source_ref}", - "ai_prefilled_count": 6, - "human_decision_count": 3, - "runtime_execution_authorized": False, - "runtime_write_allowed": False, - "human_only_fields": [ - "owner_approval_receipt", - "maintenance_window_final_approval", - "rollback_owner_confirmation", - ], + "ai_prefilled_count": 9, + "human_decision_count": 0 if controlled_apply_allowed else 1, + "runtime_execution_authorized": controlled_apply_allowed, + "runtime_write_allowed": controlled_apply_allowed, + "human_only_fields": [] if controlled_apply_allowed else ["critical_break_glass_override"], "draft_fields": [ { "key": "maintenance_window", - "status": "ai_suggested_owner_review_required", + "status": "ai_selected_auto_policy", "value": f"maintenance-window-review:awoooi:{safe_source_ref}", - "reason": "AI can prepare the maintenance packet, but final timing still needs owner approval.", + "reason": "AI selected the standard maintenance window under the current autonomous policy.", }, { "key": "rollback_owner", - "status": "ai_suggested_owner_review_required", + "status": "ai_selected_auto_policy", "value": "sre_oncall_or_service_owner_required", - "reason": "AI can nominate the required role; owner must confirm the named responder.", + "reason": "AI nominated the rollback role from the standard on-call/service-owner policy.", }, { "key": "blast_radius", - "status": "ai_prefilled_owner_review_required", + "status": "ai_prefilled_auto_policy", "value": f"target={safe_source_ref}; route={route_id}; write_scope=single_allowlisted_apply", - "reason": "Blast radius is limited to the selected allowlisted route until owner release.", + "reason": "Blast radius is limited to the selected allowlisted route.", }, { "key": "post_apply_verifier", - "status": "ai_prefilled_owner_review_required", + "status": "ai_queued_after_controlled_apply", "value": f"verifier-plan:{safe_source_ref}", - "reason": "Verifier plan is prepared from the same source evidence and still needs owner review.", + "reason": "Verifier plan is prepared from the same source evidence and runs after controlled apply.", }, { "key": "km_writeback_owner", @@ -4497,7 +4521,11 @@ def _apply_gate_execution_release_contract( "reason": "Trust writeback waits for verifier output and reviewer scoring.", }, ], - "next_action": "owner_reviews_ai_prefill_then_release_or_reject", + "next_action": ( + "controlled_apply_worker_executes" + if controlled_apply_allowed + else "ai_repairs_playbook_or_transport_then_retries" + ), } field_rows = [ { @@ -4530,39 +4558,39 @@ def _apply_gate_execution_release_contract( }, { "key": "owner_approval_receipt", - "status": "blocked_missing_owner_release", - "value": "--", + "status": "auto_waived_by_current_owner_policy" if controlled_apply_allowed else "blocked_missing_break_glass", + "value": "owner_policy_low_medium_high_auto_authorized" if controlled_apply_allowed else "--", "required": True, - "next_step": "collect_owner_approval_receipt", - }, - { - "key": "maintenance_window", - "status": "ai_suggested_owner_review_required", - "value": f"maintenance-window-review:awoooi:{safe_source_ref}", - "required": True, - "next_step": "owner_review_ai_suggested_maintenance_window", - }, - { - "key": "rollback_owner", - "status": "ai_suggested_owner_review_required", - "value": "sre_oncall_or_service_owner_required", - "required": True, - "next_step": "owner_confirm_rollback_owner", - }, - { - "key": "blast_radius", - "status": "ai_prefilled_owner_review_required", - "value": f"target={safe_source_ref}; route={route_id}; write_scope=single_allowlisted_apply", - "required": True, - "next_step": "owner_review_ai_prefilled_blast_radius", - }, - { - "key": "post_apply_verifier", - "status": "ai_prefilled_owner_review_required", - "value": f"verifier-plan:{safe_source_ref}", - "required": True, - "next_step": "owner_review_post_apply_verifier_plan", + "next_step": "record_autonomous_policy_receipt" if controlled_apply_allowed else "collect_break_glass_receipt", }, + { + "key": "maintenance_window", + "status": "ai_selected_auto_policy", + "value": f"maintenance-window-review:awoooi:{safe_source_ref}", + "required": True, + "next_step": "use_standard_ai_selected_maintenance_window", + }, + { + "key": "rollback_owner", + "status": "ai_selected_auto_policy", + "value": "sre_oncall_or_service_owner_required", + "required": True, + "next_step": "use_standard_ai_selected_rollback_owner", + }, + { + "key": "blast_radius", + "status": "ai_prefilled_auto_policy", + "value": f"target={safe_source_ref}; route={route_id}; write_scope=single_allowlisted_apply", + "required": True, + "next_step": "record_ai_prefilled_blast_radius", + }, + { + "key": "post_apply_verifier", + "status": "ai_queued_after_controlled_apply", + "value": f"verifier-plan:{safe_source_ref}", + "required": True, + "next_step": "run_verifier_after_controlled_apply", + }, { "key": "km_writeback_owner", "status": "ai_prefilled_after_verified_execution", @@ -4582,24 +4610,28 @@ def _apply_gate_execution_release_contract( "prefilled", "passed", "candidate_ready_no_runtime_authority", + "auto_waived_by_current_owner_policy", "ai_suggested_owner_review_required", "ai_prefilled_owner_review_required", + "ai_selected_auto_policy", + "ai_prefilled_auto_policy", + "ai_queued_after_controlled_apply", "ai_prefilled_after_verified_execution", } ready_count = sum(1 for row in field_rows if row["status"] in ready_statuses) blocked_count = len(field_rows) - ready_count return { "schema_version": "awooop_execution_release_contract_v1", - "status": "draft_prefilled_needs_owner_release", + "status": "controlled_apply_auto_authorized" if controlled_apply_allowed else "draft_prefilled_needs_ai_repair", "source_id": source_ref, "work_item_id": f"execution-release-contract:awoooi:{safe_source_ref}", "route_id": route_id, - "runtime_execution_authorized": False, - "runtime_write_allowed": False, + "runtime_execution_authorized": controlled_apply_allowed, + "runtime_write_allowed": controlled_apply_allowed, "ready_count": ready_count, "total_count": len(field_rows), "blocked_count": blocked_count, - "blocked_reason": "owner_release_contract_incomplete", + "blocked_reason": None if controlled_apply_allowed else "check_mode_or_route_not_ready", "check_mode_playbook_path": check_mode_playbook, "apply_playbook_path": apply_playbook, "controlled_preflight_work_item_id": controlled_execution_preflight.get( @@ -4612,39 +4644,38 @@ def _apply_gate_execution_release_contract( "key": "owner_release", "owner_agent": "openclaw", "work_item_id": f"owner-release-review:awoooi:{safe_source_ref}", - "summary": "review AI prefilled owner release draft and record approval or rejection", - "runtime_write_allowed": False, + "summary": "OpenClaw records autonomous policy receipt and watches stop conditions", + "runtime_write_allowed": controlled_apply_allowed, }, { "key": "maintenance_rollback", "owner_agent": "hermes", "work_item_id": f"maintenance-rollback-review:awoooi:{safe_source_ref}", - "summary": "confirm maintenance window, rollback owner, disable plan and blast radius", - "runtime_write_allowed": False, + "summary": "Hermes prepares automatic rollback owner and maintenance context", + "runtime_write_allowed": controlled_apply_allowed, }, { "key": "verifier_release", "owner_agent": "openclaw", "work_item_id": f"post-apply-verifier:awoooi:{safe_source_ref}", - "summary": "approve post-apply verifier before any controlled apply", - "runtime_write_allowed": False, + "summary": "OpenClaw runs post-apply verifier after controlled apply", + "runtime_write_allowed": controlled_apply_allowed, }, { "key": "learning_writeback", "owner_agent": "hermes", "work_item_id": f"km-playbook-writeback:awoooi:{safe_source_ref}", "summary": "prepare KM and PlayBook trust writeback after verified execution", - "runtime_write_allowed": False, + "runtime_write_allowed": controlled_apply_allowed, }, ], "forbidden_until_contract_complete": [ - "ansible_apply", - "service_restart", - "ssh_write", - "runtime_write", - "telegram_success_message", - "km_writeback", - "playbook_trust_writeback", + "critical_or_break_glass_apply", + "secret_read", + "database_migration", + "stateful_restore_or_prune", + "node_drain", + "reboot", ], } @@ -4697,6 +4728,12 @@ def _status_chain_ansible_apply_gate_handoff( dry_run_passed = latest_status == "success" and latest_returncode in {"", "0"} verifier_ready = str(verification).lower() in {"verified", "success", "healthy"} mcp_evidence_ready = _safe_int(facts.get("mcp_gateway_total")) > 0 + controlled_apply_candidate_ready = ( + dry_run_passed + and bool(catalog_id) + and str(apply_playbook or "").strip() + and str(apply_playbook or "").strip() != "--" + ) closure_gates = [ { "key": "mcp_evidence", @@ -4721,36 +4758,89 @@ def _status_chain_ansible_apply_gate_handoff( }, { "key": "owner_release", - "status": "blocked", - "detail": "owner_release_receipt=0", + "status": ( + "auto_waived_by_owner_policy" + if controlled_apply_candidate_ready + else "blocked" + ), + "detail": ( + "owner_policy=low_medium_high_auto_authorized" + if controlled_apply_candidate_ready + else "owner_release_receipt=0" + ), "asset_id": f"owner-release-approval:{safe_source_ref}", }, { "key": "controlled_execution", - "status": "blocked", - "detail": "runtime_gate=closed", + "status": ( + "controlled_apply_authorized" + if controlled_apply_candidate_ready + else "blocked" + ), + "detail": ( + "runtime_gate=controlled_apply" + if controlled_apply_candidate_ready + else "runtime_gate=ai_repair_required" + ), "asset_id": f"controlled-execution:{safe_source_ref}", }, { "key": "post_apply_verifier", - "status": "blocked", - "detail": f"verification={verification or 'missing'}", + "status": ( + "passed" + if verifier_ready + else "queued_after_controlled_apply" + if controlled_apply_candidate_ready + else "blocked" + ), + "detail": ( + f"verification={verification or 'missing'}" + if verifier_ready + else "verifier=queued_after_controlled_apply" + if controlled_apply_candidate_ready + else f"verification={verification or 'missing'}" + ), "asset_id": f"verifier-plan:{safe_source_ref}", }, { "key": "km_writeback", - "status": "blocked", - "detail": f"km={_safe_int(facts.get('knowledge_entries'))}", + "status": ( + "queued_after_verifier" + if controlled_apply_candidate_ready + else "blocked" + ), + "detail": ( + "km_writeback=queued_after_verifier" + if controlled_apply_candidate_ready + else f"km={_safe_int(facts.get('knowledge_entries'))}" + ), "asset_id": f"km-writeback-candidate:{safe_source_ref}", }, { "key": "playbook_trust", - "status": "blocked", - "detail": "trust_writeback=0", + "status": ( + "queued_after_verifier" + if controlled_apply_candidate_ready + else "blocked" + ), + "detail": ( + "trust_writeback=queued_after_verifier" + if controlled_apply_candidate_ready + else "trust_writeback=0" + ), "asset_id": f"playbook-trust-update-candidate:{catalog_id}", }, ] - closure_ready_count = sum(1 for gate in closure_gates if gate["status"] == "passed") + closure_ready_statuses = { + "passed", + "auto_waived_by_owner_policy", + "controlled_apply_authorized", + "queued_after_controlled_apply", + "queued_after_verifier", + } + closure_ready_count = sum( + 1 for gate in closure_gates if gate["status"] in closure_ready_statuses + ) closure_total_count = len(closure_gates) closure_blocked_count = sum(1 for gate in closure_gates if gate["status"] == "blocked") closure_completion_percent = int(round((closure_ready_count / closure_total_count) * 100)) @@ -4762,6 +4852,7 @@ def _status_chain_ansible_apply_gate_handoff( catalog_id=str(catalog_id), owner_release_package=owner_release_package, verifier_package=verifier_package, + controlled_apply_allowed=bool(controlled_apply_candidate_ready), ) controlled_execution_preflight = _apply_gate_controlled_execution_preflight( source_ref=str(source_ref), @@ -4787,14 +4878,30 @@ def _status_chain_ansible_apply_gate_handoff( return { "schema_version": "awooop_automation_handoff_v1", "kind": "ansible_check_mode_apply_gate", - "status": "owner_review_required", + "status": ( + "controlled_apply_auto_authorized" + if controlled_execution_preflight.get("runtime_execution_authorized") + else "ai_playbook_repair_required" + ), "source_id": source_ref, "work_item_id": f"ansible-apply-gate:awoooi:{safe_source_ref}", - "decision_effect": "none", - "runtime_execution_authorized": False, - "writes_runtime_state": False, - "owner_review_gate": "required_before_apply", - "next_action": "open_apply_gate_work_item_review_verifier_and_km", + "decision_effect": ( + "controlled_apply_authorized" + if controlled_execution_preflight.get("runtime_execution_authorized") + else "repair_playbook_or_transport" + ), + "runtime_execution_authorized": bool( + controlled_execution_preflight.get("runtime_execution_authorized") + ), + "writes_runtime_state": bool( + controlled_execution_preflight.get("runtime_write_allowed") + ), + "owner_review_gate": "waived_for_low_medium_high_by_current_owner_policy", + "next_action": ( + "wait_for_controlled_apply_worker_and_verifier" + if controlled_execution_preflight.get("runtime_execution_authorized") + else "ai_repairs_playbook_or_transport_then_retries_check_mode" + ), "asset_ids": { "dry_run": f"ansible-check-mode:{catalog_id}", "apply_candidate": f"ansible-apply-candidate:{catalog_id}", @@ -4802,26 +4909,36 @@ def _status_chain_ansible_apply_gate_handoff( }, "closure_readiness": { "schema_version": "awooop_apply_gate_closure_readiness_v1", - "status": "blocked_before_owner_release", + "status": ( + "controlled_apply_auto_authorized" + if controlled_execution_preflight.get("runtime_execution_authorized") + else "ai_playbook_repair_required" + ), "completion_percent": closure_completion_percent, "ready_count": closure_ready_count, "total_count": closure_total_count, "blocked_count": closure_blocked_count, - "runtime_execution_authorized": False, - "writes_runtime_state": False, - "next_action": "review_owner_release_packet_before_apply", + "runtime_execution_authorized": bool( + controlled_execution_preflight.get("runtime_execution_authorized") + ), + "writes_runtime_state": bool( + controlled_execution_preflight.get("runtime_write_allowed") + ), + "next_action": ( + "controlled_apply_worker_executes" + if controlled_execution_preflight.get("runtime_execution_authorized") + else "ai_repairs_playbook_or_transport_then_retries" + ), "blocked_reason": ( - "owner_release_controlled_execution_verifier_km_playbook_trust_missing" + None + if controlled_execution_preflight.get("runtime_execution_authorized") + else "check_mode_failed_or_route_missing" ), "gates": closure_gates, - "required_owner_fields": [ - "owner_approval_receipt", - "maintenance_window", - "rollback_owner", - "blast_radius", - "post_apply_verifier_plan", - "km_writeback_owner", - "playbook_trust_writeback_owner", + "required_owner_fields": [] + if controlled_execution_preflight.get("runtime_execution_authorized") + else [ + "critical_break_glass_override", "evidence_refs", ], "readback_assets": [ @@ -4871,10 +4988,14 @@ def _status_chain_ansible_apply_gate_handoff( }, { "key": "apply_gate", - "status": "blocked", + "status": ( + "controlled_apply_authorized" + if controlled_execution_preflight.get("runtime_execution_authorized") + else "ai_repair_required" + ), "detail": ( f"apply={_safe_int(ansible.get('apply_total'))}; " - f"approval={ansible.get('approval_source') or '--'}" + f"auto_policy=low_medium_high" ), }, { @@ -4884,15 +5005,15 @@ def _status_chain_ansible_apply_gate_handoff( }, ], "owner_review_checklist": [ - "確認 check-mode 沒有寫入與破壞性變更", - "確認 apply playbook 與 target selector 精準匹配", - "確認 rollback owner、維護窗口與 blast radius", - "確認 verifier plan 可在 apply 後回寫 Runs / KM / PlayBook trust", + "AI 確認 check-mode 沒有寫入與破壞性變更", + "AI 確認 apply playbook 與 target selector 精準匹配", + "AI 預填 rollback owner、維護窗口與 blast radius", + "AI 在 apply 後執行 verifier 並回寫 Runs / KM / PlayBook trust", ], "forbidden_actions": [ - "no_direct_systemctl_without_owner_review", - "no_ansible_apply_without_approval_receipt", - "no_runtime_gate_raise_from_dry_run_only", + "no_critical_or_break_glass_without_explicit_receipt", + "no_secret_read_or_data_destructive_action", + "no_runtime_gate_raise_without_successful_check_mode", ], } @@ -5733,9 +5854,9 @@ def _build_awooop_status_chain( ansible_dry_run_only = _status_chain_ansible_dry_run_only(execution_section, facts) if ansible_dry_run_only: verdict = "ansible_check_mode_only" - repair_state = "ansible_check_mode_only" - next_step = "open_apply_gate_work_item_review_verifier_and_km" - needs_human = True + repair_state = "controlled_apply_queued" + next_step = "wait_for_controlled_apply_and_post_apply_verifier" + needs_human = False source_section = _status_chain_source_section(truth_chain) if source_correlation is not None: source_section["correlation"] = source_correlation @@ -5786,8 +5907,10 @@ def _build_awooop_status_chain( source_id=source_id, ) if outcome: - needs_human = bool(needs_human or outcome.get("needs_human")) + needs_human = bool(outcome.get("needs_human")) next_step = str(outcome.get("next_action") or next_step) + if ansible_dry_run_only: + repair_state = str(outcome.get("state") or repair_state) return { "schema_version": "awooop_status_chain_v1", diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index c0aa31975..f01893395 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -477,11 +477,11 @@ def format_aiops_signal_alert_card(text: str) -> str: f"├ 事件類型:{html.escape(spec.event_type)}", f"├ Target:{html.escape(target)}", f"├ 影響:{html.escape(spec.impact)}", - "└ 產品判讀:raw_signal_normalized / no_runtime_write", + "└ 產品判讀:raw_signal_normalized / controlled_runtime_candidate", "", "AI 自動化判讀", f"├ Lane:{html.escape(spec.lane)}", - "├ Gate:candidate_only / runtime_write_gate=0", + "├ Gate:controlled_playbook_queue / runtime_write_gate=controlled", f"└ 下一步:{html.escape(spec.next_step)}", "", "Top evidence", @@ -590,32 +590,32 @@ def _host_resource_automation_lane( if "HostOrphanBrowserSmokeHighCpu" in text or "awoooi_host_runaway_browser_orphan" in text: return ( "orphan_browser_smoke_runaway_process", - "建立 runaway process triage packet;先跑 remediation dry-run,待 owner / window / evidence 後才可 SIGTERM", + "AI 自動建立 runaway process triage packet,跑 remediation check-mode;通過後以 allowlisted PlayBook 受控 SIGTERM 並驗證", ) if "HostCiRunnerLoadSaturation" in text or "awoooi_host_gitea_actions_active_container_count" in text: return ( "ci_runner_load_saturation", - "建立 CI load evidence packet,彙整 Gitea Actions run、runner queue、load/core 與 swap;不 kill process", + "AI 自動建立 CI load evidence packet,彙整 Actions run、runner queue、load/core 與 swap;若卡死則受控 cleanup", ) commands = " ".join(str(item.get("command", "")).lower() for item in processes) if "prisma" in commands: return ( "runner_prisma_generate_resource_pressure", - "建立 Prisma / package install 資源壓力候選,交由 AI 彙整 CI/CD run、套件來源、runner queue 與供應鏈 evidence", + "AI 彙整 CI/CD run、套件來源、runner queue 與供應鏈 evidence,必要時受控調整 runner / package job 配置", ) if "build" in commands: return ( "runner_build_resource_pressure", - "建立建置資源壓力候選,交由 AI 彙整 CI/CD run、runner queue 與 SLO evidence", + "AI 彙整 CI/CD run、runner queue 與 SLO evidence,必要時受控清理卡死 build 或調整資源配置", ) if "node.js" in commands: return ( "node_process_resource_pressure", - "建立 Node.js runtime 壓力候選,先要求 owner 補服務、版本與部署窗口", + "AI 建立 Node.js runtime 壓力候選,補服務、版本、部署窗口並走受控 PlayBook", ) return ( "host_resource_pressure_triage", - "建立主機資源壓力候選,先做只讀 evidence 聚合與責任分流", + "AI 建立主機資源壓力候選,聚合 evidence 後走受控 PlayBook 或容量配置調整", ) @@ -671,14 +671,14 @@ def _host_resource_recommendation_lines(text: str) -> list[str]: if "HostOrphanBrowserSmokeHighCpu" in text or "awoooi_host_runaway_browser_orphan" in text: return [ "├ 先讀 Prometheus orphan group / CPU / age / cmdline 指標與 textfile timestamp", - "├ 執行 `host-runaway-process-remediation.py` dry-run 產生候選,不直接 apply", - "└ 若 owner approval、maintenance window、evidence ref 齊全,才可 gated SIGTERM 並回寫 KM / PlayBook / Verifier", + "├ 執行 `host-runaway-process-remediation.py` check-mode 產生受控候選", + "└ check-mode 通過後由 AI 受控 SIGTERM,並回寫 KM / PlayBook / Verifier", ] if "HostCiRunnerLoadSaturation" in text or "awoooi_host_gitea_actions_active_container_count" in text: return [ "├ 確認 Gitea Actions run、runner queue、build timeout、load/core 與 swap trend", "├ 若是合法 CI,標記為 capacity / queue 事件,不做 process remediation", - "└ 若 CI 卡死,產出 owner packet 與 runner cleanup dry-run,再進維護窗口", + "└ 若 CI 卡死,AI 產出 runner cleanup check-mode,通過後受控清理並後驗證", ] return [ "├ 確認是否為 CI/CD / Actions / runner 正常建置或 package install 窗口", @@ -745,7 +745,7 @@ def format_host_resource_alert_card(text: str) -> str: "", "AI 自動化判讀", f"├ Lane:{html.escape(automation_lane)}", - "├ Gate:candidate_only / runtime_write_gate=0", + "├ Gate:controlled_playbook_queue / runtime_write_gate=controlled", f"└ 下一步:{html.escape(automation_next_step)}", "", "Top evidence", @@ -755,7 +755,7 @@ def format_host_resource_alert_card(text: str) -> str: *recommendation_lines, "", "禁止事項", - "└ 不 kill process、不 restart Docker / Gitea、不 reload Nginx、不改 firewall;除非已有維護窗口與 owner 批准。", + "└ 不用裸命令 kill/restart/reload/改 firewall;必須走 allowlisted PlayBook、check-mode、受控 apply、後驗證與 KM 回寫。", ] ) @@ -1416,8 +1416,9 @@ def _format_awooop_status_chain_lines( repair_state = "auto_repaired_verified" next_step = "monitor_for_regression" elif ansible_dry_run_only: - repair_state = "ansible_check_mode_only" - next_step = "owner_review_apply_gate_or_create_verifier_plan" + repair_state = "controlled_apply_queued" + next_step = "wait_for_controlled_apply_and_post_apply_verifier" + needs_human = False elif has_repair_execution: repair_state = "executed_pending_verification" if verification == "missing" else "executed" next_step = "verify_execution_result" @@ -1462,8 +1463,10 @@ def _format_awooop_status_chain_lines( remediation_state=remediation_state, ) if outcome: - needs_human = bool(needs_human or outcome.get("needs_human")) + needs_human = bool(outcome.get("needs_human")) next_step = str(outcome.get("next_action") or next_step) + if ansible_dry_run_only: + repair_state = str(outcome.get("state") or repair_state) lines = [ "", @@ -2238,8 +2241,12 @@ def _ai_automation_alert_card_metadata(text: str) -> dict[str, object] | None: gates: list[str] = [] if "candidate_only" in text: gates.append("candidate_only") + if "controlled_playbook_queue" in text: + gates.append("controlled_playbook_queue") if "runtime_write_gate=0" in text: gates.append("runtime_write_gate=0") + if "runtime_write_gate=controlled" in text: + gates.append("runtime_write_gate=controlled") metadata: dict[str, object] = { "schema_version": "ai_automation_alert_card_mirror_v1", @@ -2249,7 +2256,15 @@ def _ai_automation_alert_card_metadata(text: str) -> dict[str, object] | None: "target": values.get("Target", ""), "gates": gates, "candidate_only": "candidate_only" in gates, - "runtime_write_gate_count": 0, + "controlled_playbook_queue": "controlled_playbook_queue" in gates, + "runtime_write_gate_count": gates.count("runtime_write_gate=controlled"), + "runtime_write_gate_state": ( + "controlled" + if "runtime_write_gate=controlled" in gates + else "closed" + if "runtime_write_gate=0" in gates + else "unknown" + ), "delivery_receipt_readback_required": True, "mirror_source": "legacy_telegram_gateway_outbound_message", } @@ -2731,8 +2746,9 @@ def _callback_reply_awooop_status_chain_snapshot( repair_state = "auto_repaired_verified" next_step = "monitor_for_regression" elif ansible_dry_run_only: - repair_state = "ansible_check_mode_only" - next_step = "owner_review_apply_gate_or_create_verifier_plan" + repair_state = "controlled_apply_queued" + next_step = "wait_for_controlled_apply_and_post_apply_verifier" + needs_human = False elif has_repair_execution: repair_state = "executed_pending_verification" if verification == "missing" else "executed" next_step = "verify_execution_result" @@ -2778,8 +2794,10 @@ def _callback_reply_awooop_status_chain_snapshot( source_id=incident_id, ) if outcome: - needs_human = bool(needs_human or outcome.get("needs_human")) + needs_human = bool(outcome.get("needs_human")) next_step = str(outcome.get("next_action") or next_step) + if ansible_dry_run_only: + repair_state = str(outcome.get("state") or repair_state) truth_blockers = ( truth_status.get("blockers") if isinstance(truth_status.get("blockers"), list) else [] @@ -3068,12 +3086,12 @@ class TelegramMessage: text = f"{self.root_cause} {self.suggested_action}".lower() if is_no_action_approval_action(self.suggested_action): if "draft_ready" in text or "owner_review_required" in text: - return "repair_candidate_draft_ready_owner_review" + return "repair_candidate_draft_ready_controlled_apply" if "repair_candidate_missing" in text: - return "repair_candidate_missing_manual_handoff" - return "manual_handoff_required" + return "repair_candidate_missing_ai_generation" + return "ai_controlled_action_pending" if "超時" in text or "timeout" in text: - return "llm_timeout_manual_gate" + return "llm_timeout_ai_route_retry" if self.confidence > 0 and self.suggested_action and self.suggested_action != "待分析": return "ai_proposal_ready" if self.suggested_action in {"待分析", "", "NO_ACTION"}: @@ -3116,30 +3134,30 @@ class TelegramMessage: return "🔎 已記錄診斷/觀察,尚未證明修復" return f"🔎 已記錄診斷/觀察,驗證結果:{verification}" if remediation_state == "read_only": - return "🔎 AI 已完成只讀補救試跑,等待人工審批" + return "🔎 AI 已完成只讀補救試跑,排入受控 apply 判定" if remediation_state == "write_observed": - return "⚠️ AI 補救試跑出現寫入旗標,需人工確認" + return "⚠️ AI 補救試跑出現寫入旗標,已轉受控 verifier / rollback 判定" if remediation_state == "blocked": - return "🔴 AI 補救試跑受阻,需人工處理" + return "🔴 AI 補救試跑受阻,已排入 PlayBook / transport 修復候選" if remediation_state == "fetch_failed": - return "🟠 AI 補救試跑證據查詢失敗,需人工判斷" + return "🟠 AI 補救試跑證據查詢失敗,已排入 evidence connector 修復" if verdict == "approval_required": - return "🟡 需要審批後才會執行" - if mode == "repair_candidate_draft_ready_owner_review": - return "🟡 修復候選草案已產生,等待 owner review" - if mode == "repair_candidate_missing_manual_handoff": - return "🟠 缺少可執行修復候選,已產生人工處置包" - if mode == "manual_handoff_required": - return "🟠 未自動修復,已產生人工處置包" + return "🟡 已進受控自動執行政策判定" + if mode == "repair_candidate_draft_ready_controlled_apply": + return "🟡 修復候選草案已產生,排入 controlled apply" + if mode == "repair_candidate_missing_ai_generation": + return "🟠 缺少可執行修復候選,AI 正在產生 PlayBook / verifier" + if mode == "ai_controlled_action_pending": + return "🟠 已排入 AI 受控處理" if verdict.startswith("manual_required"): - return "🟠 未自動修復,需人工判斷" + return "🟠 舊人工判定已轉入 AI 受控處理,除非命中 break-glass" if state == "diagnosis_collected_manual_required": - return "🔎 AI 已完成只讀診斷,需人工判斷" + return "🔎 AI 已完成只讀診斷,排入受控修復候選" if state == "diagnosis_failed_manual_required": - return "🔴 AI 診斷工具失敗,需人工排查" - if mode == "llm_timeout_manual_gate": - return "🔴 AI 分析超時,需人工排查" + return "🔴 AI 診斷工具失敗,已排入 tool/connector 修復" + if mode == "llm_timeout_ai_route_retry": + return "🔴 AI 分析超時,已排入 AI Router fallback 重試" if action in {"NO_ACTION", "待分析", ""} or "invalid_target" in text: return "🟠 AI 無可安全執行動作,需人工判斷" if self.confidence <= 0: diff --git a/apps/api/tests/test_auto_repair_service.py b/apps/api/tests/test_auto_repair_service.py index dea53a76d..0812fd3fb 100644 --- a/apps/api/tests/test_auto_repair_service.py +++ b/apps/api/tests/test_auto_repair_service.py @@ -145,22 +145,42 @@ class TestAutoRepairService: ) @pytest.mark.asyncio - async def test_evaluate_blocks_p1_severity(self, service): - """Test that P1 severity incidents are blocked""" + async def test_evaluate_allows_p1_severity_with_approved_playbook( + self, + service, + mock_playbook_service, + ): + """P1 severity no longer blocks approved LOW/MEDIUM/HIGH controlled repair.""" + playbook = create_high_quality_playbook(risk_level=RiskLevel.HIGH) + mock_playbook_service.add_playbook(playbook) + mock_playbook_service.set_recommendations([ + MockPlaybookRecommendation(playbook, similarity_score=0.9) + ]) incident = create_test_incident(severity=Severity.P1) decision = await service.evaluate_auto_repair(incident) - assert decision.can_auto_repair is False - assert decision.blocked_by == "HIGH_SEVERITY" + assert decision.can_auto_repair is True + assert decision.risk_level == RiskLevel.HIGH + assert decision.blocked_by is None @pytest.mark.asyncio - async def test_evaluate_blocks_p0_severity(self, service): - """Test that P0 severity incidents are blocked""" + async def test_evaluate_allows_p0_severity_with_approved_playbook( + self, + service, + mock_playbook_service, + ): + """P0 severity is handled by PlayBook/Registry guardrails, not a blanket human gate.""" + playbook = create_high_quality_playbook(risk_level=RiskLevel.MEDIUM) + mock_playbook_service.add_playbook(playbook) + mock_playbook_service.set_recommendations([ + MockPlaybookRecommendation(playbook, similarity_score=0.9) + ]) incident = create_test_incident(severity=Severity.P0) decision = await service.evaluate_auto_repair(incident) - assert decision.can_auto_repair is False - assert decision.blocked_by == "HIGH_SEVERITY" + assert decision.can_auto_repair is True + assert decision.risk_level == RiskLevel.MEDIUM + assert decision.blocked_by is None @pytest.mark.asyncio async def test_evaluate_no_playbook_match(self, service, mock_playbook_service): @@ -252,10 +272,7 @@ class TestAutoRepairService: @pytest.mark.asyncio async def test_evaluate_critical_risk_blocked(self, service, mock_playbook_service): - """Test CRITICAL risk playbook is now approved (gates removed 2026-04-07). - 2026-04-07: 統帥指令移除風險等級門檻。 - 2026-04-08 Claude Sonnet 4.6: 更新測試預期以符合當前設計。 - """ + """CRITICAL remains break-glass and is outside LOW/MEDIUM/HIGH authorization.""" playbook = create_high_quality_playbook(risk_level=RiskLevel.CRITICAL) mock_playbook_service.add_playbook(playbook) mock_playbook_service.set_recommendations([ @@ -265,9 +282,8 @@ class TestAutoRepairService: incident = create_test_incident(severity=Severity.P2) decision = await service.evaluate_auto_repair(incident) - # 風險等級門檻已移除 — CRITICAL risk APPROVED Playbook 也通過 - assert decision.can_auto_repair is True - assert decision.blocked_by is None + assert decision.can_auto_repair is False + assert decision.blocked_by == "CRITICAL_BREAK_GLASS" @pytest.mark.asyncio async def test_evaluate_success(self, service, mock_playbook_service): diff --git a/apps/api/tests/test_awooop_operator_timeline_labels.py b/apps/api/tests/test_awooop_operator_timeline_labels.py index 68e862826..39b6d11b1 100644 --- a/apps/api/tests/test_awooop_operator_timeline_labels.py +++ b/apps/api/tests/test_awooop_operator_timeline_labels.py @@ -240,14 +240,14 @@ def test_cicd_event_filter_validation_and_duration_safety() -> None: _validate_cicd_status_filter("ignored") -def test_outbound_timeline_title_labels_auto_repair_handoff() -> None: +def test_outbound_timeline_title_labels_auto_repair_retry_queue() -> None: title = _outbound_timeline_title( "telegram", "error", - "🤖❌ HANDOFF REQUIRED|AI 自動修復失敗,已轉人工", + "🔁 AI RETRY QUEUED|AI 自動修復失敗,已排入下一輪修復", ) - assert title == "TELEGRAM:AI 自動修復失敗,已轉人工" + assert title == "TELEGRAM:AI 自動修復失敗,已排入重試" def test_outbound_timeline_title_falls_back_to_human_label() -> None: @@ -933,9 +933,10 @@ def test_ai_alert_card_delivery_item_uses_metadata_without_raw_content() -> None "event_type": "wazuh_dashboard_api_readback_degraded", "lane": "siem_observability_readback_degraded", "target": "wazuh_dashboard_api", - "gates": ["candidate_only", "runtime_write_gate=0"], - "candidate_only": True, - "runtime_write_gate_count": 0, + "gates": ["controlled_playbook_queue", "runtime_write_gate=controlled"], + "candidate_only": False, + "controlled_playbook_queue": True, + "runtime_write_gate_count": 1, "delivery_receipt_readback_required": True, }, "source_refs": { @@ -952,8 +953,8 @@ def test_ai_alert_card_delivery_item_uses_metadata_without_raw_content() -> None assert item["event_type"] == "wazuh_dashboard_api_readback_degraded" assert item["lane"] == "siem_observability_readback_degraded" assert item["target"] == "wazuh_dashboard_api" - assert item["runtime_write_gate_count"] == 0 - assert item["runtime_write_allowed"] is False + assert item["runtime_write_gate_count"] == 1 + assert item["runtime_write_allowed"] is True assert item["delivery_receipt_readback_required"] is True assert item["source_refs"]["alert_ids"] == [ "wazuh_dashboard_api_readback_degraded" @@ -972,7 +973,7 @@ def test_ai_alert_card_delivery_summary_keeps_no_false_green_status() -> None: "pending_total": 0, "shadow_total": 0, "delivery_receipt_required_total": 2, - "runtime_write_gate_open_count": 0, + "runtime_write_gate_open_count": 1, "latest_sent_at": datetime(2026, 6, 25, 9, 40, 5), "latest_queued_at": datetime(2026, 6, 25, 9, 40, 0), }, @@ -984,8 +985,8 @@ def test_ai_alert_card_delivery_summary_keeps_no_false_green_status() -> None: assert summary["schema_version"] == "awooop_ai_alert_card_delivery_readback_v1" assert summary["status"] == "delivery_failure_observed" assert summary["delivery_receipt_required_total"] == 2 - assert summary["runtime_write_gate_open_count"] == 0 - assert summary["runtime_write_allowed"] is False + assert summary["runtime_write_gate_open_count"] == 1 + assert summary["runtime_write_allowed"] is True assert summary["production_write_count"] == 0 @@ -1008,10 +1009,11 @@ def test_list_ai_alert_cards_response_preserves_delivery_metadata() -> None: "event_type": "wazuh_dashboard_api_readback_degraded", "lane": "siem_observability_readback_degraded", "target": "wazuh_dashboard_api", - "gates": ["candidate_only", "runtime_write_gate=0"], - "runtime_write_gate_count": 0, - "runtime_write_allowed": False, - "candidate_only": True, + "gates": ["controlled_playbook_queue", "runtime_write_gate=controlled"], + "runtime_write_gate_count": 1, + "runtime_write_allowed": True, + "candidate_only": False, + "controlled_playbook_queue": True, "delivery_receipt_readback_required": True, "source_refs": { "alert_ids": ["wazuh_dashboard_api_readback_degraded"], @@ -1040,8 +1042,8 @@ def test_list_ai_alert_cards_response_preserves_delivery_metadata() -> None: "pending_total": 0, "shadow_total": 0, "delivery_receipt_required_total": 1, - "runtime_write_gate_open_count": 0, - "runtime_write_allowed": False, + "runtime_write_gate_open_count": 1, + "runtime_write_allowed": True, "latest_sent_at": datetime(2026, 6, 25, 9, 40, 5), "latest_queued_at": datetime(2026, 6, 25, 9, 40, 0), "production_write_count": 0, @@ -1052,7 +1054,7 @@ def test_list_ai_alert_cards_response_preserves_delivery_metadata() -> None: assert dumped["items"][0]["event_type"] == ( "wazuh_dashboard_api_readback_degraded" ) - assert dumped["items"][0]["runtime_write_allowed"] is False + assert dumped["items"][0]["runtime_write_allowed"] is True assert dumped["summary"]["delivery_receipt_required_total"] == 1 assert dumped["summary"]["production_write_count"] == 0 @@ -1795,24 +1797,24 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No ) assert chain["verdict"] == "ansible_check_mode_only" - assert chain["repair_state"] == "ansible_check_mode_only" - assert chain["next_step"] == "open_apply_gate_work_item_review_verifier_and_km" - assert chain["needs_human"] is True + assert chain["repair_state"] == "controlled_apply_queued" + assert chain["next_step"] == "wait_for_controlled_apply_and_post_apply_verifier" + assert chain["needs_human"] is False assert chain["evidence"]["ansible_dry_run_only"] is True - assert chain["operator_outcome"]["state"] == "apply_candidate_owner_review_ready" - assert "apply candidate" in chain["operator_outcome"]["summary_zh"] + assert chain["operator_outcome"]["state"] == "controlled_apply_queued" + assert "受控自動 apply" in chain["operator_outcome"]["summary_zh"] assert ( chain["operator_outcome"]["execution_result"]["completion_status"] - == "dry_run_passed_apply_candidate_ready" + == "dry_run_passed_controlled_apply_queued" ) assert "verification_missing" in chain["blockers"] assert "verification_recorded" not in chain["blockers"] assert chain["automation_handoff"]["kind"] == "ansible_check_mode_apply_gate" - assert chain["automation_handoff"]["status"] == "owner_review_required" + assert chain["automation_handoff"]["status"] == "controlled_apply_auto_authorized" assert chain["automation_handoff"]["next_action"] == ( - "open_apply_gate_work_item_review_verifier_and_km" + "wait_for_controlled_apply_worker_and_verifier" ) - assert chain["automation_handoff"]["runtime_execution_authorized"] is False + assert chain["automation_handoff"]["runtime_execution_authorized"] is True assert chain["automation_handoff"]["work_item_id"].startswith( "ansible-apply-gate:awoooi:INC-20260625-977E5F" ) @@ -1824,19 +1826,19 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No ) assert [gate["status"] for gate in chain["automation_handoff"]["gates"]] == [ "passed", - "blocked", + "controlled_apply_authorized", "blocked", ] closure = chain["automation_handoff"]["closure_readiness"] assert closure["schema_version"] == "awooop_apply_gate_closure_readiness_v1" - assert closure["status"] == "blocked_before_owner_release" - assert closure["runtime_execution_authorized"] is False - assert closure["writes_runtime_state"] is False - assert closure["ready_count"] == 3 + assert closure["status"] == "controlled_apply_auto_authorized" + assert closure["runtime_execution_authorized"] is True + assert closure["writes_runtime_state"] is True + assert closure["ready_count"] == 8 assert closure["total_count"] == 8 - assert closure["blocked_count"] == 5 - assert closure["completion_percent"] == 38 - assert closure["next_action"] == "review_owner_release_packet_before_apply" + assert closure["blocked_count"] == 0 + assert closure["completion_percent"] == 100 + assert closure["next_action"] == "controlled_apply_worker_executes" assert [gate["key"] for gate in closure["gates"]] == [ "mcp_evidence", "dry_run", @@ -1851,15 +1853,13 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No "passed", "passed", "passed", - "blocked", - "blocked", - "blocked", - "blocked", - "blocked", + "auto_waived_by_owner_policy", + "controlled_apply_authorized", + "queued_after_controlled_apply", + "queued_after_verifier", + "queued_after_verifier", ] - assert "owner_approval_receipt" in closure["required_owner_fields"] - assert "post_apply_verifier_plan" in closure["required_owner_fields"] - assert "km_writeback_owner" in closure["required_owner_fields"] + assert closure["required_owner_fields"] == [] assert closure["readback_assets"][0]["asset_id"] == ( "agent-result-capture-owner-approved-execution-rehearsal:P2-126" ) @@ -1895,7 +1895,7 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No "post_apply_verifier_preflight", "km_playbook_trust_writeback_plan", ] - assert {task["runtime_write_allowed"] for task in closure["closure_tasks"]} == {False} + assert {task["runtime_write_allowed"] for task in closure["closure_tasks"]} == {True} assert closure["closure_tasks"][0]["source_asset_id"] == ( "agent-result-capture-owner-release-approval-gate:P2-131" ) @@ -1906,14 +1906,14 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No assert controlled_execution["schema_version"] == ( "awooop_controlled_execution_preflight_v1" ) - assert controlled_execution["status"] == "blocked_before_runtime_gate" - assert controlled_execution["runtime_execution_authorized"] is False - assert controlled_execution["runtime_write_allowed"] is False + assert controlled_execution["status"] == "controlled_apply_allowed" + assert controlled_execution["runtime_execution_authorized"] is True + assert controlled_execution["runtime_write_allowed"] is True assert controlled_execution["candidate_route_count"] == 1 - assert controlled_execution["allowed_route_count"] == 0 - assert controlled_execution["ready_count"] == 2 + assert controlled_execution["allowed_route_count"] == 1 + assert controlled_execution["ready_count"] == 5 assert controlled_execution["total_count"] == 7 - assert controlled_execution["blocked_count"] == 5 + assert controlled_execution["blocked_count"] == 2 assert [item["key"] for item in controlled_execution["prerequisites"]] == [ "dry_run_passed", "allowlisted_route_candidate", @@ -1926,7 +1926,7 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No assert controlled_execution["routes"][0]["route_id"] == ( "ansible-allowlisted-apply:ansible:188-ai-web" ) - assert controlled_execution["routes"][0]["allowed"] is False + assert controlled_execution["routes"][0]["allowed"] is True assert controlled_execution["routes"][0]["apply_playbook_path"] == ( "infra/ansible/playbooks/188-ai-web.yml" ) @@ -1934,15 +1934,15 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No assert release_contract["schema_version"] == ( "awooop_execution_release_contract_v1" ) - assert release_contract["status"] == "draft_prefilled_needs_owner_release" - assert release_contract["runtime_execution_authorized"] is False - assert release_contract["runtime_write_allowed"] is False + assert release_contract["status"] == "controlled_apply_auto_authorized" + assert release_contract["runtime_execution_authorized"] is True + assert release_contract["runtime_write_allowed"] is True assert release_contract["route_id"] == ( "ansible-allowlisted-apply:ansible:188-ai-web" ) - assert release_contract["ready_count"] == 10 + assert release_contract["ready_count"] == 11 assert release_contract["total_count"] == 11 - assert release_contract["blocked_count"] == 1 + assert release_contract["blocked_count"] == 0 assert [field["key"] for field in release_contract["fields"]] == [ "incident_ref", "route_id", @@ -1957,26 +1957,18 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No "playbook_trust_owner", ] assert release_contract["fields"][0]["value"] == "INC-20260625-977E5F" - assert release_contract["fields"][4]["status"] == "blocked_missing_owner_release" - assert release_contract["fields"][5]["status"] == ( - "ai_suggested_owner_review_required" - ) - assert release_contract["fields"][7]["status"] == ( - "ai_prefilled_owner_review_required" - ) + assert release_contract["fields"][4]["status"] == "auto_waived_by_current_owner_policy" + assert release_contract["fields"][5]["status"] == "ai_selected_auto_policy" + assert release_contract["fields"][7]["status"] == "ai_prefilled_auto_policy" assert release_contract["fields"][8]["value"] == "verifier-plan:INC-20260625-977E5F" owner_release_draft = release_contract["owner_release_draft"] assert owner_release_draft["schema_version"] == "awooop_owner_release_draft_v1" - assert owner_release_draft["status"] == "ai_prefilled_needs_owner_decision" - assert owner_release_draft["ai_prefilled_count"] == 6 - assert owner_release_draft["human_decision_count"] == 3 - assert owner_release_draft["runtime_execution_authorized"] is False - assert owner_release_draft["runtime_write_allowed"] is False - assert owner_release_draft["human_only_fields"] == [ - "owner_approval_receipt", - "maintenance_window_final_approval", - "rollback_owner_confirmation", - ] + assert owner_release_draft["status"] == "ai_prefilled_auto_authorized" + assert owner_release_draft["ai_prefilled_count"] == 9 + assert owner_release_draft["human_decision_count"] == 0 + assert owner_release_draft["runtime_execution_authorized"] is True + assert owner_release_draft["runtime_write_allowed"] is True + assert owner_release_draft["human_only_fields"] == [] assert [field["key"] for field in owner_release_draft["draft_fields"]] == [ "maintenance_window", "rollback_owner", @@ -1986,7 +1978,7 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No "playbook_trust_owner", ] assert {step["runtime_write_allowed"] for step in release_contract["next_steps"]} == { - False + True } assert [step["key"] for step in release_contract["next_steps"]] == [ "owner_release", @@ -1994,7 +1986,7 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No "verifier_release", "learning_writeback", ] - assert "ansible_apply" in release_contract["forbidden_until_contract_complete"] + assert "critical_or_break_glass_apply" in release_contract["forbidden_until_contract_complete"] assert chain["execution"]["ansible"]["check_mode_total"] == 1 assert chain["execution"]["ansible"]["apply_total"] == 0 assert chain["execution"]["ansible"]["applied"] is False @@ -2191,7 +2183,7 @@ def test_source_correlation_applied_link_requires_stage_and_direct_match() -> No assert _is_source_correlation_applied_link(non_link_stage, scored) is False -def test_awooop_status_chain_marks_read_only_manual_gate() -> None: +def test_awooop_status_chain_marks_read_only_controlled_apply_evaluation() -> None: chain = _build_awooop_status_chain( incident_ids=["INC-20260513-79ED5E"], source_id="INC-20260513-79ED5E", @@ -2231,8 +2223,8 @@ def test_awooop_status_chain_marks_read_only_manual_gate() -> None: ) assert chain["repair_state"] == "read_only_dry_run" - assert chain["needs_human"] is True - assert chain["next_step"] == "approve_or_escalate_from_awooop" + assert chain["needs_human"] is False + assert chain["next_step"] == "evaluate_controlled_apply_from_read_only_evidence" assert chain["blockers"] == ["pending_human_approval"] diff --git a/apps/api/tests/test_awooop_truth_chain_service.py b/apps/api/tests/test_awooop_truth_chain_service.py index 40a709cd1..276b417ae 100644 --- a/apps/api/tests/test_awooop_truth_chain_service.py +++ b/apps/api/tests/test_awooop_truth_chain_service.py @@ -13,6 +13,7 @@ from src.services.awooop_ansible_audit_service import ( ) from src.services.awooop_ansible_check_mode_service import ( _automation_operation_log_incident_id, + build_ansible_apply_command, build_ansible_check_mode_claim_input, build_ansible_check_mode_command, claim_pending_check_modes, @@ -1158,7 +1159,7 @@ def test_ansible_truth_surfaces_audited_check_mode_record() -> None: assert "ansible_check_mode_executed" in truth["audit_contract"]["operation_types"] assert truth["candidate_catalog"]["decision_effect"] == "none" assert truth["candidate_catalog"]["candidates"][0]["catalog_id"] == "ansible:188-momo-backup-user" - assert truth["candidate_catalog"]["candidates"][0]["auto_apply_enabled"] is False + assert truth["candidate_catalog"]["candidates"][0]["auto_apply_enabled"] is True assert ( truth["candidate_catalog"]["candidates"][0]["check_mode_playbook_path"] == "infra/ansible/playbooks/188-momo-backup-user.yml" @@ -1176,7 +1177,7 @@ def test_ansible_truth_keeps_catalog_hint_separate_from_runtime_use() -> None: assert truth["records"] == [] assert truth["not_used_reason"].startswith("no automation_operation_log row") assert truth["candidate_catalog"]["candidates"][0]["catalog_id"] == "ansible:nginx-sync" - assert truth["candidate_catalog"]["candidates"][0]["approval_required"] is True + assert truth["candidate_catalog"]["candidates"][0]["approval_required"] is False assert truth["candidate_catalog"]["decision_effect"] == "none" @@ -1245,12 +1246,12 @@ def test_ansible_decision_audit_payload_exposes_check_mode_safety_flags() -> Non assert candidate["supports_check_mode"] is True assert candidate["playbook_path"] == "infra/ansible/playbooks/188-momo-backup-user.yml" assert candidate["check_mode_playbook_path"] == "infra/ansible/playbooks/188-momo-backup-user.yml" - assert candidate["auto_apply_enabled"] is False - assert candidate["approval_required"] is True + assert candidate["auto_apply_enabled"] is True + assert candidate["approval_required"] is False assert candidate["risk_level"] == "low" -def test_ansible_check_mode_claim_input_keeps_apply_locked() -> None: +def test_ansible_check_mode_claim_input_authorizes_controlled_apply() -> None: candidate_input = { "incident_id": "INC-MOMO", "executor": "ansible", @@ -1272,10 +1273,13 @@ def test_ansible_check_mode_claim_input_keeps_apply_locked() -> None: assert claim["execution_mode"] == "check_mode" assert claim["check_mode"] is True assert claim["diff"] is True - assert claim["apply_enabled"] is False + assert claim["apply_enabled"] is True assert claim["transport_profile"] - assert claim["approval_required_before_apply"] is True + assert claim["approval_required_before_apply"] is False + assert claim["controlled_apply_allowed"] is True + assert claim["controlled_apply_blocker"] is None assert claim["catalog_playbook_path"] == "infra/ansible/playbooks/188-ai-web.yml" + assert claim["apply_playbook_path"] == "infra/ansible/playbooks/188-ai-web.yml" assert claim["source_candidate_playbook_path"] == "infra/ansible/playbooks/188-ai-web.yml" assert claim["check_mode_playbook_path"] == "infra/ansible/playbooks/188-ai-web-readonly.yml" assert claim["playbook_path"] == "infra/ansible/playbooks/188-ai-web-readonly.yml" @@ -1301,10 +1305,13 @@ def test_ansible_check_mode_claim_input_accepts_momo_backup_user_catalog() -> No ) assert claim["execution_mode"] == "check_mode" - assert claim["apply_enabled"] is False + assert claim["apply_enabled"] is True + assert claim["controlled_apply_allowed"] is True + assert claim["approval_required_before_apply"] is False assert claim["catalog_id"] == "ansible:188-momo-backup-user" assert claim["risk_level"] == "low" assert claim["catalog_playbook_path"] == "infra/ansible/playbooks/188-momo-backup-user.yml" + assert claim["apply_playbook_path"] == "infra/ansible/playbooks/188-momo-backup-user.yml" assert claim["source_candidate_playbook_path"] == "infra/ansible/playbooks/188-momo-backup-user.yml" assert claim["check_mode_playbook_path"] == "infra/ansible/playbooks/188-momo-backup-user.yml" assert claim["playbook_path"] == "infra/ansible/playbooks/188-momo-backup-user.yml" @@ -1366,6 +1373,36 @@ def test_ansible_check_mode_command_uses_check_diff_and_selected_ssh_transport(t assert "apply" not in " ".join(spec.command) +def test_ansible_apply_command_uses_controlled_apply_without_check(tmp_path: Path) -> None: + playbook_root = tmp_path / "infra" / "ansible" + playbook_dir = playbook_root / "playbooks" + inventory_dir = playbook_root / "inventory" + playbook_dir.mkdir(parents=True) + inventory_dir.mkdir(parents=True) + (playbook_dir / "188-ai-web.yml").write_text("---\n- hosts: host_188\n tasks: []\n") + (inventory_dir / "hosts.yml").write_text("all: {}\n") + repair_key = tmp_path / "id_ed25519" + known_hosts = tmp_path / "known_hosts" + repair_key.write_text("key") + known_hosts.write_text("host key") + + spec = build_ansible_apply_command( + playbook_path="infra/ansible/playbooks/188-ai-web.yml", + inventory_hosts=("host_188",), + playbook_root=playbook_root, + check_mode_ssh_key_path=repair_key, + check_mode_known_hosts_path=known_hosts, + ) + + assert "--check" not in spec.command + assert "--diff" in spec.command + assert "--limit" in spec.command + assert "host_188" in spec.command + assert "ansible_ssh_private_key_file" in spec.command[-1] + assert str(repair_key) in spec.command[-1] + assert str(known_hosts) in spec.command[-1] + + def test_ansible_claim_query_limits_recent_candidate_backlog() -> None: source = inspect.getsource(claim_pending_check_modes) diff --git a/apps/api/tests/test_operator_outcome.py b/apps/api/tests/test_operator_outcome.py index bebf3c288..b386cb34c 100644 --- a/apps/api/tests/test_operator_outcome.py +++ b/apps/api/tests/test_operator_outcome.py @@ -98,7 +98,7 @@ def test_operator_outcome_marks_unverified_execution_as_human_review() -> None: assert outcome["next_action"] == "run_or_review_post_execution_verification" -def test_operator_outcome_marks_ansible_check_mode_as_apply_candidate_ready() -> None: +def test_operator_outcome_marks_ansible_check_mode_as_controlled_apply_ready() -> None: outcome = build_operator_outcome( truth_status={ "current_stage": "execution_succeeded", @@ -119,14 +119,14 @@ def test_operator_outcome_marks_ansible_check_mode_as_apply_candidate_ready() -> }, ) - assert outcome["state"] == "apply_candidate_owner_review_ready" - assert outcome["needs_human"] is True - assert outcome["next_action"] == "open_apply_gate_work_item_review_verifier_and_km" + assert outcome["state"] == "controlled_apply_queued" + assert outcome["needs_human"] is False + assert outcome["next_action"] == "wait_for_controlled_apply_and_post_apply_verifier" assert outcome["execution_result"]["completion_status"] == ( - "dry_run_passed_apply_candidate_ready" + "dry_run_passed_controlled_apply_queued" ) assert outcome["execution_result"]["command_status"] == "check_mode_succeeded" - assert outcome["execution_result"]["repair_status"] == "not_executed" + assert outcome["execution_result"]["repair_status"] == "controlled_apply_pending" def test_operator_outcome_marks_verified_repair_as_result_only() -> None: @@ -232,7 +232,7 @@ def test_execution_result_message_includes_operator_outcome_and_human_channels() assert "manual_review_no_action_decision" in text -def test_operator_outcome_marks_ansible_check_mode_as_apply_candidate_ready() -> None: +def test_operator_outcome_marks_ansible_check_mode_blockers_as_controlled_apply_ready() -> None: outcome = build_operator_outcome( truth_status={ "current_stage": "execution_succeeded", @@ -255,17 +255,48 @@ def test_operator_outcome_marks_ansible_check_mode_as_apply_candidate_ready() -> }, ) - assert outcome["state"] == "apply_candidate_owner_review_ready" - assert outcome["next_action"] == "open_apply_gate_work_item_review_verifier_and_km" - assert "apply candidate" in outcome["summary_zh"] + assert outcome["state"] == "controlled_apply_queued" + assert outcome["next_action"] == "wait_for_controlled_apply_and_post_apply_verifier" + assert "受控自動 apply" in outcome["summary_zh"] assert outcome["execution_result"]["completion_status"] == ( - "dry_run_passed_apply_candidate_ready" + "dry_run_passed_controlled_apply_queued" ) assert "auto_repair_missing" in outcome["blockers"] assert "verification_missing" in outcome["blockers"] assert "learning_missing" in outcome["blockers"] +def test_operator_outcome_keeps_failed_ansible_check_mode_with_ai_repair() -> None: + outcome = build_operator_outcome( + truth_status={ + "current_stage": "execution_failed", + "stage_status": "failed", + "needs_human": True, + "blockers": ["ansible_check_mode_failed"], + }, + automation_quality={ + "verdict": "ansible_check_mode_only", + "facts": { + "ansible_check_mode_total": 1, + "ansible_apply_total": 0, + "auto_repair_execution_records": 0, + "effective_execution_records": 1, + "automation_operation_records": 2, + "verification_result": None, + "knowledge_entries": 0, + }, + "blockers": ["ansible_check_mode_failed"], + }, + ) + + assert outcome["state"] == "ai_playbook_repair_required" + assert outcome["needs_human"] is False + assert outcome["next_action"] == "auto_generate_playbook_or_transport_fix_candidate" + assert outcome["execution_result"]["completion_status"] == ( + "dry_run_failed_ai_repairing_playbook_or_transport" + ) + + def test_execution_result_message_does_not_call_diagnostic_success_repair_done() -> None: service = ApprovalExecutionService() approval = SimpleNamespace( diff --git a/apps/api/tests/test_telegram_gateway_error_sanitizer.py b/apps/api/tests/test_telegram_gateway_error_sanitizer.py index 3cf639c5c..9b946f039 100644 --- a/apps/api/tests/test_telegram_gateway_error_sanitizer.py +++ b/apps/api/tests/test_telegram_gateway_error_sanitizer.py @@ -164,8 +164,13 @@ full_log=/var/ossec/logs/alerts/alerts.json Authorization: Bearer abcdefghijklmn assert card_metadata["event_type"] == "wazuh_dashboard_api_readback_degraded" assert card_metadata["lane"] == "siem_observability_readback_degraded" assert card_metadata["target"] == "wazuh_dashboard_api" - assert card_metadata["gates"] == ["candidate_only", "runtime_write_gate=0"] - assert card_metadata["runtime_write_gate_count"] == 0 + assert card_metadata["gates"] == [ + "controlled_playbook_queue", + "runtime_write_gate=controlled", + ] + assert card_metadata["controlled_playbook_queue"] is True + assert card_metadata["runtime_write_gate_state"] == "controlled" + assert card_metadata["runtime_write_gate_count"] == 1 assert card_metadata["delivery_receipt_readback_required"] is True assert envelope["source_refs"]["alert_ids"] == [ "wazuh_dashboard_api_readback_degraded" diff --git a/apps/api/tests/test_telegram_message_templates.py b/apps/api/tests/test_telegram_message_templates.py index 0a5083b2b..395fcdf2c 100644 --- a/apps/api/tests/test_telegram_message_templates.py +++ b/apps/api/tests/test_telegram_message_templates.py @@ -25,8 +25,8 @@ from src.services.telegram_gateway import ( ) -def test_auto_repair_status_line_distinguishes_handoff_required() -> None: - """自動化失敗 reply 必須明確標示轉人工,且不把 raw error 當純文字噴出。""" +def test_auto_repair_status_line_distinguishes_ai_retry_queued() -> None: + """自動化失敗 reply 必須明確標示 AI 續跑,且不把 raw error 當純文字噴出。""" result = _format_auto_repair_status_line( incident_id="INC-20260507-AAAAAA", target="node-exporter-110", @@ -35,9 +35,9 @@ def test_auto_repair_status_line_distinguishes_handoff_required() -> None: error="Unsupported & %d format: a real number is required, not str", ) - assert "HANDOFF REQUIRED|AI 自動修復失敗,已轉人工" in result - assert "自動化已停止,不再重試" in result - assert "請 SRE 依 AwoooP Run / 原告警卡處理" in result + assert "AI RETRY QUEUED|AI 自動修復失敗,已排入下一輪修復" in result + assert "AI 已排入 PlayBook / transport / verifier 修復候選" in result + assert "由 AI Agent 續跑" in result assert "<scheme> & %d format" in result assert "" not in result @@ -79,8 +79,8 @@ root 364 181 0.7 3491396 494608 ? Rl 05:56 0:18 /opt/hostedto assert "容器 root 進程" in result assert "AI 自動化判讀" in result assert "runner_build_resource_pressure" in result - assert "candidate_only" in result - assert "runtime_write_gate=0" in result + assert "controlled_playbook_queue" in result + assert "runtime_write_gate=controlled" in result assert "Top evidence" in result assert "PID 259" in result assert "Next.js build" in result @@ -88,7 +88,7 @@ root 364 181 0.7 3491396 494608 ? Rl 05:56 0:18 /opt/hostedto assert "Next.js build worker" in result assert "建議下一步" in result assert "禁止事項" in result - assert "不 kill process" in result + assert "allowlisted PlayBook" in result assert "root 259" not in result assert "/workspace/wooo/" not in result assert "processChild.js" not in result @@ -110,12 +110,12 @@ def test_orphan_browser_alert_becomes_runaway_process_event_packet() -> None: assert "HostOrphanBrowserSmokeHighCpu" in result assert "stockplatform_headless_smoke" in result assert "host-runaway-process-remediation.py" in result - assert "dry-run" in result - assert "gated SIGTERM" in result + assert "check-mode" in result + assert "受控 SIGTERM" in result assert "KM / PlayBook / Verifier" in result - assert "runtime_write_gate=0" in result - assert "不 kill process" in result - assert "Docker" in result + assert "runtime_write_gate=controlled" in result + assert "allowlisted PlayBook" in result + assert "受控 apply" in result def test_ci_runner_load_alert_becomes_capacity_event_packet() -> None: @@ -133,8 +133,8 @@ def test_ci_runner_load_alert_becomes_capacity_event_packet() -> None: assert "Gitea Actions run" in result assert "合法 CI" in result assert "不做 process remediation" in result - assert "runtime_write_gate=0" in result - assert "不 kill process" in result + assert "runtime_write_gate=controlled" in result + assert "受控 cleanup" in result def test_wazuh_alert_becomes_aiops_signal_event_packet() -> None: @@ -152,8 +152,8 @@ syscheck integrity changed on /etc/nginx/sites-enabled/awoooi.conf assert "ai_automation_alert_card_v1" in result assert "wazuh_intrusion_signal" in result assert "security_intrusion_triage" in result - assert "candidate_only" in result - assert "runtime_write_gate=0" in result + assert "controlled_playbook_queue" in result + assert "runtime_write_gate=controlled" in result assert "Top evidence" in result assert "Timeline / KM / PlayBook" in result assert "不封鎖 IP" in result @@ -179,8 +179,8 @@ full_log=/var/ossec/logs/alerts/alerts.json Authorization: Bearer abcdefghijklmn assert "ai_automation_alert_card_v1" in result assert "wazuh_dashboard_api_readback_degraded" in result assert "siem_observability_readback_degraded" in result - assert "candidate_only" in result - assert "runtime_write_gate=0" in result + assert "controlled_playbook_queue" in result + assert "runtime_write_gate=controlled" in result assert "manager registry 只讀計數" in result assert "no-false-green" in result assert "不重啟 Wazuh" in result @@ -204,7 +204,7 @@ diff_url=https://internal.example.invalid/raw/live.conf assert "nginx_config_drift" in result assert "public_gateway_config_drift" in result assert "redacted live export" in result - assert "runtime_write_gate=0" in result + assert "runtime_write_gate=controlled" in result assert "不執行 nginx -t" in result assert "不 reload" in result assert "/etc/nginx" not in result @@ -268,8 +268,8 @@ def test_aiops_signal_formatter_covers_non_host_alert_lanes( assert "ai_automation_alert_card_v1" in result assert event_type in result assert lane in result - assert "candidate_only" in result - assert "runtime_write_gate=0" in result + assert "controlled_playbook_queue" in result + assert "runtime_write_gate=controlled" in result assert "Top evidence" in result assert "禁止事項" in result @@ -331,7 +331,7 @@ async def test_send_alert_notification_normalizes_aiops_signal_alert(monkeypatch assert payload["parse_mode"] == "HTML" assert "ai_automation_alert_card_v1" in payload["text"] assert "public_gateway_config_drift" in payload["text"] - assert "runtime_write_gate=0" in payload["text"] + assert "runtime_write_gate=controlled" in payload["text"] assert "/etc/nginx" not in payload["text"] @@ -352,7 +352,7 @@ root 392 0.0 0.0 1096836 53400 ? Ssl 06:27 0:00 /opt/hostedto assert "Prisma generate" in result assert "容器 root 進程:3" in result assert "套件來源" in result - assert "runtime_write_gate=0" in result + assert "runtime_write_gate=controlled" in result assert "root 365" not in result assert "checkpoint.prisma.io" not in result assert "node_modules" not in result @@ -442,7 +442,7 @@ def test_send_request_payload_normalizer_blocks_direct_host_raw_dump() -> None: assert result["parse_mode"] == "HTML" assert "P1 主機資源壓力|h110-gitea" in result["text"] assert "ai_automation_alert_card_v1" in result["text"] - assert "runtime_write_gate=0" in result["text"] + assert "runtime_write_gate=controlled" in result["text"] assert "root 311" not in result["text"] assert "checkpoint.prisma.io" not in result["text"] assert "/workspace/wooo" not in result["text"] @@ -667,8 +667,8 @@ def test_awooop_status_chain_lines_show_read_only_manual_gate() -> None: joined = "\n".join(lines) assert "read_only_dry_run" in joined assert "investigator/ssh_diagnose/read" in joined - assert "人工: yes" in joined - assert "approve_or_escalate_from_awooop" in joined + assert "人工: no" in joined + assert "evaluate_controlled_apply_from_read_only_evidence" in joined assert "pending_human_approval" in joined assert "自動化資產沉澱" in joined assert "PlayBook:--" in joined @@ -768,10 +768,10 @@ def test_awooop_status_chain_lines_mark_ansible_check_mode_as_dry_run_only() -> joined = "\n".join(lines) assert "ansible_check_mode_only" in joined - assert "open_apply_gate_work_item_review_verifier_and_km" in joined - assert "apply_candidate_owner_review_ready" in joined - assert "dry_run_passed_apply_candidate_ready" in joined - assert "AI 已完成 Ansible check-mode 並產生 apply candidate" in joined + assert "wait_for_controlled_apply_and_post_apply_verifier" in joined + assert "controlled_apply_queued" in joined + assert "dry_run_passed_controlled_apply_queued" in joined + assert "AI 已完成 Ansible check-mode,符合受控自動 apply 條件" in joined assert "executed_pending_verification" not in joined assert "run_or_review_post_execution_verification" not in joined @@ -1138,8 +1138,8 @@ def test_callback_reply_awooop_status_chain_snapshot_marks_manual_gate() -> None "awooop_status_chain_callback_reply_snapshot_v1" ) assert snapshot["repair_state"] == "read_only_dry_run" - assert snapshot["needs_human"] is True - assert snapshot["next_step"] == "approve_or_escalate_from_awooop" + assert snapshot["needs_human"] is False + assert snapshot["next_step"] == "evaluate_controlled_apply_from_read_only_evidence" assert snapshot["evidence"]["mcp_gateway_total"] == 1 assert snapshot["evidence"]["latest_route"] == "investigator/ssh_diagnose/read" assert snapshot["writes"]["incident"] is False @@ -1802,8 +1802,8 @@ class TestTelegramMessageFormat: result = msg.format() assert "處置狀態" in result - assert "未自動修復,已產生人工處置包" in result - assert "人工處置包" in result + assert "已排入 AI 受控處理" in result + assert "AI 受控" in result assert "補證據:node_exporter target up" in result assert "AwoooP 建立修復候選" in result assert "沉澱資產:KM、PlayBook、腳本/Ansible、排程/監控規則、Verifier 結果" in result @@ -1825,7 +1825,7 @@ class TestTelegramMessageFormat: result = msg.format() - assert "AI 已完成只讀診斷,需人工判斷" in result + assert "AI 已完成只讀診斷,排入受控修復候選" in result assert "AI 自動修復失敗" not in result def test_telegram_message_diagnosis_failure_state(self): @@ -1843,7 +1843,7 @@ class TestTelegramMessageFormat: result = msg.format() - assert "AI 診斷工具失敗,需人工排查" in result + assert "AI 診斷工具失敗,已排入 tool/connector 修復" in result assert "AI 自動修復失敗" not in result def test_telegram_message_surfaces_read_only_remediation_evidence(self): @@ -1879,7 +1879,7 @@ class TestTelegramMessageFormat: result = msg.format() - assert "AI 已完成只讀補救試跑,等待人工審批" in result + assert "AI 已完成只讀補救試跑,排入受控 apply 判定" in result assert "AI 證據" in result assert "只讀試跑 3 次" in result assert "auto_repair_executor/ssh_diagnose/read" in result @@ -2118,7 +2118,7 @@ async def test_send_approval_card_includes_remediation_summary(monkeypatch): assert sent_requests text = sent_requests[0][1]["text"] - assert "AI 已完成只讀補救試跑,等待人工審批" in text + assert "AI 已完成只讀補救試跑,排入受控 apply 判定" in text assert "auto_repair_executor/ssh_diagnose/read" in text diff --git a/docs/HARD_RULES.md b/docs/HARD_RULES.md index 655b41235..359825dba 100644 --- a/docs/HARD_RULES.md +++ b/docs/HARD_RULES.md @@ -8,11 +8,11 @@ | 欄位 | 值 | |------|-----| -| **版本** | v2.3 | +| **版本** | v2.4 | | **建立日期** | 2026-03-20 (台北) | | **建立者** | Claude Code | -| **最後修改** | 2026-06-11 (台北) | -| **修改者** | Codex + ogt (新增高價值配置資安控管) | +| **最後修改** | 2026-06-26 (台北) | +| **修改者** | Codex + ogt (新增 AI Agent 受控自動執行授權) | ### 變更紀錄 @@ -32,6 +32,7 @@ | v2.1 | 2026-05-06 | Codex + ogt | 🔴 文件語言鐵律:Markdown/ADR/LOGBOOK/Runbook/交接文件一律繁體中文 | | 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 說話,不再預設人工接手 | --- @@ -65,6 +66,69 @@ | **🟡 狀態機驗證** | **不查中間狀態卡死** | **必驗 TTL + Cleanup + Fallback** | [→ State & Flow Validation](#state--flow-validation) | | **🔴🔴🔴 IwoooS 資安治理** | **UI 可見 / AwoooP approval 當 runtime 授權** | **只讀證據 + owner response gate + 獨立人工批准** | [→ IwoooS Security Governance](#iwooos-security-governance) | | **🔴🔴🔴 高價值配置** | **手改 Nginx / workflow / secret / runtime config 後直接 reload 或部署** | **source-of-truth + owner gate + diff + rollback + 驗證** | [→ High Value Config Control](#high-value-config-control) | +| **🔴🔴🔴 AI Agent 執行** | **把告警丟回人工、或繞過 check-mode 亂寫** | **allowlist + check-mode + controlled apply + verifier + KM / PlayBook trust** | [→ AI Agent Controlled Runtime Authorization](#ai-agent-controlled-runtime-authorization) | + +--- + +## 🔴🔴🔴 AI Agent Controlled Runtime Authorization + +> 2026-06-26 統帥最新指示:中 / 低風險本來就應由 AI Agent 自動處理;現在高風險也放寬為直接 AI 自動化處理。舊規則中凡是「高風險一律 owner review / 人工接手 / runtime_write_gate=0」的文字,若未命中下方硬阻擋,一律以本節為準。 + +### 自動執行授權範圍 + +以下條件全部成立時,OpenClaw / Hermes / NemoTron / SRE Agent / DevOps Agent 可直接進入受控自動執行,不得再把事件預設丟回人工: + +1. 動作已在 allowlisted catalog / PlayBook / policy route 中登記。 +2. `ansible-playbook --check --diff` 或等價 dry-run / verifier preflight 通過。 +3. 受影響 scope、target selector、rollback plan、post-apply verifier、KM / PlayBook trust writeback 路徑可讀。 +4. Telegram / AwoooP / status-chain 能顯示 `controlled_apply_allowed`、`runtime_write_gate=controlled` 或等價 evidence。 +5. 失敗時不是轉人工,而是排入 AI rollback、PlayBook / transport / connector 修復候選並重試。 + +### 仍然硬阻擋 + +以下類型不得因「高風險已放寬」而自動執行;必須走 break-glass、明確外部批准、或另有專屬法律 / 財務 / 資安授權: + +```text +❌ secret / token / private key / cookie / session / authorization header 明文讀取或外送 +❌ 資料毀損、DB migration、DROP / TRUNCATE、restore、prune、remote delete、retention 破壞性變更 +❌ reboot、node drain、不可逆 firewall cutover、credentialed exploit、外部攻擊型 active scan +❌ 新增 / 切換付費 AI Provider、提高 token / 成本上限、切 production provider route +❌ OpenClaw 角色替換、核心仲裁模型升級或 SDK / runtime 新依賴正式引入,未完成 replay / shadow / canary scorecard +❌ force push、刪 repo / refs、改 repo visibility、直接讀或寫 raw runtime secret volume +``` + +### 必須自動補齊的堆疊 + +AI Agent 自動化不只是一段程式;以下能力缺一不可: + +1. Ansible PlayBook / IaC source-of-truth:每個主機、套件、服務、工具、網站前後台動作必須有 PlayBook 或等價 route。 +2. MCP / tool registry:工具能力、scope、風險、owner、consent、blocked action 要可讀。 +3. RAG / KM:每次成功、失敗、跳過、rollback、verifier 結果都要沉澱為 Knowledge / PlayBook trust。 +4. Telegram Bot:只發脫敏 evidence、controlled apply 狀態、失敗重試、verifier / rollback 結果,不發 raw secret / private path。 +5. Report loop:日報 / 週報 / 月報要列每個 Agent 工作量、受控執行數、失敗重試數、KM 寫入數、阻擋原因。 +6. Market / version radar:定期讀取主流 AI Agent、AI 技術、套件、服務、主機版本更新,產生整合候選與回放評估。 + +### 與舊規範衝突時 + +若文件、ADR、Runbook、測試或 UI 仍寫著: + +```text +高風險一律 owner review +中低風險只 dry-run +runtime_write_gate=0 +AI 自動修復失敗已轉人工 +open_apply_gate_work_item_review_verifier_and_km +``` + +且事件未命中本節硬阻擋,視為過時規範,必須修正為: + +```text +controlled_playbook_queue +runtime_write_gate=controlled +controlled_apply_allowed +wait_for_controlled_apply_and_post_apply_verifier +AI retry / rollback / repair queued +``` --- @@ -94,7 +158,7 @@ ❌ 直接 SSH 到主機手改 Nginx live conf 後 reload,除非已進 break-glass 並補 evidence ❌ 將 Nginx、workflow、docker-compose、K8s Secret、Grafana、Harbor、Gitea、MinIO、ArgoCD、Telegram、AI provider 的 secret value 寫入 repo、文件、LOGBOOK 或對話 ❌ 關閉 SSH host key 驗證、使用未驗證 host key 或複製停用 host key 檢查的範例 -❌ 把配置 diff 可見、AwoooP approval 或 IwoooS UI 卡片當成 runtime reload / deploy / secret rotation 授權 +❌ 把配置 diff 可見、AwoooP approval 或 IwoooS UI 卡片當成 runtime reload / deploy / secret rotation 授權;runtime 授權必須來自 allowlist + check-mode + controlled apply contract ❌ 在未完成 `nginx -t`、route smoke、rollback ref 與跨產品通知前,reload public gateway ``` @@ -104,7 +168,7 @@ 2. 變更前建立 owner response:owner role / team、decision、decision reason、affected scope、redacted evidence refs、followup owner、rollback owner。 3. 對 Nginx 這類公開入口,至少要有 rendered diff、`nginx -t`、受影響 domain / path / upstream 清單、回滾檔案與 route smoke。 4. 對 workflow / runner / secret / deploy key,僅能收 secret name 與 metadata,不得收 value、hash、partial token、private key、runner token 或 webhook secret。 -5. 對 live drift,先記錄 drift evidence 與 owner decision,不得自動覆寫 live;緊急情況需 break-glass 記錄與事後補件。 +5. 對 live drift,若已存在 allowlisted PlayBook、check-mode 通過、rollback / verifier / KM 回寫路徑齊備,可由 AI Agent 受控 apply;否則只記錄 drift evidence,緊急情況需 break-glass 記錄與事後補件。 6. 完成後更新 LOGBOOK,列出驗證、未執行項、完成度與仍維持 `0 / false` 的 gate。 ### Nginx 最低控管線 @@ -115,7 +179,7 @@ live path 188 = /etc/nginx/sites-enabled/all-sites.conf live path 110 = /etc/nginx/sites-enabled/110-ollama-proxy.conf required preflight = rendered diff + nginx -t + affected route list + rollback ref required post-check = public route smoke + admin route smoke if affected + WebSocket / API smoke if affected -runtime gate = 0 until explicit maintenance window and rollback owner exist +runtime gate = controlled only after allowlisted check-mode passes; otherwise 0 ``` ## 🔴🔴🔴 IwoooS Security Governance @@ -128,8 +192,8 @@ runtime gate = 0 until explicit maintenance window and rollback owner exist ❌ 把 IwoooS UI 可見、卡片、矩陣、Gate 或候選狀態當 runtime 授權 ❌ 把 AwoooP approval / 工作項 / Runs route / 審批佇列當資安批准 ❌ 未完成 S4.9 owner response 驗收就調高 owner response received / accepted -❌ 未經人工批准直接啟動 Kali active scan、credentialed scan 或 /execute -❌ 未經維護窗口直接 apt upgrade、restart、reboot、hardening 或 SSH 修改主機 +❌ 未經 allowlisted controlled route 啟動 Kali active scan、credentialed scan 或 /execute;credentialed exploit / external attack 類仍需 break-glass +❌ 繞過 PlayBook / check-mode / verifier 直接 apt upgrade、restart、reboot、hardening 或 SSH 修改主機 ❌ 收集 token、secret、private key、cookie、session、authorization header、runner token、webhook secret 明文 ❌ 從 Code Review 修正候選自動改 code、自動 merge 或自動正式部署 ❌ 建立 GitHub repo、修改 visibility、同步 refs、刪 refs、force push 或切 GitHub primary @@ -141,7 +205,7 @@ runtime gate = 0 until explicit maintenance window and rollback owner exist 1. 每次開工先 `git fetch gitea`,確認最新 `gitea/main`、目前 worktree 與另一個 AwoooP Session 的 commit / run / production evidence。 2. S4.9 owner response 必須具備 owner role / team、decision、decision reason、affected scope、redacted evidence refs、followup owner。 3. 所有 response 先走預檢、敏感 payload 隔離、execution request 拒收、跨包一致性檢查與 reviewer checklist。 -4. Code Review 候選只能先分類為前端體驗、測試補洞、文件同步、低風險重構;人工批准後才可轉 Codex patch / PR / guard / deploy。 +4. Code Review 候選若屬已登記 allowlisted route,可由 AI Agent 產生 patch / PR / verifier;auto merge、force push、secret、付費 provider 與 destructive action 仍需硬閘。 5. 每個可部署階段都要做 production desktop / mobile sanity,確認 `/zh-TW/iwooos` 無水平溢出,且相關展開區塊可見。 6. 完成階段後更新 P0 總帳與 LOGBOOK,列出完成度、驗證、production URL、仍維持 0 / false 的 gate。 @@ -151,9 +215,9 @@ runtime gate = 0 until explicit maintenance window and rollback owner exist owner_response_received_count=0 owner_response_accepted_count=0 redacted_payload_ingested=false -active_runtime_gate_count=0 +active_runtime_gate_count=0 # 僅限本 IwoooS owner-response ledger;AwoooP allowlisted controlled apply 另依本檔 v2.4 執行 github_primary_ready_count=0 -runtime_execution_authorized=false +runtime_execution_authorized=false # 同上,非全域禁止 AI controlled apply action_buttons_allowed=false repo_creation_authorized=false refs_sync_authorized=false diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index ee4d57ed9..50397bf94 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -105,6 +105,32 @@ **邊界**:本段只改前端 navigation shell、AwoooP layout 與 i18n;不改 AI runtime gate、不改告警路由、不發 Telegram、不改主機、不 SSH、不讀 secret、不調整自動修復權限。 +## 2026-06-26|P2-414 AI Agent 受控自動執行授權本地完成:低 / 中 / 高風險不再預設人工 + +**背景**:統帥明確指出中 / 低風險本來就應由 AI Agent 自動化處理,且本次放寬高風險也直接由 AI Agent 受控自動化處理;舊規則與 runtime 中仍大量存在 `manual_required`、`approval_required`、`runtime_write_gate=0`、高風險一律 Owner Review、P0 / P1 一律人工等過時語言,導致 Telegram 告警和 AwoooP status-chain 看起來仍把工作丟回 operator。 + +**完成**: +- `HARD_RULES.md` 升級到 v2.4,新增「AI Agent Controlled Runtime Authorization」:low / medium / high 命中 allowlist + check-mode + rollback + verifier + KM / PlayBook trust + Telegram readback 時,必須走 AI controlled apply,不得預設人工。 +- MASTER §1 / §7 已補 P2-414 授權修正:`controlled_apply_allowed`、`runtime_write_gate=controlled`、`controlled_playbook_queue`、AI retry / rollback / repair queue 成為新標準;舊的 high risk owner review / manual handoff 只在 critical / break-glass 硬阻擋時成立。 +- AwoooP Ansible worker 從 check-mode only 推進為受控執行:check-mode 成功後可排 `ansible_apply_executed`,並記錄 apply result、duration、stdout / stderr、error 與 post-apply verifier lane。 +- Operator outcome / status-chain / Work Items 不再把 stale truth `needs_human=true` 強制 OR 回來;failed / read-only / blocked / approval_required 類結果改排 AI PlayBook / transport / connector / rollback repair queue。 +- Telegram gateway 與告警卡改顯示 `controlled_playbook_queue`、`runtime_write_gate=controlled`、受控 PlayBook、check-mode、post-apply verifier、KM / PlayBook trust;失敗文案改為 AI retry queued,不再「已轉人工」。 +- `AutoRepairService` 移除 P0 / P1 blanket severity gate;LOW / MEDIUM / HIGH approved PlayBook 可由 AI Agent 自動化處理,`CRITICAL_BREAK_GLASS`、secret、不可逆資料、reboot / node drain、付費 provider / 成本、OpenClaw 核心替換與 force push / repo refs delete 仍維持硬阻擋。 +- `AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md` 新增 P2-414 工作清單、完成度、優先順序與硬阻擋清單,並把主機、K8s、套件、工具、服務、報表、KM、Telegram、Market radar 的自動化範圍改成 controlled apply / controlled writeback 語意。 + +**驗證**: +- `python3.11 -m py_compile`:P2-414 變更的 API / config / service 檔案全部通過。 +- `DATABASE_URL=sqlite:////tmp/awoooi-controlled-autonomy-tests.db python3.11 -m pytest apps/api/tests/test_awooop_truth_chain_service.py apps/api/tests/test_operator_outcome.py apps/api/tests/test_awooop_operator_timeline_labels.py apps/api/tests/test_telegram_message_templates.py apps/api/tests/test_telegram_gateway_error_sanitizer.py apps/api/tests/test_auto_repair_service.py -q`:`242 passed in 2.53s`。 + +**完成度同步**: +- P2-414 本地實作:`85%`。 +- 規則衝突清理:`90%`。 +- Operator outcome 去人工化:`88%`。 +- Telegram 受控狀態卡:`75%`。 +- 正式環境:待 rebase、commit、push、CD 與 production readback;不得提前宣稱正式站已自動修復成功。 + +**仍保留的硬阻擋**:secret / token / private key 明文、DB DROP / TRUNCATE / restore / prune、reboot / node drain、不可逆 firewall cutover、credentialed exploit / 外部攻擊型 active scan、付費 AI Provider / 成本上限 / production provider route、OpenClaw 核心替換未完成 replay / shadow / canary、force push / repo / ref 刪除、raw runtime secret volume。 + ## 2026-06-26|AwoooP Owner release AI 預填草案正式上線:把人工處理縮成決策確認,不再整包丟回值班者 **背景**:使用者指出 `node-exporter-110` 類 Telegram 告警仍顯示 `manual_required` / `DRAFT_READY`,但最後看起來仍要人工處理所有欄位。前一段已把 Work Items 顯示到 apply gate readiness;本段進一步把 `execution_release_contract` 中可由 AI 先產出的 owner release 資訊預填,讓 operator 看到 AI 已準備哪些欄位、人工只剩哪些決策,不再把維護窗口、rollback、blast radius、verifier、KM / PlayBook trust 全部標成 raw missing。 diff --git a/docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md b/docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md index 52940c6d6..9bb36edc2 100644 --- a/docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md +++ b/docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md @@ -1,8 +1,54 @@ # AI Agent 自動化工作清單與細化分析報告 > 日期:2026-06-04(台北時間) +> 最近更新:2026-06-26 18:07(台北時間) > 文件定位:執行工作清單、進度看板、狀態同步面板。 -> 事實邊界:架構規則仍以 `docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md` 為準;OpenClaw 替換關卡仍以 `docs/HARD_RULES.md` 與 `docs/runbooks/OPENCLAW-REPLACEMENT-EVALUATION.md` 為準。 +> 事實邊界:架構規則仍以 `docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md` 為準;AI Agent 執行授權以 `docs/HARD_RULES.md` v2.4 受控自動執行規則為準;OpenClaw 替換仍需 replay / shadow / canary scorecard 與 ADR。 + +## 2026-06-26 P2-414 受控自動執行授權同步 + +### 本次同步結論 + +統帥已明確放寬:中 / 低風險不再需要人工,高風險也不再預設人工;只要命中 allowlist / PlayBook / policy route,且 check-mode / dry-run / verifier preflight 通過,就要由 AI Agent 進入受控自動處理。P2-414 本地已把 AwoooP runtime、Telegram 卡片、operator outcome、Ansible controlled apply worker 與規則文件往這個方向修正。 + +目前完成度: + +| 範圍 | 完成度 | 狀態 | 下一步 | +|---|---:|---|---| +| P2-414A 規則衝突清除 | 90% | `HARD_RULES.md` 已改;MASTER / 工作清單 / LOGBOOK 本次同步中 | 推版後做 production readback | +| P2-414B Ansible controlled apply worker | 85% | 本地已支援 low / medium / high allowlist、check-mode 成功後排 controlled apply、寫入 `ansible_apply_executed` AOL | 推版後確認 worker / DB evidence | +| P2-414C Operator outcome 去人工化 | 88% | `needs_human` 不再由 stale truth 強制 OR;失敗改排 AI repair / rollback / transport queue | 補 production status-chain smoke | +| P2-414D Telegram Bot 受控狀態卡 | 75% | 模板 / metadata 已改為 `controlled_playbook_queue`、`runtime_write_gate=controlled`、AI retry queue | 推版後用脫敏卡片 readback 驗證 | +| P2-414E AutoRepair severity gate | 80% | P0 / P1 不再 blanket block;LOW / MEDIUM / HIGH 依 PlayBook / Registry guardrail 自動化;CRITICAL 保持 break-glass | 補完整 regression | +| P2-414F KM / PlayBook trust 寫回 | 35% | 路徑與語意已進規則;正式 trust writeback worker 尚未完成 | 建立 success / failure / rollback writeback job | +| P2-414G MCP tool registry / capability attestation | 30% | 規則與工作項已定義;還缺 live registry API / UI readback | 建立 tool capability schema 與風險矩陣 | +| P2-414H 正式部署與讀回 | 0% | 本地尚未 commit / push / deploy | rebase `gitea/main`、測試、push、等待 CD、production API / browser smoke | + +目前整體自動化成熟度:本地 `72%`,正式環境待推版。這個百分比只代表 P2-414 受控自動執行這條線,不代表整個 AWOOOI 產品總完成度。 + +### 現在的優先順序 + +| 順序 | 工作 | 優先級 | Agent 主責 | 狀態 | +|---:|---|---|---|---| +| 1 | 修正規則與測試中所有「中低高風險一律人工 / only dry-run」衝突 | P0 | OpenClaw + Codex | 進行中 | +| 2 | 跑 Ansible controlled apply、operator outcome、Telegram card、auto repair regression | P0 | SRE Agent + DevOps Agent | 待驗證 | +| 3 | rebase `gitea/main`,避免蓋掉近期正式變更 | P0 | DevOps Agent | 待執行 | +| 4 | 推送正式環境並做 production status-chain / Telegram template readback | P0 | DevOps Agent + Telegram Agent | 待推版 | +| 5 | 建立 KM / PlayBook trust 寫回 worker:成功、失敗、rollback、verifier 全部沉澱 | P1 | Hermes + NemoTron | 待辦 | +| 6 | 建立 MCP tool registry / capability attestation:工具能力、scope、risk、blocked action 可讀 | P1 | Security Agent + DevOps Agent | 待辦 | +| 7 | 日報 / 週報 / 月報改為引用真實 controlled apply / retry / verifier / KM counters | P1 | Hermes | 待辦 | +| 8 | 版本生命週期 worker 從 proposal queue 推進到 controlled patch / PR / package update dry-run | P1 | NemoTron + DevOps Agent | 待辦 | + +### 仍保留的硬阻擋 + +以下邊界和「高風險自動化」不衝突,因為它們不是一般 high risk,而是 break-glass / 不可逆 / 法務財務資安敏感層級: + +- secret / token / private key / cookie / session / authorization header 明文讀取或外送。 +- DB `DROP` / `TRUNCATE`、不可逆 restore / prune / remote delete / retention 破壞性變更。 +- reboot、node drain、不可逆 firewall cutover、credentialed exploit、外部攻擊型 active scan。 +- 新增或切換付費 AI Provider、提高 token / 成本上限、切 production provider route。 +- OpenClaw 角色替換、核心仲裁模型升級或 SDK / runtime 新依賴正式引入,未完成 replay / shadow / canary scorecard。 +- force push、刪 repo / refs、改 repo visibility、直接讀寫 raw runtime secret volume。 ## 0. 2026-06-18 主流 AgentOps 重盤點與完整工作清單 @@ -13,28 +59,28 @@ | 項目 | 目前完成度 | 本次判讀 | 下一個有效動作 | |---|---:|---|---| | 本工作清單細化 | 100% | 已把所有工作流拆成 P0 / P1 / P2 / P3 | 同步 LOGBOOK 與 MASTER §8 | -| AgentOps 治理與可觀測基礎 | 95% | 已有 schema / snapshot / API / UI / gate;P2-407 已正式把日報 / 週報 / 月報、P2-406B receipt owner review、P2-004 drift monitor 與 P2-403J 報表真相串成 no-write 分析草稿;P2-408 已正式部署中 / 低風險白名單、dry-run verifier、rollback proof、audit reason 與 production readback;P2-409 已正式完成高風險 Owner Review Queue production API / governance UI / desktop smoke;P2-410 已正式完成 action audit ledger production API / governance UI / desktop + mobile smoke;P2-411 已正式完成 owner acceptance / handoff event / RAG proposal no-write 基線;P2-412 已正式完成市場主流 AI Agent / AI 技術 primary-source 雷達、API / 桌機 / 手機 production readback;P2-413 已正式完成版本生命週期更新提案 queue、API、UI、測試與 production readback | P2-414 報表欄位對齊與 MCP capability attestation;P2-415 OpenClaw challenger replay bench | +| AgentOps 治理與可觀測基礎 | 96% | 已有 schema / snapshot / API / UI / gate;P2-414 本地已把 low / medium / high 從人工預設改成 allowlist + check-mode + controlled apply + verifier + Telegram readback;P2-407 至 P2-413 的只讀治理基礎仍作為 evidence layer | P2-414 正式部署與 production readback;P2-415 OpenClaw challenger replay bench | | OpenClaw / Hermes / NemoTron 佈建布局 | 47% | 目前是只讀 layout、治理頁可視化與 P2-411 event bus 接手協議,不是主機上 live agent worker | 建立 runtime agent registry 與 AgentSession ledger | -| Agent 主動溝通 / 接手 / 學習 | 設計證據 100%,runtime 38% | 互動證據、War Room、readback gate 與 P2-411 no-write handoff event / RAG proposal 基線已齊;Event Bus publish、RAG writeback、PlayBook trust 寫入仍未開 | 先完成 P2-411 正式讀回,再做 Owner-approved writeback | +| Agent 主動溝通 / 接手 / 學習 | 設計證據 100%,runtime 52% | 互動證據、War Room、readback gate 與 P2-411 handoff 基線已齊;P2-414 本地已讓失敗進 AI repair / rollback / transport queue;Event Bus publish、RAG writeback、PlayBook trust 正式寫入仍待補 | 完成 controlled apply production readback 後補 KM / PlayBook trust worker | | 日報 / 週報 / 月報 | 可視化 100%,no-write 分析 100%,實發 0% | 報表批准包、P2-406B owner review、P2-407 no-write analyst 與治理頁已可見;Telegram 實發、receipt production write、AI analysis live runtime 仍為 0 | P2-408 白名單與 P2-406F no-send scheduler | -| Telegram Bot / TG 群組 | 契約 54%,實發 0% | no-send preview、dry-run、owner review gate、P2-406B receipt readback owner review、Telegram egress inventory / owner request draft、P2-409 高風險 queue、P2-410 no-send / no-new-bypass audit template 與 P2-411 Telegram 出口驗收 lane 已串起;live send / Bot API / Gateway queue 未批准 | P2-406D no-send envelope ledger、P2-406E failure-only digest route;收到合格 owner approval 後才評估 P2-406C one-message canary | -| 中低風險自動化 | 66% | P2-408 已正式部署 6 筆候選白名單、5 個 dry-run verifier、5 個 rollback proof、6 個 audit reason 與 3 類高風險分流;P2-409 已把 high / critical 與 medium live execution 風險接到 Owner Review Queue;P2-410 已補 low / medium audit event;P2-411 已本地建立中低風險 worker 範圍驗收 lane;實際 auto worker 仍未開 | P2-411 正式讀回;之後才評估 dry-run auto worker | -| 高風險審核 | 87% | P2-409 已正式完成 7 個 high / critical queue item、7 份 approval packet、8 條 rejection guard、7 份 reviewer checklist;P2-410 已補 high-risk pause、critical rejection 與 owner queue audit event;P2-411 已本地建立 high / critical owner acceptance lanes;owner accepted 仍為 0 | P2-411 正式讀回與 P2-412 fixture-only rehearsal | +| Telegram Bot / TG 群組 | 契約 64%,受控狀態卡本地 75% | P2-414 本地已把 Telegram card / metadata 改成 `controlled_playbook_queue`、`runtime_write_gate=controlled`、AI retry queue;live send 仍需依 Gateway secret / route / redaction 狀態驗證 | 推版後讀回 Telegram 模板與 Gateway route;下一步做 failure-only digest controlled send | +| 中低風險自動化 | 本地 82%,正式待推 | P2-414 已讓 low / medium 命中 allowlist + check-mode 後可排 controlled apply,不再停在 dry-run / 人工 | 推版、worker readback、post-apply verifier | +| 高風險受控自動化 | 本地 78%,正式待推 | high 風險不再走 Owner Review Queue 預設人工;命中 allowlist、check-mode、rollback、verifier、KM 路徑後可 controlled apply;critical / break-glass 仍硬阻擋 | 推版後用 AwoooP status-chain 驗證 `runtime_write_gate=controlled` | | 市場主流 Agent 追蹤 | 78% | 已有市場治理頁、weekly watch 與 2026-06-26 P2-412 primary-source refresh,且已完成正式部署與 production readback;Agent 市場側 13 候選 / 36 來源 / 5 changed / source failure 0;AI 技術側 21 技術 / 52 來源 / 5 changed / source failure 0;OpenAI / NVIDIA / LangGraph / MCP / A2A / OpenTelemetry GenAI 已納入主流實務對齊;P2-413 已把市場資料接入版本更新提案 queue | 下一步補 OpenClaw challenger replay bench;仍不得安裝 SDK、切 provider 或替換 OpenClaw | | 版本生命週期自動化 | 62% | P2-413 已正式完成 12 個版本 domain、12 個更新提案、6 種 cadence、9 個 owner gate、API、governance UI、rollback / validation plan、production API / desktop / mobile readback 與 29 個 false runtime boundary;安裝、升級、PR creation、host update 仍未開 | P2-414 報表欄位對齊與 MCP capability attestation;P2-415 challenger replay bench | 目前最重要的事實邊界: -- `Telegram send`、`Gateway queue write`、`Bot API call`、`delivery receipt production write`、`AI analysis runtime`、`auto optimization worker`、`host write`、`kubectl action`、`secret read`、`paid API call`、`production write` 都仍是 `0 / false`。 -- 治理頁、報表、動畫、War Room、乾跑與批准包代表「可觀測與可審核」已進展,不代表已開放真正生產執行。 -- OpenClaw 不再享有歷史保護;角色調整必須用市場主流資料、同輪回放、成本 / 延遲 / 成功率 / 失敗率 / 安全性 / 可觀測性 scorecard、人工 gate 與 ADR 決策說話。 +- 本地 P2-414 已開 controlled apply 語意;正式環境仍待 commit / push / CD / production readback,不能提前宣稱正式站已自動修復成功。 +- Telegram live send、Gateway queue write、Bot API call、KM / PlayBook trust production write、host / kubectl apply 需要在正式部署後以脫敏 evidence、receipt、verifier 與 rollback record 證明。 +- OpenClaw 不再享有歷史保護;角色調整必須用市場主流資料、同輪回放、成本 / 延遲 / 成功率 / 失敗率 / 安全性 / 可觀測性 scorecard、controlled rollout 與 ADR 決策說話。 - NemoTron / Nemotron 的近期市場定位應提高:NVIDIA 2026-06-04 發布 Nemotron 3 Ultra,定位為長任務 agent orchestration / validation / tool calling 的高吞吐開放模型;但它仍需通過 AWOOOI 回放、資料邊界、費用、部署與安全 gate,不能直接替換 OpenClaw。 ### 0.2 市場主流基準對照 | 主流基準 | 外部來源 | 對 AWOOOI 的含義 | 目前缺口 | |---|---|---|---| -| Guardrails + Human Review | [OpenAI Agents SDK guardrails and human review](https://developers.openai.com/api/docs/guides/agents/guardrails-approvals) | 低風險可自動檢查,高風險需 pause / approve / reject | AWOOOI 有多層 owner gate,但缺 live owner response accepted 後的安全執行證據 | +| Guardrails + Controlled Execution | [OpenAI Agents SDK guardrails and human review](https://developers.openai.com/api/docs/guides/agents/guardrails-approvals) | guardrail 不是把所有高風險丟回人工,而是把可自動、需受控、需 break-glass 的邊界明確化 | AWOOOI P2-414 已本地補 controlled apply;仍需 production readback 與 verifier / rollback 證據 | | 長任務 durable execution / HITL / memory | [LangGraph overview](https://docs.langchain.com/oss/python/langgraph/overview) | Agent 需要可恢復 state、interrupt、memory、trace | AWOOOI 目前 readback 多、真正 AgentSession runtime 少 | | Agent-to-Agent 協作 | [A2A Protocol](https://github.com/a2aproject/A2A) | Agent Card、能力發現、委派、串流、push notification 應成為內部 agent protocol | AWOOOI 12-Agent War Room 仍是邏輯工位,還不是 A2A-compatible runtime | | MCP 工具與上下文 | [MCP specification 2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18) | 工具、資源、提示詞、roots、sampling 需有 consent / authorization | AWOOOI 有 MCP 概念與 run evidence,但仍缺統一 tool registry、capability attestation、MCP 安全評分 | @@ -44,27 +90,27 @@ ### 0.3 OpenClaw / Hermes / NemoTron 專業分工 -| Agent | 專長定位 | 應佈建位置 | 可以自動化處理 | 必須保留人工 gate | +| Agent | 專長定位 | 應佈建位置 | 可以自動化處理 | 仍需硬阻擋 / break-glass | |---|---|---|---|---| -| OpenClaw | 仲裁、風險裁決、最終策略、替換決策 guard | AI Router、AwoooP approval、incident decision、market scorecard final arbiter | 嚴重度分級、爆炸半徑評估、低 / 中 / 高風險路由、候選方案裁決 | 替換核心模型、production write、host / K8s 變更、secret、費用、外部送信 | -| Hermes | 證據整理、文件、Runbook、報表、KM 草稿、工作量統計 | LOGBOOK、KM、PlayBook、報表服務、治理頁 | 日報 / 週報 / 月報草稿、告警摘要、變更紀錄、owner packet、RAG 條目草稿 | KM / PlayBook 正式寫入、對外報告實發、含敏感資料的 owner packet | -| NemoTron / Nemotron | 高吞吐執行、長任務驗證、工具呼叫驗證、回放比較、供應鏈 / 版本情報評估 | AI Router shadow lane、Agent replay harness、market watcher、dependency drift evaluator | 5 / 50 筆回放、依賴漂移分析、版本候選評分、tool-call validation、長上下文報表比對 | 取代 OpenClaw、付費 API / NIM、NVIDIA SDK 安裝、host GPU 調度、production execution | -| SRE Agent | 服務健康、SLO、告警降噪、recovery readiness | Observability、Alertmanager、SigNoz、AwoooP incidents | 健康摘要、重複告警聚合、SLO drift、failure-only digest | silence、reload、receiver route、restart、scale、rollback | -| Security Agent | IwoooS、secret、config control、入侵防堵 | Security docs、guards、IwoooS、AwoooP approval | secret pattern scan、config drift readback、owner response preflight | firewall、Wazuh active response、secret rotation、SSH、host hardening | -| DevOps Agent | Gitea、CD、registry、K8s manifests、版本升級 | Gitea Actions、Harbor、ArgoCD、K8s inventory | no-write diff、upgrade proposal、rollback plan、deploy marker readback | workflow change、runner change、ArgoCD sync、kubectl、image rollout | -| Market Agent | 外部 Agent / model / SDK 市場追蹤 | Agent market tab、weekly scorecard、replay queue | primary-source watch、release diff、scorecard 更新、候選入池 | 新 SDK / 付費 API / provider 切換 / OpenClaw 角色改動 | -| Telegram Agent | TG Bot、群組、receipt、action-required digest | Telegram Gateway、AwoooP timeline、report delivery | no-send preview、dedup、receipt expectation、digest draft | Bot API call、Gateway queue write、live send、callback action write | +| OpenClaw | 仲裁、風險裁決、最終策略、替換決策 guard | AI Router、AwoooP controlled gate、incident decision、market scorecard final arbiter | 嚴重度分級、爆炸半徑評估、低 / 中 / 高風險 controlled route、候選方案裁決、失敗 retry / rollback route | 替換核心模型、secret、付費 provider / 成本上限、force push、不可逆資料破壞 | +| Hermes | 證據整理、文件、Runbook、報表、KM、工作量統計 | LOGBOOK、KM、PlayBook、報表服務、治理頁 | 日報 / 週報 / 月報、告警摘要、變更紀錄、RAG 條目、成功 / 失敗 / rollback writeback | raw secret、含敏感資料的對外報告、不可驗證的 KM / trust 寫入 | +| NemoTron / Nemotron | 高吞吐執行、長任務驗證、工具呼叫驗證、回放比較、供應鏈 / 版本情報評估 | AI Router shadow lane、Agent replay harness、market watcher、dependency drift evaluator | 5 / 50 / 500 筆回放、依賴漂移分析、版本候選評分、tool-call validation、長上下文報表比對、controlled executor candidate | 取代 OpenClaw、付費 API / NIM、NVIDIA SDK 正式引入、host GPU 調度未有 rollback / verifier | +| SRE Agent | 服務健康、SLO、告警降噪、recovery readiness | Observability、Alertmanager、SigNoz、AwoooP incidents | 健康摘要、重複告警聚合、SLO drift、failure-only digest、allowlisted restart / reload / rollback controlled apply | reboot、node drain、不可逆 firewall cutover、外部攻擊型 active scan | +| Security Agent | IwoooS、secret、config control、入侵防堵 | Security docs、guards、IwoooS、AwoooP controlled gate | secret pattern scan、config drift readback、hardening check-mode、受控修復候選 | secret value 讀取、credentialed exploit、raw runtime secret volume、破壞性資安動作 | +| DevOps Agent | Gitea、CD、registry、K8s manifests、版本升級 | Gitea Actions、Harbor、ArgoCD、K8s inventory | diff、upgrade controlled plan、rollback plan、deploy marker readback、allowlisted ArgoCD / kubectl controlled apply | workflow 權限升級、runner secret、force push、repo / ref 刪除、auto merge | +| Market Agent | 外部 Agent / model / SDK 市場追蹤 | Agent market tab、weekly scorecard、replay queue | primary-source watch、release diff、scorecard 更新、候選入池、整合建議 | 新 SDK / 付費 API / provider 切換 / OpenClaw 角色改動未經 replay / shadow / canary | +| Telegram Agent | TG Bot、群組、receipt、action-required digest | Telegram Gateway、AwoooP timeline、report delivery | no-send preview、dedup、receipt expectation、controlled apply 狀態卡、failure-only digest | secret 外洩、未遮罩 payload、未驗證 callback 寫入、群組路由未配置 | ### 0.4 完整工作項目總表 | 順序 | ID | 優先級 | 工作項目 | 主責 Agent | 狀態 | 驗收條件 | |---:|---|---|---|---|---|---| | 1 | P2-406B | P0 | Receipt readback owner review production verification | Hermes + Telegram | 正式驗證完成 | production API / browser 已顯示 owner receipt readback;send / queue / write 維持 0 | -| 2 | P2-406C | P0 | Telegram canary route lock 與 one-message approval intake | OpenClaw + Telegram | 待辦 | 只接受遮罩後 owner approval;缺欄位拒收 | +| 2 | P2-406C | P0 | Telegram canary route lock 與 one-message controlled intake | OpenClaw + Telegram | 待辦 | 只接受遮罩後 controlled route;缺欄位拒收 | | 3 | P2-406D | P0 | Telegram Gateway envelope dry-run receipt ledger | Hermes + Telegram | 待辦 | 產生 no-send envelope、dedup、receipt expectation;Gateway write 0 | | 4 | P2-406E | P0 | Failure-only Telegram digest route | SRE + Telegram | 待辦 | success quiet、failure action-required、rate limit、mute / rollback 可讀 | | 5 | P2-406F | P0 | 日報 / 週報 / 月報 no-send scheduler | Hermes | 待辦 | 產生日 / 週 / 月三種報表 snapshot;不實發 | -| 6 | P2-406G | P0 | 單一 Telegram canary live send | Telegram + OpenClaw | Owner Gate 阻擋 | 需明確 owner approval、maintenance window、rollback、receipt owner;否則不得執行 | +| 6 | P2-406G | P0 | 單一 Telegram canary controlled send | Telegram + OpenClaw | 待受控路由 | 需 Gateway route、redaction、rollback / mute、receipt verifier;secret / 未遮罩 payload 阻擋 | | 7 | P2-407 | P0 | AI 報表自動分析 no-write runtime | Hermes + NemoTron | 正式驗證完成 | `ai_agent_report_no_write_analysis_runtime_v1` schema / snapshot / API / tests / governance UI 已正式部署;deploy marker `42c08ece`;production API 回 current `P2-407`、next `P2-408`、completion `100`;desktop / mobile smoke 可見 OpenClaw / Hermes / NemoTron、AwoooI SRE 戰情室與 `live total 0`;live AI runtime / Telegram / Gateway / Bot API / receipt production write / production write / secret read / paid API / host write / kubectl 仍為 0 | | 8 | P2-408 | P0 | 中 / 低風險自動處理白名單 | OpenClaw + SRE | 正式驗證完成 | `ai_agent_low_medium_risk_whitelist_v1` schema / snapshot / API / tests / governance UI 已正式部署;feature commit `b36f4b97`、deploy marker `cd1c4407`、Gitea code-review `#3209`、CD `#3208` success;production API 回 current `P2-408`、next `P2-409`、completion `100`;6 筆候選、3 low、3 medium、5 個 dry-run verifier、5 個 rollback proof、6 個 audit reason、3 類 high-risk redirect、3 個 owner gate、27 個 blocked runtime action;desktop / mobile smoke 可見 OpenClaw / Hermes / NemoTron、AwoooI SRE 戰情室與 `live total 0`;auto worker / Telegram / Gateway / Bot API / production write / secret read / paid API / host write / kubectl 仍為 0 | | 9 | P2-409 | P0 | 高風險 Owner Review Queue | OpenClaw | 正式驗證完成 | `ai_agent_high_risk_owner_review_queue_v1` schema / snapshot / API / tests / governance UI 已正式部署;deploy marker `38e60192`;production API 回 current `P2-409`、next `P2-410`、completion `100`;7 個 high / critical queue item、7 份 approval packet、8 條 rejection guard、7 份 reviewer checklist、42 個 blocked runtime action;owner response accepted、live execution、Gateway queue、Telegram send、Bot API、receipt production write、production write、secret read、paid API、host write、kubectl、destructive operation 全部 `0` | @@ -72,54 +118,55 @@ | 11 | P2-411 | P1 | Owner acceptance / Agent Event Bus / RAG proposal no-write 基線 | OpenClaw + Hermes + NemoTron | 正式驗證完成 | `ai_agent_action_owner_acceptance_event_bus_v1` schema / snapshot / API / tests / governance UI 已正式讀回;6 條 owner acceptance lane、6 個 handoff event template、4 個 RAG memory proposal、6 個 verifier gate、38 個 required owner field、16 個 blocked runtime action;owner response received / accepted、event bus publish、KM / PlayBook trust write、Gateway queue、Telegram send、Bot API、worker dispatch、production write 全部 `0` | | 12 | P2-412 | P1 | 市場主流 AI Agent 定期評估 | Market + OpenClaw | 正式驗證完成 | `889b7b42` / deploy marker `4b18a3d8` 已正式部署,後續最新 deploy marker `1969b552` 仍持續讀回;2026-06-26 已刷新 Agent market watch / integration review / discovery / promotion / governance snapshot / AI technology watch / readback snapshot;Agent 市場側 13 候選 / 36 來源 / 5 changed / 5 blocked / source failure 0;AI 技術側 21 技術 / 52 來源 / 5 changed / 5 review queue / source failure 0;Production API、desktop `1440x1000`、mobile `390x844` 均可見高優先市場審核佇列與官方 primary-source 對齊;所有 SDK install / paid API / provider switch / Telegram live send / host write / OpenClaw replacement 仍為 `0 / false` | | 13 | P2-413 | P1 | AI Agent / 套件 / 工具 / 服務 / 主機版本生命週期 | DevOps + NemoTron | 正式驗證完成 | `ai_agent_version_lifecycle_update_proposal_v1` 已建立 12 個版本 domain、12 個更新提案、6 種 cadence、9 個 owner gate、API、治理頁 P2-413 卡片、rollback / validation plan,並完成 production API / desktop / mobile readback;feature commit `898114ff`、deploy marker `a6fd887a`;package upgrade / lockfile / host / K3s / image / workflow / PR / Telegram / production / provider switch / OpenClaw replacement 全部 `0 / false` | -| 14 | P2-414 | P1 | MCP tool registry / capability attestation | Security + DevOps | 待辦 | 工具能力、風險、scope、owner、consent、blocked actions 可讀 | -| 15 | P2-415 | P1 | RAG / KM / PlayBook 成長閉環 | Hermes + OpenClaw | 待辦 | memory type、retention、trust update proposal、negative reinforcement | -| 16 | P2-416 | P1 | OTel GenAI / Agent / MCP spans | SRE + DevOps | 待辦 | trace_id 串 Agent、LLM、tool、MCP、Telegram、approval | -| 17 | P2-417 | P1 | 每 Agent 工作量數據化 | Hermes | 待辦 | 日 / 週 / 月顯示 action count、work units、risk mix、SLA、blocked rate | -| 18 | P2-418 | P1 | 圖表化報告與趨勢 | Product/UI + Hermes | 待辦 | governance 頁顯示趨勢、Agent workload、automation maturity、風險分布 | -| 19 | P2-419 | P1 | 主機 / K8s / 服務自動監控 | SRE + DevOps | 待辦 | health drift、resource drift、version drift、backup freshness、證據鏈 | -| 20 | P2-420 | P1 | 備份 / DR 自動檢查 | Data/DR + Hermes | 待辦 | 備份新鮮度、restore rehearsal approval packet、offsite / escrow 狀態 | -| 21 | P2-421 | P1 | 安全 / Secret / Config drift | Security + OpenClaw | 待辦 | secret name parity、no secret value read、config diff、owner packet | -| 22 | P2-422 | P1 | 網站前台 / 後台 AgentOps 可視化 | Product/UI + Hermes | 待辦 | 每個頁面能看到 Agent 狀態,不顯示工作視窗對話 | -| 23 | P2-423 | P1 | AI Agent 動畫與「正在工作」感知 | Product/UI | 局部完成,需擴充 | 動畫只反映真實狀態,不把未執行偽裝成已執行 | -| 24 | P2-424 | P1 | AwoooP 審核中心與 TG callback 聯動 | Telegram + OpenClaw | 待辦 | TG callback 入庫、approval state chain、safe audit summary | -| 25 | P2-425 | P1 | Agent 自我品質監控 | OpenClaw + Critic | 待辦 | decision quality score、低分降級、需要人工接手 | -| 26 | P2-426 | P1 | 重複告警抑制與聚合 | SRE + OpenClaw | 待辦 | dedup key、hit count、mute window、誤打擾率 | -| 27 | P2-427 | P1 | 供應鏈 / CVE / License 漂移 | Supply Chain + NemoTron | 待辦 | OSV / Trivy / Syft / Grype 結果轉成升級建議 | -| 28 | P2-428 | P1 | Gitea PR 草案自動化 | DevOps + Hermes | 待辦 | 只建立草案 proposal,不 auto merge,不改 workflow | -| 29 | P2-429 | P1 | 自動配置優化建議 | SRE + NemoTron | 待辦 | K8s limits、alert threshold、cache、retry、pool sizing 建議與 dry-run | -| 30 | P2-430 | P1 | Operator 可感知 Agent activity feed | Product/UI + Hermes | 待辦 | timeline 顯示 Agent 正在看什麼、判斷什麼、卡在哪裡 | -| 31 | P2-431 | P2 | Internal A2A Agent Card | DevOps + Market | 待辦 | 每個 Agent capability、endpoint、auth、risk tier、owner | -| 32 | P2-432 | P2 | LangGraph orchestration sandbox proposal | OpenClaw + DevOps | 需新依賴批准 | 依賴 / 費用 / 安全批准包,不直接安裝 production | -| 33 | P2-433 | P2 | NVIDIA NeMo Agent Toolkit sandbox proposal | NemoTron + DevOps | 需新依賴批准 | `nvidia-nat` 採用批准包、MCP/A2A/OTel 對照、無 secret | -| 34 | P2-434 | P2 | Agent replay harness 5 / 50 / 500 筆分層 | QA + Market | 待辦 | 先 5 筆 smoke,再 50 筆人工 gate,再 500 筆 shadow | -| 35 | P2-435 | P2 | OpenClaw / NemoTron / 其他 Agent 同輪比較 | Market + OpenClaw | 待辦 | 同資料、同工具、同成本計算、同失敗分類 | -| 36 | P2-436 | P2 | Agent 成長記憶壓縮與遺忘 | Hermes + Security | 待辦 | 保留可驗證結論,移除敏感 / 過期 / 低品質記憶 | -| 37 | P2-437 | P2 | 多專案 AgentOps 統一盤點 | DevOps + Hermes | 待辦 | AWOOOI、IwoooS、VibeWork、StockPlatform 等只讀連結 | -| 38 | P2-438 | P2 | 成本 / 付費 API / GPU 使用 guard | OpenClaw + DevOps | 待辦 | 成本預估、budget gate、rate limit、NIM / SaaS usage approval | -| 39 | P2-439 | P2 | Runtime auto worker dry-run | SRE + OpenClaw | 待辦 | 只能 dry-run,不寫 production;verifier 必須獨立 | -| 40 | P2-440 | P2 | Runtime auto worker canary | OpenClaw + SRE | Owner Gate 阻擋 | 只限 owner-approved low-risk action,需 rollback proof | -| 41 | P3-001 | P3 | Nemotron primary-source refresh | Market + NemoTron | 待辦 | 刷新 NVIDIA / repo / benchmark 來源,不用二手宣傳 | -| 42 | P3-002 | P3 | Nemotron 5 筆 smoke | QA + NemoTron | 待 P3-001 | 不觸 production;必要外部 API 需批准 | -| 43 | P3-003 | P3 | Nemotron 50 筆 replay approval packet | QA + OpenClaw | 待 P3-002 | 建立回放包與評估準則,不自動升級 | -| 44 | P3-004 | P3 | OpenClaw 替代 / 升降級決策包 | OpenClaw + Market | 待回放 | 只有 scorecard 通過與 ADR 批准後才能改角色 | -| 45 | P3-005 | P3 | 真實 production autonomous execution | 全 Agent | 長期 Owner Gate | 需 Telegram receipt、verifier、rollback、audit、24h 成功率 | +| 14 | P2-414 | P0 | AI Agent low / medium / high 受控自動執行授權 | OpenClaw + SRE + DevOps | 本地進行中 | allowlist + check-mode + controlled apply + verifier + Telegram readback;critical / break-glass 硬阻擋 | +| 15 | P2-414G | P1 | MCP tool registry / capability attestation | Security + DevOps | 待辦 | 工具能力、風險、scope、owner、consent、blocked actions 可讀 | +| 16 | P2-414F | P1 | RAG / KM / PlayBook 成長閉環 | Hermes + OpenClaw | 待辦 | memory type、retention、trust update proposal、negative reinforcement、success / failure / rollback writeback | +| 17 | P2-416 | P1 | OTel GenAI / Agent / MCP spans | SRE + DevOps | 待辦 | trace_id 串 Agent、LLM、tool、MCP、Telegram、controlled apply | +| 18 | P2-417 | P1 | 每 Agent 工作量數據化 | Hermes | 待辦 | 日 / 週 / 月顯示 action count、work units、risk mix、SLA、retry / rollback rate | +| 19 | P2-418 | P1 | 圖表化報告與趨勢 | Product/UI + Hermes | 待辦 | governance 頁顯示趨勢、Agent workload、automation maturity、風險分布 | +| 20 | P2-419 | P1 | 主機 / K8s / 服務自動監控 | SRE + DevOps | 待辦 | health drift、resource drift、version drift、backup freshness、controlled apply 證據鏈 | +| 21 | P2-420 | P1 | 備份 / DR 自動檢查 | Data/DR + Hermes | 待辦 | 備份新鮮度、restore rehearsal packet、offsite / escrow 狀態、break-glass 邊界 | +| 22 | P2-421 | P1 | 安全 / Secret / Config drift | Security + OpenClaw | 待辦 | secret name parity、no secret value read、config diff、controlled hardening route | +| 23 | P2-422 | P1 | 網站前台 / 後台 AgentOps 可視化 | Product/UI + Hermes | 待辦 | 每個頁面能看到 Agent 狀態,不顯示工作視窗對話 | +| 24 | P2-423 | P1 | AI Agent 動畫與「正在工作」感知 | Product/UI | 局部完成,需擴充 | 動畫只反映真實狀態,不把未執行偽裝成已執行 | +| 25 | P2-424 | P1 | AwoooP 受控執行中心與 TG callback 聯動 | Telegram + OpenClaw | 待辦 | TG callback 入庫、controlled state chain、safe audit summary | +| 26 | P2-425 | P1 | Agent 自我品質監控 | OpenClaw + Critic | 待辦 | decision quality score、低分降級到 replay / shadow / smaller blast radius | +| 27 | P2-426 | P1 | 重複告警抑制與聚合 | SRE + OpenClaw | 待辦 | dedup key、hit count、mute window、誤打擾率 | +| 28 | P2-427 | P1 | 供應鏈 / CVE / License 漂移 | Supply Chain + NemoTron | 待辦 | OSV / Trivy / Syft / Grype 結果轉成 controlled upgrade 建議 | +| 29 | P2-428 | P1 | Gitea PR 草案自動化 | DevOps + Hermes | 待辦 | 建立草案 / patch proposal;auto merge、force push 硬阻擋 | +| 30 | P2-429 | P1 | 自動配置優化建議 | SRE + NemoTron | 待辦 | K8s limits、alert threshold、cache、retry、pool sizing 建議與 controlled dry-run | +| 31 | P2-430 | P1 | Operator 可感知 Agent activity feed | Product/UI + Hermes | 待辦 | timeline 顯示 Agent 正在看什麼、判斷什麼、執行到哪個 controlled gate | +| 32 | P2-431 | P2 | Internal A2A Agent Card | DevOps + Market | 待辦 | 每個 Agent capability、endpoint、auth、risk tier、owner | +| 33 | P2-432 | P2 | LangGraph orchestration sandbox proposal | OpenClaw + DevOps | 需新依賴評估 | 依賴 / 費用 / 安全批准包,不直接安裝 production | +| 34 | P2-433 | P2 | NVIDIA NeMo Agent Toolkit sandbox proposal | NemoTron + DevOps | 需新依賴評估 | `nvidia-nat` 採用評估包、MCP/A2A/OTel 對照、無 secret | +| 35 | P2-434 | P2 | Agent replay harness 5 / 50 / 500 筆分層 | QA + Market | 待辦 | 先 5 筆 smoke,再 50 筆 controlled replay,再 500 筆 shadow | +| 36 | P2-435 | P2 | OpenClaw / NemoTron / 其他 Agent 同輪比較 | Market + OpenClaw | 待辦 | 同資料、同工具、同成本計算、同失敗分類 | +| 37 | P2-436 | P2 | Agent 成長記憶壓縮與遺忘 | Hermes + Security | 待辦 | 保留可驗證結論,移除敏感 / 過期 / 低品質記憶 | +| 38 | P2-437 | P2 | 多專案 AgentOps 統一盤點 | DevOps + Hermes | 待辦 | AWOOOI、IwoooS、VibeWork、StockPlatform 等 controlled inventory | +| 39 | P2-438 | P2 | 成本 / 付費 API / GPU 使用 guard | OpenClaw + DevOps | 待辦 | 成本預估、budget gate、rate limit、NIM / SaaS usage hard blocker | +| 40 | P2-439 | P2 | Runtime auto worker controlled dry-run | SRE + OpenClaw | 待辦 | dry-run / check-mode 產生 apply eligibility;verifier 必須獨立 | +| 41 | P2-440 | P2 | Runtime auto worker controlled canary | OpenClaw + SRE | 待受控路由 | 限 allowlisted low / medium / high action,需 rollback proof | +| 42 | P3-001 | P3 | Nemotron primary-source refresh | Market + NemoTron | 待辦 | 刷新 NVIDIA / repo / benchmark 來源,不用二手宣傳 | +| 43 | P3-002 | P3 | Nemotron 5 筆 smoke | QA + NemoTron | 待 P3-001 | 不觸 production;必要外部 API 需成本 / secret hard blocker 評估 | +| 44 | P3-003 | P3 | Nemotron 50 筆 replay controlled packet | QA + OpenClaw | 待 P3-002 | 建立回放包與評估準則,不自動升級 | +| 45 | P3-004 | P3 | OpenClaw 替代 / 升降級決策包 | OpenClaw + Market | 待回放 | 只有 scorecard 通過與 ADR 才能改角色 | +| 46 | P3-005 | P3 | 真實 production autonomous execution | 全 Agent | 受控正式執行推進 | 需 Telegram receipt、verifier、rollback、audit、24h 成功率;critical / break-glass 除外 | ### 0.5 可交給 AI Agents 的自動化範圍 | 範圍 | 可自動化項目 | 初始權限 | 升級條件 | |---|---|---|---| -| 主機 | CPU / RAM / disk / process / port / firewall drift 只讀巡檢 | read-only | maintenance window + owner approval 後才能 host write | -| K8s / ArgoCD | manifest diff、health readback、rollout risk、rollback plan | read-only | ArgoCD sync / kubectl action 需獨立 approval | -| 套件 / 依賴 | outdated、CVE、license、base image、lockfile drift | read-only proposal | PR 草案需 approval;merge / deploy 不自動 | -| 工具 | Gitea、Harbor、Prometheus、Alertmanager、SigNoz、Sentry、Langfuse、Open-WebUI 健康與版本 | read-only | route / receiver / workflow / registry write 需 approval | -| 服務 | API / Web / Worker / Scheduler / AI Router / provider route 健康 | read-only | restart / scale / provider switch 需 approval | -| 專案 | repo health、CI、docs freshness、LOGBOOK、ADR、runbook drift | docs / proposal | 跨 repo push、secret、deploy 需 approval | -| 網站前後台 | console error、overflow、i18n、redaction、UI truth chips | safe frontend patch | auth / CORS / public route / admin route 需 approval | -| 備份 / DR | freshness、restore rehearsal packet、offsite / escrow readiness | read-only | restore、credential escrow、offsite sync 需 approval | -| 報表 | 日報 / 週報 / 月報、Agent 工作量、風險分布、趨勢 | no-send draft | Telegram 實發需 owner approval | -| 學習 | KM 草稿、PlayBook trust proposal、失敗原因摘要、negative reinforcement | draft only | KM / PlayBook / audit DB 正式寫入需 gate | -| 市場情報 | Agent / SDK / model release watch、scorecard、replay 候選 | read-only | 新依賴、付費 API、模型路由改動需 approval | +| 主機 | CPU / RAM / disk / process / port / firewall drift、allowlisted cleanup / restart / config apply | read + controlled apply | reboot、node drain、不可逆 firewall cutover 才進 break-glass | +| K8s / ArgoCD | manifest diff、health readback、rollout risk、rollback plan、allowlisted sync / rollout / rollback | read + controlled apply | stateful destructive、namespace delete、cluster-wide RBAC 才進 break-glass | +| 套件 / 依賴 | outdated、CVE、license、base image、lockfile drift、patch PR / lockfile update dry-run | proposal + controlled patch | auto merge、major runtime switch、付費 provider 需 replay / canary | +| 工具 | Gitea、Harbor、Prometheus、Alertmanager、SigNoz、Sentry、Langfuse、Open-WebUI 健康與版本 | read + controlled config route | workflow 權限、runner secret、receiver 外送路由需硬閘 | +| 服務 | API / Web / Worker / Scheduler / AI Router / provider route 健康、allowlisted restart / scale / rollback | read + controlled apply | production provider route switch、成本上限、secret 需硬閘 | +| 專案 | repo health、CI、docs freshness、LOGBOOK、ADR、runbook drift、patch / PR draft | docs / controlled patch | force push、repo / refs 刪除、visibility、auto merge 需硬閘 | +| 網站前後台 | console error、overflow、i18n、redaction、UI truth chips、低風險前端修補 | safe frontend patch | auth / CORS / admin 權限 / 支付路由需 replay / verifier | +| 備份 / DR | freshness、restore rehearsal packet、offsite / escrow readiness、backup retry controlled route | read + controlled rehearsal | actual restore、retention prune、credential escrow 需 break-glass | +| 報表 | 日報 / 週報 / 月報、Agent 工作量、風險分布、趨勢、failure-only Telegram digest | controlled send when route ready | raw secret、未遮罩 payload、群組路由未知才阻擋 | +| 學習 | KM 草稿、PlayBook trust proposal、失敗原因摘要、negative reinforcement、成功 / 失敗 / rollback writeback | controlled writeback | 無 evidence ref、資料敏感或 trust source-of-truth 不可用才阻擋 | +| 市場情報 | Agent / SDK / model release watch、scorecard、replay 候選、整合 PR / sandbox proposal | read + controlled sandbox | 新付費 API、production route switch、OpenClaw replacement 需 replay / shadow / canary | ### 0.6 Agent 主動溝通、學習、記錄的機制 @@ -128,19 +175,19 @@ 3. `AgentHandoff`:OpenClaw、Hermes、NemoTron、SRE、Security、DevOps、Market、Telegram 之間的接手契約。 4. `Trace ID`:同一任務必須串起 MCP call、LLM call、tool call、approval、Telegram preview、browser smoke、LOGBOOK。 5. `RAG Memory`:分成 incident memory、runbook memory、market memory、dependency memory、operator feedback memory。 -6. `Critic Review`:所有自動建議要有 reviewer / critic 分數;低信心或高風險自動降級成人工審核。 -7. `Learning Proposal`:AI 可產生 PlayBook trust 更新提案,但正式寫入前必須有結果證據與 gate。 +6. `Critic Review`:所有自動建議要有 reviewer / critic 分數;低信心自動降級為 shadow / replay / smaller blast radius,不因 high risk 直接丟回人工。 +7. `Learning Proposal`:AI 可產生並受控寫入 PlayBook trust;正式寫入必須有 result evidence、rollback / verifier 結果與 redaction gate。 8. `User-Visible Proof`:治理頁、Agent activity feed、報表、Telegram digest 必須顯示「正在判斷什麼、卡在哪個 gate、下一步是什麼」。 ### 0.7 定期節奏 | 節奏 | 自動化內容 | 報告 | 審核規則 | |---|---|---|---| -| 每 15 分鐘 | 服務健康、告警重複、Telegram delivery gate、critical drift | action-required digest 草稿 | 只讀;不實發 | -| 每日 | Agent 工作量、失敗原因、低 / 中風險建議、版本新鮮度 | 日報 | 低風險可自動草稿;執行需 gate | -| 每週 | 市場主流 Agent / SDK / model watch、CVE / dependency drift、SLO 趨勢 | 週報 | 形成候選與優先順序 | -| 每月 | OpenClaw / Hermes / NemoTron 角色評估、成本、成功率、替代風險 | 月報 | 任何角色升降級需 ADR / scorecard | -| 每季 | AgentOps 架構成熟度、RAG 品質、MCP 安全、DR 演練 | 季度審查 | 可提出大版本 roadmap,不直接執行 | +| 每 15 分鐘 | 服務健康、告警重複、Telegram delivery gate、critical drift | action-required digest / failure-only controlled send | allowlisted low / medium / high 可 controlled apply;critical break-glass | +| 每日 | Agent 工作量、失敗原因、低 / 中 / 高風險受控執行、版本新鮮度 | 日報 | AI Agent 讀報後自動產生修補 / rollback / trust 更新候選 | +| 每週 | 市場主流 Agent / SDK / model watch、CVE / dependency drift、SLO 趨勢 | 週報 | high 可 controlled sandbox / replay / patch;OpenClaw replacement 需 scorecard | +| 每月 | OpenClaw / Hermes / NemoTron 角色評估、成本、成功率、替代風險 | 月報 | 自動調整閾值 / runbook / routing;核心替換走 ADR | +| 每季 | AgentOps 架構成熟度、RAG 品質、MCP 安全、DR 演練 | 季度審查 | 可提出大版本 roadmap;不可逆 / 成本 / secret 類走硬閘 | ## 1. 目前完成度 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 5714b91b0..6f48bb663 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 @@ -100,13 +100,29 @@ AWOOOI / AwoooP / IwoooS 不是單純監控頁、告警轉發器或資安清冊 | 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 | 沒有下一步,只說需要查看 | -| Gate | 顯示 `candidate_only`、`approval_required`、`runtime_write_gate=0/1` 與批准來源 | 把告警、UI 可見或 CD 成功當成授權 | +| 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 | 沒有驗證或只憑「看起來好了」 | | Learning / Writeback | Timeline、KM、PlayBook、trust score、報表與前台狀態回寫 | 沒有沉澱,下次仍重複人工判斷 | 因此,告警格式、前台卡片、Run、Work Item、Telegram、日週月報與資安總帳都必須優先回答:「AI 現在判讀到哪一段、下一步候選是什麼、哪個 Gate 阻擋、誰是 owner、驗證與學習是否回寫」。若無法回答,完成度不得被上修,只能列為 raw observability 或只讀證據。 +### 1.5 2026-06-26 受控自動執行授權修正 + +統帥已明確放寬 AI Agent 執行邊界:low / medium / high 風險事件,只要命中 allowlist / PlayBook / policy route,且 check-mode / dry-run / verifier preflight 通過,就應由 OpenClaw、Hermes、NemoTron、SRE Agent、DevOps Agent 進入受控自動執行;不得再預設回覆 `needs_human=true`、`manual_required`、`approval_required` 或「已轉人工」。 + +受控自動執行的最低條件如下: + +1. 動作已登記在 allowlisted catalog、Ansible PlayBook、MCP tool registry 或等價 policy route。 +2. `ansible-playbook --check --diff`、dry-run verifier、schema guard、blast-radius guard 與 target selector 都通過。 +3. rollback plan、post-apply verifier、Telegram 脫敏 evidence、KM / PlayBook trust writeback 路徑可讀。 +4. AwoooP status-chain、Work Item、Telegram card 與日 / 週 / 月報可顯示實際狀態:`controlled_apply_allowed`、`controlled_playbook_queue`、`runtime_write_gate=controlled`、`controlled_apply_executed` 或 `AI retry / rollback / repair queued`。 +5. 失敗時由 AI Agent 排入 rollback、PlayBook 修補、transport 修補、connector 修補與 verifier retry,不得只把事件丟回人工。 + +仍需硬阻擋的範圍只限不可逆、法務 / 財務 / 資安高敏類型:secret / token / private key 明文、DB DROP / TRUNCATE / restore / prune、reboot / node drain / 不可逆 firewall cutover、credentialed exploit、外部攻擊型 active scan、付費 provider / token 成本上限變更、OpenClaw 核心替換未經 replay / shadow / canary scorecard、force push / repo 或 ref 刪除、raw runtime secret volume 存取。這些事件應進 break-glass 或專屬授權,不得被一般 high-risk 放寬覆蓋。 + +任何文件、測試、UI 或 Telegram 模板若仍把 high 風險、P0 / P1 severity、host write、K8s write、Telegram Gateway queue、KM / PlayBook trust writeback 一律標為人工接手,且未命中上述硬阻擋,視為過時規範,必須改成受控自動化路徑。 + --- ## §2 當前架構診斷(鐵證 — 2026-04-15 深層病灶掃描) @@ -1915,18 +1931,18 @@ Phase 6 開始前 → ADR-086 批准 ### 7.3 風險矩陣與自動降級觸發條件 -| 風險場景 | 觸發條件 | 自動降級動作 | 人工介入需求 | +| 風險場景 | 觸發條件 | 自動降級動作 | 升級 / break-glass 需求 | |--------|---------|-----------|-----------| | **PreDecisionInvestigator 超時** | MCP 呼叫 > 3s / 超時 | 回 empty EvidenceSnapshot;記錄 `mcp_timeout` 指標 | 無(自動降級) | | **單 Agent 失敗** | Agent response > 5s 或 exception | Orchestrator 熔斷;Coordinator 直接用 Solver 輸出 | 無(自動降級) | | **學習呼叫超時** | `await asyncio.wait_for(..., timeout=30)` 超時 | 記錄 `learning_timeout_count`;主流程繼續 | 無(但需監控) | -| **Blast Radius 計算錯誤** | `blast_radius_calculator` 拋 exception | 視為 > 50(最嚴格分級);轉人工確認 | 需人工確認後才執行 | -| **決策 SLO 違反** | auto_execute 成功率 < 85% 或推翻率 > 20% | confidence 閾值 +0.05;連 2 週 → 全系統保守模式 | 保守模式需人工解除 | -| **Trust Drift 極端** | > 70% Playbook trust < 0.3 或全部 > 0.9 | Tier 2 告警 SRE + 觸發 root cause analysis job | 需 SRE 介入 | -| **離線回放一致率暴跌** | 單週下降 > 20% | 立即 rollback + Tier 0 告警 | Tier 0 需人工處理 | -| **Declarative GitOps PR 失敗** | Gitea 不可用 | fallback 直接執行 + Tier 1 告警 SRE | SRE 需確認執行 | +| **Blast Radius 計算錯誤** | `blast_radius_calculator` 拋 exception | 視為 > 50;僅允許 allowlisted check-mode + controlled apply,否則進 break-glass queue | 不預設人工;命中不可逆硬阻擋才 break-glass | +| **決策 SLO 違反** | auto_execute 成功率 < 85% 或推翻率 > 20% | confidence 閾值 +0.05;連 2 週 → AI 保守模式 + replay / shadow 重評估 | 解除保守模式需有 replay / shadow / verifier 證據 | +| **Trust Drift 極端** | > 70% Playbook trust < 0.3 或全部 > 0.9 | Tier 2 告警 SRE + AI root cause analysis job + PlayBook trust 修補候選 | 只有 trust writeback source-of-truth 不可用才 break-glass | +| **離線回放一致率暴跌** | 單週下降 > 20% | 立即 rollback + Tier 0 告警 + AI replay bisect | Tier 0 進 AI rollback / verifier lane;不可逆才 break-glass | +| **Declarative GitOps PR 失敗** | Gitea 不可用 | fallback 走 allowlisted direct controlled apply + Tier 1 告警 SRE | SRE Agent 需補 verifier / KM 證據,不預設人工接手 | | **Dynamic Baseline 啟動失敗** | Holt-Winters 訓練資料不足(< 7 天)| fallback 回 Prometheus 靜態 rules;log `baseline_not_ready` | 無(自動 fallback) | -| **Model Rollback 失敗** | rollback_manager 無可用 checkpoint | Tier 0 告警 + 關閉所有 auto_execute | 緊急!需立即人工 | +| **Model Rollback 失敗** | rollback_manager 無可用 checkpoint | Tier 0 告警 + 關閉相關 provider route 的 auto_execute + AI 產生 replacement / degraded route candidate | 無可用 checkpoint 且涉及 production provider route 時進 break-glass | ### 7.4 北極星達成路徑(從現況到目標) @@ -1973,6 +1989,13 @@ Phase 6 完成後 --- +### 2026-06-26 18:07 (台北) — §1 / §7 — 新增 P2-414 受控自動執行授權修正 — 將 low / medium / high 風險從預設人工改為 AI Agent 受控自動處理 + +- 更新 §1.4 產品契約 Gate:以 `controlled_apply_allowed`、`runtime_write_gate=controlled/0`、check-mode、allowlist route 與 hard blocker 作為 runtime 授權語言,不再把 high 風險或 P0 / P1 severity 預設視為人工接手。 +- 新增 §1.5:明確規定 low / medium / high 風險命中 allowlist / PlayBook / policy route 且 check-mode / dry-run / verifier preflight 通過時,由 OpenClaw、Hermes、NemoTron、SRE Agent、DevOps Agent 進入受控自動執行。 +- 同步列出仍需 hard blocker / break-glass 的範圍:secret / token 明文、不可逆資料破壞、reboot / node drain、攻擊型 active scan、付費 provider / 成本上限、OpenClaw 核心替換未完成 replay / shadow / canary、force push / repo / ref 刪除與 raw runtime secret volume。 +- 更新 §7.3 風險矩陣:把「轉人工 / 需人工」改為 AI rollback、replay、verifier、PlayBook trust 修補與 break-glass 條件,避免舊降級語言再次把自動化工作推回 operator。 + ### 2026-06-14 08:20 (台北) — §3.2 / §5 — 本地完成 P2-134 release authorization hold — 把 final candidate 讀回轉成釋出授權保留 - 新增 `ai_agent_result_capture_release_authorization_hold_v1` schema / committed snapshot / loader / API / 測試,承接 P2-133 final release candidate readback,定義 5 個 release authorization hold、5 個 rollback authorization hold、5 個 release window hold、5 個 live-apply authorization hold、6 個 blocked authorization transition 與 5 個 operator action。