Skip to content

VMware Workstation Lifecycle Testing

The shared lifecycle host-state checks verify that first-boot appliances retain vcf-sdk==9.1.1.0, VCF.PowerCLI==9.1.1.25718932, and Connect-VIServer after the wheel-only test deployment. The PowerCLI import and command check run directly as the unprivileged appliance SSH user rather than through sudo.

Focused reverse-proxy acceptance

invoke-lifecycle-test.ps1 -ReverseProxyOnly -PullRequestNumber <number> uses a two-interface appliance and a host-reachable Site A network. Supply the powered-off, provenance-admitted builder VMX explicitly with -ApplianceVmxPath; the runner installs its wheel from the admitted clean source commit. Use -Purpose docs -KeepVms when retaining the dedicated documentation appliance. Credentials follow the protected lifecycle handoff. The direct runner also rejects combining -ReverseProxyOnly with any other focused lifecycle mode before VM preparation.

The host fixture binds only to the selected Site A host address. The consumer saves desired state and uses global Appliance Apply to verify standard and custom HTTP and HTTPS publication, the managed listener certificate, preserve and strip path mapping, normalized forwarding headers, upstream authentication challenges, WebSocket upgrade and frame exchange, reserved paths, exact Host selection, standard and custom redirects, duplicate hostname rejection, and the cached health API. An owned fixture outage verifies safe Apply and degraded health followed by recovery. An audited appliance reboot repeats publication and cached health checks. HTTPS upstream checks use public example.com and self-signed.badssl.com endpoints for CA validation and explicit insecure degradation; endpoint failure fails the run. Pinning and wrong-pin rejection use a separate owned TLS origin on the selected host adapter. Supply -ReverseProxyTlsFixtureDirectory with existing certificate.pem and private-key.pem files. The consumer loads one leaf and keeps its TLS listener alive across the entire run, including reboot and archive checks. Before creating the short-lived key, follow the credential policy or obtain its explicit narrow fixture exception. Record original task/source ownership outside the fixture directory before use, protect its directory and files for the current user and SYSTEM, and release it through the supported owning tool. The launcher validates access without printing key material; it never generates, copies, or adopts a fixture key. Plan-only mode does not require a key. These checks do not yet prove DNS ownership, archive/reset recovery, or failed-Apply rollback. Those remain acceptance gates for issue #723. Optional screenshot tooling uses the installed Node.js, Playwright, Sharp, and Chrome paths. Session cookies travel through standard input after the Node consumer is assigned to a Windows process job. Capture uses an isolated browser context, stores no session-state file, and proves the entire browser process tree inactive before reporting success. The lifecycle owns the screenshot output beneath its result root; no screenshot success is implied by passing the socket checks.

Configure non-interactive 1Password authentication

Use a 1Password service account with read access to the exact Atlaso Environment. Vault access is not required. Copy or rotate the service-account token in the 1Password portal, then enter it once at the non-echoing prompt:

.\scripts\windows\vmware\initialize-onepassword-service-account.ps1

On first use, the helper also prompts for the opaque ID of the exact Atlaso Environment and creates the Git-ignored .atlaso-local\onepassword-environment-id file. To supply the ID explicitly, use:

.\scripts\windows\vmware\initialize-onepassword-service-account.ps1 -EnvironmentId '<Atlaso Environment ID>'

-OnePasswordEnvironmentId is an alias for -EnvironmentId. The helper validates the ID against Atlaso's pinned identity before storing configuration. An existing Environment ID file is validated and preserved, including with -Force; it must contain exactly one non-empty line identifying the pinned Environment. Invalid selectors fail before token storage. The ID file belongs to the checkout containing the script, independently of a custom -TokenFile path.

The helper validates the service-account token shape and stores only current-user DPAPI ciphertext at the Git-ignored .atlaso-local\onepassword-service-account-token.dpapi path. The file is usable only by the same Windows user on the same machine; its ACL allows only that user and SYSTEM. Never put the plaintext token in Git, .env, a PowerShell profile, a command argument, or OP_SERVICE_ACCOUNT_TOKEN in the caller. Rotate the token by rerunning the helper with the explicit replacement option:

.\scripts\windows\vmware\initialize-onepassword-service-account.ps1 -Force

The VMware build, normal-test-VM, deployment, and export entry points use this authentication precedence:

  1. Explicit -OnePasswordServiceAccountTokenFile DPAPI ciphertext.
  2. Explicit -OnePasswordAccount desktop authorization.
  3. The checkout-local DPAPI token file when present.
  4. Existing single-account desktop discovery.

The token is decrypted only in a bounded child and supplied to the immediate SDK or op run process. The SDK removes its environment copy before Environment retrieval. A missing, corrupt, wrong-user, malformed, revoked, or unauthorized token fails before VMware or network mutation. If the original token was not retained, rotate it in the portal and use the secure setup prompt; the portal does not provide a local token filename.

Reuse primary-checkout configuration in a task worktree

When preparing a task worktree under the same Windows user on the same host, reuse the primary checkout's validated configuration before starting unattended work. This is a setup procedure for the existing authentication path; the VMware wrappers do not automatically copy configuration. A token path in the primary checkout cannot be passed directly to a task-worktree consumer: token resolution requires containment beneath the consuming checkout's own .atlaso-local directory.

  1. Resolve the primary checkout using git rev-parse --path-format=absolute --git-common-dir and git worktree list --porcelain. Cross-check the common Git directory against the primary checkout's Git metadata and the same repository's inventory; do not infer the source from a branch named main or a conventional path. Use the already verified task worktree beneath the configured Codex git-worktree-root as destination. Stop for maintainer direction if either identity, repository relationship, user/host binding, or permitted root is ambiguous.
  2. Inspect only these two named paths beneath each checkout's .atlaso-local directory: onepassword-environment-id and onepassword-service-account-token.dpapi. Validate path-component containment, ordinary directories and single-link regular files, and absence of reparse points along both paths before reading or copying. Verify the destination paths are Git-ignored with git check-ignore; reject tracked configuration. Preserve valid existing destination files. Do not overwrite, repair permissions on, or rotate invalid or conflicting files automatically. A valid destination token need not have identical ciphertext to the primary token.
  3. Import scripts/windows/vmware/Atlaso.OnePasswordCredentials.psm1 from the verified task checkout. For each selector being reused, capture Resolve-AtlasoOnePasswordEnvironmentId -RepositoryRoot <verified-checkout> into a local variable and pass it directly to Assert-AtlasoOnePasswordEnvironmentId, retaining the built-in identity pin. Never emit that variable. For each token, capture or discard the result of Resolve-AtlasoOnePasswordServiceAccountTokenFile -RepositoryRoot <verified-checkout> with its exact -TokenFile path so a missing file fails explicitly. This enforces containment and invokes Assert-AtlasoOnePasswordServiceAccountTokenFile for bounded ciphertext size, a single hard link, current-user ownership, disabled inherited access, and access restricted to the current user and SYSTEM. These helpers do not replace the preceding filesystem checks for the selector and every ancestor.
  4. Before writing any copy, prepare restrictive destination directory permissions for the current user and SYSTEM. If an existing directory is unsafe or ambiguous, stop without changing it. Copy each missing, validated named file with no-replacement semantics, retaining its bytes exactly. Do not decrypt/re-encrypt the token or change either source file. Ensure the copied token has current-user ownership, inheritance disabled, and explicit read access for only the current user and SYSTEM; verify the existing token ACL contract before use. Recheck identities and destination absence immediately before copying, and reject concurrent replacement instead of overwriting it. Never recursively copy .atlaso-local: caches, secrets staging, logs, cleanup markers, reservations, and other runtime state remain owned by their original checkout.
  5. Revalidate the destination paths, byte preservation, selector pin, and token through the same helpers. Report only non-secret success or prerequisite categories, never selector/token contents. Local file validation cannot prove DPAPI decryptability, token validity, or Environment authorization. The intended supported workflow must complete its bounded-child credential preflight against the exact Atlaso Environment before VMware or network mutation. Decryption remains inside that child; do not decrypt for display or run a separate plaintext probe.
  6. Preserve the authentication precedence above, including explicit token paths, desktop accounts, Environment selectors, and explicit credential inputs. When unattended authentication is selected, require the task-local token before invoking a consumer that could otherwise discover a desktop account. Missing source files, invalid selectors, unsafe ACLs, wrong-user/host ciphertext, revoked tokens, or denied Environment access must produce the precise non-secret prerequisite and stop that unattended operation. Do not silently switch to desktop approval or request plaintext in chat. A maintainer can separately choose the secure initialization/rotation prompt above or explicit desktop authorization.

Repeated setup copies nothing when the destination is already valid. Keep configuration out of commits, pull requests, release artifacts, and logs. Record ownership of only the task's copied configuration; later task-worktree cleanup must never remove or alter the primary checkout's originals.

For a new task that needs automatic completed-task cleanup, use the creation-receipted configuration owner before any other tooling creates its .atlaso-local directory. It records the original copy and exposes the supported configuration resource cleanup operation. Existing unreceipted copies remain preserved; never retrofit ownership or use generated-tree deletion for credential configuration.

Verify source-snapshot admission

The focused Windows source-snapshot regression runs with PowerShell 7 using python -m pytest -q tests/test_photon_image.py::test_vmware_source_snapshot_resists_during_packer_checkout_changes. Its certificate admission checks create an isolated synthetic repository under the pytest output directory, with plan and evidence paths beneath that repository's test-results tree. No pytest --basetemp override is needed. Both inspectors must reject mutable checkout invocation, out-of-root paths, missing Python, and existing evidence before any credential access. These admission fixtures do not create VMs or perform certificate handoffs.

Release task-owned LAN segments

Lifecycle runs that request lan:<name> now create an immutable receipt before registering a new segment. Runtime admission requires a clean source checkout and rechecks its exact commit before VM/segment creation and before and after wheel building. The wheel is built from a fresh Git archive of the admitted commit, so transient edits in the live checkout cannot enter the artifact. Runtime modules, seed helpers, the appliance helper, and the interop harness also load from the admitted archive; identity naming loads directly from that commit object. Extracted snapshots deny same-user write access with inherited ACLs throughout their lifetime, including new child creation. Delete rights remain available to the supported owned-artifact cleanup path. Git archive bytes are captured directly from the process output; the on-disk archive is pinned and digest-checked, and extracted file hashes are verified against that archive under write exclusion before any helper or build can use the snapshot. Every extracted file is pinned and checked against its creation identity and single-link count before ACL propagation, preserving external files if a hard link was substituted. Snapshot ancestors and newly created directories are pinned with no-follow handles that deny deletion before any child is extracted, and remain pinned through final verification. This prevents a concurrent directory-to-junction replacement from redirecting archive writes outside the snapshot. The snapshot transfers its directory and file pins to its caller after verification. The lifecycle runner retains them through all helper consumers and releases them on exit; the wheel builder retains its pins through upload and installation. Preflight failure releases those pins before ownership-aware artifact cleanup. Thus renaming the verified root cannot substitute a different source tree between verification and use. Before applying inheritable ACLs, the exporter freezes each expected directory with an object-only ACL update and checks every immediate entry against the archive inventory. Unknown files, directories, and links are refused before ACL propagation; failed construction restores the directory freeze without touching unverified descendants. Runtime admission also compares the executing runner's already parsed script text with the admitted Git object before creating resources. This detects an accidentally stale parsed runner when its pathname was restored before the clean-checkout check. The executing entry point and host process remain trusted: this self-check cannot authenticate code that has deliberately removed the verifier. Moving that check into another mutable checkout launcher would not establish an independent trust boundary. Host execution-policy enforcement is outside this ownership receipt contract.

Preferences publication rechecks the original file identity immediately before replacement and preserves a competing file when drift is already visible. A replacement during the remaining publication window retains ambiguous recovery objects for explicit recovery; an unverified backup is never installed automatically.

LAN registration creation independently verifies the exact name and ID under a no-write/no-delete preferences pin and retains that pin through result construction, including reuse of an existing shared registration.

Wheel upload requires exactly one output matching the filename, size, and SHA-256 emitted by that pip invocation. The wheel is held under an ordinary single-link read pin through upload, and the guest verifies its SHA-256 before installation. Extra, renamed, or substituted output fails instead of selecting a wheel by modification time. Ownership receipts and lifecycle manifests retain their original creation handles with writer and deletion exclusion through publication. The publisher flushes before and after renaming that exact handle, so a substituted staging pathname cannot replace ownership evidence. After publication, the receipt is identity-checked under a retained read pin through independent evidence publication and provider registration. Backup capture likewise pins the displaced pathname and requires the original provider identity; rollback bytes come from the retained original handle. An ambiguous displaced identity preserves the transaction files for explicit recovery rather than automatically restoring potentially substituted bytes. Normal rollback retains both source and target pins, captures the verified failed publication by handle, and renames the displaced handle without replacing an existing pathname. If a competing pathname appears during that transition, rollback refuses and preserves both captured objects for recovery. Successful preferences transactions delete their backup through the retained identity handle before releasing it. If archive, credential, module, or other pre-resource admission fails, the runner releases only its newly created preflight result directory. Cleanup verifies the independently derived parent, rejects links, unexpected entries, and nonempty VM/seed roots, and verifies absence so an ordinary preflight failure can be retried. The root is pinned from creation; failure cleanup captures descendant handles before validation and deletes only those exact objects. Cleanup admits only this invocation's exact archive/output paths and the archive's recorded extraction inventory. Creation-time stream identities bind each file; capture refuses replacements at expected paths. Cleanup denies descendant writes before capture through deletion, restoring the original root ACL when cleanup refuses. Cleanup freezes each verified directory through an object-only ACL update and requires single-link regular files; it never propagates an inherited ACL into unverified descendants. Refusal restores the directory ACLs through their retained handles, preserving external files reached by unexpected hard links. The result-root guard and VM/seed directory pins remain alive through all lifecycle consumers and cleanup. VM cleanup uses KeepRemovalRoot to remove validated contents while retaining the pinned empty VM directory for later owned artifact-root teardown. Preflight cleanup verifies the original root identity when upgrading its handle for deletion. Retained-root cleanup captures each remaining descendant against the admitted cleanup inventory and deletes through those handles; replaced or added entries are refused. Snapshot consumers also retain a native recursive change guard: any namespace or security change invalidates admission and wheel output, including a transient added module removed before the build finishes. The Python lifecycle consumer checks the retained source guards immediately before and after execution and uses Python isolated mode so its script directory, current directory and PYTHONPATH cannot supply shadow imports. Directory sharing alone does not prevent child creation on Windows. Unrecorded additions before capture are preserved. Cleanup rechecks the entire captured path set before marking any handle for deletion, so additions observed at that boundary preserve both owned evidence and foreign entries. The durable publisher uses its own versioned helper type so existing PowerShell sessions can reload the module after an upgrade. Dirty or changed source is refused before the next resource or wheel publication; plan-only output makes no runtime source-provenance claim. The receipt binds its random VMware ID, exact name and preferences path to the task ID, repository, source commit, PR number, and lifecycle result root. plan.json records lan_segment_owner; vmware-identity.json records lan_segments with each original receipt path and SHA-256. Outside Codex, the unique canonical lab name is the task ID. The receipt is flushed under a staging name and published by a write-through rename that refuses replacement. The required creation callback flushes and durably publishes the pending identity evidence before preferences can register the segment. An interruption therefore leaves either no registration or a registration with its original independently recorded receipt hash; cleanup can verify an absent pending registration normally. The pinned provider handle resolves its canonical long pathname; parent file identity and the canonical leaf key the mutex, so drive/UNC and short-name aliases share transaction exclusion and recovery discovery. Receipts store that canonical provider pathname, and cleanup resolves its input the same way before checking receipt identity. A provider-path mutex spans recovery preflight, preferences publication, rollback, and artifact retirement across processes and Windows sessions. An overlapping transaction refuses immediately; retries inspect retained recovery artifacts after the previous transaction releases its lock. Independent final registration and reference readback runs before rollback state is retired; a failed final check restores the displaced registration. Successful readback retains its no-delete provider pin through transaction commit and result creation. All directory guards signal one shared native event. Its final check after all requests are armed defines the reference-snapshot commit point across roots. Missing registered-parent directories are refused immediately rather than rechecked without a guard. An existing named segment is reused without claiming ownership or rewriting its ID. Shared segments and legacy residue without creation evidence remain preserved for maintainer-directed reconciliation.

