From ba0849f707cdd486ddcd9286ef0e2d6a8bd0b7d6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 3 Jul 2026 12:53:33 +0800 Subject: [PATCH] fix(reboot): project vmx locator receipt readback --- .../awoooi_priority_work_order_readback.py | 71 ++++++++++ .../windows99_vmx_source_locator_readback.py | 126 ++++++++++++++++-- ...awoooi_priority_work_order_readback_api.py | 49 +++++-- ...ndows99_vmx_source_locator_readback_api.py | 75 ++++++++++- apps/web/messages/en.json | 7 +- apps/web/messages/zh-TW.json | 7 +- .../app/[locale]/awooop/work-items/page.tsx | 69 ++++++++-- docs/LOGBOOK.md | 18 +++ ...9-vmx-source-locator-receipt.snapshot.json | 52 ++++++++ 9 files changed, 438 insertions(+), 36 deletions(-) create mode 100644 docs/operations/windows99-vmx-source-locator-receipt.snapshot.json 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 b4335895f..19c409d17 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -3457,6 +3457,24 @@ def _enrich_from_current_readbacks( state["windows99_vmx_source_locator_expected_vmx_path_count"] = _int( windows99_vmx_source_locator_readback.get("expected_vmx_path_count") ) + state["windows99_vmx_source_locator_candidate_source_count"] = _int( + windows99_vmx_source_locator_readback.get("candidate_source_count") + ) + state["windows99_vmx_source_locator_alias_hint_candidate_count"] = _int( + windows99_vmx_source_locator_readback.get("alias_hint_candidate_count") + ) + state["windows99_vmx_source_locator_unassigned_ubuntu_candidate_count"] = _int( + windows99_vmx_source_locator_readback.get("unassigned_ubuntu_candidate_count") + ) + state["windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count"] = _int( + windows99_vmx_source_locator_readback.get( + "identity_unassigned_ubuntu_candidate_count" + ) + ) + state["windows99_vmx_source_locator_candidate_confirmation_required"] = bool( + windows99_vmx_source_locator_readback.get("candidate_confirmation_required") + is True + ) state["windows99_vmx_source_locator_candidate_source_count_known"] = bool( windows99_vmx_source_locator_readback.get("candidate_source_count_known") is True @@ -3856,6 +3874,23 @@ def _enrich_from_current_readbacks( evidence["windows99_vmx_source_locator_expected_vmx_path_count"] = state[ "windows99_vmx_source_locator_expected_vmx_path_count" ] + evidence["windows99_vmx_source_locator_candidate_source_count"] = state[ + "windows99_vmx_source_locator_candidate_source_count" + ] + evidence["windows99_vmx_source_locator_alias_hint_candidate_count"] = state[ + "windows99_vmx_source_locator_alias_hint_candidate_count" + ] + evidence["windows99_vmx_source_locator_unassigned_ubuntu_candidate_count"] = state[ + "windows99_vmx_source_locator_unassigned_ubuntu_candidate_count" + ] + evidence[ + "windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count" + ] = state[ + "windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count" + ] + evidence["windows99_vmx_source_locator_candidate_confirmation_required"] = state[ + "windows99_vmx_source_locator_candidate_confirmation_required" + ] evidence["windows99_vmx_source_locator_candidate_source_count_known"] = state[ "windows99_vmx_source_locator_candidate_source_count_known" ] @@ -4630,6 +4665,24 @@ def _set_rollups_and_summary( "windows99_vmx_source_locator_expected_vmx_path_count": _int( state.get("windows99_vmx_source_locator_expected_vmx_path_count") ), + "windows99_vmx_source_locator_candidate_source_count": _int( + state.get("windows99_vmx_source_locator_candidate_source_count") + ), + "windows99_vmx_source_locator_alias_hint_candidate_count": _int( + state.get("windows99_vmx_source_locator_alias_hint_candidate_count") + ), + "windows99_vmx_source_locator_unassigned_ubuntu_candidate_count": _int( + state.get("windows99_vmx_source_locator_unassigned_ubuntu_candidate_count") + ), + "windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count": _int( + state.get( + "windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count" + ) + ), + "windows99_vmx_source_locator_candidate_confirmation_required": ( + state.get("windows99_vmx_source_locator_candidate_confirmation_required") + is True + ), "windows99_vmx_source_locator_candidate_source_count_known": ( state.get("windows99_vmx_source_locator_candidate_source_count_known") is True @@ -4934,6 +4987,24 @@ def _set_rollups_and_summary( "windows99_vmx_source_locator_expected_vmx_path_count": _int( state.get("windows99_vmx_source_locator_expected_vmx_path_count") ), + "windows99_vmx_source_locator_candidate_source_count": _int( + state.get("windows99_vmx_source_locator_candidate_source_count") + ), + "windows99_vmx_source_locator_alias_hint_candidate_count": _int( + state.get("windows99_vmx_source_locator_alias_hint_candidate_count") + ), + "windows99_vmx_source_locator_unassigned_ubuntu_candidate_count": _int( + state.get("windows99_vmx_source_locator_unassigned_ubuntu_candidate_count") + ), + "windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count": _int( + state.get( + "windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count" + ) + ), + "windows99_vmx_source_locator_candidate_confirmation_required": ( + state.get("windows99_vmx_source_locator_candidate_confirmation_required") + 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 17d37e00a..8979deda4 100644 --- a/apps/api/src/services/windows99_vmx_source_locator_readback.py +++ b/apps/api/src/services/windows99_vmx_source_locator_readback.py @@ -8,19 +8,25 @@ start VMs, stop VMs, restart services, or reboot hosts. from __future__ import annotations +import json +from pathlib import Path from typing import Any from src.services.reboot_auto_recovery_slo_scorecard import ( _taipei_now_iso, load_latest_reboot_auto_recovery_slo_scorecard, ) +from src.services.snapshot_paths import default_operations_dir from src.services.windows99_vmx_source_repair_package import ( build_windows99_vmx_source_repair_package, ) +_DEFAULT_OPERATIONS_DIR = default_operations_dir(Path(__file__)) _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" +_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" @@ -41,6 +47,33 @@ def _bool(value: Any) -> bool: return value is True +def _int(value: Any, default: int | None = None) -> int | None: + if isinstance(value, bool): + return default + if isinstance(value, int): + return value + if isinstance(value, str) and value.strip().isdigit(): + return int(value.strip()) + return default + + +def _load_locator_receipt(operations_dir: Path | None = None) -> dict[str, Any]: + path = (operations_dir or _DEFAULT_OPERATIONS_DIR) / _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") != _RECEIPT_SCHEMA_VERSION: + raise ValueError(f"{path}: unexpected schema_version") + serialized = json.dumps(payload, ensure_ascii=False) + forbidden = ["192.168.0.", "D:\\", "C:\\", "E:\\", "uuid.", "generatedAddress"] + leaked = [fragment for fragment in forbidden if fragment in serialized] + if leaked: + raise ValueError(f"{path}: unsafe locator receipt fragments: {leaked}") + return payload + + def _vm_rows_for_aliases( repair_package: dict[str, Any], target_aliases: list[str], @@ -60,6 +93,8 @@ def build_windows99_vmx_source_locator_readback( scorecard: dict[str, Any], *, repair_package: dict[str, Any] | None = None, + collector_receipt: dict[str, Any] | None = None, + operations_dir: Path | None = None, generated_at: str | None = None, ) -> dict[str, Any]: package = repair_package or build_windows99_vmx_source_repair_package(scorecard) @@ -111,6 +146,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 = _dict(receipt) + receipt_aliases = _strings(receipt.get("target_vm_aliases")) + receipt_applies = bool( + receipt.get("collector_readback_present") is True + and receipt.get("target_host_alias") == "99" + and receipt_aliases + and set(receipt_aliases).issubset(set(target_aliases or receipt_aliases)) + ) + receipt_locate_status = str(receipt.get("locate_status") or "") + if receipt_applies: + status = "collector_readback_single_identity_candidate_confirmation_required" + next_step = "confirm_single_unassigned_ubuntu_candidate_before_scoped_relink_dry_run" + safe_next_step = str(receipt.get("safe_next_step") or safe_next_step) + return { "schema_version": _SCHEMA_VERSION, "generated_at": generated_at @@ -127,8 +177,14 @@ def build_windows99_vmx_source_locator_readback( "locator_script_path": _LOCATOR_SCRIPT_PATH, "collector_schema_version": _COLLECTOR_SCHEMA_VERSION, "locator_schema_version": _LOCATOR_SCHEMA_VERSION, - "collector_readback_present": False, - "collector_collection_status": "not_collected_by_api_readback", + "collector_readback_present": receipt_applies, + "collector_receipt_ref": str(receipt.get("receipt_ref") or "") if receipt_applies else "", + "collector_receipt_generated_at": str(receipt.get("generated_at") or "") if receipt_applies else "", + "collector_collection_status": ( + str(receipt.get("collector_collection_status") or "") + if receipt_applies + else "not_collected_by_api_readback" + ), "management_channel_ready": management_channel_ready, "can_collect_vmware_verify_without_secret": ( can_collect_vmware_verify_without_secret @@ -143,15 +199,57 @@ def build_windows99_vmx_source_locator_readback( ), "remote_locator_mode": "in_memory_stdin_scriptblock", "expected_vmx_path_count": expected_vmx_path_count, - "expected_vmx_path_present_count": None, - "candidate_source_count": None, - "candidate_source_count_known": False, - "candidate_source_fingerprint_count": None, + "expected_vmx_path_present_count": ( + _int(receipt.get("expected_vmx_path_present_count")) + if receipt_applies + else None + ), + "candidate_source_count": ( + _int(receipt.get("candidate_source_count")) if receipt_applies else None + ), + "candidate_source_count_known": bool(receipt_applies), + "candidate_source_fingerprint_count": ( + _int(receipt.get("candidate_source_fingerprint_count"), 0) + if receipt_applies + else None + ), + "alias_hint_candidate_count": ( + _int(receipt.get("alias_hint_candidate_count"), 0) + if receipt_applies + else None + ), + "unassigned_ubuntu_candidate_count": ( + _int(receipt.get("unassigned_ubuntu_candidate_count"), 0) + if receipt_applies + else None + ), + "identity_metadata_read": bool( + receipt_applies and receipt.get("identity_metadata_read") is True + ), + "identity_target_hint_count": ( + _int(receipt.get("identity_target_hint_count"), 0) + if receipt_applies + else None + ), + "identity_unassigned_ubuntu_candidate_count": ( + _int(receipt.get("identity_unassigned_ubuntu_candidate_count"), 0) + if receipt_applies + else None + ), + "locate_status": receipt_locate_status if receipt_applies else "", + "candidate_confirmation_required": bool( + receipt_applies and "requires_confirmation" in receipt_locate_status + ), "public_lan_topology_redacted": True, "redacted_public_display_only": True, "raw_path_output": False, "path_fingerprint_only": True, - "file_content_read": False, + "file_content_read": bool( + receipt_applies and receipt.get("identity_metadata_read") is True + ), + "bounded_identity_metadata_only": bool( + receipt_applies and receipt.get("bounded_identity_metadata_only") is True + ), "secret_value_read": False, "password_prompt_allowed": False, "remote_write_performed": False, @@ -165,7 +263,12 @@ def build_windows99_vmx_source_locator_readback( "operation_boundaries": { "secret_value_read": False, "password_prompt_allowed": False, - "file_content_read": False, + "file_content_read": bool( + receipt_applies and receipt.get("identity_metadata_read") is True + ), + "bounded_identity_metadata_only": bool( + receipt_applies and receipt.get("bounded_identity_metadata_only") is True + ), "raw_path_output": False, "remote_write_performed": False, "host_reboot_performed": False, @@ -177,7 +280,12 @@ def build_windows99_vmx_source_locator_readback( "allowed": locator_check_mode_ready, "effective_collect_ready": effective_locator_collect_ready, "path_existence_only": True, - "vmx_file_content_read": False, + "vmx_file_content_read": bool( + receipt_applies and receipt.get("identity_metadata_read") is True + ), + "bounded_identity_metadata_only": bool( + receipt_applies and receipt.get("bounded_identity_metadata_only") is True + ), "raw_path_output": False, "path_fingerprint_only": True, "collector_command": safe_next_step if locator_check_mode_ready else "", 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 e025a0b91..6961b19ea 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 @@ -738,7 +738,7 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a "windows99_vmx_source_repair_playbook_trust_writeback_ref" ].startswith("playbook://awoooi/windows99-vmx-source-repair/") assert state["windows99_vmx_source_locator_status"] == ( - "check_mode_locator_ready_windows99_vmx_source_required" + "collector_readback_single_identity_candidate_confirmation_required" ) assert state["windows99_vmx_source_locator_required"] is True assert state["windows99_vmx_source_locator_check_mode_ready"] is True @@ -752,12 +752,22 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a ) assert state["windows99_vmx_source_locator_target_aliases"] == ["111"] assert state["windows99_vmx_source_locator_next_executable_step"] == ( - "restore_windows99_ssh_publickey_channel_then_rerun_locator" + "confirm_single_unassigned_ubuntu_candidate_before_scoped_relink_dry_run" ) assert state["windows99_vmx_source_locator_expected_vmx_path_count"] == 1 - assert state["windows99_vmx_source_locator_candidate_source_count_known"] is False - assert state["windows99_vmx_source_locator_collector_readback_present"] is False - assert state["windows99_vmx_source_locator_file_content_read"] is False + assert state["windows99_vmx_source_locator_candidate_source_count"] == 6 + assert state["windows99_vmx_source_locator_alias_hint_candidate_count"] == 0 + assert state["windows99_vmx_source_locator_unassigned_ubuntu_candidate_count"] == 5 + assert ( + state[ + "windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count" + ] + == 1 + ) + assert state["windows99_vmx_source_locator_candidate_confirmation_required"] is True + 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 assert state["windows99_vmx_source_locator_remote_write_performed"] is False assert state["windows99_vmx_source_locator_vm_power_change_performed"] is False assert payload["summary"]["controlled_service_data_backup_blocker_count"] == 0 @@ -838,19 +848,32 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a "111" ] assert payload["summary"]["windows99_vmx_source_locator_status"] == ( - "check_mode_locator_ready_windows99_vmx_source_required" + "collector_readback_single_identity_candidate_confirmation_required" ) assert payload["summary"]["windows99_vmx_source_locator_check_mode_ready"] is True assert payload["summary"]["windows99_vmx_source_locator_target_aliases"] == [ "111" ] + assert payload["summary"]["windows99_vmx_source_locator_candidate_source_count"] == 6 + assert ( + payload["summary"][ + "windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count" + ] + == 1 + ) + assert ( + payload["summary"][ + "windows99_vmx_source_locator_candidate_confirmation_required" + ] + is True + ) assert ( payload["rollups"]["windows99_vmx_source_locator_remote_write_performed"] is False ) assert ( payload["rollups"]["windows99_vmx_source_locator_file_content_read"] - is False + is True ) assert "service/data/backup readback is green" in payload[ "next_execution_order" @@ -902,7 +925,7 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a "windows99_vmx_source_repair_playbook_trust_writeback_ref" ].startswith("playbook://awoooi/windows99-vmx-source-repair/") assert in_progress["evidence"]["windows99_vmx_source_locator_status"] == ( - "check_mode_locator_ready_windows99_vmx_source_required" + "collector_readback_single_identity_candidate_confirmation_required" ) assert ( in_progress["evidence"]["windows99_vmx_source_locator_check_mode_ready"] @@ -912,6 +935,16 @@ def test_awoooi_priority_work_order_readback_routes_closed_backup_to_host_boot_a in_progress["evidence"]["windows99_vmx_source_locator_target_aliases"] == ["111"] ) + assert ( + in_progress["evidence"]["windows99_vmx_source_locator_candidate_source_count"] + == 6 + ) + assert ( + in_progress["evidence"][ + "windows99_vmx_source_locator_candidate_confirmation_required" + ] + is True + ) assert ( in_progress["evidence"]["windows99_vmx_source_locator_remote_write_performed"] is 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 f6a731729..0ea872787 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 @@ -56,6 +56,7 @@ def test_windows99_vmx_source_locator_builder_is_no_secret_check_mode() -> None: payload = build_windows99_vmx_source_locator_readback( _scorecard(), repair_package=repair_package, + collector_receipt={}, generated_at="2026-07-03T11:22:00+08:00", ) @@ -106,6 +107,69 @@ def test_windows99_vmx_source_locator_builder_is_no_secret_check_mode() -> None: assert payload["destructive_restore_allowed"] is False +def test_windows99_vmx_source_locator_builder_projects_redacted_receipt() -> None: + repair_package = build_windows99_vmx_source_repair_package( + _scorecard(), + generated_at="2026-07-03T11:21:00+08:00", + ) + + payload = build_windows99_vmx_source_locator_readback( + _scorecard(), + repair_package=repair_package, + collector_receipt={ + "schema_version": "windows99_vmx_source_locator_receipt_v1", + "generated_at": "2026-07-03T12:41:34+08:00", + "receipt_ref": "log://awoooi/p0-006/windows99-vmx-source-locator-111", + "collector_readback_present": True, + "collector_collection_status": ( + "collected_windows99_vmx_source_locator_stdout" + ), + "target_host_alias": "99", + "target_vm_aliases": ["111"], + "expected_vmx_path_present_count": 0, + "candidate_source_count": 6, + "candidate_source_fingerprint_count": 0, + "alias_hint_candidate_count": 0, + "unassigned_ubuntu_candidate_count": 5, + "identity_metadata_read": True, + "bounded_identity_metadata_only": True, + "identity_target_hint_count": 0, + "identity_unassigned_ubuntu_candidate_count": 1, + "locate_status": ( + "single_identity_unassigned_ubuntu_candidate_requires_confirmation" + ), + "safe_next_step": ( + "confirm_single_unassigned_ubuntu_candidate_is_target_alias_before_scoped_relink_dry_run" + ), + }, + generated_at="2026-07-03T12:42:00+08:00", + ) + + assert payload["status"] == ( + "collector_readback_single_identity_candidate_confirmation_required" + ) + assert payload["collector_readback_present"] is True + assert payload["collector_collection_status"] == ( + "collected_windows99_vmx_source_locator_stdout" + ) + assert payload["candidate_source_count_known"] is True + assert payload["candidate_source_count"] == 6 + assert payload["alias_hint_candidate_count"] == 0 + assert payload["unassigned_ubuntu_candidate_count"] == 5 + assert payload["identity_metadata_read"] is True + assert payload["bounded_identity_metadata_only"] is True + assert payload["identity_target_hint_count"] == 0 + assert payload["identity_unassigned_ubuntu_candidate_count"] == 1 + assert payload["candidate_confirmation_required"] is True + assert payload["next_executable_step"] == ( + "confirm_single_unassigned_ubuntu_candidate_before_scoped_relink_dry_run" + ) + assert payload["raw_path_output"] is False + assert payload["secret_value_read"] is False + assert payload["remote_write_performed"] is False + assert payload["vm_power_change_performed"] is False + + def test_windows99_vmx_source_locator_endpoint_returns_public_safe_readback( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -127,15 +191,22 @@ def test_windows99_vmx_source_locator_endpoint_returns_public_safe_readback( payload = response.json() assert payload["schema_version"] == "windows99_vmx_source_locator_readback_v1" assert payload["status"] == ( - "check_mode_locator_ready_windows99_vmx_source_required" + "collector_readback_single_identity_candidate_confirmation_required" ) assert payload["target_vm_aliases"] == ["111"] assert payload["locator_check_mode_ready"] is True assert payload["effective_locator_collect_ready"] is False assert payload["locator_collect_blocker"] == "ssh_batch_permission_denied" + assert payload["collector_readback_present"] is True + assert payload["candidate_source_count"] == 6 + assert payload["candidate_confirmation_required"] is True assert payload["public_lan_topology_redacted"] is True assert payload["raw_path_output"] is False - assert payload["file_content_read"] is False + assert payload["file_content_read"] is True + assert payload["bounded_identity_metadata_only"] is True assert payload["remote_write_performed"] is False assert payload["vm_power_change_performed"] is False assert "192.168.0." not in response.text + assert "D:\\" not in response.text + assert "uuid." not in response.text + assert "generatedAddress" not in response.text diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 81519c0f9..166348b97 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -9385,14 +9385,17 @@ "locatorWaiting": "locator wait", "locatorTarget": "target {aliases}", "locatorExpected": "expected {count}", - "locatorCandidatesKnown": "candidates known", + "locatorCandidatesKnown": "candidates {count}", "locatorCandidatesUnknown": "candidates wait", + "locatorIdentityUnassigned": "unassigned {count}", + "locatorNeedsConfirm": "confirm", + "locatorConfirmClosed": "confirm closed", "locatorCollected": "collected", "locatorPending": "collect wait", "noRawPath": "no raw path", "rawPathOpen": "raw path open", "noFileRead": "no file read", - "fileReadOpen": "file read open", + "fileReadOpen": "identity read", "noWrite": "no write", "writeOpen": "write open" }, diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 2a43cbb18..f060fac49 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -9385,14 +9385,17 @@ "locatorWaiting": "等待定位", "locatorTarget": "目標 {aliases}", "locatorExpected": "預期 {count}", - "locatorCandidatesKnown": "候選已知", + "locatorCandidatesKnown": "候選 {count}", "locatorCandidatesUnknown": "候選待收", + "locatorIdentityUnassigned": "未指派 {count}", + "locatorNeedsConfirm": "需確認", + "locatorConfirmClosed": "確認關閉", "locatorCollected": "已收件", "locatorPending": "等待收件", "noRawPath": "不吐原路徑", "rawPathOpen": "原路徑開", "noFileRead": "不讀檔", - "fileReadOpen": "讀檔開", + "fileReadOpen": "讀 identity", "noWrite": "不寫入", "writeOpen": "寫入開" }, 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 bab47302d..834e9fac0 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -1189,6 +1189,9 @@ type PriorityWorkOrderResponse = { windows99_vmx_source_locator_target_aliases?: string[] | null; windows99_vmx_source_locator_next_executable_step?: string | null; windows99_vmx_source_locator_expected_vmx_path_count?: number | null; + 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_source_count_known?: boolean | null; windows99_vmx_source_locator_collector_readback_present?: boolean | null; windows99_vmx_source_locator_collector_collection_status?: string | null; @@ -1269,6 +1272,9 @@ type PriorityWorkOrderResponse = { windows99_vmx_source_locator_target_aliases?: string[] | null; windows99_vmx_source_locator_next_executable_step?: string | null; windows99_vmx_source_locator_expected_vmx_path_count?: number | null; + 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_source_count_known?: boolean | null; windows99_vmx_source_locator_collector_readback_present?: boolean | null; windows99_vmx_source_locator_collector_collection_status?: string | null; @@ -1414,20 +1420,23 @@ const COMMANDER_INSERTED_REQUIREMENT_FALLBACK: PriorityWorkOrderResponse = { windows99_vmx_source_repair_playbook_trust_writeback_ref: "playbook://awoooi/windows99-vmx-source-repair/windows99-vmx-source-repair-111", windows99_vmx_source_locator_status: - "check_mode_locator_ready_windows99_vmx_source_required", + "collector_readback_single_identity_candidate_confirmation_required", windows99_vmx_source_locator_required: true, windows99_vmx_source_locator_check_mode_ready: true, windows99_vmx_source_locator_target_aliases: ["111"], windows99_vmx_source_locator_next_executable_step: - "run_no_secret_windows99_vmx_source_locator_collect", + "confirm_single_unassigned_ubuntu_candidate_before_scoped_relink_dry_run", windows99_vmx_source_locator_expected_vmx_path_count: 1, - windows99_vmx_source_locator_candidate_source_count_known: false, - windows99_vmx_source_locator_collector_readback_present: false, + windows99_vmx_source_locator_candidate_source_count: 6, + windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count: 1, + windows99_vmx_source_locator_candidate_confirmation_required: true, + windows99_vmx_source_locator_candidate_source_count_known: true, + windows99_vmx_source_locator_collector_readback_present: true, windows99_vmx_source_locator_collector_collection_status: - "not_collected_by_api_readback", + "collected_windows99_vmx_source_locator_stdout", windows99_vmx_source_locator_raw_path_output: false, windows99_vmx_source_locator_path_fingerprint_only: true, - windows99_vmx_source_locator_file_content_read: false, + windows99_vmx_source_locator_file_content_read: true, windows99_vmx_source_locator_remote_write_performed: false, windows99_vmx_source_locator_vm_power_change_performed: false, stockplatform_public_api_runtime_status: "stockplatform_public_api_runtime_ready", @@ -1528,20 +1537,23 @@ const COMMANDER_INSERTED_REQUIREMENT_FALLBACK: PriorityWorkOrderResponse = { windows99_vmx_source_repair_playbook_trust_writeback_ref: "playbook://awoooi/windows99-vmx-source-repair/windows99-vmx-source-repair-111", windows99_vmx_source_locator_status: - "check_mode_locator_ready_windows99_vmx_source_required", + "collector_readback_single_identity_candidate_confirmation_required", windows99_vmx_source_locator_required: true, windows99_vmx_source_locator_check_mode_ready: true, windows99_vmx_source_locator_target_aliases: ["111"], windows99_vmx_source_locator_next_executable_step: - "run_no_secret_windows99_vmx_source_locator_collect", + "confirm_single_unassigned_ubuntu_candidate_before_scoped_relink_dry_run", windows99_vmx_source_locator_expected_vmx_path_count: 1, - windows99_vmx_source_locator_candidate_source_count_known: false, - windows99_vmx_source_locator_collector_readback_present: false, + windows99_vmx_source_locator_candidate_source_count: 6, + windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count: 1, + windows99_vmx_source_locator_candidate_confirmation_required: true, + windows99_vmx_source_locator_candidate_source_count_known: true, + windows99_vmx_source_locator_collector_readback_present: true, windows99_vmx_source_locator_collector_collection_status: - "not_collected_by_api_readback", + "collected_windows99_vmx_source_locator_stdout", windows99_vmx_source_locator_raw_path_output: false, windows99_vmx_source_locator_path_fingerprint_only: true, - windows99_vmx_source_locator_file_content_read: false, + windows99_vmx_source_locator_file_content_read: true, windows99_vmx_source_locator_remote_write_performed: false, windows99_vmx_source_locator_vm_power_change_performed: false, drill_preflight_blocker_count: 5, @@ -9357,6 +9369,18 @@ function ManagerSituationBoard({ summary?.windows99_vmx_source_locator_expected_vmx_path_count ?? evidence?.windows99_vmx_source_locator_expected_vmx_path_count ?? 0; + const vmxSourceLocatorCandidateCount = + summary?.windows99_vmx_source_locator_candidate_source_count ?? + evidence?.windows99_vmx_source_locator_candidate_source_count ?? + 0; + const vmxSourceLocatorIdentityUnassignedCount = + summary?.windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count ?? + evidence?.windows99_vmx_source_locator_identity_unassigned_ubuntu_candidate_count ?? + 0; + const vmxSourceLocatorConfirmationRequired = + summary?.windows99_vmx_source_locator_candidate_confirmation_required ?? + evidence?.windows99_vmx_source_locator_candidate_confirmation_required ?? + false; const vmxSourceLocatorCandidateKnown = summary?.windows99_vmx_source_locator_candidate_source_count_known ?? evidence?.windows99_vmx_source_locator_candidate_source_count_known ?? @@ -9736,9 +9760,28 @@ function ManagerSituationBoard({ {vmxSourceLocatorCandidateKnown - ? t("vmxRepair.locatorCandidatesKnown") + ? t("vmxRepair.locatorCandidatesKnown", { + count: vmxSourceLocatorCandidateCount, + }) : t("vmxRepair.locatorCandidatesUnknown")} + + {t("vmxRepair.locatorIdentityUnassigned", { + count: vmxSourceLocatorIdentityUnassignedCount, + })} + + + {vmxSourceLocatorConfirmationRequired + ? t("vmxRepair.locatorNeedsConfirm") + : t("vmxRepair.locatorConfirmClosed")} + {vmxSourceLocatorCollectorPresent ? t("vmxRepair.locatorCollected") diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 05b26e3a5..883a02e98 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,21 @@ +## 2026-07-03 — 12:47 Windows99 VMX locator receipt readback 上卷 + +**完成內容**: +- 將 live via-110 no-secret locator receipt 收斂成 `docs/operations/windows99-vmx-source-locator-receipt.snapshot.json`,只保留 redacted counters / receipt ref / bounded identity metadata flag;明確禁止 public payload 帶 raw path、內部 IP、UUID、generatedAddress、secret、write、reboot 或 VM power change。 +- `/api/v1/agents/windows99-vmx-source-locator-readback` 從單純 package ready 推進為 collector receipt readback:投影 `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`。 +- `awoooi-priority-work-order-readback` 同步上卷 locator receipt counters / confirmation gate;AwoooP Work Items 補上候選、未指派、需確認與 identity read chips,維持 mobile overflow 防護。 +- 合併 remote `effective_locator_collect_ready` / `ssh_batch_status` / `locator_collect_blocker` 防誤判欄位:direct SSH collect 仍顯示 blocker,但 via-110 receipt 已把下一步推到 identity confirmation,而不是重複等待 blind collect。 + +**已跑驗證**: +- `git diff --check`:通過。 +- `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`:通過。 +- `DATABASE_URL=postgresql+asyncpg://test:test@localhost/test PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_windows99_vmx_source_locator_readback_api.py apps/api/tests/test_awoooi_priority_work_order_readback_api.py -q -p no:cacheprovider`:`22 passed`。 +- `pnpm --dir apps/web typecheck`:通過。 + +**仍維持**: +- P0-006 尚未完成:唯一 identity-unassigned Ubuntu candidate 需要更強 identity confirmation 後,才能產生 scoped relink / restore dry-run;本輪未執行 relink / restore / apply / VM power change。 +- 未讀 secret / token / `.env` / raw sessions / SQLite / auth;未使用 GitHub / gh;未 workflow_dispatch;未啟動或關閉 VM;未重啟 host / service;未 Docker / Nginx / K3s / DB / firewall restart;未 DROP / TRUNCATE / restore / prune / delete / force push。 + ## 2026-07-03 — 12:36 Windows99 VMX locator effective collect readiness 防誤判 **完成內容**: diff --git a/docs/operations/windows99-vmx-source-locator-receipt.snapshot.json b/docs/operations/windows99-vmx-source-locator-receipt.snapshot.json new file mode 100644 index 000000000..b09de0313 --- /dev/null +++ b/docs/operations/windows99-vmx-source-locator-receipt.snapshot.json @@ -0,0 +1,52 @@ +{ + "schema_version": "windows99_vmx_source_locator_receipt_v1", + "generated_at": "2026-07-03T12:41:34+08:00", + "receipt_ref": "log://awoooi/p0-006/windows99-vmx-source-locator-111", + "source": "live_via_110_no_secret_check_mode", + "collector_schema_version": "windows99_vmx_source_locate_collector_v1", + "collector_readback_present": true, + "collector_collection_status": "collected_windows99_vmx_source_locator_stdout", + "target_host_alias": "99", + "via_host_alias": "110", + "target_vm_aliases": ["111"], + "expected_vmx_path_count": 1, + "expected_vmx_path_present_count": 0, + "candidate_source_count": 6, + "candidate_source_count_known": true, + "candidate_source_fingerprint_count": 0, + "alias_hint_candidate_count": 0, + "unassigned_ubuntu_candidate_count": 5, + "identity_metadata_read": true, + "bounded_identity_metadata_only": true, + "identity_target_hint_count": 0, + "identity_unassigned_ubuntu_candidate_count": 1, + "locate_status": "single_identity_unassigned_ubuntu_candidate_requires_confirmation", + "safe_next_step": "confirm_single_unassigned_ubuntu_candidate_is_target_alias_before_scoped_relink_dry_run", + "candidate_confirmation_required": true, + "controlled_relink_dry_run_ready": false, + "apply_allowed_by_this_receipt": false, + "public_lan_topology_redacted": true, + "redacted_public_display_only": true, + "collector_stdout_raw_path_output_redacted": true, + "raw_path_output": false, + "path_fingerprint_only": true, + "secret_value_read": false, + "password_prompt_allowed": false, + "remote_write_performed": false, + "host_reboot_performed": false, + "vm_power_change_performed": false, + "windows_service_restart_performed": false, + "windows_registry_apply_performed": false, + "scheduled_task_write_performed": false, + "km_writeback_ref": "km://awoooi/reboot-slo/windows99-vmx-source-locator-111", + "rag_chunk_ref": "rag://awoooi/reboot-slo/windows99-vmx-source-locator-111", + "mcp_evidence_ref": "mcp://awoooi/windows99/windows99-vmx-source-locator-111", + "playbook_trust_writeback_ref": "playbook://awoooi/windows99-vmx-source-locator/windows99-vmx-source-locator-111", + "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/windows99-vmx-source-locator-readback", + "GET /api/v1/agents/reboot-auto-recovery-slo-scorecard", + "GET /api/v1/agents/awoooi-priority-work-order-readback" + ] +}