Compare commits

...

19 Commits

Author SHA1 Message Date
ogt
27d16a81d2 fix(ops): align 188 momo backup path in dev workspace [skip ci] 2026-06-27 11:41:40 +08:00
ogt
fb17197c0b feat(delivery): add closure workbench summary api 2026-06-26 18:46:52 +08:00
ogt
609dbf8983 feat(web): add delivery closure workbench 2026-06-26 18:27:51 +08:00
ogt
73586e2e9a docs(ops): prioritize delivery closure over gate sprawl 2026-06-26 18:02:40 +08:00
ogt
5d6b128854 feat(security): add GitHub private backup evidence gate 2026-06-26 17:45:00 +08:00
ogt
59485d519c docs(ops): record post-start wrapper live readback [skip ci] 2026-06-25 14:46:42 +08:00
ogt
596037cc4c docs(ops): add executable post-start quick check [skip ci] 2026-06-25 14:37:54 +08:00
ogt
5e439248b1 docs(ops): add post-start quick check SOP [skip ci] 2026-06-25 14:32:50 +08:00
ogt
454eed3915 docs(ops): record full cold-start green readback [skip ci] 2026-06-25 14:24:20 +08:00
ogt
88a9207759 docs(ops): record 11:53 cold-start refresh [skip ci] 2026-06-25 11:54:57 +08:00
ogt
cf42144c0b docs(ops): refresh momo preflight recovery evidence [skip ci] 2026-06-25 11:51:34 +08:00
ogt
480b9c4822 docs(ops): record 11:35 momo recovery readback [skip ci] 2026-06-25 11:38:10 +08:00
ogt
3b5f5582b5 docs(ops): record 11:21 recovery readback [skip ci] 2026-06-25 11:26:51 +08:00
ogt
e9a7831ff3 docs(ops): add momo preflight and cpu triage evidence [skip ci] 2026-06-25 11:06:22 +08:00
ogt
33ee5a7c8f docs(ops): harden momo drive token recovery gate [skip ci] 2026-06-25 10:44:23 +08:00
ogt
aacb26f9f8 docs(ops): record 10:35 cold-start freshness readback [skip ci] 2026-06-25 10:39:24 +08:00
ogt
d69f676b8c docs(ops): record momo fail-closed scheduler proof [skip ci] 2026-06-25 10:29:15 +08:00
ogt
9603e4d403 docs(ops): record momo drive auth recovery readback [skip ci] 2026-06-25 09:41:42 +08:00
ogt
a26b9f2c89 docs(ops): record 2026-06-25 cold-start readback [skip ci] 2026-06-25 09:16:40 +08:00
25 changed files with 4081 additions and 48 deletions

View File