After removing the owned VMs through remove-lifecycle-vms.ps1 or the lifecycle cleanup option, separately release each owned segment through Remove-AtlasoWorkstationLanSegment, exported by Atlaso.WorkstationCleanup.psm1. VM removal alone does not release a LAN registration. Preserve the creation receipts and their original hashes on the durable controller evidence surface before deleting any lifecycle result root.

  1. Independently verify the originating task, repository, creation commit, exact PR, canonical lab root, and original receipt hash against the task's creation evidence and lifecycle identity. Do not derive expected ownership or a new expected hash from the candidate receipt during cleanup.
  2. Close the Workstation UI normally and finish VMware VM/vmrun activity. The cleanup operation never closes the UI, stops another VM, or terminates another task's process. Coordinate with other validation tasks before proceeding.
  3. Supply every independently configured VM storage root in $vmRoots, including locations of unregistered VMX files. The operation additionally inspects all paths in Workstation's inventory. Missing configured roots, inaccessible registered directories, malformed adapter/inventory records, links, and surviving references refuse cleanup.
  4. Preview and execute the exact receipt-bound operation:
Import-Module ./scripts/windows/vmware/Atlaso.WorkstationCleanup.psm1
$owner = @{
    task_id = $verifiedTaskId
    repository = 'mdaneri/Atlaso'
    source_commit = $verifiedCreationCommit
    pr = $verifiedPrNumber
    lab_root = $verifiedLabRoot
}
$arguments = @{
    ReceiptPath = $preservedReceiptPath
    ReceiptSha256 = $originalReceiptSha256
    Owner = $owner
    VmRoots = $vmRoots
}
Remove-AtlasoWorkstationLanSegment @arguments -WhatIf
$releaseEvidence = Remove-AtlasoWorkstationLanSegment @arguments -Confirm:$false
  1. Retain the returned provider_id, receipt digest, task/PR binding, registration_absent, and adapter_references_absent in validation_resource_release_evidence. The cleanup controller performs a separate exact-identity invocation/readback before accepting resource absence. An already-absent retry still validates the original ownership and current adapter references.

The operation requires existing UTF-8 preferences without a BOM and an existing inventory. It removes only the two exact segment records, preserving all other bytes, line endings, segment indices, and the provider's high-water count. Duplicate or unsupported registration fields and name/ID drift fail closed. Preferences updates pin ordinary paths, exclude writers, atomically replace the file, compare the displaced identity/bytes, and independently verify absence. Native recursive directory-change requests are armed before enumeration and retained through the final readback. Any descendant change, even a transient VMX creation/deletion, or a notification error/overflow refuses the scan; read-only file pins alone cannot prevent new VMX files from appearing beneath a storage root. The verified original displaced state is restored through pinned handles; an ambiguous displaced identity, failed rollback, or interrupted transaction retains recovery copies and blocks mutation until reconciled, including retained atlaso-recovery-*.tmp and atlaso-cas-*.tmp artifacts as well as LAN staging and backup files. Never repair that condition by deleting a backup or rewriting preferences broadly. Registration absence covers the supplied complete storage roots and provider inventory; a caller must not omit an unregistered VM storage location to obtain a successful result.

Wheel-only deployment authentication

Wheel deployment admits only a canonical unsigned bootstrap environment or a legacy direct development virtualenv. Before package installation or service, helper, trust-store, or database changes, it refuses signed release destinations, unrecognized or inconsistent release links, invalid bootstrap metadata, and retained Appliance Update maintenance, status, restart-gate, or finalizer evidence. This applies to both upgrades and downgrades and to same-version wheels: matching version text does not prove matching signed bytes. Both SSH authentication paths use this admission.

The installed helper must support appliance-update wheel-deploy-lock-v1 --real before deployment can proceed. Older helpers fail closed: create a fresh disposable bootstrap VM from an image containing the lock-capable helper; do not manually replace its helper to bypass admission. Deployment holds an exclusive lock on /run/atlaso-appliance-mutation.lock through asset installation and web readiness, releasing it before starting or restoring the worker so queued updates and interrupted-helper reconciliation can resume. Appliance Update helpers take shared locks on the same inode before mutation, including status publication. The outer systemd dispatcher acquires admission before preparing privileged directories or stopping a status unit, and holds it through child execution and cleanup; an active operation refuses conflicting work. An update refused solely because deployment owns this lock remains queued for the normal worker retry interval. The helper records explicit no-mutation admission evidence with its distinct conflict status; check, source-sync and installation tasks retry using the same task and child identities. Completed children remain unchanged. This does not relax the existing cleanup-required handling when prior helper ownership or credential cleanup cannot be proven. Shared update locks permit the updater's own recovery and worker handoff helpers to run together. Ordinary startup guards with no update evidence remain available to deployment's service restarts. Never delete or replace the lock file to clear a conflict: wait for the owning operation to finish and retry admission.

Use signed Appliance Update to change a signed release. For wheel testing, create a separate disposable bootstrap VM through the receipt-bound creation workflow. A refusal preserves the original release links, receipt, package environment, database, and recovery state. It does not reset an already damaged signed environment or clear a maintenance hold. Retain the exact verified release assets and failed task evidence for supported recovery; do not rewrite receipts or finalizers, remove maintenance markers, or retry a wheel deployment to bypass them. A successful internal OpenAPI probe alone does not establish front-door recovery or a healthy rollback.

For a wheel-only deployment to the canonical test VM, use scripts/windows/vmware/deploy-wheel.ps1 with the secure Windows 1Password handoff documented in the full technical reference. Verify the unique Atlaso Environment and concealed DEFAULT_ADMIN_PASSWORD variable by name, then pass its opaque Environment ID through -OnePasswordEnvironmentId. The checkout-local service-account token is used automatically when present; pass -OnePasswordAccount only to select desktop authorization. The bridge automatically selects standard GIL-enabled Windows x64 CPython 3.14; use -OnePasswordPython only as an explicit override. Python 3.10 through 3.13, x86, ARM64, and free-threaded 3.14t are rejected. The handoff uses the supported 1Password Python SDK integration and retrieves the value only inside the bounded Paramiko deployment child, preserves SSH known-host verification, and fails closed when authorization or any required Environment input is unavailable. Authorization and Environment retrieval each use the deployment timeout, so a revoked token, ignored desktop approval prompt, or non-responsive SDK exits without beginning the VMware deployment. The build stages the SDK, Paramiko, and their transitive wheels from the hash-verified requirements-onepassword-deploy.lock; the bounded child installs only from that local wheel set with index access disabled. -SkipBuild therefore fails closed unless the complete vetted set is already present in dist. Key-backed Windows transfers keep scp sources and destinations separate and cross the PowerShell login shell through a secret-free base64 sh -lc wrapper. Password-backed SSH supports one password-only keyboard-interactive challenge, rejects OTP/MFA prompts, and uses a separate deployment timeout from the readiness allowance with a non-PTY sudo -S handoff. Do not pass a password argument, create a local .env file, or use the retired ATLASO_DEPLOY_SSH_PASSWORD fallback. The PowerShell parent performs local build and input preparation without the credential, then invokes the isolated Python child directly. That child obtains DEFAULT_ADMIN_PASSWORD with a human-approved 1Password desktop authorization prompt and keeps it in process memory only. Python starts with -I -S and an explicit dependency path so startup hooks and inherited PYTHONPATH cannot observe the value. The beta-only op run --environment flag is not supported by the stable CLI and is not part of this workflow.

Normal test VM creation uses a separate development-CA handoff. It requires the Environments-enabled beta CLI at C:\Program Files\1Password CLI\op.exe with op run --environment, retrieves the selected Environment only through that bounded child, and cryptographically verifies its concealed signer against the checked-in development root before VM mutation. The stable SDK-backed wheel deployment above does not change this normal-test-wrapper-only trust path.

Atlaso uses a VMware Workstation lifecycle lab as its canonical deployed-behavior environment. The path uses VMX/VMDK artifacts and vmrun.exe, then delegates appliance behavior checks to the provider-neutral Python lifecycle runner.

Every task-owned VM used for pull-request validation has the canonical identity Atlaso-PR-<number>-<purpose>[-<collision-safe-suffix>]. -PullRequestNumber is required and accepts only a positive integer. The shared helper sanitizes -Purpose and -CollisionSuffix to lowercase ASCII words separated by hyphens. The normal wrapper defaults the purpose to test-vm; the lifecycle wrapper defaults it to lifecycle and generates a timestamp-plus-random suffix when one is not supplied. Use an explicit suffix to distinguish multiple normal VMs for the same pull request. The exact name is used for Workstation displayName, the output or lifecycle-lab directory, the normal VMX filename, and lifecycle result/log identity. Lifecycle runs also write vmware-identity.json, which records the final display names and absolute VMX paths; include those values with pull-request validation evidence.

Reuse, redeploy, and cleanup rederive the expected identity and fail before mutation when the canonical directory, exact VMX path, displayName, or lifecycle plan ownership differs. The tools never rename, adopt, reuse, redeploy, or delete generic, issue-only, or differently owned VMs. Atlaso intentionally has no provisional shared/live VM workflow: wait until the pull request exists, then create a PR-numbered VM before collecting any acceptance evidence.

Run all Windows commands in PowerShell 7.4 or newer (pwsh). Earlier PowerShell releases and Windows PowerShell 5.1 (powershell.exe) are not supported. The single-command wrapper enforces that runtime, resolves the installed pwsh application, and launches its lifecycle child in PowerShell 7 so the default cleanup path cannot fall back to Windows PowerShell 5.1. A missing pwsh installation fails before any lifecycle VM is created.

Appliance VMX files set disk.EnableUUID = "TRUE" so Photon exposes stable /dev/disk/by-id identities. ESX Storage blank-disk claims depend on those identities and reject transient /dev/sdX names.

Same-interface gateway acceptance

The focused Python lifecycle mode --same-interface-gateway-only verifies two Atlaso appliances on the same existing Workstation NAT network. Create both through create-atlaso-test-vm.ps1 with the open PR number, separate purposes forwarding-router and forwarding-client, the same confirmed -ManagementNetwork, -SkipLabNetworkAdapters, and -SkipNetworkPrepare. Preserve each original receipt and digest outside the VM roots. Do not change the shared VMnet subnet or adopt a retained VM. A provenance-admitted powered-off appliance template remains a read-only source; deploy the clean PR wheel and helper to each exact owned clone through deploy-wheel.ps1.

Before running the consumer, prove both receipt bindings, each active eth0 NAT attachment, current Tools address ownership, and the guest-published Ed25519 fingerprints. Read the actual VMware NAT gateway instead of assuming a production address. Pass those values through --appliance-ssh-host, --appliance-ssh-hostkey, --gateway-client-host, --gateway-client-hostkey, --gateway-upstream, and --gateway-expected-commit to scripts/interop/lifecycle_test.py. Use --secret-stdin with the existing bounded protected lifecycle credential handoff; never pass a password on the command line. Keep --result-dir under an originally receipted task output root. --plan-only emits the non-secret plan without connecting to either appliance. The ordinary multi-network -RoutingWanOnly wrapper does not select this two-Atlaso mode.

The consumer uses authenticated browser editors and global Appliance Apply, with no API-token creation. It converts both fresh clones to verified VMware Tools clock authority through the NTP settings and global Apply, so the client negative control does not depend on internet time sources or leave competing clock controllers active. It then converts both owned NAT interfaces to static Access with Management UI, retains A's actual upstream default, and sets B's default to A. It completes the protected management handoff with Routing off, then enables A's Routing in a separate WAN/Firewall Apply. Inert inherited adapters must start down and unaddressed; their desired state is kept unused and down. It explicitly disables accepting ICMP redirects on B and flushes learned route state, then checks effective routes before each probe. Packet evidence requires a client-to-A Ethernet frame and the matching A-to-gateway frame with the same IPv4/TCP tuple and a decremented TTL. The negative control disables the permission and requires client ingress without router egress while A still has external IPv4 connectivity. It restores the permission, checks DNS separately, and repeats forwarding after rebooting both appliances. It checks deployed source hashes, lab-table selection, forwarding sysctls, default-drop Firewall state, and absence of masquerade. Failed steps retain actionable native evidence; unit tests and a plan do not establish deployed success. Release both exact clones through their receipt-bound normal test-VM removal procedure after terminal task activity.

Topology

The default lifecycle lab creates isolated VM directories and result/log artifacts under its canonical identity:

test-results/vmware-workstation-lifecycle/Atlaso-PR-<number>-lifecycle-<collision-safe-suffix>/vms

