From f29bc594eaa4e775e1fcbb3dbc850e7a9154b220 Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 10 Jul 2026 02:13:40 +0800 Subject: [PATCH] fix(windows99): project scoped relink dry run --- .../windows99_vmx_source_locator_readback.py | 116 ++++++++++++++++++ ...ndows99_vmx_source_locator_readback_api.py | 52 ++++++++ 2 files changed, 168 insertions(+) diff --git a/apps/api/src/services/windows99_vmx_source_locator_readback.py b/apps/api/src/services/windows99_vmx_source_locator_readback.py index 71d00900c..f45fe6a9f 100644 --- a/apps/api/src/services/windows99_vmx_source_locator_readback.py +++ b/apps/api/src/services/windows99_vmx_source_locator_readback.py @@ -35,6 +35,9 @@ _CANDIDATE_CONFIRMATION_VALIDATION_SCHEMA_VERSION = ( _CANDIDATE_CONFIRMATION_PACKET_SCHEMA_VERSION = ( "windows99_vmx_source_candidate_confirmation_packet_v1" ) +_SCOPED_RELINK_DRY_RUN_SCHEMA_VERSION = ( + "windows99_vmx_source_scoped_relink_dry_run_v1" +) _RECEIPT_FILE = "windows99-vmx-source-locator-receipt.snapshot.json" _COLLECTOR_SCRIPT_PATH = "scripts/reboot-recovery/locate-windows99-vmx-source.sh" _LOCATOR_SCRIPT_PATH = "scripts/reboot-recovery/windows99-vmx-source-locator.ps1" @@ -289,6 +292,104 @@ def _build_candidate_confirmation_gate( } +def _build_projected_scoped_relink_dry_run_plan( + *, + accepted: bool, + locator: dict[str, Any], + packet: dict[str, Any], +) -> dict[str, Any]: + target_aliases = _strings(locator.get("target_vm_aliases")) + missing_aliases = _strings(locator.get("missing_vmx_aliases")) or target_aliases + evidence_ref = ( + str(packet.get("candidate_identity_evidence_ref") or "").strip() + if accepted + else "" + ) + candidate_identity_kind = ( + str(packet.get("candidate_identity_kind") or "").strip() if accepted else "" + ) + ready = bool(accepted) + existing_plan = _dict(locator.get("controlled_relink_dry_run_plan")) + + return { + "schema_version": _SCOPED_RELINK_DRY_RUN_SCHEMA_VERSION, + "plan_id": ( + "windows99-vmx-source-relink-dry-run-" + f"{','.join(missing_aliases) or 'none'}" + ), + "status": ( + "ready_for_no_write_review" + if ready + else "blocked_candidate_confirmation_not_accepted" + ), + "ready": ready, + "mode": "dry_run_no_windows_or_vm_write", + "target_selector": { + "host_alias": "99", + "vm_aliases": target_aliases, + "missing_vmx_aliases": missing_aliases, + "candidate_identity_kind": candidate_identity_kind, + "secret_collection_allowed": False, + "vm_power_change_allowed": False, + "runtime_write_allowed": False, + }, + "source_diff": { + "expected_vmx_path_count": locator.get("expected_vmx_path_count"), + "expected_vmx_path_present_count": locator.get( + "expected_vmx_path_present_count" + ), + "candidate_source_count": locator.get("candidate_source_count"), + "candidate_source_fingerprint_count": locator.get( + "candidate_source_fingerprint_count" + ), + "alias_hint_candidate_count": locator.get("alias_hint_candidate_count"), + "identity_target_hint_count": locator.get("identity_target_hint_count"), + "identity_unassigned_ubuntu_candidate_count": locator.get( + "identity_unassigned_ubuntu_candidate_count" + ), + "candidate_identity_evidence_ref": evidence_ref, + "raw_path_output": False, + "path_fingerprint_only": True, + "bounded_identity_metadata_only": True, + }, + "pre_apply_backup_plan": { + "required": True, + "status": ( + "review_required_before_apply" + if ready + else "blocked_candidate_confirmation_not_accepted" + ), + "backup_ref_required": True, + "rollback_ref": "rollback://awoooi/windows99-vmx-source-relink-dry-run", + }, + "rollback_ref": "rollback://awoooi/windows99-vmx-source-relink-dry-run", + "post_verifier": [ + "bash scripts/reboot-recovery/collect-windows99-vmware-verify.sh --collect", + "GET /api/v1/agents/reboot-auto-recovery-slo-scorecard", + "GET /api/v1/agents/windows99-vmx-source-repair-package", + "GET /api/v1/agents/windows99-vmx-source-locator-readback", + ], + "km_writeback_ref": ( + str(existing_plan.get("km_writeback_ref") or "") + or "km://awoooi/reboot-slo/windows99-vmx-source-locator-111" + ), + "playbook_trust_writeback_ref": ( + str(existing_plan.get("playbook_trust_writeback_ref") or "") + or "playbook://awoooi/windows99-vmx-source-locator/windows99-vmx-source-locator-111" + ), + "apply_allowed_by_this_plan": False, + "remote_write_performed": False, + "vm_power_change_performed": False, + "secret_value_read": False, + "request_payload_saved": False, + "safe_next_step": ( + "review_scoped_relink_dry_run_then_run_post_verifier_no_vm_power_change" + if ready + else "submit_redacted_single_candidate_identity_evidence_without_raw_paths" + ), + } + + def validate_windows99_vmx_source_candidate_confirmation_packet( packet: dict[str, Any], *, @@ -362,6 +463,13 @@ def validate_windows99_vmx_source_candidate_confirmation_packet( if accepted else "submit_redacted_single_candidate_identity_evidence_without_raw_paths" ) + projected_scoped_relink_dry_run_plan = ( + _build_projected_scoped_relink_dry_run_plan( + accepted=accepted, + locator=locator, + packet=packet, + ) + ) return { "schema_version": _CANDIDATE_CONFIRMATION_VALIDATION_SCHEMA_VERSION, @@ -398,6 +506,14 @@ def validate_windows99_vmx_source_candidate_confirmation_packet( "secret_value_read": False, "safe_next_step": next_safe_step, }, + "scoped_relink_dry_run_ready": accepted, + "pre_apply_backup_plan_ready": accepted, + "post_verifier_ready": accepted, + "km_writeback_ready": accepted, + "playbook_trust_writeback_ready": accepted, + "projected_scoped_relink_dry_run_plan": ( + projected_scoped_relink_dry_run_plan + ), "operation_boundaries": { "no_persist_validator": True, "request_payload_saved": False, diff --git a/apps/api/tests/test_windows99_vmx_source_locator_readback_api.py b/apps/api/tests/test_windows99_vmx_source_locator_readback_api.py index c971ebab3..e4133ffa3 100644 --- a/apps/api/tests/test_windows99_vmx_source_locator_readback_api.py +++ b/apps/api/tests/test_windows99_vmx_source_locator_readback_api.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import os os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") @@ -273,6 +274,40 @@ def test_windows99_candidate_confirmation_validator_accepts_redacted_packet() -> assert payload["projected_confirmation_gate"][ "apply_allowed_by_confirmation_gate" ] is False + assert payload["scoped_relink_dry_run_ready"] is True + assert payload["pre_apply_backup_plan_ready"] is True + assert payload["post_verifier_ready"] is True + assert payload["km_writeback_ready"] is True + assert payload["playbook_trust_writeback_ready"] is True + plan = payload["projected_scoped_relink_dry_run_plan"] + assert plan["schema_version"] == "windows99_vmx_source_scoped_relink_dry_run_v1" + assert plan["status"] == "ready_for_no_write_review" + assert plan["ready"] is True + assert plan["mode"] == "dry_run_no_windows_or_vm_write" + assert plan["target_selector"]["host_alias"] == "99" + assert plan["target_selector"]["vm_aliases"] == ["111"] + assert plan["target_selector"]["missing_vmx_aliases"] == ["111"] + assert plan["target_selector"]["secret_collection_allowed"] is False + assert plan["target_selector"]["vm_power_change_allowed"] is False + assert plan["target_selector"]["runtime_write_allowed"] is False + assert plan["source_diff"]["candidate_identity_evidence_ref"] == ( + "console://awoooi/windows99/candidate-identity-redacted-111" + ) + assert plan["source_diff"]["raw_path_output"] is False + assert plan["source_diff"]["path_fingerprint_only"] is True + assert plan["pre_apply_backup_plan"]["required"] is True + assert plan["pre_apply_backup_plan"]["backup_ref_required"] is True + assert plan["rollback_ref"] == ( + "rollback://awoooi/windows99-vmx-source-relink-dry-run" + ) + assert "GET /api/v1/agents/windows99-vmx-source-locator-readback" in plan[ + "post_verifier" + ] + assert plan["apply_allowed_by_this_plan"] is False + assert plan["remote_write_performed"] is False + assert plan["vm_power_change_performed"] is False + assert plan["secret_value_read"] is False + assert plan["request_payload_saved"] is False assert payload["operation_boundaries"]["request_payload_saved"] is False assert payload["operation_boundaries"]["remote_write_performed"] is False assert payload["operation_boundaries"]["vm_power_change_performed"] is False @@ -325,6 +360,14 @@ def test_windows99_candidate_confirmation_validator_rejects_sensitive_payload() assert payload["projected_confirmation_gate"][ "can_prepare_scoped_relink_dry_run" ] is False + assert payload["scoped_relink_dry_run_ready"] is False + plan = payload["projected_scoped_relink_dry_run_plan"] + assert plan["ready"] is False + assert plan["status"] == "blocked_candidate_confirmation_not_accepted" + assert plan["source_diff"]["candidate_identity_evidence_ref"] == "" + serialized = json.dumps(payload) + assert "D:\\" not in serialized + assert "192.168.0." not in serialized assert payload["operation_boundaries"]["apply_gate_opened"] is False @@ -453,5 +496,14 @@ def test_windows99_candidate_confirmation_endpoint_is_no_persist_validator( assert payload["operation_boundaries"]["request_payload_saved"] is False assert payload["operation_boundaries"]["apply_gate_opened"] is False assert payload["operation_boundaries"]["vm_power_change_performed"] is False + assert payload["scoped_relink_dry_run_ready"] is True + plan = payload["projected_scoped_relink_dry_run_plan"] + assert plan["ready"] is True + assert plan["mode"] == "dry_run_no_windows_or_vm_write" + assert plan["source_diff"]["candidate_identity_evidence_ref"] == ( + "backup://awoooi/windows99/redacted-fingerprint-111" + ) + assert plan["apply_allowed_by_this_plan"] is False + assert plan["request_payload_saved"] is False assert "D:\\" not in response.text assert "192.168.0." not in response.text