feat(api): 新增復原演練批准包模板
All checks were successful
CD Pipeline / tests (push) Successful in 1m24s
Code Review / ai-code-review (push) Successful in 10s
CD Pipeline / build-and-deploy (push) Successful in 3m59s
CD Pipeline / post-deploy-checks (push) Successful in 2m23s

This commit is contained in:
Your Name
2026-06-05 01:20:50 +08:00
parent 2857da80b4
commit a367227d3a
12 changed files with 1609 additions and 34 deletions

View File

@@ -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],

View File

@@ -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
}

View File

@@ -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"])

View File

@@ -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"]
)

View File

@@ -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"],
}

View File

@@ -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"]
)

View File

@@ -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-104Backup / 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 仲裁與 HITLHermes 可起草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 passedpy_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. 目前風險

View File

@@ -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",

View File

@@ -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",

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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 itemrollup 更新為 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 routingblocked / action_required 目標不得被 UI 或 Agent 解讀成 ready。