feat(windows99): validate scoped relink dry run
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m9s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled

This commit is contained in:
ogt
2026-07-10 09:42:29 +08:00
parent 97312fa37a
commit 3afdb61871
3 changed files with 500 additions and 4 deletions

View File

@@ -507,6 +507,7 @@ from src.services.windows99_vmx_source_backup_search_package import (
from src.services.windows99_vmx_source_locator_readback import (
load_latest_windows99_vmx_source_locator_readback,
validate_windows99_vmx_source_candidate_confirmation_packet,
validate_windows99_vmx_source_scoped_relink_dry_run_packet,
)
from src.services.windows99_vmx_source_repair_package import (
load_latest_windows99_vmx_source_repair_package,
@@ -1741,6 +1742,39 @@ async def validate_windows99_vmx_source_candidate_confirmation(
) from exc
@router.post(
"/windows99-vmx-source-locator-readback/validate-scoped-relink-dry-run",
response_model=dict[str, Any],
summary="驗證 Windows99 VMX source scoped relink dry-run packet",
description=(
"針對 P0-006 Windows99 VMX source locator 的 redacted scoped relink "
"dry-run packet 做 no-persist reviewer validation。此端點只判斷 reviewer "
"validation 是否 passed不保存 payload、不回傳 raw VMX path、不讀 VMX "
"內容、不讀 secret、不 SSH、不啟停 VM、不寫 Windows、不改 registry/task、"
"不開 apply gate。"
),
)
async def validate_windows99_vmx_source_scoped_relink_dry_run(
scoped_relink_dry_run_payload: dict[str, Any],
) -> dict[str, Any]:
"""Return no-persist validation for one Windows99 relink dry-run packet."""
try:
payload = await asyncio.to_thread(
validate_windows99_vmx_source_scoped_relink_dry_run_packet,
scoped_relink_dry_run_payload,
)
return redact_public_lan_topology(payload)
except (json.JSONDecodeError, ValueError) as exc:
logger.error(
"windows99_vmx_source_scoped_relink_dry_run_invalid",
error=str(exc),
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Windows99 VMX source scoped relink dry-run 驗證器無效",
) from exc
@router.get(
"/windows99-vmx-source-backup-search-package",
response_model=dict[str, Any],

View File

@@ -38,6 +38,15 @@ _CANDIDATE_CONFIRMATION_PACKET_SCHEMA_VERSION = (
_SCOPED_RELINK_DRY_RUN_SCHEMA_VERSION = (
"windows99_vmx_source_scoped_relink_dry_run_v1"
)
_SCOPED_RELINK_DRY_RUN_PACKET_SCHEMA_VERSION = (
"windows99_vmx_source_scoped_relink_dry_run_packet_v1"
)
_SCOPED_RELINK_DRY_RUN_VALIDATION_SCHEMA_VERSION = (
"windows99_vmx_source_scoped_relink_dry_run_validation_v1"
)
_SCOPED_RELINK_DRY_RUN_RECEIPT_SCHEMA_VERSION = (
"windows99_vmx_source_scoped_relink_dry_run_receipt_v1"
)
_RECEIPT_FILE = "windows99-vmx-source-locator-receipt.snapshot.json"
_COLLECTOR_SCRIPT_PATH = "scripts/reboot-recovery/locate-windows99-vmx-source.sh"
_LOCATOR_SCRIPT_PATH = "scripts/reboot-recovery/windows99-vmx-source-locator.ps1"
@@ -93,6 +102,34 @@ _CANDIDATE_CONFIRMATION_FORBIDDEN_FRAGMENTS = [
"token=",
"secret_value",
]
_SCOPED_RELINK_DRY_RUN_REQUIRED_FIELDS = [
"schema_version",
"plan_id",
"target_host_alias",
"target_vm_aliases",
"candidate_confirmation_accepted",
"candidate_identity_evidence_ref",
"source_fingerprint_ref",
"backup_ref",
"source_of_truth_diff",
"pre_apply_backup_plan",
"rollback_ref",
"post_verifier",
"redacted_metadata_only",
"bounded_identity_metadata_only",
"path_fingerprint_only",
"raw_path_output",
"vmx_file_content_read",
"secret_value_read",
"remote_write_performed",
"runtime_write_performed",
"host_reboot_performed",
"vm_power_change_performed",
"windows_registry_apply_performed",
"scheduled_task_write_performed",
"apply_requested",
"vm_power_change_requested",
]
def _dict(value: Any) -> dict[str, Any]:
@@ -292,6 +329,10 @@ def _build_candidate_confirmation_gate(
}
def _scoped_relink_plan_id(target_aliases: list[str]) -> str:
return f"windows99-vmx-source-relink-dry-run-{','.join(target_aliases) or 'none'}"
def _build_projected_scoped_relink_dry_run_plan(
*,
accepted: bool,
@@ -321,10 +362,7 @@ def _build_projected_scoped_relink_dry_run_plan(
return {
"schema_version": _SCOPED_RELINK_DRY_RUN_SCHEMA_VERSION,
"plan_id": (
"windows99-vmx-source-relink-dry-run-"
f"{','.join(missing_aliases) or 'none'}"
),
"plan_id": _scoped_relink_plan_id(missing_aliases),
"status": (
"ready_for_no_write_review"
if ready
@@ -550,6 +588,234 @@ def validate_windows99_vmx_source_candidate_confirmation_packet(
}
def validate_windows99_vmx_source_scoped_relink_dry_run_packet(
packet: dict[str, Any],
*,
locator_readback: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Validate one redacted scoped relink dry-run packet.
This no-persist validator proves the dry-run is reviewable and bounded. It
does not save payloads, reveal raw VMX paths, read VMX contents, read
secrets, SSH, change VM power state, write Windows state, or open apply.
"""
locator = locator_readback or load_latest_windows99_vmx_source_locator_readback()
current_target_aliases = _strings(locator.get("target_vm_aliases"))
current_missing_aliases = _strings(locator.get("missing_vmx_aliases"))
allowed_target_aliases = current_missing_aliases or current_target_aliases
submitted_aliases = _strings(packet.get("target_vm_aliases"))
submitted_aliases_valid = bool(
submitted_aliases
and allowed_target_aliases
and set(submitted_aliases).issubset(set(allowed_target_aliases))
)
expected_plan_id = _scoped_relink_plan_id(submitted_aliases)
source_of_truth_diff = _dict(packet.get("source_of_truth_diff"))
pre_apply_backup_plan = _dict(packet.get("pre_apply_backup_plan"))
post_verifier = _strings(packet.get("post_verifier"))
missing_fields = [
field
for field in _SCOPED_RELINK_DRY_RUN_REQUIRED_FIELDS
if field not in packet
]
rejected_fields: list[str] = []
forbidden_fragments = _forbidden_fragments(packet)
if packet.get("schema_version") != _SCOPED_RELINK_DRY_RUN_PACKET_SCHEMA_VERSION:
rejected_fields.append("schema_version")
if str(packet.get("plan_id") or "") != expected_plan_id:
rejected_fields.append("plan_id")
if str(packet.get("target_host_alias") or "") != "99":
rejected_fields.append("target_host_alias")
if not submitted_aliases_valid:
rejected_fields.append("target_vm_aliases")
if packet.get("candidate_confirmation_accepted") is not True:
rejected_fields.append("candidate_confirmation_accepted")
if str(packet.get("candidate_identity_evidence_ref") or "").strip() == "":
rejected_fields.append("candidate_identity_evidence_ref")
if str(packet.get("source_fingerprint_ref") or "").strip() == "":
rejected_fields.append("source_fingerprint_ref")
if str(packet.get("backup_ref") or "").strip() == "":
rejected_fields.append("backup_ref")
if not source_of_truth_diff:
rejected_fields.append("source_of_truth_diff")
if source_of_truth_diff.get("raw_path_output", False) is not False:
rejected_fields.append("source_of_truth_diff.raw_path_output")
if source_of_truth_diff.get("path_fingerprint_only") is not True:
rejected_fields.append("source_of_truth_diff.path_fingerprint_only")
if source_of_truth_diff.get("bounded_identity_metadata_only") is not True:
rejected_fields.append("source_of_truth_diff.bounded_identity_metadata_only")
if not pre_apply_backup_plan:
rejected_fields.append("pre_apply_backup_plan")
if pre_apply_backup_plan.get("ready") is not True:
rejected_fields.append("pre_apply_backup_plan.ready")
if str(pre_apply_backup_plan.get("backup_ref") or "").strip() == "":
rejected_fields.append("pre_apply_backup_plan.backup_ref")
if str(packet.get("rollback_ref") or "").startswith("rollback://") is False:
rejected_fields.append("rollback_ref")
if not post_verifier:
rejected_fields.append("post_verifier")
if "GET /api/v1/agents/windows99-vmx-source-locator-readback" not in post_verifier:
rejected_fields.append("post_verifier.locator_readback")
required_true_flags = [
"redacted_metadata_only",
"bounded_identity_metadata_only",
"path_fingerprint_only",
]
required_false_flags = [
"raw_path_output",
"vmx_file_content_read",
"secret_value_read",
"remote_write_performed",
"runtime_write_performed",
"host_reboot_performed",
"vm_power_change_performed",
"windows_registry_apply_performed",
"scheduled_task_write_performed",
"apply_requested",
"vm_power_change_requested",
]
for field in required_true_flags:
if packet.get(field) is not True:
rejected_fields.append(field)
for field in required_false_flags:
if packet.get(field, False) is not False:
rejected_fields.append(field)
if forbidden_fragments:
rejected_fields.append("forbidden_fragments")
accepted = not missing_fields and not rejected_fields
status = (
"accepted_for_reviewer_validation_passed_no_write"
if accepted
else "rejected_scoped_relink_dry_run_packet"
)
next_safe_step = (
"commit_or_attach_no_secret_dry_run_receipt_then_rerun_no_secret_"
"collector_and_scorecard_no_vm_power_change"
if accepted
else "resubmit_redacted_scoped_relink_dry_run_packet_without_raw_paths_or_runtime_actions"
)
plan_id = expected_plan_id if accepted else str(packet.get("plan_id") or "")
source_fingerprint_ref = (
str(packet.get("source_fingerprint_ref") or "").strip() if accepted else ""
)
backup_ref = str(packet.get("backup_ref") or "").strip() if accepted else ""
candidate_identity_evidence_ref = (
str(packet.get("candidate_identity_evidence_ref") or "").strip()
if accepted
else ""
)
rollback_ref = str(packet.get("rollback_ref") or "").strip() if accepted else ""
projected_source_diff = {
"expected_vmx_path_count": locator.get("expected_vmx_path_count"),
"expected_vmx_path_present_count": source_of_truth_diff.get(
"expected_vmx_path_present_count"
)
if accepted
else None,
"candidate_source_count": source_of_truth_diff.get("candidate_source_count")
if accepted
else None,
"candidate_source_fingerprint_count": source_of_truth_diff.get(
"candidate_source_fingerprint_count"
)
if accepted
else None,
"candidate_identity_evidence_ref": candidate_identity_evidence_ref,
"source_fingerprint_ref": source_fingerprint_ref,
"backup_ref": backup_ref,
"raw_path_output": False,
"path_fingerprint_only": True,
"bounded_identity_metadata_only": True,
}
operation_boundaries = {
"no_persist_validator": True,
"request_payload_saved": False,
"secret_value_read": False,
"raw_path_output": False,
"vmx_file_content_read": False,
"password_prompt_allowed": False,
"remote_write_performed": False,
"runtime_write_performed": False,
"host_reboot_performed": False,
"vm_power_change_performed": False,
"windows_registry_apply_performed": False,
"scheduled_task_write_performed": False,
"apply_gate_opened": False,
}
return {
"schema_version": _SCOPED_RELINK_DRY_RUN_VALIDATION_SCHEMA_VERSION,
"status": status,
"accepted": accepted,
"reviewer_validation_passed": accepted,
"controlled_relink_dry_run_ready": accepted,
"target_host_alias": "99",
"target_vm_aliases": current_target_aliases,
"missing_vmx_aliases": current_missing_aliases,
"submitted_target_vm_aliases": submitted_aliases,
"validated_target_vm_aliases": submitted_aliases if accepted else [],
"current_locator_status": str(locator.get("status") or ""),
"current_candidate_confirmation_status": str(
locator.get("candidate_confirmation_status") or ""
),
"plan_id": plan_id,
"expected_plan_id": expected_plan_id,
"source_fingerprint_ref": source_fingerprint_ref,
"backup_ref": backup_ref,
"candidate_identity_evidence_ref": candidate_identity_evidence_ref,
"missing_fields": missing_fields,
"rejected_fields": rejected_fields,
"forbidden_fragment_count": len(forbidden_fragments),
"rollback_ready": accepted,
"post_verifier_ready": accepted,
"pre_apply_backup_plan_ready": accepted,
"km_writeback_ready": accepted,
"playbook_trust_writeback_ready": accepted,
"request_payload_saved": False,
"apply_allowed_by_this_validation": False,
"runtime_write_authorized": False,
"remote_write_performed": False,
"vm_power_change_performed": False,
"secret_value_read": False,
"projected_relink_dry_run_receipt": {
"schema_version": _SCOPED_RELINK_DRY_RUN_RECEIPT_SCHEMA_VERSION,
"receipt_ref": (
f"receipt://awoooi/windows99/{plan_id}" if accepted else ""
),
"ready": accepted,
"status": (
"reviewer_validation_passed_no_write"
if accepted
else "blocked_reviewer_validation_not_passed"
),
"plan_id": plan_id,
"target_selector": {
"host_alias": "99",
"vm_aliases": submitted_aliases if accepted else [],
"missing_vmx_aliases": submitted_aliases if accepted else [],
"secret_collection_allowed": False,
"vm_power_change_allowed": False,
"runtime_write_allowed": False,
},
"source_of_truth_diff": projected_source_diff,
"pre_apply_backup_plan": {
"ready": accepted,
"backup_ref": backup_ref,
"destructive_restore_allowed": False,
},
"rollback_ref": rollback_ref,
"post_verifier": post_verifier if accepted else [],
"operation_boundaries": operation_boundaries,
},
"operation_boundaries": operation_boundaries,
"next_safe_step": next_safe_step,
}
def build_windows99_vmx_source_locator_readback(
scorecard: dict[str, Any],
*,