fix(windows99): surface scoped relink receipt readback
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Failing after 1m41s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped

This commit is contained in:
ogt
2026-07-10 10:11:09 +08:00
parent 42ce3d54be
commit acbf26e089
3 changed files with 338 additions and 21 deletions

View File

@@ -48,6 +48,9 @@ _SCOPED_RELINK_DRY_RUN_RECEIPT_SCHEMA_VERSION = (
"windows99_vmx_source_scoped_relink_dry_run_receipt_v1"
)
_RECEIPT_FILE = "windows99-vmx-source-locator-receipt.snapshot.json"
_SCOPED_RELINK_DRY_RUN_RECEIPT_FILE = (
"windows99-vmx-source-scoped-relink-dry-run-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"
_CANDIDATE_CONFIRMATION_REQUIRED_FIELDS = [
@@ -200,6 +203,23 @@ def _load_locator_receipt(operations_dir: Path | None = None) -> dict[str, Any]:
return payload
def _load_scoped_relink_dry_run_receipt(
operations_dir: Path | None = None,
) -> dict[str, Any]:
path = (operations_dir or _DEFAULT_OPERATIONS_DIR) / _SCOPED_RELINK_DRY_RUN_RECEIPT_FILE
if not path.exists():
return {}
payload = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(payload, dict):
raise ValueError(f"{path}: expected JSON object")
if payload.get("schema_version") != _SCOPED_RELINK_DRY_RUN_RECEIPT_SCHEMA_VERSION:
raise ValueError(f"{path}: unexpected schema_version")
leaked = _forbidden_fragments(payload)
if leaked:
raise ValueError(f"{path}: unsafe scoped relink receipt fragments: {leaked}")
return payload
def _vm_rows_for_aliases(
repair_package: dict[str, Any],
target_aliases: list[str],
@@ -821,6 +841,7 @@ def build_windows99_vmx_source_locator_readback(
*,
repair_package: dict[str, Any] | None = None,
collector_receipt: dict[str, Any] | None = None,
scoped_relink_dry_run_receipt: dict[str, Any] | None = None,
operations_dir: Path | None = None,
generated_at: str | None = None,
) -> dict[str, Any]:
@@ -873,8 +894,21 @@ def build_windows99_vmx_source_locator_readback(
"--collect"
)
receipt = collector_receipt if collector_receipt is not None else _load_locator_receipt(operations_dir)
receipt = (
collector_receipt
if collector_receipt is not None
else _load_locator_receipt(operations_dir)
)
receipt = _dict(receipt)
scoped_receipt = (
_dict(scoped_relink_dry_run_receipt)
if scoped_relink_dry_run_receipt is not None
else (
_load_scoped_relink_dry_run_receipt(operations_dir)
if collector_receipt is None
else {}
)
)
receipt_aliases = _strings(receipt.get("target_vm_aliases"))
receipt_applies = bool(
receipt.get("collector_readback_present") is True
@@ -882,6 +916,51 @@ def build_windows99_vmx_source_locator_readback(
and receipt_aliases
and set(receipt_aliases).issubset(set(target_aliases or receipt_aliases))
)
scoped_receipt_target_selector = _dict(scoped_receipt.get("target_selector"))
scoped_receipt_source_diff = _dict(scoped_receipt.get("source_of_truth_diff"))
scoped_receipt_operation_boundaries = _dict(
scoped_receipt.get("operation_boundaries")
)
scoped_receipt_aliases = _strings(scoped_receipt.get("target_vm_aliases")) or _strings(
scoped_receipt_target_selector.get("vm_aliases")
)
scoped_receipt_false_flags = [
"request_payload_saved",
"secret_value_read",
"raw_path_output",
"vmx_file_content_read",
"remote_write_performed",
"runtime_write_performed",
"host_reboot_performed",
"vm_power_change_performed",
"windows_registry_apply_performed",
"scheduled_task_write_performed",
"apply_gate_opened",
]
scoped_receipt_boundaries_clean = all(
scoped_receipt_operation_boundaries.get(
field,
scoped_receipt.get(field, False),
)
is False
for field in scoped_receipt_false_flags
)
scoped_receipt_applies = bool(
scoped_receipt
and scoped_receipt.get("schema_version")
== _SCOPED_RELINK_DRY_RUN_RECEIPT_SCHEMA_VERSION
and scoped_receipt.get("target_host_alias") == "99"
and scoped_receipt_aliases
and set(scoped_receipt_aliases).issubset(
set(missing_aliases or target_aliases or scoped_receipt_aliases)
)
and str(scoped_receipt.get("plan_id") or "")
== _scoped_relink_plan_id(scoped_receipt_aliases)
and scoped_receipt.get("ready") is True
and scoped_receipt.get("reviewer_validation_passed") is True
and scoped_receipt.get("controlled_relink_dry_run_ready") is True
and scoped_receipt_boundaries_clean
)
receipt_locate_status = str(receipt.get("locate_status") or "")
expected_vmx_path_present_count = (
_int(receipt.get("expected_vmx_path_present_count"))
@@ -920,7 +999,8 @@ def build_windows99_vmx_source_locator_readback(
receipt_applies and "requires_confirmation" in receipt_locate_status
)
controlled_relink_dry_run_ready = bool(
receipt_applies and receipt.get("controlled_relink_dry_run_ready") is True
(receipt_applies and receipt.get("controlled_relink_dry_run_ready") is True)
or scoped_receipt_applies
)
verified_source_or_identity_evidence_required = bool(
receipt_applies
@@ -928,13 +1008,19 @@ def build_windows99_vmx_source_locator_readback(
and not controlled_relink_dry_run_ready
)
controlled_relink_dry_run_blocker = ""
if receipt_applies:
if receipt_applies or scoped_receipt_applies:
if controlled_relink_dry_run_ready:
status = "collector_readback_scoped_relink_dry_run_ready"
next_step = "run_scoped_relink_dry_run_no_windows_or_vm_write"
safe_next_step = (
"run_scoped_relink_dry_run_no_windows_or_vm_write_then_post_verifier"
)
if scoped_receipt_applies:
next_step = "rerun_no_secret_collector_and_scorecard"
safe_next_step = (
"rerun_no_secret_collector_and_scorecard_no_vm_power_change"
)
else:
next_step = "run_scoped_relink_dry_run_no_windows_or_vm_write"
safe_next_step = (
"run_scoped_relink_dry_run_no_windows_or_vm_write_then_post_verifier"
)
elif verified_source_or_identity_evidence_required:
status = "collector_readback_verified_source_required_before_relink_dry_run"
next_step = (
@@ -957,11 +1043,21 @@ def build_windows99_vmx_source_locator_readback(
)
controlled_relink_dry_run_blocker = "scoped_relink_candidate_not_verified"
relink_dry_run_plan_aliases = (
scoped_receipt_aliases if scoped_receipt_applies else missing_aliases
)
relink_dry_run_plan = {
"plan_id": f"windows99-vmx-source-relink-dry-run-{','.join(missing_aliases) or 'none'}",
"plan_id": _scoped_relink_plan_id(relink_dry_run_plan_aliases),
"mode": "dry_run_no_windows_or_vm_write",
"ready": controlled_relink_dry_run_ready,
"blocker": controlled_relink_dry_run_blocker,
"reviewer_validation_passed": scoped_receipt_applies,
"reviewer_validation_status": str(scoped_receipt.get("status") or "")
if scoped_receipt_applies
else "",
"receipt_ref": str(scoped_receipt.get("receipt_ref") or "")
if scoped_receipt_applies
else "",
"target_selector": {
"host_alias": "99",
"vm_aliases": target_aliases,
@@ -982,6 +1078,19 @@ def build_windows99_vmx_source_locator_readback(
"identity_unassigned_ubuntu_candidate_count": (
identity_unassigned_ubuntu_candidate_count
),
"candidate_identity_evidence_ref": str(
scoped_receipt_source_diff.get("candidate_identity_evidence_ref") or ""
)
if scoped_receipt_applies
else "",
"source_fingerprint_ref": str(
scoped_receipt_source_diff.get("source_fingerprint_ref") or ""
)
if scoped_receipt_applies
else "",
"backup_ref": str(scoped_receipt_source_diff.get("backup_ref") or "")
if scoped_receipt_applies
else "",
"raw_path_output": False,
"path_fingerprint_only": True,
"bounded_identity_metadata_only": bool(
@@ -1075,6 +1184,20 @@ def build_windows99_vmx_source_locator_readback(
),
"controlled_relink_dry_run_ready": controlled_relink_dry_run_ready,
"controlled_relink_dry_run_blocker": controlled_relink_dry_run_blocker,
"controlled_relink_dry_run_receipt_present": scoped_receipt_applies,
"controlled_relink_dry_run_receipt_ref": str(
scoped_receipt.get("receipt_ref") or ""
)
if scoped_receipt_applies
else "",
"controlled_relink_dry_run_reviewer_validation_passed": (
scoped_receipt_applies
),
"controlled_relink_dry_run_validation_status": str(
scoped_receipt.get("status") or ""
)
if scoped_receipt_applies
else "",
"controlled_relink_dry_run_plan": relink_dry_run_plan,
"candidate_confirmation_gate": candidate_confirmation_gate,
"candidate_confirmation_status": str(
@@ -1085,10 +1208,12 @@ def build_windows99_vmx_source_locator_readback(
),
"candidate_identity_evidence_sufficient": bool(
candidate_confirmation_gate.get("identity_evidence_sufficient") is True
or scoped_receipt_applies
),
"candidate_relink_dry_run_ready": bool(
candidate_confirmation_gate.get("can_prepare_scoped_relink_dry_run")
is True
or scoped_receipt_applies
),
"candidate_confirmation_missing_evidence": _strings(
candidate_confirmation_gate.get("missing_evidence")