Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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-msvc and mirror the structure of the X11/macOS backends. The display pipeline has been live-validated end-to-end on a real Windows host: GDI BitBlt capture, Media Foundation H.264 encode, SendInput keyboard/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 triplex86_64-pc-windows-msvc
ABIMSVC (not the GNU ABI)
Minimum OSWindows 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), installs the release’s prebuilt, sha256-verified intendant.exe + intendant-runtime.exe pair when the release publishes one (from v0.2.0-alpha.6 on; the source build remains the fallback, and -FromSource forces it — only that path needs the Rust toolchain), 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

PolicyBehavior
AutoUse existing winget; else existing Chocolatey; else continue without package-managed installs and fail with manual instructions for missing deps.
WingetRequire winget; install packages through the Windows Package Manager.
ChocolateyInstall Chocolatey if missing, then install packages through Chocolatey.
NoneDo 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 package visualstudio2022-workload-vctools) — provides cl.exe, link.exe, and the Windows SDK. Required even for cargo check.
  • NASM — required to assemble the ring crypto crate on windows-msvc. The installer commonly places it in C:\Program Files\NASM and amends the machine PATH, which the current shell may not yet see; the script detects that case, adds the install directory to the current process PATH, and re-probes so -Check recognizes NASM even when it is not on the current PATH. 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-version will rebuild (build.rs skips any other), and regenerating the committed static/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-Foundation feature.
  • 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.exe
  • target\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 livenessOpenProcess (query-only) + GetExitCodeProcess, since Windows has no kill(pid, 0) equivalent.
  • Process command lineNtQueryInformationProcess with the ProcessCommandLineInformation class, falling back to QueryFullProcessImageNameW (executable path only) when the full command line is unavailable.
  • Routable local addresses — the safe cross-platform if-addrs crate backs intendant_core::net::routable_local_addrs, which feeds the web-gateway advertise URLs and WebRTC ICE host-candidate gathering. On Windows the crate wraps GetAdaptersAddresses.

Self-Update Lane

The dashboard’s Daemon-update panel (Access → Daemons) exists on every OS — what differs per platform is what each channel can honestly do, and the daemon derives that from one declared release-asset table (RELEASE_ASSET_LANES in src/bin/caller/handover/update_lane.rs), never from a compile-time OS check:

  • Dev channel (build from main) works on a Windows source checkout, including the lock collision the naive flow hits: the cargo build’s final copy lands at target\release\intendant.exe — the running daemon’s own image, which Windows locks against write and delete. The produce job sets the running binary aside by rename (which Windows permits on a running image) so the fresh build lands at the vacated path, and restores it if the build fails. Unix needs none of this (the final step replaces the inode by unlink + relink) and is untouched.
  • Releases channel checks are honest data everywhere, and release install works on Windows: the asset table carries a windows-x86_64 row for the Intendant-<version>-windows-x86_64.zip the release lane publishes (both exes at their final sibling names at the zip root). An install whose running binary sits beside its intendant-runtime.exe sibling — the unzipped-release shape, outside any checkout — reads as the plain-binary consumer install, and its one-click install runs the same fail-closed verify chain as the macOS consumer lane: transparency-log ritual (inclusion proof, signed tree head, append-only pin), download with sha256 checked against the log, gpg --verify against the compiled-in release signing key (gnupg must be on PATH — Gpg4win or Git for Windows’ gpg; the lane refuses without it). The verified pair then lands beside the watched binary — runtime first, the watched daemon image last, through the same set-aside-then-rename dance the Dev channel uses (Windows permits renaming a running image, not overwriting it), so the update chip only fires on a complete pair — and the shipped watch/chip/swap lane (successor-exec on a CLI-launched daemon) performs the swap on its own explicit click, exactly as for a produced dev build. The zip publishes without Authenticode signing, so SmartScreen may interpose on a first manual run of a freshly downloaded exe; the PGP + transparency-log chain above is what the update lane trusts. Release install still refuses on Linux by name — “no Linux release assets are published yet — rebuild from source on this platform” — because the release lane publishes standalone Linux worker binaries, not daemon zips. The refusals, the panel’s availability catalog, and the chip’s one_click fact all derive from the asset table, so publishing daemon assets for a platform ages its refusals out by adding that platform’s table row — the platform-generic plain-binary install arm lights up with the row; there is no separate gate to update.

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 (BitBlt and 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-certs enrollment remains Unix-only. intendant access setup, recert, list, and remove are 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 into Cert:\CurrentUser\Root and the client identity into Cert:\CurrentUser\My; it does not start a server. install.ps1 runs those imports automatically at the end of a fresh install (the root import shows Windows’ consent dialog; declining costs only browser mTLS — the tokened loopback URL still grants the local dashboard). serve-certs returns 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; --sandbox or [sandbox] enabled = true accepts that first-stamp cost. When enabled, --sandbox and scoped shells work on Windows, enforced by CreateRestrictedToken plus temporary ACL entries for the RESTRICTED SID (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 a WRITE_RESTRICTED token (reads open, writes confined); scoped shells run under a fully-restricted token (deny-by-default; system dirs readable via BUILTIN\Users, devices via Everyone). Both postures strip every token privilege except SeChangeNotifyPrivilege — otherwise elevated parents (e.g. OpenSSH admin sessions) would leak SeBackup/SeRestore into the sandbox, whose backup-intent opens (FindFirstFile enumerates 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 at C:\, parts of ProgramData) stay writable inside scoped shells — user profiles do not.

See Also