The appliance is cloned through create-atlaso-vm.ps1 from the verified, powered-off two-payload-disk template. Custom -VmrunPath installations also reuse the disk manager discovered beside that executable. Before first boot, the shared clone contract creates private 500 GiB thin depot and backup disks inside the lab's appliance directory and attaches them at SCSI units 2 and 3. The source template stays unchanged, and no persistent data disks from another appliance are reused. Failed disk provisioning retains the clone identity for supported cleanup. If helper or wheel deployment fails, the runner reports bounded state for data-disk initialization, HTTPS bootstrap, Atlaso, and nginx, or explicitly reports that guest diagnostics were unavailable. Retained startup diagnostics contain only validated unit names and state fields; failed, invalid, or oversized readbacks are removed. Raw readbacks use the task checkout's .atlaso-local/lifecycle-startup-diagnostics/<lab-name>/guest-readback.txt, outside retained results. Only sanitized bytes are atomically published to the final diagnostic artifact. After a provider timeout, the shared Windows job runner terminates and verifies every descendant has exited before staging cleanup. It also proves the job empty after normal parent exit and suppresses untrusted provider console output. Unproven termination fails closed and blocks final lab and client-seed provider cleanup, even with -CleanupCreatedLab, preserving the appliance and exact staging path for recovery. After a host/process interruption, preserve that exact staging identity, verify the owning lifecycle and provider processes have exited, and remove its readback before sharing evidence or completing task cleanup. Do not archive raw staging. Client VMs use an Alpine cloud VMDK prepared from a pinned upstream QCOW2 source. The payload and SHA-512 metadata are cached only as a verified pair: corrupt entries are removed on an ordinary rerun, downloads stay in unique partial files until validation succeeds, and promotion is scoped to the exact expected cache files. The default Alpine artifact uses the versioned v3.24 release URL and a repository-pinned SHA-512 digest; custom images must pass their own -ExpectedSha512 pin. Preparation expands the powered-off client VMDK to at least 2 GiB with VMware Virtual Disk Manager; cloud-init grows its root partition and filesystem before installing lifecycle probe packages.

Default vmnets:

  • VMnet8 for management, with the appliance address assigned by DHCP by default
  • VMnet2 for SiteA
  • VMnet3 for WAN/SiteB
  • VMnet4 for trunk-like validation

Check the current Workstation host network inventory with:

pwsh -ExecutionPolicy Bypass `
  -File scripts/windows/vmware/prepare-networks.ps1 `
  -PlanOnly

If vmnets are missing, adjust them in VMware Virtual Network Editor before running the interop test. The image build wrapper reads the selected management vmnet before rendering Packer variables; for bridged vmnet0, it uses the active Windows IPv4 interface or the interface named by -BridgedInterfaceAlias.

Unless overridden, the build wrapper chooses .30 in the selected management subnet for temporary builder SSH, then leaves final appliance management on DHCP and discovers the runtime address through VMware Tools.

Isolated routing-overlap fixture contracts

Certificate handoff preparation requires independent proof that the candidate management address belongs exclusively to the appliance; the certificate peer procedure describes the current producer limitation. A MAC-derived address and task-owned VM receipts alone do not provide this proof. The certificate address-control procedure defines a maintainer-collected acceptance path for a lab; its evidence does not satisfy native receipt admission or enable the credentialed wrappers.

The opt-in -RoutingOverlapOnly mode first applies the MAC-derived management IPv6 address as static state, then requires native assignment and a CA-validated served certificate containing that address before switching to SLAAC. The preparation address is not counted as SLAAC; native acquisition must later prove the same address as dynamic and usable before overlap checks continue. A known preparation failure restores the original IPv4-only management and unused-lab baseline through the ordinary audited Apply. An unknown Apply outcome or incomplete restoration preserves the fixture for recovery. The mode then exercises DHCP/SLAAC overlap through the canonical lifecycle wrapper. -RoutingWanOnly retains its existing static routing scenario. It enables the global Routing/WAN settings before the first Apply and host forwarding checks. The Access routing permission is added only after the fixture proves that Access traffic is blocked without that permission. Focused unit tests do not replace native acceptance for automatic IPv6 acquisition, lease expiry, rollback, or reboot. The native source-rule proof also requires the persistent, dual-family local-origin terminal guard at priority 6004 after Apply and after address renewal; otherwise a newly acquired DHCP or SLAAC source could use the main table before its exact lookup/unreachable pair is installed. Priorities 6000–6002 allow only unspecified, link-local, and loopback sources into the main table. Priority 6003 sends link-local destinations to main for unbound source selection. The guard starts before networkd at boot and remains active through ordinary Network Apply. It is retired only after restoring a pre-routing-domain state or factory reset. During first-Apply migration, every still-live source keeps the domain of its installed networkd policy, including an Access link being reclassified as Management. The candidate role takes effect after Network activation; missing or ambiguous installed ownership refuses migration before source selectors are retired. Use a prepared client disk with -SkipClientPrepare -ClientVmdkPath <owned-client-disk> and -ApplianceSshUser root; the existing management VMnet is used only for control access to the two clients. Do not supply an appliance IP/URL override or dry-run Apply. Original external ownership must be enabled; human runs opt in with -OwnershipRoot and a UUID -OwnershipTaskId. Pass distinct protected -RootPassword and -AdminPassword identities from the supported credential provider. Select an owned prepared Python environment with the repository development dependencies available under python -I -B; user-site-only packages are intentionally excluded and fail preflight before resource creation.

The topology admission contract requires independent task/PR/source identity, original creation-receipt digests for two distinct task-owned LAN segments, complete enabled VMX adapter observations, and exact guest MAC/interface matches. The appliance has only private management and lab links. Both client guests retain a separate existing control VMnet; test prefixes must not overlap its observed prefixes. Shared segment registrations without creation receipts, extra adapters, ambiguous interfaces, and mismatched receipt owners are refused before configuration.

Server configuration is bound only to the admitted private management interface. DHCP is restricted to the appliance MAC and reservation; RA advertises a short-lived private IPv6 prefix. The fixture changes no Windows networking and configures no NAT. To advertise a nonzero IPv6 router lifetime, client A temporarily enables global IPv6 forwarding only after installing its own IPv6 forward-drop guard. It captures and restores all/default/per-interface forwarding and RA settings, keeps forwarding disabled on each admitted NIC, and refuses NICs with LRO enabled. The disposable Alpine client seed installs a fixture-only OpenRC local hook that disables LRO on its two VMXNET3 NICs at every boot. First boot runs the hook explicitly, so failure blocks setup. The controller still verifies the live state before changing forwarding. The original guard handles are removed only after restoration readback. The transport uses in-process, pinned SSH channels to only the private appliance's SSH and HTTPS ports, opens no host listener, and verifies HTTPS with the explicit appliance CA and private target identity. It does not follow redirects to another origin or load ambient SSH keys. The Alpine control-client seed admits only local SSH forwarding to 192.0.2.10:22 and 192.0.2.10:443, and checks the effective sshd policy before the fixture starts; all other forwarding destinations remain refused. The private lifecycle waits for first-boot HTTPS to publish a nonempty CA before pinning its trust observation. If the owned fixture's first-boot console reports the known transient DHCP activation review while eth0 already holds its reserved 192.0.2.10 address, the lifecycle submits the unchanged DHCP/disabled-IPv6 settings through the supported console correction path once. An unexpected review or a CA still absent after the bounded wait fails instead of probing with null trust.

The wrapper publishes pinned provider readback and public guest NIC/SSH identities before bootstrap. The private clients have no DHCP or RA client on their fixture NIC. Their controller receipts bind original directory, link, process, and firewall identities before use. HTTPS verifies the provider-observed appliance CA and private address; no host port forward or insecure legacy HTTP client is used. The scenario records native acquired addresses, source rules, route selection, DHCP and RA expiry, and ordinary Apply restoration. It also verifies a same-address static-to-DHCP transition while the original server lease remains unexpired, requiring the measured appliance helper's independent current client-lease proof, exact interface identity, and source rules. A static-classified address alone or a server reservation alone cannot satisfy this phase. The private peer serves fixture-only DNS on its isolated interface; the scenario stages that resolver for the static phase, then restores the original DNS settings when DHCP resumes and during baseline recovery. An uncertain Apply outcome preserves the running fixture and public job identity for reconciliation before any further cleanup. Failed temporary token revocation cannot replace that unknown-outcome signal or authorize stopping the fixture. Negative DAD Apply and reboot are separate acceptance cases and are not claimed by this mode. Do not launch ad hoc DHCP/RA servers on shared VMnets or mark native acceptance passed from unit-test results alone.

Agent lifecycle creation uses the originating CODEX_THREAD_ID and independently resolves the active desktop.git-worktree-root before creating the result directory. Human CLI runs keep the existing canonical identity behavior without new required arguments; they can opt into external creation records using -OwnershipRoot and an optional -OwnershipTaskId. The explicit root must contain the source checkout and remain outside its removal scopes. When enabled, creation publishes each original Windows filesystem identity with task, repository, source commit, and PR bindings in a durable sibling manifest under that configured root. The empty result root is recorded before source extraction; each clone is recorded immediately after the provider creates it and before configuration; client directories are recorded before their first artifact is copied. vmware-identity.json references these external manifests and their hashes. Existing retained labs are never adopted or given retrospective creation records. VM-only cleanup retains the result root; the existing artifact-root cleanup procedure still applies after VM cleanup and evidence preservation.

Before uploading a replacement helper or installing the lifecycle wheel, the wrapper collects source-image-network.json through bounded VMware guest operations using the root credential established by that run's first-boot environment. The inspector reads only the installed package version, routing service state, and validated public routing intent; it does not change networking or import appliance application code. Missing source routing support remains explicit evidence. A target without the canonical routing-domain handler refuses a source with retained intent or a loaded/active routing service before either deployment step. This inspection is distinct from postdeployment runtime acceptance.

After deployment, deployed-runtime-identity.json binds the original VM manifest hash and admitted source commit to measured wheel/helper digests, the target interface/MAC, and matching expected/installed payload digests. The guest inspector compares the uploaded wheel's payload with installed files and rejects stale extra Atlaso files; the host checks wheel and helper hashes against the immutable build inputs. This proves the inspected on-disk runtime bytes, not continuous execution or transient publication behavior.

Same-address acceptance

For #837, select -RoutingOverlapOnly -SameAddressHandoffOnly on the canonical invoke-lifecycle-test.ps1 wrapper, with the prepared appliance/client source artifacts, protected root/admin identities, -SkipClientPrepare, and -ApplianceSshUser root required by the private fixture above. The root identity is used for VMware guest operations. Native appliance observations authenticate the configured administrator over the separately pinned SSH connection, then use a bounded private su exchange with the distinct root credential. The exchange checks terminal echo before sending that credential over encrypted stdin and requires effective root identity before executing the read-only observation. Focused first boot keeps root SSH disabled even when the VMware guest-operations identity is root; the fixture does not change SSH policy or install a passwordless-sudo rule. Before initial baseline Apply, the isolated fresh clone selects the positively available VMware Tools clock source through the supported NTP form, preserving other NTP settings and keeping server mode disabled. Global Apply must verify that clock authority and synchronization; the fixture cannot depend on external upstream NTP reachability. The mode keeps management IPv6 disabled and runs only the IPv4 static-to-DHCP handoff at 192.0.2.10. It records the live server lease immediately before DHCP activation and requires the installed helper's independent current client-lease proof, exact native interface/address identity, source routing rules, successful global Apply, and ordinary baseline restoration. It does not claim SLAAC, lease-expiry, overlap, negative-DAD, or reboot acceptance. Retained management listeners still require certificate IP SAN coverage. The helper feeds the public certificate to OpenSSL's default stdin; Photon OpenSSL treats an explicit -in - as a literal filename.

This opt-in mode uses the fixture-stable-observation-v1 contract. Other VMware VMs may run on the host; no exclusive-host reservation or complete Windows process census is required. The provider inventory must contain every enrolled VMX path before and after configuration reads. Changes to unrelated VMs do not invalidate fixture admission. Preserve unrelated VMs and coordinate use of any shared source VM with its owner.

The producer pins the provider executables and original VMX files. The focused wrapper retains saved VMX read pins continuously from descriptor creation through successful controller stop. These pins deny saved adapter edits and file replacement between phases; they are released before the owned seed retirement and reboot, or on bounded failure exit. Repeated runtime configuration reads must positively match every enrolled MAC and exact PVN/VMnet identifier across all ten adapter slots; blank required fields, extra adapters, or differing bracketed observations refuse admission. startConnected is configuration, not a claim about current carrier. Pinned SSH reads independently inspect every non-loopback guest interface, including down interfaces, their IPv4/IPv6 addresses, and bridge membership. Active management/client links must have native carrier; additional network namespaces or competing candidate-address claims refuse. Client controllers recheck their original forwarding guard and DHCP-server state. Fresh fixture and guest checks precede static and DHCP Apply and baseline restoration. A nonretryable focused phase failure retains the fixture before seed or VM cleanup. Focused scenario failure receipts record preserve_fixture: true and return preservation exit status 3; ownership loss also blocks a competing restoration Apply. Transient HTTPS readiness failures may retry only after a fresh fixture check succeeds; ownership or trust refusals remain failures. Noncandidate peer server addresses may intentionally overlap on the admitted isolated segments.

These observations cover the enrolled fixture and its admitted guests. Serial reads are not an atomic switch-port inventory and cannot detect every change followed by a revert or prove the absence of an unrelated VM on the same segment. Receipts report those limits and do not claim host-wide or continuous exclusive membership. The existing producer for different-address certificate scenarios retains its stricter refusal; this mode does not admit those scenarios or relax their certificate-publication guard. Unit tests do not establish native same-address success. Keep #837 open until a PR-bound native run completes and its measured runtime, handoff, lease, and restoration evidence are reviewed.

Appliance Update status and ordering acceptance

For the 0.9.220 to 0.9.223 updater transition, use a brand-new normal test VM with a unique name and destination. Run the creation wrapper's ownership, source-provenance, VMX collision, data-disk collision, running-VM, and pending cleanup marker checks before mutation; do not reuse or redeploy the canonical test VM for this acceptance. Confirm the source appliance reports exactly 0.9.220 before publishing the isolated signed candidate fixture for 0.9.223.

Capture the parent task ID, all child IDs/positions, transaction ID hash (never the raw source credentials), created, started, child-transition, restart, recovery, and finished timestamps. Probe continuously from three vantage points:

  • guest-local Atlaso /openapi.json on the internal listener;
  • guest-local nginx /, /ui/management, /ui/public, and /openapi.json on every applied IPv4/IPv6 browser listener;
  • host-facing /, canonical management/public UI paths, and /openapi.json on the discovered VM address.

