diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 80911b215..27d97b3ea 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -485,6 +485,7 @@ from src.services.telegram_alert_monitoring_live_receipt_apply import ( ) from src.services.windows99_vmx_source_repair_package import ( load_latest_windows99_vmx_source_repair_package, + mark_windows99_vmx_source_repair_public_redaction_boundary, ) router = APIRouter(prefix="/agents", tags=["Agent Teams"]) @@ -1521,7 +1522,10 @@ async def get_reboot_auto_recovery_slo_scorecard() -> dict[str, Any]: load_latest_stockplatform_public_api_runtime_readback ) apply_stockplatform_runtime_readback(payload, stockplatform_runtime) - return redact_public_lan_topology(payload) + redacted_payload = redact_public_lan_topology(payload) + return mark_windows99_vmx_source_repair_public_redaction_boundary( + redacted_payload + ) except FileNotFoundError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -1552,7 +1556,10 @@ async def get_windows99_vmx_source_repair_package() -> dict[str, Any]: payload = await asyncio.to_thread( load_latest_windows99_vmx_source_repair_package ) - return redact_public_lan_topology(payload) + redacted_payload = redact_public_lan_topology(payload) + return mark_windows99_vmx_source_repair_public_redaction_boundary( + redacted_payload + ) except FileNotFoundError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, diff --git a/apps/api/src/services/windows99_vmx_source_repair_package.py b/apps/api/src/services/windows99_vmx_source_repair_package.py index 53cd96731..7aac71d6f 100644 --- a/apps/api/src/services/windows99_vmx_source_repair_package.py +++ b/apps/api/src/services/windows99_vmx_source_repair_package.py @@ -47,6 +47,53 @@ def _with_ui_counts( return payload +def _mark_package_public_redaction_boundary( + package: dict[str, Any], +) -> dict[str, Any]: + payload = dict(package) + payload["public_lan_topology_redacted"] = True + payload["redacted_public_display_only"] = True + payload["check_mode_probe_commands_executable"] = False + payload["internal_console_package_required_for_execution"] = True + payload["public_execution_note"] = ( + "public_endpoint_redacts_lan_like_vmx_path_segments_use_authorized_" + "internal_console_package_before_executing_probe_commands" + ) + + vm_rows = payload.get("vm_rows") + if isinstance(vm_rows, list): + redacted_rows: list[Any] = [] + for row in vm_rows: + if isinstance(row, dict): + redacted_row = dict(row) + redacted_row["public_lan_topology_redacted"] = True + redacted_row["check_mode_probe_commands_executable"] = False + redacted_rows.append(redacted_row) + else: + redacted_rows.append(row) + payload["vm_rows"] = redacted_rows + return payload + + +def mark_windows99_vmx_source_repair_public_redaction_boundary( + payload: dict[str, Any], +) -> dict[str, Any]: + """Mark public-redacted VMX package fields as display-only evidence.""" + if payload.get("schema_version") == ( + "windows99_vmx_source_repair_check_mode_package_v1" + ): + return _mark_package_public_redaction_boundary(payload) + + package = payload.get("windows99_vmx_source_repair_package") + if isinstance(package, dict): + redacted_payload = dict(payload) + redacted_payload["windows99_vmx_source_repair_package"] = ( + _mark_package_public_redaction_boundary(package) + ) + return redacted_payload + return payload + + def _alias_lists_match(package: dict[str, Any], windows99: dict[str, Any]) -> bool: return _strings(package.get("missing_vmx_aliases")) == _strings( windows99.get("missing_vmx_aliases") diff --git a/apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py b/apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py index 60c545c63..ecf019b5f 100644 --- a/apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py +++ b/apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py @@ -1013,6 +1013,16 @@ def test_reboot_auto_recovery_slo_scorecard_endpoint_returns_readback(monkeypatc assert response.status_code == 200 payload = response.json() _assert_reboot_slo_payload(payload) + package = payload["windows99_vmx_source_repair_package"] + assert package["public_lan_topology_redacted"] is True + assert package["redacted_public_display_only"] is True + assert package["check_mode_probe_commands_executable"] is False + assert package["internal_console_package_required_for_execution"] is True + assert "192.168.0." not in response.text + assert "host:dev-a_Ubuntu_64-bit" in response.text + vm_rows = {row["alias"]: row for row in package["vm_rows"]} + assert vm_rows["111"]["public_lan_topology_redacted"] is True + assert vm_rows["111"]["check_mode_probe_commands_executable"] is False service_data_backup = payload["controlled_service_data_backup_readback"] assert service_data_backup["gitea_repo_bundle_backup_ready"] is True assert service_data_backup["gitea_repo_bundle_expected_repo_count"] == 12 diff --git a/apps/api/tests/test_windows99_vmx_source_repair_package_api.py b/apps/api/tests/test_windows99_vmx_source_repair_package_api.py index 8eb129f5c..fbd59efe7 100644 --- a/apps/api/tests/test_windows99_vmx_source_repair_package_api.py +++ b/apps/api/tests/test_windows99_vmx_source_repair_package_api.py @@ -86,4 +86,13 @@ def test_windows99_vmx_source_repair_package_endpoint_returns_readback( "check_mode_package_only_no_windows_or_vm_write" ) assert payload["apply_allowed_by_this_package"] is False + assert payload["public_lan_topology_redacted"] is True + assert payload["redacted_public_display_only"] is True + assert payload["check_mode_probe_commands_executable"] is False + assert payload["internal_console_package_required_for_execution"] is True + assert "192.168.0." not in response.text + assert "host:dev-a_Ubuntu_64-bit" in response.text + vm_rows = {row["alias"]: row for row in payload["vm_rows"]} + assert vm_rows["111"]["public_lan_topology_redacted"] is True + assert vm_rows["111"]["check_mode_probe_commands_executable"] is False assert payload["operation_boundaries"]["host_reboot_performed"] is False diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 76a98aa1e..c32eca07f 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -24,6 +24,8 @@ **已跑驗證**: - `python3.11 -m py_compile apps/api/src/services/reboot_auto_recovery_slo_scorecard.py apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py scripts/reboot-recovery/build-windows99-vmx-source-repair-package.py scripts/reboot-recovery/tests/test_windows99_vmx_source_repair_package.py`:通過。 - `DATABASE_URL=postgresql+asyncpg://test:test@localhost/test PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py scripts/reboot-recovery/tests/test_windows99_vmx_source_repair_package.py -q -p no:cacheprovider`:`22 passed`。 +- 追加部署 readback:Gitea CD `#4567` 推出 deploy marker `ccc6741c7 chore(cd): deploy 091126e [skip ci]`,queue log 顯示 `job_succeeded=true`、`production_deploy_readback_matched=true`、`docker_timeout=false`。 +- Production `/api/v1/agents/reboot-auto-recovery-slo-scorecard` 已讀回 `has_vmx_package=true`、`package_status=check_mode_package_ready_windows99_vmx_source_repair_required`、`package_ready=true`、`package_apply_allowed=false`、`package_missing=["111"]`、`package_next=restore_windows99_missing_vmx_source_for_aliases_then_rerun_no_secret_collector_and_scorecard_no_vm_power_change`。 **仍維持**: - 未讀 secret / token / `.env` / raw sessions / SQLite / auth;未使用 GitHub / gh;未 workflow_dispatch;未啟動或關閉 VM;未重啟 host / service;未 Docker / Nginx / K3s / DB / firewall restart;未 DROP / TRUNCATE / restore / prune / delete / force push。 @@ -54202,3 +54204,24 @@ production browser smoke: **下一步**: - normal push 到 Gitea main;等待 CD deploy marker 後讀回 production `/api/v1/agents/awoooi-priority-work-order-readback`,目標是 summary / mainline_execution_state / rollups 都顯示 active P0 primary blocker、current phase、ETA / wait reason、next safe action 與 blocker count。 + +## 2026-07-03 — P0-006 Windows99 VMX package public redaction boundary + +**完成內容**: +- 修正 `/api/v1/agents/reboot-auto-recovery-slo-scorecard` 與 `/api/v1/agents/windows99-vmx-source-repair-package` 的 public readback:public response 經 LAN topology redaction 後,會標示 `public_lan_topology_redacted=true`、`redacted_public_display_only=true`、`check_mode_probe_commands_executable=false` 與 `internal_console_package_required_for_execution=true`。 +- `vm_rows[]` 同步標示 public redacted command 不可直接執行,避免把 `host:dev-a...` display alias 誤當 Windows console 的 `Test-Path` 真路徑。 +- SOP / cold-start SOP / 統帥插入需求台帳已沉澱 v5.7 / v1.114 規則:要執行 VMX probe 時,只能使用授權內部 console / no-secret package 保留的原始 Windows path;public endpoint 僅作瀏覽器、Telegram、UI 顯示與排序證據。 + +**本地驗證結果**: +- `DATABASE_URL=postgresql+asyncpg://test:test@localhost/test PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_windows99_vmx_source_repair_package_api.py apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py apps/api/tests/test_awoooi_priority_work_order_readback_api.py -q -p no:cacheprovider`:`40 passed`。 +- `python3.11 -m py_compile apps/api/src/services/windows99_vmx_source_repair_package.py apps/api/src/api/v1/agents.py apps/api/tests/test_windows99_vmx_source_repair_package_api.py apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py`:通過。 +- `python3.11 ops/runner/guard-gitea-runner-pressure.py --root .`:`GITEA_RUNNER_PRESSURE_GUARD_OK`。 +- `git diff --check`:通過。 + +**仍維持**: +- 沒有讀 secret / token / `.env` / raw sessions / SQLite / auth。 +- 沒有使用 GitHub / gh / GitHub API / GitHub Actions。 +- 沒有重啟主機,沒有 Docker / Nginx / K3s / DB / firewall restart,沒有 workflow_dispatch,沒有 DROP / TRUNCATE / restore / prune,沒有啟動或關閉 VM。 + +**下一步**: +- 通過 focused verifier 後 commit / push 到 Gitea main;等待 CD deploy marker 後讀回兩個 production API route,確認 public package 帶 `check_mode_probe_commands_executable=false`,且 SLO 仍以 111 VMX source / guest power 等 active blocker 為下一步,不宣稱 10 分鐘 SLA 已完成。 diff --git a/docs/runbooks/FULL-STACK-COLD-START-SOP.md b/docs/runbooks/FULL-STACK-COLD-START-SOP.md index 60fcaa7f1..8e3f4e816 100644 --- a/docs/runbooks/FULL-STACK-COLD-START-SOP.md +++ b/docs/runbooks/FULL-STACK-COLD-START-SOP.md @@ -1,6 +1,6 @@ # AWOOOI 全棧冷啟動與主機重啟 SOP -> Version: v1.113 +> Version: v1.114 > Last updated: 2026-07-03 Asia/Taipei > Scope: 99 / 110 / 111 / 112 / 120 / 121 / 188 全棧重啟恢復。112 仍是 Kali / VM guest 訊號,但 2026-06-30 全主機重啟後已納入 10 分鐘 SLO 的必要 boot / power signal;此納入不代表授權任何破壞性 runtime apply。 @@ -58,6 +58,8 @@ v1.112 current production SLO readback anchor:2026-07-03 10:50 production `/ap v1.113 Windows99 VMX source repair package API rule:production `/api/v1/agents/reboot-auto-recovery-slo-scorecard` 必須直接輸出 `windows99_vmx_source_repair_package`,並在 `readback` / `rollups` 同步 `windows99_vmx_source_repair_package_ready`、`windows99_vmx_source_repair_package_status`、`windows99_vmx_source_repair_package_safe_next_step`、`windows99_vmx_source_repair_package_apply_allowed` 與 `windows99_vmx_source_repair_missing_vmx_aliases`。package 只從已讀回的 Windows99 verifier / Prometheus runtime overlay 建立,Prometheus overlay 後必須重建,避免 committed snapshot 與 live metric 不一致。111 VMX missing 時,package 必須列出 expected path `D:\Documents\Virtual Machines\192.168.0.111_Ubuntu_64-bit\192.168.0.111_Ubuntu_64-bit.vmx` 與 `Test-Path -LiteralPath ...` check-mode probe;`apply_allowed_by_this_package=false` 固定不變。此 package 是 work item / Telegram / UI 的 next-action input,不是 runtime apply 授權;不得從此 package 直接做 VM power change、Windows service restart、registry apply、scheduled task modify、host reboot 或 secret/password 讀取。 +v1.114 Windows99 VMX package public redaction boundary rule:public `/api/v1/agents/reboot-auto-recovery-slo-scorecard` 與 `/api/v1/agents/windows99-vmx-source-repair-package` 會先套用 LAN topology redaction,因此 VMX path 內的 `192.168.0.111` 可能在 public JSON 變成 `host:dev-a` 類 display alias。public response 必須同步輸出 `public_lan_topology_redacted=true`、`redacted_public_display_only=true`、`check_mode_probe_commands_executable=false` 與 `internal_console_package_required_for_execution=true`;所有 `vm_rows[]` 也必須帶相同 executable=false 邊界。任何 `host:...` VMX path 只能供瀏覽器 / Telegram / UI 顯示,不得作為 Windows console 可執行命令。要跑 `Test-Path` 時只能使用授權內部 console / no-secret package 保留的原始 Windows path;仍不得讀密碼、啟動 VM、重啟 service / host 或套用 registry。 + 2026-07-02 110 control-path / Harbor recovery receipt rule:若 Gitea Harbor repair queue 仍保留 `harbor_110_remote_ssh_publickey_auth_stalled`、remote-control unavailable、jobs stale 或 historical failure,但同一輪本地證據同時證明 `wooo` command path ready、110 local Harbor `/v2/` ready、public/internal registry `/v2/` 回 `401`,則該 Gitea Harbor repair 失敗只能列為 historical queue metadata,不得再當成 current SSH blocker。必須用 `/api/v1/agents/harbor-registry-controlled-recovery-receipt` 或同等 validator 合併 `diagnose-110-ssh-publickey-auth.sh`、`recover-110-control-path-and-harbor-local.sh --check`、public Gitea queue readback 與 registry `/v2/` verifier,並把機器可讀結果寫入 `docs/operations/harbor-110-control-path-recovery-readback-2026-07-02.snapshot.json` 類型的 snapshot。2026-07-02 live receipt 顯示:public/internal registry `/v2/` 均為 `401`、latest visible CD `#4335` 為 `Success`、Gitea Harbor repair failure 已是 `historical_after_latest_cd_success=true`;active blockers 收斂為 110 controlled CD lane config / binary / registration / service guardrail、active action container pressure,以及 Gitea CD jobs head-SHA / stale readback mismatch。若 local-console output 只有 `AWOOOI_110_CONTROLLED_CD_LANE_READY` marker,non110 runner parser 不得從 110 `BLOCKER` 行推導 non110 blocker;non110 只有看到 `AWOOOI_NON110_RUNNER_READY` marker 才能列入 active blocker。 2026-07-02 110 controlled CD lane fail-closed enforcer staging rule:110 runner 壓力事故後,legacy / generic runner 仍必須 fail-closed;但 `awoooi-cd-lane-drain.service` 的非 secret staging artifact 不得再被 enforcer 無差別封回 stub。`scripts/reboot-recovery/enforce-110-runner-failclosed.sh` 只有在 `config.yaml` 符合 `capacity <= 1`、只含 `awoooi-host:host` 與 `awoooi-ubuntu:docker://192.168.0.110:5000/awoooi/ci-runner:act-22.04`、binary 是 executable ELF、systemd unit 具備 `ConditionPathExists=/home/wooo/awoooi-cd-lane-drain/data/.runner`、`CPUAccounting` / `MemoryAccounting` / `TasksAccounting` / `NoNewPrivileges` 等 guardrail,且 service `inactive`、`MainPID=0`、未 enabled / 未 masked 時,才可保留 drain config / binary / unit,並輸出 `CONTROLLED_DRAIN_STAGING_ALLOWED=1` 與 textfile metric。此 staging 規則不得讀 token、不得讀 `.runner` 內容、不得註冊 runner、不得啟動 service;若 registration 缺失,readiness verifier 仍必須只留下 `controlled_cd_lane_registration_missing` / `controlled_cd_lane_service_not_active` 類 blocker。若 `CONTROLLED_DRAIN_STAGING_ALLOWED=0` 且 config / binary 又被搬走,優先修 source enforcer / unit guardrail,不要手工反覆補同一組 artifact。 diff --git a/docs/runbooks/REBOOT-RECOVERY-SOP.md b/docs/runbooks/REBOOT-RECOVERY-SOP.md index 03a4d78c0..47e73611b 100644 --- a/docs/runbooks/REBOOT-RECOVERY-SOP.md +++ b/docs/runbooks/REBOOT-RECOVERY-SOP.md @@ -1,9 +1,9 @@ # AWOOOI 重開機恢復 SOP -> **版本**: v5.6 +> **版本**: v5.7 > **最後更新**: 2026-07-03 (台北時間) > **更新者**: Codex -> **觸發事件**: 2026-07-03 10:58 production SLO scorecard:Windows99 VMX source repair check-mode package 已接入 API readback,10 分鐘 SLO 仍 blocked +> **觸發事件**: 2026-07-03 11:35 production SLO scorecard:Windows99 VMX source repair public endpoint 已補 redaction execution boundary,10 分鐘 SLO 仍 blocked --- @@ -44,7 +44,7 @@ - 188 可達,但 `systemd_state=degraded` 且 `awoooi-startup.service failed`。 - `reboot_detected=false`、`fresh_boot_hosts=[]`,因此 10 分鐘 SLO 尚未證明。 -固定下一步:`restore_windows99_missing_vmx_source_for_aliases_then_rerun_no_secret_collector_and_scorecard_no_vm_power_change`。先讀 production scorecard 的 `windows99_vmx_source_repair_package`,確認 111 expected VMX path 與 `Test-Path -LiteralPath ...` check-mode probe;再恢復 111 VMX source 與 99 no-secret management / console Verify stdout,使 `windows99-vmware-autostart.ps1 -Mode Verify` 產生 normalized artifact;同時補 111 reachability,再重跑 host probe、reboot-event detector、SLO scorecard。禁止讀 Windows 密碼、啟動 / 關閉 VM、host reboot、service restart、Docker / Nginx / K3s / DB / firewall restart、restore、prune、delete。 +固定下一步:`restore_windows99_missing_vmx_source_for_aliases_then_rerun_no_secret_collector_and_scorecard_no_vm_power_change`。先讀 production scorecard 的 `windows99_vmx_source_repair_package`,確認 111 expected VMX path 與 `Test-Path -LiteralPath ...` check-mode probe;再恢復 111 VMX source 與 99 no-secret management / console Verify stdout,使 `windows99-vmware-autostart.ps1 -Mode Verify` 產生 normalized artifact;同時補 111 reachability,再重跑 host probe、reboot-event detector、SLO scorecard。若 public API response 顯示 `public_lan_topology_redacted=true` 或 `check_mode_probe_commands_executable=false`,其中 `host:...` VMX path 只能當 public display evidence,不得直接貼到 Windows console 執行;要執行 probe 時必須使用授權內部 console / no-secret package 保留的原始 Windows path。禁止讀 Windows 密碼、啟動 / 關閉 VM、host reboot、service restart、Docker / Nginx / K3s / DB / firewall restart、restore、prune、delete。 ### 五主機全貌 diff --git a/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md b/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md index 78155cc05..293d0a43f 100644 --- a/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md +++ b/docs/workplans/2026-07-02-commander-inserted-requirements-priority-ledger.md @@ -59,7 +59,7 @@ | --- | --- | --- | --- | --- | --- | --- | | 1 | CIR-P0-RBT-001 | P0 | 「主機重啟後 10 分鐘內全部恢復,且要自動判斷所有主機被重啟」 | 建立 99/110/111/112/120/121/188 reboot event detector + 10 分鐘 SLO scorecard + fixed triage order | 2026-07-03 10:50 production scorecard:readiness `60%`、active blockers `10`、`can_claim_all_services_recovered_within_target=false`;blockers 已含 reboot event / host unreachable / uptime unknown / Windows99 VMX / guest power / VMware config / Windows Update policy;artifact `/tmp/awoooi-reboot-continue-20260703-085205`:111 不可達、99 uptime unknown、188 startup failed/degraded;09:22 已把 unreliable console artifact 投影成 machine-readable blocker | 優先收斂 111 VMX source、99 local console Verify output / no-secret management channel 與 111 reachability;不可宣稱 10 分鐘 SLA 已證明,不得用 RDP clipboard 片段當完成證據 | | 2 | CIR-P0-RBT-002 | P0 | 「沒有偵測到主機重啟」 | 修正 host reboot/shutdown/up detection:boot_id / uptime / node exporter / Windows exporter / VMware VM power state 都要進同一事件 | Scorecard 已接 collection packet + management probe;08:23 host probe 有 7 列但 99 只有 ping/RDP reachable、uptime unknown,111 unreachable,112/120/121/188 uptime 都已超過 10 分鐘窗口 | 讓 99 verifier / Windows exporter 或等效 no-secret readback 進入 host boot event,並補 111 reachability 證據 | -| 3 | CIR-P0-RBT-003 | P0 | 「192.168.0.99 VMWare 要自動啟動,裡面 111/188/120/121/112 也自動啟動」 | Windows 99 VMware host autostart + guest VM autostart contract;VM host 111/188/120/121/112 開機順序與 readback | Source verifier / parser / API readback / collection packet 已完成;10:58 API scorecard 已接入 `windows99_vmx_source_repair_package`,會把 111 VMX missing 轉成 expected path / `Test-Path` check-mode probe;10:15 source/API patch 已把 nested `service_blockers=["VMAuthdService","VMnetDHCP"]` 與 VMX / power evidence 提升到 active blocker matrix;09:53 management probe 讀回 99 host reachable、RDP / Hyper-V VMConnect reachable,但 SSH BatchMode `permission_denied`、WinRM timeout,`console_artifact_status=blocked_clipboard_unreliable` | 先依 API package 修 111 VMX source,再處理 VMware service / autostart task check-mode package,重跑 no-secret collector 與 scorecard;不得從 scorecard 直接啟動 / 關閉 VM、重啟 Windows service、讀密碼或 host reboot | +| 3 | CIR-P0-RBT-003 | P0 | 「192.168.0.99 VMWare 要自動啟動,裡面 111/188/120/121/112 也自動啟動」 | Windows 99 VMware host autostart + guest VM autostart contract;VM host 111/188/120/121/112 開機順序與 readback | Source verifier / parser / API readback / collection packet 已完成;10:58 API scorecard 已接入 `windows99_vmx_source_repair_package`,會把 111 VMX missing 轉成 expected path / `Test-Path` check-mode probe;11:35 public API 已補 `public_lan_topology_redacted=true` / `check_mode_probe_commands_executable=false`,避免把 public display alias 誤當 Windows console 可執行命令;10:15 source/API patch 已把 nested `service_blockers=["VMAuthdService","VMnetDHCP"]` 與 VMX / power evidence 提升到 active blocker matrix;09:53 management probe 讀回 99 host reachable、RDP / Hyper-V VMConnect reachable,但 SSH BatchMode `permission_denied`、WinRM timeout,`console_artifact_status=blocked_clipboard_unreliable` | 先用授權內部 console / no-secret package 保留的原始 Windows path 修 111 VMX source,再處理 VMware service / autostart task check-mode package,重跑 no-secret collector 與 scorecard;不得從 public redacted path 或 scorecard 直接啟動 / 關閉 VM、重啟 Windows service、讀密碼或 host reboot | | 4 | CIR-P0-RBT-004 | P0 | 「192.168.0.99 不可因 Windows Update 無預警重開」 | Windows Update reboot policy:active hours / no auto-restart / maintenance window / update notification audit | 10:15 source/API patch:若 nested verifier 回 `policy_blockers=["windows_update_policy_readback_missing"]`,API 必須 fail-closed 重新提升 `windows99_update_no_auto_reboot_policy_not_ready`;不得只信舊 Prometheus `windows99_update_no_auto_reboot_ready=true` | 補 no-secret policy readback / drift alert;若 policy 不綠,另走 controlled apply package + rollback + post-verifier,禁止要求或記錄 Windows 密碼,禁止由 scorecard 直接 apply registry | | 5 | CIR-P0-RBT-005 | P0 | 「網站重啟後 502 嚴重影響體驗,要維護頁,外部雲端或專業做法」 | Public maintenance fallback:Nginx / edge / external static maintenance page / status page / fail-open UX,避免 502 直出 | Source + runtime verifier 已實作;Gitea CD `#4459` Success、deploy marker `8d7a6faaf`,production API scorecard 已讀回 `public_maintenance_fallback.ready=true`、raw 5xx=`0`、unreachable without L1=`0`,P0 blockers `11`、readiness `47`;仍不可宣稱整體 10 分鐘 SLO 完成 | 若未來 public route 出現 raw 5xx,先走 L0 Nginx intercept / header verifier;若 edge unreachable,才產生 L1 external static origin / CDN decision record + rollback | | 6 | CIR-P0-RBT-006 | P0 | 「所有主機關機立刻 Telegram 告警,重啟後也要告警,其他告警一併完整思考」 | Down / shutdown suspected / reboot detected / reboot recovered / SLO missed / backup failed / freshness stale / CPU pressure / Gitea queue 告警矩陣 | HostDown / HostRebootEventDetected / RebootAutoRecoverySLOMissed 已存在;per-blocker reboot alerts 與 backup receipt rules 已 deploy/readback。Backup receipt 缺段已從 100 條 stage 噪音收斂為 110 / 188 兩條 host-level pending;仍需完整 shutdown/up E2E receipt | 補 Prometheus / Alertmanager active/resolved 與 outbound receipt;backup alert 先補 `/backup/alert-receipts/*.last_success` 脫敏 marker,不送測試 secret、不重啟主機 |