feat(api): 新增復原演練批准包模板
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"])
|
||||
|
||||
@@ -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"]
|
||||
)
|
||||
|
||||
@@ -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"],
|
||||
}
|
||||
@@ -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"]
|
||||
)
|
||||
@@ -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. 目前風險
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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。
|
||||
|
||||
Reference in New Issue
Block a user