Before mutation, ordinary browser routes are HTTP 200 or the expected authentication redirect. From status admission through every Photon, PowerShell, Atlaso, web/worker/console restart, and release-finalizer boundary, browser routes are HTTP 503 with Retry-After: 3, Cache-Control: no-store, and X-Atlaso-Update-Mode: active. No sample may be HTTP 502. Stable protocol paths remain unchanged and may return the controlled maintenance 503 while their upstream is down. After definitive success or verified rollback, ordinary browser behavior returns and three consecutive host-facing samples contain no 500/502/503/504 response.

Verify the child execution sequence is Photon OS, PowerShell Modules, Atlaso Release for the all-stream selection and the same filtered order for every non-empty subset. Inject one failure before each child apply, after each child terminal commit, before/after status-marker removal, before/after nginx validation/reload, before release link switch, during worker handoff, after activation_committed, and before the restoration receipt. Later children must skip after an install failure, earlier terminal results must remain unchanged, and every parent must become terminal. Reboot once during a nonterminal child and once after a terminal parent with restoration pending. The same parent/child identity must reappear, mutation must remain blocked until recovery, and no task may remain running indefinitely.

Successful release acceptance requires /opt/atlaso/current, the compatibility virtualenv, signed release receipt, candidate manifest/bundle/commit hashes, /etc/atlaso/update-info, definitive finalizer, cleared restart gate, worker startup identity, active services, internal OpenAPI, nginx-local OpenAPI, and host-facing OpenAPI to agree on 0.9.223. Rollback acceptance requires the corresponding definitive rolled_back=true evidence and restored 0.9.220 identity. After each outcome, perform an audited appliance reboot and repeat the task-finality, status-marker absence, service, receipt/finalizer/update-info, and three-vantage probe checks. Preserve sanitized probe tables and screenshots of the desktop and responsive update-only page; redact sessions, credentials, keys, source credentials, and raw helper output.

Build The Appliance

Build the Workstation appliance with:

pwsh -ExecutionPolicy Bypass `
  -File scripts/windows/vmware/build-photon-image.ps1 `
  -PullRequestNumber <number> `
  -IsoUrl "<photon-iso-url-or-path>" `
  -IsoChecksum "<packer-checksum>" `
  -EnableRealSystemAdapters

The task build begins only after that pull request exists. Its same-repository head branch and commit must exactly match the checkout. The wrapper derives Atlaso-PR-<number>-Photon-Builder-VMware[-<collision-safe-suffix>] and keeps that identity aligned across the Packer VM, output/VMX, builder-address reservation, diagnostics, ownership manifest, schema-v3 provenance, cleanup, and evidence. Use -CollisionSuffix for another builder owned by the same pull request; never reuse the legacy generic builder for lifecycle acceptance.

Before a pull request exists, developers may build the source image with -LocalBuilder instead of -PullRequestNumber. The clean checked-out commit derives Atlaso-Local-<12-character-source-commit>-Photon-Builder-VMware[-<collision-safe-suffix>], while all snapshot, ownership, cleanup, reservation, and provenance checks remain active. This is a local/test artifact: protected release paths reject its builder_identity.kind of local, and lifecycle acceptance evidence must still come from the PR-numbered builder and test VMs after the pull request exists.

For a previously downloaded ISO, pass either its local filesystem path or an empty-authority file:/// URI. The wrapper verifies that exact file against -IsoChecksum before any remastering and does not route it through the HTTP download path. Host-authority file URIs are rejected; use an explicitly mounted local path instead.

The image wrapper uses the same exact Atlaso Environment selector contract as normal test-VM creation: an explicit -OnePasswordEnvironmentId takes precedence over the checkout-local, Git-ignored .atlaso-local/onepassword-environment-id, and -EnvironmentIdFile selects a different single-line file. When -SshPassword or -BootstrapAdminPassword is omitted, the bounded Windows SDK bridge retrieves only that value's unique concealed DEFAULT_ROOT_PASSWORD or DEFAULT_ADMIN_PASSWORD. Explicit SecureString parameters remain independently authoritative. With no explicit selectors, the bridge prefers the checkout-local current-user DPAPI service-account token before single-account desktop discovery and uses standard GIL-enabled Windows x64 CPython 3.14 registered with the Windows launcher, including current Python Install Manager bracketed architecture selectors. Legacy launcher and vendor-tagged registrations remain compatible; known x86 and unsupported or malformed entries remain ineligible. Zero or multiple accounts and a missing compatible runtime fail closed; -OnePasswordAccount and -OnePasswordPython remain explicit overrides. The bridge returns only current-user DPAPI ciphertext and removes its temporary files before the wrapper performs network discovery or preparation, output cleanup, ISO remastering, Packer initialization, or other image mutation. It rejects caller DEFAULT_* variables, local .env inputs, ambiguous or non-concealed variables, invalid exact bytes, unavailable authorization, and timeouts without printing the Environment ID or values. The parent passes only DPAPI ciphertext to a separately bounded image-build child; only that child unwraps values for kickstart and Packer serialization. Every plaintext kickstart, remastered ISO, and Packer variable file is placed under the same exact task-owned temporary root. The parent creates the child suspended, assigns it to a Windows process job, and resumes it only after the job owns every future Packer or plugin descendant. Both ordinary child exit and deadline termination require job accounting to prove zero active processes. A proven deadline also applies checked exact-output cleanup when -PackerOnError cleanup owns replacement output. -KeepExistingOutput preserves a root that existed before the child started. An ordinary replacement claims that pre-existing root durably only after network preparation and immediately before checked removal; until then, an outer timeout preserves it. A partial root created by this invocation remains cleanup-owned. The parent then removes the sensitive root and verifies its absence before returning. -ImageBuildTimeoutSeconds selects the deadline and defaults to six hours. If whole-tree termination itself cannot be proven, the wrapper retains the root and a non-secret .atlaso-local/photon-image-build-state/photon-image-build-cleanup.json ownership marker and blocks further work. The marker records the prior controller and the suspended child's unique named job plus PID/start-time identities before the child resumes. Rerun the wrapper on the same boot: it proceeds only after the prior controller is absent, terminates only the exact matching job, proves the child and all descendants gone, and then removes and verifies the pinned root before retiring the marker. It can also reconcile a terminal interruption whose exact job and child are already absent. PID reuse, an active or replaced owner, a surviving descendant after the recorded root exits, incomplete termination, and path/reparse/filesystem-identity drift preserve the marker and artifacts. Restart Windows for those cases and for legacy markers that lack process ownership evidence. Ordinary completion also requires the reloaded marker root to equal the in-memory task-created root before recursive removal. The shared SDK credential bridge follows the same recovery ownership. Each non-secret marker is flushed with write-through semantics and atomically renamed before a plaintext child starts. Cleanup flushes the sensitive root parent's directory metadata on the root's own volume before it durably records root-absent and retired phases and deletes the marker. A resurrected post-crash marker remains non-actionable while still forcing another exact-root absence check.

The wrapper uses the shared Photon ISO remastering, kickstart rendering, checksum validation, and Packer var-file generation. image/common/source holds the original Photon ISO download cache. The canonical image installs open-vm-tools and stages the locked offline RPM closures used by the provider-neutral first-boot guest-agent selector. The Workstation build wrapper opens a visible VMware console by default. Use -Headless only when an unattended build is preferred. The shared builder removes and verifies the absence of its plaintext kickstart source, generated Packer variable file, and remastered credential-bearing ISO after the bounded Packer consumer exits. The cleanup covers validation, build, failure, fallback ISO paths, and forced whole-child timeout through the parent-owned root; an ACL, file-lock, or endpoint-protection cleanup failure terminates the build instead of reporting success with a credential-bearing artifact left behind. -PrepareIsoOnly is rejected because retaining that ISO would retain a reusable build credential.

GUI builds repair exact missing Atlaso registrations inside the configured output scope before Workstation is started, while the existing full output cleanup remains after network preflight. They then start or reuse a responsive VMware Workstation UI as a process separate from Packer before invoking the VMware builder. An already-open UI still blocks required stale-registration repair with the exact close-the-UI diagnostic. The parent starts the exact resolved vmware.exe before creating the bounded image-build child, then the child verifies that UI remains responsive immediately before Packer starts. This preserves the visible console while keeping the sensitive job non-breakaway; Packer, plugins, and their VM consumers cannot leave that job. Until SSH provisioning begins, the wrapper reports sanitized, exact-VMX startup heartbeats and applies a 45-minute default timeout matching Packer's SSH communicator allowance. The interval begins at monitored Packer process start, including pre-VMX and pre-power-on failures. Use -PackerStartupTimeoutSeconds to select a different bounded start-to-provisioning interval and -PackerHeartbeatSeconds to adjust diagnostic frequency. An explicit -SshHost becomes the TCP/22 diagnostic target so the probe matches Packer's communicator endpoint. A heartbeat distinguishes output identity, provider inventory, exact running state, TCP/22 reachability, Workstation handoff, and SSH authentication; it never reads or reports VMX contents or connection credentials. With -PackerOnError cleanup, Packer retains the exact failed VMX and the wrapper routes ordinary nonzero exits, monitored startup timeouts, monitor interruptions, and the outer whole-image deadline through the same checked exact-root cleanup as an ordinary replacement build, but only after the outer Windows job proves zero Packer, plugin, and provider processes remain. This preserves provider identity until Atlaso quiesces and reconciles the exact Workstation VM. The builder-address reservation is released only after process-tree termination, provider inactivity, and artifact removal are proven; a checked cleanup failure retains the remaining artifacts and reservation with both failure diagnostics. -KeepExistingOutput preserves a root that predated the build, but not a new partial root created by this invocation; other failure selections preserve output when the wrapper cannot claim replacement ownership.

For Workstation, Photon installation is bound to VMware SCSI identity 0:0:0 through kickstart preinstall discovery, not /dev/sda enumeration. Provisioning then proves the complete root dependency chain reaches that disk and proves the blank ATLASO_SYSTEM target is the exact 20 GiB disk at 0:1:0 before formatting it. The completed VMX receives schema-v3 role-bound provenance that binds the admitted clean commit, deterministic immutable source-snapshot inventory, and matching builder identity; normal test-VM cloning and OVF export both verify it before accepting the source. A reversed, ambiguous, capacity-mismatched, legacy-unproven, or byte-modified payload must be rebuilt and is never repaired by formatting or silently swapping unrelated VMDKs.

Single-Command Run

pwsh -ExecutionPolicy Bypass `
  -File scripts/windows/vmware/invoke-lifecycle-test.ps1 `
  -PullRequestNumber <number>

The wrapper has no password defaults. It prompts securely for the appliance administrator and VCF Backup credentials; client SSH reuses the administrator SecureString unless -SshPassword is supplied. -FullEsxiPxeInstall also requires the ESXi root password that matches the selected rendered Kickstart profile. The launcher sends these values to its child through a current-user DPAPI-protected temporary CLIXML bundle and removes that bundle after the child exits. The wrapper stages that bundle only in a dedicated checkout-local directory with protected current-user-and-SYSTEM ACLs. The ordinary .atlaso-local parent may retain the initializer's inherited ACL when no untrusted principal can mutate its children; an existing unsafe bundle directory is refused without automatic permission changes. The runner streams the complete password set for the main lifecycle Python consumer as one JSON envelope over standard input, so those values do not enter that child's process arguments. Client NoCloud seed generation likewise sends its SSH password as one bounded standard-input line to the repository-controlled helper; the helper rejects empty, multiline, and oversized input before replacing an existing seed artifact. After successful lifecycle client access proves that cloud-init consumed each seed, the runner stops the clients, detaches and deletes both ISOs with absence verification, then restarts a retained lab. Failure cleanup also stops affected clients and requires verified seed absence. The isolated routing-overlap clients install a fixture-only cloud-init datasource list containing NoCloud and None. Once their credential-bearing seeds are detached, this prevents later boots from waiting for unrelated network metadata services before SSH and VMware Tools become available. The clients also install Alpine's VIX guest-operations plugin; the base VMware Tools daemon alone cannot satisfy the runner's authenticated vmrun inventory and controller operations. Provisioning readiness requires completed cloud-init with no errors. After seed removal, the fixture accepts only cloud-init's known DataSourceNone fallback warning; other recoverable warnings still refuse the run. The fixture suppresses optional SSH-key console output so an Alpine cloud-init image missing that console helper does not report a provisioning warning for an otherwise completed first boot. For a full ESXi PXE install, the consumer rotates the encrypted Lifecycle ESXi vault entry and persists only {{vault.lifecycle_esxi.esx.lifecycle.root.password}} in the Kickstart source; Atlaso resolves that marker for the authorized PXE request without storing the plaintext password in desired state. The lifecycle reuses a vault whose display name normalizes to lifecycle_esxi and fails closed if more than one vault claims that marker name. Because settings archives intentionally exclude vaults, the restored pass recreates this entry from the same standard-input secret after restore and before the ESXi PXE unit is applied. The -OidcOnly and -RoutingWanOnly paths do not prompt for or include a VCF Backup password because those focused scenarios neither stage VCF Backup nor run the settings backup/restore pass.

The wrapper selects the newest appliance VMX under image/vmware-workstation/output, prepares the tiny Alpine client VMDK when needed, creates a unique Atlaso-PR-<number>-lifecycle-<collision-safe-suffix> lab, runs the initial lifecycle scenario, and by default runs the restored backup/restore pass. Pass -SkipBackupRestoreTest only when the older single-pass behavior is intended. Unless -ApplianceIPAddress is passed, the wrapper waits for VMware Tools to report the appliance's DHCP management IPv4 address and derives the appliance URL from that discovered address.

Tiny-client preparation exits nonzero and emits no prepared-image JSON when qemu-img conversion or inspection fails. If the current run created an incomplete or unverified VMDK, the wrapper removes that exact output before returning the failure so a later lifecycle run cannot accept it as prepared.

Before it powers on the raw appliance clone, the runner injects the complete Atlaso first-boot OVF environment that a normal ESXi or Workstation OVF deployment would provide. The lifecycle lab uses IPv4 DHCP with blank DNS overrides, disabled IPv6, a generated lab FQDN, and the existing lifecycle admin-password input for both required first-boot credential properties. Each injection also carries a generated non-secret deployment identifier so an interrupted source VM's pending state cannot be mistaken for the new clone's attempt. The value is stored only in the clone's guestinfo-backed VMX setting and is excluded from plan and result artifacts. Invalid identity or password inputs fail before the runner creates the lab directories. Root SSH remains disabled for the default admin appliance SSH user; selecting -ApplianceSshUser root explicitly enables it in the same first-boot document. After customization, appliance guest operations use that applied admin password while -SshPassword remains dedicated to the client VMs, so callers may continue to supply different appliance and client credentials.

For focused deployed OIDC acceptance independent of the full service-network topology, pass -OidcOnly. The wrapper clones the selected appliance, connects one site adapter, installs the exact branch wheel, and proves appliance readiness. The runner addresses that access listener, applies its network state, configures the CA, enables the provider through its managed-certificate settings flow, and applies the CA and public listener before the OIDC Authorization Code check. The final apply also refreshes the provider's firewall admission, and the check connects to the site address with hostname and certificate verification against the downloaded CA root. It skips unrelated service configuration, client VM creation and probes, and the backup/restore pass. Because this verified probe runs on the Windows host, -OidcOnly requires a host-reachable Site A vmnet; an isolated lan:<name> Site A segment is rejected before lab creation. It also requires -SiteInterface eth1, because the focused runner attaches Site A to the appliance's second adapter. Both entry points check that -SiteCidr matches the selected vmnet's discovered IPv4 subnet and that an active host adapter has an address on that subnet before prompting for credentials or creating a lab. If the local VMnet2 subnet differs from the default 192.168.12.1/24, pass a matching site CIDR with an unused host address. For bridged VMnet0, the wrapper forwards -BridgedInterfaceAlias to the direct runner so both checks use the selected host interface; runtime discovery executes the network script from the admitted source commit.

For certificate handoff preparation, pass -CertificateOnly -KeepVms with a task-owned PR identity. This mode clones and boots only the appliance, installs the admitted source commit's helper and wheel, and leaves the appliance running for a separate native certificate scenario. The runner publishes immutable creation intent and original VM directory identity under test-results/certificate-native-evidence/<lab> before VMware populates that directory. It also records a bounded atlaso.service readback and runs a read-only SQLite source probe before deployment. The probe emits only aggregate counts and feature flags from one transaction, requiring absent routing service intent, disabled routing and WAN features, and no saved route, rule, NAT, port-forward, WAN-policy, or route-role interface rows. The runner admits and publishes that source receipt only when every required field proves absence. The actual helper and wheel digests are measured afterward. A discovered DHCP address does not prove exclusive address control. The certificate scenario must refuse mutation until a controlled network producer proves the candidate static address or DHCP reservation belongs to the clone. For a dedicated DHCP peer, add -CertificateDhcpPeer -SiteANetwork lan:<task-owned-name> and explicitly provide a provenance-admitted, prepared -ClientVmdkPath and -CertificatePeerPublicKeyPath for an existing Ed25519 public key whose private half is already loaded in the local SSH agent. The runner verifies the exact agent key before creating the peer seed. This mode does not auto-create the source client disk. The optional -CertificatePeerCidr and -CertificateLeaseAddress select a small private IPv4 subnet and one lease. This mode boots the appliance on the selected management VMnet, pins the management eth0 MAC, and then soft-stops and rewires that same adapter to the task-owned private LAN. The two-adapter Alpine peer keeps its eth0 on the management VMnet for control; its eth1 serves DHCP on the private LAN. The peer's NoCloud seed contains only the public key for SSH access, disables password login, binds dnsmasq only to its private eth1, and reserves the appliance eth0 MAC. The runner uses agent-backed SSH for boot and identity readback, pins the observed SSH host key across the seed-removal restart, and never sends a peer password into the guest. The copied disk remains writable while VMware runs; its preboot digest is not a lasting byte-integrity claim. Separate original-intent and original-directory-identity receipts precede peer VM creation, and peer-fixture.json records its source-disk digest, addresses, MAC, and network binding. A management-rewire intent receipt is written before the power-off edit, and a separate rewired-runtime receipt records the preserved MAC and the appliance's reserved DHCP address afterward. Both runtime receipts deliberately say unproven: VM creation and a guest address alone do not establish exclusive control. The peer fixture is preparatory: the current certificate_peer_proof.py producer deliberately refuses even --preflight-only before credentials because VMware tooling cannot prove exclusive current attachment to the private LAN or exclude another claimant for the candidate address. Native certificate acceptance remains blocked until a supported producer independently proves those properties. Once available, the tracked inspect-certificate-peer.ps1 wrapper can admit original creation and LAN receipts, check VMX adapter bindings, and use pinned SSH connections to read the peer's dnsmasq configuration, active lease, appliance eth0 address and route, and CA-pinned HTTPS through a private peer tunnel. The producer also requires an explicit, already configured static management baseline; it does not configure that baseline. The tracked inspect-certificate-handoff.ps1 wrapper then rechecks controlled receipts and the live private baseline before the native scenario changes the management address and restores the original interface and certificate state. Each handoff invocation keeps its own preflight admission evidence under the task result root, so a failed later stage does not consume the next attempt's admission destination. Load either credentialed wrapper from the reviewed full PR-head Git blob into an in-memory PowerShell script block; direct execution of the mutable checkout file is refused. The caller must pin $reviewedCommit to the full head already verified on the PR and use one of the two literal paths below. For example, load the read-only peer proof before obtaining the appliance credential through the bounded bridge:

$repoRoot = 'E:\.codex\worktree\issue-865-dynamic-certificate'
$reviewedCommit = '<verified full PR head SHA>'
$relative = 'scripts/windows/vmware/inspect-certificate-peer.ps1'
$gitRoot = Join-Path ([Environment]::GetFolderPath([Environment+SpecialFolder]::ProgramFiles)) 'Git'
$trustedGit = Join-Path $gitRoot 'cmd/git.exe'
$gitCore = Join-Path $gitRoot 'mingw64/bin/git.exe'
$gitPins = [Collections.Generic.List[IDisposable]]::new()
try {
    foreach ($path in @($gitRoot, (Join-Path $gitRoot 'cmd'), (Join-Path $gitRoot 'mingw64'),
            (Join-Path $gitRoot 'mingw64/bin'), $trustedGit, $gitCore)) {
        $item = Get-Item -LiteralPath $path -Force -ErrorAction Stop
        if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Trusted Git path is redirected.' }
    }
    foreach ($path in @($trustedGit, $gitCore)) {
        $gitPins.Add([IO.FileStream]::new($path, [IO.FileMode]::Open,
                [IO.FileAccess]::Read, [IO.FileShare]::Read))
        $signature = Get-AuthenticodeSignature -FilePath $path
        if ($signature.Status -ne 'Valid' -or
            $signature.SignerCertificate.Thumbprint -cne '3EB14A3AEF84B7153E139397F0A49E2FAC662B0E') {
            throw 'Trusted Git signature is unavailable or unexpected.'
        }
    }
    $head = (& $trustedGit -C $repoRoot rev-parse --verify 'HEAD^{commit}').Trim()
    if ($LASTEXITCODE -ne 0 -or $head -cne $reviewedCommit) { throw 'Reviewed certificate head changed.' }
    $source = (& $trustedGit -C $repoRoot show "${reviewedCommit}:$relative" | Out-String)
    if ($LASTEXITCODE -ne 0 -or -not $source) { throw 'Reviewed certificate entrypoint unavailable.' }
    $entrypoint = [ScriptBlock]::Create($source)
    & $entrypoint -RepositoryRoot $repoRoot -ReviewedSourceCommit $reviewedCommit `
        -Plan $plan -AddressEvidence $addressEvidence -RuntimeEvidence $runtimeEvidence `
        -EnvironmentId $environmentId -PythonPath $pythonPath
} finally {
    foreach ($pin in $gitPins) { $pin.Dispose() }
}

For the handoff, select the literal scripts/windows/vmware/inspect-certificate-handoff.ps1 blob and pass -Plan, -Evidence, -EnvironmentId, and -PythonPath (plus -Execute only for the guarded native mutation). Do not obtain the appliance credential until after selecting and loading the reviewed entrypoint. For both credentialed wrappers, install the task-owned virtual environment from the hash-locked requirements-onepassword-deploy.lock. They pin the interpreter, base runtime, and isolated import files through child termination and refuse an external import path or startup customization. Keep the original receipts outside the lab removal root. A source appliance or client disk owned by another task requires separate source-admission evidence and owner coordination; the fixture does not transfer ownership of either source. The peer receipt binds the selected client VMDK by path and SHA-256 and the read-only producer rehashes it; this detects changes but does not establish original client-disk creation provenance. Admit that source independently before the lifecycle run. For the negative case, the authenticated CA inventory must bind the chosen ID and fingerprint to the live appliance:https certificate before and after the desired edit. Keep the original receipts outside the lab removal root; clean up only through the exact lifecycle VM removal and artifact-root procedures below.

The full lifecycle uses oidc.atlaso.internal because management HTTPS already owns core.atlaso.internal. Its first Apply includes the configured CA certificate consumers, then it applies the OIDC certificate, DNS record, and public listener. Site Client A resolves the OIDC hostname through the site DNS listener and verifies the discovery endpoint over TLS against the applied CA root; the Authorization Code flow uses the management listener, which remains reachable from the Windows harness when Site A is an isolated lan:<name> segment. The harness disables VMware Tools guest time synchronization before applying NTPsec, so the two clock controllers do not compete. Before the client NTS and ordinary NTP probes, it waits up to ten minutes for the appliance NTP server to clear its unsynchronized leap alarm; an active listener alone is not accepted as ready.

For focused appliance clock-source acceptance, use -TimeSourceOnly. This creates one owned appliance clone with management and site adapters attached before first boot, without client-image preparation, VCF Backup, ESXi, or restore scenarios. Use an existing host-reachable site VMnet and an independently verified unused -SiteCidr so the Windows harness can check a real server reply. The scenario exercises authenticated global Apply for NTP client, VMware Tools, and NTP server; preserves the VMware preference while server mode overrides it; and verifies controller exclusivity, observed synchronization, client request refusal, server replies, conflict recovery, and persistence after reboot and VMware Tools restart. Ownership, source admission, credential isolation, and cleanup gates remain required. Site probes bind the verified host VMnet address and check its selected route and guest neighbor MAC; this supports multihomed Windows hosts without changing host routes or adapter settings. Server-mode probes also capture ingress and prove refusal on the unselected management destination before and after reboot. Post-reboot status allows up to 120 seconds for HTTPS bootstrap and then 240 seconds of read-only login GET retries, covering the bounded startup clock reconciliation and application initialization. It submits credentials once after the login form is ready; non-retryable HTTP failures still fail immediately.

The web-terminal check likewise probes the site route and management-path isolation from Site Client A, while exercising the authenticated page and ticket flow through the reachable management listener.

Useful commands:

# Print selected paths and topology without creating VMs.
pwsh -ExecutionPolicy Bypass `
  -File scripts/windows/vmware/invoke-lifecycle-test.ps1 `
  -PullRequestNumber <number> `
  -PlanOnly

