macOS VM plugin (prototype)
SwiftVM is a prototype plugin for testing inside disposable macOS virtual machines. A sealed macOS template is cloned for each run, the run works inside the clone, and the clone is removed after the virtual machine has confirmed it stopped. Host screenshots and input use the virtual machine’s own display. Native app journeys use Accessibility and Screen Recording inside the guest, granted to the guest agent during template provisioning.
One provider, SwiftVM (flow alias vm), covers two pages: this page describes
the virtual machines, and Swift tests and coverage
describes running Swift Testing suites and measuring coverage inside them.
To drive a native app on your own desktop through Accessibility instead, see
MacOSApp, which is also not yet released.
SwiftVM is a source-installed prototype. It is not bundled with the public CLI
and has no public release. Installation requires access to its private source
repository and the pinned build tools. Its vm.guest_run command has run native
app journeys through reflow apply; see availability.
Three things to know first:
- It needs a specific host: an Apple Silicon Mac on physical hardware running macOS 27, with Command Line Tools 27.0 (Swift 6.4), about 100 GB of free disk and the Virtualization entitlement. See host requirements.
- Nothing appears on your desktop by default. Runs and provisioning use an offscreen display, and a required check fails the run if any once-a-second sample finds the tool’s own window on screen. A window appears only when you ask for one. See display, screenshots and clicks.
- The installed master is never booted. Provisioning works on a clone of the master, and every command that would boot, provision, seal, verify or run the master refuses it. See templates.
Host requirements
Section titled “Host requirements”These describe the hosts the source-installed prototype has been built and tested for.
- An Apple Silicon Mac on physical hardware. Nested macOS virtualization is not supported, so a macOS virtual machine cannot host these guests.
- macOS 27 on the host. Unattended provisioning uses the macOS 27
VZMacGuestProvisioningOptionsAPI and is refused on an older host before anything is written. Guests must run macOS 27 or later. - Command Line Tools 27.0 on the host (Swift 6.4, SDK 27.0); provisioning installs its own copy in the guest.
- The host tool signed with the
com.apple.security.virtualizationentitlement. Its build script signs it ad hoc; every virtual machine command refuses without it. - About 100 GB of free disk for first-time setup: a 26.6 GB restore image, a 26 GB never-booted master and a 34 GB provisioned copy. Installation stops below 35 GB free, provisioning needs 40 GB, and a run warns below 40 GB. Each run’s clone is an APFS copy-on-write clone on the template’s volume and grows with what the guest writes.
- A logged-in graphical (Aqua) session on the host. The offscreen display is a
window that is never shown, but it still needs the window server, so
provisioning, runs with an offscreen or visible display, and every screenshot
and click are refused without one. SSH-only and launchd daemon contexts are
not supported. The experimental
headlessdisplay mode is not refused up front, but it has run only from a session that still had window-server access, never over SSH or as a LaunchDaemon; keep it out of CI. - For provisioning only, direct outbound HTTPS from the guest to Apple’s
software update servers (
swscan.apple.com,swcdn.apple.com), with no proxy. The guest downloads Command Line Tools there (several GB). The host probes both before the first boot and refuses if they cannot be reached. There is no offline option. Runs themselves need no network.
svm doctor checks these: macOS 27, Apple Silicon not virtualized,
Virtualization support, the entitlement, a graphical session, Command Line
Tools 27 with Swift 6.4, and free space. It exits 1 when any check fails. A
locked screen, a sleeping display, a session that is not on the console, a
newer unproven toolchain or unreachable Apple software update servers are
warnings only.
Unattended hosts
Section titled “Unattended hosts”A CI host needs more than a logged-in session:
- A dedicated Mac whose user logs in automatically after every reboot. FileVault prevents automatic login, so a FileVault Mac waits at the unlock screen and runs are refused until someone logs in.
- The CI runner started as a LaunchAgent in that user’s graphical session, not as a LaunchDaemon and not over SSH.
- Sleep disabled (for example with
pmset) and the session kept unlocked.
While it installs, provisions or runs, the prototype prevents idle system sleep, and idle display sleep when a display view exists. It cannot wake a sleeping display or unlock a locked screen.
Availability
Section titled “Availability”SwiftVM is absent from the public CLI’s bundled catalog. With authorized access to
Resilient-Software/reflow-plugin-swift-vm and its private SDK dependency, it can
be built and installed from a pinned source catalog. Catalog commit
0ca0cf2f9e683569841fd2218bd9a6ba80119e79 selects provider source
d78ca9ddfd178ea5867dbc9140e6f4c7f1140787.
On the required Apple Silicon host, prepare the source repository’s pinned Go 1.26.1, Swift 6.4 / Command Line Tools 27.0 and verified dependency cache as its README describes. Warm the Swift helper cache before initialization:
node /absolute/path/to/reflow-plugin-swift-vm/plugins/reflow-provider-swift-vm/build.mjs --warmreflow init --collection <collection-id> \ --plugin-source /absolute/path/to/reflow-plugin-swift-vm \ --plugin-catalog-commit 0ca0cf2f9e683569841fd2218bd9a6ba80119e79 \ --trust-plugin-source SwiftVM--trust-plugin-source acknowledges that trusted source runs as your user;
SwiftVM is the positional plugin name. Each device needs its own checkout with
the selected commits. With --upgrade, include every plugin the application
needs: that flag replaces its selection. This is private source installation,
not installation from a public registry.
Template installation, provisioning and sealing use the source repository’s
svm host tool. The Reflow provider consumes an already sealed template; it does
not provision one. vm.guest_run additionally needs a template provisioned with
--tcc agent, whose guest agent holds Accessibility and Screen Recording.
Reflow commands
Section titled “Reflow commands”| Command | Effect | What it does |
|---|---|---|
vm.doctor | observe | Reports macOS version, Virtualization.framework support and maximum CPU count without booting a VM. |
vm.template_verify | observe | Rechecks lifecycle admission, current seal and full-content hashes without writes. An installed master is refused with TEMPLATE_MASTER. |
vm.guest_run | mutate | Runs staged jobs in a fresh off-screen, network-less clone, exports workspace out/ with host re-hash, then confirms stop and removes the clone. |
The observations launch no guest jobs and retain observation.json. An admitted
template has passed seal and byte checks; admission alone does not establish
boot, enrollment, permissions or successful tests.
Declare vm: SwiftVM in the flow’s providers. Its private binding has four
absolute paths: templateBundle, runsRoot, exportRoot and stageRoot.
doctor accepts an empty binding, template_verify needs templateBundle, and
guest_run needs all four. Run and export roots share the template’s APFS volume;
the stage root holds inputs as sha256/<digest>.
guest_run accepts inputs (names and SHA-256 digests), sequential jobs
(argv, optional cwd/env/timeout and always-run cleanup), optional JSON files,
a new exportName and a whole-call timeoutMs. Inputs are rehashed before boot.
It retains job receipts, export hashes and attached PNG/JSON evidence; an unknown
job is never replayed. A nonzero job exit fails the step, and unconfirmed cleanup
can never pass. The source repository’s generated guest_run command guide
defines the full schema and limits.
For native UI journeys, stage the separate MacOSApp guest executor and your app by digest. The executor runs inside the guest agent’s permission chain, checks both grants in every clone, drives the app and records accessibility trees, window captures and cleanup. This does not install the desktop MacOSApp provider; see guest execution.
How the prototype works
Section titled “How the prototype works”Templates
Section titled “Templates”A template goes through install, copy, provision, seal and verify:
- Install.
svm template install --ipsw <file> --bundle <master>creates the master from an Apple restore image with the publicVZMacOSInstaller(about 4 minutes and 26 GB on one host). It exits 0 when complete, 4 when cancelled, 5 when the outcome is unknown, 2 when it failed and 1 on a setup error. A partial install is never retried or overwritten. - Master. The master is never booted, provisioned, sealed, verified or
run. Install marks it three ways: a
MASTER.jsoninside it, an install journal beside it, and an entry in the host’s master registry (~/Library/Application Support/reflow-svm/masters.json). The registry matches the directory’s inode and birth time, so a renamed master, or one moved on the same volume, stays refused. Acp -c -Rcopy gets a new inode and is accepted as a copy. Do not count on moving a master to another volume to make it usable: inode numbers are unique only within a volume, so a directory there with a matching inode is refused, and a master moved together with its install journal stays refused. When a registry entry refuses a directory, keep the entry and use a copy, as the refusal says. Provisioning, seal, verify, runs, the console (even with--allow-first-boot) andvm.template_verifyall refuse it.svm template mark-master --bundle <master>lists a master that predates these markers before you rename or move it; it boots nothing and writes nothing into the master. - Copy and provision.
svm template provision --from <master> --bundle <new> --guest-package <dir>first makes an APFS clone of the master on the same volume, without writing anything into the master, then provisions that copy.<dir>is the guest package: the guest agent app, its LaunchAgent and installer with their SHA-256 sums, made by the prototype’s packaging script into a new directory. You can also make the copy yourself withcp -c -R <master> <new>and omit--from. Provisioning boots the copy for the first time withVZMacGuestProvisioningOptions: a user with a random password, automatic login and remote login. It waits until the guest is ready, disables password SSH and proves it is refused, installs Command Line Tools and the guest agent, grants its Accessibility and Screen Recording permissions through System Settings on the VM display (--tcc agent, the default), then disables the SSH server, so clones have no port 22 listener.--tcc noneskips the grants and cannot produce aguest_run-ready template. Provisioning succeeds only when the guest confirms it has stopped; otherwise the host forces a stop and the copy can never be sealed. Observed provisioning took 5 to 31 minutes on one host, mostly the Command Line Tools download. - Seal.
svm template sealrecords a full-content SHA-256 manifest of a provisioned template. - Verify.
svm template verifyand every clone refuse a template unless its history is safe, its install is complete, and its manifest is the current seal with no boot after it.
Provisioning ends with one of these exit codes:
| Exit | Meaning |
|---|---|
| 0 | Provisioned. |
| 1 | Refused before the first boot; the copy is still unbooted. |
| 128 + signal | Interrupted (SIGINT, SIGTERM or SIGHUP) before the first boot; the preparation is undone and the copy is still unbooted. |
| 2 | Booted and stopped cleanly, but not provisioned. |
| 3 | The stop was not confirmed, including an interrupt before the guest was ready or a second interrupt. |
After exit 2 or 3 the copy cannot be reused: start again from a new copy of the
master. When --from made the copy, an interrupt or refusal before the first
boot removes that copy and leaves the master untouched.
Refusals print a route that works when followed, such as a cp -c -R copy or
--from, and never ask you to remove a registry entry: an entry that refuses a
directory is protecting the master it records. If the registry cannot be read,
every template is refused until its JSON is repaired or restored from a backup;
deleting, emptying or moving it aside would release every master it lists.
History is fail-closed. A boot without a confirmed guest stop, a power-off from
the host, or torn or unknown history makes the template permanently unusable for
sealing and cloning. History is never repaired automatically; build a fresh
template from the master instead. svm template console boots a provisioned
template for manual inspection only. Any console boot means the template must
be sealed again before the next verify or run. End the session with a shutdown
inside the guest: a power-off from the host makes the template unusable for
good. Closing the console window or pressing Ctrl-C only asks the guest to
stop, and logged-in guests ignore that request, so the host powers the virtual
machine off after a 120-second grace period.
Runs and cleanup
Section titled “Runs and cleanup”svm run --bundle <template> --runs <dir> --fixture <dir> gives each run its
own clone with a fresh machine identifier and MAC address, a read-only shared
folder for its inputs, and no network device. --network adds NAT networking,
through which the guest can also reach services listening on the host, the
local network and the internet; no current scenario needs it. Before the boot,
the template and its clone are re-hashed against the seal,
and runs on one template wait for each other.
A stop is confirmed only when the guest reports it has stopped, or a forced stop completes with the machine stopped. The clone is removed only after a confirmed stop; otherwise it is kept with recovery notes. The template is never modified.
The prototype host tool ends each run with a verdict and an exit code:
| Exit | Verdict |
|---|---|
| 0 | Passed, with cleanup confirmed. |
| 1 | Setup error before the virtual machine started. |
| 2 | A required check failed. |
| 3 | Cleanup or the report could not be confirmed. |
| 4 | Incomplete: nothing failed, but something is unproved. |
| 64 | Usage error. |
| 130, 143, 129 | Interrupted (SIGINT, SIGTERM, SIGHUP), with cleanup confirmed. A dropped terminal’s SIGHUP is handled like SIGTERM. |
The default selection, intended as the CI gate, runs the passing test suite and
the GUI proof below (the passing suite only with --display headless). An svm fixture run that requests coverage never passes: at best it
ends incomplete (exit 4); see Swift tests and coverage.
Display, screenshots and clicks
Section titled “Display, screenshots and clicks”--display selects one of three modes:
| Mode | Behavior |
|---|---|
offscreen | Default for runs and provisioning. A public VZVirtualMachineView in a borderless window that is never shown, under an app with no Dock icon that never activates. Nothing appears on the desktop. |
headless | No view: no screenshots, no input and no GUI checks. Experimental. |
window | A visible titled window. Used only when you ask for it: --display window, svm run --console to log in to a run’s clone by hand, or svm template console, whose default is a window because it is for manual inspection. |
Screenshots come from the virtual machine view’s own framebuffer. They need no
ScreenCaptureKit and no Screen Recording permission. The view is public API, but
reading its frames this way depends on undocumented behavior of
VZVirtualMachineView, so a black or missing frame is recorded as such and
never counted as success. Each run keeps full-resolution PNGs and an evidence.json with their
SHA-256 hashes, dimensions, capture method, black-frame flags and the host’s lock
and display-sleep state.
Clicks are mouse events delivered to the hidden view on the host, so the guest needs no Accessibility or Screen Recording permission. Key events are sent the same way; provisioning verifies typed text and Return in the guest. Modifier-key behavior is not fully proven. Positions map from the guest’s screen points by fraction of the guest-reported screen size.
With the offscreen display, the required host-window-hidden check reads the
host’s window list every second, from the moment the display exists until the
virtual machine stops, and looks for the tool’s own windows. The display is
not hidden, and the check fails, when any sample shows one of those windows on
screen, when the window list cannot be read, when there are no samples, when
the tool is ever a regular or active app, or when its app state cannot be read
on every sample, or on more than one sample and more than 10% of them. Provisioning samples the same way
from its first preparation step and fails with exit 2 when the display was
not hidden.
The GUI proof launches a sample AppKit counter app in the guest. Besides its label and buttons, the app draws a count marker: a 12 by 2 grid of black and white cells that encodes the current count, built so that a blank or uniform area never decodes as a number. The app reports where its window, label, marker and buttons are; that layout only tells the host where to look. The host takes a screenshot, waits a second and takes a control screenshot without any input, clicks the app’s increment button, then takes a screenshot after the click.
The host then re-reads those screenshots and decodes the marker in each one
itself. The proof requires the count to read 0 before the click, 0 in the
control screenshot and 1 after it. A marker that cannot be decoded, for example
because the window closed or something covers it, leaves the proof unproved,
not passed; any other count fails it. The label must also stay essentially
unchanged in the control pair and change after the click, and the change must be local: almost
nothing may change outside the app window, or inside the window outside the
label, marker and buttons. An ignored click that happens to coincide with an
unrelated repaint therefore fails. A second required check,
gui-app-after-input, confirms the app is still running as the same process
after the click.
Recorded evidence
Section titled “Recorded evidence”On one Apple Silicon host with a macOS 27.0.1 guest, on 2026-10-15:
- From earlier builds of the prototype: the default run (passing suite and GUI proof, offscreen display) passed with exit 0 in 226 seconds while the host screen was locked and the main display asleep, and one headless run of the passing suite passed in 194 seconds from a session that still had window-server access. Working with a locked screen or sleeping display is not guaranteed; keep CI sessions unlocked and awake.
- From a later build, on a freshly provisioned template: provisioning took about 6 minutes and the default run passed with exit 0 in 202 seconds while the host was heavily loaded. Interrupting a run with SIGINT ended it with exit 130 and confirmed cleanup.
- From a build shortly before the current source, on an APFS copy of that
template: the default run passed with exit 0 in 187 seconds, with cleanup
confirmed. The GUI proof decoded the count as 0 before the click, 0 in the
control screenshot and 1 after it, and every required check passed,
including
host-window-hidden(121 samples, none on screen or active) andgui-app-after-input. The host screen was locked and the main display asleep at the start and end of the run.
These earlier svm fixture runs are separate from Reflow guest execution.
On 2026-10-17, a source-installed SwiftVM provider ran Gigahog journeys through
reflow apply in fresh off-screen clones of a TCC-ready macOS 27 template:
J01 and J03 passed with captures and confirmed cleanup; J11-T2 failed on its own
oracle with retained evidence. Exported coverage was ingested on the host.
Subsequent QA runs use the same guest execution path. This establishes a working
source-installed workflow on one host, not a bundled release or general platform
qualification. Runtime and artifacts depend on the selected source, template,
executor and app inputs.
Not yet available
Section titled “Not yet available”- A public SwiftVM release or installation without private source access.
- A complete exact-build coverage gate for
svmfixture runs. Their diagnostic coverage verdict remains incomplete; generic guest-job export hashes do not establish a fixture coverage policy. - XCTest,
.xcresultbundles and XCTest coverage: the guest has Command Line Tools, not Xcode. See Swift tests and coverage. - Headless mode as a supported configuration: it is experimental, so keep it out of CI.
- An offline or proxied Command Line Tools installation during provisioning.
Limits
Section titled “Limits”- Untrusted job code is not supported. Jobs run as the guest’s admin user, so a job can replace the guest agent and fabricate its results. Host re-hashing proves only that artifacts were transferred intact.
- A provisioned template and every clone of it hold a shared admin credential for automatic login. Keep templates on the host that made them.
- Host display input has a narrower contract than the MacOSApp guest executor; do not infer support for arbitrary desktop automation or every system key.
- Every clone shows a persistent “App Background Activity” notification for the guest agent. A notification that appears or is dismissed between the control screenshot and the screenshot after the click fails the GUI proof. This can fail a correct run but cannot make a wrong one pass.
- Not yet measured: the frame rate while the display is hidden.
- If the host process is killed or crashes, the clone is not cleaned up: it is left behind without a report, and the virtual machine’s behavior after that is unverified.
- The prototype uses only public APIs: no private Virtualization.framework input, VNC or NVRAM access, no TCC database edits and no SIP changes. Guest permissions are granted through System Settings during provisioning. Screenshot capture still depends on undocumented behavior of the public view (see display, screenshots and clicks).