fix(reboot): gate windows99 vmx candidate confirmation
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m25s
CD Pipeline / build-and-deploy (push) Successful in 5m23s
CD Pipeline / post-deploy-checks (push) Successful in 1m47s

This commit is contained in:
Your Name
2026-07-03 13:13:51 +08:00
parent bcaf6c3a26
commit 232d680962
10 changed files with 471 additions and 4 deletions

View File

@@ -3509,6 +3509,45 @@ def _enrich_from_current_readbacks(
state["windows99_vmx_source_locator_relink_dry_run_plan_id"] = str(
locator_relink_plan.get("plan_id") or ""
)
state["windows99_vmx_source_locator_candidate_confirmation_status"] = str(
windows99_vmx_source_locator_readback.get("candidate_confirmation_status")
or "unknown"
)
state["windows99_vmx_source_locator_candidate_identity_decision"] = str(
windows99_vmx_source_locator_readback.get("candidate_identity_decision")
or "unknown"
)
state["windows99_vmx_source_locator_candidate_identity_evidence_sufficient"] = bool(
windows99_vmx_source_locator_readback.get(
"candidate_identity_evidence_sufficient"
)
is True
)
state["windows99_vmx_source_locator_candidate_relink_dry_run_ready"] = bool(
windows99_vmx_source_locator_readback.get("candidate_relink_dry_run_ready")
is True
)
state["windows99_vmx_source_locator_candidate_confirmation_missing_evidence"] = (
_strings(
windows99_vmx_source_locator_readback.get(
"candidate_confirmation_missing_evidence"
)
)
)
state["windows99_vmx_source_locator_candidate_confirmation_safe_next_step"] = str(
windows99_vmx_source_locator_readback.get(
"candidate_confirmation_safe_next_step"
)
or ""
)
state[
"windows99_vmx_source_locator_apply_allowed_by_candidate_confirmation_gate"
] = bool(
windows99_vmx_source_locator_readback.get(
"apply_allowed_by_candidate_confirmation_gate"
)
is True
)
state["windows99_vmx_source_locator_candidate_source_count_known"] = bool(
windows99_vmx_source_locator_readback.get("candidate_source_count_known")
is True
@@ -3948,6 +3987,35 @@ def _enrich_from_current_readbacks(
evidence["windows99_vmx_source_locator_relink_dry_run_plan_id"] = state[
"windows99_vmx_source_locator_relink_dry_run_plan_id"
]
evidence["windows99_vmx_source_locator_candidate_confirmation_status"] = state[
"windows99_vmx_source_locator_candidate_confirmation_status"
]
evidence["windows99_vmx_source_locator_candidate_identity_decision"] = state[
"windows99_vmx_source_locator_candidate_identity_decision"
]
evidence[
"windows99_vmx_source_locator_candidate_identity_evidence_sufficient"
] = state[
"windows99_vmx_source_locator_candidate_identity_evidence_sufficient"
]
evidence["windows99_vmx_source_locator_candidate_relink_dry_run_ready"] = state[
"windows99_vmx_source_locator_candidate_relink_dry_run_ready"
]
evidence[
"windows99_vmx_source_locator_candidate_confirmation_missing_evidence"
] = state[
"windows99_vmx_source_locator_candidate_confirmation_missing_evidence"
]
evidence[
"windows99_vmx_source_locator_candidate_confirmation_safe_next_step"
] = state[
"windows99_vmx_source_locator_candidate_confirmation_safe_next_step"
]
evidence[
"windows99_vmx_source_locator_apply_allowed_by_candidate_confirmation_gate"
] = state[
"windows99_vmx_source_locator_apply_allowed_by_candidate_confirmation_gate"
]
evidence["windows99_vmx_source_locator_candidate_source_count_known"] = state[
"windows99_vmx_source_locator_candidate_source_count_known"
]
@@ -4765,6 +4833,43 @@ def _set_rollups_and_summary(
"windows99_vmx_source_locator_relink_dry_run_plan_id": str(
state.get("windows99_vmx_source_locator_relink_dry_run_plan_id") or ""
),
"windows99_vmx_source_locator_candidate_confirmation_status": str(
state.get(
"windows99_vmx_source_locator_candidate_confirmation_status"
)
or "unknown"
),
"windows99_vmx_source_locator_candidate_identity_decision": str(
state.get("windows99_vmx_source_locator_candidate_identity_decision")
or "unknown"
),
"windows99_vmx_source_locator_candidate_identity_evidence_sufficient": (
state.get(
"windows99_vmx_source_locator_candidate_identity_evidence_sufficient"
)
is True
),
"windows99_vmx_source_locator_candidate_relink_dry_run_ready": (
state.get("windows99_vmx_source_locator_candidate_relink_dry_run_ready")
is True
),
"windows99_vmx_source_locator_candidate_confirmation_missing_evidence": _strings(
state.get(
"windows99_vmx_source_locator_candidate_confirmation_missing_evidence"
)
),
"windows99_vmx_source_locator_candidate_confirmation_safe_next_step": str(
state.get(
"windows99_vmx_source_locator_candidate_confirmation_safe_next_step"
)
or ""
),
"windows99_vmx_source_locator_apply_allowed_by_candidate_confirmation_gate": (
state.get(
"windows99_vmx_source_locator_apply_allowed_by_candidate_confirmation_gate"
)
is True
),
"windows99_vmx_source_locator_candidate_source_count_known": (
state.get("windows99_vmx_source_locator_candidate_source_count_known")
is True
@@ -5112,6 +5217,43 @@ def _set_rollups_and_summary(
"windows99_vmx_source_locator_relink_dry_run_plan_id": str(
state.get("windows99_vmx_source_locator_relink_dry_run_plan_id") or ""
),
"windows99_vmx_source_locator_candidate_confirmation_status": str(
state.get(
"windows99_vmx_source_locator_candidate_confirmation_status"
)
or "unknown"
),
"windows99_vmx_source_locator_candidate_identity_decision": str(
state.get("windows99_vmx_source_locator_candidate_identity_decision")
or "unknown"
),
"windows99_vmx_source_locator_candidate_identity_evidence_sufficient": (
state.get(
"windows99_vmx_source_locator_candidate_identity_evidence_sufficient"
)
is True
),
"windows99_vmx_source_locator_candidate_relink_dry_run_ready": (
state.get("windows99_vmx_source_locator_candidate_relink_dry_run_ready")
is True
),
"windows99_vmx_source_locator_candidate_confirmation_missing_evidence": _strings(
state.get(
"windows99_vmx_source_locator_candidate_confirmation_missing_evidence"
)
),
"windows99_vmx_source_locator_candidate_confirmation_safe_next_step": str(
state.get(
"windows99_vmx_source_locator_candidate_confirmation_safe_next_step"
)
or ""
),
"windows99_vmx_source_locator_apply_allowed_by_candidate_confirmation_gate": (
state.get(
"windows99_vmx_source_locator_apply_allowed_by_candidate_confirmation_gate"
)
is True
),
"windows99_vmx_source_locator_candidate_source_count_known": (
state.get("windows99_vmx_source_locator_candidate_source_count_known")
is True

View File

@@ -26,6 +26,9 @@ _SCHEMA_VERSION = "windows99_vmx_source_locator_readback_v1"
_COLLECTOR_SCHEMA_VERSION = "windows99_vmx_source_locator_collector_v1"
_LOCATOR_SCHEMA_VERSION = "windows99_vmx_source_locator_v1"
_RECEIPT_SCHEMA_VERSION = "windows99_vmx_source_locator_receipt_v1"
_CANDIDATE_CONFIRMATION_SCHEMA_VERSION = (
"windows99_vmx_source_candidate_confirmation_gate_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"
@@ -89,6 +92,120 @@ def _vm_rows_for_aliases(
return rows
def _build_candidate_confirmation_gate(
*,
receipt_applies: bool,
receipt: dict[str, Any],
target_aliases: list[str],
receipt_locate_status: str,
) -> dict[str, Any]:
alias_hint_count = _int(receipt.get("alias_hint_candidate_count"), 0) or 0
identity_target_hint_count = (
_int(receipt.get("identity_target_hint_count"), 0) or 0
)
identity_unassigned_count = (
_int(receipt.get("identity_unassigned_ubuntu_candidate_count"), 0) or 0
)
candidate_source_count = _int(receipt.get("candidate_source_count"), 0) or 0
candidate_confirmation_required = bool(
receipt_applies and "requires_confirmation" in receipt_locate_status
)
confirmed_candidate_count = 0
identity_evidence_sufficient = False
status = "not_required_no_candidate_confirmation"
decision = "not_required"
reason = "no active candidate confirmation gate"
safe_next_step = "run_locator_collect_or_keep_no_source_locator_required"
missing_evidence: list[str] = []
if not receipt_applies:
status = "blocked_locator_receipt_missing"
decision = "blocked"
reason = "locator receipt is not attached to the current target aliases"
safe_next_step = (
"run_no_secret_windows99_vmx_source_locator_collect_then_rerun_"
"candidate_confirmation_gate"
)
missing_evidence = ["collector_receipt"]
elif alias_hint_count == 1 or identity_target_hint_count == 1:
confirmed_candidate_count = 1
identity_evidence_sufficient = True
status = "candidate_identity_confirmed_ready_for_scoped_relink_dry_run"
decision = "confirmed_for_dry_run"
reason = "exactly one candidate carries target alias or target identity evidence"
safe_next_step = (
"build_scoped_relink_dry_run_with_rollback_and_post_verifier_"
"no_vm_power_change"
)
elif alias_hint_count > 1 or identity_target_hint_count > 1:
confirmed_candidate_count = alias_hint_count + identity_target_hint_count
status = "blocked_multiple_target_hint_candidates"
decision = "blocked"
reason = "multiple candidates carry target-like evidence"
safe_next_step = "collect_authorized_console_candidate_identity_evidence_then_rerun_gate"
missing_evidence = ["single_target_candidate"]
elif candidate_confirmation_required:
status = "blocked_single_unassigned_ubuntu_candidate_identity_not_confirmed"
decision = "blocked"
reason = (
"candidate is still generic Ubuntu evidence; no target alias, MAC, UUID, "
"or display-name proof ties it to 111"
)
safe_next_step = (
"collect_authorized_console_candidate_identity_evidence_or_verified_"
"backup_path_then_rerun_confirmation_gate"
)
missing_evidence = [
"target_alias_identity_hint",
"verified_backup_or_authorized_console_candidate_identity",
"pre_apply_backup_plan",
"scoped_relink_dry_run",
]
elif candidate_source_count <= 0:
status = "blocked_no_candidate_source_found"
decision = "blocked"
reason = "locator found no candidate source for the missing VMX alias"
safe_next_step = (
"provide_existing_vmx_source_or_verified_backup_path_then_rerun_"
"locator_check_mode"
)
missing_evidence = ["existing_vmx_source_or_verified_backup_path"]
can_prepare_scoped_relink_dry_run = bool(
identity_evidence_sufficient and confirmed_candidate_count == 1
)
return {
"schema_version": _CANDIDATE_CONFIRMATION_SCHEMA_VERSION,
"status": status,
"decision": decision,
"reason": reason,
"target_aliases": target_aliases,
"candidate_confirmation_required": candidate_confirmation_required,
"candidate_source_count": candidate_source_count,
"alias_hint_candidate_count": alias_hint_count,
"identity_target_hint_count": identity_target_hint_count,
"identity_unassigned_ubuntu_candidate_count": identity_unassigned_count,
"confirmed_candidate_count": confirmed_candidate_count,
"identity_evidence_sufficient": identity_evidence_sufficient,
"can_prepare_scoped_relink_dry_run": can_prepare_scoped_relink_dry_run,
"apply_allowed_by_confirmation_gate": False,
"vm_power_change_allowed": False,
"remote_write_performed": False,
"vm_power_change_performed": False,
"secret_value_read": False,
"safe_next_step": safe_next_step,
"missing_evidence": missing_evidence,
"required_evidence": [
"single_target_candidate_identity",
"source_path_fingerprint_or_authorized_console_artifact",
"pre_apply_backup_plan",
"scoped_relink_dry_run",
"post_apply_no_secret_verifier",
],
}
def build_windows99_vmx_source_locator_readback(
scorecard: dict[str, Any],
*,
@@ -282,6 +399,13 @@ def build_windows99_vmx_source_locator_readback(
"vm_power_change_performed": False,
}
candidate_confirmation_gate = _build_candidate_confirmation_gate(
receipt_applies=receipt_applies,
receipt=receipt,
target_aliases=target_aliases,
receipt_locate_status=receipt_locate_status,
)
return {
"schema_version": _SCHEMA_VERSION,
"generated_at": generated_at
@@ -342,6 +466,30 @@ 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_plan": relink_dry_run_plan,
"candidate_confirmation_gate": candidate_confirmation_gate,
"candidate_confirmation_status": str(
candidate_confirmation_gate.get("status") or "unknown"
),
"candidate_identity_decision": str(
candidate_confirmation_gate.get("decision") or "unknown"
),
"candidate_identity_evidence_sufficient": bool(
candidate_confirmation_gate.get("identity_evidence_sufficient") is True
),
"candidate_relink_dry_run_ready": bool(
candidate_confirmation_gate.get("can_prepare_scoped_relink_dry_run")
is True
),
"candidate_confirmation_missing_evidence": _strings(
candidate_confirmation_gate.get("missing_evidence")
),
"candidate_confirmation_safe_next_step": str(
candidate_confirmation_gate.get("safe_next_step") or ""
),
"apply_allowed_by_candidate_confirmation_gate": bool(
candidate_confirmation_gate.get("apply_allowed_by_confirmation_gate")
is True
),
"public_lan_topology_redacted": True,
"redacted_public_display_only": True,
"raw_path_output": False,