# Validate Workstation vmnet inventory.
pwsh -ExecutionPolicy Bypass `
  -File scripts/windows/vmware/invoke-lifecycle-test.ps1 `
  -PrepareNetworksOnly

# Stop and remove one exact retained PR-owned lifecycle lab.
pwsh -ExecutionPolicy Bypass `
  -File scripts/windows/vmware/invoke-lifecycle-test.ps1 `
  -PullRequestNumber <number> `
  -CollisionSuffix '<suffix-reported-by-the-run>' `
  -CleanupVmsOnly

The plan-only command does not prompt for passwords or create a protected credential bundle because the emitted plan does not consume credentials.

Completed task resource cleanup

Passing tests does not permit indefinite retention of disposable validation infrastructure. Inventory resources as they are created in a bounded validation_resource_inventory, binding each resource to its task, repository, source commit, PR when present, exact path or provider identity, and existing ownership manifest. Names alone never prove ownership. Include PR-numbered test VMs, lifecycle VMs, local and PR builders, disposable clones, address reservations, output claims, task-created disks and temporary networks, artifact/test roots, helper processes, credential-bridge recovery state, locks, and external test resources.

The originating task releases these resources through existing supported cleanup paths once required validation evidence is preserved, the PR is terminal, and no review, deployment, release, diagnosis, retry, or maintainer activity needs the environment. Failed or interrupted validation may retain resources only while diagnosis or retry needs them. This resource gate does not waive the existing merge, issue, post-merge, or private-remediation prerequisites for branch/worktree cleanup. Earlier per-operation sensitive-material cleanup and recovery remain mandatory.

Use remove-atlaso-vm.ps1 with the exact VMX and expected name after independently verifying ownership; use remove-lifecycle-vms.ps1 or the lifecycle wrapper's -CleanupVmsOnly for the exact PR-owned lab. These VM-only paths retain the result root; after preserving evidence and verifying ownership and quiescence, release that exact root with Remove-AtlasoWorkstationArtifactRoot using its exact configured-root binding as documented in the lifecycle guide. First use Assert-AtlasoStrictDescendantPath against independently configured permitted and canonical lifecycle roots; derive the expected lab path separately from validated task/PR identity, never from the candidate manifest path. Preserve existing identity, filesystem, shared-disk, provider-state, process-termination, and recovery safeguards. Release associated resources through their owning tools; VM removal alone does not prove reservations, claims, or recovery state released. Never delete shared, reusable, permanent, user-created, differently owned, or ambiguous resources.

The cleanup-ready handoff includes the inventory and durable, sanitized validation_resource_release_evidence: the cleanup entry point and result, exact resource identities, and verified absence of each disposable resource, VM registration and path, reservation, claim, process, lock, and temporary root. Preserve required validation and ownership evidence outside every root scheduled for deletion, on a permitted durable task/controller evidence surface. The primary-checkout controller independently reads back the exact resource states before recording validation_resources_released and before branch/worktree cleanup. For no resources, require an explicitly verified validation_resource_inventory_empty statement; missing evidence is never proof of absence. If another owner must perform teardown, hand off this bounded inventory and require the same ownership checks and independent readback.

Record explicit maintainer retention or a proven active downstream need as validation_resource_retention, with the exact resource, owner, reason, and retry condition. Any unresolved retention, failure, unsupported cleanup path, or ambiguous ownership records validation_resource_cleanup_blocked, preserves uncertain resources and recovery evidence, and keeps the task actionable. Do not delete the branch/worktree or mark Done to hide a blocked resource gate. Track missing cleanup capabilities separately; never substitute ad hoc deletion or broad VMware inventory cleanup.

