Windows Support
Intendant builds and runs on Windows. This page covers the supported target, how to set up a development machine, the per-OS backend architecture, and the known limitations of the Windows port.
Maturity. The Windows backends compile and link cleanly for
x86_64-pc-windows-msvcand mirror the structure of the X11/macOS backends. The display pipeline has been live-validated end-to-end on a real Windows host: GDIBitBltcapture, Media Foundation H.264 encode,SendInputkeyboard/mouse injection, and the encrypted WebRTC transport all work with real remote clients (display and input, over WebRTC). The voice audio bridge (ffmpeg/DirectShow + ffplay/VB-CABLE) is the one runtime path still pending end-to-end validation. Remaining gaps are called out under Known Limitations — the port never panics or silently no-ops; unsupported paths return a clear error.
Supported Target
| Target triple | x86_64-pc-windows-msvc |
| ABI | MSVC (not the GNU ABI) |
| Minimum OS | Windows 10 / Windows 11 (client), Windows Server 2019+ |
The MSVC ABI is required: the Windows-only crates (windows, windows-sys,
arboard, clipboard-win) link against the Microsoft C runtime and the
Windows SDK import libraries. The x86_64-pc-windows-gnu target is not
supported.
Quickstart
The fastest path is the release-pinned installer — the Windows counterpart of
the curl | sh one-liner, from an elevated PowerShell on a fresh box:
& ([scriptblock]::Create((irm https://github.com/intendant-dev/Intendant/releases/latest/download/install.ps1)))
# Keep it running unattended (Task Scheduler entry — at boot when elevated,
# at logon otherwise — supervised by the built-in restart loop; the one-time
# claim code lands in the service log the installer prints):
& ([scriptblock]::Create((irm https://github.com/intendant-dev/Intendant/releases/latest/download/install.ps1))) -Service
It clones the repo, runs the dependency setup below (elevated shells only),
builds, and launches. The script is a per-release GitHub asset — stamped
with the release it installs, hash-committed to the public transparency
log, and fail-closed (RELEASE_PIN_MISMATCH) when the checkout doesn’t
match its release; a rendezvous serves at most a redirect to it. Add
-Connect https://intendant.dev (or your self-hosted rendezvous) to link
the daemon for discovery. intendant service install|uninstall|status manages the scheduled task directly once built.
The claim code links discovery metadata only and grants no daemon access.
Establish control from a trusted local or independently reached daemon-served
direct/mTLS surface. No signed/notarized native release exists for this alpha.
Hosted Connect remains discovery-only at immutable
role:none in the default build.
Working from an existing checkout instead, run the setup script from an elevated (Administrator) PowerShell in the repo root:
powershell -ExecutionPolicy Bypass -File .\scripts\setup-windows.ps1
# Read-only dependency check:
powershell -ExecutionPolicy Bypass -File .\scripts\setup-windows.ps1 -Check
By default, the script uses -PackageManager Auto: it uses an already-installed
winget first, then an already-installed Chocolatey. It does not install
Chocolatey by surprise. If you want the old one-command bootstrap behavior, make
that explicit:
powershell -ExecutionPolicy Bypass -File .\scripts\setup-windows.ps1 -PackageManager Chocolatey
Other useful modes:
# Verify/install dependencies but skip the cargo build:
powershell -ExecutionPolicy Bypass -File .\scripts\setup-windows.ps1 -NoBuild
# Never install package-managed dependencies; report manual fixes instead:
powershell -ExecutionPolicy Bypass -File .\scripts\setup-windows.ps1 -PackageManager None
# Skip the runtime hello smoke check after build:
powershell -ExecutionPolicy Bypass -File .\scripts\setup-windows.ps1 -SkipSmoke
After a successful build, the script verifies that both release binaries exist,
runs a small intendant-runtime hello-command smoke check, and provisions the
managed Chrome for Testing browser used by CDP browser workspaces. It then
prints a status summary split into build, web/display, voice, and LAN readiness.
Package Manager Policy
| Policy | Behavior |
|---|---|
Auto | Use existing winget; else existing Chocolatey; else continue without package-managed installs and fail with manual instructions for missing deps. |
Winget | Require winget; install packages through the Windows Package Manager. |
Chocolatey | Install Chocolatey if missing, then install packages through Chocolatey. |
None | Do not install package-managed dependencies; required build deps must already be present or the script reports manual fixes. |
-Check separates required-to-build dependencies from optional/runtime
ones and sets its exit code accordingly: it exits nonzero if any
required build dependency is missing or unusable, and 0 when they are all
present — so CI and automation can gate on it. A missing optional dependency
(wasm-pack, ffmpeg/ffplay, VB-CABLE) is reported but never fails the
check.
What Gets Installed
Required to build (a missing/unusable one fails -Check):
- rustup with the default host set to
x86_64-pc-windows-msvc - Visual Studio 2022 Build Tools with the C++ workload
(
Microsoft.VisualStudio.Workload.VCTools; Chocolatey packagevisualstudio2022-workload-vctools) — providescl.exe,link.exe, and the Windows SDK. Required even forcargo check. - NASM — required to assemble the
ringcrypto crate on windows-msvc. The installer commonly places it inC:\Program Files\NASMand amends the machinePATH, which the current shell may not yet see; the script detects that case, adds the install directory to the current processPATH, and re-probes so-Checkrecognizes NASM even when it is not on the currentPATH. Install mode also persists that PATH repair for future shells. - git
Optional / runtime / manual (reported, but never fail -Check):
- wasm-pack (optional) — for rebuilding the browser WASM crates
(
presence-web,station-web) locally. Only the version pinned in.wasm-pack-versionwill rebuild (build.rsskips any other), and regenerating the committedstatic/wasm-*artifacts is a macOS-only step — WASM output is not byte-deterministic across host triples. - ffmpeg — the voice audio bridge shells out to
ffmpeg/ffplay. - Media Foundation — built into Windows client SKUs; on Windows Server the
script enables the
Server-Media-Foundationfeature. - VB-CABLE — manual, voice-only virtual audio cable setup. The script cannot install the vendor driver.
It then runs cargo build --release --target x86_64-pc-windows-msvc, producing:
target\x86_64-pc-windows-msvc\release\intendant.exetarget\x86_64-pc-windows-msvc\release\intendant-runtime.exe
One step the script cannot automate is the VB-CABLE virtual audio cable (the vendor ships a manual installer, not a package). The script prints instructions and flags it in the final summary, but voice remains optional and the Windows voice bridge is still pending end-to-end validation. See Audio below.
Manual build, if you already have the toolchain:
rustup set default-host x86_64-pc-windows-msvc
cargo build --release --target x86_64-pc-windows-msvc
Provide an API key via a .env file or environment variables exactly as on the
other platforms (see Getting Started), then:
.\target\x86_64-pc-windows-msvc\release\intendant.exe "your task here"
.\target\x86_64-pc-windows-msvc\release\intendant.exe --web
Per-OS Backend Architecture
Intendant prefers platform-agnostic code; where the OS forces a difference, the
Windows implementation slots in behind the same trait or cfg gate the X11,
Wayland, and macOS backends use. The Windows-specific backends are:
Capture — GDI BitBlt (default) + DXGI Desktop Duplication (opt-in)
crates/intendant-display/src/windows.rs ships two capture paths behind the
same DisplayBackend seam, selected at runtime by the
INTENDANT_WINDOWS_CAPTURE environment variable (gdi | dxgi,
case-insensitive; anything unset or unrecognized uses the GDI default).
GDI BitBlt — the default. BitBlt from the screen device context reads
the DWM-composed desktop — the same pixels a user sees. Crucially it works
on every display adapter, including the virtual / indirect displays an
always-on host commonly runs on (RDP indirect display, cloud virtual display,
headless). The capture loop runs on a dedicated std::thread (GDI HDC /
HBITMAP handles are not Send) and BitBlts the screen DC into a cached
top-down 32-bit DIB (SRCCOPY | CAPTUREBLT, so layered/overlay windows are
included). The DIB is BGRA8 top-down, so emitted rows are the identical
DXGI_FORMAT_B8G8R8A8_UNORM byte layout the DXGI path produces and feed the
existing bgra_to_i420 / Media Foundation H.264 encoder unchanged.
DXGI Desktop Duplication — opt-in fast path. IDXGIOutputDuplication is the
GPU-accelerated path (zero-copy from the GPU into a CPU-readable staging
texture, lowest overhead on physical hardware). It is retained as an opt-in
fast path (INTENDANT_WINDOWS_CAPTURE=dxgi) for hosts with a real GPU/scanout.
It is not the default because it captures all-black frames on
virtual / RDP / cloud / headless adapters: those displays don’t perform the
real frame presentation/scanout that Desktop Duplication requires, so it
“succeeds” yet duplicates black. Like the GDI path, the duplication interface,
the Direct3D 11 device, and the device context are single-threaded COM objects
that are not Send across await, so the loop runs on a dedicated
std::thread and feeds the tokio runtime over a bounded mpsc channel (the
same drop-on-full backpressure policy as the macOS and X11 backends).
DXGI_ERROR_ACCESS_LOST (resolution change, full-screen exclusive app,
secure-desktop/UAC transition, GPU mode switch) tears down and re-acquires the
duplication on the next iteration.
Input — SendInput
Keyboard and mouse injection uses the Win32 SendInput API. Keyboard events
carry a Win32 virtual-key code (mapped in
crates/intendant-display/src/windows_keymap.rs) plus the
KEYEVENTF_EXTENDEDKEY flag for keys in the extended block. Mouse moves use
MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK, scaling the browser’s
normalized 0.0..1.0 coordinates into the 0..65535 absolute coordinate space
that spans the entire virtual desktop.
Clipboard — arboard
Bidirectional clipboard sync uses the arboard crate (which wraps the Win32
clipboard API via clipboard-win), providing text and RGBA image get/set. This
is the one platform where the clipboard is handled in-process: the macOS and
Linux arms shell out to pbcopy / wl-copy / xclip.
Encode — Media Foundation H264
Windows video encode targets the synchronous software Media Foundation H264
MFT rather than VP8/libvpx; the backend deliberately does not enumerate
hardware MFTs or NVENC. It is live-validated over the encrypted WebRTC
transport with real clients. The libvpx-backed VP8 encoder is
gated off Windows in Cargo.toml (cfg(not(target_os = "windows"))), so the
MSVC build never tries to compile the env-libvpx-sys C-FFI crate (which needs a
C toolchain plus the vpx headers); the VP8 code paths are themselves cfg’d off
Windows. (The former OpenSSL C-FFI dependency is gone entirely — the access cert
subsystem is now pure-Rust via rcgen + p12-keystore — so nothing OpenSSL
needs gating on any platform.)
Because VP8 is unavailable, H.264 is also the always-on baseline codec in the
shared encoder pool on Windows (where macOS/Linux use VP8 simulcast as the
baseline) — a single full-resolution H.264 layer the Media Foundation encoder
serves, rather than a simulcast set. A baseline construction failure on Windows
degrades gracefully (the Video tab reports no stream; the rest of the dashboard
stays up) instead of panicking, because the pool is built eagerly at --web
startup. See Display Pipeline for the
pool architecture.
Audio — ffmpeg + VB-CABLE bridge
There is no PulseAudio (Linux) or CoreAudio/BlackHole (macOS) on Windows. The
voice audio bridge instead routes through VB-CABLE, a virtual audio cable,
shelling out to ffmpeg/ffplay to move audio in and out. ffmpeg
captures the CABLE Output (VB-Audio Virtual Cable) recording endpoint through
DirectShow; ffplay consumes raw model PCM on stdin and plays through the
system default output. VB-CABLE is the Windows analogue of BlackHole on macOS /
a PulseAudio null sink on Linux: install it, then set
CABLE Input (VB-Audio Virtual Cable) as the default playback device.
Per-app routing and automatic default-device save/restore are not available.
Process and Network Introspection
crates/intendant-platform/src/platform.rs provides the Windows
implementations of the cross-platform process and network helpers:
- Process liveness —
OpenProcess(query-only) +GetExitCodeProcess, since Windows has nokill(pid, 0)equivalent. - Process command line —
NtQueryInformationProcesswith theProcessCommandLineInformationclass, falling back toQueryFullProcessImageNameW(executable path only) when the full command line is unavailable. - Routable local addresses — the safe cross-platform
if-addrscrate backsintendant_core::net::routable_local_addrs, which feeds the web-gateway advertise URLs and WebRTC ICE host-candidate gathering. On Windows the crate wrapsGetAdaptersAddresses.
Known Limitations
These are tracked deferrals, not bugs. Each degrades with a clear error rather than a panic or silent no-op.
- Interactive desktop session required. Capture (
BitBltand DXGI),SendInput, and the ffmpeg/VB-CABLE audio bridge all need an interactive desktop session. They do not work on the headless / service / disconnected-RDP “Session 0” desktop. Within an interactive session, frame delivery, H.264 encode, input injection, and the encrypted WebRTC transport are live-validated; only the voice audio bridge is still pending end-to-end validation (see below). - Voice audio bridge pending validation. The ffmpeg/DirectShow + ffplay/VB-CABLE bridge is wired up but has not yet been validated end-to-end on a Windows host. It also requires the manual VB-CABLE install (see Audio).
- Remote
serve-certsenrollment remains Unix-only.intendant access setup,recert,list, andremoveare supported on Windows. Setup uses the same pure-Rust CA/server/client generation and owner-IAM seeding as Unix, then prints exact PowerShell commands to import the CA intoCert:\CurrentUser\Rootand the client identity intoCert:\CurrentUser\My; it does not start a server.serve-certsreturns an explicit unsupported/manual-import error. Remote phone/browser enrollment can be run from a macOS/Linux anchor. A generic HTTPS reverse proxy is not an authentication substitute: if it terminates into daemon loopback it becomes a root trust boundary and must enforce approved client identity plus a protected upstream. See Peer Federation for the full dashboard TLS/mTLS and federation auth story. - No virtual-display equivalent. There is no Windows analogue of Xvfb, so the lazily-launched virtual displays the Linux pipeline uses do not exist on Windows. Capture targets the real interactive desktop only.
- Filesystem sandboxing uses restricted tokens and is opt-in. Windows
defaults the runtime write sandbox off because stamping inheritable grant
ACEs through a large existing tree is synchronous and can make startup take
minutes;
--sandboxor[sandbox] enabled = trueaccepts that first-stamp cost. When enabled,--sandboxand scoped shells work on Windows, enforced byCreateRestrictedTokenplus temporary ACL entries for theRESTRICTEDSID (S-1-5-12) on the granted roots (win_sandbox.rs) — the Windows twin of Landlock (Linux) and Seatbelt (macOS). The runtime re-execs itself under aWRITE_RESTRICTEDtoken (reads open, writes confined); scoped shells run under a fully-restricted token (deny-by-default; system dirs readable viaBUILTIN\Users, devices viaEveryone). Both postures strip every token privilege exceptSeChangeNotifyPrivilege— otherwise elevated parents (e.g. OpenSSH admin sessions) would leakSeBackup/SeRestoreinto the sandbox, whose backup-intent opens (FindFirstFileenumerates directories that way) bypass DACLs entirely. Grant ACEs are refcounted and journaled to disk before stamping; a crashed daemon’s leftover grants are swept on the next start. Accepted surface:Users-writable OS spots (directory creation atC:\, parts ofProgramData) stay writable inside scoped shells — user profiles do not.
See Also
- Getting Started — building, the
.envfile, and run modes - Display Pipeline — capture/encode/WebRTC architecture and the encoder pool
- Peer Federation — dashboard TLS/mTLS,
--tls, and cross-machine display - Computer Use & Live Audio — input and voice