diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index b98c902e..097e7815 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -53,6 +53,9 @@ from src.services.backup_dr_readiness_matrix import ( from src.services.backup_notification_policy import ( load_latest_backup_notification_policy, ) +from src.services.backup_restore_drill_approval_package_template import ( + load_latest_backup_restore_drill_approval_package_template, +) from src.services.package_supply_chain_inventory import ( load_latest_package_supply_chain_inventory, ) @@ -551,6 +554,35 @@ async def get_backup_notification_policy() -> dict[str, Any]: ) from exc +@router.get( + "/backup-restore-drill-approval-package-template", + response_model=dict[str, Any], + summary="取得 Backup / DR 復原演練批准包模板", + description=( + "讀取最新已提交的 Backup / DR restore drill、credential escrow review、" + "K8s resource recovery、observability recovery 與 route reconstruction 批准包模板;" + "此端點只回傳 read-only template,不執行 backup、restore、offsite sync、" + "不寫 credential marker、不改排程、不寫 workflow、不送 Telegram 測試通知、" + "不輸出 secret 明文、不做破壞性 prune、不呼叫付費 API、不建立 shadow/canary、不改生產路由。" + ), +) +async def get_backup_restore_drill_approval_package_template() -> dict[str, Any]: + """Return the latest read-only Backup / DR restore drill approval package template.""" + try: + return await asyncio.to_thread(load_latest_backup_restore_drill_approval_package_template) + except FileNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(exc), + ) from exc + except (json.JSONDecodeError, ValueError) as exc: + logger.error("backup_restore_drill_approval_package_template_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Backup / DR 復原演練批准包模板快照無效", + ) from exc + + @router.get( "/package-supply-chain-inventory", response_model=dict[str, Any], diff --git a/apps/api/src/services/backup_restore_drill_approval_package_template.py b/apps/api/src/services/backup_restore_drill_approval_package_template.py new file mode 100644 index 00000000..1ab673b0 --- /dev/null +++ b/apps/api/src/services/backup_restore_drill_approval_package_template.py @@ -0,0 +1,145 @@ +""" +Backup / DR restore drill approval package template snapshot. + +Loads the latest committed, read-only approval package template for restore +drills, credential escrow review, K8s resource recovery, observability +recovery, and route reconstruction. The template never runs backups, restores, +offsite sync, credential marker writes, schedule changes, workflow writes, +Telegram test notifications, destructive prune, secret plaintext export, paid +API calls, shadow/canary, or production routing changes. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from src.services.snapshot_paths import default_evaluations_dir + +_DEFAULT_EVALUATIONS_DIR = default_evaluations_dir(Path(__file__)) +_SNAPSHOT_PATTERN = "backup_restore_drill_approval_package_template_*.json" +_SCHEMA_VERSION = "backup_restore_drill_approval_package_template_v1" + + +def load_latest_backup_restore_drill_approval_package_template( + evaluations_dir: Path | None = None, +) -> dict[str, Any]: + """Load the newest committed Backup / DR restore drill approval package template.""" + directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR + candidates = sorted(directory.glob(_SNAPSHOT_PATTERN)) + if not candidates: + raise FileNotFoundError( + f"no Backup / DR restore drill approval package template snapshots found in {directory}" + ) + + latest = candidates[-1] + with latest.open(encoding="utf-8") as handle: + payload = json.load(handle) + + if not isinstance(payload, dict): + raise ValueError(f"{latest}: expected JSON object") + _require_schema(payload, _SCHEMA_VERSION, str(latest)) + _require_read_only_boundaries(payload, str(latest)) + _require_operation_boundaries(payload, str(latest)) + _require_rollup_consistency(payload, str(latest)) + return payload + + +def _require_schema(payload: dict[str, Any], expected: str, label: str) -> None: + actual = payload.get("schema_version") + if actual != expected: + raise ValueError(f"{label}: expected schema_version={expected}, got {actual!r}") + + +def _require_read_only_boundaries(payload: dict[str, Any], label: str) -> None: + program_status = payload.get("program_status") or {} + if program_status.get("read_only_mode") is not True: + raise ValueError(f"{label}: program_status.read_only_mode must be true") + + boundaries = payload.get("approval_boundaries") or {} + blocked_flags = { + "sdk_installation_allowed", + "paid_api_call_allowed", + "shadow_or_canary_allowed", + "production_routing_allowed", + "destructive_operation_allowed", + "restore_execution_allowed", + "offsite_sync_execution_allowed", + "credential_marker_write_allowed", + } + allowed = sorted(flag for flag in blocked_flags if boundaries.get(flag) is not False) + if allowed: + raise ValueError(f"{label}: approval boundaries must remain false: {allowed}") + + +def _require_operation_boundaries(payload: dict[str, Any], label: str) -> None: + boundaries = payload.get("operation_boundaries") or {} + if boundaries.get("read_only_template_allowed") is not True: + raise ValueError(f"{label}: read_only_template_allowed must be true") + + blocked_flags = { + "backup_execution_allowed", + "restore_execution_allowed", + "offsite_sync_execution_allowed", + "credential_marker_write_allowed", + "schedule_change_allowed", + "workflow_write_allowed", + "telegram_test_notification_allowed", + "destructive_prune_allowed", + "secret_plaintext_allowed", + "production_routing_allowed", + "sdk_installation_allowed", + "paid_api_call_allowed", + "shadow_or_canary_allowed", + } + allowed = sorted(flag for flag in blocked_flags if boundaries.get(flag) is not False) + if allowed: + raise ValueError(f"{label}: operation boundaries must remain false: {allowed}") + + +def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None: + templates = payload.get("package_templates") or [] + rollups = payload.get("rollups") or {} + if rollups.get("total_templates") != len(templates): + raise ValueError(f"{label}: rollups.total_templates must match package_templates") + + ready_ids = { + template.get("template_id") + for template in templates + if template.get("status") == "template_ready" + } + if set(rollups.get("template_ready_ids") or []) != ready_ids: + raise ValueError(f"{label}: rollups.template_ready_ids must match template_ready templates") + + hitl_ids = { + template.get("template_id") + for template in templates + if "HITL approval" in (template.get("manual_approvals") or []) + } + if set(rollups.get("hitl_required_template_ids") or []) != hitl_ids: + raise ValueError(f"{label}: rollups.hitl_required_template_ids must match HITL templates") + + blocked_target_ids = _target_ids_by_readiness(templates, "blocked") + if set(rollups.get("blocked_source_target_ids") or []) != blocked_target_ids: + raise ValueError( + f"{label}: rollups.blocked_source_target_ids must match blocked source targets" + ) + + action_required_target_ids = _target_ids_by_readiness(templates, "action_required") + if set(rollups.get("action_required_source_target_ids") or []) != action_required_target_ids: + raise ValueError( + f"{label}: rollups.action_required_source_target_ids must match action_required source targets" + ) + + if (payload.get("decision_gate_contract") or {}).get("hitl_required") is not True: + raise ValueError(f"{label}: decision_gate_contract.hitl_required must be true") + + +def _target_ids_by_readiness(templates: list[dict[str, Any]], readiness: str) -> set[str]: + return { + target.get("target_id") + for template in templates + for target in template.get("source_target_statuses", []) + if target.get("readiness") == readiness + } diff --git a/apps/api/tests/test_ai_agent_automation_backlog_snapshot_api.py b/apps/api/tests/test_ai_agent_automation_backlog_snapshot_api.py index 10990379..f474a177 100644 --- a/apps/api/tests/test_ai_agent_automation_backlog_snapshot_api.py +++ b/apps/api/tests/test_ai_agent_automation_backlog_snapshot_api.py @@ -18,11 +18,12 @@ def test_ai_agent_automation_backlog_snapshot_endpoint_returns_committed_snapsho assert data["schema_version"] == "ai_agent_automation_backlog_v1" assert data["program_status"]["overall_completion_percent"] == 100 assert data["program_status"]["read_only_mode"] is True - assert data["program_status"]["current_task_id"] == "P1-104" - assert data["program_status"]["next_task_id"] == "P1-105" - assert data["rollups"]["total_items"] == len(data["backlog_items"]) == 19 - assert data["rollups"]["by_priority"]["P1"] == 17 - assert data["rollups"]["by_status"]["done"] == 12 + assert data["program_status"]["current_task_id"] == "P1-105" + assert data["program_status"]["next_task_id"] == "P1-106" + assert data["rollups"]["total_items"] == len(data["backlog_items"]) == 20 + assert data["rollups"]["by_priority"]["P1"] == 18 + assert data["rollups"]["by_status"]["done"] == 13 + assert data["rollups"]["by_gate_status"]["read_only_allowed"] == 17 assert data["approval_boundaries"]["sdk_installation_allowed"] is False assert data["approval_boundaries"]["paid_api_call_allowed"] is False assert data["approval_boundaries"]["production_routing_allowed"] is False @@ -31,4 +32,5 @@ def test_ai_agent_automation_backlog_snapshot_endpoint_returns_committed_snapsho assert any(item["item_id"] == "AUTO-P1-206" for item in data["backlog_items"]) assert any(item["item_id"] == "AUTO-P1-103" for item in data["backlog_items"]) assert any(item["item_id"] == "AUTO-P1-104" for item in data["backlog_items"]) + assert any(item["item_id"] == "AUTO-P1-105" for item in data["backlog_items"]) assert any(item["item_id"] == "AUTO-P3-001" for item in data["backlog_items"]) diff --git a/apps/api/tests/test_ai_agent_automation_inventory_snapshot_api.py b/apps/api/tests/test_ai_agent_automation_inventory_snapshot_api.py index dbcbb078..ba6cb896 100644 --- a/apps/api/tests/test_ai_agent_automation_inventory_snapshot_api.py +++ b/apps/api/tests/test_ai_agent_automation_inventory_snapshot_api.py @@ -18,8 +18,8 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps assert data["schema_version"] == "ai_agent_automation_inventory_snapshot_v1" assert data["program_status"]["overall_completion_percent"] == 100 assert data["program_status"]["read_only_mode"] is True - assert data["program_status"]["current_task_id"] == "P1-104" - assert data["program_status"]["next_task_id"] == "P1-105" + assert data["program_status"]["current_task_id"] == "P1-105" + assert data["program_status"]["next_task_id"] == "P1-106" assert data["approval_boundaries"]["sdk_installation_allowed"] is False assert data["approval_boundaries"]["paid_api_call_allowed"] is False assert data["approval_boundaries"]["production_routing_allowed"] is False @@ -29,6 +29,7 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps assert any(task["task_id"] == "P1-206" for task in data["tasks"]) assert any(task["task_id"] == "P1-103" for task in data["tasks"]) assert any(task["task_id"] == "P1-104" for task in data["tasks"]) + assert any(task["task_id"] == "P1-105" for task in data["tasks"]) assert any(evidence["evidence_id"] == "dependency_risk_policy_api" for evidence in data["evidence"]) assert any(evidence["evidence_id"] == "dependency_drift_check_plan_api" for evidence in data["evidence"]) assert any( @@ -37,3 +38,7 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps ) assert any(evidence["evidence_id"] == "backup_notification_policy_api" for evidence in data["evidence"]) assert any(evidence["evidence_id"] == "backup_dr_evidence_ui" for evidence in data["evidence"]) + assert any( + evidence["evidence_id"] == "backup_restore_drill_approval_package_template_api" + for evidence in data["evidence"] + ) diff --git a/apps/api/tests/test_backup_restore_drill_approval_package_template.py b/apps/api/tests/test_backup_restore_drill_approval_package_template.py new file mode 100644 index 00000000..7a3aa162 --- /dev/null +++ b/apps/api/tests/test_backup_restore_drill_approval_package_template.py @@ -0,0 +1,236 @@ +from __future__ import annotations + +import json + +import pytest + +from src.services.backup_restore_drill_approval_package_template import ( + load_latest_backup_restore_drill_approval_package_template, +) + + +def test_load_latest_backup_restore_drill_approval_package_template_reads_newest_file(tmp_path): + older = _snapshot(generated_at="2026-06-04T00:00:00+08:00", completion=75) + newer = _snapshot(generated_at="2026-06-05T00:00:00+08:00", completion=100) + (tmp_path / "backup_restore_drill_approval_package_template_2026-06-04.json").write_text( + json.dumps(older), + encoding="utf-8", + ) + (tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text( + json.dumps(newer), + encoding="utf-8", + ) + + loaded = load_latest_backup_restore_drill_approval_package_template(tmp_path) + + assert loaded["generated_at"] == "2026-06-05T00:00:00+08:00" + assert loaded["program_status"]["overall_completion_percent"] == 100 + assert loaded["rollups"]["total_templates"] == 2 + assert loaded["operation_boundaries"]["restore_execution_allowed"] is False + + +def test_backup_restore_drill_approval_package_template_requires_read_only_mode(tmp_path): + snapshot = _snapshot() + snapshot["program_status"]["read_only_mode"] = False + (tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="read_only_mode"): + load_latest_backup_restore_drill_approval_package_template(tmp_path) + + +def test_backup_restore_drill_approval_package_template_requires_blocked_operations(tmp_path): + snapshot = _snapshot() + snapshot["operation_boundaries"]["restore_execution_allowed"] = True + (tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="operation boundaries"): + load_latest_backup_restore_drill_approval_package_template(tmp_path) + + +def test_backup_restore_drill_approval_package_template_requires_blocked_approval_boundaries(tmp_path): + snapshot = _snapshot() + snapshot["approval_boundaries"]["credential_marker_write_allowed"] = True + (tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="approval boundaries"): + load_latest_backup_restore_drill_approval_package_template(tmp_path) + + +def test_backup_restore_drill_approval_package_template_requires_total_consistency(tmp_path): + snapshot = _snapshot() + snapshot["rollups"]["total_templates"] = 999 + (tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="total_templates"): + load_latest_backup_restore_drill_approval_package_template(tmp_path) + + +def test_backup_restore_drill_approval_package_template_requires_ready_id_consistency(tmp_path): + snapshot = _snapshot() + snapshot["rollups"]["template_ready_ids"] = [] + (tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="template_ready_ids"): + load_latest_backup_restore_drill_approval_package_template(tmp_path) + + +def test_backup_restore_drill_approval_package_template_requires_hitl_consistency(tmp_path): + snapshot = _snapshot() + snapshot["rollups"]["hitl_required_template_ids"] = [] + (tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="hitl_required_template_ids"): + load_latest_backup_restore_drill_approval_package_template(tmp_path) + + +def test_backup_restore_drill_approval_package_template_requires_source_target_rollups(tmp_path): + snapshot = _snapshot() + snapshot["rollups"]["blocked_source_target_ids"] = [] + (tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="blocked_source_target_ids"): + load_latest_backup_restore_drill_approval_package_template(tmp_path) + + +def test_backup_restore_drill_approval_package_template_requires_hitl_gate(tmp_path): + snapshot = _snapshot() + snapshot["decision_gate_contract"]["hitl_required"] = False + (tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="hitl_required"): + load_latest_backup_restore_drill_approval_package_template(tmp_path) + + +def test_backup_restore_drill_approval_package_template_fails_when_missing(tmp_path): + with pytest.raises(FileNotFoundError): + load_latest_backup_restore_drill_approval_package_template(tmp_path) + + +def _snapshot( + *, + generated_at: str = "2026-06-05T00:00:00+08:00", + completion: int = 100, +) -> dict: + return { + "schema_version": "backup_restore_drill_approval_package_template_v1", + "generated_at": generated_at, + "source_refs": ["docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json"], + "program_status": { + "overall_completion_percent": completion, + "current_priority": "P1", + "current_task_id": "P1-105", + "next_task_id": "P1-106", + "read_only_mode": True, + }, + "rollups": { + "total_templates": 2, + "by_domain": {"database_restore": 1, "k8s_resource_restore": 1}, + "template_ready_ids": [ + "database_restore_drill_approval_package", + "velero_k8s_restore_drill_package", + ], + "hitl_required_template_ids": [ + "database_restore_drill_approval_package", + "velero_k8s_restore_drill_package", + ], + "blocked_source_target_ids": ["configs_capture"], + "action_required_source_target_ids": ["velero_k8s_resources"], + }, + "approval_fields": [ + { + "field_id": "manual_approval", + "required": True, + "description": "approval", + } + ], + "package_templates": [ + _template( + "database_restore_drill_approval_package", + "database_restore", + [{"target_id": "configs_capture", "readiness": "blocked"}], + ), + _template( + "velero_k8s_restore_drill_package", + "k8s_resource_restore", + [{"target_id": "velero_k8s_resources", "readiness": "action_required"}], + ), + ], + "decision_gate_contract": { + "openclaw_role": "arbitrate", + "hermes_role": "summarize", + "nemotron_role": "offline review", + "hitl_required": True, + "expires_after": "7 days", + "invalidated_by": ["snapshot change"], + }, + "operation_boundaries": { + "read_only_template_allowed": True, + "backup_execution_allowed": False, + "restore_execution_allowed": False, + "offsite_sync_execution_allowed": False, + "credential_marker_write_allowed": False, + "schedule_change_allowed": False, + "workflow_write_allowed": False, + "telegram_test_notification_allowed": False, + "destructive_prune_allowed": False, + "secret_plaintext_allowed": False, + "production_routing_allowed": False, + "sdk_installation_allowed": False, + "paid_api_call_allowed": False, + "shadow_or_canary_allowed": False, + }, + "approval_boundaries": { + "sdk_installation_allowed": False, + "paid_api_call_allowed": False, + "shadow_or_canary_allowed": False, + "production_routing_allowed": False, + "destructive_operation_allowed": False, + "restore_execution_allowed": False, + "offsite_sync_execution_allowed": False, + "credential_marker_write_allowed": False, + }, + } + + +def _template(template_id: str, domain: str, source_target_statuses: list[dict]) -> dict: + return { + "template_id": template_id, + "domain": domain, + "status": "template_ready", + "owner_agent": "openclaw", + "purpose": "approval package", + "source_target_statuses": source_target_statuses, + "required_evidence": ["docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json"], + "required_decisions": ["approve or reject"], + "required_prechecks": ["precheck"], + "required_tests": ["schema validation"], + "rollback_requirements": ["rollback plan"], + "abort_criteria": ["abort"], + "manual_approvals": ["OpenClaw arbitration", "HITL approval"], + "prohibited_without_approval": ["restore execution"], + "evidence_refs": ["docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json"], + } diff --git a/apps/api/tests/test_backup_restore_drill_approval_package_template_api.py b/apps/api/tests/test_backup_restore_drill_approval_package_template_api.py new file mode 100644 index 00000000..680b96d5 --- /dev/null +++ b/apps/api/tests/test_backup_restore_drill_approval_package_template_api.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.api.v1.agents import router + + +def test_backup_restore_drill_approval_package_template_endpoint_returns_committed_snapshot(): + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.get("/api/v1/agents/backup-restore-drill-approval-package-template") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "backup_restore_drill_approval_package_template_v1" + assert data["program_status"]["overall_completion_percent"] == 100 + assert data["program_status"]["read_only_mode"] is True + assert data["program_status"]["current_task_id"] == "P1-105" + assert data["program_status"]["next_task_id"] == "P1-106" + assert data["rollups"]["total_templates"] == len(data["package_templates"]) == 6 + assert len(data["rollups"]["hitl_required_template_ids"]) == 6 + assert data["rollups"]["blocked_source_target_ids"] == [ + "configs_capture", + "credential_escrow_markers", + ] + assert data["rollups"]["action_required_source_target_ids"] == [ + "signoz", + "velero_k8s_resources", + ] + assert data["operation_boundaries"]["read_only_template_allowed"] is True + assert data["operation_boundaries"]["backup_execution_allowed"] is False + assert data["operation_boundaries"]["restore_execution_allowed"] is False + assert data["operation_boundaries"]["offsite_sync_execution_allowed"] is False + assert data["operation_boundaries"]["credential_marker_write_allowed"] is False + assert data["operation_boundaries"]["workflow_write_allowed"] is False + assert data["operation_boundaries"]["telegram_test_notification_allowed"] is False + assert data["operation_boundaries"]["secret_plaintext_allowed"] is False + assert data["operation_boundaries"]["production_routing_allowed"] is False + assert data["decision_gate_contract"]["hitl_required"] is True + assert any( + template["template_id"] == "credential_escrow_review_package" + for template in data["package_templates"] + ) + assert any( + template["template_id"] == "velero_k8s_restore_drill_package" + for template in data["package_templates"] + ) 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 d30dfee4..ad007029 100644 --- a/docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md +++ b/docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md @@ -10,7 +10,7 @@ |---|---:|---|---| | Agent 市場治理 | 72% | 進行中 | `agent_market_governance_snapshot_v1`、API、UI 分頁、每週觀察流程 | | Nemotron 實際整合應用 | 30% | 完整回放前仍被關卡擋下 | `blocked_needs_evidence`,下一關是 `refresh_source_evidence_then_5_record_smoke_only` | -| 工具 / 服務 / 套件 AI 自動化 | 100% | P0 已完成,P1 套件 / 供應鏈主線已完成;備份通知政策已完成,下一主線是 DR UI 證據 | 狀態分類、盤點 schema、權限矩陣、靜態盤點種子、只讀 API、UI 骨架、驗證、自動化待辦 schema / 快照 / API / 分組 UI、Backup / DR 目標盤點、準備度矩陣、備份通知政策、Python 套件 / 供應鏈只讀基線、JS pnpm/npm 只讀基線、Docker build surface 只讀基線、CVE / license / drift 嚴重度政策、定期依賴漂移與外部資料來源檢查設計、依賴升級批准包模板已完成 | +| 工具 / 服務 / 套件 AI 自動化 | 100% | P0 已完成,P1 套件 / 供應鏈主線已完成;備份 / DR 主線已完成到復原演練批准包,下一主線是異地 / escrow 準備度顯示 | 狀態分類、盤點 schema、權限矩陣、靜態盤點種子、只讀 API、UI 骨架、驗證、自動化待辦 schema / 快照 / API / 分組 UI、Backup / DR 目標盤點、準備度矩陣、備份通知政策、Backup / DR 證據 UI、復原演練批准包模板、Python 套件 / 供應鏈只讀基線、JS pnpm/npm 只讀基線、Docker build surface 只讀基線、CVE / license / drift 嚴重度政策、定期依賴漂移與外部資料來源檢查設計、依賴升級批准包模板已完成 | | 本工作清單與分析報告 | 100% | 已完成 | 本 MD 文件 | 整體計畫完成度:**100%**。 @@ -250,9 +250,9 @@ Schema 目標: 快照內容: -- 總項目:`18` -- P1:`16`、P2:`1`、P3:`1` -- 只讀允許:`15` +- 總項目:`20` +- P1:`18`、P2:`1`、P3:`1` +- 只讀允許:`17` - 生產變更阻擋:`1` - 費用批准需求:`1` - 證據不足阻擋:`1` @@ -270,6 +270,8 @@ Schema 目標: - P1-205:定期依賴漂移與外部資料來源檢查設計。已完成。 - P1-206:依賴升級、digest pin、publish boundary 批准包模板。已完成。 - P1-103:備份通知政策。已完成。 +- P1-104:Backup / DR 證據 UI。已完成。 +- P1-105:復原演練批准包模板。已完成。 ### P1-303 自動化待辦只讀 API 摘要 @@ -732,6 +734,58 @@ API: - 本地 390px mobile `/zh-TW/governance?tab=automation-inventory&_v=p1-104-backup-evidence-local`:Backup / DR 證據、準備度矩陣、通知政策、成功抑制、即時升級、Gitea 與 SignOz disruptive guard 可見;無載入錯誤;`horizontalOverflow=-6`。 - 截圖:`/tmp/awoooi-p1-104-backup-evidence-local-desktop.png`、`/tmp/awoooi-p1-104-backup-evidence-local-mobile.png`。 +### P1-105 復原演練批准包模板摘要 + +正式 JSON Schema: + +- `docs/schemas/backup_restore_drill_approval_package_template_v1.schema.json` + +正式 JSON Snapshot: + +- `docs/evaluations/backup_restore_drill_approval_package_template_2026-06-05.json` + +API: + +- `GET /api/v1/agents/backup-restore-drill-approval-package-template` + +模板內容: + +- 批准包模板:`6` +- database restore:`1` +- configuration restore:`1` +- credential escrow:`1` +- K8s resource restore:`1` +- observability restore:`1` +- route reconstruction:`1` +- blocked source targets:`configs_capture`、`credential_escrow_markers` +- action-required source targets:`signoz`、`velero_k8s_resources` + +核心裁決: + +- P1-105 只產生 restore drill / escrow review / route reconstruction 的批准包模板,不授權任何實際 restore。 +- 6 類模板全部要求 OpenClaw 仲裁與 HITL;Hermes 可起草,Nemotron 只可離線檢查 sanitized 演練計畫完整性。 +- `configs_capture` 與 `credential_escrow_markers` 仍為 blocked;`signoz` 與 `velero_k8s_resources` 仍為 action_required,不得被 UI 或 API 解讀為 ready。 +- 每個批准包都必須列出 operator、維護窗口、source backup ref、target environment、blast radius、precheck evidence、abort criteria、rollback 與 post-verification。 + +實作邊界: + +- 不執行 backup。 +- 不執行 restore。 +- 不執行 offsite sync。 +- 不寫 credential marker。 +- 不改排程、不寫 workflow。 +- 不發 Telegram 測試訊息。 +- 不輸出 secret 明文。 +- 不做 destructive prune。 +- 不呼叫付費 API、不建立 shadow / canary、不改生產路由。 + +驗證: + +- `python3 -m json.tool docs/schemas/backup_restore_drill_approval_package_template_v1.schema.json` 通過。 +- `python3 -m json.tool docs/evaluations/backup_restore_drill_approval_package_template_2026-06-05.json` 通過。 +- `PYTHONDONTWRITEBYTECODE=1 apps/api/.venv/bin/python -m pytest apps/api/tests/test_backup_restore_drill_approval_package_template.py apps/api/tests/test_backup_restore_drill_approval_package_template_api.py -q`:`11 passed`。 +- `python3 -m py_compile apps/api/src/services/backup_restore_drill_approval_package_template.py apps/api/src/api/v1/agents.py apps/api/tests/test_backup_restore_drill_approval_package_template.py apps/api/tests/test_backup_restore_drill_approval_package_template_api.py` 通過。 + ### P0 - 治理與 Inventory 基礎 | ID | 狀態 | % | 負責 Agent | 任務 | 產出 | 關卡 | @@ -765,7 +819,7 @@ API: | P1-102 | 完成 | 100 | OpenClaw | 顯示備份新鮮度、完整性、復原演練狀態 | `docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json` | 不執行 restore | | P1-103 | 完成 | 100 | Hermes | 對齊備份通知政策 | `docs/evaluations/backup_notification_policy_2026-06-04.json` | 不發成功洗版 | | P1-104 | 完成 | 100 | OpenClaw | 在 AwoooP / governance UI 加備份證據 | `/zh-TW/governance?tab=automation-inventory` | 只讀 + 瀏覽器驗證 | -| P1-105 | 待辦 | 0 | OpenClaw | 定義復原演練批准包 | 復原計畫範本 | 人工批准 | +| P1-105 | 完成 | 100 | OpenClaw | 定義復原演練批准包 | `docs/evaluations/backup_restore_drill_approval_package_template_2026-06-05.json` | 只讀模板 + 人工批准 | | P1-106 | 待辦 | 0 | Hermes | 顯示異地 / escrow 準備度狀態 | DR 準備度區塊 | 不暴露 credential | ### P1 - 套件與供應鏈自動化 @@ -911,19 +965,18 @@ API: ```text 進度:100%。 目前優先級:P1。 -目前任務:P1-104 在 AwoooP / governance UI 加備份證據。 +目前任務:P1-105 定義復原演練批准包。 狀態變更:待辦 -> 完成。 -證據:typecheck 通過;本地 desktop 與 390px mobile governance automation-inventory tab 驗證 Backup / DR 證據、準備度矩陣、通知政策、成功抑制、即時升級、Gitea 與 SignOz disruptive guard 可見;無載入錯誤;horizontalOverflow <= 0。 -阻擋:無;backup、restore、offsite sync、credential marker、排程、workflow、Telegram 測試通知仍未批准。 -下一步:P1-105 定義復原演練批准包。 +證據:backup_restore_drill_approval_package_template schema / snapshot JSON parse 通過;service + API tests 11 passed;py_compile 通過;API 端點為 GET /api/v1/agents/backup-restore-drill-approval-package-template。 +阻擋:無;backup、restore、offsite sync、credential marker、排程、workflow、Telegram 測試通知、secret 明文、生產路由仍未批准。 +下一步:P1-106 顯示異地 / escrow 準備度狀態。 ``` ## 13. 立即執行順序 -1. P1-105:定義復原演練批准包。 -2. P1-106:顯示異地 / escrow 準備度狀態。 -3. P1-305 / P1-306:補每個任務的批准邊界與進度彙總細節。 -4. P2 / P3 必須等 P1 可見且關卡穩定後再做。 +1. P1-106:顯示異地 / escrow 準備度狀態。 +2. P1-305 / P1-306:補每個任務的批准邊界與進度彙總細節。 +3. P2 / P3 必須等 P1 可見且關卡穩定後再做。 ## 14. 目前風險 diff --git a/docs/evaluations/ai_agent_automation_backlog_2026-06-04.json b/docs/evaluations/ai_agent_automation_backlog_2026-06-04.json index f5feda4b..930b5cd8 100644 --- a/docs/evaluations/ai_agent_automation_backlog_2026-06-04.json +++ b/docs/evaluations/ai_agent_automation_backlog_2026-06-04.json @@ -1,34 +1,34 @@ { "schema_version": "ai_agent_automation_backlog_v1", - "generated_at": "2026-06-04T21:42:18+08:00", + "generated_at": "2026-06-05T05:36:00+08:00", "source_inventory_snapshot_ref": "docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json", "program_status": { "overall_completion_percent": 100, "current_priority": "P1", - "current_task_id": "P1-104", - "next_task_id": "P1-105", + "current_task_id": "P1-105", + "next_task_id": "P1-106", "read_only_mode": true }, "rollups": { - "total_items": 19, + "total_items": 20, "by_priority": { - "P1": 17, + "P1": 18, "P2": 1, "P3": 1 }, "by_status": { "planned": 7, - "done": 12 + "done": 13 }, "by_gate_status": { - "read_only_allowed": 16, + "read_only_allowed": 17, "production_change_blocked": 1, "cost_approval_required": 1, "blocked_by_evidence": 1 }, "by_owner_agent": { "hermes": 10, - "openclaw": 8, + "openclaw": 9, "nemotron": 1 } }, @@ -307,6 +307,33 @@ ], "next_review": "P1-104" }, + { + "item_id": "AUTO-P1-105", + "priority": "P1", + "status": "done", + "workstream_id": "WS4", + "source_asset_id": "backup_restore_drill_approval_package_template", + "source_signal_kind": "approval_boundary", + "title": "定義復原演練批准包", + "owner_agent": "openclaw", + "recommended_action": "建立 read-only restore drill / escrow review approval package template,要求 evidence、precheck、blast radius、abort、rollback、OpenClaw 仲裁與 HITL;模板本身不執行 restore。", + "action_class": "backup_restore_approval_template", + "gate_status": "read_only_allowed", + "risk_level": "critical", + "evidence_refs": [ + "docs/schemas/backup_restore_drill_approval_package_template_v1.schema.json", + "docs/evaluations/backup_restore_drill_approval_package_template_2026-06-05.json", + "GET /api/v1/agents/backup-restore-drill-approval-package-template" + ], + "acceptance_criteria": [ + "不執行 backup / restore / offsite sync", + "不寫 credential marker、不輸出 secret 明文", + "不改排程、不寫 workflow、不發 Telegram 測試通知", + "6 類批准包模板全部要求 OpenClaw 仲裁與 HITL", + "blocked / action-required 目標必須維持 blocked 或 action-required,不得被 UI 解讀為 ready" + ], + "next_review": "P1-105" + }, { "item_id": "AUTO-P1-201", "priority": "P1", diff --git a/docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json b/docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json index 7372b27c..d8a83626 100644 --- a/docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json +++ b/docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json @@ -1,11 +1,11 @@ { "schema_version": "ai_agent_automation_inventory_snapshot_v1", - "generated_at": "2026-06-04T21:42:18+08:00", + "generated_at": "2026-06-05T05:36:00+08:00", "program_status": { "overall_completion_percent": 100, "current_priority": "P1", - "current_task_id": "P1-104", - "next_task_id": "P1-105", + "current_task_id": "P1-105", + "next_task_id": "P1-106", "read_only_mode": true }, "status_taxonomy": { @@ -423,9 +423,9 @@ { "workstream_id": "WS4", "display_name": "備份與 DR 自動化", - "completion_percent": 67, + "completion_percent": 83, "status": "in_progress", - "next_task_id": "P1-105" + "next_task_id": "P1-106" }, { "workstream_id": "WS5", @@ -631,7 +631,18 @@ "title": "在 AwoooP / governance UI 加備份證據", "output": "/zh-TW/governance?tab=automation-inventory", "gate_status": "read_only_allowed", - "next_action": "完成,P1-105 定義復原演練批准包。" + "next_action": "完成,P1-105 復原演練批准包模板已推進。" + }, + { + "task_id": "P1-105", + "priority": "P1", + "status": "done", + "completion_percent": 100, + "owner_agent": "openclaw", + "title": "定義復原演練批准包", + "output": "docs/evaluations/backup_restore_drill_approval_package_template_2026-06-05.json", + "gate_status": "read_only_allowed", + "next_action": "完成,P1-106 顯示異地 / escrow 準備度狀態。" }, { "task_id": "P1-201", @@ -827,6 +838,24 @@ "ref": "/zh-TW/governance?tab=automation-inventory", "result": "P1-104 Backup / DR 證據 UI 已接入 automation inventory tab;本地 desktop 與 390px mobile 驗證 Backup / DR 證據、準備度矩陣、通知政策、成功抑制、即時升級、Gitea 與 SignOz disruptive guard 可見,無載入錯誤,horizontalOverflow <= 0。" }, + { + "evidence_id": "backup_restore_drill_approval_package_template_schema", + "kind": "schema", + "ref": "docs/schemas/backup_restore_drill_approval_package_template_v1.schema.json", + "result": "Backup / DR 復原演練批准包 schema 已建立,明確禁止 backup execution、restore execution、offsite sync、credential marker 寫入、workflow 寫入、Telegram 測試通知、secret 明文與生產路由變更。" + }, + { + "evidence_id": "backup_restore_drill_approval_package_template_snapshot", + "kind": "doc", + "ref": "docs/evaluations/backup_restore_drill_approval_package_template_2026-06-05.json", + "result": "復原演練批准包模板快照已建立,涵蓋 database restore、configuration restore、credential escrow、K8s resource restore、observability restore 與 public route reconstruction 6 類模板;全部要求 OpenClaw 仲裁與 HITL。" + }, + { + "evidence_id": "backup_restore_drill_approval_package_template_api", + "kind": "api", + "ref": "GET /api/v1/agents/backup-restore-drill-approval-package-template", + "result": "復原演練批准包模板只讀 API 已新增,只回傳 committed template,不執行 backup、restore、offsite sync、不寫 credential marker、不送 Telegram 測試通知、不改生產路由。" + }, { "evidence_id": "package_supply_chain_inventory_schema", "kind": "schema", diff --git a/docs/evaluations/backup_restore_drill_approval_package_template_2026-06-05.json b/docs/evaluations/backup_restore_drill_approval_package_template_2026-06-05.json new file mode 100644 index 00000000..ba82637f --- /dev/null +++ b/docs/evaluations/backup_restore_drill_approval_package_template_2026-06-05.json @@ -0,0 +1,510 @@ +{ + "schema_version": "backup_restore_drill_approval_package_template_v1", + "generated_at": "2026-06-05T05:36:00+08:00", + "source_refs": [ + "docs/evaluations/backup_dr_target_inventory_2026-06-04.json", + "docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json", + "docs/evaluations/backup_notification_policy_2026-06-04.json", + "docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md", + "docs/HARD_RULES.md" + ], + "program_status": { + "overall_completion_percent": 100, + "current_priority": "P1", + "current_task_id": "P1-105", + "next_task_id": "P1-106", + "read_only_mode": true + }, + "rollups": { + "total_templates": 6, + "by_domain": { + "database_restore": 1, + "configuration_restore": 1, + "credential_escrow": 1, + "k8s_resource_restore": 1, + "observability_restore": 1, + "route_reconstruction": 1 + }, + "template_ready_ids": [ + "database_restore_drill_approval_package", + "configuration_restore_approval_package", + "credential_escrow_review_package", + "velero_k8s_restore_drill_package", + "observability_restore_drill_package", + "public_route_reconstruction_package" + ], + "hitl_required_template_ids": [ + "database_restore_drill_approval_package", + "configuration_restore_approval_package", + "credential_escrow_review_package", + "velero_k8s_restore_drill_package", + "observability_restore_drill_package", + "public_route_reconstruction_package" + ], + "blocked_source_target_ids": [ + "configs_capture", + "credential_escrow_markers" + ], + "action_required_source_target_ids": [ + "signoz", + "velero_k8s_resources" + ] + }, + "approval_fields": [ + { + "field_id": "operator_and_window", + "required": true, + "description": "列出人工 operator、維護窗口、時區、通訊負責人與批准到期時間。" + }, + { + "field_id": "source_backup_ref", + "required": true, + "description": "列出備份來源、快照 ID、freshness、integrity 與 offsite 狀態;不得輸出 secret 或 credential 明文。" + }, + { + "field_id": "target_environment", + "required": true, + "description": "列出演練目標環境、隔離邊界、資料遮罩策略與不得碰觸的 production surface。" + }, + { + "field_id": "blast_radius", + "required": true, + "description": "列出受影響服務、資料、路由、通知、監控與回滾責任。" + }, + { + "field_id": "precheck_evidence", + "required": true, + "description": "列出 backup freshness、integrity、offsite、credential escrow、restore dry-run plan 與 observer readiness。" + }, + { + "field_id": "abort_and_rollback", + "required": true, + "description": "列出演練中止條件、回復步驟、驗證指標與復原後觀察期。" + }, + { + "field_id": "post_verification", + "required": true, + "description": "列出復原後 smoke、資料一致性、告警靜音恢復、Run / LOGBOOK / evidence 更新。" + }, + { + "field_id": "manual_approval", + "required": true, + "description": "列出 OpenClaw 仲裁、HITL、資料 owner、credential owner 與必要的維護窗口批准。" + } + ], + "package_templates": [ + { + "template_id": "database_restore_drill_approval_package", + "domain": "database_restore", + "status": "template_ready", + "owner_agent": "openclaw", + "purpose": "為 Gitea、AWOOOI PostgreSQL、momo PostgreSQL 與 Langfuse 類資料庫建立 restore drill 批准包,不執行 restore。", + "source_target_statuses": [ + { + "target_id": "gitea", + "readiness": "ready" + }, + { + "target_id": "awoooi_postgresql_daily", + "readiness": "ready" + }, + { + "target_id": "momo_postgresql", + "readiness": "ready" + }, + { + "target_id": "langfuse_postgresql", + "readiness": "ready" + } + ], + "required_evidence": [ + "docs/evaluations/backup_dr_target_inventory_2026-06-04.json", + "docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json", + "scripts/backup/backup-gitea.sh", + "scripts/backup/backup-momo-188-pg.sh" + ], + "required_decisions": [ + "是否允許在隔離環境執行資料庫 restore drill", + "是否需要遮罩資料或改用 sanitized backup", + "是否需要資料 owner 與維護窗口共同批准" + ], + "required_prechecks": [ + "確認 backup freshness 與 integrity evidence 未過期", + "確認 restore 目標環境與 production database 完全隔離", + "確認通知政策仍為 failure/action-required escalation" + ], + "required_tests": [ + "restore plan schema validation", + "post-restore smoke checklist", + "data consistency checklist", + "rollback evidence checklist" + ], + "rollback_requirements": [ + "列出刪除隔離演練環境與保留 evidence 的步驟", + "列出生產環境未受影響的 verification evidence" + ], + "abort_criteria": [ + "backup freshness 或 integrity 失效", + "目標環境指向 production", + "operator 無法確認資料遮罩或隔離邊界" + ], + "manual_approvals": [ + "OpenClaw arbitration", + "database owner review", + "HITL approval" + ], + "prohibited_without_approval": [ + "database restore", + "production connection string use", + "credential plaintext export", + "backup execution", + "schedule change" + ], + "evidence_refs": [ + "docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json", + "docs/evaluations/backup_notification_policy_2026-06-04.json" + ] + }, + { + "template_id": "configuration_restore_approval_package", + "domain": "configuration_restore", + "status": "template_ready", + "owner_agent": "hermes", + "purpose": "為設定、公開路由與 config capture 缺口建立 restore 批准包;configs_capture blocked 時只能提交修復批准,不可演練 restore。", + "source_target_statuses": [ + { + "target_id": "configs_capture", + "readiness": "blocked" + }, + { + "target_id": "public_routes", + "readiness": "ready" + } + ], + "required_evidence": [ + "docs/evaluations/backup_dr_target_inventory_2026-06-04.json", + "scripts/backup/backup-public-routes.sh", + "docs/runbooks/OFFSITE-BACKUP-ESCROW-RUNBOOK.md" + ], + "required_decisions": [ + "configs_capture blocked 是否先轉成修復批准包", + "公開路由重建是否只允許在 staging 或隔離環境驗證", + "設定來源權威與 owner 是否已確認" + ], + "required_prechecks": [ + "確認 config snapshot 不含 secret 明文", + "確認路由重建計畫不改 production ingress 或 DNS", + "確認 blocked target 不被標為可 restore" + ], + "required_tests": [ + "config diff review", + "route reconstruction dry plan review", + "secret redaction checklist" + ], + "rollback_requirements": [ + "列出 config restore revert patch", + "列出 route backup 與現況比對方式" + ], + "abort_criteria": [ + "config evidence 含 secret 明文", + "source authority 未確認", + "restore target 需要 production 寫入" + ], + "manual_approvals": [ + "OpenClaw arbitration", + "configuration owner review", + "HITL approval" + ], + "prohibited_without_approval": [ + "config write", + "workflow write", + "production ingress change", + "DNS change", + "secret plaintext output" + ], + "evidence_refs": [ + "docs/evaluations/backup_dr_target_inventory_2026-06-04.json" + ] + }, + { + "template_id": "credential_escrow_review_package", + "domain": "credential_escrow", + "status": "template_ready", + "owner_agent": "openclaw", + "purpose": "為 credential escrow marker blocked 狀態建立人工 review 批准包;模板不寫 marker、不讀 secret、不輸出 credential。", + "source_target_statuses": [ + { + "target_id": "credential_escrow_markers", + "readiness": "blocked" + } + ], + "required_evidence": [ + "docs/evaluations/backup_dr_target_inventory_2026-06-04.json", + "docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json", + "docs/runbooks/OFFSITE-BACKUP-ESCROW-RUNBOOK.md" + ], + "required_decisions": [ + "credential owner 是否確認 escrow marker 來源與保管流程", + "是否允許建立或更新 redacted marker", + "是否需要安全負責人與資料 owner 共同批准" + ], + "required_prechecks": [ + "確認批准包不得含 token、private key、cookie、authorization header 或 runner token", + "確認 marker 更新若被批准也必須走獨立執行流程", + "確認 blocked 狀態不被 UI 或 API 解讀為 ready" + ], + "required_tests": [ + "secret redaction checklist", + "escrow owner checklist", + "approval payload schema validation" + ], + "rollback_requirements": [ + "列出 marker 變更若被批准後的 audit trail 與 revert plan", + "列出 marker 缺失時的人工 break-glass 聯絡方式" + ], + "abort_criteria": [ + "批准包含 credential 明文", + "credential owner 未確認", + "嘗試由 Agent 自動寫 marker" + ], + "manual_approvals": [ + "OpenClaw arbitration", + "credential owner review", + "security owner review", + "HITL approval" + ], + "prohibited_without_approval": [ + "credential marker write", + "secret read", + "secret plaintext export", + "break-glass activation", + "Telegram test notification" + ], + "evidence_refs": [ + "docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json", + "docs/HARD_RULES.md" + ] + }, + { + "template_id": "velero_k8s_restore_drill_package", + "domain": "k8s_resource_restore", + "status": "template_ready", + "owner_agent": "openclaw", + "purpose": "為 Velero / K8s resource restore drill 建立批准包;velero_k8s_resources 仍為 action_required 時只能提交補證據與演練計畫。", + "source_target_statuses": [ + { + "target_id": "velero_k8s_resources", + "readiness": "action_required" + }, + { + "target_id": "harbor_registry", + "readiness": "ready" + } + ], + "required_evidence": [ + "docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json", + "k8s/awoooi-prod/", + "docs/runbooks/OFFSITE-BACKUP-ESCROW-RUNBOOK.md" + ], + "required_decisions": [ + "是否允許在非 production namespace 執行 K8s resource restore drill", + "是否需要先補 Velero backup evidence", + "是否允許 Harbor registry backup 作為 image restore 證據" + ], + "required_prechecks": [ + "確認 namespace、service account、ingress 與 secret 不會指向 production", + "確認 restore dry plan 只用隔離資源", + "確認 action_required target 補 evidence 後才可升級" + ], + "required_tests": [ + "kubectl dry plan review", + "namespace isolation checklist", + "post-restore workload health checklist" + ], + "rollback_requirements": [ + "列出刪除演練 namespace 與資源的步驟", + "列出不影響 production service / ingress 的驗證" + ], + "abort_criteria": [ + "restore plan 指向 production namespace", + "manifest 包含未遮罩 secret", + "Velero evidence 仍為 action_required 且未被人工接受" + ], + "manual_approvals": [ + "OpenClaw arbitration", + "platform owner review", + "HITL approval" + ], + "prohibited_without_approval": [ + "kubectl apply", + "velero restore", + "namespace mutation", + "secret restore", + "production routing change" + ], + "evidence_refs": [ + "docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json" + ] + }, + { + "template_id": "observability_restore_drill_package", + "domain": "observability_restore", + "status": "template_ready", + "owner_agent": "hermes", + "purpose": "為 SigNoz / ClickHouse、Prometheus 與 Alertmanager 可觀測性 restore drill 建立批准包;SignOz disruptive guard 必須保留。", + "source_target_statuses": [ + { + "target_id": "signoz", + "readiness": "action_required" + }, + { + "target_id": "prometheus_alertmanager", + "readiness": "ready" + } + ], + "required_evidence": [ + "docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json", + "docs/evaluations/backup_notification_policy_2026-06-04.json", + "ops/monitoring/", + "k8s/monitoring/" + ], + "required_decisions": [ + "是否允許 disruptive backup guard 覆蓋的服務進入演練", + "restore drill 是否需要暫時調整告警靜音與通知策略", + "Prometheus / Alertmanager evidence 是否足以支持復原驗證" + ], + "required_prechecks": [ + "確認 SignOz disruptive guard 已被 operator 看見並接受", + "確認成功訊息不即時洗版", + "確認 failure / action-required 仍會升級" + ], + "required_tests": [ + "metrics readback checklist", + "alert route readback checklist", + "notification suppression checklist" + ], + "rollback_requirements": [ + "列出恢復原告警靜音與通知政策的步驟", + "列出演練後 metrics / alert route readback" + ], + "abort_criteria": [ + "演練需要停止 production collector 且未批准", + "通知政策會發送成功洗版訊息", + "observer 無法驗證 restore 結果" + ], + "manual_approvals": [ + "OpenClaw arbitration", + "observability owner review", + "HITL approval" + ], + "prohibited_without_approval": [ + "collector stop", + "ClickHouse restore", + "Alertmanager route write", + "Telegram test notification", + "schedule change" + ], + "evidence_refs": [ + "docs/evaluations/backup_notification_policy_2026-06-04.json" + ] + }, + { + "template_id": "public_route_reconstruction_package", + "domain": "route_reconstruction", + "status": "template_ready", + "owner_agent": "openclaw", + "purpose": "為公開路由、Ingress、Nginx 備份與 DNS 重建建立批准包;模板不改生產路由。", + "source_target_statuses": [ + { + "target_id": "public_routes", + "readiness": "ready" + } + ], + "required_evidence": [ + "scripts/backup/backup-public-routes.sh", + "docs/evaluations/backup_dr_target_inventory_2026-06-04.json", + "docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json" + ], + "required_decisions": [ + "是否允許在 staging 或 isolated host 重建公開路由", + "是否需要 DNS / TLS / ingress owner review", + "是否需要 production traffic freeze window" + ], + "required_prechecks": [ + "確認重建目標不接 production traffic", + "確認 TLS / DNS evidence 不含 private key", + "確認回滾與 readback 指標已列出" + ], + "required_tests": [ + "route diff review", + "TLS public certificate readback checklist", + "HTTP smoke checklist", + "production no-change checklist" + ], + "rollback_requirements": [ + "列出路由設定 revert patch", + "列出 DNS / ingress / Nginx readback 與保留 evidence" + ], + "abort_criteria": [ + "計畫會改 production route", + "TLS private key 或 secret 明文進入批准包", + "缺少可驗證回滾點" + ], + "manual_approvals": [ + "OpenClaw arbitration", + "route owner review", + "HITL approval" + ], + "prohibited_without_approval": [ + "production routing change", + "DNS change", + "TLS private key export", + "Nginx config write", + "workflow write" + ], + "evidence_refs": [ + "docs/evaluations/backup_dr_target_inventory_2026-06-04.json" + ] + } + ], + "decision_gate_contract": { + "openclaw_role": "仲裁 restore drill、escrow review、route reconstruction 與 action-required target 是否可進下一關。", + "hermes_role": "彙整 runbook、evidence、operator checklist、LOGBOOK 與批准包文字。", + "nemotron_role": "只可離線檢查 sanitized 演練計畫完整性,不得接觸 production、secret、shadow/canary 或執行權。", + "hitl_required": true, + "expires_after": "7 days or any source backup/readiness snapshot change", + "invalidated_by": [ + "backup freshness evidence expired", + "readiness matrix changed", + "credential escrow blocker changed", + "production topology changed", + "manual approval window expired" + ] + }, + "operation_boundaries": { + "read_only_template_allowed": true, + "backup_execution_allowed": false, + "restore_execution_allowed": false, + "offsite_sync_execution_allowed": false, + "credential_marker_write_allowed": false, + "schedule_change_allowed": false, + "workflow_write_allowed": false, + "telegram_test_notification_allowed": false, + "destructive_prune_allowed": false, + "secret_plaintext_allowed": false, + "production_routing_allowed": false, + "sdk_installation_allowed": false, + "paid_api_call_allowed": false, + "shadow_or_canary_allowed": false + }, + "approval_boundaries": { + "sdk_installation_allowed": false, + "paid_api_call_allowed": false, + "shadow_or_canary_allowed": false, + "production_routing_allowed": false, + "destructive_operation_allowed": false, + "restore_execution_allowed": false, + "offsite_sync_execution_allowed": false, + "credential_marker_write_allowed": false + } +} diff --git a/docs/schemas/backup_restore_drill_approval_package_template_v1.schema.json b/docs/schemas/backup_restore_drill_approval_package_template_v1.schema.json new file mode 100644 index 00000000..dd551ab4 --- /dev/null +++ b/docs/schemas/backup_restore_drill_approval_package_template_v1.schema.json @@ -0,0 +1,461 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:backup-restore-drill-approval-package-template-v1", + "title": "AWOOOI Backup / DR 復原演練批准包模板 v1", + "description": "Backup / DR restore drill、credential escrow review、offsite readiness 與公開路由重建的只讀批准包模板。此 schema 不授權備份執行、restore 執行、offsite sync、credential marker 寫入、排程變更、workflow 寫入、Telegram 測試通知、secret 明文輸出、破壞性 prune 或生產路由變更。", + "type": "object", + "required": [ + "schema_version", + "generated_at", + "source_refs", + "program_status", + "rollups", + "approval_fields", + "package_templates", + "decision_gate_contract", + "operation_boundaries", + "approval_boundaries" + ], + "properties": { + "schema_version": { + "type": "string", + "const": "backup_restore_drill_approval_package_template_v1" + }, + "generated_at": { + "type": "string", + "minLength": 1 + }, + "source_refs": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "program_status": { + "type": "object", + "required": [ + "overall_completion_percent", + "current_priority", + "current_task_id", + "next_task_id", + "read_only_mode" + ], + "properties": { + "overall_completion_percent": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "current_priority": { + "type": "string", + "enum": ["P0", "P1", "P2", "P3"] + }, + "current_task_id": { + "type": "string", + "minLength": 1 + }, + "next_task_id": { + "type": "string", + "minLength": 1 + }, + "read_only_mode": { + "type": "boolean", + "const": true + } + }, + "additionalProperties": false + }, + "rollups": { + "type": "object", + "required": [ + "total_templates", + "by_domain", + "template_ready_ids", + "hitl_required_template_ids", + "blocked_source_target_ids", + "action_required_source_target_ids" + ], + "properties": { + "total_templates": { + "type": "integer", + "minimum": 1 + }, + "by_domain": { + "type": "object", + "additionalProperties": { + "type": "integer", + "minimum": 0 + } + }, + "template_ready_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "hitl_required_template_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "blocked_source_target_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "action_required_source_target_ids": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": false + }, + "approval_fields": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["field_id", "required", "description"], + "properties": { + "field_id": { + "type": "string", + "minLength": 1 + }, + "required": { + "type": "boolean", + "const": true + }, + "description": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + } + }, + "package_templates": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "template_id", + "domain", + "status", + "owner_agent", + "purpose", + "source_target_statuses", + "required_evidence", + "required_decisions", + "required_prechecks", + "required_tests", + "rollback_requirements", + "abort_criteria", + "manual_approvals", + "prohibited_without_approval", + "evidence_refs" + ], + "properties": { + "template_id": { + "type": "string", + "minLength": 1 + }, + "domain": { + "type": "string", + "enum": [ + "database_restore", + "configuration_restore", + "credential_escrow", + "k8s_resource_restore", + "observability_restore", + "route_reconstruction" + ] + }, + "status": { + "type": "string", + "enum": ["template_ready"] + }, + "owner_agent": { + "type": "string", + "enum": ["openclaw", "hermes", "nemotron"] + }, + "purpose": { + "type": "string", + "minLength": 1 + }, + "source_target_statuses": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["target_id", "readiness"], + "properties": { + "target_id": { + "type": "string", + "minLength": 1 + }, + "readiness": { + "type": "string", + "enum": ["ready", "action_required", "blocked", "deferred"] + } + }, + "additionalProperties": false + } + }, + "required_evidence": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "required_decisions": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "required_prechecks": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "required_tests": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "rollback_requirements": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "abort_criteria": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "manual_approvals": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "prohibited_without_approval": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "evidence_refs": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": false + } + }, + "decision_gate_contract": { + "type": "object", + "required": [ + "openclaw_role", + "hermes_role", + "nemotron_role", + "hitl_required", + "expires_after", + "invalidated_by" + ], + "properties": { + "openclaw_role": { + "type": "string", + "minLength": 1 + }, + "hermes_role": { + "type": "string", + "minLength": 1 + }, + "nemotron_role": { + "type": "string", + "minLength": 1 + }, + "hitl_required": { + "type": "boolean", + "const": true + }, + "expires_after": { + "type": "string", + "minLength": 1 + }, + "invalidated_by": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "additionalProperties": false + }, + "operation_boundaries": { + "type": "object", + "required": [ + "read_only_template_allowed", + "backup_execution_allowed", + "restore_execution_allowed", + "offsite_sync_execution_allowed", + "credential_marker_write_allowed", + "schedule_change_allowed", + "workflow_write_allowed", + "telegram_test_notification_allowed", + "destructive_prune_allowed", + "secret_plaintext_allowed", + "production_routing_allowed", + "sdk_installation_allowed", + "paid_api_call_allowed", + "shadow_or_canary_allowed" + ], + "properties": { + "read_only_template_allowed": { + "type": "boolean", + "const": true + }, + "backup_execution_allowed": { + "type": "boolean", + "const": false + }, + "restore_execution_allowed": { + "type": "boolean", + "const": false + }, + "offsite_sync_execution_allowed": { + "type": "boolean", + "const": false + }, + "credential_marker_write_allowed": { + "type": "boolean", + "const": false + }, + "schedule_change_allowed": { + "type": "boolean", + "const": false + }, + "workflow_write_allowed": { + "type": "boolean", + "const": false + }, + "telegram_test_notification_allowed": { + "type": "boolean", + "const": false + }, + "destructive_prune_allowed": { + "type": "boolean", + "const": false + }, + "secret_plaintext_allowed": { + "type": "boolean", + "const": false + }, + "production_routing_allowed": { + "type": "boolean", + "const": false + }, + "sdk_installation_allowed": { + "type": "boolean", + "const": false + }, + "paid_api_call_allowed": { + "type": "boolean", + "const": false + }, + "shadow_or_canary_allowed": { + "type": "boolean", + "const": false + } + }, + "additionalProperties": false + }, + "approval_boundaries": { + "type": "object", + "required": [ + "sdk_installation_allowed", + "paid_api_call_allowed", + "shadow_or_canary_allowed", + "production_routing_allowed", + "destructive_operation_allowed", + "restore_execution_allowed", + "offsite_sync_execution_allowed", + "credential_marker_write_allowed" + ], + "properties": { + "sdk_installation_allowed": { + "type": "boolean", + "const": false + }, + "paid_api_call_allowed": { + "type": "boolean", + "const": false + }, + "shadow_or_canary_allowed": { + "type": "boolean", + "const": false + }, + "production_routing_allowed": { + "type": "boolean", + "const": false + }, + "destructive_operation_allowed": { + "type": "boolean", + "const": false + }, + "restore_execution_allowed": { + "type": "boolean", + "const": false + }, + "offsite_sync_execution_allowed": { + "type": "boolean", + "const": false + }, + "credential_marker_write_allowed": { + "type": "boolean", + "const": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} 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 2d632402..9660092b 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 @@ -3393,3 +3393,28 @@ Phase 6 完成後 3. P1-305 / P1-306:補任務批准邊界與進度彙總細節。 **裁決:** P1-104 已完成,但仍只屬於 read-only evidence surface。不得執行 backup、restore、offsite sync、credential marker 寫入、排程變更、workflow 寫入或 Telegram 測試通知;不得把 Backup / DR UI 可見解讀成 full DR green。下一步只能產生復原演練與 escrow review 的批准包,必須保留 OpenClaw 仲裁與人工批准邊界。 + +### 2026-06-05 凌晨 (台北) — P1-105 復原演練批准包模板完成 + +**觸發**:統帥批准繼續,要求依工作清單優先順序推進,並同步工作完成度與狀態。 + +**已推進:** +- P1-105:建立 Backup / DR 復原演練批准包模板,只讀回傳 restore drill、credential escrow review、K8s resource recovery、observability recovery 與 route reconstruction 的批准包欄位,不執行任何 restore。 +- 新增 `docs/schemas/backup_restore_drill_approval_package_template_v1.schema.json`,明確禁止 backup execution、restore execution、offsite sync、credential marker 寫入、schedule change、workflow write、Telegram test notification、secret plaintext、destructive prune、paid API、shadow/canary 與 production routing。 +- 新增 `docs/evaluations/backup_restore_drill_approval_package_template_2026-06-05.json`,6 類模板全部要求 OpenClaw 仲裁與 HITL;`configs_capture`、`credential_escrow_markers` 維持 blocked,`signoz`、`velero_k8s_resources` 維持 action_required。 +- 新增 `GET /api/v1/agents/backup-restore-drill-approval-package-template`,只讀取 committed template,不呼叫外部來源、不碰 DB/Redis、不執行備份或復原。 +- `docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json` 已將 `current_task_id` 推進到 `P1-105`、`next_task_id` 推進到 `P1-106`;WS4 備份與 DR 自動化由 `67%` 推進到 `83%`。 +- `docs/evaluations/ai_agent_automation_backlog_2026-06-04.json` 新增 `AUTO-P1-105` done item,rollup 更新為 total `20`、P1 `18`、done `13`、read_only_allowed `17`、OpenClaw owner `9`。 +- `docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md` 已新增 P1-105 摘要、驗證結果、進度同步紀錄與下一步順序。 + +**驗證:** +- `python3 -m json.tool docs/schemas/backup_restore_drill_approval_package_template_v1.schema.json` 通過。 +- `python3 -m json.tool docs/evaluations/backup_restore_drill_approval_package_template_2026-06-05.json` 通過。 +- `PYTHONDONTWRITEBYTECODE=1 apps/api/.venv/bin/python -m pytest apps/api/tests/test_backup_restore_drill_approval_package_template.py apps/api/tests/test_backup_restore_drill_approval_package_template_api.py -q`:`11 passed`。 +- `python3 -m py_compile apps/api/src/services/backup_restore_drill_approval_package_template.py apps/api/src/api/v1/agents.py apps/api/tests/test_backup_restore_drill_approval_package_template.py apps/api/tests/test_backup_restore_drill_approval_package_template_api.py` 通過。 + +**下一步:** +1. P1-106:顯示異地 / escrow 準備度狀態。 +2. P1-305 / P1-306:補任務批准邊界與進度彙總細節。 + +**裁決:** P1-105 只完成批准包模板與只讀 API,不代表已批准任何 restore drill。不得執行 backup、restore、offsite sync、credential marker 寫入、排程變更、workflow 寫入、Telegram 測試通知、secret 明文輸出、destructive prune 或 production routing;blocked / action_required 目標不得被 UI 或 Agent 解讀成 ready。