For ordinary public tasks, enforce task_title_done with scripts/completed_task_title.py after all prior gates pass. Supply every linked issue and PR from verified task/GitHub evidence, not from a potentially truncated current title; use repeated --issue and --pr arguments and a short --description without traceability or completion segments. For an issue-less GitHub-managed Dependabot PR, independently verify the documented dependency-update exception and pass --dependabot; preserve every PR number and any linked issue that does exist. Ordinary tasks still require issues. The formatter puts all identifiers first, trims only the description to a conservative 60 UTF-16-unit budget, and retains exactly one " · Done" suffix. If the identifiers alone do not fit, keep completion blocked for maintainer direction; never drop an issue or PR. Use supported task-title controls to set the exact generated title, then read the persisted title through a supported task read tool and rerun the same formatter inputs with --observed-title. Record task_title_readback_verified only when verification succeeds. A rename acknowledgement alone is insufficient. A stale, truncated, missing, or duplicated completion marker blocks Done and requires an idempotent title-only retry after revalidating earlier gates. Do not repeat destructive cleanup on a title retry. If title controls are unavailable, retain the existing capability-evidence exception. Private remediation keeps its sanitized title and private evidence; never pass advisory identifiers to this public formatter, and still require exact supported-tool title readback.

Terminal order:

  1. validation_resources_released
  2. remote_branch_absent
  3. worktree_removed
  4. task_title_done

The completed-task controller policy owns the remaining branch, worktree, and title transitions. Resource release also applies to a terminal closed-unmerged or retained-open PR once its environment is no longer needed; it does not authorize branch/worktree deletion or Done for that task.

Supported removal entry points

Run the existing PowerShell 7 entry points from the task checkout before removing it. For an individual test VM or disposable builder, use scripts/windows/vmware/remove-atlaso-vm.ps1 -VmxPath <exact-vmx> -ExpectedName <canonical-name>. Verify the creation/ownership manifest, exact VMX path, and matching displayName first: the script's optional name check is not a substitute for the caller's complete ownership proof. Add -AllowImageOutputRemoval only for intentional removal of an independently verified disposable task-owned builder under the protected image-output root; it never authorizes removal of a reusable template or wider output directory.

For a lifecycle lab, use scripts/windows/vmware/invoke-lifecycle-test.ps1 -CleanupVmsOnly with the exact -PullRequestNumber and -CollisionSuffix reported by the run, or scripts/windows/vmware/remove-lifecycle-vms.ps1 with those same identities and the recorded -Purpose when it differs from the default. Preserve the lab ownership plan as evidence before removal. These paths reuse Atlaso.WorkstationCleanup.psm1; do not replace them with raw provider deletion or recursive filesystem commands.

VM-only cleanup deliberately leaves the lab result root, including plan.json, vmware-identity.json, and logs. Before releasing that root, preserve sanitized validation and ownership evidence outside it, independently bind the absolute root to the originating task and recorded lab manifest, verify no VMX remains, and verify all lab helpers and recovery activity have finished. A path or PR-shaped name alone is insufficient. Independently read the supported configured permitted root, verify the registered task worktree and its exclusive ownership, and derive the canonical lifecycle parent as test-results/vmware-workstation-lifecycle beneath that worktree. Do not derive either trusted root from lifecycle metadata. Independently verify the PR, purpose, and collision suffix against the task's creation evidence. Use those identities to derive the expected lab path separately from the candidate manifest path:

$ErrorActionPreference = 'Stop'
Import-Module ./scripts/windows/vmware/Atlaso.WorkstationCleanup.psm1 -Force
Import-Module ./scripts/windows/vmware/Atlaso.VmwareTestIdentity.psm1 -Force
Assert-AtlasoStrictDescendantPath `
  -ParentPath $configuredPermittedRoot -ChildPath $verifiedTaskWorktree `
  -FailureMessage 'Task worktree is outside the configured permitted root'
$canonicalLifecycleParent = Join-Path $verifiedTaskWorktree 'test-results/vmware-workstation-lifecycle'
Assert-AtlasoStrictDescendantPath `
  -ParentPath $verifiedTaskWorktree -ChildPath $canonicalLifecycleParent `
  -FailureMessage 'Lifecycle parent is outside the verified task worktree'
$verifiedLabIdentity = New-AtlasoVmwareTestIdentity `
  -PullRequestNumber $verifiedPr -Purpose $verifiedPurpose -CollisionSuffix $verifiedCollisionSuffix
$expectedLabRoot = Join-Path $canonicalLifecycleParent $verifiedLabIdentity.Name
Assert-AtlasoStrictDescendantPath `
  -ParentPath $canonicalLifecycleParent -ChildPath $expectedLabRoot `
  -FailureMessage 'Expected lab is outside the canonical lifecycle parent'
$candidateLabRoot = [System.IO.Path]::GetFullPath($manifestBoundLabRoot)
if (-not $candidateLabRoot.Equals($expectedLabRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
  throw 'Candidate lab does not match independently derived task identity'
}
Remove-AtlasoWorkstationArtifactRoot `
  -VmrunPath $verifiedVmrunPath `
  -ExpectedRemovalRoot $expectedLabRoot `
  -RemovalRoot $candidateLabRoot

The caller supplies the configured root and verified task/provider identities from independent supported configuration and ownership readback. Missing, ambiguous, outside-root, or reparse-point state blocks this sequence. The candidate manifest path must equal the independently derived expected path; never feed that candidate back as its own expected root. Run the containment assertions even on an already-absent retry. This existing helper supports VM-free residual roots and already-absent retries, retains filesystem identity and provider-state safeguards, and confines missing-registration handling to the exact root. Do not substitute its broader -ArtifactParentRoot parameter set or aggregate artifact cleanup for this lab-specific release. Read back root and registration absence; an already-absent return still requires the same ownership evidence and independent readback before release is recorded.

Builder-address release, output claims, and credential-bridge recovery retain their owning wrapper/module's existing release and retry paths. Do not erase their ledgers or recovery markers to simulate completion, and do not start a new build merely to trigger recovery. If no supported bounded cleanup-only path applies, record the blocked resource and track the missing capability separately. A successful VM-removal command does not waive independent absence checks.

For a retained current Photon sensitive-staging marker, the builder also supports a cleanup-only mode. Use the exact registered task worktree and the sensitive-root identity recorded by its creating task. The executing wrapper may live in another registered worktree of the same Git repository. Missing markers, mismatched identities, legacy markers without an original root identity, changed ancestry, and unproven process ownership preserve resources. Admission ignores inherited Git environment overrides and restores them after checking the repository. Identity-bound recovery admits the exact credential parent, including nested custom build-state roots. Admission checks Git's NUL-delimited registered worktree inventory and the private Git directory's backlink; sharing repository metadata alone does not admit a moved or copied worktree.

$taskRepository = 'E:\task\Atlaso'
$originalRootIdentity = '<original task-recorded sensitive-root identity>'
.\scripts\windows\vmware\build-photon-image.ps1 -CleanupOnly -CleanupRepositoryRoot $taskRepository -CleanupRootIdentity $originalRootIdentity
.\scripts\windows\vmware\build-photon-image.ps1 -CleanupOnly -CleanupRepositoryRoot $taskRepository -CleanupRootIdentity $originalRootIdentity -Cleanup -WhatIf
.\scripts\windows\vmware\build-photon-image.ps1 -CleanupOnly -CleanupRepositoryRoot $taskRepository -CleanupRootIdentity $originalRootIdentity -Cleanup

The first two commands inspect marker and filesystem identity without terminating processes or changing files. An inspection does not prove process quiescence or cleanup eligibility. If the identity-bound marker disappears between inspection and execution, recovery fails; marker absence alone never proves sensitive-root retirement. Execution repeats Git registration and backlink admission after confirmation, immediately before recovery, and uses the existing same-boot named-job/controller/child proof or verified host-restart recovery before retiring sensitive staging. It retains proven same-boot termination in the unique matching pending builder-address handoff before deleting the sensitive root or retiring the marker. That receipt includes the original schema-3 process ownership and binds every allocation field to the existing shared ledger under its allocator lock. Missing or conflicting allocation evidence or failed proof publication preserves the marker for the same cleanup command to retry; no allocator state is created. Recovery also repeats this step for a terminal marker left by an interrupted retirement. Preview does not publish proof. When no matching handoff remains, the existing ledger must independently prove no allocation remains for that child or checkout before the marker can retire. Missing ledger state and ambiguous retained allocations preserve the marker. It returns before PowerCLI refresh, package downloads, credential retrieval, reservation initialization, output claims, source protection, or Packer. Build and credential arguments are rejected in this mode. Ordinary image builds reject -WhatIf and -Confirm, which are supported only with -CleanupOnly. VMware VMs, builder output, output-claim markers, and address reservations require their separate owning cleanup procedures. Record their independent absence or preservation; staging retirement does not prove deployment acceptance or authorize a new build.

Cleanup Safety

The synthetic lifecycle storage fixture exercises the runner's cleanup statements inside their containing try body, without executing the outer resource-release block. It checks both refusal before any provider call when diagnostic termination is unproven and admission to mocked providers when termination is proven. A fixture parse failure is reported separately from a cleanup-guard failure; this validation requires no live VMware resources or credentials.

Workstation cleanup is authoritative only for an exact Atlaso artifact root. It rejects filesystem roots, sibling or parent targets, and any root or descendant containing a reparse point. Every recursively discovered VMX must be a strict descendant of that root and must match the caller's validated target set. Cleanup captures the root and descendant filesystem identities before provider operations. It checks the root identity both before and after descendant traversal; a new or replaced entry, or a replaced root, blocks the final recursive removal and preserves the replacement.

For each target, cleanup uses checked vmrun -T ws list output to decide whether the exact VMX is running. It stops a running target with checked vmrun -T ws stop <vmx> hard and verifies that the target is no longer listed. A well-formed registration row for that exact in-root VMX selects checked vmrun -T ws deleteVM <vmx>. Already-stopped and already-unregistered VMs remain idempotent cleanup cases. A nonzero provider command, a target that remains running, or a VMX that survives provider deletion preserves the remaining root and returns failure. Insufficient permissions from deleteVM does not by itself prove an NTFS permission problem. VIX can emit it after an Authd connection failure prevents the GUI unmanage transition and the stopped VM's tab retains a VMX lock. Cleanup reads only a bounded tail of recent VIX logs, matches the exact VMX lock path and current invocation time, and checks the reported GUI process ID and start time before identifying a still-live owner. These diagnostics never authorize deletion, process termination, or reservation release. Missing or stale evidence remains explicitly unclassified; a running Authorization Service does not prove that the earlier Authd connection succeeded.

When this occurs, close the exact stopped builder's tab in Workstation. If it still retains ownership, review other VMs before closing Workstation normally, then retry the original checked cleanup workflow. Do not delete .lck files, terminate a shared GUI, or elevate/change ACLs merely because of the generic provider error. Cleanup performs one provider deletion attempt; a retry must re-enter all ownership, identity, registration, inactivity, and external-disk checks. Keep retained artifacts and the builder-address reservation until the existing completion gates prove removal.

Immediately before each deleteVM, cleanup repeats the target identity and identity-aware running check, confirms the exact scoped registration, and verifies that the recursive VMX set still contains only the validated targets.

Immediately before deleteVM, cleanup removes every VMX device whose resolved VMDK path is outside the exact cleanup root. This prevents provider deletion from following a shared depot, backup, or other external disk. A failed provider operation atomically restores the displaced original VMX only when the stopped target still has the protected identity and content; a concurrent replacement is preserved instead of overwritten.

Normal deletion does not require global inventory.vmls consistency. Unrelated stale, malformed, missing, duplicate, or otherwise inconsistent Workstation library entries cannot block cleanup of the Atlaso root. Inventory parsing is limited to well-formed rows that resolve lexically beneath the approved cleanup scope. If an Atlaso-scoped row points to an already-missing VMX, the exceptional stale-registration fallback removes only that library record and its matching index record. It rechecks that the VMX is still missing, compares the inventory bytes immediately before replacement, and restores a concurrently displaced provider file before reporting failure. Unrelated registration records are left in place and are never required to resolve. Close the Workstation UI before this exceptional repair of the real user inventory. When lifecycle execution and cleanup both fail, the final error reports the original scenario failure together with the cleanup failure and preserved path.

Durable normal-test-VM recovery repeats that same narrow registration proof after an earlier provider attempt removed the exact artifact root. The marker-bound VMX path and display name must either have no registration or identify one well-formed stale library row that can be removed safely. A mismatched, duplicate, malformed, changed, or otherwise ambiguous selected row keeps the cleanup marker and any quarantined disks actionable for the next bounded retry; unrelated Workstation registrations and artifact roots remain untouched. Workstation must remain closed and is revalidated after inventory exclusion is acquired. Recovery restores quarantined disks and retires the marker while the verified inventory remains read-and-write-excluded. If no inventory exists, recovery creates and exclusively locks an empty inventory so the proven zero-registration state remains authoritative after the marker is retired.

Checked deleteVM may remove the complete validated artifact root itself. Cleanup records that transition immediately, continues the same registration and identity-aware running-inventory postconditions, and requires the exact root to stay absent through the final gate. It does not enumerate or recursively remove the missing directory, so the same create-atlaso-test-vm.ps1 -Redeploy invocation can proceed to a fresh clone. A recreated root or changed target inventory remains an ambiguous state that fails closed and preserves the new path. Focused regression coverage runs a test-only copy of the normal wrapper with only its host-global signer, credential, and post-clone startup boundaries stubbed. The wrapper still performs its real input validation, redeploy cleanup, data-disk checks, and low-level clone orchestration against a synthetic schema-v3, role-bound source payload bound to both an immutable source snapshot and the matching builder identity. This proves that provider-owned root removal continues into cloning and that missing targets, sibling-prefix data disks, and invalid SSH-key inputs fail before cleanup. The harness does not use -NoStart; the production wrapper still requires first boot to consume and scrub the shared development signer.

The read-only Workstation registration inventory may reside beneath a redirected %APPDATA% junction or symbolic link. The non-reparse-point requirement remains enforced on the Atlaso artifact root that cleanup recursively deletes, not on unrelated provider state. Recursive deletion errors are terminating, and cleanup reports success only after confirming that the artifact root is absent.

