From 3afdb61871d19418205de7469496c3e486d27d86 Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 10 Jul 2026 09:42:29 +0800 Subject: [PATCH] feat(windows99): validate scoped relink dry run --- apps/api/src/api/v1/agents.py | 34 +++ .../windows99_vmx_source_locator_readback.py | 274 +++++++++++++++++- ...ndows99_vmx_source_locator_readback_api.py | 196 +++++++++++++ 3 files changed, 500 insertions(+), 4 deletions(-) diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 28395190f..373441920 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -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], 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 7861ed693..98b4851b0 100644 --- a/apps/api/src/services/windows99_vmx_source_locator_readback.py +++ b/apps/api/src/services/windows99_vmx_source_locator_readback.py @@ -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], *, 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 63b9306ee..028e662c1 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 @@ -14,6 +14,7 @@ from src.api.v1.agents import router from src.services.windows99_vmx_source_locator_readback import ( build_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 ( build_windows99_vmx_source_repair_package, @@ -53,6 +54,85 @@ def _scorecard() -> dict: } +def _locator_ready_for_scoped_relink_dry_run() -> dict: + repair_package = build_windows99_vmx_source_repair_package( + _scorecard(), + generated_at="2026-07-03T11:21:00+08:00", + ) + return build_windows99_vmx_source_locator_readback( + _scorecard(), + repair_package=repair_package, + collector_receipt={ + "schema_version": "windows99_vmx_source_locator_receipt_v1", + "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": 1, + "candidate_source_fingerprint_count": 1, + "alias_hint_candidate_count": 0, + "unassigned_ubuntu_candidate_count": 0, + "identity_metadata_read": True, + "bounded_identity_metadata_only": True, + "identity_target_hint_count": 1, + "identity_unassigned_ubuntu_candidate_count": 0, + "locate_status": "single_identity_candidate_requires_confirmation", + }, + ) + + +def _valid_scoped_relink_dry_run_packet() -> dict: + return { + "schema_version": "windows99_vmx_source_scoped_relink_dry_run_packet_v1", + "plan_id": "windows99-vmx-source-relink-dry-run-111", + "target_host_alias": "99", + "target_vm_aliases": ["111"], + "candidate_confirmation_accepted": True, + "candidate_identity_evidence_ref": ( + "console://awoooi/windows99/candidate-identity-redacted-111" + ), + "source_fingerprint_ref": "fingerprint://awoooi/windows99/vmx-source-111", + "backup_ref": "backup://awoooi/windows99/pre-relink-redacted-111", + "source_of_truth_diff": { + "expected_vmx_path_present_count": 0, + "candidate_source_count": 1, + "candidate_source_fingerprint_count": 1, + "raw_path_output": False, + "path_fingerprint_only": True, + "bounded_identity_metadata_only": True, + }, + "pre_apply_backup_plan": { + "ready": True, + "backup_ref": "backup://awoooi/windows99/pre-relink-redacted-111", + "destructive_restore_allowed": False, + }, + "rollback_ref": "rollback://awoooi/windows99-vmx-source-relink-dry-run", + "post_verifier": [ + "bash scripts/reboot-recovery/collect-windows99-vmware-verify.sh --collect", + "GET /api/v1/agents/reboot-auto-recovery-slo-scorecard", + "GET /api/v1/agents/windows99-vmx-source-repair-package", + "GET /api/v1/agents/windows99-vmx-source-locator-readback", + ], + "redacted_metadata_only": True, + "bounded_identity_metadata_only": True, + "path_fingerprint_only": True, + "raw_path_output": False, + "vmx_file_content_read": False, + "secret_value_read": 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_requested": False, + "vm_power_change_requested": False, + } + + def test_windows99_vmx_source_locator_builder_is_no_secret_check_mode() -> None: repair_package = build_windows99_vmx_source_repair_package( _scorecard(), @@ -442,6 +522,78 @@ def test_windows99_candidate_confirmation_validator_rejects_sensitive_payload() assert payload["operation_boundaries"]["apply_gate_opened"] is False +def test_windows99_scoped_relink_dry_run_validator_accepts_redacted_packet() -> None: + payload = validate_windows99_vmx_source_scoped_relink_dry_run_packet( + _valid_scoped_relink_dry_run_packet(), + locator_readback=_locator_ready_for_scoped_relink_dry_run(), + ) + + assert payload["schema_version"] == ( + "windows99_vmx_source_scoped_relink_dry_run_validation_v1" + ) + assert payload["status"] == "accepted_for_reviewer_validation_passed_no_write" + assert payload["accepted"] is True + assert payload["reviewer_validation_passed"] is True + assert payload["controlled_relink_dry_run_ready"] is True + assert payload["validated_target_vm_aliases"] == ["111"] + assert payload["plan_id"] == "windows99-vmx-source-relink-dry-run-111" + assert payload["rollback_ready"] is True + assert payload["post_verifier_ready"] is True + assert payload["pre_apply_backup_plan_ready"] is True + assert payload["apply_allowed_by_this_validation"] is False + assert payload["runtime_write_authorized"] is False + assert payload["request_payload_saved"] is False + assert payload["remote_write_performed"] is False + assert payload["vm_power_change_performed"] is False + assert payload["secret_value_read"] is False + receipt = payload["projected_relink_dry_run_receipt"] + assert receipt["schema_version"] == ( + "windows99_vmx_source_scoped_relink_dry_run_receipt_v1" + ) + assert receipt["ready"] is True + assert receipt["target_selector"]["host_alias"] == "99" + assert receipt["target_selector"]["vm_aliases"] == ["111"] + assert receipt["target_selector"]["runtime_write_allowed"] is False + assert receipt["source_of_truth_diff"]["raw_path_output"] is False + assert receipt["source_of_truth_diff"]["path_fingerprint_only"] is True + assert receipt["pre_apply_backup_plan"]["ready"] is True + assert receipt["pre_apply_backup_plan"]["destructive_restore_allowed"] is False + assert "GET /api/v1/agents/windows99-vmx-source-locator-readback" in receipt[ + "post_verifier" + ] + assert receipt["operation_boundaries"]["request_payload_saved"] is False + assert receipt["operation_boundaries"]["remote_write_performed"] is False + assert receipt["operation_boundaries"]["vm_power_change_performed"] is False + + +def test_windows99_scoped_relink_dry_run_validator_rejects_sensitive_payload() -> None: + packet = _valid_scoped_relink_dry_run_packet() + packet["source_fingerprint_ref"] = "D:\\raw\\path\\vm.vmx" + packet["source_of_truth_diff"]["raw_path_output"] = True + packet["vmx_file_content_read"] = True + + payload = validate_windows99_vmx_source_scoped_relink_dry_run_packet( + packet, + locator_readback=_locator_ready_for_scoped_relink_dry_run(), + ) + + assert payload["accepted"] is False + assert payload["status"] == "rejected_scoped_relink_dry_run_packet" + assert payload["reviewer_validation_passed"] is False + assert payload["controlled_relink_dry_run_ready"] is False + assert "source_of_truth_diff.raw_path_output" in payload["rejected_fields"] + assert "vmx_file_content_read" in payload["rejected_fields"] + assert "forbidden_fragments" in payload["rejected_fields"] + assert payload["source_fingerprint_ref"] == "" + assert payload["backup_ref"] == "" + assert payload["projected_relink_dry_run_receipt"]["ready"] is False + assert payload["projected_relink_dry_run_receipt"]["post_verifier"] == [] + serialized = json.dumps(payload) + assert "D:\\" not in serialized + assert "192.168.0." not in serialized + assert payload["operation_boundaries"]["apply_gate_opened"] is False + + def test_windows99_vmx_source_locator_endpoint_returns_public_safe_readback( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -578,3 +730,47 @@ def test_windows99_candidate_confirmation_endpoint_is_no_persist_validator( assert plan["request_payload_saved"] is False assert "D:\\" not in response.text assert "192.168.0." not in response.text + + +def test_windows99_scoped_relink_dry_run_endpoint_is_no_persist_validator( + monkeypatch: pytest.MonkeyPatch, +) -> None: + locator = _locator_ready_for_scoped_relink_dry_run() + monkeypatch.setattr( + agents, + "validate_windows99_vmx_source_scoped_relink_dry_run_packet", + lambda packet: validate_windows99_vmx_source_scoped_relink_dry_run_packet( + packet, + locator_readback=locator, + ), + ) + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.post( + "/api/v1/agents/windows99-vmx-source-locator-readback/" + "validate-scoped-relink-dry-run", + json=_valid_scoped_relink_dry_run_packet(), + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["schema_version"] == ( + "windows99_vmx_source_scoped_relink_dry_run_validation_v1" + ) + assert payload["accepted"] is True + assert payload["reviewer_validation_passed"] is True + assert payload["controlled_relink_dry_run_ready"] is True + assert payload["operation_boundaries"]["no_persist_validator"] is True + assert payload["operation_boundaries"]["request_payload_saved"] is False + assert payload["operation_boundaries"]["apply_gate_opened"] is False + assert payload["operation_boundaries"]["vm_power_change_performed"] is False + receipt = payload["projected_relink_dry_run_receipt"] + assert receipt["ready"] is True + assert receipt["target_selector"]["vm_aliases"] == ["111"] + assert receipt["pre_apply_backup_plan"]["destructive_restore_allowed"] is False + assert payload["apply_allowed_by_this_validation"] is False + assert payload["runtime_write_authorized"] is False + assert "D:\\" not in response.text + assert "192.168.0." not in response.text