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"
+ ]
+}