Merge remote-tracking branch 'origin/main' into codex/p0-006-ai-log-triage-20260709
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user