The Photon image builder applies the same inventory checks to the exact resolved -OutputDirectory. Absolute custom output directories remain supported even when they are outside the Packer directory; cleanup binds deletion to that exact configured path and will not accept a sibling or parent path instead.

The normal test-VM -Redeploy path also requires the exact named VMX and exactly one well-formed, matching displayName; missing, duplicate, malformed, or conflicting assignments preserve the existing directory. -ResetDataDisks accepts only strict canonical descendants of the selected VM output, so sibling-prefix and reparse-point paths are refused.

Normal test-VM creation ownership

Codex runs of create-atlaso-test-vm.ps1 independently read the active desktop.git-worktree-root and bind the exact task, registered clean checkout, branch, source commit, and canonical VM name. Durable receipts live in the existing .task-ownership directory beneath that configured root, outside the task checkout and VM removal scope. Missing, redirected, conflicting, or out-of-root evidence blocks creation before credential access.

The wrapper publishes an immutable atlaso-test-vm-intent-<id>.json before cloning. The low-level clone callback publishes atlaso-test-vm-original-<id>.json with the original clone-directory and VMX identities before appliance configuration, credential staging, or startup. A persistent non-secret VMX identity preserves the binding when Workstation replaces the VMX file during startup; the original filesystem identity remains historical creation evidence. The source template is recorded as read-only provenance and is never a cleanup target. The owned clone starts suspended in a fresh named kill-on-close Windows job. Before creating the destination or resuming the child, the wrapper publishes an immutable atlaso-test-vm-clone-process-<id>.json recording the original controller and child creation times, boot identity, job name, task/source and intent binding. Each original directory receipt includes that process receipt's path and SHA-256. Failed publication terminates the suspended child tree; an existing job name is refused without adopting its processes. Clone completion requires whole-job quiescence. Before provider cloning, the wrapper exclusively creates each missing destination component relative to its pinned parent and immediately publishes an immutable atlaso-test-vm-directory-<id>.json creation record. It retains the original directory handles with writer and replacement exclusion through cloning and the original VM receipt callback. The original VM receipt publisher verifies and borrows the retained creation handle instead of opening a conflicting second directory pin. A concurrently inserted directory or junction is refused rather than adopted. Failed preparation preserves its published records. If directory-receipt publication fails, the still-suspended clone cannot resume and the newly created empty object is retired through its retained creation handle. Nonempty retirement refuses and preserves inserted artifacts. Successfully recorded but incomplete destinations retain their original records; preparation does not invent a completed VM receipt. Retry an incomplete canonical output with -RecoverPreparedDirectory, -PreparedDirectoryReceiptPath, and -PreparedDirectoryReceiptSha256 using the selectors emitted at its original preparation. This is a cleanup-only invocation: it rejects creation options before removal and exits without cloning; retry creation separately. Recovery retains the original root through handle-based deletion and verifies the original receipt, intent, task/source ancestry and directory identity inside the owning remover before retiring an empty destination. Directory and process receipts do not prove child creation: every nonempty prepared directory, including one containing ordinary partial-clone files, is preserved without child creation evidence. A later cleanup snapshot cannot supply that evidence. A replaced directory or altered receipt is also preserved. Recovery verifies the original process binding and refuses an active or reused controller. An empty preparation receipt owns no VMX or Workstation registration. Prepared cleanup preserves stale provider registrations, including entries introduced after admission, rather than repairing them through the directory scope. On the same boot it uses the exact recorded child identity and named job to prove or complete owned-tree termination; a proven prior host boot independently establishes quiescence. Missing, ambiguous or legacy process evidence preserves the directory without retrofitting receipts or stopping unrelated processes. Completed clones still require their original VM receipt and the normal redeploy path. Ancestor preparation records remain historical evidence; recovery never removes a shared parent directory. The clone callback emits the original receipt path and SHA-256 immediately after publication, so preserve those selectors even when later configuration or readiness fails.

Preserve each returned receipt path and SHA-256 digest with the task's resource inventory. Redeployment must prove the same task and original directory and persistent VM identity from its creation receipt. Remaining receipt-bound VM artifacts retire through captured directory/file handles even when the VM is unregistered or provider deletion leaves the directory behind. Immediate failed-creation rollback and persisted rollback cleanup pass the verified original directory and persistent VM identity selectors to the removal child. Missing original receipt admission preserves the VM; persisted cleanup also refuses a marker for a different task destination. Redeployment creates a new intent and original receipt for the replacement. For an existing VM, pass -OwnershipReceiptPath and -OwnershipReceiptSha256 from the independently preserved task inventory; do not derive a replacement digest from the current receipt. Both selectors are required in receipt-enabled runs. Output ancestors are checked before credential access and pinned through cloning, and the recorded source template identity and expected commit are rechecked at that boundary. Source ancestors stay pinned against replacement; the VMX, provenance and both payload disks exclude writes and replacement from byte validation through cloning. A missing receipt, changed identity, failed publication, or ambiguous state keeps the VM preserved. The admitted task branch, exact commit and clean checkout are revalidated immediately before network preparation, recovery/removal, disk reset and clone preparation; preflight drift refuses mutation. Admission records the original worktree directory identity while pinned. Every source guard re-pins and checks that identity before checking Git state; a byte-identical replacement checkout cannot authorize mutation. Verified tracked files and their directories stay pinned against writes, deletion and replacement through invocation completion, covering helpers and their checked-in dependencies after each guard returns. The wrapper releases these handles in its outer finally on success or failure; failed initial admission releases partial pins before returning. Within the trusted entrypoint/host contract above, guards also compare the already parsed entry/call-stack scripts and loaded checkout functions, including module-private functions, with the pinned commit-verified text. Restoring files after parsing cannot authorize stale in-memory code. This does not create an independent host trust boundary. Each boundary also rejects assume-unchanged/skip-worktree entries and independently hashes all tracked file bytes against the immutable admitted commit tree, applying Git's normal clean conversion for checkout line endings. The staged inventory and flags must match that tree; an index swap cannot change the expected helper hashes. Cached status or fsmonitor results cannot substitute for this byte verification; index changes or hashing failures refuse mutation. The source guard also runs immediately before post-clone signer staging, startup, signer-scrub restart, NIC configuration, immediate/persisted removal and the final readiness helper launch. Late source drift preserves the VM rather than executing changed helper code. Never manufacture an original receipt from a later snapshot, edit a receipt, or delete an incomplete publication to make admission pass. -WhatIf publishes no receipts and performs no credential or VM mutation.

Ordinary human CLI runs retain their existing behavior. Explicit externally recorded human runs select -OwnershipRoot and -OwnershipTaskId together; the root must be an existing permitted durable root containing the source checkout, with receipts outside the checkout and removal scopes. Agent runs use their active task and supported configuration instead of accepting those overrides.

Use the exact receipt's VMX and canonical name with remove-atlaso-vm.ps1 only after independent ownership and quiescence checks. Pass its original directory identity as -ExpectedRootIdentity and persistent VM identity digest as -ExpectedPersistentVmIdentitySha256; the owning remover binds its cleanup snapshot to both before stopping the VM. Preserve the receipts outside cleanup scopes and record provider, VM path, and resource absence separately. A published creation receipt establishes ownership; it does not establish appliance readiness or KMIP interoperability.

Normal Test VM

For a normal Workstation appliance VM, separate from the lifecycle lab, store the exact Atlaso Environment ID once in the checkout-local, Git-ignored configuration file. The prompt is masked and the command does not print the ID:

$atlasoLocal = Join-Path (git rev-parse --show-toplevel) '.atlaso-local'
New-Item -ItemType Directory -Path $atlasoLocal -Force | Out-Null
$atlasoEnvironmentId = Read-Host 'Paste the Atlaso Environment ID' -MaskInput
try {
    [System.IO.File]::WriteAllText(
        (Join-Path $atlasoLocal 'onepassword-environment-id'),
        $atlasoEnvironmentId
    )
}
finally {
    Remove-Variable atlasoEnvironmentId -ErrorAction SilentlyContinue
}

Then use the ordinary command without an ID argument:

pwsh -ExecutionPolicy Bypass `
  -File scripts/windows/vmware/create-atlaso-test-vm.ps1 `
  -PullRequestNumber <number> `
  -Redeploy `
  -ResetDataDisks `
  -WaitForIp `
  -TrustRootCa

For exploratory normal-VM testing before a pull request exists, replace -PullRequestNumber <number> with -LocalBuilder. A clean checked-out branch derives Atlaso-Local-<12-character-source-commit>-test-vm[-<collision-safe-suffix>] while retaining exact VM ownership and redeploy checks. Lifecycle labs and acceptance evidence remain PR-numbered.

It defaults to the management vmnet only; pass -IncludeLabNetworkAdapters after creating the SiteA, WAN/SiteB, and trunk-like vmnets. For real creation, the wrapper prefers an explicit -OnePasswordEnvironmentId override and otherwise reads the exact single-line .atlaso-local/onepassword-environment-id file. The entire .atlaso-local directory is ignored by Git. A custom file may be selected with -EnvironmentIdFile; the legacy -OnePasswordEnvironmentIdFile spelling remains an alias for existing automation. Pending signer cleanup runs first because it consumes no 1Password material. After that recovery, a missing or malformed ID fails with an actionable preflight error before network preparation, disk reset, cloning, or other new VM mutation. The wrapper verifies its SHA-256 identity against the repository pin without printing the ID. Install the Environments-enabled beta 1Password CLI under C:\Program Files\1Password CLI; a stable CLI without op run --environment fails before Environment access. A wrong Environment ID or signer fails before new VM mutation. The concealed ATLASO_DEVELOPMENT_ROOT_CA_PRIVATE_KEY value accepts the original complete PEM or canonical single-line base64 of the complete PEM text (including the BEGIN and END lines). Encode the original PEM bytes without changing line endings or encoding only the DER body. The bounded secret child decodes the value and validates the PEM against the checked-in certificate before VM mutation; the encoded value remains secret material.

The development-only root CA was rotated after its former matching private key became unavailable. Its public certificate is checked in at image/vmware-workstation/development-trust/atlaso-development-root-ca.pem; the matching private key is concealed only in the exact Atlaso 1Password Environment. Keep certificate and Environment changes coordinated: the wrapper rejects a mismatched pair before creating a VM. Newly created normal test VMs use the current root. Existing test VMs retain the prior root and leaf certificates they imported at first boot and cannot satisfy the current checked-in fingerprint or -TrustRootCa on a clean Windows host. Redeploy every normal test VM through its owning wrapper after a root rotation before using it for current validation. Preserve an existing VM until its owner can safely replace it, and verify the new public fingerprint before trusting it on Windows. The cleanup marker uses a non-secret identity stored in the VMX, not the VMX file ID alone, because Workstation may replace the VMX during power-on. The wrapper exposes the marker path as recovery state only after its write-through rename succeeds. If publication fails before a credential or signer child starts, it preserves the original actionable error and rolls back only the VM artifacts created by that invocation; a durable child-active or unproven marker still blocks destructive rollback. Before a pre-secret rollback launches removal, it reconciles any exact identity-bound marker whose atomic rename completed before its caller path was exposed, then durably binds the exact stopped VMX, quarantine path, and current host boot. An ambiguous publication or failed fallback preserves the VM instead of starting removal. After first boot proves encrypted import and VMware reports the exact empty runtime sentinel, the wrapper stops the exact VM, removes and verifies the powered-off signing-key assignment, restarts it, and requires three empty runtime readbacks before retiring the marker. This successful-import stop is graceful and bounded so certificate and database state reach durable storage; the wrapper does not fall back to a hard power-off and preserves the retryable marker when graceful shutdown cannot be proven. First boot commits guest-agent provider selection before erasing the offline package closure. That retryable cleanup is a mandatory 15-minute data-disk pre-start gate and may run concurrently with VMware customization, so it cannot delay signing-key guest-info scrub while still blocking data-disk and application readiness. The data-disk unit reserves five additional minutes for formatting and mounting after cleanup. The host retains at least 25 minutes for encrypted-import proof so cleanup, disk preparation, and subsequent bootstrap startup cannot trigger false rollback. The wrapper transports the validated signer as one canonical base64 PKCS#8 DER value whose complete assignment stays below VMware's 4,096-character VMX line boundary; first boot reconstructs standard PKCS#8 PEM before mode-0600 staging. That cleanup erases only the offline closure; portable KVM and Hyper-V first-boot access remains available until the next boot. During scrub and encrypted-import proof, the guest publishes only bounded fixed first-boot stage identifiers. A timeout reports the last valid stage, or a fixed diagnostic that provider selection or customizer startup did not complete; it never includes command output or secret material. The wrapper injects the same complete DHCP-first OVF environment before power-on. Use -FirstBootFqdn for the test identity. The wrapper prefers the checkout-local current-user DPAPI service-account token and uses standard GIL-enabled Windows x64 CPython 3.14 registered with the Windows launcher, including bracketed Python Install Manager architecture selectors. Use -OnePasswordServiceAccountTokenFile, -OnePasswordAccount, or -OnePasswordPython only when an explicit override is required. Without a token, zero or multiple desktop accounts fail closed; a missing compatible runtime always fails before VMware mutation. -AdminPassword and -RootPassword accept only SecureString objects. When either parameter is omitted, the wrapper retrieves only that credential's exact concealed DEFAULT_ADMIN_PASSWORD or DEFAULT_ROOT_PASSWORD from the already verified Environment through the supported 1Password SDK integration. Service-account authentication does not invoke DesktopAuth; explicit account selection preserves the existing desktop flow. Each explicit password parameter remains independently authoritative; there are no password prompts, caller-environment fallbacks, repository defaults, plaintext token files, or local .env inputs.

The SDK runtime is built from the hash-locked deployment wheel set and invoked only after credential-independent signer recovery, exact Environment verification, and local input validation. It returns only current-user DPAPI ciphertext to the PowerShell parent. A second bounded child stages the DPAPI-protected OVF environment into the exact new VMX. Authorization, Environment, variable uniqueness, concealment, value validation, runtime, or timeout failure stops before network preparation, redeploy cleanup, data-disk reset, or cloning. -WhatIf does not prepare the SDK, authorize 1Password, or retrieve either credential. Password plaintext never enters parent memory, process arguments, caller-controlled environment, logs, output, markers, evidence, documentation, or GitHub surfaces. The wrapper commits durable child-active recovery state before the VMX staging child starts. If whole-tree termination cannot be proven, later cleanup or redeploy remains blocked until a new host boot proves that child is gone.

