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。