docs GitHub

ArchitectureDraft

ADR 0037: Windows support: tiers and the 0.1.x cut

Context

Some people's main computer is Windows, sometimes more than one. vyred (core, deck, box, CLI) already runs headless on Linux with no macOS in the loop, node.yml runs the full suite on ubuntu-latest, and the Mac-only surface is concentrated and already gated behind process.platform === "darwin": the native Capsule and its modules (local/capsule, local/hands-mac, local/screen-mac, local/sideview, local/voice/swift), and the native vault backend (Keychain, Touch ID, core/vault/mac, core/presence/touchid). Nothing in the kernel assumes macOS.

Full assessment, with the file-level inventory: docs/design/windows-plan.md.

Decision

Windows PCs and Macs are devices; the server is Linux only, a bare Linux machine or a Linux VM, including one inside WSL2 on a Windows PC (words per ADR 0038, terminology; link added once it merges). There is no Windows server and no Windows Capsule, and a Windows PC is never asked to be one.

docs/design/interaction.md (cohesion's interaction pass, binding for 0.1.1) is the acceptance criteria for Tier A and Tier B, decided by the lead 2026-09-28, not a bar to retrofit once a Windows device ships: streaming, live events instead of polling, and the one motion/component vocabulary in docs/design/one-app/DIRECTION.md and docs/design/system/components/* apply to a Windows device exactly as they do to a Mac or the Deck, since Tier A and Tier B run the same client and server code as everywhere else. Nothing here is deferred to Tier C.

Four tiers, cheapest first:

  • Tier A, a Windows PC as a device against a Linux server: the Deck in a browser, the PWA, the CLI, and Claude Code with the Vyre plugin. Already close to working, since these are pure web/Node; ships in 0.1.x.
  • Tier B, a Linux server on a Windows PC, run inside WSL2, not natively on Windows. WSL2 reuses the existing Docker Compose server image unchanged; a native Windows server would mean a second, ongoing platform branch through core/daemon, core/vault, core/files, local/apps for every darwin gate that exists today. Ships in 0.1.x, documented and tested as far as windows-latest CI allows (no WSL2 there); a hands-on pass on real hardware is still needed. Ships with a concrete "Windows already feels alive" milestone for free: suggest and sight are server-side tools with no Mac dependency, so suggest.query and sight.now/ sight.stepped work on Windows the moment vyred runs under WSL2, no port needed; worth stating and testing as its own deliverable, not left implicit inside "Tier B works" (cohesion, see below).
  • Tier C, a native Windows Capsule, most likely Tauri (a Rust shell built on docs/design/system/components/* directly, the same card/row/button vocabulary every other surface uses, not a reimplementation in a new toolkit), a real global-hotkey API, over WinUI 3/.NET or Electron. 0.2.
  • Tier D, computer use on Windows (UI Automation + SendInput, mirroring hands-mac's observe/act/verify/floor/stop-key shape), a voice port, and Windows Credential Manager / Windows Hello standing in for Keychain / Touch ID. 0.2.

Changes landing with this ADR

  • core/config/index.js defaults(): a fresh install's role defaults to local (a device) on win32 as well as darwin; only a bare non-Mac, non-Windows install still defaults to box (a server). Existing configs are untouched; role is only ever guessed once, on a fresh install.
  • core/cli/kit.js gets one shared openInBrowser(url): open on darwin, xdg-open elsewhere, and on win32 rundll32 url.dll,FileProtocolHandler <url> with the URL as its own argv entry, never through cmd.exe (a security review caught the first version of this, cmd /c start, letting a query string's &/|/^/</> run as command operators after it; every OAuth URL has a &). Every scheme but http:/https: is refused, on every platform, VYRE_OPEN_BIN included. Replaces four near-duplicate, Mac-or-Linux-only implementations in up.js, box.js, connect.js and vault.js, the last of which only opened a browser on darwin at all, so this also fixes it on Linux, not just Windows. Tests: core/cli/kit.test.js.
  • node.yml: a test-windows job, the same npm test on windows-latest, to catch path/shell assumptions with no new native code; and a windows-socket-acl job (not continue-on-error) proving the local socket ACL below.
  • core/config/index.js: socketPath's win32 branch, a named pipe name, covered by the Consequences entry below.
  • docs/using/windows.md: the person-facing how-to for Tier A and Tier B.

Consequences

  • vyre capsule already said "not on this device, only macOS" before this ADR, at the CLI level, with no vyred needed. local/sideview already said the same at the module level (its own SideviewError, once vyred starts). local/voice's push-to-talk (vyre voice talk) did not: it printed "vyre-mic is not built. Build it with: sh build.sh" on every non-Mac device, telling someone to build a Swift binary that cannot exist on their device. Fixed to say plainly that push-to-talk needs the Capsule's mic and is macOS only (local/voice/talk.js). vyre voice status and vyre voice key were never Mac-only to begin with and are untouched.
  • Tier B is undertested until someone runs it on real Windows hardware; windows-latest CI proves the Node suite, not WSL2 or Docker Desktop itself.
  • Security review's LOW, addressed without hardware (the lead's call: use windows-latest CI, not a physical machine); the real fix turned out to be a different Windows primitive, not an ACL. The CLI on any device role always talks to a local vyred (never the remote server's socket directly, per the federation model), so Tier A on a Windows PC needs a local vyred, same as a Mac. The first attempt put the socket under a per-user %LOCALAPPDATA%\Vyre\sockets folder with an explicit icacls grant, standing in for chmod; every CI run still failed listen() with EACCES, and a diagnostic round proved the ACL was never the problem: a from-scratch folder with zero icacls calls applied failed identically, and whoami /priv showed SeCreateSymbolicLinkPrivilege Disabled. A bound socket file on Windows is an NTFS reparse point, and creating one needs that privilege, which most Windows accounts, this runner's included, don't hold. The fix: socketPath's win32 branch returns a literal named pipe name (\\.\pipe\vyre-<hash>), never a filesystem path; a named pipe needs no privilege and gets a current-user-only security descriptor from Node by default, the same restriction chmod 0600 gives the POSIX socket. No folder, no ACL, nothing for ensureWindowsSocketDir to do; it's gone, along with scripts/win-socket-acl-check.mjs. .github/workflows/node.yml's windows-socket-acl job now proves the real security property directly: it starts a real vyred, connects as the owner (scripts/win-connect-probe.mjs), then proves a second local user (net user) is refused. This job is not continue-on-error: per the lead, if it cannot be made to pass, Tier A ships in 0.1.1 marked "preview" with the gap written down, not silently green.
  • Every future Windows-only module (local/hands-win, local/screen-win, a vault backend for Credential Manager, a Capsule shell) ships through the existing local/* module registry, with its own manifest and tests, no fork of core, no special-casing per file the way the four browser-opening functions had drifted into before this ADR.

Rejected

  • Native Windows Node as the server, instead of WSL2, for Tier B: real, ongoing maintenance (a second platform branch everywhere darwin is checked today) for no benefit over WSL2, which already gets the unchanged Linux image via Docker Desktop's WSL2 backend.
  • WinUI 3 / .NET for Tier C over Tauri: deepest native UI Automation access, but a second UI codebase with nothing shared with the Deck; left as an open question to prototype against Tauri before Tier C actually starts, not decided here.
  • Electron for Tier C: already retired on the Mac for the native Capsule; no reason to bring it back for Windows.