GUI and headless starts use a short-lived launcher that starts vmrun without redirected standard streams. This keeps the launcher's bounded diagnostics available while preventing the detached Workstation UI or vmware-vmx.exe process from retaining the parent wrapper's output pipes. The root-process wait and redirected-stream drain each have a finite deadline. If an unexpected descendant retains a writer, the operation fails closed with the durable child-active marker intact; do not stop an otherwise healthy, exactly owned VM merely to release a pipe. Rerun the supported wrapper so its identity-bound recovery can advance the exact marker. Never edit the marker or VMX manually, and do not reuse, stop, or remove a VM whose PR-numbered display name and absolute VMX ownership cannot be proven. It also resolves the current Windows user's existing .ssh/id_ed25519.pub before any network preparation, cleanup, or VM creation, installs that Ed25519 public key for admin, and adds a separate test-only passwordless-sudo rule. Pass -SshPublicKeyPath <path> to select another existing Ed25519 public key, or -SkipSshKeyProvisioning to retain password-backed SSH and sudo. The two options are mutually exclusive. Missing, malformed, multiline, non-Ed25519, and unbounded key input fails before the existing VM can be changed. The wrapper never generates or copies a private key. This development authority does not apply to the Workstation lifecycle lab or exported OVF/OVA appliances, and root SSH remains disabled. Complete factory reset removes both the development authorization key and its passwordless-sudo drop-in, restoring the ordinary password-backed sudo policy. Approve the appliance's verified host key through the wrapper's host-derived output: after startup it prints the exact Ed25519 public host key and SHA-256 fingerprint from test-only VMware guest-info for explicit known_hosts verification without trusting unauthenticated ssh-keyscan output. Subsequent Codex and Copilot tasks under the same Windows user reuse that trust and key identity. Before any ready message or connection endpoint, every started normal clone must prove that VMware Tools' management IPv4 address belongs uniquely to the exact running VMX. An explicit normal-test marker, independent of optional SSH key provisioning, makes first boot publish the actual applied hostname through VMware Tools. The proof records the VMX, its ethernet0 MAC, the matching injected and observed hostnames, and the host-facing address; it requires an address answer from every running Workstation guest and requires the Windows neighbor entry for that address to match the target MAC. An unanswered running guest remains incomplete evidence and retries. A hostname might be returned directly or inside one matching pair of VMware representation quotes; readiness removes only that outer pair and rejects unbalanced, nested, or embedded quotes as ambiguous. A hostname mismatch, duplicate static address, or neighbor entry owned by another running VM fails closed with the relevant exact identity evidence. The wrapper re-lists the running inventory and rechecks the target address immediately before returning; a concurrent VM start, stop, or target-address change retries the complete proof.

The development-signer scrub deliberately stops the VM, removes the powered-off VMX assignment, and restarts the exact clone. VMware discards runtime guest-info during that power cycle. On the applied-marker boot, a normal test VM therefore republishes its current hostname and regenerated Ed25519 host key from inside the guest before dependent services start. The durable marker's explicit normal_test_vm value owns this behavior; ordinary appliances never enter the test-only identity channel. Publication failure keeps the customizer failed and prevents identity-bound readiness from trusting stale or host-synthesized evidence.

Address ownership and guest initialization are reported as separate readiness layers. Once a stable VMX, MAC, Tools address, running inventory, and Windows neighbor tuple has been proven, an absent guest-published hostname no longer falls through to the generic address-discovery timeout. The failure retains that non-secret ownership tuple, shows the injected and observed hostname state, and includes only the last allowlisted bounded first-boot stage when the guest published one. A missing or unknown stage is reported as unavailable; arbitrary guest-info is never echoed. This means the guest did not establish complete identity evidence, even when HTTPS happens to answer independently. Inspect the exact clone's local console and first-boot service state, and rebuild an outdated or unfixed source appliance before redeploying. Do not weaken or bypass the hostname gate merely because SSH, HTTPS, or an address is reachable.

Recover from that failure through the exact clone's local console: stop the named conflicting VM, or give the clone a unique applied static address. A temporary DHCP reservation is acceptable only when it is bound to the exact target MAC shown by the failure. Then rerun get-atlaso-vm-ip.ps1 with the exact VMX and the original -ExpectedHostname, or redeploy the normal test VM, before running SSH or HTTPS validation. Keep SSH trust explicit: compare the separately published Ed25519 key and SHA-256 fingerprint, and update known_hosts yourself only when intended. The wrapper never changes normal SSH known_hosts automatically. The IP helper accepts a positive -TimeoutSeconds up to 2,147,483 seconds, matching the provider's signed millisecond wait limit, and a positive -PollSeconds. Each retry sleeps for at most the remaining deadline; choosing a poll interval longer than the timeout does not add a full extra interval to the wait. Changing the applied management listener from a dedicated interface to an access physical interface or VLAN with Management UI enabled must retain TCP/22 admission for this ordinary admin SSH workflow, under the same management Source Group restriction as TCP/80 and TCP/443. It does not enable root SSH and must not expose SSH on an unflagged access network. Validate TCP/22, authenticated admin SSH, and HTTPS /openapi.json before and after the protected handoff and again after reboot. Credential overrides must be at least 12 characters, contain no leading, trailing, tab, carriage-return, or newline whitespace, and contain only XML-representable characters so the OVF value round-trips unchanged. -TrustRootCa waits for the first-boot CA endpoint, removes partial downloads best-effort between retries, validates the self-signed Atlaso root CA, and imports it into the current-user Trusted Root store. After a successful import, the wrapper retries the exact raw-certificate readback for a bounded interval so a stale in-process certificate-provider view does not report a false failure. The temporary-file cleanup remains idempotent for missing files and safely handles dotted user-profile directories and valid DOS 8.3 short paths, so a cleanup race or path alias cannot stop the readiness retry loop. Use -TimeoutSeconds to change the secret-child, IP, and CA deadlines; rollback mutates the VM only after staging and start child-tree termination is proven. It also keeps reused disks quarantined while an artifact-removal child may survive. An unproven termination preserves the durable marker until a Windows host restart provides that proof. If this final stop/scrub/restart sequence is interrupted, rerun the same wrapper command. The pending marker resumes before 1Password preflight or any new network, disk, or VM mutation; do not edit the marker or VMX manually. Agent-owned retries validate the original VM receipt and exact marker destination before scrubbing, stopping, quarantining disks, retiring a marker, or removing artifacts. Foreign markers and legacy identity repairs are preserved without mutation; an ownership context for another VM cannot authorize their recovery. Live pending recovery retains the receipt-bound original root pin through signer, stop/restart and quarantine helpers. It releases that pin only for the removal child's independent original-identity admission and captured-handle removal; all refusal and completion paths release their pins without adopting a replacement root. Owned marker publication also emits an immutable cleanup recovery receipt path and SHA-256 before staging or removal. Preserve both independently. If an interrupted removal has already removed the VM root, rerun with the original VM receipt selectors plus -CleanupRecoveryReceiptPath and -CleanupRecoveryReceiptSha256. This recovery-only mode validates the immutable task, VM receipt, marker, preserved disk and quarantine bindings without requiring the live VM; stopped/process-quiescence and disk identity checks still precede restoration. It exits after recovery and forbids redeploy. Legacy markers without original recovery evidence are preserved; never manufacture that evidence afterward. Receipt-only recovery runs before Python/1Password SDK artifact admission. Missing SDK tooling cannot strand preserved disks; artifact verification remains mandatory before fresh credential or creation work. If recovery restores canonical in-directory disk components, it publishes an original restored-directory receipt path and SHA-256 before moving them. Zero-disk removal recovery also publishes an original empty restored root before retiring its marker; preserve those selectors for the next creation retry. Any pre-existing unreceipted replacement root is refused before provider reconciliation, even when no disks need restoring. Preserve both. A later creation retry passes -RestoredDirectoryReceiptPath and -RestoredDirectoryReceiptSha256. Recovery pins each moved original disk, validates the complete restored inventory, and retires the cleanup marker while those pins still exclude writes, deletion and replacement. A failed validation or marker retirement preserves the marker for another bounded recovery. Retry proceeds without -Redeploy. Admission checks that original root and its exact ordinary disk components, refusing replacements, extra files/directories, missing selectors and legacy state. The original root and disk handles remain held through cloning; a fresh VM receives its own original VM receipt. -ResetDataDisks removes only that admitted complete component set through captured handles before creating replacement disks. Nested or unproven component layouts remain quarantined rather than acquiring ownership from a later snapshot. Restored-root cloning validates both system payload roles and permits only the separately receipt-bound data VMDK components alongside them. Source-image validation still requires exactly two payload disks; unexpected clone VMDKs are refused. Rerun an interrupted creation with the preserved original receipt selectors without adding -Redeploy solely for recovery. Pending cleanup runs before an existing-VM rejection. If import recovery leaves a healthy VM in place, the wrapper preserves it and rejects recreation after recovery, before obtaining new credentials. Fresh source-template resolution, provenance validation, and creation-intent publication follow pending recovery and the existing-VM check. A removed or replaced template therefore cannot strand recovery for an already-receipted VM; if recovery removes the failed VM, any subsequent creation still requires valid source provenance.

Fidelity Boundary

For ESX Storage appliance acceptance, attach an extra blank VMDK to the normal Workstation test appliance, initialize it only through global esx_storage apply, and apply the matching DNS/DHCP and Firewall units. Record the job ID, /dev/disk/by-id fingerprint, UUID mount, generated A/AAAA names, exportfs -v, TCP/111/2049/20048 sockets, nftables family rules, and persistence after appliance reboot. Workstation proves real Photon disk/NFS/DNS/firewall behavior. External ESX 9 integration evidence remains required for IPv4 and IPv6 VMkernel mounts and datastore I/O.

VMware Workstation vmnets provide isolated layer-2 segments. The lifecycle validates the appliance workflow, management reachability, service apply behavior, tty1 console ownership with tty2 left available for normal login, backup/restore portability, and host/client integration where separate vmnets are equivalent. Tagged-trunk acceptance requires a compatible upstream virtual-network configuration and recorded topology evidence.

For the four-adapter lifecycle appliance, run-lifecycle-test.ps1 assigns explicit VMware PCI slots so Photon enumerates management as eth0, site A as eth1, trunk as eth2, and site B as eth3. Without those slots, Workstation can enumerate the isolated site B adapter as eth0; the management DHCP configuration then binds to the wrong network and lifecycle startup cannot reach the appliance. The management-only test VM path retains its existing slot layout.

Recover a retained builder address

A failed image build can finish process cleanup while Workstation still holds its stopped VM tab open. The image wrapper saves its exact allocation and process-termination proof before attempting VMware deletion. Successful bounded image builds also save that proof before attempting address release. If release fails, the wrapper reports image build succeeded, builder-address cleanup failed, returns a failure for the incomplete cleanup, and retains the completed VM artifacts and exact recovery handoff. After closing that stopped tab and completing checked VM cleanup, use the standalone reservation command; a new build and a Windows restart are unnecessary when the saved proof is valid.

Use the exact handoff path reported by the build, under its admitted BuildStateRoot in vmware-builder-addresses/pending-releases. The default build state is .atlaso-local/photon-image-build-state:

$handoff = 'E:\task\.atlaso-local\photon-image-build-state\vmware-builder-addresses\pending-releases\builder-address-reservation-0123456789abcdef0123456789abcdef.json'
.\scripts\windows\vmware\manage-builder-reservation.ps1 -HandoffPath $handoff -Json
.\scripts\windows\vmware\manage-builder-reservation.ps1 -HandoffPath $handoff -Cleanup -WhatIf
.\scripts\windows\vmware\manage-builder-reservation.ps1 -HandoffPath $handoff -Cleanup

The default and -WhatIf verify without changing reservation or handoff contents. -Cleanup rechecks the exact allocation under the shared allocator lock, releases only that record, and then removes its handoff. It does not retrieve credentials, launch a build, stop a VM, delete VM artifacts, or change DHCP. -ReservationStateRoot selects an explicitly configured existing shared ledger; omit it to use the ordinary allocator location. -VmrunPath selects the installed Workstation executable when it cannot be found in the standard installation directories.

JSON reports Id, Address, Status, Reason, and HandoffPath, plus the source checkout, VMX, owner process, and recorded/current boot identities. Exit code 0 means verification or release completed; 2 means recovery is blocked. A live owner/controller, a running VM, observed address use, changed allocation, or unreadable evidence keeps the reservation. already-released means ledger removal previously completed and cleanup can finish handoff retirement. An interrupted cleanup can be retried with the same handoff.

Recovery distinguishes a running VM reporting the address, non-stale Windows neighbor evidence, stale-only cache evidence, and unavailable provider observations. A Stale entry proves neither current use nor ownership by the stopped builder. Once process-tree termination and exact-VM inactivity are proven, stale-only evidence permits release of the completed builder's reservation. It does not make the address eligible for another build: automatic allocation still skips it, and an explicit request for it fails while the cache entry remains. Release does not flush the neighbor cache or claim that its MAC belongs to the builder. Current host-interface assignments, running-VM address use, non-stale neighbors, and unavailable provider evidence still block release.

For an already retained handoff, let the original controller exit before retrying the read-only command. When the build was launched from an interactive PowerShell session, that session is the controller: after the build returns, close that session and run recovery from a new session. A valid receipt then permits same-boot recovery without waiting for stale-only cache entries to disappear. Verify releasable before requesting -Cleanup; do not edit reservation JSON. The recovery command retains its active-controller and fresh provider checks.

Older handoffs have no retained process-termination receipt. The tool reports that limitation instead of creating proof from VM-file absence or a dead PID. Such records still require independently available termination evidence or a changed host boot, followed by the same provider and address checks. Do not edit ledger JSON or insert receipts manually. The recovery command never restarts Windows automatically.

When the original schema-3 staging marker still exists, run the identity-bound cleanup-only procedure first. Its authoritative named-job recovery can preserve proof for the exact original allocation on the same boot. Then complete checked VM cleanup and inspect the reported handoff with manage-builder-reservation.ps1 before requesting its -Cleanup operation. Close the recovery controller session before reservation recovery when using an interactive PowerShell session. Provider activity and current address use still block release independently of that receipt. Markers already removed, legacy ownership, PID reuse, and conflicting identities cannot be reconstructed from current filesystem state; preserve those allocations for their existing supported recovery path.

A changed host boot supplies process-termination evidence independently of the saved receipt. Recovery therefore does not depend on an old receipt's timestamp after a restart; clock corrections cannot strand that allocation. Exact allocation identity, owner inactivity, provider state, and address-use checks still apply.