From 35e5a84a0f45d54a8584ea4538f19e7f079a7d9f Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 3 Jul 2026 14:41:01 +0800 Subject: [PATCH] fix(reboot): expose windows99 autostart check-mode package --- apps/api/src/api/v1/agents.py | 38 ++ .../awoooi_priority_work_order_readback.py | 353 ++++++++++++++++ .../reboot_auto_recovery_slo_scorecard.py | 396 ++++++++++++++++++ ...s99_vmware_autostart_check_mode_package.py | 138 ++++++ ...awoooi_priority_work_order_readback_api.py | 59 +++ ..._reboot_auto_recovery_slo_scorecard_api.py | 24 ++ ...vmware_autostart_check_mode_package_api.py | 154 +++++++ docs/LOGBOOK.md | 21 + 8 files changed, 1183 insertions(+) create mode 100644 apps/api/src/services/windows99_vmware_autostart_check_mode_package.py create mode 100644 apps/api/tests/test_windows99_vmware_autostart_check_mode_package_api.py diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index aeeb404ec..554681c2c 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -486,6 +486,9 @@ from src.services.telegram_alert_monitoring_live_receipt_apply import ( from src.services.windows99_vmx_source_locator_readback import ( load_latest_windows99_vmx_source_locator_readback, ) +from src.services.windows99_vmware_autostart_check_mode_package import ( + load_latest_windows99_vmware_autostart_check_mode_package, +) from src.services.windows99_vmx_source_repair_package import ( load_latest_windows99_vmx_source_repair_package, mark_windows99_vmx_source_repair_public_redaction_boundary, @@ -1576,6 +1579,41 @@ async def get_windows99_vmx_source_repair_package() -> dict[str, Any]: ) from exc +@router.get( + "/windows99-vmware-autostart-check-mode-package", + response_model=dict[str, Any], + summary="取得 Windows99 VMware autostart check-mode package", + description=( + "讀取 P0-006 scorecard 的 Windows99 powered-off VM aliases,產生獨立 " + "VMware autostart check-mode package。此端點只回傳 target selector、" + "check-mode、post-verifier、learning writeback 與禁止 runtime action;" + "不讀 secret、不 SSH、不啟停 VM、不寫 Windows、不改 registry/task、" + "不重啟主機、不 restart service。" + ), +) +async def get_windows99_vmware_autostart_check_mode_package() -> dict[str, Any]: + """回傳 Windows99 VMware autostart check-mode package。""" + try: + payload = await asyncio.to_thread( + load_latest_windows99_vmware_autostart_check_mode_package + ) + 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( + "windows99_vmware_autostart_check_mode_package_invalid", + error=str(exc), + ) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Windows99 VMware autostart check-mode package 無效", + ) from exc + + @router.get( "/windows99-vmx-source-locator-readback", response_model=dict[str, Any], 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 902ace2e5..6e95b64df 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -2987,6 +2987,9 @@ def _enrich_from_current_readbacks( from src.services.windows99_vmx_source_locator_readback import ( build_windows99_vmx_source_locator_readback, ) + from src.services.windows99_vmware_autostart_check_mode_package import ( + build_windows99_vmware_autostart_check_mode_package, + ) from src.services.windows99_vmx_source_repair_package import ( build_windows99_vmx_source_repair_package, ) @@ -3032,6 +3035,9 @@ def _enrich_from_current_readbacks( windows99_vmx_source_repair_package = ( build_windows99_vmx_source_repair_package(reboot_slo) ) + windows99_vmware_autostart_check_mode_package = ( + build_windows99_vmware_autostart_check_mode_package(reboot_slo) + ) windows99_vmx_source_locator_readback = ( build_windows99_vmx_source_locator_readback( reboot_slo, @@ -3072,6 +3078,42 @@ def _enrich_from_current_readbacks( windows99_vmx_source_repair_learning_refs = _dict( windows99_vmx_source_repair_package.get("learning_writeback_refs") ) or _dict(windows99_vmx_source_repair_action_chain.get("learning_writeback_refs")) + windows99_vmware_autostart_action_chain = _dict( + windows99_vmware_autostart_check_mode_package.get("action_chain") + ) + windows99_vmware_autostart_target_selector = _dict( + windows99_vmware_autostart_action_chain.get("target_selector") + ) + windows99_vmware_autostart_check_mode = _dict( + windows99_vmware_autostart_action_chain.get("check_mode") + ) + windows99_vmware_autostart_candidate = _dict( + windows99_vmware_autostart_action_chain.get("controlled_apply_candidate") + ) + windows99_vmware_autostart_execution_boundary = _dict( + windows99_vmware_autostart_action_chain.get("execution_boundary") + ) + windows99_vmware_autostart_rollback = _dict( + windows99_vmware_autostart_action_chain.get("rollback") + ) + windows99_vmware_autostart_diff = ( + windows99_vmware_autostart_action_chain.get("source_of_truth_diff") + if isinstance( + windows99_vmware_autostart_action_chain.get("source_of_truth_diff"), + list, + ) + else [] + ) + windows99_vmware_autostart_post_apply_verifier = _strings( + windows99_vmware_autostart_check_mode_package.get("post_apply_verifier") + or windows99_vmware_autostart_check_mode_package.get("post_verifier") + or windows99_vmware_autostart_action_chain.get("post_apply_verifier") + ) + windows99_vmware_autostart_learning_refs = _dict( + windows99_vmware_autostart_check_mode_package.get("learning_writeback_refs") + ) or _dict( + windows99_vmware_autostart_action_chain.get("learning_writeback_refs") + ) p0_006_event_gated = bool( reboot_slo_rollups.get("blocked_by_fresh_reboot_window_only") is True ) @@ -3426,6 +3468,177 @@ def _enrich_from_current_readbacks( ) or "" ) + state["windows99_vmware_autostart_check_mode_package_status"] = str( + windows99_vmware_autostart_check_mode_package.get("status") or "unknown" + ) + state["windows99_vmware_autostart_check_mode_package_ready"] = bool( + windows99_vmware_autostart_check_mode_package.get("package_ready") is True + ) + state["windows99_vmware_autostart_check_mode_powered_off_aliases"] = _strings( + windows99_vmware_autostart_check_mode_package.get("powered_off_aliases") + ) + state["windows99_vmware_autostart_check_mode_powered_off_count"] = _int( + windows99_vmware_autostart_check_mode_package.get("powered_off_count") + ) + state["windows99_vmware_autostart_check_mode_safe_next_step"] = str( + windows99_vmware_autostart_check_mode_package.get("safe_next_step") or "" + ) + state["windows99_vmware_autostart_check_mode_controlled_apply_mode"] = str( + windows99_vmware_autostart_check_mode_package.get("controlled_apply_mode") + or "" + ) + state[ + "windows99_vmware_autostart_check_mode_apply_allowed_by_this_package" + ] = bool( + windows99_vmware_autostart_check_mode_package.get( + "apply_allowed_by_this_package" + ) + is True + ) + state["windows99_vmware_autostart_check_mode_work_order_id"] = str( + windows99_vmware_autostart_check_mode_package.get("work_order_id") + or windows99_vmware_autostart_action_chain.get("work_order_id") + or "" + ) + state["windows99_vmware_autostart_check_mode_action_stage"] = str( + windows99_vmware_autostart_check_mode_package.get("action_stage") + or windows99_vmware_autostart_action_chain.get("stage") + or "" + ) + state["windows99_vmware_autostart_check_mode_next_executable_step"] = str( + windows99_vmware_autostart_check_mode_package.get("next_executable_step") + or windows99_vmware_autostart_action_chain.get("next_executable_step") + or "" + ) + state["windows99_vmware_autostart_check_mode_allowed"] = bool( + windows99_vmware_autostart_check_mode_package.get( + "check_mode_allowed_by_this_package" + ) + is True + or windows99_vmware_autostart_check_mode.get("allowed_by_this_package") + is True + ) + state["windows99_vmware_autostart_check_mode_rollback_required"] = bool( + windows99_vmware_autostart_check_mode_package.get("rollback_required") is True + or windows99_vmware_autostart_rollback.get("required") is True + ) + state["windows99_vmware_autostart_check_mode_vm_power_change_allowed"] = bool( + windows99_vmware_autostart_check_mode_package.get("vm_power_change_allowed") + is True + or windows99_vmware_autostart_target_selector.get("vm_power_change_allowed") + is True + ) + state["windows99_vmware_autostart_check_mode_registry_apply_allowed"] = bool( + windows99_vmware_autostart_check_mode_package.get( + "windows_registry_apply_allowed" + ) + is True + or windows99_vmware_autostart_target_selector.get( + "windows_registry_apply_allowed" + ) + is True + ) + state[ + "windows99_vmware_autostart_check_mode_scheduled_task_modify_allowed" + ] = bool( + windows99_vmware_autostart_check_mode_package.get( + "scheduled_task_modify_allowed" + ) + is True + or windows99_vmware_autostart_target_selector.get( + "scheduled_task_modify_allowed" + ) + is True + ) + state["windows99_vmware_autostart_check_mode_source_diff_alias_count"] = len( + windows99_vmware_autostart_diff + ) + state["windows99_vmware_autostart_check_mode_target_vm_aliases"] = _strings( + windows99_vmware_autostart_check_mode_package.get("target_vm_aliases") + or windows99_vmware_autostart_target_selector.get("vm_aliases") + ) + state["windows99_vmware_autostart_check_mode_probe_command_count"] = _int( + windows99_vmware_autostart_check_mode_package.get( + "check_mode_probe_command_count" + ) + or windows99_vmware_autostart_check_mode.get("probe_command_count") + ) + state[ + "windows99_vmware_autostart_check_mode_controlled_apply_candidate_status" + ] = str( + windows99_vmware_autostart_check_mode_package.get( + "controlled_apply_candidate_status" + ) + or windows99_vmware_autostart_candidate.get("status") + or "" + ) + state[ + "windows99_vmware_autostart_check_mode_controlled_apply_candidate_executor" + ] = str( + windows99_vmware_autostart_check_mode_package.get( + "controlled_apply_candidate_executor" + ) + or windows99_vmware_autostart_candidate.get("executor") + or windows99_vmware_autostart_execution_boundary.get("executor") + or "" + ) + state[ + "windows99_vmware_autostart_check_mode_controlled_apply_candidate_command_id" + ] = str( + windows99_vmware_autostart_check_mode_package.get( + "controlled_apply_candidate_command_id" + ) + or windows99_vmware_autostart_candidate.get("command_id") + or windows99_vmware_autostart_execution_boundary.get("command_id") + or "" + ) + state["windows99_vmware_autostart_check_mode_post_apply_verifier_count"] = len( + windows99_vmware_autostart_post_apply_verifier + ) + state["windows99_vmware_autostart_check_mode_learning_writeback_ref_count"] = _int( + windows99_vmware_autostart_check_mode_package.get( + "learning_writeback_ref_count" + ) + or windows99_vmware_autostart_action_chain.get( + "learning_writeback_ref_count" + ) + or len( + [ + value + for value in windows99_vmware_autostart_learning_refs.values() + if str(value or "").strip() + ] + ) + ) + state["windows99_vmware_autostart_check_mode_log_receipt_ref"] = str( + windows99_vmware_autostart_check_mode_package.get("log_receipt_ref") + or windows99_vmware_autostart_learning_refs.get("log_receipt_ref") + or "" + ) + state["windows99_vmware_autostart_check_mode_km_writeback_ref"] = str( + windows99_vmware_autostart_check_mode_package.get("km_writeback_ref") + or windows99_vmware_autostart_learning_refs.get("km_writeback_ref") + or "" + ) + state["windows99_vmware_autostart_check_mode_rag_chunk_ref"] = str( + windows99_vmware_autostart_check_mode_package.get("rag_chunk_ref") + or windows99_vmware_autostart_learning_refs.get("rag_chunk_ref") + or "" + ) + state["windows99_vmware_autostart_check_mode_mcp_evidence_ref"] = str( + windows99_vmware_autostart_check_mode_package.get("mcp_evidence_ref") + or windows99_vmware_autostart_learning_refs.get("mcp_evidence_ref") + or "" + ) + state["windows99_vmware_autostart_check_mode_playbook_trust_writeback_ref"] = str( + windows99_vmware_autostart_check_mode_package.get( + "playbook_trust_writeback_ref" + ) + or windows99_vmware_autostart_learning_refs.get( + "playbook_trust_writeback_ref" + ) + or "" + ) state["windows99_vmx_source_locator_status"] = str( windows99_vmx_source_locator_readback.get("status") or "unknown" ) @@ -3917,6 +4130,104 @@ def _enrich_from_current_readbacks( evidence["windows99_vmx_source_repair_playbook_trust_writeback_ref"] = ( state["windows99_vmx_source_repair_playbook_trust_writeback_ref"] ) + evidence["windows99_vmware_autostart_check_mode_package_status"] = state[ + "windows99_vmware_autostart_check_mode_package_status" + ] + evidence["windows99_vmware_autostart_check_mode_package_ready"] = state[ + "windows99_vmware_autostart_check_mode_package_ready" + ] + evidence["windows99_vmware_autostart_check_mode_powered_off_aliases"] = ( + state["windows99_vmware_autostart_check_mode_powered_off_aliases"] + ) + evidence["windows99_vmware_autostart_check_mode_powered_off_count"] = state[ + "windows99_vmware_autostart_check_mode_powered_off_count" + ] + evidence["windows99_vmware_autostart_check_mode_safe_next_step"] = state[ + "windows99_vmware_autostart_check_mode_safe_next_step" + ] + evidence["windows99_vmware_autostart_check_mode_controlled_apply_mode"] = ( + state["windows99_vmware_autostart_check_mode_controlled_apply_mode"] + ) + evidence[ + "windows99_vmware_autostart_check_mode_apply_allowed_by_this_package" + ] = state[ + "windows99_vmware_autostart_check_mode_apply_allowed_by_this_package" + ] + evidence["windows99_vmware_autostart_check_mode_work_order_id"] = state[ + "windows99_vmware_autostart_check_mode_work_order_id" + ] + evidence["windows99_vmware_autostart_check_mode_action_stage"] = state[ + "windows99_vmware_autostart_check_mode_action_stage" + ] + evidence[ + "windows99_vmware_autostart_check_mode_next_executable_step" + ] = state["windows99_vmware_autostart_check_mode_next_executable_step"] + evidence["windows99_vmware_autostart_check_mode_allowed"] = state[ + "windows99_vmware_autostart_check_mode_allowed" + ] + evidence["windows99_vmware_autostart_check_mode_rollback_required"] = state[ + "windows99_vmware_autostart_check_mode_rollback_required" + ] + evidence[ + "windows99_vmware_autostart_check_mode_vm_power_change_allowed" + ] = state["windows99_vmware_autostart_check_mode_vm_power_change_allowed"] + evidence[ + "windows99_vmware_autostart_check_mode_registry_apply_allowed" + ] = state["windows99_vmware_autostart_check_mode_registry_apply_allowed"] + evidence[ + "windows99_vmware_autostart_check_mode_scheduled_task_modify_allowed" + ] = state[ + "windows99_vmware_autostart_check_mode_scheduled_task_modify_allowed" + ] + evidence[ + "windows99_vmware_autostart_check_mode_source_diff_alias_count" + ] = state["windows99_vmware_autostart_check_mode_source_diff_alias_count"] + evidence["windows99_vmware_autostart_check_mode_target_vm_aliases"] = state[ + "windows99_vmware_autostart_check_mode_target_vm_aliases" + ] + evidence[ + "windows99_vmware_autostart_check_mode_probe_command_count" + ] = state["windows99_vmware_autostart_check_mode_probe_command_count"] + evidence[ + "windows99_vmware_autostart_check_mode_controlled_apply_candidate_status" + ] = state[ + "windows99_vmware_autostart_check_mode_controlled_apply_candidate_status" + ] + evidence[ + "windows99_vmware_autostart_check_mode_controlled_apply_candidate_executor" + ] = state[ + "windows99_vmware_autostart_check_mode_controlled_apply_candidate_executor" + ] + evidence[ + "windows99_vmware_autostart_check_mode_controlled_apply_candidate_command_id" + ] = state[ + "windows99_vmware_autostart_check_mode_controlled_apply_candidate_command_id" + ] + evidence[ + "windows99_vmware_autostart_check_mode_post_apply_verifier_count" + ] = state["windows99_vmware_autostart_check_mode_post_apply_verifier_count"] + evidence[ + "windows99_vmware_autostart_check_mode_learning_writeback_ref_count" + ] = state[ + "windows99_vmware_autostart_check_mode_learning_writeback_ref_count" + ] + evidence["windows99_vmware_autostart_check_mode_log_receipt_ref"] = state[ + "windows99_vmware_autostart_check_mode_log_receipt_ref" + ] + evidence["windows99_vmware_autostart_check_mode_km_writeback_ref"] = state[ + "windows99_vmware_autostart_check_mode_km_writeback_ref" + ] + evidence["windows99_vmware_autostart_check_mode_rag_chunk_ref"] = state[ + "windows99_vmware_autostart_check_mode_rag_chunk_ref" + ] + evidence["windows99_vmware_autostart_check_mode_mcp_evidence_ref"] = state[ + "windows99_vmware_autostart_check_mode_mcp_evidence_ref" + ] + evidence[ + "windows99_vmware_autostart_check_mode_playbook_trust_writeback_ref" + ] = state[ + "windows99_vmware_autostart_check_mode_playbook_trust_writeback_ref" + ] evidence["windows99_vmx_source_locator_status"] = state[ "windows99_vmx_source_locator_status" ] @@ -4701,6 +5012,25 @@ def _set_rollups_and_summary( "windows99_vmx_source_repair_check_mode_allowed": ( state.get("windows99_vmx_source_repair_check_mode_allowed") is True ), + "windows99_vmware_autostart_check_mode_package_ready": ( + state.get("windows99_vmware_autostart_check_mode_package_ready") is True + ), + "windows99_vmware_autostart_check_mode_powered_off_count": _int( + state.get("windows99_vmware_autostart_check_mode_powered_off_count") + ), + "windows99_vmware_autostart_check_mode_apply_allowed_by_this_package": ( + state.get( + "windows99_vmware_autostart_check_mode_apply_allowed_by_this_package" + ) + is True + ), + "windows99_vmware_autostart_check_mode_work_order_id": str( + state.get("windows99_vmware_autostart_check_mode_work_order_id") or "" + ), + "windows99_vmware_autostart_check_mode_next_executable_step": str( + state.get("windows99_vmware_autostart_check_mode_next_executable_step") + or "" + ), "windows99_vmx_source_repair_rollback_required": ( state.get("windows99_vmx_source_repair_rollback_required") is True ), @@ -5085,6 +5415,29 @@ def _set_rollups_and_summary( "windows99_vmx_source_repair_check_mode_allowed": ( state.get("windows99_vmx_source_repair_check_mode_allowed") is True ), + "windows99_vmware_autostart_check_mode_package_status": str( + state.get("windows99_vmware_autostart_check_mode_package_status") + or "unknown" + ), + "windows99_vmware_autostart_check_mode_package_ready": ( + state.get("windows99_vmware_autostart_check_mode_package_ready") is True + ), + "windows99_vmware_autostart_check_mode_powered_off_aliases": _strings( + state.get("windows99_vmware_autostart_check_mode_powered_off_aliases") + ), + "windows99_vmware_autostart_check_mode_powered_off_count": _int( + state.get("windows99_vmware_autostart_check_mode_powered_off_count") + ), + "windows99_vmware_autostart_check_mode_safe_next_step": str( + state.get("windows99_vmware_autostart_check_mode_safe_next_step") or "" + ), + "windows99_vmware_autostart_check_mode_work_order_id": str( + state.get("windows99_vmware_autostart_check_mode_work_order_id") or "" + ), + "windows99_vmware_autostart_check_mode_next_executable_step": str( + state.get("windows99_vmware_autostart_check_mode_next_executable_step") + or "" + ), "windows99_vmx_source_repair_rollback_required": ( state.get("windows99_vmx_source_repair_rollback_required") is True ), diff --git a/apps/api/src/services/reboot_auto_recovery_slo_scorecard.py b/apps/api/src/services/reboot_auto_recovery_slo_scorecard.py index a1aa13595..e4937699a 100644 --- a/apps/api/src/services/reboot_auto_recovery_slo_scorecard.py +++ b/apps/api/src/services/reboot_auto_recovery_slo_scorecard.py @@ -146,6 +146,16 @@ _WINDOWS99_VMX_REPAIR_FORBIDDEN_ACTIONS = [ "scheduled_task_register_or_modify", "docker_nginx_k3s_db_firewall_restart", ] +_WINDOWS99_VMWARE_AUTOSTART_CHECK_FORBIDDEN_ACTIONS = [ + "read_windows_password_or_secret", + "vm_power_change", + "vmrun_start_or_stop", + "host_reboot", + "windows_service_restart", + "windows_registry_apply", + "scheduled_task_register_or_modify", + "docker_nginx_k3s_db_firewall_restart", +] def load_latest_reboot_auto_recovery_slo_scorecard( @@ -838,6 +848,12 @@ def _apply_prometheus_windows99_vmware_readback( payload.get("runtime_readback_generated_at") or _taipei_now_iso() ), ) + _sync_windows99_vmware_autostart_check_mode_package( + payload, + generated_at=str( + payload.get("runtime_readback_generated_at") or _taipei_now_iso() + ), + ) def _reconcile_prometheus_metric_active_blockers_with_source_controls( @@ -1718,6 +1734,12 @@ def _build_payload(scorecard: dict[str, Any], path: Path) -> dict[str, Any]: generated_at=runtime_readback_generated_at, ) ) + windows99_vmware_autostart_check_mode_package = ( + _build_windows99_vmware_autostart_check_mode_package( + windows99, + generated_at=runtime_readback_generated_at, + ) + ) blocked_by_fresh_reboot_window_only = active_blockers == [ "host_boot_observation_older_than_target_window" ] @@ -1860,6 +1882,40 @@ def _build_payload(scorecard: dict[str, Any], path: Path) -> dict[str, Any]: "windows99_vmx_source_repair_missing_vmx_aliases": _strings( windows99_vmx_source_repair_package.get("missing_vmx_aliases") ), + "windows99_vmware_autostart_check_mode_package_ready": ( + windows99_vmware_autostart_check_mode_package["package_ready"] is True + ), + "windows99_vmware_autostart_check_mode_package_status": str( + windows99_vmware_autostart_check_mode_package["status"] + ), + "windows99_vmware_autostart_check_mode_package_safe_next_step": str( + windows99_vmware_autostart_check_mode_package["safe_next_step"] + ), + "windows99_vmware_autostart_check_mode_package_apply_allowed": ( + windows99_vmware_autostart_check_mode_package[ + "apply_allowed_by_this_package" + ] + is True + ), + "windows99_vmware_autostart_check_mode_powered_off_aliases": _strings( + windows99_vmware_autostart_check_mode_package.get("powered_off_aliases") + ), + "windows99_vmware_autostart_check_mode_post_apply_verifier_count": _int( + windows99_vmware_autostart_check_mode_package.get( + "post_apply_verifier_count" + ) + ), + "windows99_vmware_autostart_check_mode_learning_writeback_ref_count": _int( + windows99_vmware_autostart_check_mode_package.get( + "learning_writeback_ref_count" + ) + ), + "windows99_vmware_autostart_check_mode_controlled_apply_candidate_executor": str( + windows99_vmware_autostart_check_mode_package.get( + "controlled_apply_candidate_executor" + ) + or "" + ), "windows99_verify_collection_status": windows99_verify_collection["status"], "windows99_verify_collection_can_collect_no_secret": ( windows99_verify_collection["can_collect_no_secret_verify"] is True @@ -2066,6 +2122,30 @@ def _build_payload(scorecard: dict[str, Any], path: Path) -> dict[str, Any]: "windows99_vmx_source_repair_missing_vmx_aliases": rollups[ "windows99_vmx_source_repair_missing_vmx_aliases" ], + "windows99_vmware_autostart_check_mode_package_ready": rollups[ + "windows99_vmware_autostart_check_mode_package_ready" + ], + "windows99_vmware_autostart_check_mode_package_status": rollups[ + "windows99_vmware_autostart_check_mode_package_status" + ], + "windows99_vmware_autostart_check_mode_package_safe_next_step": rollups[ + "windows99_vmware_autostart_check_mode_package_safe_next_step" + ], + "windows99_vmware_autostart_check_mode_package_apply_allowed": rollups[ + "windows99_vmware_autostart_check_mode_package_apply_allowed" + ], + "windows99_vmware_autostart_check_mode_powered_off_aliases": rollups[ + "windows99_vmware_autostart_check_mode_powered_off_aliases" + ], + "windows99_vmware_autostart_check_mode_post_apply_verifier_count": rollups[ + "windows99_vmware_autostart_check_mode_post_apply_verifier_count" + ], + "windows99_vmware_autostart_check_mode_learning_writeback_ref_count": rollups[ + "windows99_vmware_autostart_check_mode_learning_writeback_ref_count" + ], + "windows99_vmware_autostart_check_mode_controlled_apply_candidate_executor": rollups[ + "windows99_vmware_autostart_check_mode_controlled_apply_candidate_executor" + ], "windows99_verify_collection_status": rollups[ "windows99_verify_collection_status" ], @@ -2122,6 +2202,9 @@ def _build_payload(scorecard: dict[str, Any], path: Path) -> dict[str, Any]: "windows99_vmware_autostart": windows99, "windows99_vmware_verify_collector": windows99_collector, "windows99_vmx_source_repair_package": windows99_vmx_source_repair_package, + "windows99_vmware_autostart_check_mode_package": ( + windows99_vmware_autostart_check_mode_package + ), "windows99_verify_collection": windows99_verify_collection, "windows99_management_channel": windows99_management, "source_controls": source_controls, @@ -2469,6 +2552,253 @@ def _powershell_test_path(path: str) -> str: return f"Test-Path -LiteralPath '{escaped}'" +def _build_windows99_vmware_autostart_action_chain( + *, + powered_off_aliases: list[str], +) -> dict[str, Any]: + primary_alias = powered_off_aliases[0] if powered_off_aliases else "" + if powered_off_aliases: + stage = "power_readback_check_mode_ready" + next_executable_step = "build_vmware_autostart_power_readback_check_mode" + candidate_status = "candidate_waiting_autostart_policy_check_mode" + else: + stage = "verify_only_ready" + next_executable_step = "rerun_no_secret_collector_and_scorecard" + candidate_status = "no_power_readback_repair_required" + + work_order_id = ( + f"windows99-vmware-autostart-check-mode-{primary_alias}" + if primary_alias + else "windows99-vmware-autostart-check-mode-verify" + ) + source_diffs = [ + { + "alias": alias, + "current_state": "guest_power_readback_not_ready", + "desired_state": "guest_power_state_observable_without_start_or_stop", + "diff_mode": "vmrun_list_and_autostart_policy_verify_only", + "vm_power_change_allowed": False, + "windows_registry_apply_allowed": False, + "scheduled_task_modify_allowed": False, + } + for alias in powered_off_aliases + ] + post_apply_verifier = [ + "bash scripts/reboot-recovery/collect-windows99-vmware-verify.sh --collect", + "bash scripts/reboot-recovery/install-reboot-auto-recovery-slo-110.sh --verify-only", + "GET /api/v1/agents/reboot-auto-recovery-slo-scorecard", + "GET /api/v1/agents/windows99-vmware-autostart-check-mode-package", + "GET /api/v1/agents/awoooi-priority-work-order-readback", + ] + learning_writeback_refs = { + "log_receipt_ref": f"log://awoooi/p0-006/{work_order_id}", + "km_writeback_ref": f"km://awoooi/reboot-slo/{work_order_id}", + "rag_chunk_ref": f"rag://awoooi/reboot-slo/{work_order_id}", + "mcp_evidence_ref": f"mcp://awoooi/windows99/{work_order_id}", + "playbook_trust_writeback_ref": ( + f"playbook://awoooi/windows99-vmware-autostart/{work_order_id}" + ), + "work_item_receipt_ref": f"awooop://work-items/{work_order_id}", + } + controlled_apply_candidate = { + "status": candidate_status, + "executor": "windows99_no_secret_management_channel_or_local_console", + "command_id": "windows99_vmware_autostart_power_readback_check_mode", + "apply_allowed_by_this_package": False, + "allowed_when": [ + "fresh_vmware_verify_stdout_confirms_powered_off_aliases", + "vmware_autostart_config_ready", + "windows_update_no_auto_reboot_policy_ready", + "check_mode_plan_has_no_vmrun_start_or_stop", + "post_apply_no_secret_verifier_is_available", + ], + } + + return { + "work_order_id": work_order_id, + "stage": stage, + "next_executable_step": next_executable_step, + "target_selector": { + "host_alias": "99", + "vm_aliases": powered_off_aliases, + "power_state_scope": "windows99_vmware_powered_off_aliases", + "secret_collection_allowed": False, + "vm_power_change_allowed": False, + "windows_registry_apply_allowed": False, + "scheduled_task_modify_allowed": False, + }, + "source_of_truth_diff": source_diffs, + "check_mode": { + "allowed_by_this_package": True, + "probe": "verify_vmrun_list_autostart_task_and_update_policy_no_start_stop", + "probe_command_count": 2 if powered_off_aliases else 1, + "collector_command": ( + "bash scripts/reboot-recovery/collect-windows99-vmware-verify.sh " + "--collect" + ), + "scorecard_command": ( + "python3.11 scripts/reboot-recovery/" + "reboot-auto-recovery-slo-scorecard.py --output " + ), + }, + "controlled_apply_candidate": controlled_apply_candidate, + "execution_boundary": { + "executor": controlled_apply_candidate["executor"], + "command_id": controlled_apply_candidate["command_id"], + "execution_channel": "authorized_internal_console_or_no_secret_collector", + "execution_authorized_by_this_package": False, + "public_endpoint_display_only": True, + "host_write_allowed_by_this_package": False, + "vm_power_change_allowed_by_this_package": False, + "windows_service_restart_allowed_by_this_package": False, + "scheduled_task_modify_allowed_by_this_package": False, + }, + "rollback": { + "required": True, + "mode": "revert_scoped_autostart_policy_or_task_change_before_verify", + "backup_required_before_apply": True, + }, + "post_apply_verifier": post_apply_verifier, + "post_apply_verifier_count": len(post_apply_verifier), + "learning_writeback": [ + "awooop_work_item_receipt", + "reboot_slo_scorecard_rollups", + "log_receipt_ref", + "mcp_evidence_ref", + "rag_chunk_ref", + "km_playbook_trust_writeback_after_verifier", + ], + "learning_writeback_refs": learning_writeback_refs, + "learning_writeback_ref_count": len(learning_writeback_refs), + "manager_summary": { + "primary_alias": primary_alias or "none", + "powered_off_count": len(powered_off_aliases), + "check_mode_allowed": True, + "apply_allowed": False, + "vm_power_change_allowed": False, + "post_apply_verifier_count": len(post_apply_verifier), + "learning_writeback_ref_count": len(learning_writeback_refs), + }, + } + + +def _build_windows99_vmware_autostart_check_mode_package( + windows99: dict[str, Any], + *, + generated_at: str, +) -> dict[str, Any]: + powered_off_aliases = _unique_strings( + _strings(windows99.get("powered_off_aliases")) + ) + required_aliases = _unique_strings( + _strings(windows99.get("required_vm_aliases")) + or _WINDOWS99_REQUIRED_VM_ALIASES + ) + package_ready = bool(powered_off_aliases) + status = ( + "check_mode_package_ready_windows99_vmware_guest_power_readback_required" + if powered_off_aliases + else "ready_no_windows99_vmware_autostart_power_check_required" + ) + safe_next_step = ( + _WINDOWS99_POWERED_OFF_NEXT_SAFE_ACTION + if powered_off_aliases + else "rerun_no_secret_collector_and_reboot_slo_scorecard_verify_only" + ) + rows = [ + { + "alias": alias, + "powered_off": alias in powered_off_aliases, + "check_mode_probe": ( + "verify_vmrun_list_autostart_task_and_update_policy_no_start_stop" + ), + "vm_power_change_allowed": False, + "controlled_power_apply_required": alias in powered_off_aliases, + "controlled_power_apply_hint": ( + "collect_fresh_verify_readback_and_build_scoped_autostart_policy_" + "check_mode_before_any_power_action" + if alias in powered_off_aliases + else "" + ), + } + for alias in _unique_strings([*required_aliases, *powered_off_aliases]) + ] + action_chain = _build_windows99_vmware_autostart_action_chain( + powered_off_aliases=powered_off_aliases, + ) + controlled_apply_candidate = _dict(action_chain.get("controlled_apply_candidate")) + learning_writeback_refs = _dict(action_chain.get("learning_writeback_refs")) + post_apply_verifier = _strings(action_chain.get("post_apply_verifier")) + + return { + "schema_version": "windows99_vmware_autostart_check_mode_package_v1", + "generated_at": generated_at, + "status": status, + "package_ready": package_ready, + "target_host_alias": "99", + "powered_off_aliases": powered_off_aliases, + "required_vm_aliases": required_aliases, + "vm_rows": rows, + "action_chain": action_chain, + "work_order_id": action_chain["work_order_id"], + "action_stage": action_chain["stage"], + "next_executable_step": action_chain["next_executable_step"], + "target_vm_aliases": _strings( + _dict(action_chain.get("target_selector")).get("vm_aliases") + ), + "check_mode_allowed_by_this_package": action_chain["check_mode"][ + "allowed_by_this_package" + ], + "check_mode_probe_command_count": _int( + _dict(action_chain.get("check_mode")).get("probe_command_count") + ), + "rollback_required": action_chain["rollback"]["required"], + "vm_power_change_allowed": action_chain["target_selector"][ + "vm_power_change_allowed" + ], + "windows_registry_apply_allowed": action_chain["target_selector"][ + "windows_registry_apply_allowed" + ], + "scheduled_task_modify_allowed": action_chain["target_selector"][ + "scheduled_task_modify_allowed" + ], + "controlled_apply_candidate_status": str( + controlled_apply_candidate.get("status") or "" + ), + "controlled_apply_candidate_executor": str( + controlled_apply_candidate.get("executor") or "" + ), + "controlled_apply_candidate_command_id": str( + controlled_apply_candidate.get("command_id") or "" + ), + "safe_next_step": safe_next_step, + "controlled_apply_mode": "check_mode_package_only_no_windows_or_vm_write", + "apply_allowed_by_this_package": False, + "post_apply_verifier": post_apply_verifier, + "post_verifier": post_apply_verifier, + "post_apply_verifier_count": len(post_apply_verifier), + "learning_writeback_refs": learning_writeback_refs, + "learning_writeback_ref_count": len(learning_writeback_refs), + "log_receipt_ref": str(learning_writeback_refs.get("log_receipt_ref") or ""), + "km_writeback_ref": str(learning_writeback_refs.get("km_writeback_ref") or ""), + "rag_chunk_ref": str(learning_writeback_refs.get("rag_chunk_ref") or ""), + "mcp_evidence_ref": str(learning_writeback_refs.get("mcp_evidence_ref") or ""), + "playbook_trust_writeback_ref": str( + learning_writeback_refs.get("playbook_trust_writeback_ref") or "" + ), + "forbidden_actions": _WINDOWS99_VMWARE_AUTOSTART_CHECK_FORBIDDEN_ACTIONS, + "operation_boundaries": { + "secret_value_read": False, + "remote_write_performed": False, + "vm_power_change_performed": False, + "windows_service_restart_performed": False, + "windows_registry_apply_performed": False, + "scheduled_task_modify_performed": False, + "host_reboot_performed": False, + }, + } + + def _build_windows99_vmx_source_repair_action_chain( *, missing_vmx_aliases: list[str], @@ -2794,6 +3124,72 @@ def _sync_windows99_vmx_source_repair_package( return package +def _sync_windows99_vmware_autostart_check_mode_package( + payload: dict[str, Any], + *, + generated_at: str, +) -> dict[str, Any]: + windows99 = _dict(payload.get("windows99_vmware_autostart")) + package = _build_windows99_vmware_autostart_check_mode_package( + windows99, + generated_at=generated_at, + ) + payload["windows99_vmware_autostart_check_mode_package"] = package + + readback = _dict(payload.setdefault("readback", {})) + readback["windows99_vmware_autostart_check_mode_package_ready"] = ( + package["package_ready"] is True + ) + readback["windows99_vmware_autostart_check_mode_package_status"] = str( + package["status"] + ) + readback["windows99_vmware_autostart_check_mode_package_safe_next_step"] = str( + package["safe_next_step"] + ) + readback["windows99_vmware_autostart_check_mode_package_apply_allowed"] = ( + package["apply_allowed_by_this_package"] is True + ) + readback["windows99_vmware_autostart_check_mode_powered_off_aliases"] = _strings( + package.get("powered_off_aliases") + ) + readback["windows99_vmware_autostart_check_mode_post_apply_verifier_count"] = ( + _int(package.get("post_apply_verifier_count")) + ) + readback[ + "windows99_vmware_autostart_check_mode_learning_writeback_ref_count" + ] = _int(package.get("learning_writeback_ref_count")) + readback[ + "windows99_vmware_autostart_check_mode_controlled_apply_candidate_executor" + ] = str(package.get("controlled_apply_candidate_executor") or "") + + rollups = _dict(payload.setdefault("rollups", {})) + rollups["windows99_vmware_autostart_check_mode_package_ready"] = ( + package["package_ready"] is True + ) + rollups["windows99_vmware_autostart_check_mode_package_status"] = str( + package["status"] + ) + rollups["windows99_vmware_autostart_check_mode_package_safe_next_step"] = str( + package["safe_next_step"] + ) + rollups["windows99_vmware_autostart_check_mode_package_apply_allowed"] = ( + package["apply_allowed_by_this_package"] is True + ) + rollups["windows99_vmware_autostart_check_mode_powered_off_aliases"] = _strings( + package.get("powered_off_aliases") + ) + rollups["windows99_vmware_autostart_check_mode_post_apply_verifier_count"] = ( + _int(package.get("post_apply_verifier_count")) + ) + rollups[ + "windows99_vmware_autostart_check_mode_learning_writeback_ref_count" + ] = _int(package.get("learning_writeback_ref_count")) + rollups[ + "windows99_vmware_autostart_check_mode_controlled_apply_candidate_executor" + ] = str(package.get("controlled_apply_candidate_executor") or "") + return package + + def _host_row_by_alias( host_boot_detection: dict[str, Any], alias: str, diff --git a/apps/api/src/services/windows99_vmware_autostart_check_mode_package.py b/apps/api/src/services/windows99_vmware_autostart_check_mode_package.py new file mode 100644 index 000000000..bd0489e65 --- /dev/null +++ b/apps/api/src/services/windows99_vmware_autostart_check_mode_package.py @@ -0,0 +1,138 @@ +"""Windows99 VMware autostart check-mode package readback. + +This wrapper exposes the P0-006 powered-off VM lane as its own package instead +of hiding it inside the VMX source repair package. It is display/readback only: +no secret read, host write, VM power change, service restart, or registry/task +apply happens here. +""" + +from __future__ import annotations + +from typing import Any + +from src.services.reboot_auto_recovery_slo_scorecard import ( + _build_windows99_vmware_autostart_check_mode_package, + _taipei_now_iso, + load_latest_reboot_auto_recovery_slo_scorecard, +) + + +def _dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + +def _strings(value: Any) -> list[str]: + if isinstance(value, list): + return [str(item).strip() for item in value if str(item).strip()] + if isinstance(value, str): + return [part.strip() for part in value.split(",") if part.strip()] + return [] + + +def _with_ui_counts( + package: dict[str, Any], + scorecard: dict[str, Any], +) -> dict[str, Any]: + payload = dict(package) + powered_off_aliases = _strings(payload.get("powered_off_aliases")) + readback = _dict(scorecard.get("readback")) + windows99_management = _dict(scorecard.get("windows99_management_channel")) + ssh_batch = _dict(windows99_management.get("ssh_batch")) + payload["powered_off_count"] = len(powered_off_aliases) + payload["scorecard_status"] = str(scorecard.get("status") or "unknown") + payload["scorecard_generated_at"] = str(scorecard.get("generated_at") or "") + payload["collector_status"] = str( + readback.get("windows99_no_secret_collector_status") or "unknown" + ) + payload["collector_readback_present"] = ( + readback.get("windows99_no_secret_collector_readback_present") is True + ) + payload["collector_ssh_batchmode_auth_ready"] = ( + readback.get("windows99_no_secret_collector_ssh_batchmode_auth_ready") + is True + ) + payload["collector_remote_verify_attempted"] = ( + readback.get("windows99_no_secret_collector_remote_verify_attempted") + is True + ) + payload["collector_safe_next_step"] = str( + readback.get("windows99_no_secret_collector_safe_next_step") or "" + ) + payload["management_channel_ready"] = ( + windows99_management.get("remote_execution_channel_ready") is True + ) + payload["can_collect_vmware_verify_without_secret"] = ( + windows99_management.get("can_collect_vmware_verify_without_secret") is True + ) + payload["ssh_batch_ready"] = ssh_batch.get("ready") is True + payload["ssh_batch_status"] = str(ssh_batch.get("status") or "unknown") + payload["winrm_http_open"] = windows99_management.get("winrm_http_open") is True + payload["winrm_https_open"] = windows99_management.get("winrm_https_open") is True + payload["local_console_channel_reachable"] = ( + windows99_management.get("local_console_channel_reachable") is True + ) + payload["console_collection_channels"] = _strings( + windows99_management.get("console_collection_channels") + ) + payload["collector_management_channel_conflict"] = bool( + payload["collector_ssh_batchmode_auth_ready"] + and not payload["management_channel_ready"] + ) + payload["effective_check_mode_execution_ready"] = bool( + payload.get("package_ready") is True + and payload.get("check_mode_allowed_by_this_package") is True + and ( + payload["management_channel_ready"] + or payload["local_console_channel_reachable"] + ) + ) + payload["public_display_only"] = True + payload["check_mode_package_execution_authorized_by_this_endpoint"] = False + return payload + + +def _alias_lists_match(package: dict[str, Any], windows99: dict[str, Any]) -> bool: + return _strings(package.get("powered_off_aliases")) == _strings( + windows99.get("powered_off_aliases") + ) + + +def _package_has_action_chain(package: dict[str, Any]) -> bool: + action_chain = _dict(package.get("action_chain")) + target_selector = _dict(action_chain.get("target_selector")) + return bool( + action_chain.get("work_order_id") + and action_chain.get("stage") + and action_chain.get("next_executable_step") + and target_selector.get("host_alias") + ) + + +def build_windows99_vmware_autostart_check_mode_package( + scorecard: dict[str, Any], + *, + generated_at: str | None = None, +) -> dict[str, Any]: + existing_package = _dict( + scorecard.get("windows99_vmware_autostart_check_mode_package") + ) + windows99 = _dict(scorecard.get("windows99_vmware_autostart")) + package = ( + dict(existing_package) + if existing_package + and generated_at is None + and _alias_lists_match(existing_package, windows99) + and _package_has_action_chain(existing_package) + else _build_windows99_vmware_autostart_check_mode_package( + windows99, + generated_at=generated_at + or str(scorecard.get("generated_at") or _taipei_now_iso()), + ) + ) + return _with_ui_counts(package, scorecard) + + +def load_latest_windows99_vmware_autostart_check_mode_package() -> dict[str, Any]: + return build_windows99_vmware_autostart_check_mode_package( + load_latest_reboot_auto_recovery_slo_scorecard() + ) 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 398dc6804..5e830ffbd 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 @@ -737,6 +737,53 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a assert state[ "windows99_vmx_source_repair_playbook_trust_writeback_ref" ].startswith("playbook://awoooi/windows99-vmx-source-repair/") + assert state["windows99_vmware_autostart_check_mode_package_ready"] is True + assert state[ + "windows99_vmware_autostart_check_mode_package_status" + ] == "check_mode_package_ready_windows99_vmware_guest_power_readback_required" + assert state["windows99_vmware_autostart_check_mode_powered_off_aliases"] == [ + "111", + "112", + "120", + "121", + "188", + ] + assert state["windows99_vmware_autostart_check_mode_powered_off_count"] == 5 + assert state[ + "windows99_vmware_autostart_check_mode_apply_allowed_by_this_package" + ] is False + assert state["windows99_vmware_autostart_check_mode_work_order_id"] == ( + "windows99-vmware-autostart-check-mode-111" + ) + assert state["windows99_vmware_autostart_check_mode_action_stage"] == ( + "power_readback_check_mode_ready" + ) + assert state["windows99_vmware_autostart_check_mode_next_executable_step"] == ( + "build_vmware_autostart_power_readback_check_mode" + ) + assert state["windows99_vmware_autostart_check_mode_allowed"] is True + assert state[ + "windows99_vmware_autostart_check_mode_vm_power_change_allowed" + ] is False + assert state[ + "windows99_vmware_autostart_check_mode_registry_apply_allowed" + ] is False + assert state[ + "windows99_vmware_autostart_check_mode_scheduled_task_modify_allowed" + ] is False + assert state["windows99_vmware_autostart_check_mode_target_vm_aliases"] == [ + "111", + "112", + "120", + "121", + "188", + ] + assert state[ + "windows99_vmware_autostart_check_mode_controlled_apply_candidate_command_id" + ] == "windows99_vmware_autostart_power_readback_check_mode" + assert state[ + "windows99_vmware_autostart_check_mode_playbook_trust_writeback_ref" + ].startswith("playbook://awoooi/windows99-vmware-autostart/") assert state["windows99_vmx_source_locator_status"] == ( "collector_readback_verified_source_required_before_relink_dry_run" ) @@ -877,6 +924,18 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a assert payload["summary"]["windows99_vmx_source_repair_mcp_evidence_ref"].startswith( "mcp://awoooi/windows99/" ) + assert payload["summary"][ + "windows99_vmware_autostart_check_mode_package_ready" + ] is True + assert payload["summary"][ + "windows99_vmware_autostart_check_mode_work_order_id" + ] == "windows99-vmware-autostart-check-mode-111" + assert payload["summary"][ + "windows99_vmware_autostart_check_mode_next_executable_step" + ] == "build_vmware_autostart_power_readback_check_mode" + assert payload["rollups"][ + "windows99_vmware_autostart_check_mode_apply_allowed_by_this_package" + ] is False assert ( payload["rollups"]["windows99_vmx_source_repair_rollback_required"] is True ) 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 ec33ee59b..035f1989f 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 @@ -637,6 +637,30 @@ def test_reboot_auto_recovery_slo_scorecard_does_not_reopen_windows99_config_whe assert payload["readback"]["windows99_update_no_auto_reboot_ready"] is True assert payload["rollups"]["windows99_vmware_config_ready"] is True assert payload["rollups"]["windows99_update_no_auto_reboot_ready"] is True + autostart_package = payload["windows99_vmware_autostart_check_mode_package"] + assert autostart_package["schema_version"] == ( + "windows99_vmware_autostart_check_mode_package_v1" + ) + assert autostart_package["package_ready"] is True + assert autostart_package["powered_off_aliases"] == [ + "110", + "112", + "120", + "121", + "188", + ] + assert autostart_package["work_order_id"] == ( + "windows99-vmware-autostart-check-mode-110" + ) + assert autostart_package["action_stage"] == "power_readback_check_mode_ready" + assert autostart_package["apply_allowed_by_this_package"] is False + assert autostart_package["vm_power_change_allowed"] is False + assert payload["readback"][ + "windows99_vmware_autostart_check_mode_package_ready" + ] is True + assert payload["rollups"][ + "windows99_vmware_autostart_check_mode_package_status" + ] == "check_mode_package_ready_windows99_vmware_guest_power_readback_required" def test_reboot_auto_recovery_slo_scorecard_surfaces_windows99_console_artifact_blocker( diff --git a/apps/api/tests/test_windows99_vmware_autostart_check_mode_package_api.py b/apps/api/tests/test_windows99_vmware_autostart_check_mode_package_api.py new file mode 100644 index 000000000..44841139e --- /dev/null +++ b/apps/api/tests/test_windows99_vmware_autostart_check_mode_package_api.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.api.v1 import agents +from src.api.v1.agents import router +from src.services.windows99_vmware_autostart_check_mode_package import ( + build_windows99_vmware_autostart_check_mode_package, +) + + +def _scorecard() -> dict: + return { + "status": "blocked_reboot_auto_recovery_slo_not_ready", + "generated_at": "2026-07-03T14:26:00+08:00", + "windows99_vmware_autostart": { + "required_vm_aliases": ["110", "188", "120", "121", "112"], + "missing_vmx_aliases": [], + "powered_off_aliases": ["110", "112", "120", "121", "188"], + "config_ready": True, + "windows_update_no_auto_reboot_ready": True, + }, + "readback": { + "windows99_no_secret_collector_status": ( + "collected_windows99_vmware_verify_stdout" + ), + "windows99_no_secret_collector_readback_present": True, + "windows99_no_secret_collector_ssh_batchmode_auth_ready": True, + "windows99_no_secret_collector_remote_verify_attempted": True, + "windows99_no_secret_collector_safe_next_step": ( + "rerun_no_secret_collector_and_reboot_slo_scorecard_verify_only" + ), + }, + "windows99_management_channel": { + "remote_execution_channel_ready": False, + "can_collect_vmware_verify_without_secret": False, + "winrm_http_open": False, + "winrm_https_open": False, + "local_console_channel_reachable": True, + "console_collection_channels": ["rdp_console", "hyperv_vmconnect"], + "ssh_batch": { + "checked": True, + "ready": False, + "status": "permission_denied", + }, + }, + } + + +def test_windows99_vmware_autostart_check_mode_package_builder_is_no_power_write() -> None: + payload = build_windows99_vmware_autostart_check_mode_package( + _scorecard(), + generated_at="2026-07-03T14:27:00+08:00", + ) + + assert payload["schema_version"] == ( + "windows99_vmware_autostart_check_mode_package_v1" + ) + assert payload["status"] == ( + "check_mode_package_ready_windows99_vmware_guest_power_readback_required" + ) + assert payload["package_ready"] is True + assert payload["target_host_alias"] == "99" + assert payload["powered_off_aliases"] == ["110", "112", "120", "121", "188"] + assert payload["powered_off_count"] == 5 + assert payload["safe_next_step"] == ( + "build_windows99_vmware_autostart_check_mode_package_for_powered_" + "off_aliases_then_rerun_no_secret_collector_no_vm_power_change" + ) + assert payload["controlled_apply_mode"] == ( + "check_mode_package_only_no_windows_or_vm_write" + ) + assert payload["apply_allowed_by_this_package"] is False + assert payload["work_order_id"] == ( + "windows99-vmware-autostart-check-mode-110" + ) + assert payload["action_stage"] == "power_readback_check_mode_ready" + assert payload["next_executable_step"] == ( + "build_vmware_autostart_power_readback_check_mode" + ) + assert payload["target_vm_aliases"] == ["110", "112", "120", "121", "188"] + assert payload["check_mode_allowed_by_this_package"] is True + assert payload["check_mode_probe_command_count"] == 2 + assert payload["rollback_required"] is True + assert payload["vm_power_change_allowed"] is False + assert payload["windows_registry_apply_allowed"] is False + assert payload["scheduled_task_modify_allowed"] is False + assert payload["controlled_apply_candidate_status"] == ( + "candidate_waiting_autostart_policy_check_mode" + ) + assert payload["controlled_apply_candidate_executor"] == ( + "windows99_no_secret_management_channel_or_local_console" + ) + assert payload["controlled_apply_candidate_command_id"] == ( + "windows99_vmware_autostart_power_readback_check_mode" + ) + assert payload["post_apply_verifier_count"] == 5 + assert payload["learning_writeback_ref_count"] == 6 + assert payload["playbook_trust_writeback_ref"].startswith( + "playbook://awoooi/windows99-vmware-autostart/" + ) + assert payload["operation_boundaries"]["secret_value_read"] is False + assert payload["operation_boundaries"]["remote_write_performed"] is False + assert payload["operation_boundaries"]["vm_power_change_performed"] is False + assert ( + payload["operation_boundaries"]["windows_registry_apply_performed"] is False + ) + assert ( + payload["operation_boundaries"]["scheduled_task_modify_performed"] is False + ) + assert payload["collector_status"] == "collected_windows99_vmware_verify_stdout" + assert payload["collector_readback_present"] is True + assert payload["local_console_channel_reachable"] is True + assert payload["effective_check_mode_execution_ready"] is True + assert "vm_power_change" in payload["forbidden_actions"] + assert "scheduled_task_register_or_modify" in payload["forbidden_actions"] + + +def test_windows99_vmware_autostart_check_mode_package_endpoint_returns_readback( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr( + agents, + "load_latest_windows99_vmware_autostart_check_mode_package", + lambda: build_windows99_vmware_autostart_check_mode_package( + _scorecard(), + generated_at="2026-07-03T14:27:00+08:00", + ), + ) + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.get( + "/api/v1/agents/windows99-vmware-autostart-check-mode-package" + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["schema_version"] == ( + "windows99_vmware_autostart_check_mode_package_v1" + ) + assert payload["work_order_id"] == ( + "windows99-vmware-autostart-check-mode-110" + ) + assert payload["public_display_only"] is True + assert payload["check_mode_package_execution_authorized_by_this_endpoint"] is False + assert payload["apply_allowed_by_this_package"] is False + assert payload["operation_boundaries"]["vm_power_change_performed"] is False + assert payload["action_chain"]["execution_boundary"][ + "vm_power_change_allowed_by_this_package" + ] is False diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index f6192627b..ab2f6771a 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,24 @@ +## 2026-07-03 — P0-006 Windows99 VMware autostart check-mode package + +**完成內容**: +- 新增 `windows99_vmware_autostart_check_mode_package_v1`,把 powered-off VM aliases 從 VMX source repair package 中拆成獨立 check-mode package;目前 target aliases 由 runtime/scorecard `powered_off_aliases` 決定,不再把 missing VMX 與 guest power readback 混成同一工作項。 +- `/api/v1/agents/reboot-auto-recovery-slo-scorecard` 新增 `windows99_vmware_autostart_check_mode_package` 與 readback / rollups 欄位,包含 package status、safe next step、powered-off aliases、post-verifier count、learning writeback ref count 與 executor。 +- 新增 `/api/v1/agents/windows99-vmware-autostart-check-mode-package`,可直接讀回 target selector、check-mode、controlled apply candidate、execution boundary、rollback、post-verifier 與 LOG / KM / RAG / MCP / PlayBook writeback refs。 +- `awoooi-priority-work-order-readback` 會把 autostart check-mode package 投影到 `mainline_execution_state`、P0-006 evidence、summary 與 rollups,Work Item 不再只看到 VMX source repair 欄位。 + +**已跑驗證**: +- `DATABASE_URL=postgresql+asyncpg://test:test@localhost/test PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_windows99_vmware_autostart_check_mode_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`:`41 passed`。 +- `python3.11 -m py_compile apps/api/src/services/reboot_auto_recovery_slo_scorecard.py apps/api/src/services/windows99_vmware_autostart_check_mode_package.py apps/api/src/services/awoooi_priority_work_order_readback.py apps/api/src/api/v1/agents.py apps/api/tests/test_windows99_vmware_autostart_check_mode_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`:通過。 +- `python3.11 ops/runner/guard-gitea-runner-pressure.py --root .`:`GITEA_RUNNER_PRESSURE_GUARD_OK workflow_files=12 scheduled_workflows=4 auto_branch_events_on_110=0 generic_runner_labels=0`。 +- `git diff --check`:通過。 + +**仍維持**: +- 本輪只新增 source/API/readback/check-mode package,不啟動或關閉 VM、不改 Windows registry / scheduled task、不重啟 host / service、不碰 Docker / Nginx / K3s / DB / firewall;未讀 secret / token / `.env` / raw sessions / SQLite / auth;未使用 GitHub / gh;未 workflow_dispatch;未 DROP / TRUNCATE / restore / prune / delete / force push。 +- P0-006 尚未完成:剩餘 blocker 仍是 fresh all-host reboot event / 10-minute window / host reachability / `reboot_event_required_host_unreachable` / Windows99 guest power readback。 + +**下一步**: +- commit / push 到 Gitea main;等待 CD deploy marker 後讀回 production `/api/v1/agents/reboot-auto-recovery-slo-scorecard`、`/api/v1/agents/windows99-vmware-autostart-check-mode-package` 與 `/api/v1/agents/awoooi-priority-work-order-readback`,確認 autostart package 欄位在 production 可見。 + ## 2026-07-03 — 14:26 Windows99 runtime metric overlay production readback 收斂 **完成內容**: