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/appsfor everydarwingate that exists today. Ships in 0.1.x, documented and tested as far aswindows-latestCI 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:suggestandsightare server-side tools with no Mac dependency, sosuggest.queryandsight.now/sight.steppedwork on Windows the momentvyredruns 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, mirroringhands-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.jsdefaults(): a fresh install's role defaults tolocal(a device) onwin32as well asdarwin; only a bare non-Mac, non-Windows install still defaults tobox(a server). Existing configs are untouched; role is only ever guessed once, on a fresh install.core/cli/kit.jsgets one sharedopenInBrowser(url):openon darwin,xdg-openelsewhere, and onwin32rundll32 url.dll,FileProtocolHandler <url>with the URL as its own argv entry, never throughcmd.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 buthttp:/https:is refused, on every platform,VYRE_OPEN_BINincluded. Replaces four near-duplicate, Mac-or-Linux-only implementations inup.js,box.js,connect.jsandvault.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: atest-windowsjob, the samenpm testonwindows-latest, to catch path/shell assumptions with no new native code; and awindows-socket-acljob (notcontinue-on-error) proving the local socket ACL below.core/config/index.js:socketPath'swin32branch, 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 capsulealready said "not on this device, only macOS" before this ADR, at the CLI level, with no vyred needed.local/sideviewalready said the same at the module level (its ownSideviewError, 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 statusandvyre voice keywere never Mac-only to begin with and are untouched.- Tier B is undertested until someone runs it on real Windows hardware;
windows-latestCI proves the Node suite, not WSL2 or Docker Desktop itself. - Security review's LOW, addressed without hardware (the lead's call: use
windows-latestCI, 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 localvyred(never the remote server's socket directly, per the federation model), so Tier A on a Windows PC needs a localvyred, same as a Mac. The first attempt put the socket under a per-user%LOCALAPPDATA%\Vyre\socketsfolder with an expliciticaclsgrant, standing in forchmod; every CI run still failedlisten()withEACCES, and a diagnostic round proved the ACL was never the problem: a from-scratch folder with zeroicaclscalls applied failed identically, andwhoami /privshowedSeCreateSymbolicLinkPrivilegeDisabled. 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'swin32branch 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 restrictionchmod 0600gives the POSIX socket. No folder, no ACL, nothing forensureWindowsSocketDirto do; it's gone, along withscripts/win-socket-acl-check.mjs..github/workflows/node.yml'swindows-socket-acljob now proves the real security property directly: it starts a realvyred, connects as the owner (scripts/win-connect-probe.mjs), then proves a second local user (net user) is refused. This job is notcontinue-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, avaultbackend for Credential Manager, a Capsule shell) ships through the existinglocal/*module registry, with its own manifest and tests, no fork ofcore, 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
darwinis 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.