feat(governance): 顯示異地 escrow 準備度
Some checks failed
CD Pipeline / tests (push) Successful in 1m32s
Code Review / ai-code-review (push) Successful in 15s
CD Pipeline / build-and-deploy (push) Successful in 5m41s
CD Pipeline / post-deploy-checks (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-05 02:11:44 +08:00
parent d7b5dfd85e
commit 4360628864
16 changed files with 1337 additions and 37 deletions

View File

@@ -56,6 +56,9 @@ from src.services.backup_notification_policy import (
from src.services.backup_restore_drill_approval_package_template import (
load_latest_backup_restore_drill_approval_package_template,
)
from src.services.offsite_escrow_readiness_status import (
load_latest_offsite_escrow_readiness_status,
)
from src.services.package_supply_chain_inventory import (
load_latest_package_supply_chain_inventory,
)
@@ -583,6 +586,34 @@ async def get_backup_restore_drill_approval_package_template() -> dict[str, Any]
) from exc
@router.get(
"/offsite-escrow-readiness-status",
response_model=dict[str, Any],
summary="取得異地 / Escrow 準備度狀態",
description=(
"讀取最新已提交的異地備份、credential escrow 與 K8s resource offsite readiness 狀態;"
"此端點只回傳 read-only status不執行 backup、restore、offsite sync、"
"不寫 credential marker、不讀 credential、不輸出 secret 明文、不改排程、不寫 workflow、"
"不送 Telegram 測試通知、不做破壞性 prune、不改生產路由。"
),
)
async def get_offsite_escrow_readiness_status() -> dict[str, Any]:
"""Return the latest read-only offsite / escrow readiness status."""
try:
return await asyncio.to_thread(load_latest_offsite_escrow_readiness_status)
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("offsite_escrow_readiness_status_invalid", error=str(exc))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="異地 / Escrow 準備度狀態快照無效",
) from exc
@router.get(
"/package-supply-chain-inventory",
response_model=dict[str, Any],

View File

@@ -0,0 +1,160 @@
"""
Offsite / escrow readiness status snapshot.
Loads the latest committed, read-only offsite / escrow readiness status. The
status view never runs backups, restores, offsite sync, credential marker
writes, credential reads, schedule changes, workflow writes, Telegram test
notifications, destructive prune, 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 = "offsite_escrow_readiness_status_*.json"
_SCHEMA_VERSION = "offsite_escrow_readiness_status_v1"
def load_latest_offsite_escrow_readiness_status(
evaluations_dir: Path | None = None,
) -> dict[str, Any]:
"""Load the newest committed offsite / escrow readiness status snapshot."""
directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR
candidates = sorted(directory.glob(_SNAPSHOT_PATTERN))
if not candidates:
raise FileNotFoundError(f"no offsite / escrow readiness status 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))
_require_redacted_cards(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_status_allowed") is not True:
raise ValueError(f"{label}: read_only_status_allowed must be true")
blocked_flags = {
"backup_execution_allowed",
"restore_execution_allowed",
"offsite_sync_execution_allowed",
"credential_marker_write_allowed",
"credential_read_allowed",
"secret_plaintext_allowed",
"schedule_change_allowed",
"workflow_write_allowed",
"telegram_test_notification_allowed",
"destructive_prune_allowed",
"production_routing_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:
cards = payload.get("readiness_cards") or []
rollups = payload.get("rollups") or {}
if rollups.get("total_cards") != len(cards):
raise ValueError(f"{label}: rollups.total_cards must match readiness_cards")
verified_offsite = {
card.get("card_id")
for card in cards
if card.get("kind") == "offsite_mirror" and card.get("readiness") == "verified"
}
if set(rollups.get("verified_offsite_card_ids") or []) != verified_offsite:
raise ValueError(f"{label}: rollups.verified_offsite_card_ids must match verified offsite cards")
blocked_escrow = {
card.get("card_id")
for card in cards
if card.get("kind") == "credential_escrow" and card.get("readiness") == "blocked"
}
if set(rollups.get("blocked_escrow_card_ids") or []) != blocked_escrow:
raise ValueError(f"{label}: rollups.blocked_escrow_card_ids must match blocked escrow cards")
action_required = {
card.get("card_id")
for card in cards
if card.get("readiness") == "action_required"
}
if set(rollups.get("action_required_card_ids") or []) != action_required:
raise ValueError(f"{label}: rollups.action_required_card_ids must match action_required cards")
execution_blocked = {
card.get("card_id")
for card in cards
if any(operation.endswith("_execution") or operation == "credential_marker_write" for operation in card.get("blocked_operations", []))
}
if set(rollups.get("execution_blocked_card_ids") or []) != execution_blocked:
raise ValueError(f"{label}: rollups.execution_blocked_card_ids must match cards with blocked execution operations")
def _require_redacted_cards(payload: dict[str, Any], label: str) -> None:
cards = payload.get("readiness_cards") or []
forbidden_exposure = {
"plaintext",
"secret_plaintext",
"credential_plaintext",
"token_visible",
}
exposed = sorted(
card.get("card_id")
for card in cards
if card.get("credential_exposure_status") in forbidden_exposure
)
if exposed:
raise ValueError(f"{label}: credential exposure must stay redacted: {exposed}")
contract = payload.get("operator_contract") or {}
must_not_interpret_as = set(contract.get("must_not_interpret_as") or [])
required_denials = {
"復原批准",
"異地同步批准",
"credential marker 寫入批准",
"完整 DR 綠燈",
}
if not required_denials.issubset(must_not_interpret_as):
raise ValueError(f"{label}: operator_contract.must_not_interpret_as is missing required denials")

View File

@@ -18,12 +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-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["program_status"]["current_task_id"] == "P1-106"
assert data["program_status"]["next_task_id"] == "P1-305"
assert data["rollups"]["total_items"] == len(data["backlog_items"]) == 21
assert data["rollups"]["by_priority"]["P1"] == 19
assert data["rollups"]["by_status"]["done"] == 14
assert data["rollups"]["by_gate_status"]["read_only_allowed"] == 18
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
@@ -33,4 +33,5 @@ def test_ai_agent_automation_backlog_snapshot_endpoint_returns_committed_snapsho
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-P1-106" 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-105"
assert data["program_status"]["next_task_id"] == "P1-106"
assert data["program_status"]["current_task_id"] == "P1-106"
assert data["program_status"]["next_task_id"] == "P1-305"
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
@@ -30,6 +30,7 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps
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(task["task_id"] == "P1-106" 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(
@@ -42,3 +43,4 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps
evidence["evidence_id"] == "backup_restore_drill_approval_package_template_api"
for evidence in data["evidence"]
)
assert any(evidence["evidence_id"] == "offsite_escrow_readiness_status_api" for evidence in data["evidence"])

View File

@@ -0,0 +1,213 @@
from __future__ import annotations
import json
import pytest
from src.services.offsite_escrow_readiness_status import (
load_latest_offsite_escrow_readiness_status,
)
def test_load_latest_offsite_escrow_readiness_status_reads_newest_file(tmp_path):
older = _snapshot(generated_at="2026-06-04T00:00:00+08:00", completion=40)
newer = _snapshot(generated_at="2026-06-05T00:00:00+08:00", completion=100)
(tmp_path / "offsite_escrow_readiness_status_2026-06-04.json").write_text(
json.dumps(older),
encoding="utf-8",
)
(tmp_path / "offsite_escrow_readiness_status_2026-06-05.json").write_text(
json.dumps(newer),
encoding="utf-8",
)
loaded = load_latest_offsite_escrow_readiness_status(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_cards"] == 3
def test_offsite_escrow_readiness_status_requires_read_only_mode(tmp_path):
snapshot = _snapshot()
snapshot["program_status"]["read_only_mode"] = False
(tmp_path / "offsite_escrow_readiness_status_2026-06-05.json").write_text(
json.dumps(snapshot),
encoding="utf-8",
)
with pytest.raises(ValueError, match="read_only_mode"):
load_latest_offsite_escrow_readiness_status(tmp_path)
def test_offsite_escrow_readiness_status_requires_blocked_approval_boundaries(tmp_path):
snapshot = _snapshot()
snapshot["approval_boundaries"]["credential_marker_write_allowed"] = True
(tmp_path / "offsite_escrow_readiness_status_2026-06-05.json").write_text(
json.dumps(snapshot),
encoding="utf-8",
)
with pytest.raises(ValueError, match="approval boundaries"):
load_latest_offsite_escrow_readiness_status(tmp_path)
def test_offsite_escrow_readiness_status_requires_blocked_operation_boundaries(tmp_path):
snapshot = _snapshot()
snapshot["operation_boundaries"]["offsite_sync_execution_allowed"] = True
(tmp_path / "offsite_escrow_readiness_status_2026-06-05.json").write_text(
json.dumps(snapshot),
encoding="utf-8",
)
with pytest.raises(ValueError, match="operation boundaries"):
load_latest_offsite_escrow_readiness_status(tmp_path)
def test_offsite_escrow_readiness_status_requires_total_consistency(tmp_path):
snapshot = _snapshot()
snapshot["rollups"]["total_cards"] = 999
(tmp_path / "offsite_escrow_readiness_status_2026-06-05.json").write_text(
json.dumps(snapshot),
encoding="utf-8",
)
with pytest.raises(ValueError, match="total_cards"):
load_latest_offsite_escrow_readiness_status(tmp_path)
def test_offsite_escrow_readiness_status_requires_escrow_blocked_consistency(tmp_path):
snapshot = _snapshot()
snapshot["rollups"]["blocked_escrow_card_ids"] = []
(tmp_path / "offsite_escrow_readiness_status_2026-06-05.json").write_text(
json.dumps(snapshot),
encoding="utf-8",
)
with pytest.raises(ValueError, match="blocked_escrow_card_ids"):
load_latest_offsite_escrow_readiness_status(tmp_path)
def test_offsite_escrow_readiness_status_rejects_credential_plaintext(tmp_path):
snapshot = _snapshot()
snapshot["readiness_cards"][1]["credential_exposure_status"] = "credential_plaintext"
(tmp_path / "offsite_escrow_readiness_status_2026-06-05.json").write_text(
json.dumps(snapshot),
encoding="utf-8",
)
with pytest.raises(ValueError, match="credential exposure"):
load_latest_offsite_escrow_readiness_status(tmp_path)
def test_offsite_escrow_readiness_status_requires_operator_denials(tmp_path):
snapshot = _snapshot()
snapshot["operator_contract"]["must_not_interpret_as"] = ["復原批准"]
(tmp_path / "offsite_escrow_readiness_status_2026-06-05.json").write_text(
json.dumps(snapshot),
encoding="utf-8",
)
with pytest.raises(ValueError, match="must_not_interpret_as"):
load_latest_offsite_escrow_readiness_status(tmp_path)
def test_offsite_escrow_readiness_status_fails_when_missing(tmp_path):
with pytest.raises(FileNotFoundError):
load_latest_offsite_escrow_readiness_status(tmp_path)
def _snapshot(
*,
generated_at: str = "2026-06-05T00:00:00+08:00",
completion: int = 100,
) -> dict:
return {
"schema_version": "offsite_escrow_readiness_status_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-106",
"next_task_id": "P1-305",
"read_only_mode": True,
},
"rollups": {
"total_cards": 3,
"by_readiness": {"verified": 1, "action_required": 1, "blocked": 1},
"by_kind": {
"offsite_mirror": 1,
"credential_escrow": 1,
"k8s_resource_offsite": 1,
},
"verified_offsite_card_ids": ["offsite_rclone_full_sync"],
"blocked_escrow_card_ids": ["credential_escrow_markers"],
"action_required_card_ids": ["velero_k8s_resources"],
"execution_blocked_card_ids": [
"offsite_rclone_full_sync",
"credential_escrow_markers",
"velero_k8s_resources",
],
},
"readiness_cards": [
_card("offsite_rclone_full_sync", "offsite_mirror", "verified"),
_card("credential_escrow_markers", "credential_escrow", "blocked"),
_card("velero_k8s_resources", "k8s_resource_offsite", "action_required"),
],
"operator_contract": {
"display_mode": "read_only_status",
"success_notification_policy": "成功狀態不得即時通知洗版。",
"failure_notification_policy": "失敗或阻擋維持 action-required。",
"credential_display_policy": "只顯示 redacted metadata。",
"must_not_interpret_as": [
"復原批准",
"異地同步批准",
"credential marker 寫入批准",
"完整 DR 綠燈",
],
},
"operation_boundaries": {
"read_only_status_allowed": True,
"backup_execution_allowed": False,
"restore_execution_allowed": False,
"offsite_sync_execution_allowed": False,
"credential_marker_write_allowed": False,
"credential_read_allowed": False,
"secret_plaintext_allowed": False,
"schedule_change_allowed": False,
"workflow_write_allowed": False,
"telegram_test_notification_allowed": False,
"destructive_prune_allowed": False,
"production_routing_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 _card(card_id: str, kind: str, readiness: str) -> dict:
return {
"card_id": card_id,
"target_id": card_id,
"display_name": card_id,
"kind": kind,
"readiness": readiness,
"offsite_status": "verified" if readiness == "verified" else "not_applicable",
"escrow_status": "missing_markers" if kind == "credential_escrow" else "not_applicable",
"restore_drill_status": "approval_required",
"credential_exposure_status": "redacted_only",
"automation_gate_status": "read_only_allowed",
"operator_summary": "只讀狀態。",
"next_action": "維持只讀。",
"evidence_refs": ["docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json"],
"blocked_operations": ["offsite_sync_execution", "credential_marker_write"],
}

View File

@@ -0,0 +1,43 @@
from __future__ import annotations
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.v1.agents import router
def test_offsite_escrow_readiness_status_endpoint_returns_committed_snapshot():
app = FastAPI()
app.include_router(router, prefix="/api/v1")
client = TestClient(app)
response = client.get("/api/v1/agents/offsite-escrow-readiness-status")
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == "offsite_escrow_readiness_status_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-106"
assert data["program_status"]["next_task_id"] == "P1-305"
assert data["rollups"]["total_cards"] == len(data["readiness_cards"]) == 3
assert data["rollups"]["verified_offsite_card_ids"] == ["offsite_rclone_full_sync"]
assert data["rollups"]["blocked_escrow_card_ids"] == ["credential_escrow_markers"]
assert data["rollups"]["action_required_card_ids"] == ["velero_k8s_resources"]
assert data["operation_boundaries"]["read_only_status_allowed"] is True
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"]["credential_read_allowed"] is False
assert data["operation_boundaries"]["secret_plaintext_allowed"] is False
assert data["operation_boundaries"]["telegram_test_notification_allowed"] is False
assert data["approval_boundaries"]["credential_marker_write_allowed"] is False
assert any(
card["card_id"] == "credential_escrow_markers" and card["readiness"] == "blocked"
for card in data["readiness_cards"]
)
assert any(
card["card_id"] == "velero_k8s_resources" and card["readiness"] == "action_required"
for card in data["readiness_cards"]
)
assert "完整 DR 綠燈" in data["operator_contract"]["must_not_interpret_as"]

View File

@@ -3022,7 +3022,31 @@
"deferred_until_service_active": "等服務啟用",
"suppress_immediate_success": "成功不即時通知",
"escalate_immediate": "立即升級",
"create_action_required": "建立待處置"
"create_action_required": "建立待處置",
"missing_markers": "缺 marker",
"redacted_only": "僅脫敏",
"read_only_allowed": "只讀允許"
}
},
"offsiteEscrow": {
"title": "異地 / Escrow 準備度",
"source": "{generated} · {current} → {next}",
"contractTitle": "顯示契約",
"metrics": {
"total": "狀態卡",
"verified": "異地已驗證",
"actionRequired": "需處置",
"blocked": "Escrow 阻擋",
"executionBlocked": "執行阻擋"
},
"labels": {
"escrow": "Escrow",
"credential": "Credential"
},
"kinds": {
"offsite_mirror": "異地鏡像",
"credential_escrow": "Credential escrow",
"k8s_resource_offsite": "K8s offsite"
}
},
"boundaries": {

View File

@@ -3022,7 +3022,31 @@
"deferred_until_service_active": "等服務啟用",
"suppress_immediate_success": "成功不即時通知",
"escalate_immediate": "立即升級",
"create_action_required": "建立待處置"
"create_action_required": "建立待處置",
"missing_markers": "缺 marker",
"redacted_only": "僅脫敏",
"read_only_allowed": "只讀允許"
}
},
"offsiteEscrow": {
"title": "異地 / Escrow 準備度",
"source": "{generated} · {current} → {next}",
"contractTitle": "顯示契約",
"metrics": {
"total": "狀態卡",
"verified": "異地已驗證",
"actionRequired": "需處置",
"blocked": "Escrow 阻擋",
"executionBlocked": "執行阻擋"
},
"labels": {
"escrow": "Escrow",
"credential": "Credential"
},
"kinds": {
"offsite_mirror": "異地鏡像",
"credential_escrow": "Credential escrow",
"k8s_resource_offsite": "K8s offsite"
}
},
"boundaries": {

View File

@@ -33,6 +33,7 @@ import {
type BackupDrReadinessMatrixSnapshot,
type BackupDrTargetInventorySnapshot,
type BackupNotificationPolicySnapshot,
type OffsiteEscrowReadinessStatusSnapshot,
} from '@/lib/api-client'
function formatDateTime(value: string): string {
@@ -203,6 +204,7 @@ export function AutomationInventoryTab() {
const [backupTargets, setBackupTargets] = useState<BackupDrTargetInventorySnapshot | null>(null)
const [backupReadiness, setBackupReadiness] = useState<BackupDrReadinessMatrixSnapshot | null>(null)
const [backupPolicy, setBackupPolicy] = useState<BackupNotificationPolicySnapshot | null>(null)
const [offsiteEscrow, setOffsiteEscrow] = useState<OffsiteEscrowReadinessStatusSnapshot | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)
@@ -214,13 +216,15 @@ export function AutomationInventoryTab() {
apiClient.getBackupDrTargetInventory(),
apiClient.getBackupDrReadinessMatrix(),
apiClient.getBackupNotificationPolicy(),
apiClient.getOffsiteEscrowReadinessStatus(),
])
.then(([inventoryData, backlogData, targetData, readinessData, policyData]) => {
.then(([inventoryData, backlogData, targetData, readinessData, policyData, offsiteEscrowData]) => {
setSnapshot(inventoryData)
setBacklog(backlogData)
setBackupTargets(targetData)
setBackupReadiness(readinessData)
setBackupPolicy(policyData)
setOffsiteEscrow(offsiteEscrowData)
setError(false)
})
.catch(() => setError(true))
@@ -282,6 +286,17 @@ export function AutomationInventoryTab() {
.slice(0, 6)
}, [backupTargets])
const visibleOffsiteEscrowCards = useMemo(() => {
if (!offsiteEscrow) return []
const priority = { blocked: 0, action_required: 1, verified: 2 } as Record<string, number>
return [...offsiteEscrow.readiness_cards].sort((a, b) => {
const left = priority[a.readiness] ?? 3
const right = priority[b.readiness] ?? 3
if (left !== right) return left - right
return a.card_id.localeCompare(b.card_id)
})
}, [offsiteEscrow])
if (loading) {
return (
<div style={{ padding: 20, display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12 }} className="automation-inventory-kpi-grid">
@@ -295,7 +310,7 @@ export function AutomationInventoryTab() {
)
}
if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy) {
if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy || !offsiteEscrow) {
return (
<div style={{ padding: 20 }}>
<GlassCard variant="subtle" padding="lg">
@@ -338,6 +353,10 @@ export function AutomationInventoryTab() {
const blockedBackupRows = backupReadiness.rollups.by_overall_readiness.blocked ?? 0
const suppressedSuccessRules = backupPolicy.rollups.by_decision.suppress_immediate_success ?? 0
const immediateEscalationRules = backupPolicy.rollups.by_decision.escalate_immediate ?? 0
const verifiedOffsiteCards = offsiteEscrow.rollups.by_readiness.verified ?? 0
const actionRequiredOffsiteCards = offsiteEscrow.rollups.by_readiness.action_required ?? 0
const blockedEscrowCards = offsiteEscrow.rollups.by_readiness.blocked ?? 0
const executionBlockedCards = offsiteEscrow.rollups.execution_blocked_card_ids.length
const blockedApprovals = Object.entries(snapshot.approval_boundaries)
.filter(([, allowed]) => allowed === false)
.map(([key]) => key)
@@ -350,6 +369,14 @@ export function AutomationInventoryTab() {
}
}
const kindLabel = (value: string) => {
try {
return t(`offsiteEscrow.kinds.${value}` as never)
} catch {
return value
}
}
return (
<div style={{ padding: 20, display: 'flex', flexDirection: 'column', gap: 16 }}>
<GlassCard variant="subtle" padding="md">
@@ -640,6 +667,94 @@ export function AutomationInventoryTab() {
</div>
</GlassCard>
<GlassCard variant="subtle" padding="md">
<div style={{ display: 'flex', flexDirection: 'column', gap: 13, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 }}>
<Archive size={14} style={{ color: '#d97757' }} />
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
{t('offsiteEscrow.title')}
</span>
</div>
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f' }}>
{t('offsiteEscrow.source', {
generated: formatDateTime(offsiteEscrow.generated_at),
current: offsiteEscrow.program_status.current_task_id,
next: offsiteEscrow.program_status.next_task_id,
})}
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, minmax(0, 1fr))', gap: 12 }} className="automation-inventory-offsite-kpi-grid">
<MetricCard label={t('offsiteEscrow.metrics.total')} value={offsiteEscrow.rollups.total_cards} icon={<HardDrive size={16} />} />
<MetricCard label={t('offsiteEscrow.metrics.verified')} value={verifiedOffsiteCards} tone="ok" icon={<ShieldCheck size={16} />} />
<MetricCard label={t('offsiteEscrow.metrics.actionRequired')} value={actionRequiredOffsiteCards} tone="warn" icon={<AlertTriangle size={16} />} />
<MetricCard label={t('offsiteEscrow.metrics.blocked')} value={blockedEscrowCards} tone="danger" icon={<Lock size={16} />} />
<MetricCard label={t('offsiteEscrow.metrics.executionBlocked')} value={executionBlockedCards} tone="danger" icon={<Lock size={16} />} />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.35fr) minmax(0, 0.65fr)', gap: 12 }} className="automation-inventory-offsite-grid">
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 10 }} className="automation-inventory-offsite-card-grid">
{visibleOffsiteEscrowCards.map(card => (
<div key={card.card_id} style={{ padding: 11, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', minWidth: 0 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, minWidth: 0 }}>
<span style={{
fontFamily: 'Syne, sans-serif',
fontSize: 12,
fontWeight: 700,
color: '#141413',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}>
{card.display_name}
</span>
<Chip value={statusLabel(card.readiness)} muted={card.readiness === 'verified'} />
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
<Chip value={kindLabel(card.kind)} />
<Chip value={`${t('backupEvidence.labels.offsite')}: ${statusLabel(card.offsite_status)}`} muted={evidenceTone(card.offsite_status) === 'ok'} />
<Chip value={`${t('offsiteEscrow.labels.escrow')}: ${statusLabel(card.escrow_status)}`} muted={card.escrow_status === 'not_applicable'} />
<Chip value={`${t('backupEvidence.labels.restore')}: ${statusLabel(card.restore_drill_status)}`} muted={card.restore_drill_status === 'not_applicable'} />
<Chip value={statusLabel(card.automation_gate_status)} muted />
</div>
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{card.operator_summary}
</div>
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#141413', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{card.next_action}
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
<Chip value={`${t('offsiteEscrow.labels.credential')}: ${statusLabel(card.credential_exposure_status)}`} muted />
<Chip value={card.evidence_refs[0] ?? t('backupEvidence.noEvidence')} muted />
</div>
</div>
</div>
))}
</div>
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
{t('offsiteEscrow.contractTitle')}
</span>
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.5, overflowWrap: 'anywhere' }}>
{offsiteEscrow.operator_contract.credential_display_policy}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 7, minWidth: 0 }}>
<Chip value={offsiteEscrow.operator_contract.success_notification_policy} muted />
<Chip value={offsiteEscrow.operator_contract.failure_notification_policy} muted />
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
{offsiteEscrow.operator_contract.must_not_interpret_as.slice(0, 6).map(item => (
<Chip key={item} value={item} />
))}
</div>
</div>
</div>
</div>
</GlassCard>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.2fr) minmax(0, 0.8fr)', gap: 12 }} className="automation-inventory-bottom-grid">
<GlassCard variant="subtle" padding="md">
<div style={{ display: 'flex', flexDirection: 'column', gap: 13, minWidth: 0 }}>
@@ -704,6 +819,9 @@ export function AutomationInventoryTab() {
.automation-inventory-backup-kpi-grid,
.automation-inventory-backup-evidence-grid,
.automation-inventory-backup-readiness-grid,
.automation-inventory-offsite-kpi-grid,
.automation-inventory-offsite-grid,
.automation-inventory-offsite-card-grid,
.automation-inventory-bottom-grid,
.automation-inventory-task-grid {
grid-template-columns: 1fr !important;

View File

@@ -276,6 +276,11 @@ export const apiClient = {
const res = await fetch(`${API_BASE_URL}/agents/backup-notification-policy`)
return handleResponse<BackupNotificationPolicySnapshot>(res)
},
async getOffsiteEscrowReadinessStatus() {
const res = await fetch(`${API_BASE_URL}/agents/offsite-escrow-readiness-status`)
return handleResponse<OffsiteEscrowReadinessStatusSnapshot>(res)
},
}
// =========================================================================
@@ -859,3 +864,50 @@ export interface BackupNotificationPolicySnapshot {
approval_boundaries: Record<string, false>
operation_boundaries: Record<string, boolean>
}
export interface OffsiteEscrowReadinessStatusSnapshot {
schema_version: 'offsite_escrow_readiness_status_v1'
generated_at: string
source_refs: string[]
program_status: {
overall_completion_percent: number
current_priority: 'P0' | 'P1' | 'P2' | 'P3'
current_task_id: string
next_task_id: string
read_only_mode: true
}
rollups: {
total_cards: number
by_readiness: Record<string, number>
by_kind: Record<string, number>
verified_offsite_card_ids: string[]
blocked_escrow_card_ids: string[]
action_required_card_ids: string[]
execution_blocked_card_ids: string[]
}
readiness_cards: Array<{
card_id: string
target_id: string
display_name: string
kind: 'offsite_mirror' | 'credential_escrow' | 'k8s_resource_offsite'
readiness: 'verified' | 'action_required' | 'blocked'
offsite_status: string
escrow_status: string
restore_drill_status: string
credential_exposure_status: string
automation_gate_status: string
operator_summary: string
next_action: string
evidence_refs: string[]
blocked_operations: string[]
}>
operator_contract: {
display_mode: 'read_only_status'
success_notification_policy: string
failure_notification_policy: string
credential_display_policy: string
must_not_interpret_as: string[]
}
approval_boundaries: Record<string, false>
operation_boundaries: Record<string, boolean>
}

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 主線已完成到復原演練批准包,下一主線是異地 / escrow 準備度顯示 | 狀態分類、盤點 schema、權限矩陣、靜態盤點種子、只讀 API、UI 骨架、驗證、自動化待辦 schema / 快照 / API / 分組 UI、Backup / DR 目標盤點、準備度矩陣、備份通知政策、Backup / DR 證據 UI、復原演練批准包模板、Python 套件 / 供應鏈只讀基線、JS pnpm/npm 只讀基線、Docker build surface 只讀基線、CVE / license / drift 嚴重度政策、定期依賴漂移與外部資料來源檢查設計、依賴升級批准包模板已完成 |
| 工具 / 服務 / 套件 AI 自動化 | 100% | P0 已完成P1 套件 / 供應鏈主線已完成;備份 / DR 主線已完成到異地 / escrow 準備度顯示,下一主線是 P1-305 / P1-306 任務批准邊界與進度彙總細節 | 狀態分類、盤點 schema、權限矩陣、靜態盤點種子、只讀 API、UI 骨架、驗證、自動化待辦 schema / 快照 / API / 分組 UI、Backup / DR 目標盤點、準備度矩陣、備份通知政策、Backup / DR 證據 UI、復原演練批准包模板、異地 / escrow 準備度狀態、Python 套件 / 供應鏈只讀基線、JS pnpm/npm 只讀基線、Docker build surface 只讀基線、CVE / license / drift 嚴重度政策、定期依賴漂移與外部資料來源檢查設計、依賴升級批准包模板已完成 |
| 本工作清單與分析報告 | 100% | 已完成 | 本 MD 文件 |
整體計畫完成度:**100%**。
@@ -713,7 +713,7 @@ API
核心裁決:
- UI 只顯示備份目標、readiness matrix、通知政策、關鍵 blocker 與 evidence ref。
- `configs_capture``credential_escrow_markers` 仍為 blocked不得宣稱 full DR green
- `configs_capture``credential_escrow_markers` 仍為 blocked不得宣稱完整 DR 綠燈
- `signoz` 顯示 disruptive backup guardAgent 不得任意觸發會短暫停止 collector 的備份。
- 成功備份仍不即時送 Telegram / AwoooP成功狀態由每日摘要與查詢承載。
- warning、failed、action-required、core blocker 才能進即時升級。
@@ -786,6 +786,69 @@ API
- `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` 通過。
### P1-106 異地 / Escrow 準備度狀態摘要
正式 JSON Schema
- `docs/schemas/offsite_escrow_readiness_status_v1.schema.json`
正式 JSON Snapshot
- `docs/evaluations/offsite_escrow_readiness_status_2026-06-05.json`
API
- `GET /api/v1/agents/offsite-escrow-readiness-status`
UI
- `/zh-TW/governance?tab=automation-inventory`
狀態內容:
- 狀態卡:`3`
- 異地已驗證:`1``offsite_rclone_full_sync`
- 需處置:`1``velero_k8s_resources`
- Escrow 阻擋:`1``credential_escrow_markers`
- 執行阻擋:`3`
核心裁決:
- P1-106 只顯示異地 / escrow readiness不批准 offsite sync、restore、credential marker write 或 secret read。
- `offsite_rclone_full_sync` 可顯示 verified但 sync execution 仍 blocked成功不即時送 Telegram / AwoooP 洗版。
- `credential_escrow_markers` 必須維持 blockedUI 只能顯示 redacted marker metadata 與 evidence refs不得顯示 token、password、private key、cookie、authorization header、runner token、webhook secret、rclone credential 或 secret payload value。
- `velero_k8s_resources` 必須維持 action_requiredrestore drill 仍需 OpenClaw 仲裁與 HITL。
- P1-106 UI 可見不得被 Agent 或 operator 解讀為完整 DR 綠燈。
實作邊界:
- 不執行 backup。
- 不執行 restore。
- 不執行 offsite sync。
- 不寫 credential marker。
- 不讀 credential。
- 不輸出 secret 明文。
- 不改排程、不寫 workflow。
- 不發 Telegram 測試訊息。
- 不做 destructive prune。
- 不改生產路由。
驗證:
- `python3 -m json.tool docs/schemas/offsite_escrow_readiness_status_v1.schema.json` 通過。
- `python3 -m json.tool docs/evaluations/offsite_escrow_readiness_status_2026-06-05.json` 通過。
- `python3 -m json.tool docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json` 通過。
- `python3 -m json.tool docs/evaluations/ai_agent_automation_backlog_2026-06-04.json` 通過。
- `python3 -m json.tool apps/web/messages/zh-TW.json apps/web/messages/en.json` 通過。
- `PYTHONDONTWRITEBYTECODE=1 /Users/ogt/awoooi/apps/api/.venv/bin/python -m pytest apps/api/tests/test_offsite_escrow_readiness_status.py apps/api/tests/test_offsite_escrow_readiness_status_api.py apps/api/tests/test_ai_agent_automation_inventory_snapshot.py apps/api/tests/test_ai_agent_automation_inventory_snapshot_api.py apps/api/tests/test_ai_agent_automation_backlog_snapshot.py apps/api/tests/test_ai_agent_automation_backlog_snapshot_api.py -q``21 passed`
- `python3 -m py_compile apps/api/src/services/offsite_escrow_readiness_status.py apps/api/src/api/v1/agents.py apps/api/tests/test_offsite_escrow_readiness_status.py apps/api/tests/test_offsite_escrow_readiness_status_api.py apps/api/tests/test_ai_agent_automation_inventory_snapshot_api.py apps/api/tests/test_ai_agent_automation_backlog_snapshot_api.py` 通過。
- `/Users/ogt/awoooi/apps/web/node_modules/.bin/tsc --noEmit --tsBuildInfoFile /tmp/awoooi-p1-106-offsite-escrow-readiness-after-ff2.tsbuildinfo -p apps/web/tsconfig.json` 在 clean worktree 透過既有 node_modules toolchain 通過,不執行 `pnpm install`、不改 lockfile。
- `NEXT_PUBLIC_API_URL=https://awoooi.wooo.work NEXT_PRIVATE_BUILD_WORKER_COUNT=1 SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING=1 pnpm --filter @awoooi/web build` 通過;`/zh-TW/governance` First Load JS `381 kB`
- `python3 scripts/security/source-control-owner-response-guard.py --root .``SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK`
- `python3 scripts/security/security-mirror-progress-guard.py --root .``SECURITY_MIRROR_PROGRESS_GUARD_OK`
- `git diff --check` 通過。
- Production desktop / 390px mobile browser checks 需等 Gitea CD 正式部署後回寫 `docs/LOGBOOK.md`
### P0 - 治理與 Inventory 基礎
| ID | 狀態 | % | 負責 Agent | 任務 | 產出 | 關卡 |
@@ -820,7 +883,7 @@ API
| 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 | 完成 | 100 | OpenClaw | 定義復原演練批准包 | `docs/evaluations/backup_restore_drill_approval_package_template_2026-06-05.json` | 只讀模板 + 人工批准 |
| P1-106 | 待辦 | 0 | Hermes | 顯示異地 / escrow 準備度狀態 | DR 準備度區塊 | 不暴露 credential |
| P1-106 | 完成 | 100 | Hermes | 顯示異地 / escrow 準備度狀態 | `docs/evaluations/offsite_escrow_readiness_status_2026-06-05.json` + `/zh-TW/governance?tab=automation-inventory` | 不暴露 credential |
### P1 - 套件與供應鏈自動化
@@ -965,17 +1028,17 @@ API
```text
進度100%。
目前優先級P1。
目前任務P1-105 定義復原演練批准包
目前任務P1-106 顯示異地 / escrow 準備度狀態
狀態變更:待辦 -> 完成。
證據: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 準備度狀態
證據:offsite_escrow_readiness_status schema / snapshot JSON parse 通過service + API + inventory + backlog 目標測試 21 passedweb typecheck / build 通過API 端點為 GET /api/v1/agents/offsite-escrow-readiness-statusUI 已接入 governance automation inventory tab
阻擋backup、restore、offsite sync、credential marker、credential read、排程、workflow、Telegram 測試通知、secret 明文、生產路由仍未批准。
下一步P1-305 / P1-306 補每個任務的批准邊界與進度彙總細節
```
## 13. 立即執行順序
1. P1-106顯示異地 / escrow 準備度狀態
2. P1-305 / P1-306補每個任務的批准邊界與進度彙總細節
1. P1-305 / P1-306補每個任務的批准邊界與進度彙總細節
2. P1-001盤點 API / Web / Worker / K8s runtime surface
3. P2 / P3 必須等 P1 可見且關卡穩定後再做。
## 14. 目前風險

View File

@@ -1,33 +1,33 @@
{
"schema_version": "ai_agent_automation_backlog_v1",
"generated_at": "2026-06-05T05:36:00+08:00",
"generated_at": "2026-06-05T08:40: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-105",
"next_task_id": "P1-106",
"current_task_id": "P1-106",
"next_task_id": "P1-305",
"read_only_mode": true
},
"rollups": {
"total_items": 20,
"total_items": 21,
"by_priority": {
"P1": 18,
"P1": 19,
"P2": 1,
"P3": 1
},
"by_status": {
"planned": 7,
"done": 13
"done": 14
},
"by_gate_status": {
"read_only_allowed": 17,
"read_only_allowed": 18,
"production_change_blocked": 1,
"cost_approval_required": 1,
"blocked_by_evidence": 1
},
"by_owner_agent": {
"hermes": 10,
"hermes": 11,
"openclaw": 9,
"nemotron": 1
}
@@ -334,6 +334,34 @@
],
"next_review": "P1-105"
},
{
"item_id": "AUTO-P1-106",
"priority": "P1",
"status": "done",
"workstream_id": "WS4",
"source_asset_id": "offsite_escrow_readiness_status",
"source_signal_kind": "ui_visibility_gap",
"title": "顯示異地 / escrow 準備度狀態",
"owner_agent": "hermes",
"recommended_action": "建立 read-only offsite / escrow readiness status 與治理頁狀態區塊,顯示 offsite verified、credential escrow blocked、Velero action-required 與 credential redaction policy。",
"action_class": "execute_read_only",
"gate_status": "read_only_allowed",
"risk_level": "critical",
"evidence_refs": [
"docs/schemas/offsite_escrow_readiness_status_v1.schema.json",
"docs/evaluations/offsite_escrow_readiness_status_2026-06-05.json",
"GET /api/v1/agents/offsite-escrow-readiness-status",
"apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx"
],
"acceptance_criteria": [
"不執行 offsite sync、backup、restore 或 Velero restore",
"不寫 credential marker、不讀 credential、不輸出 secret 明文",
"UI 必須把 credential_escrow_markers 維持 blocked不能解讀成 full DR green",
"成功 offsite evidence 不即時送 Telegram / AwoooP 洗版",
"desktop 與 390px mobile 無橫向溢出"
],
"next_review": "P1-106"
},
{
"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-05T05:36:00+08:00",
"generated_at": "2026-06-05T08:40:00+08:00",
"program_status": {
"overall_completion_percent": 100,
"current_priority": "P1",
"current_task_id": "P1-105",
"next_task_id": "P1-106",
"current_task_id": "P1-106",
"next_task_id": "P1-305",
"read_only_mode": true
},
"status_taxonomy": {
@@ -423,9 +423,9 @@
{
"workstream_id": "WS4",
"display_name": "備份與 DR 自動化",
"completion_percent": 83,
"status": "in_progress",
"next_task_id": "P1-106"
"completion_percent": 100,
"status": "done",
"next_task_id": "P1-305"
},
{
"workstream_id": "WS5",
@@ -451,7 +451,7 @@
{
"workstream_id": "WS8",
"display_name": "產品 UI",
"completion_percent": 82,
"completion_percent": 86,
"status": "in_progress",
"next_task_id": "P1-305"
}
@@ -644,6 +644,17 @@
"gate_status": "read_only_allowed",
"next_action": "完成P1-106 顯示異地 / escrow 準備度狀態。"
},
{
"task_id": "P1-106",
"priority": "P1",
"status": "done",
"completion_percent": 100,
"owner_agent": "hermes",
"title": "顯示異地 / escrow 準備度狀態",
"output": "docs/evaluations/offsite_escrow_readiness_status_2026-06-05.json + /zh-TW/governance?tab=automation-inventory",
"gate_status": "read_only_allowed",
"next_action": "完成P1-305 / P1-306 補任務批准邊界與進度彙總細節。"
},
{
"task_id": "P1-201",
"priority": "P1",
@@ -856,6 +867,24 @@
"ref": "GET /api/v1/agents/backup-restore-drill-approval-package-template",
"result": "復原演練批准包模板只讀 API 已新增,只回傳 committed template不執行 backup、restore、offsite sync、不寫 credential marker、不送 Telegram 測試通知、不改生產路由。"
},
{
"evidence_id": "offsite_escrow_readiness_status_schema",
"kind": "schema",
"ref": "docs/schemas/offsite_escrow_readiness_status_v1.schema.json",
"result": "異地 / Escrow 準備度狀態 schema 已建立,明確禁止 offsite sync、credential marker 寫入、credential read、secret 明文、restore、workflow 寫入與 Telegram 測試通知。"
},
{
"evidence_id": "offsite_escrow_readiness_status_snapshot",
"kind": "doc",
"ref": "docs/evaluations/offsite_escrow_readiness_status_2026-06-05.json",
"result": "異地 / Escrow 準備度快照已建立offsite_rclone_full_sync verified、credential_escrow_markers blocked、velero_k8s_resources action_required全部執行型操作仍 blocked。"
},
{
"evidence_id": "offsite_escrow_readiness_status_api",
"kind": "api",
"ref": "GET /api/v1/agents/offsite-escrow-readiness-status",
"result": "異地 / Escrow 準備度只讀 API 已新增,只回傳 committed status不執行 backup、restore、offsite sync、不寫 credential marker、不讀 credential、不輸出 secret 明文。"
},
{
"evidence_id": "package_supply_chain_inventory_schema",
"kind": "schema",

View File

@@ -0,0 +1,163 @@
{
"schema_version": "offsite_escrow_readiness_status_v1",
"generated_at": "2026-06-05T08:40: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_restore_drill_approval_package_template_2026-06-05.json",
"docs/runbooks/OFFSITE-BACKUP-ESCROW-RUNBOOK.md",
"docs/HARD_RULES.md"
],
"program_status": {
"overall_completion_percent": 100,
"current_priority": "P1",
"current_task_id": "P1-106",
"next_task_id": "P1-305",
"read_only_mode": true
},
"rollups": {
"total_cards": 3,
"by_readiness": {
"verified": 1,
"action_required": 1,
"blocked": 1
},
"by_kind": {
"offsite_mirror": 1,
"credential_escrow": 1,
"k8s_resource_offsite": 1
},
"verified_offsite_card_ids": [
"offsite_rclone_full_sync"
],
"blocked_escrow_card_ids": [
"credential_escrow_markers"
],
"action_required_card_ids": [
"velero_k8s_resources"
],
"execution_blocked_card_ids": [
"offsite_rclone_full_sync",
"credential_escrow_markers",
"velero_k8s_resources"
]
},
"readiness_cards": [
{
"card_id": "offsite_rclone_full_sync",
"target_id": "offsite_rclone_full_sync",
"display_name": "Google Drive / rclone offsite mirror",
"kind": "offsite_mirror",
"readiness": "verified",
"offsite_status": "verified",
"escrow_status": "not_applicable",
"restore_drill_status": "not_applicable",
"credential_exposure_status": "not_applicable",
"automation_gate_status": "read_only_allowed",
"operator_summary": "latest-only remote mirror 證據已可見且已驗證,但 Agent 觸發異地同步仍維持阻擋。",
"next_action": "持續顯示 verify freshness任何新的 sync 執行都需要獨立人工批准。",
"evidence_refs": [
"scripts/backup/sync-offsite-backups.sh",
"scripts/backup/verify-offsite-full-sync.sh",
"docs/runbooks/BACKUP-STATUS.md"
],
"blocked_operations": [
"offsite_sync_execution",
"schedule_change",
"workflow_write",
"telegram_test_notification"
]
},
{
"card_id": "credential_escrow_markers",
"target_id": "credential_escrow_markers",
"display_name": "Credential escrow evidence markers",
"kind": "credential_escrow",
"readiness": "blocked",
"offsite_status": "not_applicable",
"escrow_status": "missing_markers",
"restore_drill_status": "blocked",
"credential_exposure_status": "redacted_only",
"automation_gate_status": "credential_approval_required",
"operator_summary": "5 個 escrow evidence marker 仍缺失UI 必須維持 blocked且不得暴露任何 credential value。",
"next_action": "顯示 blocked 狀態;任何 marker 更新都必須走 P1-105 credential escrow review package 與 HITL。",
"evidence_refs": [
"scripts/backup/mark-credential-escrow-verified.sh",
"scripts/backup/offsite-escrow-evidence-report.sh",
"docs/runbooks/BACKUP-STATUS.md",
"docs/evaluations/backup_restore_drill_approval_package_template_2026-06-05.json"
],
"blocked_operations": [
"credential_marker_write",
"credential_read",
"secret_plaintext_export",
"restore_execution",
"telegram_test_notification"
]
},
{
"card_id": "velero_k8s_resources",
"target_id": "velero_k8s_resources",
"display_name": "Velero K8s resource snapshots",
"kind": "k8s_resource_offsite",
"readiness": "action_required",
"offsite_status": "needs_metric_binding",
"escrow_status": "not_applicable",
"restore_drill_status": "approval_required",
"credential_exposure_status": "redacted_only",
"automation_gate_status": "restore_approval_required",
"operator_summary": "Velero / MinIO freshness 與 independent offsite evidence 仍需 metric binding才能進入 restore drill 升級判定。",
"next_action": "顯示 action-required 狀態restore drill 仍由 OpenClaw 仲裁與 HITL 批准阻擋。",
"evidence_refs": [
"docs/runbooks/BACKUP-STATUS.md",
"k8s/awoooi-prod/16-cronjob-backup-restore-test.yaml",
"docs/evaluations/backup_restore_drill_approval_package_template_2026-06-05.json"
],
"blocked_operations": [
"velero_restore",
"kubectl_apply",
"secret_restore",
"offsite_sync_execution",
"production_routing_change"
]
}
],
"operator_contract": {
"display_mode": "read_only_status",
"success_notification_policy": "已驗證的異地證據可進每日摘要;成功狀態不得觸發即時 Telegram / AwoooP 洗版。",
"failure_notification_policy": "escrow marker blocked、metric binding gap、verify failure 或 approval-required restore attempt 必須維持 action-required。",
"credential_display_policy": "只能顯示 redacted marker metadata 與 evidence refs禁止顯示 token、password、private key、cookie、authorization header、runner token、webhook secret、rclone credential 與 secret payload value。",
"must_not_interpret_as": [
"復原批准",
"異地同步批准",
"credential marker 寫入批准",
"secret 讀取批准",
"完整 DR 綠燈",
"生產路由批准"
]
},
"operation_boundaries": {
"read_only_status_allowed": true,
"backup_execution_allowed": false,
"restore_execution_allowed": false,
"offsite_sync_execution_allowed": false,
"credential_marker_write_allowed": false,
"credential_read_allowed": false,
"secret_plaintext_allowed": false,
"schedule_change_allowed": false,
"workflow_write_allowed": false,
"telegram_test_notification_allowed": false,
"destructive_prune_allowed": false,
"production_routing_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,324 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:awoooi:offsite-escrow-readiness-status-v1",
"title": "AWOOOI 異地 / Escrow 準備度狀態 v1",
"description": "異地備份、credential escrow 與 K8s resource offsite readiness 的只讀狀態。此 schema 不授權 offsite sync、credential marker 寫入、secret 讀取、restore、workflow 寫入、Telegram 測試通知或任何生產操作。",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"generated_at",
"source_refs",
"program_status",
"rollups",
"readiness_cards",
"operator_contract",
"operation_boundaries",
"approval_boundaries"
],
"properties": {
"schema_version": {
"const": "offsite_escrow_readiness_status_v1"
},
"generated_at": {
"type": "string"
},
"source_refs": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"program_status": {
"type": "object",
"additionalProperties": false,
"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": {
"enum": ["P0", "P1", "P2", "P3"]
},
"current_task_id": {
"const": "P1-106"
},
"next_task_id": {
"type": "string"
},
"read_only_mode": {
"const": true
}
}
},
"rollups": {
"type": "object",
"additionalProperties": false,
"required": [
"total_cards",
"by_readiness",
"by_kind",
"verified_offsite_card_ids",
"blocked_escrow_card_ids",
"action_required_card_ids",
"execution_blocked_card_ids"
],
"properties": {
"total_cards": {
"type": "integer",
"minimum": 0
},
"by_readiness": {
"type": "object",
"additionalProperties": {
"type": "integer",
"minimum": 0
}
},
"by_kind": {
"type": "object",
"additionalProperties": {
"type": "integer",
"minimum": 0
}
},
"verified_offsite_card_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"blocked_escrow_card_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"action_required_card_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"execution_blocked_card_ids": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"readiness_cards": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"card_id",
"target_id",
"display_name",
"kind",
"readiness",
"offsite_status",
"escrow_status",
"restore_drill_status",
"credential_exposure_status",
"automation_gate_status",
"operator_summary",
"next_action",
"evidence_refs",
"blocked_operations"
],
"properties": {
"card_id": {
"type": "string"
},
"target_id": {
"type": "string"
},
"display_name": {
"type": "string"
},
"kind": {
"enum": ["offsite_mirror", "credential_escrow", "k8s_resource_offsite"]
},
"readiness": {
"enum": ["verified", "action_required", "blocked"]
},
"offsite_status": {
"enum": ["verified", "needs_metric_binding", "blocked", "not_applicable"]
},
"escrow_status": {
"enum": ["verified", "missing_markers", "blocked", "not_applicable"]
},
"restore_drill_status": {
"enum": ["approval_required", "blocked", "not_applicable"]
},
"credential_exposure_status": {
"enum": ["redacted_only", "not_applicable"]
},
"automation_gate_status": {
"type": "string"
},
"operator_summary": {
"type": "string"
},
"next_action": {
"type": "string"
},
"evidence_refs": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"blocked_operations": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
}
}
}
},
"operator_contract": {
"type": "object",
"additionalProperties": false,
"required": [
"display_mode",
"success_notification_policy",
"failure_notification_policy",
"credential_display_policy",
"must_not_interpret_as"
],
"properties": {
"display_mode": {
"const": "read_only_status"
},
"success_notification_policy": {
"type": "string"
},
"failure_notification_policy": {
"type": "string"
},
"credential_display_policy": {
"type": "string"
},
"must_not_interpret_as": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"operation_boundaries": {
"type": "object",
"additionalProperties": false,
"required": [
"read_only_status_allowed",
"backup_execution_allowed",
"restore_execution_allowed",
"offsite_sync_execution_allowed",
"credential_marker_write_allowed",
"credential_read_allowed",
"secret_plaintext_allowed",
"schedule_change_allowed",
"workflow_write_allowed",
"telegram_test_notification_allowed",
"destructive_prune_allowed",
"production_routing_allowed"
],
"properties": {
"read_only_status_allowed": {
"const": true
},
"backup_execution_allowed": {
"const": false
},
"restore_execution_allowed": {
"const": false
},
"offsite_sync_execution_allowed": {
"const": false
},
"credential_marker_write_allowed": {
"const": false
},
"credential_read_allowed": {
"const": false
},
"secret_plaintext_allowed": {
"const": false
},
"schedule_change_allowed": {
"const": false
},
"workflow_write_allowed": {
"const": false
},
"telegram_test_notification_allowed": {
"const": false
},
"destructive_prune_allowed": {
"const": false
},
"production_routing_allowed": {
"const": false
}
}
},
"approval_boundaries": {
"type": "object",
"additionalProperties": false,
"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": {
"const": false
},
"paid_api_call_allowed": {
"const": false
},
"shadow_or_canary_allowed": {
"const": false
},
"production_routing_allowed": {
"const": false
},
"destructive_operation_allowed": {
"const": false
},
"restore_execution_allowed": {
"const": false
},
"offsite_sync_execution_allowed": {
"const": false
},
"credential_marker_write_allowed": {
"const": false
}
}
}
}
}

View File

@@ -3392,7 +3392,7 @@ Phase 6 完成後
2. P1-106顯示異地 / escrow 準備度狀態。
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 仲裁與人工批准邊界。
**裁決:** P1-104 已完成,但仍只屬於 read-only evidence surface。不得執行 backup、restore、offsite sync、credential marker 寫入、排程變更、workflow 寫入或 Telegram 測試通知;不得把 Backup / DR UI 可見解讀成完整 DR 綠燈。下一步只能產生復原演練與 escrow review 的批准包,必須保留 OpenClaw 仲裁與人工批准邊界。
### 2026-06-05 凌晨 (台北) — P1-105 復原演練批准包模板完成
@@ -3418,3 +3418,28 @@ Phase 6 完成後
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。
### 2026-06-05 上午 (台北) — P1-106 異地 / Escrow 準備度狀態完成
**觸發**:統帥批准繼續,要求依 `docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md` 的優先順序推進,並同步工作完成度與狀態。
**已推進:**
- P1-106建立異地 / Escrow 準備度只讀狀態,將 `offsite_rclone_full_sync``credential_escrow_markers``velero_k8s_resources` 從 Backup / DR readiness matrix 中抽成 operator 可掃描狀態卡。
- 新增 `docs/schemas/offsite_escrow_readiness_status_v1.schema.json`,明確禁止 offsite sync、credential marker 寫入、credential read、secret 明文、restore、workflow 寫入、Telegram 測試通知、destructive prune 與 production routing。
- 新增 `docs/evaluations/offsite_escrow_readiness_status_2026-06-05.json`3 張狀態卡中 `offsite_rclone_full_sync=verified``credential_escrow_markers=blocked``velero_k8s_resources=action_required`,三者執行型操作全部 blocked。
- 新增 `GET /api/v1/agents/offsite-escrow-readiness-status`,只讀取 committed snapshot不呼叫外部來源、不碰 DB/Redis、不執行 backup / restore / offsite sync。
- `/zh-TW/governance?tab=automation-inventory` 已接入異地 / Escrow 準備度區塊顯示狀態卡、blocked execution、credential redaction policy 與不可解讀為完整 DR 綠燈的契約。
- `docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json` 已將 `current_task_id` 推進到 `P1-106``next_task_id` 推進到 `P1-305`WS4 備份與 DR 自動化推進到 `100%`
- `docs/evaluations/ai_agent_automation_backlog_2026-06-04.json` 新增 `AUTO-P1-106` done itemrollup 更新為 total `21`、P1 `19`、done `14`、read_only_allowed `18`、Hermes owner `11`
**驗證:**
- P1-106 schema / snapshot JSON parse 通過。
- P1-106 service / API / inventory / backlog 目標測試 `21 passed`
- `py_compile`、web typecheck、Next build、security guards 與 `git diff --check` 通過。
- Production browser smoke 需等 Gitea CD 正式部署後回寫 `docs/LOGBOOK.md`
**下一步:**
1. P1-305 / P1-306補任務批准邊界與進度彙總細節。
2. P1-001盤點 API / Web / Worker / K8s runtime surface。
**裁決:** P1-106 只完成異地 / Escrow readiness 的 read-only status 與 UI 顯示,不代表 offsite sync、credential marker、restore drill 或完整 DR 綠燈已批准。成功 offsite evidence 不即時通知洗版blocked / action-required 仍只能進 action-required 與人工批准流程。