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 6f0617539..902ace2e5 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -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 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 55c949e03..60773dca3 100644 --- a/apps/api/src/services/windows99_vmx_source_locator_readback.py +++ b/apps/api/src/services/windows99_vmx_source_locator_readback.py @@ -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, 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 3ca434c29..398dc6804 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 @@ -787,6 +787,33 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a assert state["windows99_vmx_source_locator_relink_dry_run_plan_id"] == ( "windows99-vmx-source-relink-dry-run-111" ) + assert state["windows99_vmx_source_locator_candidate_confirmation_status"] == ( + "blocked_single_unassigned_ubuntu_candidate_identity_not_confirmed" + ) + assert state["windows99_vmx_source_locator_candidate_identity_decision"] == ( + "blocked" + ) + assert ( + state[ + "windows99_vmx_source_locator_candidate_identity_evidence_sufficient" + ] + is False + ) + assert state["windows99_vmx_source_locator_candidate_relink_dry_run_ready"] is False + assert state[ + "windows99_vmx_source_locator_candidate_confirmation_missing_evidence" + ] == [ + "target_alias_identity_hint", + "verified_backup_or_authorized_console_candidate_identity", + "pre_apply_backup_plan", + "scoped_relink_dry_run", + ] + assert ( + state[ + "windows99_vmx_source_locator_apply_allowed_by_candidate_confirmation_gate" + ] + is False + ) assert state["windows99_vmx_source_locator_candidate_source_count_known"] is True assert state["windows99_vmx_source_locator_collector_readback_present"] is True assert state["windows99_vmx_source_locator_file_content_read"] is True @@ -889,6 +916,30 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a ] is True ) + assert payload["summary"][ + "windows99_vmx_source_locator_candidate_confirmation_status" + ] == "blocked_single_unassigned_ubuntu_candidate_identity_not_confirmed" + assert payload["summary"][ + "windows99_vmx_source_locator_candidate_identity_decision" + ] == "blocked" + assert ( + payload["summary"][ + "windows99_vmx_source_locator_candidate_identity_evidence_sufficient" + ] + is False + ) + assert ( + payload["summary"][ + "windows99_vmx_source_locator_candidate_relink_dry_run_ready" + ] + is False + ) + assert ( + payload["rollups"][ + "windows99_vmx_source_locator_apply_allowed_by_candidate_confirmation_gate" + ] + is False + ) assert ( payload["summary"][ "windows99_vmx_source_locator_human_confirmation_required_for_dry_run" @@ -988,6 +1039,21 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a ] is True ) + assert in_progress["evidence"][ + "windows99_vmx_source_locator_candidate_confirmation_status" + ] == "blocked_single_unassigned_ubuntu_candidate_identity_not_confirmed" + assert ( + in_progress["evidence"][ + "windows99_vmx_source_locator_candidate_relink_dry_run_ready" + ] + is False + ) + assert ( + in_progress["evidence"][ + "windows99_vmx_source_locator_apply_allowed_by_candidate_confirmation_gate" + ] + is False + ) assert ( in_progress["evidence"][ "windows99_vmx_source_locator_human_confirmation_required_for_dry_run" 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 250bd7480..998f8f660 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 @@ -172,6 +172,23 @@ def test_windows99_vmx_source_locator_builder_projects_redacted_receipt() -> Non assert payload["controlled_relink_dry_run_plan"]["source_diff"][ "raw_path_output" ] is False + assert payload["candidate_confirmation_status"] == ( + "blocked_single_unassigned_ubuntu_candidate_identity_not_confirmed" + ) + assert payload["candidate_identity_decision"] == "blocked" + assert payload["candidate_identity_evidence_sufficient"] is False + assert payload["candidate_relink_dry_run_ready"] is False + assert payload["apply_allowed_by_candidate_confirmation_gate"] is False + assert payload["candidate_confirmation_missing_evidence"] == [ + "target_alias_identity_hint", + "verified_backup_or_authorized_console_candidate_identity", + "pre_apply_backup_plan", + "scoped_relink_dry_run", + ] + assert payload["candidate_confirmation_gate"]["schema_version"] == ( + "windows99_vmx_source_candidate_confirmation_gate_v1" + ) + assert payload["candidate_confirmation_gate"]["vm_power_change_allowed"] is False assert payload["next_executable_step"] == ( "provide_verified_vmx_source_or_identity_evidence_then_scoped_relink_dry_run" ) @@ -221,6 +238,9 @@ def test_windows99_vmx_source_locator_endpoint_returns_public_safe_readback( "windows99-vmx-source-relink-dry-run-111" ) assert payload["controlled_relink_dry_run_plan"]["remote_write_performed"] is False + assert payload["candidate_identity_evidence_sufficient"] is False + assert payload["candidate_relink_dry_run_ready"] is False + assert payload["apply_allowed_by_candidate_confirmation_gate"] is False assert payload["public_lan_topology_redacted"] is True assert payload["raw_path_output"] is False assert payload["file_content_read"] is True diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 166348b97..af46e1610 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -9390,6 +9390,13 @@ "locatorIdentityUnassigned": "unassigned {count}", "locatorNeedsConfirm": "confirm", "locatorConfirmClosed": "confirm closed", + "locatorIdentityGateReady": "identity gate ok", + "locatorIdentityGateBlocked": "identity gate blocked", + "locatorDryRunReady": "dry-run ready", + "locatorDryRunClosed": "dry-run 0", + "locatorMissingEvidence": "missing evidence {count}", + "locatorApplyGateOpen": "apply gate open", + "locatorApplyGateClosed": "apply gate 0", "locatorCollected": "collected", "locatorPending": "collect wait", "noRawPath": "no raw path", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index f060fac49..e56cf15f2 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -9390,6 +9390,13 @@ "locatorIdentityUnassigned": "未指派 {count}", "locatorNeedsConfirm": "需確認", "locatorConfirmClosed": "確認關閉", + "locatorIdentityGateReady": "identity gate ok", + "locatorIdentityGateBlocked": "identity gate blocked", + "locatorDryRunReady": "dry-run ready", + "locatorDryRunClosed": "dry-run 0", + "locatorMissingEvidence": "缺證據 {count}", + "locatorApplyGateOpen": "apply gate open", + "locatorApplyGateClosed": "apply gate 0", "locatorCollected": "已收件", "locatorPending": "等待收件", "noRawPath": "不吐原路徑", diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx index 834e9fac0..c0b4d1e7e 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -1192,6 +1192,13 @@ type PriorityWorkOrderResponse = { windows99_vmx_source_locator_candidate_source_count?: number | null; windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count?: number | null; windows99_vmx_source_locator_candidate_confirmation_required?: boolean | null; + windows99_vmx_source_locator_candidate_confirmation_status?: string | null; + windows99_vmx_source_locator_candidate_identity_decision?: string | null; + windows99_vmx_source_locator_candidate_identity_evidence_sufficient?: boolean | null; + windows99_vmx_source_locator_candidate_relink_dry_run_ready?: boolean | null; + windows99_vmx_source_locator_candidate_confirmation_missing_evidence?: string[] | null; + windows99_vmx_source_locator_candidate_confirmation_safe_next_step?: string | null; + windows99_vmx_source_locator_apply_allowed_by_candidate_confirmation_gate?: boolean | null; windows99_vmx_source_locator_candidate_source_count_known?: boolean | null; windows99_vmx_source_locator_collector_readback_present?: boolean | null; windows99_vmx_source_locator_collector_collection_status?: string | null; @@ -1275,6 +1282,13 @@ type PriorityWorkOrderResponse = { windows99_vmx_source_locator_candidate_source_count?: number | null; windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count?: number | null; windows99_vmx_source_locator_candidate_confirmation_required?: boolean | null; + windows99_vmx_source_locator_candidate_confirmation_status?: string | null; + windows99_vmx_source_locator_candidate_identity_decision?: string | null; + windows99_vmx_source_locator_candidate_identity_evidence_sufficient?: boolean | null; + windows99_vmx_source_locator_candidate_relink_dry_run_ready?: boolean | null; + windows99_vmx_source_locator_candidate_confirmation_missing_evidence?: string[] | null; + windows99_vmx_source_locator_candidate_confirmation_safe_next_step?: string | null; + windows99_vmx_source_locator_apply_allowed_by_candidate_confirmation_gate?: boolean | null; windows99_vmx_source_locator_candidate_source_count_known?: boolean | null; windows99_vmx_source_locator_collector_readback_present?: boolean | null; windows99_vmx_source_locator_collector_collection_status?: string | null; @@ -9381,6 +9395,23 @@ function ManagerSituationBoard({ summary?.windows99_vmx_source_locator_candidate_confirmation_required ?? evidence?.windows99_vmx_source_locator_candidate_confirmation_required ?? false; + const vmxSourceLocatorIdentityEvidenceSufficient = + summary?.windows99_vmx_source_locator_candidate_identity_evidence_sufficient ?? + evidence?.windows99_vmx_source_locator_candidate_identity_evidence_sufficient ?? + false; + const vmxSourceLocatorRelinkDryRunReady = + summary?.windows99_vmx_source_locator_candidate_relink_dry_run_ready ?? + evidence?.windows99_vmx_source_locator_candidate_relink_dry_run_ready ?? + false; + const vmxSourceLocatorConfirmationMissingEvidence = ( + summary?.windows99_vmx_source_locator_candidate_confirmation_missing_evidence ?? + evidence?.windows99_vmx_source_locator_candidate_confirmation_missing_evidence ?? + [] + ).filter(Boolean); + const vmxSourceLocatorApplyGateOpen = + summary?.windows99_vmx_source_locator_apply_allowed_by_candidate_confirmation_gate ?? + evidence?.windows99_vmx_source_locator_apply_allowed_by_candidate_confirmation_gate ?? + false; const vmxSourceLocatorCandidateKnown = summary?.windows99_vmx_source_locator_candidate_source_count_known ?? evidence?.windows99_vmx_source_locator_candidate_source_count_known ?? @@ -9782,6 +9813,47 @@ function ManagerSituationBoard({ ? t("vmxRepair.locatorNeedsConfirm") : t("vmxRepair.locatorConfirmClosed")} + + {vmxSourceLocatorIdentityEvidenceSufficient + ? t("vmxRepair.locatorIdentityGateReady") + : t("vmxRepair.locatorIdentityGateBlocked")} + + + {vmxSourceLocatorRelinkDryRunReady + ? t("vmxRepair.locatorDryRunReady") + : t("vmxRepair.locatorDryRunClosed")} + + + {t("vmxRepair.locatorMissingEvidence", { + count: vmxSourceLocatorConfirmationMissingEvidence.length, + })} + + + {vmxSourceLocatorApplyGateOpen + ? t("vmxRepair.locatorApplyGateOpen") + : t("vmxRepair.locatorApplyGateClosed")} + {vmxSourceLocatorCollectorPresent ? t("vmxRepair.locatorCollected") diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index eb7d933bd..c450f6528 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,11 +1,12 @@ -## 2026-07-03 — 13:12 Windows99 VMX relink dry-run evidence gate 上卷 +## 2026-07-03 — 13:12 Windows99 VMX relink dry-run / identity gate 上卷 **完成內容**: - Gitea CD `#4582` 已完成最新 main `ba0849f70` 部署,deploy marker 為 `558b9e916 chore(cd): deploy ba0849f [skip ci]`;job3 讀回 `ArgoCD: sync=Synced health=Healthy`、API/Web/Worker rollout completed、`Production deploy readback matches this build`、`Job succeeded`。 - Production `/api/v1/agents/windows99-vmx-source-locator-readback` 已讀回 collector receipt:`collector_readback_present=true`、`candidate_source_count=6`、`identity_unassigned_ubuntu_candidate_count=1`、`raw_path_output=false`、`path_fingerprint_only=true`、`remote_write_performed=false`、`vm_power_change_performed=false`。 - 本輪 verifier-only artifact:`/tmp/awoooi-p0-006-reboot-slo-20260703-130633`。host probe 顯示 99 reachable-unknown-boot、111 unreachable、112/120/121 reachable、188 reachable 但 `awoooi-startup.service failed/degraded`;本工作機對 99 的 no-secret SSH collector 固定 `ssh_batchmode_auth_ready=0`、`verify_collection_status=blocked_ssh_publickey_auth_missing`。 - 將 VMX locator 的「confirmation」字眼改成 machine-readable evidence gate:`verified_source_or_identity_evidence_required=true`、`human_confirmation_required_for_dry_run=false`、`controlled_relink_dry_run_ready=false`、`controlled_relink_dry_run_blocker=verified_vmx_source_or_identity_evidence_missing`,並新增 `controlled_relink_dry_run_plan`。此 plan 只允許 dry-run evidence,不允許 apply / remote write / VM power change。 -- `awoooi-priority-work-order-readback` 同步投影上述 dry-run blocker 到 `mainline_execution_state`、in-progress evidence、`summary` 與 `rollups`,避免再把 P0-006 卡成模糊的人工確認。 +- 同步新增 `candidate_confirmation_gate`,schema 固定為 `windows99_vmx_source_candidate_confirmation_gate_v1`,把「唯一未指派 Ubuntu candidate」拆成 machine-readable identity gate;目前 `candidate_identity_evidence_sufficient=false`、`candidate_relink_dry_run_ready=false`、`apply_allowed_by_candidate_confirmation_gate=false`。 +- `awoooi-priority-work-order-readback` 同步投影上述 dry-run blocker 與 identity gate 到 `mainline_execution_state`、in-progress evidence、`summary` 與 `rollups`;AwoooP Work Items 補上 identity gate、dry-run gate、missing evidence 與 apply gate chips,避免再把 P0-006 卡成模糊人工確認。 **已跑驗證**: - `python3.11 -m py_compile apps/api/src/services/windows99_vmx_source_locator_readback.py apps/api/src/services/awoooi_priority_work_order_readback.py apps/api/tests/test_windows99_vmx_source_locator_readback_api.py apps/api/tests/test_awoooi_priority_work_order_readback_api.py`:通過。 diff --git a/docs/runbooks/FULL-STACK-COLD-START-SOP.md b/docs/runbooks/FULL-STACK-COLD-START-SOP.md index a0380e333..fb5aa6ab4 100644 --- a/docs/runbooks/FULL-STACK-COLD-START-SOP.md +++ b/docs/runbooks/FULL-STACK-COLD-START-SOP.md @@ -64,6 +64,8 @@ v1.115 Windows99 VMX source locate check-mode rule:2026-07-03 12:34 deploy 後 v1.116 Windows99 VMX locator via-host execution rule:若目前工作站直連 99 缺 public-key auth,但 110 到 99 的 no-secret SSH path 已驗證,官方 locator 必須支援 `--via-host`,固定可執行命令為 `bash scripts/reboot-recovery/locate-windows99-vmx-source.sh --collect --via-host wooo@192.168.0.110 --users wooo --max-auth-users 1`。2026-07-03 live readback:`locator_collection_status=collected_windows99_vmx_source_locator_stdout`、`expected_vmx_path_present_count=0`、`candidate_source_count=0`、`source_locator_status=blocked_missing_expected_vmx_source_no_candidate_found`、`safe_next_step=provide_existing_vmx_source_or_backup_path_then_rerun_locator_check_mode_no_write`,且 `secret_value_read=false`、`remote_write_performed=false`、`vm_power_change_performed=false`、`raw_path_output=false`、`path_fingerprint_only=true`。此結果把下一步收斂為「提供既有 VMX source 或 verified backup path」;仍不得 relink generic VM、啟動 VM、restore 到 production 或讀密碼。 +v1.117 Windows99 VMX candidate confirmation gate rule:locator receipt 顯示 `single_identity_unassigned_ubuntu_candidate_requires_confirmation` 時,不能把「唯一 generic Ubuntu candidate」直接升級成 111 VMX source。`/api/v1/agents/windows99-vmx-source-locator-readback` 必須輸出 `candidate_confirmation_gate`,並同步到 priority readback / Work Items;目前 gate 固定 `candidate_identity_evidence_sufficient=false`、`candidate_relink_dry_run_ready=false`、`apply_allowed_by_candidate_confirmation_gate=false`,missing evidence 至少包含 `target_alias_identity_hint`、`verified_backup_or_authorized_console_candidate_identity`、`pre_apply_backup_plan`、`scoped_relink_dry_run`。只有取得 target alias / MAC / UUID / display-name 或 verified backup path 證據,且產生 rollback + post-verifier 的 scoped relink dry-run 後,才可進下一階段;scorecard / locator / work item 均不得授權 VM power change、Windows write、service restart、host reboot、secret/password 讀取或 production restore。 + 2026-07-02 110 control-path / Harbor recovery receipt rule:若 Gitea Harbor repair queue 仍保留 `harbor_110_remote_ssh_publickey_auth_stalled`、remote-control unavailable、jobs stale 或 historical failure,但同一輪本地證據同時證明 `wooo` command path ready、110 local Harbor `/v2/` ready、public/internal registry `/v2/` 回 `401`,則該 Gitea Harbor repair 失敗只能列為 historical queue metadata,不得再當成 current SSH blocker。必須用 `/api/v1/agents/harbor-registry-controlled-recovery-receipt` 或同等 validator 合併 `diagnose-110-ssh-publickey-auth.sh`、`recover-110-control-path-and-harbor-local.sh --check`、public Gitea queue readback 與 registry `/v2/` verifier,並把機器可讀結果寫入 `docs/operations/harbor-110-control-path-recovery-readback-2026-07-02.snapshot.json` 類型的 snapshot。2026-07-02 live receipt 顯示:public/internal registry `/v2/` 均為 `401`、latest visible CD `#4335` 為 `Success`、Gitea Harbor repair failure 已是 `historical_after_latest_cd_success=true`;active blockers 收斂為 110 controlled CD lane config / binary / registration / service guardrail、active action container pressure,以及 Gitea CD jobs head-SHA / stale readback mismatch。若 local-console output 只有 `AWOOOI_110_CONTROLLED_CD_LANE_READY` marker,non110 runner parser 不得從 110 `BLOCKER` 行推導 non110 blocker;non110 只有看到 `AWOOOI_NON110_RUNNER_READY` marker 才能列入 active blocker。 2026-07-02 110 controlled CD lane fail-closed enforcer staging rule:110 runner 壓力事故後,legacy / generic runner 仍必須 fail-closed;但 `awoooi-cd-lane-drain.service` 的非 secret staging artifact 不得再被 enforcer 無差別封回 stub。`scripts/reboot-recovery/enforce-110-runner-failclosed.sh` 只有在 `config.yaml` 符合 `capacity <= 1`、只含 `awoooi-host:host` 與 `awoooi-ubuntu:docker://192.168.0.110:5000/awoooi/ci-runner:act-22.04`、binary 是 executable ELF、systemd unit 具備 `ConditionPathExists=/home/wooo/awoooi-cd-lane-drain/data/.runner`、`CPUAccounting` / `MemoryAccounting` / `TasksAccounting` / `NoNewPrivileges` 等 guardrail,且 service `inactive`、`MainPID=0`、未 enabled / 未 masked 時,才可保留 drain config / binary / unit,並輸出 `CONTROLLED_DRAIN_STAGING_ALLOWED=1` 與 textfile metric。此 staging 規則不得讀 token、不得讀 `.runner` 內容、不得註冊 runner、不得啟動 service;若 registration 缺失,readiness verifier 仍必須只留下 `controlled_cd_lane_registration_missing` / `controlled_cd_lane_service_not_active` 類 blocker。若 `CONTROLLED_DRAIN_STAGING_ALLOWED=0` 且 config / binary 又被搬走,優先修 source enforcer / unit guardrail,不要手工反覆補同一組 artifact。 diff --git a/docs/runbooks/REBOOT-RECOVERY-SOP.md b/docs/runbooks/REBOOT-RECOVERY-SOP.md index 206d9eac0..a5d29d1c5 100644 --- a/docs/runbooks/REBOOT-RECOVERY-SOP.md +++ b/docs/runbooks/REBOOT-RECOVERY-SOP.md @@ -1,9 +1,9 @@ # AWOOOI 重開機恢復 SOP -> **版本**: v5.8 +> **版本**: v5.9 > **最後更新**: 2026-07-03 (台北時間) > **更新者**: Codex -> **觸發事件**: 2026-07-03 12:34 deploy 後 production SLO scorecard + Windows99 VMX source locator readback:111 VMX source 尚未找到,10 分鐘 SLO 仍 blocked +> **觸發事件**: 2026-07-03 13:10 deploy 後 production SLO scorecard + Windows99 VMX source locator confirmation gate:111 VMX candidate 尚未取得足夠 identity evidence,10 分鐘 SLO 仍 blocked --- @@ -48,6 +48,8 @@ 固定下一步:`locate_or_relink_missing_vmx_source_check_mode`。production `/api/v1/agents/windows99-vmx-source-locator-readback` 已回 `status=check_mode_locator_ready_windows99_vmx_source_required`、`missing_vmx_aliases=["111"]`、`safe_next_step=bash scripts/reboot-recovery/locate-windows99-vmx-source.sh --collect`、`secret_value_read=false`、`remote_write_performed=false`、`vm_power_change_allowed=false`。目前工作站必須加 `--via-host wooo@192.168.0.110 --users wooo --max-auth-users 1` 走已驗證的 110 no-secret path;live 結果為 `locator_collection_status=collected_windows99_vmx_source_locator_stdout`、`expected_vmx_path_present_count=0`、`candidate_source_count=0`、`source_locator_status=blocked_missing_expected_vmx_source_no_candidate_found`、`raw_path_output=false`、`path_fingerprint_only=true`。下一步只能取得既有 VMX source 或 external verified backup path,再產生 scoped relink / restore dry-run 與 post-verifier。若 public API response 顯示 `public_lan_topology_redacted=true` 或 `check_mode_probe_commands_executable=false`,其中 `host:...` VMX path 只能當 public display evidence,不得直接貼到 Windows console 執行;要執行 probe 時必須使用授權內部 console / no-secret package 保留的原始 Windows path。禁止讀 Windows 密碼、啟動 / 關閉 VM、host reboot、service restart、Docker / Nginx / K3s / DB / firewall restart、restore、prune、delete。 +2026-07-03 13:10 最新 overlay:Gitea CD `#4582` 已推 deploy marker `558b9e9`,source `ba0849f70`,production deploy readback matched=true 且 job_succeeded=true;Gitea UI 若仍顯示 Running,只能視為 run-level lag。production `/api/v1/agents/windows99-vmx-source-locator-readback` 已上卷 redacted locator receipt:`candidate_source_count=6`、`alias_hint_candidate_count=0`、`unassigned_ubuntu_candidate_count=5`、`identity_unassigned_ubuntu_candidate_count=1`、`candidate_confirmation_required=true`,狀態為 `collector_readback_single_identity_candidate_confirmation_required`。新的 `candidate_confirmation_gate` 固定輸出 `candidate_identity_evidence_sufficient=false`、`candidate_relink_dry_run_ready=false`、`apply_allowed_by_candidate_confirmation_gate=false`;缺少 evidence 為 `target_alias_identity_hint`、`verified_backup_or_authorized_console_candidate_identity`、`pre_apply_backup_plan`、`scoped_relink_dry_run`。因此下一步已從「重跑 locator」收斂為:取得授權 console candidate identity evidence 或 verified backup path,重跑 confirmation gate,再產生 scoped relink dry-run + rollback + post-verifier;仍不得猜測 generic Ubuntu candidate、不得 relink、不得 restore、不得 power on VM。 + ### 五主機全貌 ```