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:
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:
The VMware build, normal-test-VM, deployment, and export entry points use this authentication precedence:
- Explicit
-OnePasswordServiceAccountTokenFileDPAPI ciphertext. - Explicit
-OnePasswordAccountdesktop authorization. - The checkout-local DPAPI token file when present.
- 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.
- Resolve the primary checkout using
git rev-parse --path-format=absolute --git-common-dirandgit 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 namedmainor a conventional path. Use the already verified task worktree beneath the configured Codexgit-worktree-rootas destination. Stop for maintainer direction if either identity, repository relationship, user/host binding, or permitted root is ambiguous. - Inspect only these two named paths beneath each checkout's
.atlaso-localdirectory:onepassword-environment-idandonepassword-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 withgit 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. - Import
scripts/windows/vmware/Atlaso.OnePasswordCredentials.psm1from the verified task checkout. For each selector being reused, captureResolve-AtlasoOnePasswordEnvironmentId -RepositoryRoot <verified-checkout>into a local variable and pass it directly toAssert-AtlasoOnePasswordEnvironmentId, retaining the built-in identity pin. Never emit that variable. For each token, capture or discard the result ofResolve-AtlasoOnePasswordServiceAccountTokenFile -RepositoryRoot <verified-checkout>with its exact-TokenFilepath so a missing file fails explicitly. This enforces containment and invokesAssert-AtlasoOnePasswordServiceAccountTokenFilefor 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. - 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. - 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
AtlasoEnvironment before VMware or network mutation. Decryption remains inside that child; do not decrypt for display or run a separate plaintext probe. - 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.
- 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.
- 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.
- 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. - 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
- Retain the returned
provider_id, receipt digest, task/PR binding,registration_absent, andadapter_references_absentinvalidation_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:
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:
VMnet8for management, with the appliance address assigned by DHCP by defaultVMnet2for SiteAVMnet3for WAN/SiteBVMnet4for trunk-like validation
Check the current Workstation host network inventory with:
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.jsonon the internal listener; - guest-local nginx
/,/ui/management,/ui/public, and/openapi.jsonon every applied IPv4/IPv6 browser listener; - host-facing
/, canonical management/public UI paths, and/openapi.jsonon 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:
validation_resources_releasedremote_branch_absentworktree_removedtask_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.