@@ -310,12 +310,18 @@ from src.services.dependency_supply_chain_drift_monitor import (
from src.services.dependency_upgrade_approval_package_template import (
load_latest_dependency_upgrade_approval_package_template,
)
from src.services.delivery_closure_workbench import (
load_delivery_closure_workbench,
)
from src.services.docker_build_surface_inventory import (
load_latest_docker_build_surface_inventory,
)
from src.services.gitea_workflow_runner_health import (
load_latest_gitea_workflow_runner_health,
)
from src.services.github_target_private_backup_evidence_gate import (
load_latest_github_target_private_backup_evidence_gate,
)
from src.services.host_runaway_aiops_loop_readiness import (
load_latest_host_runaway_aiops_loop_readiness,
)
@@ -796,6 +802,36 @@ async def get_awoooi_status_cleanup_dashboard() -> dict[str, Any]:
) from exc
@router.get(
"/delivery-closure-workbench",
response_model=dict[str, Any],
summary="取得交付閉環工作台彙總",
description=(
"彙總 AWOOOI 狀態清理、GitHub 私有備援、Gitea / CI-CD、Runtime surface "
"與 Backup / DR 的既有只讀快照,回傳前端工作台可直接使用的交付主線、"
"完成度、阻擋數與下一步。此端點不呼叫 GitHub / Gitea / Wazuh / K8s live API、"
"不建立 repo、不改 visibility、不同步 refs、不觸發 workflow、不執行備份或還原、"
"不讀 secret、不做 runtime write。"
),
)
async def get_delivery_closure_workbench() -> dict[str, Any]:
"""回傳 AWOOOI 交付閉環工作台只讀彙總。"""
try:
payload = await asyncio.to_thread(load_delivery_closure_workbench)
return redact_public_lan_topology(payload)
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("delivery_closure_workbench_invalid", error=str(exc))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="AWOOOI 交付閉環工作台彙總無效",
) from exc
@router.get(
"/agent-12-agent-war-room",
response_model=dict[str, Any],
@@ -3109,6 +3145,33 @@ async def get_gitea_workflow_runner_health() -> dict[str, Any]:
) from exc
@router.get(
"/github-target-private-backup-evidence-gate",
response_model=dict[str, Any],
summary="取得 GitHub 私有備援 evidence gate",
description=(
"讀取最新已提交的 GitHub target private backup evidence gate"
"此端點不呼叫 GitHub / Gitea API、不建立 repo、不修改 visibility、"
"不同步 refs、不觸發 workflow、不切 GitHub primary、不讀取或保存 secret value。"
),
)
async def get_github_target_private_backup_evidence_gate() -> dict[str, Any]:
"""Return the latest read-only GitHub private backup evidence gate."""
try:
return await asyncio.to_thread(load_latest_github_target_private_backup_evidence_gate)
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("github_target_private_backup_evidence_gate_invalid", error=str(exc))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="GitHub 私有備援 evidence gate 快照無效",
) from exc
@router.get(
"/observability-contract-matrix",
response_model=dict[str, Any],

View File

@@ -0,0 +1,289 @@
"""Delivery closure workbench summary.
Builds the product-facing delivery closure view from existing committed,
read-only snapshots. The summary is intentionally compact so the UI does not
need to fan out across five separate endpoints or duplicate blocker math.
"""
from __future__ import annotations
from typing import Any
from src.services.awoooi_status_cleanup_dashboard import (
load_latest_awoooi_status_cleanup_dashboard,
)
from src.services.backup_dr_readiness_matrix import (
load_latest_backup_dr_readiness_matrix,
)
from src.services.gitea_workflow_runner_health import (
load_latest_gitea_workflow_runner_health,
)
from src.services.github_target_private_backup_evidence_gate import (
load_latest_github_target_private_backup_evidence_gate,
)
from src.services.runtime_surface_inventory import (
load_latest_runtime_surface_inventory,
)
_SCHEMA_VERSION = "delivery_closure_workbench_v1"
def load_delivery_closure_workbench() -> dict[str, Any]:
"""Load existing delivery snapshots and return a compact workbench model."""
status_cleanup = load_latest_awoooi_status_cleanup_dashboard()
github = load_latest_github_target_private_backup_evidence_gate()
gitea = load_latest_gitea_workflow_runner_health()
runtime = load_latest_runtime_surface_inventory()
backup = load_latest_backup_dr_readiness_matrix()
return build_delivery_closure_workbench(
status_cleanup=status_cleanup,
github=github,
gitea=gitea,
runtime=runtime,
backup=backup,
)
def build_delivery_closure_workbench(
*,
status_cleanup: dict[str, Any],
github: dict[str, Any],
gitea: dict[str, Any],
runtime: dict[str, Any],
backup: dict[str, Any],
) -> dict[str, Any]:
"""Build the delivery workbench response from already validated snapshots."""
status_summary = _dict(status_cleanup.get("summary"))
github_summary = _dict(github.get("summary"))
gitea_status = _dict(gitea.get("program_status"))
gitea_rollups = _dict(gitea.get("rollups"))
runtime_status = _dict(runtime.get("program_status"))
runtime_rollups = _dict(runtime.get("rollups"))
backup_status = _dict(backup.get("program_status"))
backup_rollups = _dict(backup.get("rollups"))
github_required = _int(github_summary.get("approval_required_target_count"))
github_verified = _int(github_summary.get("private_backup_verified_count"))
runtime_action_required = set(_strings(runtime_rollups.get("action_required_surface_ids")))
runtime_secret_surfaces = set(_strings(runtime_rollups.get("secret_surface_ids")))
lanes = [
{
"id": "release",
"source_id": "status_cleanup",
"completion_percent": _percent(status_summary.get("overall_completion_percent")),
"status": str(status_summary.get("dashboard_status") or "unknown"),
"blocker_count": _int(status_summary.get("blocked_gate_count")),
"metric": {
"kind": "blocked_gate",
"blocked": _int(status_summary.get("blocked_gate_count")),
"total": _int(status_summary.get("gate_count")),
},
"href": "/governance?tab=automation-inventory",
"next_action": _first_string(status_cleanup.get("next_actions")),
},
{
"id": "github",
"source_id": "github_private_backup",
"completion_percent": _percent(
(github_verified / github_required) * 100 if github_required else 0
),
"status": str(github.get("status") or "unknown"),
"blocker_count": _int(github_summary.get("blocked_target_count")),
"metric": {
"kind": "private_backup_verified",
"verified": github_verified,
"total": github_required,
},
"href": "/governance?tab=automation-inventory",
"next_action": _first_target_action(github.get("targets")),
},
{
"id": "gitea",
"source_id": "gitea_ci_cd",
"completion_percent": _percent(gitea_status.get("overall_completion_percent")),
"status": str(gitea_status.get("current_task_id") or "unknown"),
"blocker_count": len(_strings(gitea_rollups.get("runner_contracts_requiring_action"))),
"metric": {
"kind": "workflow_count",
"count": _int(gitea_rollups.get("total_workflows")),
},
"href": "/deployments",
"next_action": _first_contract_action(gitea.get("runner_contracts")),
},
{
"id": "runtime",
"source_id": "runtime_surface",
"completion_percent": _percent(runtime_status.get("overall_completion_percent")),
"status": str(runtime_status.get("current_task_id") or "unknown"),
"blocker_count": len(runtime_action_required | runtime_secret_surfaces),
"metric": {
"kind": "surface_count",
"total": _int(runtime_rollups.get("total_surfaces")),
},
"href": "/governance?tab=automation-inventory",
"next_action": _first_surface_action(runtime.get("runtime_surfaces")),
},
{
"id": "backup",
"source_id": "backup_dr",
"completion_percent": _percent(backup_status.get("overall_completion_percent")),
"status": str(backup_status.get("current_task_id") or "unknown"),
"blocker_count": len(_strings(backup_rollups.get("blocked_row_ids"))),
"metric": {
"kind": "readiness_row_count",
"rows": _int(backup_rollups.get("total_rows")),
},
"href": "/operations",
"next_action": _first_backup_action(backup.get("readiness_rows")),
},
]
for lane in lanes:
lane["tone"] = _tone(_int(lane["blocker_count"]), _int(lane["completion_percent"]))
source_statuses = [
_source_status("status_cleanup", status_cleanup),
_source_status("github_private_backup", github),
_source_status("gitea_ci_cd", gitea),
_source_status("runtime_surface", runtime),
_source_status("backup_dr", backup),
]
generated_candidates = [source["generated_at"] for source in source_statuses if source["generated_at"]]
high_risk_blocker_count = sum(_int(lane["blocker_count"]) for lane in lanes)
average_completion = _percent(
sum(_int(lane["completion_percent"]) for lane in lanes) / max(len(lanes), 1)
)
next_focus = [
{
"lane_id": lane["id"],
"blocker_count": lane["blocker_count"],
"completion_percent": lane["completion_percent"],
"next_action": lane["next_action"],
}
for lane in lanes
if _int(lane["blocker_count"]) > 0 or _int(lane["completion_percent"]) < 80
][:5]
return {
"schema_version": _SCHEMA_VERSION,
"generated_at": max(generated_candidates) if generated_candidates else "",
"status": "blocked_delivery_actions_required" if high_risk_blocker_count else "ready",
"summary": {
"source_count": len(source_statuses),
"loaded_source_count": len(source_statuses),
"average_completion_percent": average_completion,
"high_risk_blocker_count": high_risk_blocker_count,
"runtime_execution_authorized": False,
"remote_write_authorized": False,
"repo_creation_authorized": False,
"refs_sync_authorized": False,
"workflow_trigger_authorized": False,
"secret_values_collected": False,
},
"source_statuses": source_statuses,
"lanes": lanes,
"next_focus": next_focus,
"operation_boundaries": {
"read_only_api_allowed": True,
"runtime_write_allowed": False,
"remote_write_allowed": False,
"repo_creation_allowed": False,
"visibility_change_allowed": False,
"refs_sync_allowed": False,
"workflow_trigger_allowed": False,
"secret_value_collection_allowed": False,
"backup_restore_execution_allowed": False,
"active_scan_allowed": False,
},
}
def _source_status(source_id: str, payload: dict[str, Any]) -> dict[str, Any]:
return {
"id": source_id,
"loaded": True,
"schema_version": str(payload.get("schema_version") or ""),
"generated_at": str(payload.get("generated_at") or ""),
}
def _tone(blocker_count: int, percent: int) -> str:
if blocker_count > 0:
return "danger"
if percent < 80:
return "warn"
return "ok"
def _dict(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _int(value: Any) -> int:
if isinstance(value, bool):
return int(value)
if isinstance(value, (int, float)):
return int(value)
return 0
def _percent(value: Any) -> int:
return max(0, min(100, round(float(value or 0))))
def _strings(value: Any) -> list[str]:
if not isinstance(value, list):
return []
return [str(item) for item in value if item is not None]
def _first_string(value: Any) -> str:
if isinstance(value, list) and value:
return str(value[0])
return ""
def _first_target_action(value: Any) -> str:
if not isinstance(value, list):
return ""
for row in value:
if isinstance(row, dict) and row.get("approval_required") is True:
return str(row.get("next_action") or "")
return _first_row_action(value)
def _first_contract_action(value: Any) -> str:
if not isinstance(value, list):
return ""
for row in value:
if isinstance(row, dict) and row.get("status") == "action_required":
return str(row.get("next_action") or "")
return _first_row_action(value)
def _first_surface_action(value: Any) -> str:
if not isinstance(value, list):
return ""
for row in value:
if isinstance(row, dict) and row.get("status") != "manifest_mapped":
return str(row.get("next_action") or "")
return _first_row_action(value)
def _first_backup_action(value: Any) -> str:
if not isinstance(value, list):
return ""
for row in value:
if isinstance(row, dict) and row.get("overall_readiness") in {"blocked", "action_required"}:
return str(row.get("next_action") or "")
return _first_row_action(value)
def _first_row_action(value: Any) -> str:
if not isinstance(value, list):
return ""
for row in value:
if isinstance(row, dict) and row.get("next_action"):
return str(row["next_action"])
return ""

View File

@@ -0,0 +1,192 @@
"""GitHub target private backup evidence gate snapshot loader.
Loads the committed, read-only GitHub private backup evidence gate. This module
never calls GitHub / Gitea, creates repos, changes visibility, syncs refs,
triggers workflows, switches primary source control, or reads secret values.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from src.services.snapshot_paths import resolve_repo_root
_DEFAULT_SECURITY_DIR = resolve_repo_root(Path(__file__)) / "docs" / "security"
_SNAPSHOT_NAME = "github-target-private-backup-evidence-gate.snapshot.json"
_SCHEMA_VERSION = "github_target_private_backup_evidence_gate_v1"
def load_latest_github_target_private_backup_evidence_gate(
security_dir: Path | None = None,
) -> dict[str, Any]:
"""Load the committed GitHub private backup evidence gate snapshot."""
directory = security_dir or _DEFAULT_SECURITY_DIR
latest = directory / _SNAPSHOT_NAME
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, str(latest))
_require_summary_consistency(payload, str(latest))
_require_fail_closed_boundaries(payload, str(latest))
_require_target_gate_consistency(payload, str(latest))
_require_no_secret_payload_keys(payload, str(latest))
return payload
def _require_schema(payload: dict[str, Any], label: str) -> None:
actual = payload.get("schema_version")
if actual != _SCHEMA_VERSION:
raise ValueError(f"{label}: expected schema_version={_SCHEMA_VERSION}, got {actual!r}")
def _require_summary_consistency(payload: dict[str, Any], label: str) -> None:
summary = payload.get("summary") or {}
targets = payload.get("targets") or []
approval_targets = [target for target in targets if target.get("approval_required") is True]
public_visible = [
target
for target in approval_targets
if str(target.get("probe_status") or "").startswith("exists")
]
not_found_or_private = [
target
for target in approval_targets
if target.get("probe_status") == "not_found_or_private"
]
expected = {
"target_decision_count": len(targets),
"approval_required_target_count": len(approval_targets),
"public_probe_visible_target_count": len(public_visible),
"not_found_or_private_target_count": len(not_found_or_private),
"private_backup_verified_count": 0,
"private_visibility_evidence_missing_count": len(approval_targets),
"safe_credential_required_count": len(approval_targets),
"safe_credential_accepted_evidence_count": 0,
"execution_ready_count": 0,
"blocked_target_count": len(approval_targets),
}
mismatches = {
key: {"expected": value, "actual": summary.get(key)}
for key, value in expected.items()
if summary.get(key) != value
}
if mismatches:
raise ValueError(f"{label}: summary mismatch {mismatches}")
if summary.get("public_repo_allowed") is not False:
raise ValueError(f"{label}: public_repo_allowed must stay false")
if summary.get("not_found_or_private_as_absent_allowed") is not False:
raise ValueError(f"{label}: not_found_or_private_as_absent_allowed must stay false")
def _require_fail_closed_boundaries(payload: dict[str, Any], label: str) -> None:
boundaries = payload.get("operation_boundaries") or {}
if boundaries.get("read_only_api_allowed") is not True:
raise ValueError(f"{label}: read_only_api_allowed must be true")
blocked_operation_flags = {
"github_api_write_allowed",
"gitea_api_write_allowed",
"repo_creation_allowed",
"visibility_change_allowed",
"refs_sync_allowed",
"workflow_modification_allowed",
"workflow_trigger_allowed",
"github_primary_switch_allowed",
"secret_value_collection_allowed",
"private_clone_url_collection_allowed",
}
allowed_operations = sorted(
flag for flag in blocked_operation_flags if boundaries.get(flag) is not False
)
if allowed_operations:
raise ValueError(f"{label}: operation boundaries must remain false: {allowed_operations}")
flags = payload.get("authorization_flags") or {}
allowed_flags = sorted(flag for flag, value in flags.items() if value is not False)
if allowed_flags:
raise ValueError(f"{label}: authorization flags must remain false: {allowed_flags}")
summary = payload.get("summary") or {}
false_summary_flags = {
"repo_creation_authorized",
"visibility_change_authorized",
"refs_sync_authorized",
"github_primary_switch_authorized",
"workflow_modification_authorized",
"workflow_trigger_authorized",
"secret_value_collection_allowed",
"private_clone_url_collection_allowed",
}
allowed_summary = sorted(flag for flag in false_summary_flags if summary.get(flag) is not False)
if allowed_summary:
raise ValueError(f"{label}: summary authorization flags must remain false: {allowed_summary}")
def _require_target_gate_consistency(payload: dict[str, Any], label: str) -> None:
targets = payload.get("targets") or []
for target in targets:
if target.get("approval_required") is not True:
continue
repo = target.get("github_repo")
if target.get("private_backup_verified") is not False:
raise ValueError(f"{label}: {repo} private_backup_verified must stay false")
if target.get("refs_sync_ready") is not False or target.get("execution_ready") is not False:
raise ValueError(f"{label}: {repo} refs_sync_ready/execution_ready must stay false")
if not target.get("blockers"):
raise ValueError(f"{label}: {repo} must keep blockers until owner evidence is accepted")
false_flags = {
"repo_creation_authorized",
"visibility_change_authorized",
"refs_sync_authorized",
"github_primary_switch_authorized",
"secret_values_collected",
}
allowed = sorted(flag for flag in false_flags if target.get(flag) is not False)
if allowed:
raise ValueError(f"{label}: {repo} target flags must remain false: {allowed}")
status = str(target.get("visibility_evidence_status") or "")
probe_status = str(target.get("probe_status") or "")
if probe_status.startswith("exists") and "public_probe_visible" not in status:
raise ValueError(f"{label}: {repo} public probe visibility must be blocked")
if probe_status == "not_found_or_private" and "not_verified" not in status:
raise ValueError(f"{label}: {repo} not_found_or_private must not be private verification")
def _require_no_secret_payload_keys(payload: Any, label: str, path: str = "$") -> None:
forbidden_fragments = {
"token",
"secret",
"private_key",
"cookie",
"session",
"credential",
"authorization",
"clone_url",
}
if isinstance(payload, dict):
for key, value in payload.items():
normalized = str(key).lower()
if any(fragment in normalized for fragment in forbidden_fragments):
if normalized not in {
"secret_value_collection_allowed",
"secret_values_collected",
"private_clone_url_collection_allowed",
"safe_credential_evidence_status",
"safe_credential_evidence_ref",
"safe_credential_required_count",
"safe_credential_accepted_evidence_count",
"forbidden_action_count",
"authorization_flags",
}:
raise ValueError(f"{label}: forbidden secret payload key at {path}.{key}")
_require_no_secret_payload_keys(value, label, f"{path}.{key}")
elif isinstance(payload, list):
for index, value in enumerate(payload):
_require_no_secret_payload_keys(value, label, f"{path}[{index}]")

View File

@@ -0,0 +1,52 @@
from __future__ import annotations
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.v1.agents import router
def test_delivery_closure_workbench_endpoint_returns_product_summary():
app = FastAPI()
app.include_router(router, prefix="/api/v1")
client = TestClient(app)
response = client.get("/api/v1/agents/delivery-closure-workbench")
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == "delivery_closure_workbench_v1"
assert data["summary"]["source_count"] == 5
assert data["summary"]["loaded_source_count"] == 5
assert data["summary"]["runtime_execution_authorized"] is False
assert data["summary"]["remote_write_authorized"] is False
assert data["summary"]["repo_creation_authorized"] is False
assert data["summary"]["refs_sync_authorized"] is False
assert data["summary"]["workflow_trigger_authorized"] is False
assert data["summary"]["secret_values_collected"] is False
assert data["summary"]["average_completion_percent"] >= 0
assert data["summary"]["high_risk_blocker_count"] > 0
lanes = {lane["id"]: lane for lane in data["lanes"]}
assert sorted(lanes) == ["backup", "gitea", "github", "release", "runtime"]
assert lanes["release"]["metric"]["kind"] == "blocked_gate"
assert lanes["github"]["metric"]["kind"] == "private_backup_verified"
assert lanes["gitea"]["metric"]["kind"] == "workflow_count"
assert lanes["runtime"]["metric"]["kind"] == "surface_count"
assert lanes["backup"]["metric"]["kind"] == "readiness_row_count"
assert all(0 <= lane["completion_percent"] <= 100 for lane in lanes.values())
assert all(lane["tone"] in {"ok", "warn", "danger"} for lane in lanes.values())
boundaries = data["operation_boundaries"]
assert boundaries["read_only_api_allowed"] is True
assert boundaries["runtime_write_allowed"] is False
assert boundaries["remote_write_allowed"] is False
assert boundaries["repo_creation_allowed"] is False
assert boundaries["visibility_change_allowed"] is False
assert boundaries["refs_sync_allowed"] is False
assert boundaries["workflow_trigger_allowed"] is False
assert boundaries["secret_value_collection_allowed"] is False
assert boundaries["backup_restore_execution_allowed"] is False
assert boundaries["active_scan_allowed"] is False
assert "192.168.0." not in response.text

View File

@@ -0,0 +1,171 @@
from __future__ import annotations
import json
from pathlib import Path
import pytest
from src.services.github_target_private_backup_evidence_gate import (
load_latest_github_target_private_backup_evidence_gate,
)
def test_load_latest_github_target_private_backup_evidence_gate_reads_committed_snapshot():
data = load_latest_github_target_private_backup_evidence_gate()
assert data["schema_version"] == "github_target_private_backup_evidence_gate_v1"
assert data["status"] == "blocked_public_visibility_and_safe_credential_evidence_required"
assert data["summary"]["approval_required_target_count"] == 9
assert data["summary"]["public_probe_visible_target_count"] == 4
assert data["summary"]["not_found_or_private_target_count"] == 5
assert data["summary"]["private_backup_verified_count"] == 0
assert data["summary"]["safe_credential_accepted_evidence_count"] == 0
assert data["summary"]["safe_credential_required_count"] == 9
assert data["summary"]["refs_sync_authorized"] is False
assert data["summary"]["public_repo_allowed"] is False
assert data["operation_boundaries"]["read_only_api_allowed"] is True
assert data["operation_boundaries"]["repo_creation_allowed"] is False
assert data["authorization_flags"]["repo_creation_authorized"] is False
assert data["authorization_flags"]["secret_values_collected"] is False
def test_private_backup_gate_rejects_runtime_authorization(tmp_path: Path):
snapshot = _snapshot()
snapshot["authorization_flags"]["refs_sync_authorized"] = True
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="authorization flags"):
load_latest_github_target_private_backup_evidence_gate(tmp_path)
def test_private_backup_gate_rejects_summary_drift(tmp_path: Path):
snapshot = _snapshot()
snapshot["summary"]["private_backup_verified_count"] = 1
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="summary mismatch"):
load_latest_github_target_private_backup_evidence_gate(tmp_path)
def test_private_backup_gate_rejects_public_target_marked_private(tmp_path: Path):
snapshot = _snapshot()
snapshot["targets"][0]["private_backup_verified"] = True
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="private_backup_verified"):
load_latest_github_target_private_backup_evidence_gate(tmp_path)
def test_private_backup_gate_rejects_not_found_as_verified(tmp_path: Path):
snapshot = _snapshot()
snapshot["targets"][1]["visibility_evidence_status"] = "private_verified"
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="not_found_or_private"):
load_latest_github_target_private_backup_evidence_gate(tmp_path)
def _write_snapshot(directory: Path, snapshot: dict) -> None:
(directory / "github-target-private-backup-evidence-gate.snapshot.json").write_text(
json.dumps(snapshot),
encoding="utf-8",
)
def _snapshot() -> dict:
return {
"schema_version": "github_target_private_backup_evidence_gate_v1",
"generated_at": "2026-06-26T00:00:00+00:00",
"status": "blocked_public_visibility_and_safe_credential_evidence_required",
"mode": "read_only_private_backup_evidence_gate",
"source_reviews": {},
"summary": {
"target_decision_count": 3,
"approval_required_target_count": 2,
"approval_package_item_count": 2,
"public_probe_visible_target_count": 1,
"not_found_or_private_target_count": 1,
"private_backup_verified_count": 0,
"private_visibility_evidence_missing_count": 2,
"safe_credential_required_count": 2,
"safe_credential_accepted_evidence_count": 0,
"owner_response_received_count": 0,
"owner_response_accepted_count": 0,
"execution_ready_count": 0,
"blocked_target_count": 2,
"external_scope_target_count": 1,
"forbidden_action_count": 12,
"repo_creation_authorized": False,
"visibility_change_authorized": False,
"refs_sync_authorized": False,
"github_primary_switch_authorized": False,
"workflow_modification_authorized": False,
"workflow_trigger_authorized": False,
"secret_value_collection_allowed": False,
"private_clone_url_collection_allowed": False,
"not_found_or_private_as_absent_allowed": False,
"public_repo_allowed": False,
},
"targets": [
_target("owenhytsai/awoooi", True, "exists", "blocked_public_probe_visible_private_evidence_required"),
_target("owenhytsai/VibeWork", True, "not_found_or_private", "blocked_private_or_absent_not_verified"),
_target("nexu-io/open-design", False, "exists", "external_scope_not_backup_target"),
],
"acceptance_requirements": ["private evidence required"],
"rejection_rules": ["reject secrets"],
"operation_boundaries": {
"read_only_api_allowed": True,
"github_api_write_allowed": False,
"gitea_api_write_allowed": False,
"repo_creation_allowed": False,
"visibility_change_allowed": False,
"refs_sync_allowed": False,
"workflow_modification_allowed": False,
"workflow_trigger_allowed": False,
"github_primary_switch_allowed": False,
"secret_value_collection_allowed": False,
"private_clone_url_collection_allowed": False,
},
"authorization_flags": {
"runtime_execution_authorized": False,
"repo_creation_authorized": False,
"visibility_change_authorized": False,
"refs_sync_authorized": False,
"workflow_modification_authorized": False,
"workflow_trigger_authorized": False,
"github_primary_switch_authorized": False,
"secret_values_collected": False,
},
}
def _target(
repo: str,
approval_required: bool,
probe_status: str,
visibility_status: str,
) -> dict:
return {
"github_repo": repo,
"source_key": repo,
"approval_required": approval_required,
"probe_status": probe_status,
"target_state": probe_status,
"risk": "HIGH",
"visibility_evidence_status": visibility_status,
"private_backup_verified": False,
"private_visibility_owner_evidence_ref": None,
"safe_credential_evidence_status": "missing_safe_credential_metadata",
"safe_credential_evidence_ref": None,
"owner_response_accepted": False,
"refs_sync_ready": False,
"execution_ready": False,
"blockers": ["blocked"],
"evidence_refs": ["docs/security/github-target-decision.snapshot.json"],
"forbidden_actions": ["push_refs"],
"repo_creation_authorized": False,
"visibility_change_authorized": False,
"refs_sync_authorized": False,
"github_primary_switch_authorized": False,
"secret_values_collected": False,
}

View File

@@ -0,0 +1,44 @@
from __future__ import annotations
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.v1.agents import router
def test_github_target_private_backup_evidence_gate_endpoint_returns_committed_snapshot():
app = FastAPI()
app.include_router(router, prefix="/api/v1")
client = TestClient(app)
response = client.get("/api/v1/agents/github-target-private-backup-evidence-gate")
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == "github_target_private_backup_evidence_gate_v1"
assert data["status"] == "blocked_public_visibility_and_safe_credential_evidence_required"
assert data["summary"]["approval_required_target_count"] == 9
assert data["summary"]["public_probe_visible_target_count"] == 4
assert data["summary"]["not_found_or_private_target_count"] == 5
assert data["summary"]["private_backup_verified_count"] == 0
assert data["summary"]["safe_credential_accepted_evidence_count"] == 0
assert data["summary"]["execution_ready_count"] == 0
assert data["summary"]["public_repo_allowed"] is False
assert data["operation_boundaries"]["read_only_api_allowed"] is True
assert data["operation_boundaries"]["repo_creation_allowed"] is False
assert data["operation_boundaries"]["refs_sync_allowed"] is False
assert data["operation_boundaries"]["workflow_trigger_allowed"] is False
assert data["authorization_flags"]["runtime_execution_authorized"] is False
assert data["authorization_flags"]["repo_creation_authorized"] is False
assert data["authorization_flags"]["secret_values_collected"] is False
public_target = next(target for target in data["targets"] if target["github_repo"] == "owenhytsai/awoooi")
assert public_target["visibility_evidence_status"] == (
"blocked_public_probe_visible_private_evidence_required"
)
private_or_absent_target = next(
target for target in data["targets"] if target["github_repo"] == "owenhytsai/VibeWork"
)
assert private_or_absent_target["visibility_evidence_status"] == (
"blocked_private_or_absent_not_verified"
)

View File

@@ -63,6 +63,7 @@
"drift": "漂移偵測",
"neuralCommand": "神經指揮中心",
"commandCenter": "指令中心",
"delivery": "交付閉環",
"observability": "可觀測性",
"automation": "自動化",
"operations": "營運",
@@ -80,6 +81,82 @@
"iwooos": "IwoooS",
"iwooosSecurityCompliance": "IwoooS 安全合規"
},
"delivery": {
"eyebrow": "AWOOOI Delivery",
"title": "交付閉環工作台",
"subtitle": "把目前真正會推進交付的主線集中在同一頁:乾淨 release、GitHub 私有備援、Gitea / CI/CD、runtime surface、資料與備份。這裡只顯示能推動下一步的狀態不再把文件數量當成完成度。",
"states": {
"loading": "讀取中",
"ready": "資料鏈正常",
"partial": "部分資料待補",
"noData": "尚未回讀"
},
"actions": {
"refresh": "重新整理"
},
"sources": {
"0": { "error": "狀態清理資料未回讀" },
"1": { "error": "GitHub 備援資料未回讀" },
"2": { "error": "Gitea / runner 資料未回讀" },
"3": { "error": "Runtime surface 資料未回讀" },
"4": { "error": "Backup readiness 資料未回讀" }
},
"metrics": {
"loaded": "資料來源",
"loadedDetail": "只計入正式 API 成功回讀的來源。",
"completion": "平均進度",
"completionDetail": "以本頁五條交付主線計算。",
"blockers": "高風險阻擋",
"blockersDetail": "只統計會影響交付決策的阻擋。",
"execution": "執行授權",
"executionDetail": "本頁只讀,不執行 runtime 或 remote write。"
},
"sections": {
"lanes": "交付主線",
"lanesDetail": "每張卡只回答完成度、阻擋數、下一步入口。",
"next": "下一步焦點",
"nextDetail": "只列需要處理的主線,不列文件清單。",
"boundary": "保留硬邊界",
"boundaryDetail": "這些仍需明確授權,但不得阻擋低風險 coding / UI / test。"
},
"lanes": {
"release": {
"title": "乾淨 release 工作流",
"description": "把可交付的變更切出乾淨分支與可驗證提交,避免髒分支整包推送。",
"metric": "阻擋 gate {blocked}"
},
"github": {
"title": "GitHub 私有備援",
"description": "所有備援 repo 必須私有,公開可讀或未驗證 private 都不能標綠。",
"metric": "已驗證 {verified}/{total}"
},
"gitea": {
"title": "Gitea / CI-CD",
"description": "確認 workflow、runner label、通知與 dev / prod 發版線是真實可跑。",
"metric": "workflow {count}"
},
"runtime": {
"title": "Runtime surface",
"description": "把產品、網站、服務與部署面映射到實際 runtime不再停在文件描述。",
"metric": "surface {total}"
},
"backup": {
"title": "資料與備份",
"description": "資料庫、類資料庫、備份與還原演練必須能支撐一鍵上雲與獨立部署。",
"metric": "readiness row {rows}"
}
},
"boundaries": {
"secret": "不收 secret value、token、private key、cookie 或 private clone credential。",
"production": "不直接改 production runtime、public gateway、Nginx、Docker、K8s 或 firewall。",
"repo": "不直接建立 GitHub repo、改 visibility、sync refs、force push 或 trigger workflow。",
"data": "不直接做資料庫、backup、restore 或 migration 寫操作。",
"security": "不啟動 Wazuh / Kali active response、active scan 或 host containment。"
},
"errors": {
"title": "部分資料沒有回讀"
}
},
"observabilityCommand": {
"eyebrow": "AWOOOI 可觀測性指揮面板",
"title": "主機、專案、網站、服務與工具全域監控",

View File

@@ -63,6 +63,7 @@
"drift": "漂移偵測",
"neuralCommand": "神經指揮中心",
"commandCenter": "指令中心",
"delivery": "交付閉環",
"observability": "可觀測性",
"automation": "自動化",
"operations": "營運",
@@ -80,6 +81,82 @@
"iwooos": "IwoooS",
"iwooosSecurityCompliance": "IwoooS 安全合規"
},
"delivery": {
"eyebrow": "AWOOOI Delivery",
"title": "交付閉環工作台",
"subtitle": "把目前真正會推進交付的主線集中在同一頁:乾淨 release、GitHub 私有備援、Gitea / CI/CD、runtime surface、資料與備份。這裡只顯示能推動下一步的狀態不再把文件數量當成完成度。",
"states": {
"loading": "讀取中",
"ready": "資料鏈正常",
"partial": "部分資料待補",
"noData": "尚未回讀"
},
"actions": {
"refresh": "重新整理"
},
"sources": {
"0": { "error": "狀態清理資料未回讀" },
"1": { "error": "GitHub 備援資料未回讀" },
"2": { "error": "Gitea / runner 資料未回讀" },
"3": { "error": "Runtime surface 資料未回讀" },
"4": { "error": "Backup readiness 資料未回讀" }
},
"metrics": {
"loaded": "資料來源",
"loadedDetail": "只計入正式 API 成功回讀的來源。",
"completion": "平均進度",
"completionDetail": "以本頁五條交付主線計算。",
"blockers": "高風險阻擋",
"blockersDetail": "只統計會影響交付決策的阻擋。",
"execution": "執行授權",
"executionDetail": "本頁只讀,不執行 runtime 或 remote write。"
},
"sections": {
"lanes": "交付主線",
"lanesDetail": "每張卡只回答完成度、阻擋數、下一步入口。",
"next": "下一步焦點",
"nextDetail": "只列需要處理的主線,不列文件清單。",
"boundary": "保留硬邊界",
"boundaryDetail": "這些仍需明確授權,但不得阻擋低風險 coding / UI / test。"
},
"lanes": {
"release": {
"title": "乾淨 release 工作流",
"description": "把可交付的變更切出乾淨分支與可驗證提交,避免髒分支整包推送。",
"metric": "阻擋 gate {blocked}"
},
"github": {
"title": "GitHub 私有備援",
"description": "所有備援 repo 必須私有,公開可讀或未驗證 private 都不能標綠。",
"metric": "已驗證 {verified}/{total}"
},
"gitea": {
"title": "Gitea / CI-CD",
"description": "確認 workflow、runner label、通知與 dev / prod 發版線是真實可跑。",
"metric": "workflow {count}"
},
"runtime": {
"title": "Runtime surface",
"description": "把產品、網站、服務與部署面映射到實際 runtime不再停在文件描述。",
"metric": "surface {total}"
},
"backup": {
"title": "資料與備份",
"description": "資料庫、類資料庫、備份與還原演練必須能支撐一鍵上雲與獨立部署。",
"metric": "readiness row {rows}"
}
},
"boundaries": {
"secret": "不收 secret value、token、private key、cookie 或 private clone credential。",
"production": "不直接改 production runtime、public gateway、Nginx、Docker、K8s 或 firewall。",
"repo": "不直接建立 GitHub repo、改 visibility、sync refs、force push 或 trigger workflow。",
"data": "不直接做資料庫、backup、restore 或 migration 寫操作。",
"security": "不啟動 Wazuh / Kali active response、active scan 或 host containment。"
},
"errors": {
"title": "部分資料沒有回讀"
}
},
"observabilityCommand": {
"eyebrow": "AWOOOI 可觀測性指揮面板",
"title": "主機、專案、網站、服務與工具全域監控",

View File

@@ -0,0 +1,628 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
import { useTranslations } from 'next-intl'
import {
AlertTriangle,
ArrowRight,
CheckCircle2,
GitBranch,
HardDrive,
Lock,
PackageCheck,
RefreshCw,
Rocket,
Server,
} from 'lucide-react'
import { AppLayout } from '@/components/layout'
import { GlassCard } from '@/components/ui/glass-card'
import {
apiClient,
type AwoooIStatusCleanupDashboardSnapshot,
type BackupDrReadinessMatrixSnapshot,
type DeliveryClosureWorkbenchSnapshot,
type GiteaWorkflowRunnerHealthSnapshot,
type GithubTargetPrivateBackupEvidenceGateSnapshot,
type RuntimeSurfaceInventorySnapshot,
} from '@/lib/api-client'
type DeliveryTone = 'ok' | 'warn' | 'danger' | 'neutral'
interface DeliveryData {
statusCleanup: AwoooIStatusCleanupDashboardSnapshot | null
github: GithubTargetPrivateBackupEvidenceGateSnapshot | null
gitea: GiteaWorkflowRunnerHealthSnapshot | null
runtime: RuntimeSurfaceInventorySnapshot | null
backup: BackupDrReadinessMatrixSnapshot | null
}
interface DeliveryLane {
id: string
title: string
description: string
percent: number
status: string
metric: string
blockerCount: number
nextAction: string
href: string
tone: DeliveryTone
Icon: typeof Rocket
}
const SOURCE_COUNT = 5
const EMPTY_DATA: DeliveryData = {
statusCleanup: null,
github: null,
gitea: null,
runtime: null,
backup: null,
}
const clampPercent = (value: number | null | undefined) => Math.max(0, Math.min(100, Math.round(value ?? 0)))
const toneColor = (tone: DeliveryTone) => {
if (tone === 'ok') return '#2f7d54'
if (tone === 'warn') return '#9a6a22'
if (tone === 'danger') return '#b2432d'
return '#706f68'
}
const toneBackground = (tone: DeliveryTone) => {
if (tone === 'ok') return 'rgba(47, 125, 84, 0.10)'
if (tone === 'warn') return 'rgba(154, 106, 34, 0.10)'
if (tone === 'danger') return 'rgba(178, 67, 45, 0.10)'
return 'rgba(112, 111, 104, 0.10)'
}
const resolveTone = (blockerCount: number, percent: number): DeliveryTone => {
if (blockerCount > 0) return 'danger'
if (percent < 80) return 'warn'
return 'ok'
}
const firstActionRequiredTarget = (data: GithubTargetPrivateBackupEvidenceGateSnapshot | null) =>
data?.targets.find(target => target.approval_required)?.next_action ?? ''
const firstActionRequiredRunner = (data: GiteaWorkflowRunnerHealthSnapshot | null) =>
data?.runner_contracts.find(contract => contract.status === 'action_required')?.next_action ?? ''
const firstRuntimeAction = (data: RuntimeSurfaceInventorySnapshot | null) =>
data?.runtime_surfaces.find(surface => surface.status !== 'manifest_mapped')?.next_action ?? ''
const firstBackupAction = (data: BackupDrReadinessMatrixSnapshot | null) =>
data?.readiness_rows.find(row => row.overall_readiness === 'blocked' || row.overall_readiness === 'action_required')?.next_action ?? ''
function StatusPill({ tone, label }: { tone: DeliveryTone; label: string }) {
return (
<span
style={{
display: 'inline-flex',
alignItems: 'center',
minHeight: 26,
borderRadius: 6,
padding: '3px 8px',
background: toneBackground(tone),
color: toneColor(tone),
fontSize: 12,
fontWeight: 700,
overflowWrap: 'anywhere',
}}
>
{label}
</span>
)
}
function ProgressBar({ percent, tone }: { percent: number; tone: DeliveryTone }) {
return (
<div
aria-hidden="true"
style={{
height: 8,
borderRadius: 4,
background: '#ebe8dd',
overflow: 'hidden',
}}
>
<div
style={{
width: `${percent}%`,
height: '100%',
background: toneColor(tone),
transition: 'width 240ms ease',
}}
/>
</div>
)
}
function MetricTile({
label,
value,
detail,
tone,
}: {
label: string
value: string | number
detail: string
tone: DeliveryTone
}) {
return (
<GlassCard variant="subtle" padding="sm" data-testid={`delivery-metric-${label}`}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, minHeight: 112 }}>
<span style={{ fontSize: 12, color: '#706f68', fontWeight: 700 }}>{label}</span>
<strong style={{ fontFamily: 'var(--font-heading), sans-serif', fontSize: 30, color: '#141413', lineHeight: 1 }}>
{value}
</strong>
<span style={{ fontSize: 12, lineHeight: 1.45, color: toneColor(tone), overflowWrap: 'anywhere' }}>
{detail}
</span>
</div>
</GlassCard>
)
}
function LaneCard({ lane, locale }: { lane: DeliveryLane; locale: string }) {
return (
<GlassCard variant="default" padding="md" data-testid={`delivery-lane-${lane.id}`}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, minHeight: 230 }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
<div
style={{
width: 38,
height: 38,
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: toneBackground(lane.tone),
color: toneColor(lane.tone),
flex: '0 0 auto',
}}
>
<lane.Icon size={19} aria-hidden="true" />
</div>
<div style={{ minWidth: 0, flex: 1 }}>
<h2 style={{ fontSize: 17, fontWeight: 800, color: '#141413', margin: 0, overflowWrap: 'anywhere' }}>
{lane.title}
</h2>
<p style={{ fontSize: 13, lineHeight: 1.5, color: '#706f68', margin: '5px 0 0', overflowWrap: 'anywhere' }}>
{lane.description}
</p>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
<StatusPill tone={lane.tone} label={lane.status} />
<span style={{ fontSize: 13, fontWeight: 800, color: '#141413' }}>{lane.percent}%</span>
</div>
<ProgressBar percent={lane.percent} tone={lane.tone} />
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 10, alignItems: 'center', marginTop: 'auto' }}>
<span style={{ fontSize: 13, color: '#45443f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>{lane.metric}</span>
<StatusPill tone={lane.blockerCount > 0 ? 'danger' : 'ok'} label={String(lane.blockerCount)} />
</div>
<Link
href={`/${locale}${lane.href}`}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 6,
color: '#141413',
fontWeight: 800,
fontSize: 13,
textDecoration: 'none',
}}
>
<ArrowRight size={15} aria-hidden="true" />
<span>{lane.metric}</span>
</Link>
</div>
</GlassCard>
)
}
export default function DeliveryPage({ params }: { params: { locale: string } }) {
const t = useTranslations('delivery')
const [workbench, setWorkbench] = useState<DeliveryClosureWorkbenchSnapshot | null>(null)
const [data, setData] = useState<DeliveryData>(EMPTY_DATA)
const [errors, setErrors] = useState<string[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
let cancelled = false
const load = async () => {
setLoading(true)
setErrors([])
try {
const summary = await apiClient.getDeliveryClosureWorkbench()
if (cancelled) return
setWorkbench(summary)
setData(EMPTY_DATA)
setErrors([])
setLoading(false)
return
} catch {
// Summary endpoint may not exist until the API release lands; keep the page useful with legacy reads.
}
const results = await Promise.allSettled([
apiClient.getAwoooIStatusCleanupDashboard(),
apiClient.getGithubTargetPrivateBackupEvidenceGate(),
apiClient.getGiteaWorkflowRunnerHealth(),
apiClient.getRuntimeSurfaceInventory(),
apiClient.getBackupDrReadinessMatrix(),
])
if (cancelled) return
const nextData: DeliveryData = {
statusCleanup: results[0].status === 'fulfilled' ? results[0].value : null,
github: results[1].status === 'fulfilled' ? results[1].value : null,
gitea: results[2].status === 'fulfilled' ? results[2].value : null,
runtime: results[3].status === 'fulfilled' ? results[3].value : null,
backup: results[4].status === 'fulfilled' ? results[4].value : null,
}
const nextErrors = results
.map((result, index) => ({ result, index }))
.filter(({ result }) => result.status === 'rejected')
.map(({ index }) => t(`sources.${index}.error`))
setWorkbench(null)
setData(nextData)
setErrors(nextErrors)
setLoading(false)
}
load()
return () => {
cancelled = true
}
}, [t])
const lanes = useMemo<DeliveryLane[]>(() => {
if (workbench) {
return workbench.lanes.map(lane => {
const iconMap = {
release: Rocket,
github: GitBranch,
gitea: PackageCheck,
runtime: Server,
backup: HardDrive,
}
const metric =
lane.metric.kind === 'blocked_gate'
? t('lanes.release.metric', { blocked: lane.metric.blocked })
: lane.metric.kind === 'private_backup_verified'
? t('lanes.github.metric', { verified: lane.metric.verified, total: lane.metric.total })
: lane.metric.kind === 'workflow_count'
? t('lanes.gitea.metric', { count: lane.metric.count })
: lane.metric.kind === 'surface_count'
? t('lanes.runtime.metric', { total: lane.metric.total })
: t('lanes.backup.metric', { rows: lane.metric.rows })
return {
id: lane.id,
title: t(`lanes.${lane.id}.title`),
description: t(`lanes.${lane.id}.description`),
percent: clampPercent(lane.completion_percent),
status: lane.status || t('states.noData'),
metric,
blockerCount: lane.blocker_count,
nextAction: lane.next_action,
href: lane.href,
tone: lane.tone,
Icon: iconMap[lane.id],
}
})
}
const statusBlocked = Number(data.statusCleanup?.summary.blocked_gate_count ?? 0)
const statusPercent = clampPercent(data.statusCleanup?.summary.overall_completion_percent)
const githubRequired = data.github?.summary.approval_required_target_count ?? 0
const githubVerified = data.github?.summary.private_backup_verified_count ?? 0
const githubBlocked = data.github?.summary.blocked_target_count ?? 0
const githubPercent = githubRequired > 0 ? clampPercent((githubVerified / githubRequired) * 100) : 0
const giteaPercent = clampPercent(data.gitea?.program_status.overall_completion_percent)
const giteaBlocked = data.gitea?.rollups.runner_contracts_requiring_action.length ?? 0
const runtimePercent = clampPercent(data.runtime?.program_status.overall_completion_percent)
const runtimeBlocked = (data.runtime?.rollups.action_required_surface_ids.length ?? 0) + (data.runtime?.rollups.secret_surface_ids.length ?? 0)
const backupPercent = clampPercent(data.backup?.program_status.overall_completion_percent)
const backupBlocked = data.backup?.rollups.blocked_row_ids.length ?? 0
return [
{
id: 'release',
title: t('lanes.release.title'),
description: t('lanes.release.description'),
percent: statusPercent,
status: data.statusCleanup?.summary.dashboard_status ?? t('states.noData'),
metric: t('lanes.release.metric', { blocked: statusBlocked }),
blockerCount: statusBlocked,
nextAction: data.statusCleanup?.next_actions[0] ?? '',
href: '/governance?tab=automation-inventory',
tone: resolveTone(statusBlocked, statusPercent),
Icon: Rocket,
},
{
id: 'github',
title: t('lanes.github.title'),
description: t('lanes.github.description'),
percent: githubPercent,
status: data.github?.status ?? t('states.noData'),
metric: t('lanes.github.metric', { verified: githubVerified, total: githubRequired }),
blockerCount: githubBlocked,
nextAction: firstActionRequiredTarget(data.github),
href: '/governance?tab=automation-inventory',
tone: resolveTone(githubBlocked, githubPercent),
Icon: GitBranch,
},
{
id: 'gitea',
title: t('lanes.gitea.title'),
description: t('lanes.gitea.description'),
percent: giteaPercent,
status: data.gitea?.program_status.current_task_id ?? t('states.noData'),
metric: t('lanes.gitea.metric', { count: data.gitea?.rollups.total_workflows ?? 0 }),
blockerCount: giteaBlocked,
nextAction: firstActionRequiredRunner(data.gitea),
href: '/deployments',
tone: resolveTone(giteaBlocked, giteaPercent),
Icon: PackageCheck,
},
{
id: 'runtime',
title: t('lanes.runtime.title'),
description: t('lanes.runtime.description'),
percent: runtimePercent,
status: data.runtime?.program_status.current_task_id ?? t('states.noData'),
metric: t('lanes.runtime.metric', { total: data.runtime?.rollups.total_surfaces ?? 0 }),
blockerCount: runtimeBlocked,
nextAction: firstRuntimeAction(data.runtime),
href: '/governance?tab=automation-inventory',
tone: resolveTone(runtimeBlocked, runtimePercent),
Icon: Server,
},
{
id: 'backup',
title: t('lanes.backup.title'),
description: t('lanes.backup.description'),
percent: backupPercent,
status: data.backup?.program_status.current_task_id ?? t('states.noData'),
metric: t('lanes.backup.metric', { rows: data.backup?.rollups.total_rows ?? 0 }),
blockerCount: backupBlocked,
nextAction: firstBackupAction(data.backup),
href: '/operations',
tone: resolveTone(backupBlocked, backupPercent),
Icon: HardDrive,
},
]
}, [data, t, workbench])
const sourceTotal = workbench?.summary.source_count ?? SOURCE_COUNT
const loadedCount = workbench?.summary.loaded_source_count ?? Object.values(data).filter(Boolean).length
const highRiskBlockers = workbench?.summary.high_risk_blocker_count ?? lanes.reduce((sum, lane) => sum + lane.blockerCount, 0)
const averageCompletion = workbench?.summary.average_completion_percent ?? clampPercent(lanes.reduce((sum, lane) => sum + lane.percent, 0) / Math.max(lanes.length, 1))
const pageTone: DeliveryTone = highRiskBlockers > 0 ? 'danger' : loadedCount === sourceTotal ? 'ok' : 'warn'
return (
<AppLayout locale={params.locale}>
<div data-testid="delivery-page" style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
<section className="delivery-hero">
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#d97757', fontSize: 12, fontWeight: 800 }}>
<Rocket size={16} aria-hidden="true" />
<span>{t('eyebrow')}</span>
</div>
<h1 style={{ fontSize: 34, lineHeight: 1.1, fontWeight: 900, color: '#141413', margin: '8px 0 8px' }}>
{t('title')}
</h1>
<p style={{ maxWidth: 780, fontSize: 15, lineHeight: 1.65, color: '#595852', margin: 0 }}>
{t('subtitle')}
</p>
</div>
<div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
<StatusPill tone={pageTone} label={loading ? t('states.loading') : errors.length > 0 ? t('states.partial') : t('states.ready')} />
<button
type="button"
onClick={() => window.location.reload()}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 6,
minHeight: 34,
borderRadius: 7,
border: '0.5px solid #d8d3c5',
background: '#fffaf0',
color: '#141413',
padding: '0 10px',
fontSize: 13,
fontWeight: 800,
cursor: 'pointer',
}}
>
<RefreshCw size={15} aria-hidden="true" />
{t('actions.refresh')}
</button>
</div>
</section>
<section className="delivery-metrics">
<MetricTile label={t('metrics.loaded')} value={`${loadedCount}/${sourceTotal}`} detail={t('metrics.loadedDetail')} tone={loadedCount === sourceTotal ? 'ok' : 'warn'} />
<MetricTile label={t('metrics.completion')} value={`${averageCompletion}%`} detail={t('metrics.completionDetail')} tone={averageCompletion >= 80 ? 'ok' : 'warn'} />
<MetricTile label={t('metrics.blockers')} value={highRiskBlockers} detail={t('metrics.blockersDetail')} tone={highRiskBlockers > 0 ? 'danger' : 'ok'} />
<MetricTile label={t('metrics.execution')} value="0" detail={t('metrics.executionDetail')} tone="ok" />
</section>
{errors.length > 0 && (
<section className="delivery-alert" data-testid="delivery-partial-errors">
<AlertTriangle size={18} aria-hidden="true" />
<div>
<strong>{t('errors.title')}</strong>
<p>{errors.join(' · ')}</p>
</div>
</section>
)}
<section>
<div className="delivery-section-heading">
<div>
<h2>{t('sections.lanes')}</h2>
<p>{t('sections.lanesDetail')}</p>
</div>
</div>
<div className="delivery-lanes">
{lanes.map(lane => <LaneCard key={lane.id} lane={lane} locale={params.locale} />)}
</div>
</section>
<section className="delivery-grid-two">
<div>
<div className="delivery-section-heading">
<div>
<h2>{t('sections.next')}</h2>
<p>{t('sections.nextDetail')}</p>
</div>
</div>
<div className="delivery-next-list">
{lanes
.filter(lane => lane.blockerCount > 0 || lane.percent < 80)
.slice(0, 5)
.map(lane => (
<div key={lane.id} className="delivery-next-row">
<lane.Icon size={17} aria-hidden="true" />
<div>
<strong>{lane.title}</strong>
<span>{lane.nextAction || lane.metric}</span>
</div>
<StatusPill tone={lane.tone} label={`${lane.percent}%`} />
</div>
))}
</div>
</div>
<div>
<div className="delivery-section-heading">
<div>
<h2>{t('sections.boundary')}</h2>
<p>{t('sections.boundaryDetail')}</p>
</div>
</div>
<div className="delivery-boundary-list">
{['secret', 'production', 'repo', 'data', 'security'].map(key => (
<div key={key} className="delivery-boundary-row">
<Lock size={16} aria-hidden="true" />
<span>{t(`boundaries.${key}`)}</span>
<CheckCircle2 size={16} aria-hidden="true" />
</div>
))}
</div>
</div>
</section>
</div>
<style jsx>{`
.delivery-hero {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 18px;
align-items: start;
padding-bottom: 4px;
}
.delivery-metrics,
.delivery-lanes {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
gap: 12px;
}
.delivery-section-heading {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
margin: 4px 0 10px;
}
.delivery-section-heading h2 {
margin: 0;
font-size: 19px;
font-weight: 900;
color: #141413;
}
.delivery-section-heading p {
margin: 4px 0 0;
color: #706f68;
font-size: 13px;
line-height: 1.5;
}
.delivery-alert {
display: flex;
align-items: flex-start;
gap: 10px;
border: 0.5px solid rgba(178, 67, 45, 0.22);
background: rgba(178, 67, 45, 0.08);
color: #7f2f21;
border-radius: 8px;
padding: 12px;
}
.delivery-alert p {
margin: 3px 0 0;
font-size: 13px;
line-height: 1.5;
}
.delivery-grid-two {
display: grid;
grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.75fr);
gap: 14px;
}
.delivery-next-list,
.delivery-boundary-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.delivery-next-row,
.delivery-boundary-row {
display: grid;
grid-template-columns: 20px minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
min-height: 54px;
border: 0.5px solid #e0ddd4;
background: rgba(255, 255, 255, 0.62);
border-radius: 8px;
padding: 10px;
color: #45443f;
}
.delivery-next-row div {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.delivery-next-row strong {
color: #141413;
font-size: 13px;
overflow-wrap: anywhere;
}
.delivery-next-row span,
.delivery-boundary-row span {
font-size: 12px;
line-height: 1.45;
overflow-wrap: anywhere;
}
.delivery-boundary-row {
grid-template-columns: 20px minmax(0, 1fr) 20px;
}
@media (max-width: 900px) {
.delivery-hero,
.delivery-grid-two {
grid-template-columns: 1fr;
}
}
`}</style>
</AppLayout>
)
}

View File

@@ -42,6 +42,7 @@ import {
Monitor,
Package,
Radar,
Rocket,
Route,
Settings,
ShieldCheck,
@@ -91,6 +92,7 @@ const NAV_SECTIONS: NavSection[] = [
sectionKey: 'queues',
items: [
{ id: 'command-center', href: '/', labelKey: 'commandCenter', Icon: LayoutDashboard },
{ id: 'delivery', href: '/delivery', labelKey: 'delivery', Icon: Rocket },
{ id: 'awooop-home', href: '/awooop', labelKey: 'awooopHome', Icon: BrainCircuit, exact: true },
{ id: 'awooop-work-items', href: '/awooop/work-items', labelKey: 'workItems', Icon: ClipboardList },
{ id: 'awooop-runs', href: '/awooop/runs', labelKey: 'runMonitor', Icon: Activity },

View File

@@ -334,6 +334,11 @@ export const apiClient = {
return handleResponse<AwoooIStatusCleanupDashboardSnapshot>(res)
},
async getDeliveryClosureWorkbench() {
const res = await fetch(`${API_BASE_URL}/agents/delivery-closure-workbench`)
return handleResponse<DeliveryClosureWorkbenchSnapshot>(res)
},
async getAiAgent12AgentWarRoom() {
const res = await fetch(`${API_BASE_URL}/agents/agent-12-agent-war-room`)
return handleResponse<AiAgent12AgentWarRoomSnapshot>(res)
@@ -704,6 +709,11 @@ export const apiClient = {
return handleResponse<GiteaWorkflowRunnerHealthSnapshot>(res)
},
async getGithubTargetPrivateBackupEvidenceGate() {
const res = await fetch(`${API_BASE_URL}/agents/github-target-private-backup-evidence-gate`)
return handleResponse<GithubTargetPrivateBackupEvidenceGateSnapshot>(res)
},
async getObservabilityContractMatrix() {
const res = await fetch(`${API_BASE_URL}/agents/observability-contract-matrix`)
return handleResponse<ObservabilityContractMatrixSnapshot>(res)
@@ -1391,6 +1401,64 @@ export interface AwoooIStatusCleanupDashboardSnapshot {
ui_implementation_authorized: false
}
export interface DeliveryClosureWorkbenchSnapshot {
schema_version: 'delivery_closure_workbench_v1'
generated_at: string
status: 'blocked_delivery_actions_required' | 'ready' | string
summary: {
source_count: number
loaded_source_count: number
average_completion_percent: number
high_risk_blocker_count: number
runtime_execution_authorized: false
remote_write_authorized: false
repo_creation_authorized: false
refs_sync_authorized: false
workflow_trigger_authorized: false
secret_values_collected: false
}
source_statuses: Array<{
id: string
loaded: boolean
schema_version: string
generated_at: string
}>
lanes: Array<{
id: 'release' | 'github' | 'gitea' | 'runtime' | 'backup'
source_id: string
completion_percent: number
status: string
blocker_count: number
metric:
| { kind: 'blocked_gate'; blocked: number; total: number }
| { kind: 'private_backup_verified'; verified: number; total: number }
| { kind: 'workflow_count'; count: number }
| { kind: 'surface_count'; total: number }
| { kind: 'readiness_row_count'; rows: number }
href: string
next_action: string
tone: 'ok' | 'warn' | 'danger'
}>
next_focus: Array<{
lane_id: string
blocker_count: number
completion_percent: number
next_action: string
}>
operation_boundaries: {
read_only_api_allowed: true
runtime_write_allowed: false
remote_write_allowed: false
repo_creation_allowed: false
visibility_change_allowed: false
refs_sync_allowed: false
workflow_trigger_allowed: false
secret_value_collection_allowed: false
backup_restore_execution_allowed: false
active_scan_allowed: false
}
}
export interface AiAgent12AgentWarRoomSnapshot {
schema_version: 'ai_agent_12_agent_war_room_v1'
generated_at: string
@@ -12587,6 +12655,88 @@ export interface GiteaWorkflowRunnerHealthSnapshot {
approval_boundaries: Record<string, false>
}
export interface GithubTargetPrivateBackupEvidenceGateSnapshot {
schema_version: 'github_target_private_backup_evidence_gate_v1'
generated_at: string
status:
| 'blocked_public_visibility_and_safe_credential_evidence_required'
| 'blocked_private_visibility_and_safe_credential_evidence_required'
mode: 'read_only_private_backup_evidence_gate'
source_reviews: Record<string, string>
summary: {
target_decision_count: number
approval_required_target_count: number
approval_package_item_count: number
public_probe_visible_target_count: number
not_found_or_private_target_count: number
private_backup_verified_count: number
private_visibility_evidence_missing_count: number
safe_credential_required_count: number
safe_credential_accepted_evidence_count: number
owner_response_received_count: number
owner_response_accepted_count: number
execution_ready_count: number
blocked_target_count: number
external_scope_target_count: number
forbidden_action_count: number
repo_creation_authorized: false
visibility_change_authorized: false
refs_sync_authorized: false
github_primary_switch_authorized: false
workflow_modification_authorized: false
workflow_trigger_authorized: false
secret_value_collection_allowed: false
private_clone_url_collection_allowed: false
not_found_or_private_as_absent_allowed: false
public_repo_allowed: false
}
targets: Array<{
github_repo: string
source_key: string
approval_required: boolean
probe_status: string
target_state: string
risk: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' | string
visibility_evidence_status:
| 'external_scope_not_backup_target'
| 'blocked_public_probe_visible_private_evidence_required'
| 'blocked_private_or_absent_not_verified'
| 'blocked_probe_status_unknown'
private_backup_verified: false
private_visibility_owner_evidence_ref: string | null
safe_credential_evidence_status: string
safe_credential_evidence_ref: string | null
owner_response_accepted: false
refs_sync_ready: false
execution_ready: false
blockers: string[]
evidence_refs: string[]
next_action: string
forbidden_actions: string[]
repo_creation_authorized: false
visibility_change_authorized: false
refs_sync_authorized: false
github_primary_switch_authorized: false
secret_values_collected: false
}>
acceptance_requirements: string[]
rejection_rules: string[]
operation_boundaries: {
read_only_api_allowed: true
github_api_write_allowed: false
gitea_api_write_allowed: false
repo_creation_allowed: false
visibility_change_allowed: false
refs_sync_allowed: false
workflow_modification_allowed: false
workflow_trigger_allowed: false
github_primary_switch_allowed: false
secret_value_collection_allowed: false
private_clone_url_collection_allowed: false
}
authorization_flags: Record<string, false>
}
export interface ObservabilityContractMatrixSnapshot {
schema_version: 'observability_contract_matrix_v1'
generated_at: string

View File

@@ -8,11 +8,11 @@
| 欄位 | 值 |
|------|-----|
| **版本** | v2.3 |
| **版本** | v2.4 |
| **建立日期** | 2026-03-20 (台北) |
| **建立者** | Claude Code |
| **最後修改** | 2026-06-11 (台北) |
| **修改者** | Codex + ogt (新增高價值配置資安控管) |
| **最後修改** | 2026-06-26 (台北) |
| **修改者** | Codex + ogt (新增交付閉環優先規範) |
### 變更紀錄
@@ -32,6 +32,7 @@
| v2.1 | 2026-05-06 | Codex + ogt | 🔴 文件語言鐵律Markdown/ADR/LOGBOOK/Runbook/交接文件一律繁體中文 |
| v2.2 | 2026-06-04 | Codex + ogt | 🔴🔴🔴 IwoooS 資安治理禁令:只讀證據、低摩擦、不可誤讀 UI / AwoooP approval / runtime gate |
| v2.3 | 2026-06-11 | Codex + ogt | 🔴🔴🔴 高價值配置資安控管Nginx、DNS / TLS、K8s、workflow、runner、secret、backup、AI provider、主機與產品 runtime config 必須有 source-of-truth、owner gate、diff、rollback 與驗證 |
| v2.4 | 2026-06-26 | Codex + ogt | 🔴🔴🔴 交付閉環優先:治理與 gate 只能服務於完成交付,不得取代問題解決、新需求實作或 release closure |
---
@@ -63,11 +64,51 @@
| **🔴🔴 熔斷機制** | **不確定就停下來問** | **主動做完,爆炸半徑 >3 模組才熔斷** | [→ 主動執行與熔斷](#proactive-execution--circuit-breaker) |
| **🔴🔴 工作流節奏** | **每步都來回報** | **內部自循環,全局單次回報** | [→ 自循環工作流](#self-loop-workflow) |
| **🟡 狀態機驗證** | **不查中間狀態卡死** | **必驗 TTL + Cleanup + Fallback** | [→ State & Flow Validation](#state--flow-validation) |
| **🔴🔴🔴 交付閉環** | **一直新增 gate / snapshot / owner package 取代交付** | **先完成可安全交付的 code / test / UI / release worktree再保留必要安全 gate** | [→ Delivery Closure First](#delivery-closure-first) |
| **🔴🔴🔴 IwoooS 資安治理** | **UI 可見 / AwoooP approval 當 runtime 授權** | **只讀證據 + owner response gate + 獨立人工批准** | [→ IwoooS Security Governance](#iwooos-security-governance) |
| **🔴🔴🔴 高價值配置** | **手改 Nginx / workflow / secret / runtime config 後直接 reload 或部署** | **source-of-truth + owner gate + diff + rollback + 驗證** | [→ High Value Config Control](#high-value-config-control) |
---
## 🔴🔴🔴 Delivery Closure First
> 2026-06-26 統帥修正AWOOOI / 全產品治理的目標是「問題持續被解決、工作和新需求持續完成」。治理防呆不是交付本身,任何 gate、snapshot、owner package 或 read-only dashboard 都必須服務於交付閉環,不得成為新的工作泥沼。
### 絕對禁止
```text
❌ 為了看起來更安全而新增低價值 gate / snapshot / owner response 文件,卻沒有關閉任何實際問題
❌ 把一般 coding、UI、測試、API client、文件修正、乾淨 release worktree 準備,誤擋成需要 owner response
❌ 將 blocked count、0/false 清單、read-only dashboard 當成主要產出,卻沒有下一個可執行交付動作
❌ 在同一個 blocker 連續三輪無法推進時,仍繼續包裝同一個 blocker而不切換到可完成的 P0 交付
❌ 因為缺少最終 production / runtime 授權,就停止完成 repo-side 可驗證工作
❌ 把「治理覆蓋率」上升誤報成產品、網站、CI/CD、GitHub mirror、備份或資安 runtime 已完成
```
### 正確流程
1. 每個工作項目先定義可交付結果code、test、UI、API、release branch、production readback、repo sync、備份驗收或 owner evidence。
2. 只有下列情境可以硬擋secret value、不可逆操作、production / public gateway runtime、repo creation、visibility change、refs sync、workflow trigger、資料庫 / backup / restore / migration、費用、主機維護、資安 active scan。
3. 不屬於上述高風險範圍時,收到「批准 / 繼續」後應直接完成可安全執行的 repo-side 工作,不得要求額外 owner response。
4. 需要 owner response 時,必須同時提供最小可回答欄位與下一個可執行動作;不得要求 10 個以上欄位才允許低風險工作前進。
5. 同一個 gate 連續阻擋三輪時,必須壓縮成一個 blocker並立即切到下一個可完成的 P0 交付項。
6. 每次回報以交付狀態為主:已解決問題、已交付功能、已驗證結果、仍阻擋的高風險項、下一個 P0。`0 / false` 只列真正會影響決策的項目。
7. 完成度只能因真實交付上升程式碼合併、測試通過、UI 可用、API 可讀、release branch 乾淨、production readback、GitHub private evidence verified、Gitea dev/prod refs 實際存在、backup / restore drill 通過。
### Gate 保留條件
任何新 gate / snapshot / owner package 必須同時滿足:
1. 對應一個實際高風險動作或合規要求。
2. 有明確 owner 或可自動驗證來源。
3. 有退出條件,不得永久停在 `waiting_owner_response`
4. 會解鎖下一個交付動作,而不是只新增另一個 gate。
5. 可以被 API / UI / test / release lane 驗證。
不滿足以上條件時,禁止新增;改以 existing issue / workplan row / LOGBOOK 一行追蹤即可。
---
## 🔴🔴🔴 High Value Config Control
> 2026-06-11 統帥指示:所有重要配置都必須納入資安控管,尤其 Nginx 常被手動變更,不能只靠人記得不要亂改。

View File

@@ -1,3 +1,338 @@
## 2026-06-26交付閉環優先規範修正
**背景**:統帥指出目前規範已偏向「治理防呆」而不是「問題一直被解決、工作和新需求一直被完成」。本輪直接修正上位規範,不再讓低風險 repo-side 工作被 owner response / snapshot / gate 無限阻擋。
**修正**
- `docs/HARD_RULES.md` 升級為 `v2.4`,新增 `Delivery Closure First`
- `~/.codex/CODEX-START-HERE.md` 新增 `delivery_closure_override=enabled_2026-06-26`,並同步兩台 Codex 開工入口的執行語意。
**新規則**
- 交付閉環優先於治理包裝gate / snapshot / owner package 只能服務於完成交付,不得取代交付。
- 一般 coding、UI、API client、測試、文件修正、乾淨 release worktree、read-only evidence 整理,收到「批准 / 繼續」後直接完成,不再要求額外 owner response。
- 只有 secret value、不可逆操作、production / public gateway runtime、repo creation、visibility change、refs sync、workflow trigger、資料庫 / backup / restore / migration、費用、主機維護、資安 active scan 這類高風險動作可以硬擋。
- 同一 blocker 連續阻擋三輪時,必須壓縮成一個 blocker切換到下一個可完成的 P0 交付項。
- 新 gate / snapshot 必須能解鎖下一個交付動作,且有退出條件;否則只用 LOGBOOK 一行追蹤。
**不變的安全邊界**
- 本規範修正不授權 secret 收集、GitHub repo creation、visibility change、refs sync、workflow trigger、production deploy、Nginx / Docker / K8s / firewall / host mutation、Wazuh / Kali runtime 或資料庫 / backup / restore / migration。
- 但上述邊界不得再阻擋可安全完成的程式碼、測試、UI、API、release worktree 與 read-only 驗證。
## 2026-06-26GitHub 私有備援 evidence gate / API client 收斂
**整體完成度**:依 `~/.codex/CODEX-START-HERE.md``product-runtime-governance-completion-scorecard`,全產品治理總工程仍為 `42.2%``not_complete`。本段只把 GitHub 備援鏡像的「私有性證據與安全 credential 前置 gate」補成 source-side snapshot、API service、API route、前端 client 與測試;不調高 GitHub mirror 實際完成度,也不宣稱任何專案已推上 GitHub。
**受控工作區**
- path`/Users/ogt/codex-workspaces/awoooi-dev`
- branch`codex/awoooi-current-main-dev-base-20260624`
- base HEAD`59485d51`
- commit / push本段尚未 commit、尚未 push。
**本段完成度**
- GitHub private backup evidence gate source / snapshot / markdown`100%`
- FastAPI loader / read-only endpoint / 前端 API client`100%`
- focused tests / JSON parse / doc secret / diff check`已通過`
- 實際 GitHub private repo verification`0/9`
- GitHub refs sync / repo creation / visibility change`0 / false`
- 全產品 GitHub mirror ready仍為 `0/11`
**新增與修改範圍**
- `scripts/security/github-target-private-backup-evidence-gate.py`
- `docs/security/github-target-private-backup-evidence-gate.snapshot.json`
- `docs/security/GITHUB-TARGET-PRIVATE-BACKUP-EVIDENCE-GATE.md`
- `docs/schemas/github_target_private_backup_evidence_gate_v1.schema.json`
- `apps/api/src/services/github_target_private_backup_evidence_gate.py`
- `apps/api/tests/test_github_target_private_backup_evidence_gate.py`
- `apps/api/tests/test_github_target_private_backup_evidence_gate_api.py`
- `apps/api/src/api/v1/agents.py`
- `apps/web/src/lib/api-client.ts`
**目前事實**
- approval-required GitHub targets`9`
- unauthenticated probe publicly visible`4`,分別是 `owenhytsai/awoooi``owenhytsai/clawbot-v5``owenhytsai/wooo-aiops``owenhytsai/wooo-infra-config`;全部維持 `blocked_public_probe_visible_private_evidence_required`,不得標綠。
- `not_found_or_private``5`,分別是 `owenhytsai/ewoooc``owenhytsai/bitan-pharmacy``owenhytsai/tsenyang-website``owenhytsai/VibeWork``owenhytsai/agent-bounty-protocol`;這只代表未授權公開 probe 看不到,不得視為已 private也不得視為 repo 不存在。
- private backup verified`0`
- private visibility owner evidence`0/9`
- safe credential metadata accepted`0/9`
- owner response received / accepted`0 / 0`
- execution ready`0`
**驗證**
- `python3 scripts/security/github-target-private-backup-evidence-gate.py --root .``GITHUB_TARGET_PRIVATE_BACKUP_EVIDENCE_GATE_BLOCKED targets=9 public_visible=4 private_verified=0 credential=0/9 refs_sync=False`
- `python3 -m py_compile scripts/security/github-target-private-backup-evidence-gate.py apps/api/src/services/github_target_private_backup_evidence_gate.py apps/api/src/api/v1/agents.py` → OK
- `python3 -m json.tool docs/security/github-target-private-backup-evidence-gate.snapshot.json` → OK
- `python3 -m json.tool docs/schemas/github_target_private_backup_evidence_gate_v1.schema.json` → OK
- `DATABASE_URL=postgresql://test:test@localhost:5432/test REDIS_URL=redis://localhost:6379/15 SECRET_KEY=test-secret ENVIRONMENT=dev pytest apps/api/tests/test_github_target_private_backup_evidence_gate.py apps/api/tests/test_github_target_private_backup_evidence_gate_api.py -q``6 passed`
- `python3 scripts/ops/doc-secrets-sanity-check.py docs/security/GITHUB-TARGET-PRIVATE-BACKUP-EVIDENCE-GATE.md docs/security/github-target-private-backup-evidence-gate.snapshot.json docs/schemas/github_target_private_backup_evidence_gate_v1.schema.json``DOC_SECRET_SANITY_OK scanned_files=3`
- `git diff --check -- ...` → OK
- `pnpm --filter @awoooi/web typecheck` → blocked此 worktree 未安裝 `node_modules``tsc: command not found`;需在依賴恢復後補跑,不能把前端 typecheck 視為已通過。
**安全旗標**
- `runtime_execution_authorized=false`
- `active_scan_authorized=false`
- `credentialed_scan_authorized=false`
- `host_update_authorized=false`
- `wazuh_api_live_query_authorized=false`
- `wazuh_active_response_authorized=false`
- `repo_creation_authorized=false`
- `visibility_change_authorized=false`
- `refs_sync_authorized=false`
- `workflow_modification_authorized=false`
- `workflow_trigger_authorized=false`
- `github_primary_switch_authorized=false`
- `secret_values_collected=false`
- `backup_execution_authorized=false`
- `restore_execution_authorized=false`
- `migration_authorized=false`
**邊界與修正**
- 本段沒有 SSH、沒有改 110/111/112/120/121/168、沒有改 Nginx / Docker / K8s / firewall、沒有觸發 Gitea / GitHub workflow、沒有建立 repo、沒有修改 visibility、沒有同步 refs、沒有讀取或保存 secret value。
- Wazuh / SOC / Kali runtime 仍由 IwoooS 主控線處理;本段沒有新增 Wazuh UI / API也沒有把 GitHub mirror governance 接到 Wazuh runtime。
- 先前曾因工具預設工作目錄短暫在 `/Users/ogt/awoooi` 誤新增同名 script已立即移除並驗證 `WRONG_WORKTREE_CLEAN_OK`;實際有效工作僅保留在受控 worktree `/Users/ogt/codex-workspaces/awoooi-dev`
**下一個 P0 gate**
- 需要 owner 提供每個 GitHub target 的 private visibility evidence ref 與 safe credential metadata evidence ref`private_backup_verified=0``safe_credential_accepted_evidence=0/9``owner_response_accepted=0` 之前,不得進入 repo creation、visibility change、refs sync、workflow trigger 或 GitHub primary switch。
## 2026-06-2514:41 post-start quick check live wrapper 分級讀回
**背景**:第一版 `post-start-quick-check.sh` live run 將預期中的 `escrow_missing=5` 與 MOMO 非服務面 warning 一併算成 `DEGRADED`,容易讓重啟 SOP 看起來永遠差一點。這不符合本輪目標服務恢復、資料新鮮、備份健康、DR escrow、Wazuh registry 必須分層判定。
**修正**
- `scripts/reboot-recovery/post-start-quick-check.sh` 將 warning 分成 `SERVICE``BOUNDARY``EVIDENCE`
- `SERVICE>0` 才回 `RESULT=DEGRADED` / exit `1`
- `BOUNDARY>0` 且 service blocker 為 `0` 時回 `RESULT=FULL_STACK_GREEN_DR_ESCROW_BLOCKED` / exit `0`
- `EVIDENCE>0` 且 service blocker / boundary 為 `0` 時回 `RESULT=GREEN_WITH_EVIDENCE_WARNINGS` / exit `0`
- `docs/runbooks/REBOOT-POST-START-QUICK-CHECK.md` 已補 wrapper exit code 與分級語意。
**14:41 live read-only wrapper 證據**
- `scripts/reboot-recovery/post-start-quick-check.sh --no-color` 回 exit code `0`
- Hosts / SSH110、120、121、188 ping 與 SSH port OK。
- Cold-start`PASS=89 WARN=0 BLOCKED=0`Result `GREEN`
- MOMOhealth `V10.676`dedicated preflight `PASS=19 WARN=2 BLOCKED=0`latest job `57 completed``DB_DAILY_FRESHNESS 1|2026-06-24`current-month DB parity through `2026-06-24`
- Backup110 `13/13 fresh failed=0`188 `2/2 fresh failed=0``core_blockers=0``offsite_fresh=1``rclone_gdrive_fresh=1``escrow_missing=5`
- RoutesAWOOI API、IwoooS、MOMO health、Stock direct smoke all `200`
- 110 CPUload 約 `4.09 / 4.52 / 4.39`top processes show active Gitea / runner / build-test load and platform services沒有 orphan Chrome 復發證據。
- Wrapper summary`POST_START_QUICK_CHECK PASS=18 WARN=2 BLOCKED=0``POST_START_QUICK_CHECK_WARNINGS SERVICE=0 BOUNDARY=1 EVIDENCE=1``RESULT=FULL_STACK_GREEN_DR_ESCROW_BLOCKED`
**判定**
- 可宣稱:重啟後服務面 / public routes / K3s / MOMO data freshness / backup core 已恢復post-start quick check wrapper 可作為 T+10 分鐘標準入口。
- 不可宣稱DR complete、credential escrow complete、Wazuh manager registry accepted、Wazuh active response / host write / runtime security gate。`escrow_missing=5` 必須保留為 DR blocker不得偽造 marker。
**邊界**:本輪只做 read-only wrapper live run、repo-side script / docs 修正與 guard沒有 Docker / systemd / Nginx / firewall / K8s / ArgoCD / Wazuh runtime 寫操作,沒有 import沒有讀 token沒有推 `gitea/main` 或觸發 CD。
## 2026-06-25重啟後一頁式總檢查 SOP 補強
**背景**SOP v1.51 已能判定 full-stack service GREEN但長 SOP 太完整,不適合作為每次重啟後 T+10 分鐘內的操作頁。為避免下一次又在 route 200、container healthy、DB freshness、backup、Wazuh registry、DR escrow 之間混淆,本輪新增一頁式 post-start quick check。
**更新**
- 新增 `scripts/reboot-recovery/post-start-quick-check.sh`,提供重啟後 10 分鐘只讀 wrapper主機 / SSH、cold-start scorecard、MOMO freshness、backup / offsite / escrow、public routes、110 CPU / runaway process。
- 新增 `docs/runbooks/REBOOT-POST-START-QUICK-CHECK.md` 作為 wrapper 說明與人工 fallback。
- `docs/runbooks/FULL-STACK-COLD-START-SOP.md` 升級為 `v1.52`,於最新 baseline 直接連到 quick check。
- `docs/workplans/2026-06-04-reboot-cold-start-backup-recovery-workplan.md` 更新 P3 docs / automation contract 與 P3-008明確區分短版 quick check 與長 SOP / Plan B。
**邊界**:本輪 repo-side script / docs-only驗證只跑語法與 guard沒有執行 live SSH、Docker、systemd、Nginx、firewall、K8s、ArgoCD、Wazuh runtime、active scan 或 secret 操作。Quick check 仍禁止把網站 200 當資料最新、把 backup fresh 當 DR complete、或把 Wazuh route 200 當 agent registry accepted。
## 2026-06-2514:16 full cold-start GREEN / MOMO data freshness recovered
**背景**11:53 full cold-start 仍因 MOMO business data stale blocked。14:16 read-only refresh 顯示 MOMO 已成功匯入新資料,資料新鮮度與 Google Drive token metadata gate 均恢復,因此必須更新 SOP / workplan 的釋出判定。
**只讀證據**
- MOMO dedicated preflight`PASS=18 WARN=3 BLOCKED=0``https://mo.wooo.work/health` 與 local health 均 `200`,版本 `V10.674`
- MOMO container readback`momo-pro-system``momo-scheduler``momo-telegram-bot` 均 healthyscheduler `RestartCount=0`recent lifecycle events `11` 仍記為 WARN / stability evidence。
- Token metadatahost `TOKEN_STAT 100000:100000:600` matches scheduler UID `100000`container token artifact exists with restrictive mode `600`。未讀取 token 內容。
- DB freshness`DB_DAILY 109061|2025-07-01|2026-06-24``DB_MONTHLY_SYNC 15383|15383|2026-06-01|2026-06-24|2026-06-01|2026-06-24``DB_DAILY_FRESHNESS 1|2026-06-24`
- Latest importjob `57 completed|即時業績_當日.xlsx|2026-06-25T13:16:47|2026-06-25T13:18:02|15383|15383|0`
- Full cold-start`scripts/reboot-recovery/full-stack-cold-start-check.sh --monitor-read-only --no-color --watch --interval 1 --max-attempts 1``2026-06-25 14:16:30 CST` 回 exit code `0``PASS=89 WARN=0 BLOCKED=0`Result `GREEN`
- Public route/TLS gate OKAWOOI API/Web、MOMO Web/health、Gitea、Harbor、Registry、Sentry、SigNoz、Stock、Langfuse、Bitan、AIops 均為 expected 2xx/3xx。Direct smoke 補充AWOOI API `200`、IwoooS `200`、MOMO health `200`、Stock `200`
- Backup status 仍為 core healthy / DR incomplete110 `13/13 fresh failed=0`、188 `2/2 fresh failed=0``core_blockers=0``integrity_stale=0``offsite_fresh=1``rclone_gdrive_fresh=1``escrow_missing=5`
- 110 CPUload 約 `3.85 / 3.33 / 3.19`top CPU 是 active Gitea Actions / 2026 World Cup pipeline 與正常平台服務;沒有 orphan Chrome 復發證據。
**判定**
- 可宣稱full-stack service readiness is GREEN for controlled runner/CD releaseMOMO service and business data freshness are recoveredpublic routes / K3s / backup core / exporters are green。
- 不可宣稱DR complete、credential escrow complete、Wazuh host registry accepted、runtime/security acceptance。`escrow_missing=5` 仍是 DR blocker。
**邊界**:本輪全部只讀;沒有 Docker / systemd / Nginx / firewall / K8s / ArgoCD write沒有手動 import沒有讀 token沒有 Wazuh / 112 / SOC 操作。
## 2026-06-2511:53 cold-start / backup 最終 read-only refresh
**背景**11:44 已確認 MOMO preflight 可捕捉 `V10.667`、container StartedAt、recent lifecycle events 與 source absence。本輪補最後一個全棧 read-only scorecard / backup refresh避免只用單站 `/health` 判斷恢復。
**只讀證據**
- `scripts/reboot-recovery/full-stack-cold-start-check.sh --monitor-read-only --no-color --watch --interval 1 --max-attempts 1``2026-06-25 11:53:00 CST` 預期 exit code `2`,結果 `PASS=87 WARN=1 BLOCKED=1`
- 110 / 120 / 121 / 188 ping + SSH OK188 PostgreSQL / Redis / SignOz / MOMO health OK110 Harbor / Gitea / Prometheus / Alertmanager / Sentry OKK3s `mon` / `mon1` ReadyVIP presentnode storage conditions cleanAWOOI pods Running/Completed。
- Public route/TLS gate OKAWOOI API/Web、MOMO Web/health、Gitea、Harbor、Registry、Sentry、SigNoz、Stock、Langfuse、Bitan、AIops 均回 expected 2xx/3xx。
- MOMO cold-start evidence`MOMO_GDRIVE_TOKEN_STAT missing scheduler_uid=100000``MOMO_MONTHLY_SYNC 10936|10936|2026-06-01|2026-06-17|2026-06-01|2026-06-17``MOMO_DAILY_FRESHNESS 8|2026-06-17`、latest job `56 completed|即時業績_當日.xlsx|2026-06-18T11:41:00|2026-06-18T11:42:02|10936|10936|0`
- 110 backup status110 `13/13 fresh failed=0`、188 `2/2 fresh failed=0``core_blockers=0``integrity_stale=0``offsite_fresh=1``rclone_gdrive_fresh=1``escrow_missing=5`、last aggregate `2026-06-25 02:35:09`
- Direct route smoke 補充AWOOI API `200`、IwoooS `200`、VibeWork `200`、MOMO health `200`
**判定**主機、K3s、public routes、core backup/offsite、AWOOI API/Web、MOMO service health 均可用full-stack 仍不可宣稱 green唯一 service hard blocker 仍是 `188 momo daily sales data stale beyond 3 days`DR 仍因 credential escrow `5` 缺口 blocked。
**邊界**:本輪全部只讀;沒有 Docker / systemd / Nginx / firewall / K8s / ArgoCD write沒有 import / token read / Drive file movement沒有 Wazuh / 112 / SOC 操作。
## 2026-06-2511:44 MOMO V10.667 preflight 強化與替換事件回讀
**背景**11:35 readback 後188 上 MOMO 又經歷一次自動替換 / restart warm-up`/health` 版本前進到 `V10.667`。為避免把舊 StartedAt 或單純 HTTP 200 當成最新狀態,本輪增強 `scripts/reboot-recovery/momo-drive-token-source-recovery-preflight.sh`,讓 SOP 直接讀 MOMO version、三個核心容器 StartedAt / health / restart count、45 分鐘內 lifecycle events、以及 188 / backup 路徑上是否存在精確 `即時業績_當日.xlsx` 候選檔。
**只讀證據**
- `scripts/reboot-recovery/momo-drive-token-source-recovery-preflight.sh` 語法檢查通過live run 預期 exit code `2`,結果 `PASS=15 WARN=5 BLOCKED=2`
- `https://mo.wooo.work/health` 與 188 local health 都回 `200`,版本為 `V10.667`
- 188 container metadata`momo-pro-system` StartedAt `2026-06-25T03:43:30Z``momo-scheduler` StartedAt `2026-06-25T03:43:31Z``momo-telegram-bot` StartedAt `2026-06-25T03:43:30Z`,三者 health 皆 `healthy`scheduler `RestartCount=0`
- `MOMO_CONTAINER_REPLACE_EVENTS_45M 23`,代表 11:42-11:43 仍有 recent lifecycle / replacement evidence本視窗沒有執行 `docker compose`、container restart、Docker / systemd / Nginx / firewall / K8s write。
- `LOCAL_EXACT_DAILY_SOURCE_COUNT 0``LOCAL_EXACT_DAILY_SOURCE_LATEST none`188 `/home/ollama/momo-pro``/backup` 未找到可直接作為 Drive daily-sales intake 的精確 `即時業績_當日.xlsx` 候選。
- DB freshness 未變:`DB_DAILY 104614|2025-07-01|2026-06-17``DB_MONTHLY_SYNC 10936|10936|2026-06-01|2026-06-17|2026-06-01|2026-06-17``DB_DAILY_FRESHNESS 8|2026-06-17`、latest daily import job `56 completed|即時業績_當日.xlsx|2026-06-18T11:41:00|2026-06-18T11:42:02|10936|10936|0`
**判定**
- 可宣稱MOMO public service 目前是 `V10.667` 且 app / scheduler / bot healthypreflight 現在能捕捉 version、容器替換事件與 source-file absence不再只看 `/health`
- 不可宣稱MOMO data current、Google token repaired、source file restored、full-stack green、DR complete。唯一 hard service blocker 仍是 MOMO business data freshnesscredential escrow 仍缺 `5`
**邊界**:本輪全部為 read-only SSH / curl / Docker metadata / DB query / docs-only沒有 import、沒有移動 Drive 檔、沒有讀 token、沒有主機 runtime write沒有 Wazuh / 112 / SOC 操作。
## 2026-06-2511:35 MOMO V10.665 replace 後 cold-start / source absence readback
**背景**11:32 read-only refresh 後188 Docker events 顯示 MOMO `momo-pro-system``momo-scheduler``momo-telegram-bot` 在 11:33 發生 compose replace / restart。這不是本視窗手動操作本視窗沒有執行 Docker / systemd / Nginx / firewall / K8s write。因版本與容器 StartedAt 已變,需要重跑最新 health / preflight / cold-start避免用 11:21 的舊容器證據宣稱當前狀態。
**只讀證據**
- `https://mo.wooo.work/health``{"database":"postgresql","status":"healthy","version":"V10.665"}`
- 188 container metadata`momo-pro-system` StartedAt `2026-06-25T03:33:25Z``momo-scheduler` StartedAt `2026-06-25T03:33:28Z``momo-telegram-bot` StartedAt `2026-06-25T03:33:28Z`,三者健康狀態皆 `healthy``RestartCount=0`。Docker events 顯示 compose replace / SIGTERM / restart sequence。
- 11:35 repo-side cold-start check 回預期 exit code `2`,分數仍為 `PASS=87 WARN=1 BLOCKED=1`。110 / 120 / 121 / 188 ping + SSH OKK3s `mon` / `mon1` ReadyVIP presentpublic routes/TLS greenAWOOOI API/Web reachablebackup/exporter surfaces fresh。
- 11:35 MOMO dedicated preflight 仍為 `PASS=8 WARN=4 BLOCKED=2`public/local health OK、scheduler running/healthy、token metadata host/container both `missing``DB_DAILY_FRESHNESS 8|2026-06-17`、latest job `56 completed|即時業績_當日.xlsx|2026-06-18T11:41:00|2026-06-18T11:42:02|10936|10936|0`
- Targeted read-only source search on 188 did not find a newer `即時業績_當日` intake file; latest relevant successful daily import remains job `56`。Local `data/excel_exports/MOMO_All_20260620_2211.xlsx` exists, but it is an export artifact, not the configured Drive daily-sales intake source `當日業績匯入|即時業績_當日`
- Import config readback remains `gdrive_folder_path=當日業績匯入` and `gdrive_file_pattern=即時業績_當日`。DB bounds remain `daily_sales_snapshot 2025-07-01..2026-06-17 / 104614` and current-month `realtime_sales_monthly 2026-06-01..2026-06-17 / 10936`
**判定**
- 可宣稱MOMO public service is healthy on `V10.665`; 11:33 replace 後 routes / K3s / backup / monitoring / MOMO container health 仍可用。
- 不可宣稱MOMO data current、source file restored、Google token repaired、full-stack green、DR complete。新版服務與網站 `200` 不等於資料新鮮;唯一 hard blocker 仍是 MOMO business data freshness。
**邊界**:本輪全部為 read-only SSH / curl / DB / Docker metadata / cold-start / backup evidence沒有 import、沒有移動 Drive 檔、沒有讀 token、沒有主機 runtime write沒有 Wazuh / 112 / SOC 操作。
## 2026-06-2511:21 CPU 清理後 live route / cold-start / backup refresh
**背景**11:01 已完成 MOMO preflight gate 與 110 orphan Chrome SIGTERM 收斂;本輪補一個更完整的只讀 refresh確認服務、route、backup 與 CPU 狀態是否回穩。
**只讀證據**
- Public routes direct smoke 在必要時 follow redirect 後全部回 `200`AWOOOI API、`/zh-TW/iwooos`、VibeWork、AwoooGo、MOMO health、Stock、Bitan。
- Repo-side cold-start check 於 `2026-06-25 11:21:07 CST` 回預期 exit code `2`,分數為 `PASS=87 WARN=1 BLOCKED=1`。110 / 120 / 121 / 188 ping + SSH OKK3s `mon` / `mon1` ReadyVIP presentnode storage conditions cleanpublic routes/TLS greenAWOOOI API 與 Web reachable。
- MOMO 在 cold-start 的狀態未變:`MOMO_GDRIVE_TOKEN_STAT missing scheduler_uid=100000``MOMO_MONTHLY_SYNC 10936|10936|2026-06-01|2026-06-17|2026-06-01|2026-06-17``MOMO_DAILY_FRESHNESS 8|2026-06-17`、latest import job `56 completed` clean。這與 dedicated preflight `PASS=8 WARN=4 BLOCKED=2` 一致。
- 110 的 backup status 使用 `/backup/scripts/backup-status.sh --no-notify --no-refresh` 讀回:核心備份健康但 DR 未完成110 `13/13 fresh failed=0`、188 `2/2 fresh failed=0``core_blockers=0``integrity_stale=0``offsite_fresh=1``rclone_gdrive_fresh=1``escrow_missing=5`、last aggregate `2026-06-25 02:35:09`
- 110 CPU 在前一輪 orphan cleanup 後11:20 load 約 `6.71 / 5.79 / 6.00``vmstat` 顯示 CPU idle 約 `56%..58%`、iowait `4%`。最高 CPU 來源是有 parent node process 的 active `stockplatform-product-ux-smoke.mjs`,另有 `pnpm install` / `uvicorn`;這不是 orphan Chrome本輪未清掉。
**判定**
- 可宣稱:此證據集內 core hosts、K3s、public routes、backup/offsite surfaces、AWOOOI API/Web、MOMO service health、monitoring/exporter surfaces 可用。
- 不可宣稱full-stack green、MOMO data current、Drive token repaired、DR complete、credential escrow complete。唯一服務硬阻擋仍是 MOMO business data freshnesstoken metadata 仍是 WARNcredential escrow 仍是 DR blocker。
**邊界**:本輪全部為 read-only SSH / curl / DB / cold-start / backup-status evidence沒有 Docker / systemd / Nginx / firewall / K8s / ArgoCD write沒有 CI cancellation沒有 Wazuh / 112 / SOC change沒有 secret read。
## 2026-06-2511:01 MOMO preflight gate 與 110 CPU orphan Chrome 收斂
**背景**10:45 已把 MOMO Drive token/source recovery 寫成 owner-gated 文件邊界;同時使用者詢問 110 CPU load 持續偏高。需要把兩者都收斂成可重跑證據MOMO 不能只看 `/health`110 CPU 也不能只看 load average。
**Read-only / approved evidence**
- 新增 `scripts/reboot-recovery/momo-drive-token-source-recovery-preflight.sh`,只做 read-only SSH / Docker metadata / scheduler logs / DB query不讀 token 內容、不 import、不移動 Drive 檔、不 restart。
- 11:01 live preflight returned expected exit code `2``PASS=8 WARN=4 BLOCKED=2`。MOMO public health `200`、local health `200``momo-scheduler` running/healthy、current-month DB parity `10936|10936|2026-06-01|2026-06-17|2026-06-01|2026-06-17`、latest daily import job `56 completed` clean。
- Remaining MOMO blockershost/container Google token metadata both `missing``DB_DAILY_FRESHNESS 8|2026-06-17`scheduler recent fail-closed notification log not observed after the latest container restart window, so it remains WARN rather than success evidence。
- 110 CPU read-only triage at 10:56 showed load `10.99 / 9.19 / 7.49`, 12 cores, low iowait and no active swap thrash. Top CPU was `stockplatform-review-bulk-ux` orphan Chrome groups plus active Gitea Actions CI load。
- 使用者在 CPU triage 後批准繼續10:58 only write action was targeted `SIGTERM` to orphan Chrome process groups `438005`, `471295`, `640155`, `670628` on 110。Post-check `OLD_GROUPS_REMAINING` returned empty。
- 11:01 110 load dropped to around `8.24 / 9.62 / 8.24`; remaining CPU was active Gitea Actions / CI build work, not the killed orphan groups。
**仍維持 0 / false**
- token value / hash / partial collection、manual import、Drive file movement、DB truncate / restore、Docker / systemd / Nginx / firewall / K8s / ArgoCD write、CI cancellation、Wazuh / 112 / SOC change、secret read。
**判定**MOMO recovery 現在有獨立 preflight gate110 CPU PlayBook 再次確認 orphan browser smoke 可被精準 SIGTERM但 active Gitea Actions / CI load 必須觀察 queue / timeout不可當成 runaway process 亂殺。
## 2026-06-2510:45 MOMO Drive token recovery gate hardening
**背景**10:35 live refresh 確認 MOMO 服務與 DB parity 正常,但 Drive token artifact metadata missingscheduler 因 auth failure 正確 fail closed。既有 SOP 14.28 仍保留 2026-06-24「token live repair」舊口徑容易讓 operator 誤以為只要 owner/mode 還在就已修復。本輪只做 docs-only gate hardening。
**更新內容**
- `FULL-STACK-COLD-START-SOP.md` bump to v1.48 context and rewrite §14.28:最新狀態為 `MOMO_GDRIVE_TOKEN_STAT missing scheduler_uid=100000`host / container token path missing必須走 owner-gated token/source recovery。
- `FULL-STACK-COLD-START-SOP.md` 188 recovery card 補明:`could not locate runnable browser` / auth failure 先做 metadata-only 判讀;不得讀 token content不得沿用 2026-06-24 owner/mode repair as current truth。
- `workplan` P2-002 / P2-008 改成 `BLOCKED_MOMO_DATA_FRESHNESS_WITH_GDRIVE_TOKEN_WARN`:解除條件是 owner-provided non-secret evidence ref、maintenance window、rollback owner、token metadata-only verification、合法 newer source、import `sync_success=true`、file movement only after success、`MOMO_DAILY_FRESHNESS <= 2`
**仍維持 0 / false**
- token value / hash / partial collection、manual import、Drive file movement、DB truncate / restore、Docker / Nginx / firewall / K8s / ArgoCD write、Wazuh / 112 / SOC change、runtime gate。
**判定**SOP 現在不再把「Google token owner/mode 曾修過」誤當成當前狀態;最新 blocker 是 Drive token/source evidence + stale business data服務重啟不是正解。
## 2026-06-2510:35 live cold-start / route / DB refresh
**背景**10:23 已確認 MOMO Drive auth failure 會 fail closed本輪補一個更近的 read-only refresh確認 route、backup、DB freshness 與 import job 狀態是否有變。
**Read-only evidence**
- 110 live cold-start monitor at `2026-06-25 10:35:17 CST` returned expected exit code `2` with `PASS=85 WARN=1 BLOCKED=1`
- Host / K3s110 / 120 / 121 / 188 ping + SSH OKK3s `mon` / `mon1` ReadyVIP `192.168.0.125` presentnode not-ready / read-only filesystem / disk-pressure / filesystem-error events all `0`
- Public routes direct smoke all returned `200` after redirect-following where applicableAWOOOI API、`/zh-TW/iwooos`、VibeWork、AwoooGo、MOMO health、Stock、Bitan。
- Backup status from 110 remains green for backup surfaces110 `13/13 fresh failed=0`、188 `2/2 fresh failed=0``core_blockers=0``integrity_stale=0``offsite_fresh=1``rclone_gdrive_fresh=1``escrow_missing=5`
- MOMO DB read-only query`daily_sales_snapshot=104614 rows, 2025-07-01..2026-06-17`current-month `realtime_sales_monthly=10936 rows, 2026/06/01..2026/06/17`
- Latest import jobs remain unchanged after the fail-closed scheduler proofjob `56 completed` for `即時業績_當日.xlsx`, created `2026-06-18 11:41:00`, completed `2026-06-18 11:42:02`, `10936/10936/0`jobs `49..53` remain validation failures, not false successes。
- MOMO scheduler logs still show the 10:04 Drive auth failure path, Telegram failure notification success, then unrelated price / PChome backfill activity. No newer successful daily-sales import appeared by 10:35。
**判定**
- 可宣稱10:35 refresh reconfirms service availability, route health, backup/offsite freshness, and MOMO fail-closed behavior。
- 不可宣稱full-stack green、MOMO data current、Drive token repaired、new daily-sales source imported、DR complete。The hard blocker remains MOMO business data stale beyond 3 days; token metadata/writeback remains a WARN and escrow missing remains a DR blocker。
**邊界**:本輪全部為 read-only SSH / curl / DB query / log readback沒有主機寫入、沒有 Docker / Nginx / firewall / K8s / ArgoCD 操作,沒有 Wazuh / 112 / SOC 修改,沒有讀取或保存 secret。
## 2026-06-2510:23 MOMO fail-closed live scheduler proof / cold-start refresh
**背景**09:37 已完成 MOMO Drive auth fail-closed 正式部署與 cold-start readback但當時尚未等到下一次 scheduler 週期證明 live 行為。本輪只做 read-only scheduler log / cold-start / backup evidence refresh確認程式不再把 Google Drive auth failure 誤報成「沒有新檔案」成功。
**Read-only evidence**
- 188 `momo-scheduler``2026-06-25 10:04:39` 執行 `auto_import_task`,先記錄啟動 Google Drive 自動匯入,接著偵測到舊版 `token.pickle`,嘗試認證後回 `Google Drive 認證失敗: could not locate runnable browser`
- 同一輪 scheduler 已走 fail-closed 路徑:`Google Drive 連線或認證失敗未能確認來源資料夾是否有新檔案could not locate runnable browser`,並記錄 `[Scheduler] [AutoImport] ❌ 自動匯入失敗`
- 同一輪已送出正式失敗通知:`準備發送自動匯入失敗通知...``Telegram 通知發送成功``匯入失敗通知已發送`。這是正確紅燈,不是心跳噪音,也不是 no-file success。
- 10:23 full cold-start read-only check returned expected exit code `2` with `PASS=87 WARN=1 BLOCKED=1`。110 / 120 / 121 / 188 reachableK3s `mon` / `mon1` ReadyVIP presentpublic routes / AWOOOI API / MOMO service health / backup exporters available。
- MOMO current-month DB parity still matches but is stale`MOMO_MONTHLY_SYNC 10936|10936|2026-06-01|2026-06-17|2026-06-01|2026-06-17``MOMO_DAILY_FRESHNESS 8|2026-06-17`
- 10:23 cold-start now reports `MOMO_SOURCE_EMPTY_EVIDENCE_LINES 0`because the latest scheduler evidence is auth failure rather than a successful empty-folder listing。Hard blocker wording is now `188 momo daily sales data stale beyond 3 days`and WARN remains `188 momo Google Drive token ownership/writeback not confirmed` with `MOMO_GDRIVE_TOKEN_STAT missing scheduler_uid=100000`
- Backup status remains separate and green for core backup110 `13/13 fresh failed=0`、188 `2/2 fresh failed=0``core_blockers=0``integrity_stale=0``offsite_fresh=1``rclone_gdrive_fresh=1``escrow_missing=5`
**判定**
- 可宣稱MOMO Drive auth/API failure false-green 已完成 production 行為驗證scheduler 會 fail job path / 發正式失敗通知,而不是把 auth failure 當「沒有新檔案」成功。
- 可宣稱核心主機、K3s、public routes、AWOOI API health、MOMO service health、backup/offsite surfaces are available for this read-only evidence set。
- 不可宣稱full-stack green、MOMO data current、Google Drive token repaired、new source file imported、credential escrow complete、DR complete。現在剩下的是資料來源 / token metadata owner evidence gate而不是服務未啟動。
**邊界**:本輪沒有主機寫入、沒有 Docker / Nginx / firewall / K8s / ArgoCD 操作、沒有 Wazuh / 112 / SOC 修改,沒有讀取或保存 secret沒有使用聊天中的密碼。09:37 前一輪 MOMO `main` fast-forward / Gitea CD `#910` 是正式 release lane10:23 本輪只是 read-only evidence refresh。
## 2026-06-2509:37 MOMO Drive auth fail-closed deploy / cold-start readback
**背景**09:05 live refresh 顯示 MOMO 資料 freshness 仍停在 `2026-06-17`,且 Google Drive token metadata 缺失。後續 read-only log 追查確認 2026-06-25 00:37 後 scheduler 因缺 JSON token 嘗試 browser OAuth無頭環境回 `could not locate runnable browser`,但舊程式仍把它折成「沒有找到待匯入檔案」成功。這會讓排程假綠,必須先修 code boundary。
**Read-only / release evidence**
- MOMO local fix in `/Users/ogt/codex-workspaces/momo-pro-dev``e137d7a5d02a7595a44c3f3cc1cf54b766424ee7 fix(import): fail auto import on drive auth failure`
- 修補內容:`GoogleDriveService` 新增 metadata-only `last_error_kind` / `last_error``auto_import_from_drive()` 在 Drive auth/API 失敗時回 `success=false``failed_count=1``connection_error=true`,只在 Drive listing 成功且回空清單時保留「沒有找到待匯入的檔案」成功。
- MOMO tests`pytest tests/test_auto_import_failure_boundaries.py -q``4 passed``python3 -m py_compile services/google_drive_service.py services/import_service.py tests/test_auto_import_failure_boundaries.py` OK`git diff --check` OK。
- Gitea write / CDMOMO `main` fast-forwarded to `e137d7a5d02a7595a44c3f3cc1cf54b766424ee7`Gitea Actions `cd.yaml #910` returned `Success` in `1m9s` at `2026-06-25 09:35:20 +08:00`。這是正式 CD lane不是本視窗手動 SSH restart。
- 188 deploy readback`/home/ollama/momo-pro/services/import_service.py` and `momo-scheduler:/app/services/import_service.py` both contain the fail-closed marker `Google Drive 連線或認證失敗,未能確認來源資料夾是否有新檔案``google_drive_service.py` contains `last_error_kind``momo-scheduler` and `momo-telegram-bot` were restarted by CD and are healthy。
- MOMO public health after deploy`https://mo.wooo.work/health` returns healthy / PostgreSQL / `V10.657`
- Full cold-start after deploy at `2026-06-25 09:37:52 CST` remains expected `PASS=87 WARN=1 BLOCKED=1`。Hosts / K3s / public routes / AWOOOI API / backup surfaces are available; MOMO scheduler has `SCHEDULER_REGISTERED 5` and recent activity; current-month table parity remains `10936|10936|2026-06-01|2026-06-17|2026-06-01|2026-06-17`
- Backup readback remains green for core backup: 110 `13/13 fresh failed=0`、188 `2/2 fresh failed=0``core_blockers=0``integrity_stale=0``offsite_fresh=1``rclone_gdrive_fresh=1``escrow_missing=5`
**判定**
- 可宣稱MOMO import pipeline no longer falsely reports Drive auth failure as no-file success after CD `#910`; websites/routes/K3s/backups are available for this evidence set。
- 不可宣稱full-stack green、MOMO data current、Google Drive token repaired、new source file imported、credential escrow complete、DR complete。Hard blocker remains `188 momo source file absent while daily sales data stale` with `MOMO_DAILY_FRESHNESS 8|2026-06-17`; token/writeback remains a separate WARN and must not be repaired by reading token content。
**邊界**:本輪手動操作為 local code edit / tests / Gitea push / CD readback / read-only SSH verification。沒有手動 SSH 修改 188、沒有手動 Docker restart、沒有 Nginx / firewall / K8s / ArgoCD 操作,沒有 Wazuh / 112 / SOC 修改,沒有讀取或保存 secret。
## 2026-06-2509:05 live cold-start / backup / MOMO token read-only refresh
**背景**2026-06-24 23:33 已確認 SOP 能正確阻擋 MOMO source absence但今天已是 2026-06-25不能沿用昨日證據。本輪只做 read-only refresh 與文件同步,不碰 Wazuh / 112不做 Docker、Nginx、firewall、K8s、ArgoCD 或 host runtime 寫操作。
**Read-only evidence**
- Repo / Gitea baseline before this refresh`bb2ad032 docs(ops): record 23:33 cold-start readback [skip ci]`controlled AWOOOI workspace clean on `codex/awoooi-current-main-dev-base-20260624`
- `scripts/reboot-recovery/full-stack-cold-start-check.sh --monitor-read-only --no-color --watch --interval 1 --max-attempts 1` at `2026-06-25 09:05:37 CST` returned expected exit code `2` with `PASS=87 WARN=1 BLOCKED=1`
- Hosts / K3s110 / 120 / 121 / 188 ping and SSH port OKK3s `mon` / `mon1` both `Ready control-plane`VIP `192.168.0.125` presentnode filesystem / disk-pressure / readonly events `0`latest `km-vectorize-29705460-55rgs` completed about 6h before the check。
- Public routes direct smoke`awoooi API=200``/zh-TW/iwooos=200``vibework=200``awooogo=200``mo health=200``stock=200``gitea=200``harbor=200``registry /v2=401``sentry=200``signoz=200``langfuse=200``bitan=200``aiops=200`
- AWOOOI API health`status=healthy``environment=prod``mock_mode=false`postgresql / redis / openclaw / signoz / gcp ollama providers are up`ollama_local` was in a short cooldown and is not the current release blocker。
- MOMO service health`https://mo.wooo.work/health` returned healthy / PostgreSQL / `V10.655`
- MOMO data / scheduler`MOMO_MONTHLY_SYNC 10936|10936|2026-06-01|2026-06-17|2026-06-01|2026-06-17` remains green`MOMO_DAILY_FRESHNESS 8|2026-06-17` remains a hard blockerlatest job `56 completed` still has `sync_success=true` and bounds `2026-06-01..2026-06-17`
- MOMO Google Drive token metadata check, without reading token contenthost path `/home/ollama/momo-pro/config/google_token.json` is `missing`; container-side `config/google_token.json` is also `missing`; scheduler process runs as UID/GID `100000:100000`。This matches the cold-start WARN `188 momo Google Drive token ownership/writeback not confirmed` and is separate from the hard data-freshness blocker。
- Backup read-only status from 110 `/backup/scripts/backup-status.sh --no-notify --no-refresh` at 09:05110 `13/13 fresh failed=0`、188 `2/2 fresh failed=0``core_blockers=0``integrity_stale=0``offsite_fresh=1``rclone_gdrive_fresh=1``escrow_missing=5`、last aggregate `2026-06-25 02:35:09`
**判定**
- SOP 仍有效:它正確區分 hosts/routes/K3s/backups/service health 已恢復,以及 MOMO business data freshness / source evidence 仍 blocked沒有被網站 200、MOMO health 200、DB parity 或 backup green 誤判成 full-stack green。
- 可宣稱核心主機、K3s、public routes、AWOOOI API health、MOMO service health、backup/offsite surfaces are available for this read-only evidence set。
- 不可宣稱full-stack green、MOMO data current、DR complete、credential escrow complete、或 110 live monitor 已同步 repo v1.42。Google Drive token missing / writeback not confirmed 也不可用猜測或讀 token 的方式補證。
**邊界**:本輪沒有主機寫入、沒有 `scp` live script、沒有 Docker / Nginx / firewall / K8s / ArgoCD 操作、沒有 Wazuh / 112 / SOC 修改、沒有使用聊天中的密碼,也沒有讀取或保存 secret。
## 2026-06-2423:33 live cold-start / public routes / backup read-only refresh
**背景**23:15 已確認 110 live cold-start monitor 尚未同步 repo-side v1.42 hash本輪不做 live script install只用 repo-side authoritative script 重新跑完整 read-only cold-start確認重啟 SOP 的現場判斷是否仍正確。

View File

@@ -15,6 +15,38 @@
> 2026-06-24 23:04 Codex cold-start gate refresh: repo-side v1.42 dry-run now emits MOMO source-absence evidence and blocks with `188 momo source file absent while daily sales data stale`; backup/offsite remains green and live 110 script deployment is not claimed.
> 2026-06-24 23:15 Codex live-sync gate readback: read-only deploy parity check correctly blocks because repo cold-start hash `f60b81029969a527dc742ebc9558d2933f11fe24ec4f46f7a7bc6637759b7b05` differs from 110 live hash `10608873d406911a519afa96218abebc2b85ab6123bdf46b6e21eb269e554bb8`; installer remains a live write requiring explicit approval.
> 2026-06-24 23:33 Codex backup readback: 110 `13/13 fresh failed=0`, 188 `2/2 fresh failed=0`, `core_blockers=0`, `integrity_stale=0`, `offsite_fresh=1`, `rclone_gdrive_fresh=1`, `escrow_missing=5`; live cold-start still blocks only on MOMO source absence / data freshness, not backup.
> 2026-06-25 09:05 Codex backup readback: 110 `13/13 fresh failed=0`, 188 `2/2 fresh failed=0`, `core_blockers=0`, `integrity_stale=0`, `offsite_fresh=1`, `rclone_gdrive_fresh=1`, `escrow_missing=5`; live cold-start is `PASS=87 WARN=1 BLOCKED=1` because MOMO business data is stale and MOMO Google Drive token metadata is missing.
> 2026-06-25 09:37 Codex MOMO deploy readback: MOMO `main` is `e137d7a5d02a7595a44c3f3cc1cf54b766424ee7`, Gitea Actions `cd.yaml #910` succeeded, and 188 host / `momo-scheduler` source now fail closed on Google Drive auth/API failure. Backup/offsite remains green; full-stack still blocks on MOMO data freshness and `escrow_missing=5`.
> 2026-06-25 10:23 Codex MOMO fail-closed live proof: 10:04 scheduler run recorded Google Drive auth failure as `❌ 自動匯入失敗` and sent Telegram failure notification successfully; cold-start remains `PASS=87 WARN=1 BLOCKED=1` because business data is stale beyond 3 days and Drive token metadata/writeback is not confirmed. Backup/offsite remains green and `escrow_missing=5` remains the DR blocker.
> 2026-06-25 10:35 Codex route / DB / backup refresh: direct public routes for AWOOOI API, IwoooS, VibeWork, AwoooGo, MOMO health, Stock, and Bitan are 200; backup remains 110 `13/13` and 188 `2/2` fresh; MOMO daily and monthly DB bounds still stop at `2026-06-17`; latest import job remains `56 completed`.
---
## 2026-06-25 10:35 Backup / Offsite / Escrow Live Status
Read-only evidence sources: `/backup/scripts/backup-status.sh --no-notify --no-refresh` from 110 at 10:35 Asia/Taipei; scheduler log proof at 10:04; cold-start rerun at 10:35; public route and DB readback at 10:35.
- 110 backup health: `13/13 fresh failed=0`
- 188 backup health: `2/2 fresh failed=0`
- Integrity / configured blockers: `core_blockers=0``dr_warnings=5``configured_missing_110=0``configured_missing_188=0``script_missing_110=0``script_missing_188=0``integrity_stale=0`
- Offsite / GDrive freshness: `offsite_configured=1``offsite_fresh=1``rclone_gdrive_configured=1``rclone_gdrive_fresh=1`
- Last aggregate backup: `2026-06-25 02:35:09`
- DR blocker remains: `escrow_missing=5`,不得偽造 evidence marker也不得貼 secret value / hash / partial token。
- Full-stack service release blocker remains separate: cold-start `PASS=85 WARN=1 BLOCKED=1`,原因是 MOMO business data freshness stale (`MOMO_DAILY_FRESHNESS 8|2026-06-17`) plus Google Drive token metadata missing / writeback not confirmed。這不是 backup freshness failure。
- MOMO code boundary now covers both failure modes: `cd.yaml #904` makes monthly sync failure fail the import job and prevents Drive file movement; `cd.yaml #910` makes Drive auth/API failure return `success=false` instead of a no-file success.
- Live scheduler proof: 2026-06-25 10:04 `auto_import_task` logs `Google Drive 認證失敗: could not locate runnable browser`then logs `❌ 自動匯入失敗` and sends Telegram failure notification successfully. Therefore the alert is now a correct failure signal, not a heartbeat / no-file false green.
- MOMO DB readback: `daily_sales_snapshot=104614|2025-07-01|2026-06-17`; current-month `realtime_sales_monthly=10936|2026/06/01|2026/06/17`; latest import job remains `56 completed` with `10936/10936/0` and no newer successful daily-sales import by 10:35.
| Gate | Status | Evidence |
|------|--------|----------|
| 110 backup freshness | VERIFIED | 13/13 fresh, failed count 0. |
| 188 backup freshness | VERIFIED | 2/2 fresh, failed count 0. |
| Offsite / GDrive freshness | VERIFIED | `offsite_fresh=1`, `rclone_gdrive_fresh=1`. |
| Backup core blockers | GREEN | `core_blockers=0`. |
| Credential escrow | BLOCKED | `escrow_missing=5`; only real non-secret owner evidence may close this. |
| MOMO Drive token metadata | WARN | Host and container token metadata paths are missing; no token content was read. |
| MOMO Drive auth false-green | FIX_DEPLOYED_AND_LIVE_PROVEN | Gitea Actions `cd.yaml #910` success; 188 host and scheduler container source include fail-closed marker; 10:04 scheduler cycle failed closed and sent failure notification. |
| Service full green | NO-GO | Blocked by MOMO source absence / data freshness; token metadata warning also requires owner-gated evidence. |
---

View File

@@ -1,7 +1,7 @@
# AWOOOI 全棧冷啟動與主機重啟 SOP
> Version: v1.44
> Last updated: 2026-06-24 Asia/Taipei
> Version: v1.53
> Last updated: 2026-06-25 Asia/Taipei
> Scope: 110 / 120 / 121 / 188 full-stack reboot recovery. 112 Kali is recorded as P3 optional and is not part of this recovery path.
---
@@ -10,23 +10,27 @@
本節是每次接手、開機、關機、重啟後的第一個判定錨點。若日期不是今天,必須先重跑 live check再更新本節與 `docs/workplans/2026-06-04-reboot-cold-start-backup-recovery-workplan.md`
2026-06-24 23:33 live read-only refresh supersedes the earlier 22:16 / 23:04 scorecard wording. It confirms the SOP is behaving correctly: hosts, routes, K3s, AWOOOI API health, MOMO service health, and backup/offsite are available, while full-stack release remains blocked only by MOMO source absence / business data freshness and DR remains blocked by missing credential escrow evidence. The 110 live script parity blocker from 23:15 still applies.
若只是重啟後要快速判斷能不能宣稱恢復,先跑一頁式總檢查:`scripts/reboot-recovery/post-start-quick-check.sh --no-color`,並以 `docs/runbooks/REBOOT-POST-START-QUICK-CHECK.md` 作為人工 fallback。長 SOP 保留完整背景、例外處理與 Plan B短版 wrapper / checklist 負責每次 T+10 分鐘內的固定判定。
2026-06-25 14:41 post-start wrapper live read-only refresh supersedes the first wrapper wording. Hosts, routes, K3s, AWOOOI API health, MOMO service health, MOMO business data freshness, backup core/offsite, and core monitoring/exporter surfaces are green for controlled runner/CD release. MOMO is healthy on `V10.676`; latest import job `57` completed cleanly; `MOMO_DAILY_FRESHNESS 1|2026-06-24`; current-month daily snapshot and realtime tables match through `2026-06-24`. `post-start-quick-check.sh` now separates `SERVICE / BOUNDARY / EVIDENCE` warnings and returns `RESULT=FULL_STACK_GREEN_DR_ESCROW_BLOCKED` when service blockers are zero but `escrow_missing=5` remains. Do not turn this into a DR complete or security/runtime acceptance claim. Wazuh host registry acceptance remains outside this SOP lane and is still not complete.
```text
Repo-side reboot SOP / Plan B / automation contracts: COMPLETE, 100%.
Live cold-start read-only check: 2026-06-24 23:33 PASS=88 WARN=0 BLOCKED=1, Result=BLOCKED.
Repo-side cold-start v1.42 live read-only run: New MOMO fields remain MOMO_SOURCE_EMPTY_EVIDENCE_LINES=21, MOMO_IMPORT_CONFIG=當日業績匯入|即時業績_當日, MOMO_LATEST_IMPORT_JOB=56|completed|即時業績_當日.xlsx|2026-06-18T11:41:00.853176|2026-06-18T11:42:02.309425|10936|10936|0. The only BLOCKED text is "188 momo source file absent while daily sales data stale". Live 110 script sync is not claimed until a separate approved deployment/sync happens.
Live cold-start read-only check: 2026-06-25 14:41 wrapper delegated cold-start PASS=89 WARN=0 BLOCKED=0, Result=GREEN.
Post-start quick check: 2026-06-25 14:41 PASS=18 WARN=2 BLOCKED=0; warning split SERVICE=0 BOUNDARY=1 EVIDENCE=1; Result=FULL_STACK_GREEN_DR_ESCROW_BLOCKED; exit code 0.
Repo-side cold-start v1.42+ live read-only run: MOMO source absence / stale data blocker is cleared by import job 57 and `MOMO_DAILY_FRESHNESS 1|2026-06-24`. Live 110 script sync is not claimed until a separate approved deployment/sync happens.
110 live-sync parity: 2026-06-24 23:15 read-only `verify-cold-start-monitor-deploy.sh` correctly BLOCKED because repo script hash `f60b81029969a527dc742ebc9558d2933f11fe24ec4f46f7a7bc6637759b7b05` differs from 110 live hash `10608873d406911a519afa96218abebc2b85ab6123bdf46b6e21eb269e554bb8`. Do not use live 110 monitor output to prove v1.42 behavior until the approved live-sync gate in §13.3.1 passes.
Service state: SERVICE_AVAILABLE_MOMO_SOURCE_BLOCKED_DR_ESCROW_BLOCKED; 110/120/121/188 reachable, K3s mon/mon1 Ready, ArgoCD awoooi-prod Synced/Healthy at revision 7db7800e399caed5487a705c81ec993dec76c70f, public routes/TLS green, 110/188 backup health fresh, 188 node-exporter / PostgreSQL exporter / Redis exporter restored, 188 MinIO endpoint and Velero BackupStorageLocation restored, 110 disk pressure cleared.
Runtime release state: API/Web/Worker are ready; production API health returns healthy with `environment=prod`, `mock_mode=false`, and postgresql / redis / openclaw / signoz / ollama providers all up. 23:33 redirect-followed route batch shows awoooi API=200, `/zh-TW/iwooos`=200, vibework=200, awoooogo=200, momo health=200, stock=200, bitan=200, gitea=200, harbor=200, sentry=200, signoz=200, langfuse=200, aiops=200, registry /v2=401; cold-start raw route gate still records expected redirect statuses such as awoooi web=307, momo web=302, sentry=302.
MOMO release state: mo.wooo.work health is healthy on version V10.653. Gitea main fast-forwarded to 84035906aba0e5e190d031a13cfd9b47a8cd1f73 and Gitea Actions cd.yaml #904 completed Success. 188 live source contains the production marker `def _table_columns`, `業績分析儀表板同步失敗`, and `保留來源檔案等待重試,不移動 Google Drive 檔案`, proving the import-boundary fix is deployed. Mac Mini and MacBook Pro controlled Codex workspaces are both on branch codex/momo-current-main-dev-base-20260624 at commit 84035906aba0e5e190d031a13cfd9b47a8cd1f73 with dirty=0.
MOMO data state: full-table read-only DB query shows `daily_sales_snapshot=104614 rows, 2025/07/01..2026/06/17` and `realtime_sales_monthly=786621 rows, 2024/01/01..2026/06/17`. Current-month daily_sales_snapshot and realtime_sales_monthly match, but both stop at 2026-06-17. MOMO_DAILY_FRESHNESS is 7 days, which is a hard blocker because business data is not current.
Google Drive / source-file state: momo scheduler token ownership is fixed for Docker userns, container-side Drive listing works, and import config is `gdrive_folder_path=當日業績匯入`, `gdrive_file_pattern=即時業績_當日`; however scheduler stats and logs show repeated AutoImport runs with `file_count=0`, `imported_count=0`, including 2026-06-24 21:56 where the folder had `0` matching Excel files. Latest import job 56 was already completed on 2026-06-18 with `sync_success=true`, `source_file=即時業績_當日.xlsx`, and bounds `2026-06-01..2026-06-17`. Mac Mini and MacBook candidate spreadsheets were also read-only inspected: the local current daily candidate only contains 2025-07-01..2025-07-02, the iCloud full-month candidate only contains 2025-06-01..2025-06-30, and MacBook candidates are either header-only or the same 2025-07-01..2025-07-02 dataset. These are not legitimate newer sources.
Backup / monitoring state: backup-status core blockers are 0, 110 is 13/13 fresh failed=0, 188 is 2/2 fresh failed=0, offsite_fresh=1, rclone_gdrive_fresh=1, integrity_stale=0, last aggregate is 2026-06-24 02:28:39, 188 MinIO is healthy, Velero BackupStorageLocation default is Available, backup-health textfile reports Velero freshness green, PostgreSQL / Redis exporters are green, 188 nginx-exporter is restored with nginx_up=1, monitoring coverage is 14/14 jobs UP, and VeleroBackupNotRun / PostgreSQLDown / RedisDown / disk-pressure / nginx-exporter target-down evidence is resolved. 23:33 backup-status --no-notify --no-refresh reports 110 13/13 fresh failed=0, 188 2/2 fresh failed=0, core_blockers=0, integrity_stale=0, offsite_fresh=1, rclone_gdrive_fresh=1, escrow_missing=5.
Service state: FULL_STACK_GREEN_DR_ESCROW_BLOCKED; 110/120/121/188 reachable, K3s mon/mon1 Ready, public routes/TLS green, MOMO data fresh, 110/188 backup health fresh, 188 node-exporter / PostgreSQL exporter / Redis exporter restored, 188 MinIO endpoint and Velero BackupStorageLocation restored, 110 disk pressure cleared.
Runtime release state: API/Web/Worker are ready; production API health returns healthy with `environment=prod`, `mock_mode=false`, and postgresql / redis / openclaw / signoz / gcp ollama providers up. 14:16 direct route smoke returned 200 for AWOOOI API, `/zh-TW/iwooos`, MOMO health, and Stock; cold-start raw route gate returned all expected route statuses, including redirects such as awoooi web=307 and sentry=302.
MOMO release state: mo.wooo.work health is healthy on version V10.676 after 14:14-14:15 lifecycle / replacement events observed via Docker metadata. `momo-pro-system`, `momo-scheduler`, and `momo-telegram-bot` are healthy; scheduler `RestartCount=0`. 14:41 dedicated preflight returns PASS=19 WARN=2 BLOCKED=0, so retain stability / evidence notes, but no service blocker remains.
MOMO data state: current-month daily_sales_snapshot and realtime_sales_monthly match through 2026-06-24: `daily_sales_snapshot=109061|2025-07-01|2026-06-24`, `MOMO_MONTHLY_SYNC 15383|15383|2026-06-01|2026-06-24|2026-06-01|2026-06-24`, and `MOMO_DAILY_FRESHNESS 1|2026-06-24`. Latest import job is `57 completed|即時業績_當日.xlsx|2026-06-25T13:16:47.359958|2026-06-25T13:18:02.964985|15383|15383|0`.
Google Drive / source-file state: 14:16 cold-start reports `MOMO_GDRIVE_TOKEN_STAT 100000:100000:600 scheduler_uid=100000`. Dedicated preflight confirms host token metadata matches scheduler UID and restrictive mode; container token artifact exists with mode `600`. Token content was not read. Future Drive auth/API failure must still be treated as failed import evidence rather than no-file success.
110 CPU/load readback: 2026-06-25 10:58 user-approved minimal SIGTERM targeted only orphan `stockplatform-review-bulk-ux` Chrome process groups `438005`, `471295`, `640155`, and `670628`; `OLD_GROUPS_REMAINING` returned empty. 11:20 readback shows remaining CPU is active `stockplatform-product-ux-smoke.mjs` with parent node process plus install/build work, not orphan Chrome. No Docker/systemd/Nginx/firewall/K8s write was performed; do not cancel active CI/smoke unless separately approved.
Backup / monitoring state: 14:41 wrapper readback confirms backup core blockers are 0, 110 is 13/13 fresh failed=0, 188 is 2/2 fresh failed=0, offsite_fresh=1, rclone_gdrive_fresh=1, integrity_stale=0, last aggregate is 2026-06-25 02:35:09, and escrow_missing=5.
Notification-noise state: healthy AWOOOI heartbeat is suppressed; heartbeat warning dedupe uses stable actionable fingerprints so HTTP status / timeout / latency drift does not create a new Telegram event every 30 minutes; MOMO Pro monitor uses https://mo.wooo.work/health as primary truth and no longer checks the 188 root path; MoWoooWorkDown now labels component=momo-pro-system and requires public/local/container/data-freshness triage instead of blind restart; docker-health-monitor keeps 5-minute repair cadence but has a separate 30-minute Telegram fallback cooldown; Bitan public-content check keeps failure alerting with same-fingerprint cooldown and one recovery notice.
Monitoring coverage recovery state: if CD post-deploy fails only because `scripts/generate_monitoring.py --check` reports `nginx-exporter` down on `192.168.0.188:9113`, first verify 188 `stub_status` and restore the stateless exporter with `scripts/ops/188-nginx-exporter-restore.sh`; do not reload Nginx or restart product containers for this symptom.
Allowed declaration: core hosts, routes, K3s, backup/exporter surfaces are recovered; MOMO production code release includes the import-boundary fix at Gitea main 84035906aba0; both controlled Codex workspaces are aligned on the same MOMO fix branch; MOMO data pipeline is blocked waiting for a newer source file or owner-provided source evidence.
Forbidden declaration: full-stack green, MOMO data current, DR complete, or runtime/security acceptance. Credential escrow evidence is still missing and must not be forged.
Allowed declaration: full-stack service readiness is GREEN for controlled runner/CD release; core hosts, routes, K3s, backup/exporter surfaces, AWOOOI API health, MOMO service health, and MOMO data freshness are green for the latest read-only evidence set.
Forbidden declaration: DR complete, credential escrow complete, Wazuh host registry accepted, 110 live monitor synced, or runtime/security acceptance. Credential escrow evidence is still missing and must not be forged.
```
2026-06-24 22:17 Codex workstation continuity readback:
@@ -1346,7 +1350,7 @@ docker ps --format "{{.Names}}\t{{.Status}}" | head -120
188 post-reboot 不可用「首頁 200」取代 DB parity也不可用 DB parity 取代資料新鮮度。若出現 `posting list tuple ... cannot be split`,只走 `REINDEX TABLE CONCURRENTLY public.realtime_sales_monthly;`,不可 truncate 或整庫 restore。
2026-06-24 補充:若 `momo-scheduler` logs 出現 `Google Drive 認證失敗` / `Permission denied: 'config/google_token.json'`優先檢查 Docker user namespace 對應 UID。當前已驗證 scheduler process 在 host 上為 `100000:100000`token 檔必須是 `100000:100000 600` 才能讓 Google client 刷新並寫回 token。此步只改檔案 owner/mode不讀取、不保存、不貼上 token value。
2026-06-25 補充:若 `momo-scheduler` logs 出現 `Google Drive 認證失敗` / `could not locate runnable browser` / `Permission denied: 'config/google_token.json'`先做 metadata-only 判讀,不得讀 token 內容。最新 10:35 readback 顯示 host path `/home/ollama/momo-pro/config/google_token.json` 與 container-side `config/google_token.json` 都是 missingscheduler host UID 仍是 `100000`;因此不能沿用 2026-06-24「只改 owner/mode」的修復結論。解除 WARN 的最小安全流程是:取得 owner-provided non-secret evidence ref、確認維護窗口與 rollback owner、用不貼 token 的方式重新建立或恢復 token artifact、只檢查 `stat owner:group:mode` 與 scheduler auth readback、再跑 cold-start。未完成前MOMO health 200 與 DB parity 不能取代 token/writeback evidence。
### 14.4.3 120 恢復指揮卡
@@ -1818,31 +1822,34 @@ ssh ollama@192.168.0.188 'bash -s' < scripts/ops/188-node-exporter-restore.sh
恢復後再查 Prometheus / Alertmanager不要直接 silence。
### 14.28 2026-06-24 MOMO Google Drive token 與資料新鮮度 blocker
### 14.28 2026-06-25 MOMO Google Drive token 與資料新鮮度 blocker
2026-06-24 的第三段變更是把「MOMO 服務活著但資料不新」納入 cold-start hard gate。這不是網站 502也不是 DB parity failure實際問題是 Google Drive 待匯入資料夾沒有新來源檔,且重啟後 token file ownership 讓 scheduler 一度無法刷新 token。
2026-06-24 的第三段變更是把「MOMO 服務活著但資料不新」納入 cold-start hard gate。2026-06-25 11:44 曾證明 MOMO 服務、public route、DB parity、scheduler activity、backup/offsite 都可用,但 Google Drive token artifact metadata missing 且資料停在 `2026-06-17`,所以 cold-start 正確 BLOCKED。2026-06-25 14:16 的最新狀態已由合法匯入 job `57` 解除該資料新鮮度 blockerMOMO service health 是 `V10.674``daily_sales_snapshot``realtime_sales_monthly` 皆到 `2026-06-24``MOMO_DAILY_FRESHNESS 1|2026-06-24`dedicated preflight `PASS=18 WARN=3 BLOCKED=0`。這仍不代表 DR complete也不代表可以讀取或保存 Google Drive token 內容
| 項目 | 2026-06-24 MOMO freshness baseline |
| 項目 | 2026-06-25 MOMO freshness / token baseline |
|------|------------------------------------|
| SOP version | `v1.29` |
| Token root cause | Docker user namespace 下 `momo-scheduler` host UID/GID 為 `100000:100000``google_token.json` 原本是 `1000:1000 600`Google client 需要寫回 token 時 permission denied |
| Token live repair | `google_token.json` 修為 `100000:100000 600`;只改 owner/mode不讀取、不輸出、不保存 token value |
| Drive pending folder | `當日業績匯入`pattern `即時業績_當日`,目前 matching Excel count `0` |
| Drive archive folder | `當日業績匯入/已匯入`,最新 matching file modifiedTime `2026-06-18T01:30:39Z`,已由 import job `56` 匯入 |
| DB parity | `MOMO_MONTHLY_SYNC 10936|10936|2026-06-01|2026-06-17|2026-06-01|2026-06-17` |
| Data freshness | `MOMO_DAILY_FRESHNESS 7|2026-06-17` as of 2026-06-24 11:35 |
| Live cold-start readback | `PASS=86 WARN=0 BLOCKED=1`, result `BLOCKED` |
| SOP version | `v1.51` |
| Token current state | `MOMO_GDRIVE_TOKEN_STAT 100000:100000:600 scheduler_uid=100000`; dedicated preflight also saw host token metadata aligned to scheduler UID and container-side token artifact mode `600`; token content was not read |
| Token recovery boundary | Owner-gated maintenance only不得讀取、貼上、保存 token value / hash / partial不得把聊天密碼或 workaround 寫進 repo |
| Drive auth behavior | 2026-06-25 10:04 fail-closed evidence remains historical proof that auth failure does not become a fake success. 14:16 readback shows the later legitimate import succeeded and the blocker is cleared. |
| Drive pending folder | `當日業績匯入`pattern `即時業績_當日`; latest successful source recorded by job `57` |
| Latest valid import | Job `57 completed``即時業績_當日.xlsx``2026-06-25T13:16:47.359958..2026-06-25T13:18:02.964985``15383/15383/0` |
| DB parity | `daily_sales_snapshot=109061|2025-07-01|2026-06-24`; cold-start `MOMO_MONTHLY_SYNC 15383|15383|2026-06-01|2026-06-24|2026-06-01|2026-06-24` |
| Data freshness | `MOMO_DAILY_FRESHNESS 1|2026-06-24` as of 2026-06-25 14:16 |
| Live cold-start readback | `PASS=89 WARN=0 BLOCKED=0`, result `GREEN`; dedicated MOMO preflight `PASS=18 WARN=3 BLOCKED=0` |
| 110 live script sync | `/home/wooo/scripts/full-stack-cold-start-check.sh` hash `10608873d406911a519afa96218abebc2b85ab6123bdf46b6e21eb269e554bb8` |
| Alert dedupe | `data_stale_alert` for `upstream_drive` has 24h dedupe; latest evidence was 2026-06-23 with last_date `2026-06-17` |
| Declaration limit | 可宣稱 hosts/routes/K3s/backups recovered不可宣稱 MOMO data current、full-stack green 或 DR complete |
| Alert behavior | Drive auth failure must send failure notification; heartbeat success remains suppressed; stale data alert should clear only with fresh DB evidence like job `57` / freshness `1` |
| Declaration limit | 可宣稱 hosts/routes/K3s/backups/MOMO service/MOMO data freshness recovered for this evidence set不可宣稱 DR complete、credential escrow complete、Wazuh host registry accepted 或 runtime/security acceptance |
MOMO post-reboot 最小 readback
```bash
scripts/reboot-recovery/momo-drive-token-source-recovery-preflight.sh
ssh ollama@192.168.0.188 '
stat -c "%u:%g:%a %n" /home/ollama/momo-pro/config/google_token.json
stat -c "%u:%g:%a %n" /home/ollama/momo-pro/config/google_token.json 2>/dev/null || echo "google_token.json missing"
docker top momo-scheduler -eo pid,user,uid,gid,args | head -n 3
docker logs --since 2h momo-scheduler 2>&1 | grep -E "AutoImport|Google Drive|Permission denied|沒有找到|發現檔案" | tail -80
docker logs --since 2h momo-scheduler 2>&1 | grep -E "AutoImport|Google Drive|Permission denied|could not locate runnable browser|沒有找到|發現檔案|匯入失敗通知" | tail -120
'
ssh ollama@192.168.0.188 'db_user=$(docker exec momo-pro-system printenv POSTGRES_USER); db_name=$(docker exec momo-pro-system printenv POSTGRES_DB); db_pass=$(docker exec momo-pro-system printenv POSTGRES_PASSWORD); docker exec -i -e PGPASSWORD="$db_pass" momo-db psql -h 127.0.0.1 -U "$db_user" -d "$db_name" -At' <<'SQL'
@@ -1852,7 +1859,16 @@ SELECT 'daily_freshness|' || (CURRENT_DATE - max(snapshot_date)::date) || '|' ||
SQL
```
若 Drive pending folder 無新來源檔,不可手動 truncate、不可以舊 archive 檔重複匯入來製造「最新」,也不可把 DB parity 當 data freshness。下一個解除 blocker 的證據必須是:新的 `即時業績_當日` source file 可見、import job 成功、`sync_success=true``daily_sales_snapshot``realtime_sales_monthly` 日期上下界一致,且 `MOMO_DAILY_FRESHNESS <= 2`
Preferred path is the scripted preflight. It is read-only and returns `0` for clean, `1` for WARN-only, and `2` for BLOCKED. 2026-06-25 14:16 live run returned `PASS=18 WARN=3 BLOCKED=0`: `https://mo.wooo.work/health` and local health both returned `200`, health version was `V10.674`, app / scheduler / Telegram bot were healthy, scheduler restart count was `0`, token metadata aligned to scheduler UID without reading token content, current-month DB parity matched, latest daily import job `57` was clean, and `DB_DAILY_FRESHNESS 1|2026-06-24` cleared the MOMO hard blocker. The remaining WARNs are stability / future-evidence notes, not blockers.
若 Drive token artifact missing 或 Drive pending folder 無新來源檔,不可手動 truncate、不可以舊 archive 檔重複匯入來製造「最新」,也不可把 DB parity 當 data freshness。下一個解除 blocker 的證據必須是:
1. Owner 提供非 secret evidence ref確認可以恢復 Google Drive token artifact 或合法來源檔。
2. 維護窗口、rollback owner、post-check owner 明確記錄。
3. token artifact 只用 metadata 驗證owner 對齊 scheduler UID、mode 不寬於 `600`、不輸出 token 內容。
4. 新的 `即時業績_當日` source file 可見,或 scheduler 能成功列出待匯入來源。
5. import job 成功,`sync_success=true`,且 Drive 檔案只在成功後移動。
6. `daily_sales_snapshot``realtime_sales_monthly` 日期上下界一致,且 `MOMO_DAILY_FRESHNESS <= 2`
### 14.29 2026-06-24 188 MinIO / Velero、DB exporter 與 110 disk pressure recovery
@@ -2123,6 +2139,61 @@ NO-GO: send a generic success notification for file_count > 0 before verify_impo
3. Data correctness: import job completed with sync_success=true, and daily_sales_snapshot / realtime_sales_monthly match the imported date range.
```
### 14.35 2026-06-25 MOMO preflight 與 110 CPU orphan Chrome 分流
2026-06-25 11:01 的第九段變更是把兩個常見誤判收斂成可重跑 SOP
1. MOMO service health green 不等於 data fresh。
2. 110 high load 不等於可以重啟 Docker 或取消 CI。
MOMO 專用 preflight
```bash
scripts/reboot-recovery/momo-drive-token-source-recovery-preflight.sh
```
此腳本只做 read-only SSH / Docker metadata / logs / DB query不讀 token 內容、不 import、不移動 Drive 檔、不 restart。14:16 live result:
```text
MOMO_DRIVE_TOKEN_SOURCE_PREFLIGHT PASS=18 WARN=3 BLOCKED=0 HOST=ollama@192.168.0.188 FRESHNESS_MAX_DAYS=2
MOMO_PUBLIC_HEALTH_CODE 200
MOMO_HEALTH_CODE 200
MOMO_HEALTH_VERSION V10.674
MOMO_APP_HEALTH healthy
SCHEDULER_RUNNING true
SCHEDULER_HEALTH healthy
SCHEDULER_RESTART_COUNT 0
TELEGRAM_BOT_HEALTH healthy
MOMO_CONTAINER_REPLACE_EVENTS_45M 11
TOKEN_STAT 100000:100000:600
CONTAINER_TOKEN_STAT 0:0:600
LOCAL_EXACT_DAILY_SOURCE_COUNT 0
LOCAL_EXACT_DAILY_SOURCE_LATEST none
DB_DAILY 109061|2025-07-01|2026-06-24
DB_MONTHLY_SYNC 15383|15383|2026-06-01|2026-06-24|2026-06-01|2026-06-24
DB_DAILY_FRESHNESS 1|2026-06-24
DB_LATEST_DAILY_IMPORT_JOB 57|completed|即時業績_當日.xlsx|2026-06-25T13:16:47.359958|2026-06-25T13:18:02.964985|15383|15383|0
```
110 CPU 分流:
| Evidence | Decision |
|----------|----------|
| `ps` shows `stockplatform-review-bulk-ux` Chrome groups with root process PPID `1`, no parent node smoke, and sustained high CPU | Treat as orphan browser smoke. Run dry-run if available, then only with owner approval use targeted `SIGTERM` by process group. |
| Active Gitea Actions container is consuming CPU, e.g. `GITEA-ACTIONS-TASK-*`, `next build`, `uv pip install`, `docker-buildx` | Treat as legitimate CI/CD load. Do not kill unless there is explicit release owner approval to cancel the run. |
| `vmstat` shows high iowait or active swap in/out | Treat as storage / memory pressure, not browser runaway. Do not kill random processes; capture disk / memory evidence first. |
2026-06-25 10:58 user-approved action:
```text
Targeted command type: process SIGTERM only.
Targeted process groups: 438005, 471295, 640155, 670628.
Scope: orphan `stockplatform-review-bulk-ux` Chrome groups on 110.
Post-check: `OLD_GROUPS_REMAINING` empty.
Not performed: Docker restart, systemd restart, Nginx reload, firewall/iptables change, K8s action, CI cancellation, Wazuh/SOC change, secret read.
Remaining load: active Gitea Actions / CI build work; observe queue and timeout instead of killing.
```
### 14.22 重啟後時間軸驗證
每次重啟後照時間軸推進,不要等到最後才一次判定。

View File

@@ -0,0 +1,197 @@
# 主機重啟後一頁式總檢查
> Version: v1.1
> Last updated: 2026-06-25 Asia/Taipei
> Scope: 110 / 120 / 121 / 188 post-reboot service recovery. 112 Kali / Wazuh / active scan 不屬於本流程。
---
## 1. 使用時機
每次 110 / 120 / 121 / 188 任一台主機開機、關機、重啟、斷電恢復、VMware console fsck、Docker / K3s 大量重排後,都先跑本頁,再決定是否宣稱恢復。
本頁只回答四件事:
1. 主機是否開起來。
2. 服務是否真的可用。
3. 資料與備份是否新鮮。
4. 有哪些不能宣稱完成。
---
## 2. 絕對判定規則
| 層級 | 可以宣稱 | 必要證據 |
|------|----------|----------|
| `HOST_BOOTED` | 主機已開機 | ping / SSH port 回應,或 console login prompt。 |
| `HOST_READY` | 主機可管理 | SSH read-only 可登入failed units / disk / clock / network 無硬阻塞。 |
| `SERVICE_READY` | 單站服務可用 | route / container / local health / DB 或依賴健康都通過。 |
| `FULL_STACK_GREEN` | 本輪重啟服務恢復完成 | cold-start `WARN=0``BLOCKED=0`route、K3s、DB freshness、backup、alert、CronJob、exporter 都通過。 |
| `DR_COMPLETE` | 災難復原也完成 | `FULL_STACK_GREEN` 加上 credential escrow missing `0`、offsite / restore / escrow evidence 完整。 |
禁止用單一訊號取代整體判定:
- 網站 `200` 不等於資料最新。
- container `healthy` 不等於 DB / backup / alert 正常。
- K3s node `Ready` 不等於 workload 分散與 CronJob freshness 正常。
- Wazuh route `200` 不等於所有主機 agent registry accepted。
- backup fresh 不等於 DR completecredential escrow 缺口必須獨立保留。
---
## 3. 10 分鐘只讀總檢查順序
優先使用 repo-side wrapper
```bash
scripts/reboot-recovery/post-start-quick-check.sh --no-color
```
此 wrapper 只做 read-only 檢查,並委派既有 cold-start / MOMO preflight / backup-status不 restart、不 reload、不 import、不改 K8s、不讀 token 內容。wrapper 會把 warning 分成 `SERVICE``BOUNDARY``EVIDENCE` 三類,避免把 `escrow_missing>0` 誤判成服務降級。若 wrapper 因某個 SSH 權限或路徑失敗,再依下列分段命令手動補證據。
### Step 1 - 主機與 SSH
```bash
for host in 192.168.0.110 192.168.0.120 192.168.0.121 192.168.0.188; do
ping -c 1 -W 1 "$host" >/dev/null && echo "PING_OK $host" || echo "PING_FAIL $host"
nc -z -w 2 "$host" 22 && echo "SSH_PORT_OK $host" || echo "SSH_PORT_FAIL $host"
done
```
若任一 P0 host 失敗,不要跳去修 Nginx。先判斷是 power / NIC / fsck / SSH trust / host boot 問題。
### Step 2 - 全棧 cold-start scorecard
```bash
scripts/reboot-recovery/full-stack-cold-start-check.sh --monitor-read-only --no-color --watch --interval 1 --max-attempts 1
```
判定:
- `PASS>0 WARN=0 BLOCKED=0`:可進入 `FULL_STACK_GREEN` 候選。
- `WARN>0 BLOCKED=0`:只能宣稱 `SERVICE_AVAILABLE_DEGRADED`,必須列 WARN。
- `BLOCKED>0`:不可宣稱恢復完成,先處理第一個 blocker。
### Step 3 - MOMO 專用 freshness gate
```bash
scripts/reboot-recovery/momo-drive-token-source-recovery-preflight.sh
```
必要欄位:
- `MOMO_HEALTH_VERSION`
- `SCHEDULER_HEALTH`
- `TOKEN_STAT` / `CONTAINER_TOKEN_STAT` 只看 metadata不讀 token。
- `DB_MONTHLY_SYNC`
- `DB_DAILY_FRESHNESS`
- `DB_LATEST_DAILY_IMPORT_JOB`
`DB_DAILY_FRESHNESS > 2` 或 import job 失敗時,不可宣稱 MOMO 資料已恢復。
### Step 4 - Backup / offsite / escrow
在 110 只讀執行:
```bash
/backup/scripts/backup-status.sh --no-notify --no-refresh
```
必要欄位:
- 110 backup fresh / failed count。
- 188 backup fresh / failed count。
- `core_blockers=0`
- `integrity_stale=0`
- `offsite_fresh=1`
- `rclone_gdrive_fresh=1`
- `escrow_missing` 必須照實回報。
`escrow_missing>0` 時,服務可 green但 DR 不可 green。
### Step 5 - Public routes 只作輔助證據
```bash
for url in \
https://awoooi.wooo.work/api/v1/health \
https://awoooi.wooo.work/zh-TW/iwooos \
https://mo.wooo.work/health \
https://stock.wooo.work/; do
code="$(curl -k -sS -o /dev/null -w '%{http_code}' "$url" || true)"
echo "$code $url"
done
```
Route smoke 必須和 cold-start / DB / backup 一起看;不能單獨當恢復證明。
### Step 6 - 110 CPU / runaway process
```bash
ssh wooo@192.168.0.110 'uptime; vmstat 1 5; ps -eo pid,ppid,pgid,stat,pcpu,pmem,comm,args --sort=-pcpu | head -25'
```
分類:
- orphan Chrome / headless smoke走 runaway process PlayBook未批准不得 kill。
- Gitea Actions / CI build / test先標註短期 CI load不當事故處理。
- Docker / DB / Harbor / Sentry 持續高載:回到服務相依與 exporter readback。
---
## 4. 放行與阻擋口徑
| 結果 | 口徑 |
|------|------|
| `FULL_STACK_GREEN_DR_ESCROW_BLOCKED` | 可宣稱所有服務面恢復;不可宣稱 DR complete。 |
| `SERVICE_AVAILABLE_DEGRADED` | 可宣稱服務可用;必須列 WARN 與下一步。 |
| `BLOCKED_MOMO_DATA_FRESHNESS` | 可宣稱網站可用;不可宣稱資料最新。 |
| `BLOCKED_HOST_OR_K3S` | 不可宣稱全棧恢復;先修主機 / K3s。 |
| `BLOCKED_BACKUP_CORE` | 不可宣稱恢復完成;備份紅燈優先。 |
| `BLOCKED_WAZUH_REGISTRY` | 不屬於本 SOP 的服務恢復 blocker必須交給 IwoooS / Wazuh lane不可改 Wazuh runtime。 |
| `GREEN_WITH_EVIDENCE_WARNINGS` | 服務可宣稱恢復,但仍有非服務面證據提醒,必須列入 LOGBOOK。 |
Wrapper exit code
- `0`:沒有 service blocker。可能仍有 DR boundary / evidence warning。
- `1`:有 service warning只能宣稱 degraded。
- `2`:有 service blocker不可宣稱恢復完成。
---
## 5. 完成後必填 LOGBOOK 摘要
```text
時間:
命令類型read-only / docs-only / write-with-approval
主機110 / 120 / 121 / 188
Cold-startPASS=? WARN=? BLOCKED=? RESULT=?
MOMOversion=? daily_freshness=? latest_job=?
Backup110=? 188=? core_blockers=? offsite=? escrow_missing=?
Routes列出主要 route code
CPU / runawayorphan=? active_ci=? load=?
仍 blocked
不可宣稱:
```
---
## 6. 目前最新已驗證基線
2026-06-25 14:41 wrapper live run
- Wrapper`POST_START_QUICK_CHECK PASS=18 WARN=2 BLOCKED=0`
- Warning split`SERVICE=0 BOUNDARY=1 EVIDENCE=1`
- Result`FULL_STACK_GREEN_DR_ESCROW_BLOCKED`exit code `0`
- Cold-start`PASS=89 WARN=0 BLOCKED=0`Result `GREEN`
- MOMO`V10.676`dedicated preflight `PASS=19 WARN=2 BLOCKED=0`job `57` clean`DB_DAILY_FRESHNESS 1|2026-06-24`
- Backup110 `13/13 fresh failed=0`188 `2/2 fresh failed=0``core_blockers=0`
- DR`escrow_missing=5`,不可宣稱 DR complete。
- CPU110 顯示 active CI / build / test load沒有 orphan Chrome 復發證據。
2026-06-25 14:16
- Cold-start`PASS=89 WARN=0 BLOCKED=0`Result `GREEN`
- MOMO`V10.674`job `57` clean`DB_DAILY_FRESHNESS 1|2026-06-24`
- Backup110 `13/13 fresh failed=0`188 `2/2 fresh failed=0``core_blockers=0`
- DR`escrow_missing=5`,不可宣稱 DR complete。
- Wazuhhost registry accepted 仍不屬於本 SOP 完成項,不可宣稱全部主機納管完成。

View File

@@ -0,0 +1,160 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "github_target_private_backup_evidence_gate_v1.schema.json",
"title": "GitHub Target Private Backup Evidence Gate",
"type": "object",
"required": [
"schema_version",
"generated_at",
"status",
"mode",
"source_reviews",
"summary",
"targets",
"acceptance_requirements",
"rejection_rules",
"operation_boundaries",
"authorization_flags"
],
"properties": {
"schema_version": {
"const": "github_target_private_backup_evidence_gate_v1"
},
"generated_at": {
"type": "string"
},
"status": {
"type": "string",
"pattern": "^blocked_"
},
"mode": {
"const": "read_only_private_backup_evidence_gate"
},
"source_reviews": {
"type": "object"
},
"summary": {
"type": "object",
"required": [
"approval_required_target_count",
"public_probe_visible_target_count",
"not_found_or_private_target_count",
"private_backup_verified_count",
"safe_credential_required_count",
"safe_credential_accepted_evidence_count",
"execution_ready_count",
"repo_creation_authorized",
"visibility_change_authorized",
"refs_sync_authorized",
"github_primary_switch_authorized",
"workflow_modification_authorized",
"workflow_trigger_authorized",
"secret_value_collection_allowed",
"private_clone_url_collection_allowed",
"not_found_or_private_as_absent_allowed",
"public_repo_allowed"
],
"properties": {
"repo_creation_authorized": { "const": false },
"visibility_change_authorized": { "const": false },
"refs_sync_authorized": { "const": false },
"github_primary_switch_authorized": { "const": false },
"workflow_modification_authorized": { "const": false },
"workflow_trigger_authorized": { "const": false },
"secret_value_collection_allowed": { "const": false },
"private_clone_url_collection_allowed": { "const": false },
"not_found_or_private_as_absent_allowed": { "const": false },
"public_repo_allowed": { "const": false }
}
},
"targets": {
"type": "array",
"items": {
"type": "object",
"required": [
"github_repo",
"approval_required",
"probe_status",
"visibility_evidence_status",
"private_backup_verified",
"owner_response_accepted",
"refs_sync_ready",
"execution_ready",
"blockers",
"repo_creation_authorized",
"visibility_change_authorized",
"refs_sync_authorized",
"github_primary_switch_authorized",
"secret_values_collected"
],
"properties": {
"private_backup_verified": { "const": false },
"owner_response_accepted": { "const": false },
"refs_sync_ready": { "const": false },
"execution_ready": { "const": false },
"repo_creation_authorized": { "const": false },
"visibility_change_authorized": { "const": false },
"refs_sync_authorized": { "const": false },
"github_primary_switch_authorized": { "const": false },
"secret_values_collected": { "const": false },
"blockers": {
"type": "array",
"minItems": 1
}
}
}
},
"acceptance_requirements": {
"type": "array",
"minItems": 1,
"items": { "type": "string" }
},
"rejection_rules": {
"type": "array",
"minItems": 1,
"items": { "type": "string" }
},
"operation_boundaries": {
"type": "object",
"required": [
"read_only_api_allowed",
"github_api_write_allowed",
"gitea_api_write_allowed",
"repo_creation_allowed",
"visibility_change_allowed",
"refs_sync_allowed",
"workflow_modification_allowed",
"workflow_trigger_allowed",
"github_primary_switch_allowed",
"secret_value_collection_allowed",
"private_clone_url_collection_allowed"
],
"properties": {
"read_only_api_allowed": { "const": true },
"github_api_write_allowed": { "const": false },
"gitea_api_write_allowed": { "const": false },
"repo_creation_allowed": { "const": false },
"visibility_change_allowed": { "const": false },
"refs_sync_allowed": { "const": false },
"workflow_modification_allowed": { "const": false },
"workflow_trigger_allowed": { "const": false },
"github_primary_switch_allowed": { "const": false },
"secret_value_collection_allowed": { "const": false },
"private_clone_url_collection_allowed": { "const": false }
}
},
"authorization_flags": {
"type": "object",
"properties": {
"runtime_execution_authorized": { "const": false },
"repo_creation_authorized": { "const": false },
"visibility_change_authorized": { "const": false },
"refs_sync_authorized": { "const": false },
"workflow_modification_authorized": { "const": false },
"workflow_trigger_authorized": { "const": false },
"github_primary_switch_authorized": { "const": false },
"secret_values_collected": { "const": false }
}
}
}
}

View File

@@ -0,0 +1,32 @@
# GitHub Target Private Backup Evidence Gate
| 項目 | 值 |
|------|----|
| 狀態 | `blocked_public_visibility_and_safe_credential_evidence_required` |
| approval-required targets | `9` |
| public probe visible | `4` |
| not_found_or_private | `5` |
| private backup verified | `0` |
| safe credential evidence | `0/9` |
| execution ready | `0` |
## Target Gate
| GitHub target | probe | visibility evidence | private verified | blockers |
|---------------|-------|---------------------|------------------|----------|
| `owenhytsai/awoooi` | `exists` | `blocked_public_probe_visible_private_evidence_required` | `false` | `4` |
| `owenhytsai/clawbot-v5` | `exists` | `blocked_public_probe_visible_private_evidence_required` | `false` | `4` |
| `owenhytsai/wooo-aiops` | `exists` | `blocked_public_probe_visible_private_evidence_required` | `false` | `4` |
| `owenhytsai/wooo-infra-config` | `exists` | `blocked_public_probe_visible_private_evidence_required` | `false` | `4` |
| `owenhytsai/ewoooc` | `not_found_or_private` | `blocked_private_or_absent_not_verified` | `false` | `4` |
| `owenhytsai/bitan-pharmacy` | `not_found_or_private` | `blocked_private_or_absent_not_verified` | `false` | `4` |
| `owenhytsai/tsenyang-website` | `not_found_or_private` | `blocked_private_or_absent_not_verified` | `false` | `4` |
| `owenhytsai/VibeWork` | `not_found_or_private` | `blocked_private_or_absent_not_verified` | `false` | `4` |
| `owenhytsai/agent-bounty-protocol` | `not_found_or_private` | `blocked_private_or_absent_not_verified` | `false` | `4` |
## 不可誤讀
- 本 gate 不是 GitHub repo creation / visibility change / refs sync 授權。
- 公開 probe 可讀的 target 需要 private visibility owner evidence不能標綠。
- `not_found_or_private` 不能當成已 private也不能當成 repo 不存在。
- safe credential evidence 只收 metadata不收 secret value。

View File

@@ -0,0 +1,533 @@
{
"schema_version": "github_target_private_backup_evidence_gate_v1",
"generated_at": "2026-06-26T09:43:15.948000+00:00",
"status": "blocked_public_visibility_and_safe_credential_evidence_required",
"mode": "read_only_private_backup_evidence_gate",
"source_reviews": {
"github_target_decision": "docs/security/github-target-decision.snapshot.json",
"github_target_owner_decision_response": "docs/security/github-target-owner-decision-response.snapshot.json",
"github_target_repo_approval_package": "docs/security/github-target-repo-approval-package.snapshot.json"
},
"summary": {
"target_decision_count": 10,
"approval_required_target_count": 9,
"approval_package_item_count": 9,
"public_probe_visible_target_count": 4,
"not_found_or_private_target_count": 5,
"private_backup_verified_count": 0,
"private_visibility_evidence_missing_count": 9,
"safe_credential_required_count": 9,
"safe_credential_accepted_evidence_count": 0,
"owner_response_received_count": 0,
"owner_response_accepted_count": 0,
"execution_ready_count": 0,
"blocked_target_count": 9,
"external_scope_target_count": 1,
"forbidden_action_count": 12,
"repo_creation_authorized": false,
"visibility_change_authorized": false,
"refs_sync_authorized": false,
"github_primary_switch_authorized": false,
"workflow_modification_authorized": false,
"workflow_trigger_authorized": false,
"secret_value_collection_allowed": false,
"private_clone_url_collection_allowed": false,
"not_found_or_private_as_absent_allowed": false,
"public_repo_allowed": false
},
"targets": [
{
"github_repo": "owenhytsai/awoooi",
"source_key": "wooo/awoooi",
"approval_required": true,
"probe_status": "exists",
"target_state": "exists_refs_blocked",
"risk": "HIGH",
"visibility_evidence_status": "blocked_public_probe_visible_private_evidence_required",
"private_backup_verified": false,
"private_visibility_owner_evidence_ref": null,
"safe_credential_evidence_status": "missing_safe_credential_metadata",
"safe_credential_evidence_ref": null,
"owner_response_accepted": false,
"refs_sync_ready": false,
"execution_ready": false,
"blockers": [
"github_target_publicly_readable_by_unauthenticated_probe",
"private_visibility_owner_evidence_missing",
"safe_credential_metadata_missing",
"refs_sync_not_authorized"
],
"evidence_refs": [
"docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md",
"docs/security/github-target-probe.snapshot.json",
"docs/security/github-target-owner-decision-response.snapshot.json"
],
"forbidden_actions": [
"create_github_repo",
"change_repo_visibility",
"push_refs",
"delete_refs",
"force_push",
"mirror_sync",
"switch_github_primary",
"disable_gitea",
"workflow_modification",
"workflow_trigger",
"secret_value_collection",
"private_clone_url_collection"
],
"repo_creation_authorized": false,
"visibility_change_authorized": false,
"refs_sync_authorized": false,
"github_primary_switch_authorized": false,
"secret_values_collected": false
},
{
"github_repo": "owenhytsai/clawbot-v5",
"source_key": "wooo/clawbot-v5",
"approval_required": true,
"probe_status": "exists",
"target_state": "exists_refs_blocked",
"risk": "MEDIUM",
"visibility_evidence_status": "blocked_public_probe_visible_private_evidence_required",
"private_backup_verified": false,
"private_visibility_owner_evidence_ref": null,
"safe_credential_evidence_status": "missing_safe_credential_metadata",
"safe_credential_evidence_ref": null,
"owner_response_accepted": false,
"refs_sync_ready": false,
"execution_ready": false,
"blockers": [
"github_target_publicly_readable_by_unauthenticated_probe",
"private_visibility_owner_evidence_missing",
"safe_credential_metadata_missing",
"refs_sync_not_authorized"
],
"evidence_refs": [
"docs/security/SOURCE-CONTROL-CLAWBOT-V5-SNAPSHOT.md",
"docs/security/github-target-probe.snapshot.json",
"docs/security/github-target-owner-decision-response.snapshot.json"
],
"forbidden_actions": [
"create_github_repo",
"change_repo_visibility",
"push_refs",
"delete_refs",
"force_push",
"mirror_sync",
"switch_github_primary",
"disable_gitea",
"workflow_modification",
"workflow_trigger",
"secret_value_collection",
"private_clone_url_collection"
],
"repo_creation_authorized": false,
"visibility_change_authorized": false,
"refs_sync_authorized": false,
"github_primary_switch_authorized": false,
"secret_values_collected": false
},
{
"github_repo": "owenhytsai/wooo-aiops",
"source_key": "wooo/wooo-aiops",
"approval_required": true,
"probe_status": "exists",
"target_state": "exists_refs_blocked",
"risk": "MEDIUM",
"visibility_evidence_status": "blocked_public_probe_visible_private_evidence_required",
"private_backup_verified": false,
"private_visibility_owner_evidence_ref": null,
"safe_credential_evidence_status": "missing_safe_credential_metadata",
"safe_credential_evidence_ref": null,
"owner_response_accepted": false,
"refs_sync_ready": false,
"execution_ready": false,
"blockers": [
"github_target_publicly_readable_by_unauthenticated_probe",
"private_visibility_owner_evidence_missing",
"safe_credential_metadata_missing",
"refs_sync_not_authorized"
],
"evidence_refs": [
"docs/security/SOURCE-CONTROL-WOOO-AIOPS-SNAPSHOT.md",
"docs/security/github-target-probe.snapshot.json",
"docs/security/github-target-owner-decision-response.snapshot.json"
],
"forbidden_actions": [
"create_github_repo",
"change_repo_visibility",
"push_refs",
"delete_refs",
"force_push",
"mirror_sync",
"switch_github_primary",
"disable_gitea",
"workflow_modification",
"workflow_trigger",
"secret_value_collection",
"private_clone_url_collection"
],
"repo_creation_authorized": false,
"visibility_change_authorized": false,
"refs_sync_authorized": false,
"github_primary_switch_authorized": false,
"secret_values_collected": false
},
{
"github_repo": "owenhytsai/wooo-infra-config",
"source_key": "wooo/wooo-infra-config",
"approval_required": true,
"probe_status": "exists",
"target_state": "exists_aligned",
"risk": "MEDIUM",
"visibility_evidence_status": "blocked_public_probe_visible_private_evidence_required",
"private_backup_verified": false,
"private_visibility_owner_evidence_ref": null,
"safe_credential_evidence_status": "missing_safe_credential_metadata",
"safe_credential_evidence_ref": null,
"owner_response_accepted": false,
"refs_sync_ready": false,
"execution_ready": false,
"blockers": [
"github_target_publicly_readable_by_unauthenticated_probe",
"private_visibility_owner_evidence_missing",
"safe_credential_metadata_missing",
"refs_sync_not_authorized"
],
"evidence_refs": [
"docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md",
"docs/security/github-target-probe.snapshot.json",
"docs/security/github-target-owner-decision-response.snapshot.json"
],
"forbidden_actions": [
"create_github_repo",
"change_repo_visibility",
"push_refs",
"delete_refs",
"force_push",
"mirror_sync",
"switch_github_primary",
"disable_gitea",
"workflow_modification",
"workflow_trigger",
"secret_value_collection",
"private_clone_url_collection"
],
"repo_creation_authorized": false,
"visibility_change_authorized": false,
"refs_sync_authorized": false,
"github_primary_switch_authorized": false,
"secret_values_collected": false
},
{
"github_repo": "owenhytsai/ewoooc",
"source_key": "wooo/ewoooc / root/momo-pro-system / momo working trees",
"approval_required": true,
"probe_status": "not_found_or_private",
"target_state": "not_found_or_private",
"risk": "HIGH",
"visibility_evidence_status": "blocked_private_or_absent_not_verified",
"private_backup_verified": false,
"private_visibility_owner_evidence_ref": null,
"safe_credential_evidence_status": "missing_safe_credential_metadata",
"safe_credential_evidence_ref": null,
"owner_response_accepted": false,
"refs_sync_ready": false,
"execution_ready": false,
"blockers": [
"not_found_or_private_is_not_private_verification",
"private_visibility_owner_evidence_missing",
"safe_credential_metadata_missing",
"refs_sync_not_authorized"
],
"evidence_refs": [
"docs/security/GITEA-PUBLIC-REPO-SEARCH-SNAPSHOT.md",
"docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md",
"docs/security/github-target-probe.snapshot.json",
"docs/security/github-target-owner-decision-response.snapshot.json"
],
"forbidden_actions": [
"create_github_repo",
"change_repo_visibility",
"push_refs",
"delete_refs",
"force_push",
"mirror_sync",
"switch_github_primary",
"disable_gitea",
"workflow_modification",
"workflow_trigger",
"secret_value_collection",
"private_clone_url_collection"
],
"repo_creation_authorized": false,
"visibility_change_authorized": false,
"refs_sync_authorized": false,
"github_primary_switch_authorized": false,
"secret_values_collected": false
},
{
"github_repo": "owenhytsai/bitan-pharmacy",
"source_key": "bitan-pharmacy",
"approval_required": true,
"probe_status": "not_found_or_private",
"target_state": "not_found_or_private",
"risk": "MEDIUM",
"visibility_evidence_status": "blocked_private_or_absent_not_verified",
"private_backup_verified": false,
"private_visibility_owner_evidence_ref": null,
"safe_credential_evidence_status": "missing_safe_credential_metadata",
"safe_credential_evidence_ref": null,
"owner_response_accepted": false,
"refs_sync_ready": false,
"execution_ready": false,
"blockers": [
"not_found_or_private_is_not_private_verification",
"private_visibility_owner_evidence_missing",
"safe_credential_metadata_missing",
"refs_sync_not_authorized"
],
"evidence_refs": [
"docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md",
"docs/security/github-target-probe.snapshot.json",
"docs/security/github-target-owner-decision-response.snapshot.json"
],
"forbidden_actions": [
"create_github_repo",
"change_repo_visibility",
"push_refs",
"delete_refs",
"force_push",
"mirror_sync",
"switch_github_primary",
"disable_gitea",
"workflow_modification",
"workflow_trigger",
"secret_value_collection",
"private_clone_url_collection"
],
"repo_creation_authorized": false,
"visibility_change_authorized": false,
"refs_sync_authorized": false,
"github_primary_switch_authorized": false,
"secret_values_collected": false
},
{
"github_repo": "owenhytsai/tsenyang-website",
"source_key": "tsenyang-website",
"approval_required": true,
"probe_status": "not_found_or_private",
"target_state": "not_found_or_private",
"risk": "MEDIUM",
"visibility_evidence_status": "blocked_private_or_absent_not_verified",
"private_backup_verified": false,
"private_visibility_owner_evidence_ref": null,
"safe_credential_evidence_status": "missing_safe_credential_metadata",
"safe_credential_evidence_ref": null,
"owner_response_accepted": false,
"refs_sync_ready": false,
"execution_ready": false,
"blockers": [
"not_found_or_private_is_not_private_verification",
"private_visibility_owner_evidence_missing",
"safe_credential_metadata_missing",
"refs_sync_not_authorized"
],
"evidence_refs": [
"docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md",
"docs/security/github-target-probe.snapshot.json",
"docs/security/github-target-owner-decision-response.snapshot.json"
],
"forbidden_actions": [
"create_github_repo",
"change_repo_visibility",
"push_refs",
"delete_refs",
"force_push",
"mirror_sync",
"switch_github_primary",
"disable_gitea",
"workflow_modification",
"workflow_trigger",
"secret_value_collection",
"private_clone_url_collection"
],
"repo_creation_authorized": false,
"visibility_change_authorized": false,
"refs_sync_authorized": false,
"github_primary_switch_authorized": false,
"secret_values_collected": false
},
{
"github_repo": "nexu-io/open-design",
"source_key": "open-design",
"approval_required": false,
"probe_status": "exists",
"target_state": "external_scope",
"risk": "LOW",
"visibility_evidence_status": "external_scope_not_backup_target",
"private_backup_verified": false,
"private_visibility_owner_evidence_ref": null,
"safe_credential_evidence_status": "not_required_external_scope",
"safe_credential_evidence_ref": null,
"owner_response_accepted": false,
"refs_sync_ready": false,
"execution_ready": false,
"blockers": [
"external_scope_review_only"
],
"evidence_refs": [
"docs/security/github-target-probe.snapshot.json"
],
"forbidden_actions": [
"create_github_repo",
"change_repo_visibility",
"push_refs",
"delete_refs",
"force_push",
"mirror_sync",
"switch_github_primary",
"disable_gitea",
"workflow_modification",
"workflow_trigger",
"secret_value_collection",
"private_clone_url_collection"
],
"repo_creation_authorized": false,
"visibility_change_authorized": false,
"refs_sync_authorized": false,
"github_primary_switch_authorized": false,
"secret_values_collected": false
},
{
"github_repo": "owenhytsai/VibeWork",
"source_key": "vibework",
"approval_required": true,
"probe_status": "not_found_or_private",
"target_state": "not_found_or_private",
"risk": "HIGH",
"visibility_evidence_status": "blocked_private_or_absent_not_verified",
"private_backup_verified": false,
"private_visibility_owner_evidence_ref": null,
"safe_credential_evidence_status": "missing_safe_credential_metadata",
"safe_credential_evidence_ref": null,
"owner_response_accepted": false,
"refs_sync_ready": false,
"execution_ready": false,
"blockers": [
"not_found_or_private_is_not_private_verification",
"private_visibility_owner_evidence_missing",
"safe_credential_metadata_missing",
"refs_sync_not_authorized"
],
"evidence_refs": [
"docs/security/source-control-workflow-secret-name-local-evidence.snapshot.json",
"docs/security/source-control-primary-readiness-gate.snapshot.json",
"docs/security/github-target-owner-decision-response.snapshot.json"
],
"forbidden_actions": [
"create_github_repo",
"change_repo_visibility",
"push_refs",
"delete_refs",
"force_push",
"mirror_sync",
"switch_github_primary",
"disable_gitea",
"workflow_modification",
"workflow_trigger",
"secret_value_collection",
"private_clone_url_collection"
],
"repo_creation_authorized": false,
"visibility_change_authorized": false,
"refs_sync_authorized": false,
"github_primary_switch_authorized": false,
"secret_values_collected": false
},
{
"github_repo": "owenhytsai/agent-bounty-protocol",
"source_key": "agent-bounty-protocol",
"approval_required": true,
"probe_status": "not_found_or_private",
"target_state": "not_found_or_private",
"risk": "HIGH",
"visibility_evidence_status": "blocked_private_or_absent_not_verified",
"private_backup_verified": false,
"private_visibility_owner_evidence_ref": null,
"safe_credential_evidence_status": "missing_safe_credential_metadata",
"safe_credential_evidence_ref": null,
"owner_response_accepted": false,
"refs_sync_ready": false,
"execution_ready": false,
"blockers": [
"not_found_or_private_is_not_private_verification",
"private_visibility_owner_evidence_missing",
"safe_credential_metadata_missing",
"refs_sync_not_authorized"
],
"evidence_refs": [
"docs/security/source-control-workflow-secret-name-local-evidence.snapshot.json",
"docs/security/source-control-primary-readiness-gate.snapshot.json",
"docs/security/github-target-owner-decision-response.snapshot.json"
],
"forbidden_actions": [
"create_github_repo",
"change_repo_visibility",
"push_refs",
"delete_refs",
"force_push",
"mirror_sync",
"switch_github_primary",
"disable_gitea",
"workflow_modification",
"workflow_trigger",
"secret_value_collection",
"private_clone_url_collection"
],
"repo_creation_authorized": false,
"visibility_change_authorized": false,
"refs_sync_authorized": false,
"github_primary_switch_authorized": false,
"secret_values_collected": false
}
],
"acceptance_requirements": [
"每個 approval-required GitHub target 必須有 private visibility owner evidence ref。",
"公開 probe 可讀的 target 不得被視為符合私有備援要求。",
"`not_found_or_private` 只代表未授權只讀 probe 看不到,不得當成 private verified 或 repo absent。",
"safe credential evidence 只允許 credential storage / owner / scope / rotation metadata不得收 token value。",
"owner response accepted count 在 reviewer acceptance 前必須維持 0。",
"private evidence 與 safe credential evidence 完整前不得建立 repo、改 visibility、push refs 或切 GitHub primary。"
],
"rejection_rules": [
"任何 public repo 或 unauthenticated readable target 均不得標示 private_backup_verified=true。",
"任何 token、PAT、private key、cookie、session、private clone credential 或 partial secret 必須拒收。",
"任何 repo creation、visibility change、refs sync、force push、tag rewrite、workflow trigger 或 primary switch request 必須拒收。",
"任何把 `not_found_or_private` 解讀為 repo 不存在或可建立新 repo 的 response 必須拒收。"
],
"operation_boundaries": {
"read_only_api_allowed": true,
"github_api_write_allowed": false,
"gitea_api_write_allowed": false,
"repo_creation_allowed": false,
"visibility_change_allowed": false,
"refs_sync_allowed": false,
"workflow_modification_allowed": false,
"workflow_trigger_allowed": false,
"github_primary_switch_allowed": false,
"secret_value_collection_allowed": false,
"private_clone_url_collection_allowed": false
},
"authorization_flags": {
"runtime_execution_authorized": false,
"repo_creation_authorized": false,
"visibility_change_authorized": false,
"refs_sync_authorized": false,
"workflow_modification_authorized": false,
"workflow_trigger_authorized": false,
"github_primary_switch_authorized": false,
"secret_values_collected": false
}
}

View File

@@ -11,13 +11,15 @@
| Area | Status | Completion | Evidence |
|------|--------|------------|----------|
| Overall recovery readiness | SERVICE_AVAILABLE_MOMO_SOURCE_BLOCKED_DR_ESCROW_BLOCKED | 98% | 2026-06-24 23:33 live cold-start returned `PASS=88 WARN=0 BLOCKED=1`, result `BLOCKED` because MOMO business data freshness remains stale and no newer legitimate source file is present. 110 / 120 / 121 / 188 ping and SSH port are OK, K3s `mon` / `mon1` are Ready, public routes/TLS are green, 110 / 188 runtime and backup checks are green。188 `node-exporter`、PostgreSQL exporter、Redis exporter、`nginx-exporter`、MinIO / Velero BSL are restored; monitoring coverage is now `14/14 UP`; 110 disk pressure cleared。Remaining service blocker is MOMO business data freshness: `MOMO_DAILY_FRESHNESS 7|2026-06-17`; 23:33 cold-start plus scheduler / DB / import metadata read-only evidence confirms Drive listing works from the scheduler container, `import_config` points to `當日業績匯入` / `即時業績_當日`, but recent scheduler runs report `file_count=0` and no newer legitimate source file exists. 2026-06-24 22:17 confirms MOMO `main` and Gitea Actions `cd.yaml #904` deployed `84035906aba0`, so monthly sync failure now fails the import job and prevents Drive file movement in production. DR remains blocked because credential escrow evidence markers are still missing and must not be forged. |
| P0 host / K3s recovery | DONE | 100% | 120 booted after console fsck at `2026-06-12 15:13`; latest 2026-06-14 18:15 readback shows 120 is reachable, K3s is active, `mon` and `mon1` are both `Ready control-plane`, and cold-start P0/P1 checks are green. |
| P1 backup / alert / escrow | BLOCKED_DR_ESCROW | 97% | 2026-06-24 23:33 backup / alert readback shows 110 `13/13 fresh failed=0`, 188 `2/2 fresh failed=0`, `core_blockers=0`, `integrity_stale=0`, `offsite_fresh=1`, `rclone_gdrive_fresh=1`, `escrow_missing=5`。188 `node-exporter` textfile scrape、PostgreSQL exporter、Redis exporter、`nginx-exporter`、MinIO endpoint、Velero BSL and latest completed backup freshness are restored; monitoring coverage is `14/14 UP`; `BackupHealthMonitorMissing188``PostgreSQLDown``RedisDown``VeleroBackupNotRun` and 110 disk-pressure alerts resolved. DR remains blocked on real non-secret credential escrow evidence IDs. |
| P2 service / data truth | BLOCKED_MOMO_DATA_FRESHNESS | 98% | Public route/TLS, API/Web route, momo health `V10.653`, MOMO main / CD `#904` commit `84035906aba0e5e190d031a13cfd9b47a8cd1f73`, 188 live import-boundary source marker, current-month parity `10936|10936|2026-06-01|2026-06-17|2026-06-01|2026-06-17`, backup exporters, schedules, K3s node readiness/storage conditions, VIP, and 110 / 188 runtime health are green. Mac Mini / MacBook Pro controlled MOMO workspaces both point to the same codex branch commit. MOMO latest business date remains `2026-06-17`; stale age is `7` days as of 23:33. Drive pending folder has `0` matching files in repeated scheduler checks; scheduler stats show `file_count=0` / `imported_count=0` for repeated AutoImport runs; latest valid job `56` already imported `即時業績_當日.xlsx` with `sync_success=true` and bounds `2026-06-01..2026-06-17`; Mac Mini / MacBook candidate files are old or header-only, so there is no safe newer source to import. |
| P3 docs / automation contracts | DONE_WITH_MOMO_SOURCE_ABSENCE_GATE_V142_REPO_ONLY | 100% | Workplan, SOP v1.44, BACKUP-STATUS, LOGBOOK, 120 console/fsck recovery, Gitea backup stale-dump hardening, reboot ledger/version-comparison SOP, escrow evidence audit, 188 nginx Ansible baseline, 110 cold-start detector script, startup judgment layers, GO/NO-GO tree, host recovery cards, explicit Plan B degraded-operation path, machine-readable `plan_b` baseline, readiness-audit Plan B guard, B0-B5 service levels, T+0/T+120 fallback timeline checks, host role / load-balancing assessment, CD `known_hosts` guardrail, `fwupd-refresh.timer` rollback note, K3s filesystem event blocker, AWOOOI backup no-direct-offsite-sync contract, 110/188 Ansible source-of-truth, Gitea self-hosted readiness validation workflow, post-CD no-regression readbacks, stale-vs-active K8s failed Job classification, 110 runaway browser / CI load AIOps exporter + alert + gated remediation PlayBook, Telegram / AI event packet mapping, healthy heartbeat Telegram suppression, MOMO scheduler / current-month detector fix, 188 node-exporter restore helper, 188 DB/Redis exporter restore helper, 188 MinIO/Velero restore helper, 188 nginx-exporter restore helper, 110 Docker disk pressure cleanup boundary, MOMO Google Drive token userns readback, MOMO daily freshness blocker, MOMO Pro false-noise health monitor source-of-truth, docker-health direct Telegram fallback cooldown, Bitan public-content same-fingerprint cooldown, notification-noise readback, MOMO source-file absence GO/NO-GO gate with scheduler stats / import_config / job 56 evidence, repo-side cold-start v1.42 source absence classifier, live-sync parity gate, MOMO V10.653 / Gitea main / dual-workstation Codex baseline readback, MOMO import-boundary production deploy, MacBook Pro Codex safe artifact sync readback, and MacBook Pro AwoooGo Gitea SSH / dev workspace readback are updated. Latest deploy marker `622bc372` points runtime image to `2ec7f6f4`; CD `#3294` retains a historical Failure because post-deploy monitoring coverage saw 188 `nginx-exporter` down before recovery, while manual coverage now passes `14/14 UP`. 2026-06-24 23:15 read-only verify still shows repo cold-start hash `f60b81029969a527dc742ebc9558d2933f11fe24ec4f46f7a7bc6637759b7b05` differs from 110 live hash `10608873d406911a519afa96218abebc2b85ab6123bdf46b6e21eb269e554bb8`; live 110 script sync of the v1.42 classifier is not claimed until separately approved and recorded. |
| Overall recovery readiness | FULL_STACK_GREEN_DR_ESCROW_BLOCKED | 99% | 2026-06-25 14:41 post-start quick check returned exit `0`, `POST_START_QUICK_CHECK PASS=18 WARN=2 BLOCKED=0`, warning split `SERVICE=0 BOUNDARY=1 EVIDENCE=1`, result `FULL_STACK_GREEN_DR_ESCROW_BLOCKED`: 110 / 120 / 121 / 188 ping and SSH port are OK, K3s `mon` / `mon1` are Ready, public routes/TLS are green, AWOOOI API health is healthy/prod/mock=false, delegated cold-start is `PASS=89 WARN=0 BLOCKED=0`, MOMO service health is healthy on `V10.676`, MOMO data freshness is `1|2026-06-24`, 110 / 188 runtime and backup checks are green。MOMO latest valid job `57` completed cleanly at `2026-06-25T13:18:02`, `15383/15383/0`, and current-month snapshot / realtime bounds match through `2026-06-24`. DR remains blocked because credential escrow evidence markers are still missing (`escrow_missing=5`) and must not be forged. |
| P0 host / K3s recovery | DONE | 100% | 120 booted after console fsck at `2026-06-12 15:13`; latest 2026-06-25 09:05 readback shows 120 is reachable, K3s is active, `mon` and `mon1` are both `Ready control-plane`, VIP `192.168.0.125` is present, node filesystem / disk-pressure / readonly events are `0`, and latest `km-vectorize-29705460-55rgs` completed. |
| P1 backup / alert / escrow | BLOCKED_DR_ESCROW | 97% | 2026-06-25 09:05 backup / alert readback shows 110 `13/13 fresh failed=0`, 188 `2/2 fresh failed=0`, `core_blockers=0`, `integrity_stale=0`, `offsite_fresh=1`, `rclone_gdrive_fresh=1`, `escrow_missing=5`, last aggregate `2026-06-25 02:35:09`DR remains blocked on real non-secret credential escrow evidence IDs. |
| P2 service / data truth | GREEN | 100% | Public route/TLS, API/Web route, MOMO health `V10.676`, MOMO main / CD `#904` monthly-sync failure boundary, MOMO main / CD `#910` Drive-auth fail-closed boundary, direct 14:41 wrapper public route smoke all expected 2xx/3xx, current-month parity `15383|15383|2026-06-01|2026-06-24|2026-06-01|2026-06-24`, backup exporters, schedules, K3s node readiness/storage conditions, VIP, and 110 / 188 runtime health are green. 14:41 preflight confirms app / scheduler / Telegram bot healthy, scheduler restart count `0`, token metadata aligned to scheduler UID, latest job `57` completed cleanly, and `DB_DAILY_FRESHNESS 1|2026-06-24`. |
| P3 docs / automation contracts | DONE_WITH_WRAPPER_LIVE_READBACK | 100% | Workplan, SOP v1.53, one-page post-start quick check wrapper + fallback runbook, BACKUP-STATUS, LOGBOOK, 120 console/fsck recovery, Gitea backup stale-dump hardening, reboot ledger/version-comparison SOP, escrow evidence audit, 188 nginx Ansible baseline, 110 cold-start detector script, startup judgment layers, GO/NO-GO tree, host recovery cards, explicit Plan B degraded-operation path, machine-readable `plan_b` baseline, readiness-audit Plan B guard, B0-B5 service levels, T+0/T+120 fallback timeline checks, host role / load-balancing assessment, CD `known_hosts` guardrail, `fwupd-refresh.timer` rollback note, K3s filesystem event blocker, AWOOOI backup no-direct-offsite-sync contract, 110/188 Ansible source-of-truth, Gitea self-hosted readiness validation workflow, post-CD no-regression readbacks, stale-vs-active K8s failed Job classification, 110 runaway browser / CI load AIOps exporter + alert + gated remediation PlayBook, Telegram / AI event packet mapping, healthy heartbeat Telegram suppression, MOMO scheduler / current-month detector fix, 188 node-exporter restore helper, 188 DB/Redis exporter restore helper, 188 MinIO/Velero restore helper, 188 nginx-exporter restore helper, 110 Docker disk pressure cleanup boundary, MOMO Google Drive token userns readback, MOMO data freshness hard blocker, MOMO Pro false-noise health monitor source-of-truth, docker-health direct Telegram fallback cooldown, Bitan public-content same-fingerprint cooldown, notification-noise readback, MOMO source-file absence decision gate with scheduler stats / import_config / job 56 evidence, repo-side cold-start v1.42 source absence classifier, live-sync parity gate, MOMO import-boundary production deploy, MOMO Drive-auth fail-closed production deploy, 10:04 scheduler fail-closed live proof, 10:35 route / DB / backup refresh, 11:44 MOMO dedicated preflight blocked readback, 14:16 MOMO dedicated preflight recovery on V10.674 / job 57 / freshness 1, 14:41 wrapper warning split `SERVICE=0 BOUNDARY=1 EVIDENCE=1`, 10:58 user-approved 110 orphan Chrome SIGTERM evidence, MacBook Pro Codex safe artifact sync readback, and 2026-06-25 live refresh with full cold-start GREEN are updated. 2026-06-24 23:15 read-only verify still shows repo cold-start hash `f60b81029969a527dc742ebc9558d2933f11fe24ec4f46f7a7bc6637759b7b05` differs from 110 live hash `10608873d406911a519afa96218abebc2b85ab6123bdf46b6e21eb269e554bb8`; live 110 script sync of the v1.42 classifier is not claimed until separately approved and recorded. |
Full cold-start service readiness may not be declared green for the latest verified evidence set. As of 2026-06-24 23:33, routes/hosts/K3s/backups/exporters/Velero/monitoring coverage are available, and MOMO production code has the import-boundary fix, but the latest repo-side live read-only cold-start scorecard remains `PASS=88 WARN=0 BLOCKED=1` because MOMO business data freshness is stale beyond 3 days and no newer legitimate source file is available. The blocker is explicitly `188 momo source file absent while daily sales data stale`; this is repo-side source-of-truth evidence and not yet a claim that the 110 live monitor script was deployed. Do not declare DR scorecard complete while credential escrow evidence remains blocked.
2026-06-25 14:41 supplemental wrapper readback supersedes the 14:16 wrapper wording: direct route smoke is 200 for AWOOOI API / IwoooS / MOMO health / Stock, and cold-start public route/TLS gate is green for all expected 2xx/3xx routes. Repo-side cold-start returns `PASS=89 WARN=0 BLOCKED=0`; `/backup/scripts/backup-status.sh --no-notify --no-refresh` reports 110 `13/13 fresh failed=0`, 188 `2/2 fresh failed=0`, `core_blockers=0`, `integrity_stale=0`, `offsite_fresh=1`, `rclone_gdrive_fresh=1`, `escrow_missing=5`; MOMO dedicated preflight returns `PASS=19 WARN=2 BLOCKED=0`; MOMO health is `V10.676`; 110 load is around `4.09 / 4.52 / 4.39`, with active Gitea Actions / build / test load visible, not orphan Chrome. Wrapper result is `FULL_STACK_GREEN_DR_ESCROW_BLOCKED`, not `DEGRADED`, because service warnings are `0` and only DR boundary / evidence warnings remain.
Full cold-start service readiness may now be declared GREEN for the latest verified evidence set. As of 2026-06-25 14:41, routes/hosts/K3s/backups/exporters/monitoring surfaces are available, AWOOOI API is healthy, MOMO service health is `V10.676`, and MOMO business data is fresh through `2026-06-24`. The live read-only cold-start scorecard is `PASS=89 WARN=0 BLOCKED=0`, and the post-start wrapper result is `FULL_STACK_GREEN_DR_ESCROW_BLOCKED`. Do not declare DR scorecard complete while credential escrow evidence remains blocked.
2026-06-13 01:26 refresh: full cold-start is again green for the current evidence set. AWOOOI API/Web workload balancing survived the next normal CD deploy: Gitea main `e4a349bc`, ArgoCD revision `e4a349bc`, images from `414413a5`, API/Web split across `mon` / `mon1`, and global `known_hosts` retained 120 / 188 after CD fix `80e6ec1a`. Do not declare DR complete while credential escrow is missing. `km-vectorize` remediation is `90%`: schedule/label fix is live, and the remaining gate is the next official 03:00 CronJob success readback.
@@ -158,8 +160,8 @@ Next: <single next action>
| ID | Status | % | Work item | Fine analysis | Next action | Done criteria |
|----|--------|---:|-----------|---------------|-------------|---------------|
| P2-001 | VERIFIED | 100 | Public route smoke | 2026-06-12 18:57 cold-start confirms all listed domains returned expected 2xx/3xx over HTTPS; registry root route returned 200 in the scorecard and `/v2/` remains the normal unauthenticated 401 pattern from earlier checks. This proves ingress/TLS plus current route availability. | Keep as one row in scorecard. | Public route table updated after each reboot. |
| P2-002 | BLOCKED_MOMO_DATA_FRESHNESS | 97 | momo latest/current-month parity and freshness | Latest current-month parity is good: `10936|10936|2026-06-01|2026-06-17|2026-06-01|2026-06-17`. Full-table read-only DB evidence is `daily_sales_snapshot=104614 rows, 2025-07-01..2026-06-17` and `realtime_sales_monthly=786621 rows, 2024-01-01..2026-06-17`. However latest business data is stale: `MOMO_DAILY_FRESHNESS 7|2026-06-17`; `import_config` expects folder `當日業績匯入` and pattern `即時業績_當日`; repeated scheduler runs, including 2026-06-24 21:56, report `file_count=0` / `imported_count=0`; latest valid job `56` already imported `即時業績_當日.xlsx` with `sync_success=true` and bounds `2026-06-01..2026-06-17`. | Wait for or obtain a newer legitimate PChome daily-sales source file, then verify import job `sync_success=true`, file movement only after success, table bounds, and `MOMO_DAILY_FRESHNESS <= 2`. Do not manually import stale local samples, product exports, header-only sheets, or already imported archives. | Snapshot/current-month row count and bounds match, source folder has a newer legitimate source or no unprocessed stale file, next import has `sync_success=true`, and daily freshness is within threshold. |
| P2-008 | DONE | 100 | Separate MOMO service recovery from upstream source absence | 2026-06-24 11:35 readback proves MOMO service is healthy (`V10.639`), DB parity is good, scheduler container can list Drive, and recent logs have no current token `Permission denied`; the blocker is source-file absence, not service outage. SOP v1.32 records GO/NO-GO rules forbidding old archive re-import, product-export import, truncate, whole-DB restore, or fake freshness. | Keep the stale warning active until a legitimate newer `即時業績_當日` source file appears and imports cleanly. | Operators can say "MOMO service recovered, data pipeline waiting for upstream source file" without calling the full stack green. |
| P2-002 | GREEN | 100 | momo latest/current-month parity and freshness | Latest current-month parity is good: `15383|15383|2026-06-01|2026-06-24|2026-06-01|2026-06-24`. Full-table read-only DB evidence is `daily_sales_snapshot=109061 rows, 2025-07-01..2026-06-24`. 14:16 dedicated preflight returns `PASS=18 WARN=3 BLOCKED=0`: public/local health and scheduler are healthy on `V10.674`, latest job `57` is clean, latest business data is fresh `DB_DAILY_FRESHNESS 1|2026-06-24`, and token metadata aligns with scheduler UID without reading token content. | Keep running `scripts/reboot-recovery/momo-drive-token-source-recovery-preflight.sh` before any future MOMO recovery claim; monitor next scheduled import and ensure any Drive/API failure still fails closed. | Preflight has no BLOCKED result; snapshot/current-month row count and bounds match; daily freshness is within threshold. |
| P2-008 | DONE_SUPERSEDED_BY_JOB_57_RECOVERY | 100 | Separate MOMO service recovery from upstream source absence | 2026-06-24 11:35 readback proved MOMO service was healthy and source-file absence was the blocker. 2026-06-25 10:35 superseded that with a stricter split: service healthy, DB parity good, but token / Drive auth evidence not sufficient and scheduler fail-closed behavior required. 2026-06-25 14:16 supersedes the blocker with job `57` clean import, `V10.674`, token metadata aligned to scheduler UID, current-month parity through `2026-06-24`, and `DB_DAILY_FRESHNESS 1|2026-06-24`. SOP v1.51 preserves the GO/NO-GO rules forbidding old archive re-import, product-export import, truncate, whole-DB restore, fake freshness, or token secret exposure. | Keep running the dedicated preflight after each reboot/import window; if Drive/API auth fails again, it must fail closed and alert rather than becoming an empty-folder success. | Operators can now say "MOMO service and business data freshness recovered for the 2026-06-25 14:16 evidence set" while still refusing DR complete / credential escrow complete / Wazuh registry accepted claims. |
| P2-003 | DONE_PRODUCTION_DEPLOYED_WAITING_NEXT_REAL_IMPORT | 99 | Fix momo job semantics | Gitea-first repair is in `/Users/ogt/codex-workspaces/momo-pro-dev` commit `84035906aba0e5e190d031a13cfd9b47a8cd1f73` on branch `codex/momo-current-main-dev-base-20260624`, also fast-forwarded to MacBook Pro and fast-forwarded to MOMO `main`. Gitea Actions `cd.yaml #904` succeeded, and 188 live source contains `_table_columns`, `業績分析儀表板同步失敗`, and `保留來源檔案等待重試,不移動 Google Drive 檔案`. `process_daily_sales_import()` marks monthly sync failure as `failed`, records the sync error in summary, returns `False`, and leaves `auto_import_from_drive()` outside the Drive archive/move path. Regression tests cover both job failure and no-move behavior. | Watch the next real Google Drive import and confirm no file moves unless both tables sync; if a real monthly sync failure happens, verify import job status is `failed` and source file remains pending. | `pytest tests/test_import_service_sql_params.py tests/test_auto_import_data_sync.py tests/test_auto_import_failure_boundaries.py -q` returns `10 passed`; production deployment/readback is complete; final behavioral closeout requires next real import evidence. |
| P2-004 | DONE | 100 | PostgreSQL index corruption runbook path | SOP v1.2 now states `posting list tuple ... cannot be split` is an index repair incident. | Use only concurrent reindex if the error returns. | No truncate, no whole DB restore; `REINDEX TABLE CONCURRENTLY public.realtime_sales_monthly;` and idempotent resync evidence recorded. |
| P2-005 | VERIFIED | 100 | Do not rely on route 200 only | 2026-06-12 closeout has route + DB + backup + offsite + schedule + alert + K3s + cold-start scorecard evidence. The only remaining blocker is DR credential escrow, outside service availability. | Keep this cross-surface checklist mandatory after every reboot. | Each reboot record has route, DB, backup, schedules, alert, scorecard rows. |
@@ -179,7 +181,7 @@ Next: <single next action>
| P3-005 | DONE | 100 | Update cold-start SOP | SOP now includes start, shutdown, reboot, record, comparison, and 120 blocker handling. | Increment SOP version after each process change. | SOP has controlled power-operation sections and ledger template. |
| P3-006 | DONE | 100 | Update backup status | Backup status now reflects current cron, rclone latest-only, failure-only alert posture, and escrow blocker. | Refresh after 120 backup rerun. | Backup status no longer claims noisy success Telegram notifications. |
| P3-007 | DONE | 100 | Harden Gitea backup stale dump handling | 2026-06-05 manual Gitea backup failed because the container retained `/tmp/gitea-dump.zip` from the 02:00 failure. `scripts/backup/backup-gitea.sh` now renames stale container dump files to timestamped evidence before running a new dump, and the live 110 script is updated. | Watch the next 02:00 Gitea backup. | `bash -n` passes locally and on 110; manual Gitea backup completed after stale evidence rename. |
| P3-008 | DONE | 100 | Continuously optimize host reboot SOP | SOP v1.44 adds startup judgment layers, GO/NO-GO decision tree, freeze execution checklist, host boot detection, 110/188/120/121 recovery cards, explicit Plan B degraded-operation path, machine-readable `plan_b` baseline, readiness-audit Plan B guard, B0-B5 service levels, T+0/T+120 fallback timeline, K3s filesystem event blocker, stale-vs-active K8s failed Job classification, post-reboot / post-CD recovery anchors, AA/AS 判定, workload 分散判定, CD SSH trust guardrail, CronJob failure evidence retention rule, `fwupd-refresh.timer` rollback note, 110 runaway browser / CI load 分流 PlayBook, healthy-heartbeat suppression, 188 node-exporter restore, 188 DB/Redis exporter restore, 188 MinIO/Velero restore, 188 nginx-exporter restore, 110 Docker disk cleanup boundary, MOMO Google Drive token userns readback, MOMO data freshness hard blocker, post-reboot notification noise gates, MOMO source-file absence decision gate with scheduler stats / import_config / job 56 evidence, repo-side scorecard source-absence classifier, 110 live-sync parity gate, and CD monitoring coverage target-down classification. | Use v1.43 for the next reboot record, then compare actual timing, Plan B trigger, degraded level, failed/stale/active Job counters, runaway-process metrics, CI load attribution, MOMO source availability, data freshness, Velero freshness, exporter scrape, disk usage, notification-noise state, monitoring coverage, and blockers against §1.4 plus §11.1 / §14.8 through §14.32. Before any real reboot, rerun same-day live cold-start / backup / offsite / alert / escrow / runaway-process / notification-noise / MOMO source-file / monitoring coverage checks. If using the live 110 script, record its hash and do not assume repo-side v1.42 behavior until synced under approval and deploy parity passes. | SOP distinguishes `HOST_BOOTED`, `HOST_READY`, `SERVICE_READY`, `FULL_STACK_GREEN`, `K3S_CONTROL_PLANE_AA`, `WORKLOAD_BALANCED`, `B0_ABORTED_BEFORE_REBOOT`, `B1_HOST_RECOVERY_ONLY`, `B2_CORE_SERVICE_READY`, `B3_SERVICE_AVAILABLE_DEGRADED`, `B4_FULL_STACK_GREEN`, and `B5_DR_COMPLETE`; live cold-start returns `PASS=86 WARN=0 BLOCKED=1` for the current evidence set, repo-side v1.42 dry-run returns `PASS=88 WARN=0 BLOCKED=1` with blocker `188 momo source file absent while daily sales data stale`, and 23:15 deploy parity correctly blocks live-sync claims until hash parity is restored; repeated healthy/same-failure notification noise is controlled without hiding real alerts, and monitoring coverage target-down is routed through exporter restore before any product restart. |
| P3-008 | DONE | 100 | Continuously optimize host reboot SOP | SOP v1.52 adds one-page post-start quick check wrapper, fallback runbook, startup judgment layers, GO/NO-GO decision tree, freeze execution checklist, host boot detection, 110/188/120/121 recovery cards, explicit Plan B degraded-operation path, machine-readable `plan_b` baseline, readiness-audit Plan B guard, B0-B5 service levels, T+0/T+120 fallback timeline, K3s filesystem event blocker, stale-vs-active K8s failed Job classification, post-reboot / post-CD recovery anchors, AA/AS 判定, workload 分散判定, CD SSH trust guardrail, CronJob failure evidence retention rule, `fwupd-refresh.timer` rollback note, 110 runaway browser / CI load 分流 PlayBook, healthy-heartbeat suppression, 188 node-exporter restore, 188 DB/Redis exporter restore, 188 MinIO/Velero restore, 188 nginx-exporter restore, 110 Docker disk cleanup boundary, MOMO Google Drive token userns readback, MOMO data freshness hard blocker, post-reboot notification noise gates, MOMO source-file absence decision gate with scheduler stats / import_config / job 56 evidence, repo-side scorecard source-absence classifier, 110 live-sync parity gate, CD monitoring coverage target-down classification, MOMO dedicated token/source preflight, MOMO V10.674 / StartedAt / lifecycle / job 57 / freshness 1 recovery readback, and 2026-06-25 110 CPU orphan Chrome vs active CI 分流 evidence. | Use `scripts/reboot-recovery/post-start-quick-check.sh --no-color` for T+10 post-reboot triage, then use `docs/runbooks/REBOOT-POST-START-QUICK-CHECK.md` as manual fallback and SOP v1.52 for exceptions, Plan B, blocker-specific recovery, and historical comparison. Before any real reboot, rerun same-day live cold-start / backup / offsite / alert / escrow / runaway-process / notification-noise / MOMO preflight / monitoring coverage checks. If using the live 110 script, record its hash and do not assume repo-side v1.42 behavior until synced under approval and deploy parity passes. | SOP distinguishes `HOST_BOOTED`, `HOST_READY`, `SERVICE_READY`, `FULL_STACK_GREEN`, `K3S_CONTROL_PLANE_AA`, `WORKLOAD_BALANCED`, `B0_ABORTED_BEFORE_REBOOT`, `B1_HOST_RECOVERY_ONLY`, `B2_CORE_SERVICE_READY`, `B3_SERVICE_AVAILABLE_DEGRADED`, `B4_FULL_STACK_GREEN`, and `B5_DR_COMPLETE`; quick check wrapper has one command order and LOGBOOK summary; latest MOMO dedicated preflight returns `PASS=18 WARN=3 BLOCKED=0`; 110 CPU evidence records old orphan Chrome groups removed by approved SIGTERM while active CI load remains observation-only; repeated healthy/same-failure notification noise is controlled without hiding real alerts, and monitoring coverage target-down is routed through exporter restore before any product restart. |
| P3-009 | DONE | 100 | Assess 120/121 AA/AS role and host load balancing | 2026-06-12 15:19 live check confirms 120 and 121 are both `Ready control-plane`, `k3s active`, `k3s-agent inactive`, with no taints; however most AWOOOI / ArgoCD / Velero workload remains on 121 after 120 fsck recovery. New assessment defines control-plane AA vs workload AA, migration candidates from 110/188, and stateful migration blockers. | After P0 backup/offsite/cold-start green, implement topology spread for AWOOOI API/Web before moving additional services. | `docs/runbooks/HOST-ROLE-LOAD-BALANCING-ASSESSMENT.md` exists; SOP v1.6 links AA/AS and load-balancing checks; migration implementation remains explicitly `0%`. |
| P3-010 | DONE | 100 | Update workload balancing docs with 2026-06-13 live truth | Host role assessment, workplan, SOP, backup status, and LOGBOOK are refreshed with current cold-start, backup, 188 certbot degraded, ArgoCD `km-vectorize` degraded, Gitea main `acaae999`, ArgoCD sync, and final pod placement evidence. | Keep updating this file after the next reboot or deploy. | Docs separate service-green status from DR escrow, workload rollout, and non-service governance debt. |
| P3-011 | DONE | 100 | Record `km-vectorize` remediation status | LOGBOOK, this workplan, and SOP now state the schedule/label fix, ArgoCD sync evidence, the invalid manual Job boundary, and the 90% waiting-for-next-schedule gate. | After next 03:00 run, update this row and the top verdict with `lastSuccessfulTime` / ArgoCD health evidence. | No document claims ArgoCD green before official CronJob success evidence exists. |

View File

@@ -50,7 +50,7 @@
- name: "Backup | inspect momo PostgreSQL backup script"
ansible.builtin.stat:
path: /home/ollama/momo-pro/scripts/pg_backup.sh
path: /home/ollama/bin/momo-pg-backup.sh
register: momo_pg_backup_script
- name: "Backup | inspect AwoooP ops notification helper"
@@ -83,4 +83,4 @@
- "notify_helper_exists={{ momo_notify_helper.stat.exists | default(false) }}"
- "notify_helper_executable={{ momo_notify_helper.stat.executable | default(false) }}"
- "backup_dir_exists={{ momo_backup_dir.stat.exists | default(false) }}"
- "cron_has_pg_backup={{ '/home/ollama/momo-pro/scripts/pg_backup.sh' in (ollama_crontab.stdout | default('')) }}"
- "cron_has_pg_backup={{ '/home/ollama/bin/momo-pg-backup.sh' in (ollama_crontab.stdout | default('')) }}"

View File

@@ -11,14 +11,14 @@
vars:
momo_backup_script_source: "{{ playbook_dir }}/../../../scripts/backup/backup-momo-188-pg.sh"
momo_notify_helper_source: "{{ playbook_dir }}/../../../scripts/ops/notify-awoooi-ops.sh"
momo_scripts_dir: /home/ollama/momo-pro/scripts
momo_backup_script_path: /home/ollama/momo-pro/scripts/pg_backup.sh
momo_notify_helper_path: /home/ollama/momo-pro/scripts/notify-awoooi-ops.sh
momo_scripts_dir: /home/ollama/bin
momo_backup_script_path: /home/ollama/bin/momo-pg-backup.sh
momo_notify_helper_path: /home/ollama/bin/notify-awoooi-ops.sh
momo_backup_dir: /home/ollama/momo_backups
momo_backup_cron_name: AWOOOI momo PostgreSQL daily backup
momo_backup_cron_job: >-
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
/home/ollama/momo-pro/scripts/pg_backup.sh
/home/ollama/bin/momo-pg-backup.sh
>> /home/ollama/momo_backups/backup.log 2>&1
momo_legacy_bin_cron_line: "0 2 * * * /home/ollama/bin/momo-pg-backup.sh >> /home/ollama/momo_backups/backup.log 2>&1"
momo_legacy_direct_cron_line: "0 2 * * * /home/ollama/momo-pro/scripts/pg_backup.sh >> /home/ollama/momo_backups/backup.log 2>&1"

View File

@@ -0,0 +1,278 @@
#!/usr/bin/env bash
set -uo pipefail
# Read-only MOMO recovery preflight. This script must not import files, move
# Drive artifacts, restart containers, change token ownership, or print secrets.
MOMO_HOST="${MOMO_HOST:-ollama@192.168.0.188}"
FRESHNESS_MAX_DAYS="${FRESHNESS_MAX_DAYS:-2}"
SSH_CONNECT_TIMEOUT="${SSH_CONNECT_TIMEOUT:-8}"
PASS_COUNT=0
WARN_COUNT=0
BLOCKED_COUNT=0
ok() {
PASS_COUNT=$((PASS_COUNT + 1))
printf 'OK: %s\n' "$*"
}
warn() {
WARN_COUNT=$((WARN_COUNT + 1))
printf 'WARN: %s\n' "$*"
}
blocked() {
BLOCKED_COUNT=$((BLOCKED_COUNT + 1))
printf 'BLOCKED: %s\n' "$*"
}
usage() {
cat <<'USAGE'
Usage: momo-drive-token-source-recovery-preflight.sh [--host user@host] [--freshness-max-days N]
Read-only checks:
- MOMO public health and local health endpoint on 188
- MOMO version, container StartedAt, health, recent replace/restart evidence
- momo-scheduler running / health / UID
- Google token metadata only, never token content
- scheduler fail-closed log evidence and notification evidence
- exact local daily-sales source candidate presence
- daily_sales_snapshot / realtime_sales_monthly bounds
- latest daily_sales import job
Exit codes:
0 = no warnings or blockers
1 = warnings only
2 = one or more blockers
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--host)
MOMO_HOST="${2:-}"
shift 2
;;
--freshness-max-days)
FRESHNESS_MAX_DAYS="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
printf 'Unknown argument: %s\n' "$1" >&2
usage >&2
exit 2
;;
esac
done
if ! [[ "$FRESHNESS_MAX_DAYS" =~ ^[0-9]+$ ]]; then
printf 'FRESHNESS_MAX_DAYS must be numeric: %s\n' "$FRESHNESS_MAX_DAYS" >&2
exit 2
fi
tmp_output="$(mktemp -t momo-drive-preflight.XXXXXX)"
trap 'rm -f "$tmp_output"' EXIT
if ! ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout="$SSH_CONNECT_TIMEOUT" "$MOMO_HOST" 'bash -s' >"$tmp_output" <<'REMOTE'
set -uo pipefail
emit() {
printf '%s %s\n' "$1" "${2:-}"
}
emit HOST "$(hostname 2>/dev/null || true)"
momo_health_json="$(curl -s --max-time 5 http://127.0.0.1:5003/health 2>/dev/null || true)"
momo_public_health_json="$(curl -s --max-time 8 https://mo.wooo.work/health 2>/dev/null || true)"
emit MOMO_HEALTH_CODE "$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:5003/health 2>/dev/null || true)"
emit MOMO_PUBLIC_HEALTH_CODE "$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 https://mo.wooo.work/health 2>/dev/null || true)"
emit MOMO_HEALTH_VERSION "$(printf '%s\n%s\n' "$momo_health_json" "$momo_public_health_json" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)"
emit MOMO_APP_STARTED_AT "$(docker inspect -f '{{.State.StartedAt}}' momo-pro-system 2>/dev/null || true)"
emit MOMO_APP_HEALTH "$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' momo-pro-system 2>/dev/null || true)"
emit SCHEDULER_RUNNING "$(docker inspect -f '{{.State.Running}}' momo-scheduler 2>/dev/null || true)"
emit SCHEDULER_HEALTH "$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' momo-scheduler 2>/dev/null || true)"
emit SCHEDULER_STARTED_AT "$(docker inspect -f '{{.State.StartedAt}}' momo-scheduler 2>/dev/null || true)"
emit SCHEDULER_RESTART_COUNT "$(docker inspect -f '{{.RestartCount}}' momo-scheduler 2>/dev/null || true)"
emit SCHEDULER_UID "$(docker top momo-scheduler -eo pid,user,uid 2>/dev/null | awk 'NR==2 {print $3}' || true)"
emit TELEGRAM_BOT_STARTED_AT "$(docker inspect -f '{{.State.StartedAt}}' momo-telegram-bot 2>/dev/null || true)"
emit TELEGRAM_BOT_HEALTH "$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' momo-telegram-bot 2>/dev/null || true)"
event_until="$(date --iso-8601=seconds 2>/dev/null || date -Iseconds)"
recent_events="$(docker events --since 45m --until "$event_until" --filter container=momo-pro-system --filter container=momo-scheduler --filter container=momo-telegram-bot 2>/dev/null || true)"
emit MOMO_CONTAINER_REPLACE_EVENTS_45M "$(printf '%s\n' "$recent_events" | grep -Ec 'container (restart|start|kill|die|stop)' || true)"
token_stat="$(stat -c '%u:%g:%a' /home/ollama/momo-pro/config/google_token.json 2>/dev/null || true)"
emit TOKEN_STAT "${token_stat:-missing}"
container_token_stat="$(docker exec momo-scheduler sh -lc 'stat -c "%u:%g:%a" config/google_token.json 2>/dev/null || true' 2>/dev/null || true)"
emit CONTAINER_TOKEN_STAT "${container_token_stat:-missing}"
logs="$(docker logs --since 8h momo-scheduler 2>&1 || true)"
emit LOG_AUTH_FAILURE_COUNT "$(printf '%s\n' "$logs" | grep -Ec 'Google Drive 認證失敗|could not locate runnable browser|Permission denied.*google_token|連線或認證失敗' || true)"
emit LOG_FAIL_CLOSED_COUNT "$(printf '%s\n' "$logs" | grep -Ec '自動匯入失敗|未能確認來源資料夾是否有新檔案' || true)"
emit LOG_FAILURE_NOTIFY_SUCCESS_COUNT "$(printf '%s\n' "$logs" | grep -Ec '匯入失敗通知已發送|Telegram 通知發送成功' || true)"
emit LOG_EMPTY_SOURCE_COUNT "$(printf '%s\n' "$logs" | grep -Ec '找到 0 個 Excel|沒有找到待匯入' || true)"
emit LOG_SUCCESS_IMPORT_COUNT "$(printf '%s\n' "$logs" | grep -Ec '自動匯入完成|匯入成功|成功匯入' || true)"
source_count="$(find /home/ollama/momo-pro /backup -type f -name '即時業績_當日.xlsx' 2>/dev/null | wc -l | awk '{print $1}' || true)"
latest_source="$(find /home/ollama/momo-pro /backup -type f -name '即時業績_當日.xlsx' -printf '%T@|%TY-%Tm-%TdT%TH:%TM:%TS|%s|%f\n' 2>/dev/null | sort -n | tail -n 1 | cut -d'|' -f2- || true)"
emit LOCAL_EXACT_DAILY_SOURCE_COUNT "${source_count:-0}"
emit LOCAL_EXACT_DAILY_SOURCE_LATEST "${latest_source:-none}"
psql_query() {
docker exec momo-db psql -h 127.0.0.1 -U momo -d momo_analytics -Atc "$1" 2>/dev/null || true
}
emit DB_DAILY "$(psql_query "SELECT count(*) || chr(124) || coalesce(min(snapshot_date::date)::text, chr(45)) || chr(124) || coalesce(max(snapshot_date::date)::text, chr(45)) FROM daily_sales_snapshot;")"
emit DB_MONTHLY_CURRENT "$(psql_query "SELECT count(*) || chr(124) || coalesce(min(\"日期\"::date)::text, chr(45)) || chr(124) || coalesce(max(\"日期\"::date)::text, chr(45)) FROM realtime_sales_monthly WHERE \"日期\"::date >= make_date(extract(year from current_date)::int, extract(month from current_date)::int, 1);")"
emit DB_MONTHLY_SYNC "$(psql_query "WITH scope AS (SELECT min(snapshot_date::date) dmin, max(snapshot_date::date) dmax, count(*) sc FROM daily_sales_snapshot WHERE snapshot_date::date >= make_date(extract(year from current_date)::int, extract(month from current_date)::int, 1)), monthly AS (SELECT count(*) mc, min(\"日期\"::date) mmin, max(\"日期\"::date) mmax FROM realtime_sales_monthly, scope WHERE scope.sc > 0 AND \"日期\"::date BETWEEN scope.dmin AND scope.dmax) SELECT coalesce(scope.sc,0)::text || chr(124) || coalesce(monthly.mc,0)::text || chr(124) || coalesce(scope.dmin::text,chr(45)) || chr(124) || coalesce(scope.dmax::text,chr(45)) || chr(124) || coalesce(monthly.mmin::text,chr(45)) || chr(124) || coalesce(monthly.mmax::text,chr(45)) FROM scope, monthly;")"
emit DB_DAILY_FRESHNESS "$(psql_query "SELECT coalesce((current_date - max(snapshot_date::date))::text, chr(45)) || chr(124) || coalesce(max(snapshot_date::date)::text, chr(45)) FROM daily_sales_snapshot;")"
emit DB_LATEST_DAILY_IMPORT_JOB "$(psql_query "SELECT coalesce(id::text, chr(45)) || chr(124) || coalesce(status, chr(45)) || chr(124) || coalesce(drive_file_name, chr(45)) || chr(124) || coalesce(replace(created_at::text, chr(32), chr(84)), chr(45)) || chr(124) || coalesce(replace(completed_at::text, chr(32), chr(84)), chr(45)) || chr(124) || coalesce(total_rows::text, chr(45)) || chr(124) || coalesce(success_rows::text, chr(45)) || chr(124) || coalesce(error_rows::text, chr(45)) FROM import_jobs WHERE job_type = 'daily_sales' ORDER BY created_at DESC LIMIT 1;")"
emit IMPORT_CONFIG "$(psql_query "SELECT config_key || chr(61) || config_value FROM import_config;" | awk -F= '$1 == "gdrive_folder_path" {folder=$2} $1 == "gdrive_file_pattern" {pattern=$2} END {if (folder || pattern) print folder "|" pattern}')"
REMOTE
then
cat "$tmp_output"
blocked "MOMO host read-only SSH preflight failed: $MOMO_HOST"
else
cat "$tmp_output"
fi
value_for() {
awk -v key="$1" '$1 == key {sub($1 " ", ""); print; exit}' "$tmp_output"
}
num_for() {
local value
value="$(value_for "$1")"
[[ "$value" =~ ^[0-9]+$ ]] && printf '%s\n' "$value" || printf '0\n'
}
health_code="$(value_for MOMO_HEALTH_CODE)"
public_health_code="$(value_for MOMO_PUBLIC_HEALTH_CODE)"
[[ "$public_health_code" == "200" ]] && ok "MOMO public health endpoint returns 200" || blocked "MOMO public health endpoint is not 200: ${public_health_code:-missing}"
[[ "$health_code" == "200" ]] && ok "MOMO local health endpoint returns 200" || warn "MOMO local health endpoint is not 200: ${health_code:-missing}"
momo_version="$(value_for MOMO_HEALTH_VERSION)"
[[ -n "$momo_version" ]] && ok "MOMO health version readback is available: $momo_version" || warn "MOMO health version readback unavailable"
scheduler_running="$(value_for SCHEDULER_RUNNING)"
scheduler_health="$(value_for SCHEDULER_HEALTH)"
scheduler_started_at="$(value_for SCHEDULER_STARTED_AT)"
scheduler_restart_count="$(value_for SCHEDULER_RESTART_COUNT)"
momo_app_health="$(value_for MOMO_APP_HEALTH)"
momo_app_started_at="$(value_for MOMO_APP_STARTED_AT)"
telegram_bot_health="$(value_for TELEGRAM_BOT_HEALTH)"
telegram_bot_started_at="$(value_for TELEGRAM_BOT_STARTED_AT)"
[[ "$scheduler_running" == "true" ]] && ok "momo-scheduler container is running" || blocked "momo-scheduler container is not running"
[[ "$scheduler_health" == "healthy" ]] && ok "momo-scheduler container health is healthy" || warn "momo-scheduler health is not healthy: ${scheduler_health:-missing}"
[[ -n "$scheduler_started_at" ]] && ok "momo-scheduler started_at metadata is available: $scheduler_started_at" || warn "momo-scheduler started_at metadata unavailable"
[[ "$scheduler_restart_count" == "0" ]] && ok "momo-scheduler restart count is 0" || warn "momo-scheduler restart count is not 0: ${scheduler_restart_count:-missing}"
[[ "$momo_app_health" == "healthy" ]] && ok "momo-pro-system health is healthy" || warn "momo-pro-system health is not healthy: ${momo_app_health:-missing}"
[[ -n "$momo_app_started_at" ]] && ok "momo-pro-system started_at metadata is available: $momo_app_started_at" || warn "momo-pro-system started_at metadata unavailable"
[[ "$telegram_bot_health" == "healthy" ]] && ok "momo-telegram-bot health is healthy" || warn "momo-telegram-bot health is not healthy: ${telegram_bot_health:-missing}"
[[ -n "$telegram_bot_started_at" ]] && ok "momo-telegram-bot started_at metadata is available: $telegram_bot_started_at" || warn "momo-telegram-bot started_at metadata unavailable"
replace_events="$(num_for MOMO_CONTAINER_REPLACE_EVENTS_45M)"
if [[ "$replace_events" -gt 0 ]]; then
warn "recent MOMO container replace/restart events observed in the last 45m: $replace_events"
else
ok "no MOMO container replace/restart events observed in the last 45m"
fi
scheduler_uid="$(value_for SCHEDULER_UID)"
token_stat="$(value_for TOKEN_STAT)"
container_token_stat="$(value_for CONTAINER_TOKEN_STAT)"
if [[ "$token_stat" == "missing" || -z "$token_stat" ]]; then
warn "host Google token artifact metadata is missing"
elif [[ "$scheduler_uid" =~ ^[0-9]+$ ]]; then
token_uid="${token_stat%%:*}"
token_mode="${token_stat##*:}"
if [[ "$token_uid" == "$scheduler_uid" && "$token_mode" =~ ^[0-9]+$ && "$token_mode" -le 600 ]]; then
ok "host Google token metadata matches scheduler UID and restrictive mode"
else
warn "host Google token metadata does not match scheduler UID/mode: token=$token_stat scheduler_uid=$scheduler_uid"
fi
else
warn "scheduler UID unavailable; token metadata cannot be matched"
fi
if [[ "$container_token_stat" == "missing" || -z "$container_token_stat" ]]; then
warn "container Google token artifact metadata is missing"
else
ok "container Google token artifact metadata exists"
fi
auth_failures="$(num_for LOG_AUTH_FAILURE_COUNT)"
fail_closed="$(num_for LOG_FAIL_CLOSED_COUNT)"
notify_success="$(num_for LOG_FAILURE_NOTIFY_SUCCESS_COUNT)"
if [[ "$auth_failures" -gt 0 && "$fail_closed" -gt 0 ]]; then
ok "scheduler has recent Drive auth/API failure fail-closed evidence"
else
warn "scheduler recent fail-closed evidence not observed in the last 8h"
fi
if [[ "$notify_success" -gt 0 ]]; then
ok "scheduler failure notification success evidence exists"
else
warn "scheduler failure notification success evidence not observed in the last 8h"
fi
local_source_count="$(num_for LOCAL_EXACT_DAILY_SOURCE_COUNT)"
local_source_latest="$(value_for LOCAL_EXACT_DAILY_SOURCE_LATEST)"
if [[ "$local_source_count" -gt 0 ]]; then
warn "exact local daily-sales source candidates exist outside Drive intake: count=$local_source_count latest=${local_source_latest:-unknown}"
else
ok "no exact local daily-sales source candidate found on 188 / backup paths"
fi
import_config="$(value_for IMPORT_CONFIG)"
[[ "$import_config" == *"當日業績匯入|即時業績_當日"* ]] && ok "Drive import config points to expected daily-sales intake" || blocked "Drive import config is unavailable or drifted: ${import_config:-missing}"
monthly_sync="$(value_for DB_MONTHLY_SYNC)"
IFS='|' read -r sync_snapshot_count sync_monthly_count sync_dmin sync_dmax sync_mmin sync_mmax <<<"$monthly_sync"
if [[ "$sync_snapshot_count" =~ ^[0-9]+$ && "$sync_snapshot_count" -gt 0 && "$sync_snapshot_count" == "$sync_monthly_count" && "$sync_dmin" == "$sync_mmin" && "$sync_dmax" == "$sync_mmax" ]]; then
ok "current-month daily snapshot and realtime tables are in sync"
else
blocked "current-month daily snapshot and realtime sync is not proven: ${monthly_sync:-missing}"
fi
freshness="$(value_for DB_DAILY_FRESHNESS)"
IFS='|' read -r freshness_days latest_daily_date <<<"$freshness"
if [[ "$freshness_days" =~ ^[0-9]+$ && "$freshness_days" -le "$FRESHNESS_MAX_DAYS" ]]; then
ok "daily sales data freshness is within ${FRESHNESS_MAX_DAYS} days: $freshness"
elif [[ "$freshness_days" =~ ^[0-9]+$ ]]; then
blocked "daily sales data is stale: $freshness"
else
blocked "daily sales freshness is unavailable: ${freshness:-missing}"
fi
latest_job="$(value_for DB_LATEST_DAILY_IMPORT_JOB)"
IFS='|' read -r job_id job_status job_file job_created job_completed job_total job_success job_errors <<<"$latest_job"
if [[ "$job_id" =~ ^[0-9]+$ && "$job_status" == "completed" && "$job_total" == "$job_success" && "$job_errors" == "0" ]]; then
ok "latest daily import job completed cleanly: id=$job_id file=$job_file"
else
warn "latest daily import job is not a clean completed job: ${latest_job:-missing}"
fi
if [[ "$freshness_days" =~ ^[0-9]+$ && "$freshness_days" -gt "$FRESHNESS_MAX_DAYS" ]]; then
if [[ "$auth_failures" -gt 0 ]]; then
blocked "release blocker is stale business data with active Drive auth/source evidence gate"
else
blocked "release blocker is stale business data; source evidence must be refreshed"
fi
fi
printf 'MOMO_DRIVE_TOKEN_SOURCE_PREFLIGHT PASS=%d WARN=%d BLOCKED=%d HOST=%s FRESHNESS_MAX_DAYS=%s\n' \
"$PASS_COUNT" "$WARN_COUNT" "$BLOCKED_COUNT" "$MOMO_HOST" "$FRESHNESS_MAX_DAYS"
if [[ "$BLOCKED_COUNT" -gt 0 ]]; then
exit 2
fi
if [[ "$WARN_COUNT" -gt 0 ]]; then
exit 1
fi
exit 0

View File

@@ -0,0 +1,309 @@
#!/usr/bin/env bash
set -uo pipefail
# One-entry read-only post-reboot check. This wrapper intentionally delegates
# deep checks to the existing recovery scripts and does not restart, patch,
# delete, import, reload, or write runtime state.
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SSH_CONNECT_TIMEOUT="${SSH_CONNECT_TIMEOUT:-6}"
RUN_COLD_START=1
RUN_MOMO=1
RUN_BACKUP=1
RUN_ROUTES=1
RUN_CPU=1
NO_COLOR_FLAG=0
PASS_COUNT=0
WARN_COUNT=0
BLOCKED_COUNT=0
SERVICE_WARN_COUNT=0
BOUNDARY_WARN_COUNT=0
EVIDENCE_WARN_COUNT=0
HOSTS=(
"192.168.0.110"
"192.168.0.120"
"192.168.0.121"
"192.168.0.188"
)
ROUTES=(
"https://awoooi.wooo.work/api/v1/health"
"https://awoooi.wooo.work/zh-TW/iwooos"
"https://mo.wooo.work/health"
"https://stock.wooo.work/"
)
usage() {
cat <<'USAGE'
Usage: post-start-quick-check.sh [options]
Read-only post-reboot quick check for 110 / 120 / 121 / 188.
Options:
--skip-cold-start Do not run full-stack-cold-start-check.sh.
--skip-momo Do not run momo-drive-token-source-recovery-preflight.sh.
--skip-backup Do not run /backup/scripts/backup-status.sh on 110.
--skip-routes Do not curl public route smoke targets.
--skip-cpu Do not read 110 CPU / process summary.
--no-color Disable ANSI color.
-h, --help Show this help.
Exit codes:
0 = no service blockers. Boundary / evidence warnings may still be present.
1 = service warnings only.
2 = service blockers observed.
This script never reads token content and never writes runtime state.
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--skip-cold-start)
RUN_COLD_START=0
;;
--skip-momo)
RUN_MOMO=0
;;
--skip-backup)
RUN_BACKUP=0
;;
--skip-routes)
RUN_ROUTES=0
;;
--skip-cpu)
RUN_CPU=0
;;
--no-color)
NO_COLOR_FLAG=1
;;
-h|--help)
usage
exit 0
;;
*)
printf 'Unknown argument: %s\n' "$1" >&2
usage >&2
exit 2
;;
esac
shift
done
if [[ -n "${NO_COLOR:-}" || "$NO_COLOR_FLAG" -eq 1 ]]; then
RED=""
GREEN=""
YELLOW=""
BLUE=""
NC=""
else
RED=$'\033[0;31m'
GREEN=$'\033[0;32m'
YELLOW=$'\033[1;33m'
BLUE=$'\033[0;34m'
NC=$'\033[0m'
fi
section() {
printf '\n%s=== %s ===%s\n' "$BLUE" "$1" "$NC"
}
ok() {
PASS_COUNT=$((PASS_COUNT + 1))
printf '%sOK%s %s\n' "$GREEN" "$NC" "$*"
}
warn() {
WARN_COUNT=$((WARN_COUNT + 1))
printf '%sWARN%s %s\n' "$YELLOW" "$NC" "$*"
}
service_warn() {
SERVICE_WARN_COUNT=$((SERVICE_WARN_COUNT + 1))
warn "$@"
}
boundary_warn() {
BOUNDARY_WARN_COUNT=$((BOUNDARY_WARN_COUNT + 1))
warn "$@"
}
evidence_warn() {
EVIDENCE_WARN_COUNT=$((EVIDENCE_WARN_COUNT + 1))
warn "$@"
}
blocked() {
BLOCKED_COUNT=$((BLOCKED_COUNT + 1))
printf '%sBLOCKED%s %s\n' "$RED" "$NC" "$*"
}
ssh_read() {
local user_host="$1"
local command="$2"
ssh -o BatchMode=yes -o ConnectTimeout="$SSH_CONNECT_TIMEOUT" "$user_host" "$command"
}
run_and_capture() {
local label="$1"
shift
local tmp
tmp="$(mktemp -t post-start-quick-check.XXXXXX)"
if "$@" >"$tmp" 2>&1; then
ok "$label"
cat "$tmp"
rm -f "$tmp"
return 0
fi
local rc=$?
cat "$tmp"
rm -f "$tmp"
return "$rc"
}
section "主機 / SSH"
for host in "${HOSTS[@]}"; do
if ping -c 1 -W 1 "$host" >/dev/null 2>&1; then
ok "PING_OK $host"
else
blocked "PING_FAIL $host"
fi
if nc -z -w 2 "$host" 22 >/dev/null 2>&1; then
ok "SSH_PORT_OK $host"
else
blocked "SSH_PORT_FAIL $host"
fi
done
if [[ "$RUN_COLD_START" -eq 1 ]]; then
section "Cold-start scorecard"
cold_tmp="$(mktemp -t post-start-cold-start.XXXXXX)"
if bash "$ROOT_DIR/scripts/reboot-recovery/full-stack-cold-start-check.sh" --monitor-read-only --no-color --watch --interval 1 --max-attempts 1 >"$cold_tmp" 2>&1; then
ok "cold-start command exited 0"
else
blocked "cold-start command returned non-zero"
fi
cat "$cold_tmp"
cold_summary="$(grep -E 'PASS=[0-9]+ WARN=[0-9]+ BLOCKED=[0-9]+' "$cold_tmp" | tail -n 1 || true)"
if [[ -n "$cold_summary" ]]; then
ok "cold-start summary: $cold_summary"
else
service_warn "cold-start summary not found"
fi
rm -f "$cold_tmp"
fi
if [[ "$RUN_MOMO" -eq 1 ]]; then
section "MOMO freshness"
momo_tmp="$(mktemp -t post-start-momo.XXXXXX)"
bash "$ROOT_DIR/scripts/reboot-recovery/momo-drive-token-source-recovery-preflight.sh" >"$momo_tmp" 2>&1
momo_rc=$?
cat "$momo_tmp"
momo_summary="$(grep -E 'MOMO_DRIVE_TOKEN_SOURCE_PREFLIGHT PASS=[0-9]+ WARN=[0-9]+ BLOCKED=[0-9]+' "$momo_tmp" | tail -n 1 || true)"
case "$momo_rc" in
0)
ok "MOMO preflight clean"
;;
1)
if [[ "$momo_summary" =~ BLOCKED=0 ]]; then
evidence_warn "MOMO preflight has non-service warnings"
else
service_warn "MOMO preflight has warnings and no clean summary"
fi
;;
*)
blocked "MOMO preflight has blockers"
;;
esac
grep -E 'MOMO_DRIVE_TOKEN_SOURCE_PREFLIGHT|MOMO_HEALTH_VERSION|DB_MONTHLY_SYNC|DB_DAILY_FRESHNESS|DB_LATEST_DAILY_IMPORT_JOB' "$momo_tmp" || true
rm -f "$momo_tmp"
fi
if [[ "$RUN_BACKUP" -eq 1 ]]; then
section "Backup / offsite / escrow"
backup_tmp="$(mktemp -t post-start-backup.XXXXXX)"
if ssh_read "wooo@192.168.0.110" '/backup/scripts/backup-status.sh --no-notify --no-refresh' >"$backup_tmp" 2>&1; then
ok "backup-status readback succeeded"
else
blocked "backup-status readback failed"
fi
cat "$backup_tmp"
if grep -Eq 'core_blockers=0|CORE_BLOCKERS[ =]0' "$backup_tmp"; then
ok "backup core blockers are 0"
elif grep -Eq 'core_blockers=[1-9]|CORE_BLOCKERS[ =][1-9]' "$backup_tmp"; then
blocked "backup core blockers are non-zero"
else
service_warn "backup core blocker summary not confirmed"
fi
if grep -Eq 'escrow_missing=0|ESCROW_MISSING_COUNT[ =]0' "$backup_tmp"; then
ok "credential escrow missing is 0"
elif grep -Eq 'escrow_missing=[1-9]|ESCROW_MISSING_COUNT[ =][1-9]' "$backup_tmp"; then
boundary_warn "credential escrow still missing; DR_COMPLETE is forbidden"
else
evidence_warn "credential escrow count not found"
fi
rm -f "$backup_tmp"
fi
if [[ "$RUN_ROUTES" -eq 1 ]]; then
section "Public routes"
for url in "${ROUTES[@]}"; do
code="$(curl -k -sS -o /dev/null -w '%{http_code}' --max-time 12 "$url" 2>/dev/null || true)"
case "$code" in
2*|3*)
ok "$code $url"
;;
*)
blocked "${code:-curl_failed} $url"
;;
esac
done
fi
if [[ "$RUN_CPU" -eq 1 ]]; then
section "110 CPU / process attribution"
cpu_tmp="$(mktemp -t post-start-cpu.XXXXXX)"
if ssh_read "wooo@192.168.0.110" 'uptime; vmstat 1 5; ps -eo pid,ppid,pgid,stat,pcpu,pmem,comm,args --sort=-pcpu | head -25' >"$cpu_tmp" 2>&1; then
ok "110 CPU/process readback succeeded"
else
evidence_warn "110 CPU/process readback failed"
fi
cat "$cpu_tmp"
if grep -Eiq 'chrome|chromium|playwright' "$cpu_tmp"; then
evidence_warn "browser/smoke process is visible; classify orphan vs active parent before action"
fi
if grep -Eiq 'gitea|actions|runner|npm|pnpm|pytest|pip-audit' "$cpu_tmp"; then
ok "active CI/build/test load is visible"
fi
rm -f "$cpu_tmp"
fi
section "總結"
printf 'POST_START_QUICK_CHECK PASS=%s WARN=%s BLOCKED=%s\n' "$PASS_COUNT" "$WARN_COUNT" "$BLOCKED_COUNT"
printf 'POST_START_QUICK_CHECK_WARNINGS SERVICE=%s BOUNDARY=%s EVIDENCE=%s\n' "$SERVICE_WARN_COUNT" "$BOUNDARY_WARN_COUNT" "$EVIDENCE_WARN_COUNT"
if [[ "$BLOCKED_COUNT" -gt 0 ]]; then
printf 'RESULT=BLOCKED\n'
exit 2
fi
if [[ "$SERVICE_WARN_COUNT" -gt 0 ]]; then
printf 'RESULT=DEGRADED\n'
exit 1
fi
if [[ "$BOUNDARY_WARN_COUNT" -gt 0 ]]; then
printf 'RESULT=FULL_STACK_GREEN_DR_ESCROW_BLOCKED\n'
exit 0
fi
if [[ "$EVIDENCE_WARN_COUNT" -gt 0 ]]; then
printf 'RESULT=GREEN_WITH_EVIDENCE_WARNINGS\n'
exit 0
fi
printf 'RESULT=GREEN\n'
exit 0

View File

@@ -0,0 +1,298 @@
#!/usr/bin/env python3
"""GitHub 私有備援 evidence gate。
此工具只讀既有 GitHub target decision / owner response / approval package
snapshot產生「私有備援是否可進入執行」的 fail-closed gate。它不呼叫
GitHub / Gitea API、不建立 repo、不修改 visibility、不同步 refs、不讀取
或保存任何 secret value。
"""
from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
SCHEMA_VERSION = "github_target_private_backup_evidence_gate_v1"
FORBIDDEN_ACTIONS = [
"create_github_repo",
"change_repo_visibility",
"push_refs",
"delete_refs",
"force_push",
"mirror_sync",
"switch_github_primary",
"disable_gitea",
"workflow_modification",
"workflow_trigger",
"secret_value_collection",
"private_clone_url_collection",
]
def load_json(path: Path) -> dict[str, Any]:
with path.open(encoding="utf-8") as handle:
payload = json.load(handle)
if not isinstance(payload, dict):
raise ValueError(f"{path}: expected JSON object")
return payload
def build_target_gate(decision: dict[str, Any]) -> dict[str, Any]:
approval_required = bool(decision.get("approval_required"))
probe_status = str(decision.get("probe_status") or "unknown")
github_repo = str(decision.get("github_repo") or "")
if not approval_required:
visibility_status = "external_scope_not_backup_target"
blockers = ["external_scope_review_only"]
elif probe_status.startswith("exists"):
visibility_status = "blocked_public_probe_visible_private_evidence_required"
blockers = [
"github_target_publicly_readable_by_unauthenticated_probe",
"private_visibility_owner_evidence_missing",
"safe_credential_metadata_missing",
"refs_sync_not_authorized",
]
elif probe_status == "not_found_or_private":
visibility_status = "blocked_private_or_absent_not_verified"
blockers = [
"not_found_or_private_is_not_private_verification",
"private_visibility_owner_evidence_missing",
"safe_credential_metadata_missing",
"refs_sync_not_authorized",
]
else:
visibility_status = "blocked_probe_status_unknown"
blockers = [
"github_target_probe_status_unknown",
"private_visibility_owner_evidence_missing",
"safe_credential_metadata_missing",
"refs_sync_not_authorized",
]
return {
"github_repo": github_repo,
"source_key": decision.get("source_key"),
"approval_required": approval_required,
"probe_status": probe_status,
"target_state": decision.get("target_state"),
"risk": decision.get("risk"),
"visibility_evidence_status": visibility_status,
"private_backup_verified": False,
"private_visibility_owner_evidence_ref": None,
"safe_credential_evidence_status": (
"not_required_external_scope" if not approval_required else "missing_safe_credential_metadata"
),
"safe_credential_evidence_ref": None,
"owner_response_accepted": False,
"refs_sync_ready": False,
"execution_ready": False,
"blockers": blockers,
"evidence_refs": decision.get("evidence_refs", []),
"forbidden_actions": FORBIDDEN_ACTIONS,
"repo_creation_authorized": False,
"visibility_change_authorized": False,
"refs_sync_authorized": False,
"github_primary_switch_authorized": False,
"secret_values_collected": False,
}
def count_targets(targets: list[dict[str, Any]], predicate) -> int:
return sum(1 for target in targets if predicate(target))
def build_payload(
decision_snapshot: dict[str, Any],
owner_response_snapshot: dict[str, Any],
approval_package_snapshot: dict[str, Any],
) -> dict[str, Any]:
decisions = decision_snapshot.get("decisions") or []
if not isinstance(decisions, list):
raise ValueError("github target decision snapshot missing decisions")
targets = [build_target_gate(decision) for decision in decisions if isinstance(decision, dict)]
approval_targets = [target for target in targets if target["approval_required"]]
public_visible_targets = [
target for target in approval_targets if str(target["probe_status"]).startswith("exists")
]
unknown_private_targets = [
target for target in approval_targets if target["probe_status"] == "not_found_or_private"
]
owner_summary = owner_response_snapshot.get("summary") or {}
package_items = approval_package_snapshot.get("approval_items") or []
received_count = int(owner_summary.get("received_response_count", 0) or 0)
accepted_count = int(owner_summary.get("accepted_response_count", 0) or 0)
status = "blocked_public_visibility_and_safe_credential_evidence_required"
if not public_visible_targets:
status = "blocked_private_visibility_and_safe_credential_evidence_required"
summary = {
"target_decision_count": len(targets),
"approval_required_target_count": len(approval_targets),
"approval_package_item_count": len(package_items),
"public_probe_visible_target_count": len(public_visible_targets),
"not_found_or_private_target_count": len(unknown_private_targets),
"private_backup_verified_count": 0,
"private_visibility_evidence_missing_count": len(approval_targets),
"safe_credential_required_count": len(approval_targets),
"safe_credential_accepted_evidence_count": 0,
"owner_response_received_count": received_count,
"owner_response_accepted_count": accepted_count,
"execution_ready_count": 0,
"blocked_target_count": len(approval_targets),
"external_scope_target_count": count_targets(targets, lambda target: not target["approval_required"]),
"forbidden_action_count": len(FORBIDDEN_ACTIONS),
"repo_creation_authorized": False,
"visibility_change_authorized": False,
"refs_sync_authorized": False,
"github_primary_switch_authorized": False,
"workflow_modification_authorized": False,
"workflow_trigger_authorized": False,
"secret_value_collection_allowed": False,
"private_clone_url_collection_allowed": False,
"not_found_or_private_as_absent_allowed": False,
"public_repo_allowed": False,
}
return {
"schema_version": SCHEMA_VERSION,
"generated_at": datetime.now(timezone.utc).isoformat(),
"status": status,
"mode": "read_only_private_backup_evidence_gate",
"source_reviews": {
"github_target_decision": "docs/security/github-target-decision.snapshot.json",
"github_target_owner_decision_response": "docs/security/github-target-owner-decision-response.snapshot.json",
"github_target_repo_approval_package": "docs/security/github-target-repo-approval-package.snapshot.json",
},
"summary": summary,
"targets": targets,
"acceptance_requirements": [
"每個 approval-required GitHub target 必須有 private visibility owner evidence ref。",
"公開 probe 可讀的 target 不得被視為符合私有備援要求。",
"`not_found_or_private` 只代表未授權只讀 probe 看不到,不得當成 private verified 或 repo absent。",
"safe credential evidence 只允許 credential storage / owner / scope / rotation metadata不得收 token value。",
"owner response accepted count 在 reviewer acceptance 前必須維持 0。",
"private evidence 與 safe credential evidence 完整前不得建立 repo、改 visibility、push refs 或切 GitHub primary。",
],
"rejection_rules": [
"任何 public repo 或 unauthenticated readable target 均不得標示 private_backup_verified=true。",
"任何 token、PAT、private key、cookie、session、private clone credential 或 partial secret 必須拒收。",
"任何 repo creation、visibility change、refs sync、force push、tag rewrite、workflow trigger 或 primary switch request 必須拒收。",
"任何把 `not_found_or_private` 解讀為 repo 不存在或可建立新 repo 的 response 必須拒收。",
],
"operation_boundaries": {
"read_only_api_allowed": True,
"github_api_write_allowed": False,
"gitea_api_write_allowed": False,
"repo_creation_allowed": False,
"visibility_change_allowed": False,
"refs_sync_allowed": False,
"workflow_modification_allowed": False,
"workflow_trigger_allowed": False,
"github_primary_switch_allowed": False,
"secret_value_collection_allowed": False,
"private_clone_url_collection_allowed": False,
},
"authorization_flags": {
"runtime_execution_authorized": False,
"repo_creation_authorized": False,
"visibility_change_authorized": False,
"refs_sync_authorized": False,
"workflow_modification_authorized": False,
"workflow_trigger_authorized": False,
"github_primary_switch_authorized": False,
"secret_values_collected": False,
},
}
def write_markdown(payload: dict[str, Any], path: Path) -> None:
summary = payload["summary"]
lines = [
"# GitHub Target Private Backup Evidence Gate",
"",
"| 項目 | 值 |",
"|------|----|",
f"| 狀態 | `{payload['status']}` |",
f"| approval-required targets | `{summary['approval_required_target_count']}` |",
f"| public probe visible | `{summary['public_probe_visible_target_count']}` |",
f"| not_found_or_private | `{summary['not_found_or_private_target_count']}` |",
f"| private backup verified | `{summary['private_backup_verified_count']}` |",
f"| safe credential evidence | `{summary['safe_credential_accepted_evidence_count']}/{summary['safe_credential_required_count']}` |",
f"| execution ready | `{summary['execution_ready_count']}` |",
"",
"## Target Gate",
"",
"| GitHub target | probe | visibility evidence | private verified | blockers |",
"|---------------|-------|---------------------|------------------|----------|",
]
for target in payload["targets"]:
if not target["approval_required"]:
continue
lines.append(
"| "
+ " | ".join(
[
f"`{target['github_repo']}`",
f"`{target['probe_status']}`",
f"`{target['visibility_evidence_status']}`",
f"`{str(target['private_backup_verified']).lower()}`",
f"`{len(target['blockers'])}`",
]
)
+ " |"
)
lines.extend(
[
"",
"## 不可誤讀",
"",
"- 本 gate 不是 GitHub repo creation / visibility change / refs sync 授權。",
"- 公開 probe 可讀的 target 需要 private visibility owner evidence不能標綠。",
"- `not_found_or_private` 不能當成已 private也不能當成 repo 不存在。",
"- safe credential evidence 只收 metadata不收 secret value。",
"",
]
)
path.write_text("\n".join(lines), encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--root", default=".")
parser.add_argument("--output-json", default="docs/security/github-target-private-backup-evidence-gate.snapshot.json")
parser.add_argument("--output-md", default="docs/security/GITHUB-TARGET-PRIVATE-BACKUP-EVIDENCE-GATE.md")
args = parser.parse_args()
root = Path(args.root)
payload = build_payload(
load_json(root / "docs/security/github-target-decision.snapshot.json"),
load_json(root / "docs/security/github-target-owner-decision-response.snapshot.json"),
load_json(root / "docs/security/github-target-repo-approval-package.snapshot.json"),
)
output_json = root / args.output_json
output_md = root / args.output_md
output_json.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
write_markdown(payload, output_md)
summary = payload["summary"]
print(
"GITHUB_TARGET_PRIVATE_BACKUP_EVIDENCE_GATE_BLOCKED "
f"targets={summary['approval_required_target_count']} "
f"public_visible={summary['public_probe_visible_target_count']} "
f"private_verified={summary['private_backup_verified_count']} "
f"credential={summary['safe_credential_accepted_evidence_count']}/{summary['safe_credential_required_count']} "
f"refs_sync={summary['refs_sync_authorized']}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())