Parallels VM system-test control plane for macOS hosts. Test real installers, trust stores, network devices, and upgrade paths on pristine end-user guest VMs (Windows / Linux / macOS), with the operating discipline that makes that reliable:
- Golden-snapshot ladder —
golden(powered-off, human-made, pristine) →ready(booted once) →built(provisioned); every test starts from a revert, never from leftover state. - Internal-disk-only boot policy — archive copies on an external drive are clone sources, never boot targets; CI hard-fails rather than silently booting off slow storage.
- Guarded guest execution — host-side timeouts, guest straggler cleanup, revert-collapse recovery, one-VM-at-a-time.
- Cache-first provisioning — downloads happen once into an offline cache; the test path only reverts snapshots (metered-link safe).
- Encoded guest quirks — SYSTEM/UNC on Windows, tar.gz-push (+ keepalive) transport on macOS (shared folder is TCC-blocked under headless exec), headless-OS capability limits (docs/CAPABILITIES.md).
The division of labor is strict and documented: a human installs the OS + Parallels Guest Tools and takes one golden snapshot per VM (docs/HUMAN-SETUP.md); vmkit owns everything after. Every failure mode this harness has hit is cataloged with its guard in docs/FAILURES.md.
vmkit is internal and not distributed through Homebrew — it is not part of the Port Zero product and must not appear in the product's public tap. Clone and install:
git clone git@github.com:PortZeroNetwork/vmkit.git && cd vmkit
just install # -> /usr/local/bin/vmkit (override: just install ~/.local)
vmkit init-host # then edit ~/.config/vmkit/host.conf
vmkit doctor # validate the machine against the contractTo update: git pull && just install.
cd my-repo
vmkit init # writes ./vmkit.conf — define flavors there
vmkit test linux smoke # reset to "built", run one flavor on one platform
vmkit series lifecycle # every configured platform in series + summarySome guest config must survive the per-test reset — a toolchain, Homebrew,
a Windows Defender exclusion for unsigned/network-sourced installers. It can't
live in a flavor script (every test reverts to built first); it has to be
baked into the checkpoint itself. vmkit provision is that
reset → run-a-guest-script → re-checkpoint dance as one reusable primitive,
with "preserve the pristine baseline once" built in:
vmkit provision windows vmtest/scripts/windows-add-defender-exclusions.ps1 \
--checkpoint built --label defender # bakes the exclusion into "built"See docs/PROVISIONING.md for the full model (anchors, preservation, failure handling).
ensure_only enforces one-VM-at-a-time by stopping every other running VM.
That is right for a queue of harness runs and destructive for a human working
inside a guest: a CI job landing mid-session powers their VM off. A GitHub
concurrency: group serializes CI jobs against each other and knows nothing
about your local session.
vmkit hold is the missing half — a cooperative lock every implicit
VM-stopping path checks:
export VMKIT_HOLD_TOKEN=$(vmkit hold --print-token "debugging the installer" --vm windows)
vmkit hold # who has it, until when
vmkit unhold # releaseAuthorization is by token, never by VM name. A live hold blocks every
VM-stopping path for everybody; only a caller carrying VMKIT_HOLD_TOKEN from
the record gets through. Export it, or your own vmkit reset is refused by your
own hold. --vm is documentation of what the host is being used for, nothing
more.
That distinction is the whole point. An earlier version keyed on the VM name — holding a VM permitted work on that VM, so the holder could reset their own guest. It protected every VM except the one actually in use: a CI job targeting the same guest matched and reverted it mid-provision. Everyone on this host runs as the same unix user, so pid/user/VM-name cannot tell a session from a CI job; a token can.
Holds always expire (--ttl, default 4h) — a forgotten hold that wedged CI
until someone noticed would be worse than the failure this prevents.
VMKIT_IGNORE_HOLD=1 overrides, vmkit doctor surfaces an active one, and
vmkit hold --steal takes over.
For a long session on a machine that is also a CI runner, stop the runner service too: a hold makes the job fail with the reason, whereas an offline runner makes it queue until you're done.
Drive a running guest's interactive desktop from the host (no guest agent):
vmkit screenshot windows # PNG path printed on stdout
vmkit key windows win+r # combos: ctrl+c, alt+tab, enter, …
vmkit type windows "notepad"
vmkit key windows enter
vmkit mouse windows click # buttons + relative nudge onlyThis is a vision loop substrate (screenshot → decide → key/type). Absolute click-at-(x,y) and accessibility trees need a guest agent (planned next).
Run vmkit init-agents to generate .instructions/vmkit.md (VM inventory,
snapshot ladder, test flavors, archive drive) and wire it the same way
agent-toolbox instruction modules
do: one @.instructions/vmkit.md line in AGENTS.md, and CLAUDE.md as a
thin @AGENTS.md pointer. No marker blocks in the root agent files.
Flavor scripts live in your repo, run inside the guests, and speak a tiny
greppable protocol (PHASE=… ok=true|false|SKIP, final RESULT=PASS|FAIL|SKIP).
Copy the helpers from $(vmkit guest-lib)/assert.sh|.ps1 into your repo's
vmtest/scripts/lib/ and source them.
vmkit adopt # new/cleaned machine: clone archive VMs -> internal disk
vmkit sync macos # mirror an internal VM bundle out to its archive copy
vmkit list # VMs + snapshot laddersRun the self-hosted runner as a service (never interactive run.sh), give
the workflow a concurrency group that queues (never cancels) VM jobs, stage
CI-built artifacts where VMKIT_ARTIFACT_* points, and call
vmkit series <flavor>. Split triggers so PRs run a fast smoke and pushes run
the full pristine-machine suite.
- SPEC.md — v1 specification: runtime primitives + the Vmkitfile, written as acceptance criteria for the Rust port
- HUMAN-SETUP.md — the human/machine contract, new-machine bootstrap (including Apple Silicon arch migration)
- CAPABILITIES.md — what guest scripts can/can't do per OS
- FAILURES.md — failure catalog: every known failure class → its guard
- PROVISIONING.md — bake one-time guest OS config into a checkpoint (
vmkit provision)