diff --git a/apps/api/src/services/awoooi_priority_work_order_readback.py b/apps/api/src/services/awoooi_priority_work_order_readback.py index 42e885eb0..99602cf67 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -2561,6 +2561,9 @@ def _enrich_from_current_readbacks(payload: dict[str, Any]) -> None: reboot_service_data_backup = _dict( reboot_slo.get("controlled_service_data_backup_readback") ) + windows99_verify_collection = _dict( + reboot_slo.get("windows99_verify_collection") + ) reboot_preflight_rollups = _dict(reboot_preflight.get("rollups")) reboot_preflight_target_selector = _dict(reboot_preflight.get("target_selector")) reboot_active_blockers = _strings(reboot_slo.get("active_blockers")) @@ -2570,6 +2573,9 @@ def _enrich_from_current_readbacks(payload: dict[str, Any]) -> None: service_data_backup_blocking_fields = _strings( reboot_service_data_backup.get("blocking_fields") ) + windows99_verify_collection_blockers = _strings( + windows99_verify_collection.get("collection_blockers") + ) p0_006_event_gated = bool( reboot_slo_rollups.get("blocked_by_fresh_reboot_window_only") is True ) @@ -2630,6 +2636,27 @@ def _enrich_from_current_readbacks(payload: dict[str, Any]) -> None: state["controlled_service_data_backup_next_safe_action"] = str( reboot_service_data_backup.get("next_safe_action") or "" ) + state["windows99_verify_collection_status"] = str( + windows99_verify_collection.get("status") or "unknown" + ) + state["windows99_verify_collection_can_collect_no_secret"] = bool( + windows99_verify_collection.get("can_collect_no_secret_verify") is True + ) + state["windows99_verify_collection_blocker_count"] = len( + windows99_verify_collection_blockers + ) + state["windows99_verify_collection_blockers"] = ( + windows99_verify_collection_blockers + ) + state["windows99_verify_collection_host99_reachable"] = bool( + windows99_verify_collection.get("host99_reachable") is True + ) + state["windows99_verify_collection_host99_uptime_known"] = bool( + windows99_verify_collection.get("host99_uptime_known") is True + ) + state["windows99_verify_collection_post_verifier"] = str( + windows99_verify_collection.get("post_verifier") or "" + ) state["stale_snapshot_or_old_cd_runs_must_not_reopen_closed_work"] = True state["p0_004_template_copy_apply_gate_production_http_status"] = 200 state["p0_004_template_copy_apply_gate_runtime_readback_state"] = ( @@ -2787,6 +2814,21 @@ def _enrich_from_current_readbacks(payload: dict[str, Any]) -> None: evidence["controlled_service_data_backup_next_safe_action"] = str( reboot_service_data_backup.get("next_safe_action") or "" ) + evidence["windows99_verify_collection_status"] = str( + windows99_verify_collection.get("status") or "unknown" + ) + evidence["windows99_verify_collection_can_collect_no_secret"] = bool( + windows99_verify_collection.get("can_collect_no_secret_verify") is True + ) + evidence["windows99_verify_collection_blockers"] = ( + windows99_verify_collection_blockers + ) + evidence["windows99_verify_collection_host99_reachable"] = bool( + windows99_verify_collection.get("host99_reachable") is True + ) + evidence["windows99_verify_collection_host99_uptime_known"] = bool( + windows99_verify_collection.get("host99_uptime_known") is True + ) evidence["drill_preflight_status"] = str(reboot_preflight.get("status") or "") evidence["drill_preflight_ready"] = ( reboot_preflight_rollups.get("preflight_ready") is True @@ -2848,10 +2890,12 @@ def _enrich_from_current_readbacks(payload: dict[str, Any]) -> None: "service, product-data, backup, 188 service, host reachability, " "disk, and Wazuh blockers in controlled lanes; keep the " "controlled service/data/backup readback visible as the source " - "selector for these blockers. For StockPlatform freshness/ingestion " - "postgres_not_ready, use the production migration/control-channel " - "path with target selector, dry-run, rollback, public API verifier, " - "and KM/RAG/MCP/PlayBook writeback. " + "selector for these blockers. For Windows 99, use the " + "windows99_verify_collection packet to collect no-secret Verify " + "stdout and rerun the scorecard. For StockPlatform " + "freshness/ingestion postgres_not_ready, use the production " + "migration/control-channel path with target selector, dry-run, " + "rollback, public API verifier, and KM/RAG/MCP/PlayBook writeback. " "Do not reboot, restart services, write DB rows, fake freshness, " "trigger workflows, or read secrets from this lane." ) @@ -3033,6 +3077,21 @@ def _set_rollups_and_summary( "controlled_service_data_backup_can_clear_blockers": ( state.get("controlled_service_data_backup_can_clear_blockers") is True ), + "windows99_verify_collection_status": str( + state.get("windows99_verify_collection_status") or "unknown" + ), + "windows99_verify_collection_can_collect_no_secret": ( + state.get("windows99_verify_collection_can_collect_no_secret") is True + ), + "windows99_verify_collection_blocker_count": _int( + state.get("windows99_verify_collection_blocker_count") + ), + "windows99_verify_collection_host99_reachable": ( + state.get("windows99_verify_collection_host99_reachable") is True + ), + "windows99_verify_collection_host99_uptime_known": ( + state.get("windows99_verify_collection_host99_uptime_known") is True + ), "p0_004_runtime_readback_ready": p0_004_ready, "reboot_drill_preflight_runtime_readback_ready": ( state.get("reboot_drill_preflight_runtime_readback_state") == "ready" @@ -3068,6 +3127,27 @@ def _set_rollups_and_summary( "controlled_service_data_backup_next_safe_action": str( state.get("controlled_service_data_backup_next_safe_action") or "" ), + "windows99_verify_collection_status": str( + state.get("windows99_verify_collection_status") or "unknown" + ), + "windows99_verify_collection_can_collect_no_secret": ( + state.get("windows99_verify_collection_can_collect_no_secret") is True + ), + "windows99_verify_collection_blocker_count": _int( + state.get("windows99_verify_collection_blocker_count") + ), + "windows99_verify_collection_blockers": _strings( + state.get("windows99_verify_collection_blockers") + ), + "windows99_verify_collection_host99_reachable": ( + state.get("windows99_verify_collection_host99_reachable") is True + ), + "windows99_verify_collection_host99_uptime_known": ( + state.get("windows99_verify_collection_host99_uptime_known") is True + ), + "windows99_verify_collection_post_verifier": str( + state.get("windows99_verify_collection_post_verifier") or "" + ), "latest_successful_deployed_source_sha": latest_source_sha, "latest_successful_deployed_source_short_sha": latest_source_sha[:10], "latest_successful_deploy_marker": str( diff --git a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py index 19b1a4568..554237e67 100644 --- a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py +++ b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py @@ -98,6 +98,36 @@ def test_awoooi_priority_work_order_readback_loader_returns_mainline_order(): assert payload["mainline_execution_state"][ "stale_snapshot_or_old_cd_runs_must_not_reopen_closed_work" ] is True + assert payload["mainline_execution_state"][ + "windows99_verify_collection_status" + ] == "blocked_windows99_verify_output_missing_host_reachable" + assert ( + payload["mainline_execution_state"][ + "windows99_verify_collection_can_collect_no_secret" + ] + is True + ) + assert payload["mainline_execution_state"][ + "windows99_verify_collection_blocker_count" + ] == 2 + assert payload["mainline_execution_state"][ + "windows99_verify_collection_blockers" + ] == [ + "windows99_vmware_autostart_readback_missing", + "windows99_uptime_unknown", + ] + assert ( + payload["mainline_execution_state"][ + "windows99_verify_collection_host99_reachable" + ] + is True + ) + assert ( + payload["mainline_execution_state"][ + "windows99_verify_collection_host99_uptime_known" + ] + is False + ) assert payload["next_execution_order"][0].startswith("P0-006:") in_progress = payload["in_progress_or_blocked_in_priority_order"][0] assert in_progress["workplan_id"] == "P0-006" @@ -136,6 +166,27 @@ def test_awoooi_priority_work_order_readback_loader_returns_mainline_order(): "stockplatform_freshness_status", "stockplatform_ingestion_status", ] + assert in_progress["evidence"]["windows99_verify_collection_status"] == ( + "blocked_windows99_verify_output_missing_host_reachable" + ) + assert ( + in_progress["evidence"][ + "windows99_verify_collection_can_collect_no_secret" + ] + is True + ) + assert in_progress["evidence"]["windows99_verify_collection_blockers"] == [ + "windows99_vmware_autostart_readback_missing", + "windows99_uptime_unknown", + ] + assert ( + in_progress["evidence"]["windows99_verify_collection_host99_reachable"] + is True + ) + assert ( + in_progress["evidence"]["windows99_verify_collection_host99_uptime_known"] + is False + ) assert ( in_progress["evidence"][ "controlled_service_data_backup_can_clear_blockers" @@ -156,6 +207,9 @@ def test_awoooi_priority_work_order_readback_loader_returns_mainline_order(): assert "controlled service/data/backup readback visible" in in_progress[ "professional_fix" ]["action"] + assert "windows99_verify_collection packet" in in_progress[ + "professional_fix" + ]["action"] assert "production migration/control-channel" in in_progress[ "professional_fix" ]["action"] @@ -180,6 +234,23 @@ def test_awoooi_priority_work_order_readback_loader_returns_mainline_order(): "collect_public_service_data_backup_readback_and_refresh_reboot_slo_" "scorecard_no_reboot_no_db_write" ) + assert payload["summary"]["windows99_verify_collection_status"] == ( + "blocked_windows99_verify_output_missing_host_reachable" + ) + assert ( + payload["summary"]["windows99_verify_collection_can_collect_no_secret"] + is True + ) + assert payload["summary"]["windows99_verify_collection_blocker_count"] == 2 + assert payload["summary"]["windows99_verify_collection_blockers"] == [ + "windows99_vmware_autostart_readback_missing", + "windows99_uptime_unknown", + ] + assert payload["rollups"]["windows99_verify_collection_blocker_count"] == 2 + assert ( + payload["rollups"]["windows99_verify_collection_host99_reachable"] + is True + ) assert payload["operation_boundaries"]["github_api_used"] is False assert payload["operation_boundaries"]["github_cli_used"] is False assert payload["operation_boundaries"]["secret_or_runner_token_read"] is False diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 100b31daa..94882dbdd 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -3,12 +3,14 @@ **完成內容**: - `scripts/reboot-recovery/reboot-auto-recovery-slo-scorecard.py` 新增 `windows99_verify_collection`,把 99 VMware / Windows Update no-secret Verify 的下一步拆成機器可讀 collection packet。 - `/api/v1/agents/reboot-auto-recovery-slo-scorecard` loader 即使讀舊 snapshot,也會從 `host_boot_detection` 與 `windows99_vmware_autostart` 推導 `windows99_verify_collection`、`rollups.windows99_verify_collection_status`、`can_collect_no_secret`、`host99_reachable` 與 `host99_uptime_known`。 +- `awoooi-priority-work-order-readback` 同步上卷 `windows99_verify_collection_status`、`can_collect_no_secret`、blockers、99 reachable / uptime-known 與 post-verifier 到 `mainline_execution_state`、P0 evidence、summary、rollups,讓 AwoooP / governance 總覽不用鑽深層 SLO endpoint 才能看到 99 collection truth。 - 目前 production truth 仍應維持 fail-closed:99 host 可達但缺 live `Verify` output,`collection_blockers=["windows99_vmware_autostart_readback_missing","windows99_uptime_unknown"]`;不得宣稱 VM autostart / Windows Update policy 已驗證。 - collection packet 明確列出 expected no-secret output fields、`-Mode Verify` command、post-verifier 與 forbidden actions:不得讀 Windows 密碼 / secret,不得重啟 host,不得改 VM power,不得套用 Windows Update policy,不得 service restart,不得使用 GitHub API。 **驗證**: - `python3.11 -m py_compile scripts/reboot-recovery/reboot-auto-recovery-slo-scorecard.py apps/api/src/services/reboot_auto_recovery_slo_scorecard.py`:通過。 - `DATABASE_URL=sqlite:///tmp/awoooi-test.db python3.11 -m pytest -q scripts/reboot-recovery/tests/test_reboot_auto_recovery_slo_scorecard.py apps/api/tests/test_reboot_auto_recovery_slo_scorecard_api.py`:`14 passed`。 +- `python3.11 -m py_compile apps/api/src/services/awoooi_priority_work_order_readback.py && DATABASE_URL=sqlite:///tmp/awoooi-test.db python3.11 -m pytest -q apps/api/tests/test_awoooi_priority_work_order_readback_api.py`:`14 passed`。 - 本機 API loader readback:`status=blocked_reboot_auto_recovery_slo_not_ready`、`collection=blocked_windows99_verify_output_missing_host_reachable`、`can_collect=True`、`blockers=windows99_vmware_autostart_readback_missing,windows99_uptime_unknown`。 **仍維持**: