Skip to content

Add Windows support: CUA daemon wiring, platform-gated macOS features, NSIS packaging, .cmd CLI shims - #5

Closed
Agwstin wants to merge 4 commits into
milind-soni:mainfrom
Agwstin:windows-port
Closed

Add Windows support: CUA daemon wiring, platform-gated macOS features, NSIS packaging, .cmd CLI shims#5
Agwstin wants to merge 4 commits into
milind-soni:mainfrom
Agwstin:windows-port

Conversation

@Agwstin

@Agwstin Agwstin commented Aug 12, 2026

Copy link
Copy Markdown

Summary

This PR ports OpenMausBot to Windows (x64, NSIS installer) so the chat-app agents work on Windows with local computer use, and lays the groundwork for Linux. The harness was already portable Node; the macOS-specific surface was the Electron shell (TCC permission flows, Swift dictation, CuaDriver.app paths) plus packaging. Everything macOS-only is now platform-gated, and the CUA daemon wiring works in embedded mode on win32.

Changes

  • electron/cua.mjs: resolve the cua-driver binary per platform (trycua Cua app under %LOCALAPPDATA%\Programs, PATH via where on Windows; CuaDriver.app on macOS); probe daemon liveness with cua-driver status on Windows (named pipes can't be socket-probed). The SDK's EmbeddedCuaDriverHost works on win32 via its native DLL.
  • electron/main.mjs: TCC permission flows (mic/screen/speech) and native dictation gated to macOS; Windows deep-links into ms-settings privacy panes; default window frame outside macOS. Electron main passes its exact userData path to the harness (OMB_USER_DATA).
  • server/index.ts: readCuaConnection now reads from the real userData location on every OS (was hardcoded to ~/Library/Application Support — local computer use silently never connected on Windows).
  • server/drivers/claude.ts: the permission broker uses a named pipe (\\.\pipe\ogb-perm-) on Windows — drive-letter socket paths are invalid pipe names (EACCES) and every permission ask auto-denied.
  • server/drivers/cli.ts (new): Windows npm/yarn/pnpm CLIs ship as .cmd shims that child_process can't spawn directly; resolve the shim to its real JS entry and spawn process.execPath — no cmd.exe, no shell-quoting hazards. All probes are async (spawnSync was blocking the harness event loop), where results are cached, and process trees are killed with taskkill /T /F on Windows. Exact no-op on macOS/Linux.
  • electron-builder.yml: NSIS x64 target + icon.ico; the Swift helpers (speech-helper, perm-helper) moved to mac.extraResources so Windows builds don't need swiftc.
  • src/: dictation mic button hidden outside macOS; onboarding TCC step collapses to a note; "this Mac" copy made platform-neutral.
  • package.json: pnpm.onlyBuiltDependencies (pnpm 10 blocks the electron postinstall) + package:win script.
  • README updated with Windows install + platform notes.

Testing

Verified on Windows 10/11 x64 (Node 25, pnpm 10): typecheck + build pass, the NSIS installer builds and runs, the packaged app's harness serves the UI, CUA embedded spawns its own daemon on a private named pipe, agent turns with local computer use work end-to-end (get_screen_size round-trip through the permission broker), and Claude + Codex CLIs are detected correctly (including npm-installed codex.cmd). macOS behavior is unchanged (all new code paths are gated or no-ops on POSIX).

…, NSIS packaging, .cmd CLI shims

- electron/cua.mjs: resolve cua-driver per platform (trycua Cua app under
  %LOCALAPPDATA%, PATH via where) and probe daemon liveness with
  'cua-driver status' on Windows (named pipes can't be socket-probed);
  embedded host works on win32 via the SDK's native DLL
- electron/main.mjs: gate TCC permission flows (macOS-only) and native
  dictation; Windows deep-links to ms-settings privacy panes; default
  window frame outside macOS
- electron/speech.mjs + Composer: dictation stays macOS-only, mic button
  hidden elsewhere
- electron-builder.yml: NSIS x64 target + icon.ico; mac-only Swift helpers
  moved to mac.extraResources so Windows builds don't require swiftc
- package.json: pnpm.onlyBuiltDependencies (pnpm 10 blocks electron
  postinstall) + package:win script
- server/drivers/cli.ts: resolve .cmd shims (npm-global CLIs on Windows
  can't be spawned by child_process); codex/claude drivers now detect
  codex.cmd correctly
- README: Windows install + platform notes
…shell-less CLI spawn

- server/index.ts: readCuaConnection now uses the real userData path
  (Electron main passes it via OMB_USER_DATA) with per-platform fallbacks —
  previously hardcoded ~/Library/Application Support, so local computer use
  silently never connected on Windows
- server/drivers/claude.ts: permission broker socket is a \\.\pipe\ name
  on Windows (drive-letter paths are invalid pipe names, EACCES) — every
  permission ask used to auto-deny
- server/drivers/cli.ts: resolve .cmd shims to their real JS entry and
  spawn process.execPath directly (no cmd.exe, no shell quoting hazards);
  all probes async (spawnSync was blocking the harness event loop up to
  60s in generateText); `where` results cached; killProcessTree uses
  taskkill /T /F on Windows (process.kill(-pid) throws ESRCH and orphaned
  the CLI)
- electron/cua.mjs: windowsHide on all child processes; daemon liveness
  probe is async (spawn) instead of a blocking spawnSync at startup
- UI copy: 'this Mac' -> 'this computer' in Onboarding + ComputerPanel
The harness spawns the agent CLIs (claude, codex) from the Electron GUI
host; without windowsHide each turn flashed a console window on Windows.
Both driver spawns now pass windowsHide: true (dev probes already did).
windowsHide only hides the direct child; console-app descendants (cmd.exe
for device-id probes, MCP servers like cua-driver.exe) still flash their
own windows. Spawn the CLI through PowerShell 7 (pwsh) with a hidden
console instead, so the entire subtree inherits it and nothing flashes.
Args travel through a temp JSON file (no cmd/PowerShell quoting hazards;
PS 5.1 mangles embedded quotes in --mcp-config, pwsh passes argv
correctly). Falls back to a plain windowsHide spawn when pwsh is missing.

detached is stripped on Windows: it maps to DETACHED_PROCESS there, which
makes pwsh exit 0 immediately without running the script (surfaces as
"cli exited 0 before result"). POSIX keeps it (killProcessTree uses the
process group); Windows reaps the tree with taskkill /T /F.

Verified end-to-end on Windows: agent turn with local computer use
(get_screen_size -> 1920x1080) through the packaged app, no console
windows.
@milind-soni

Copy link
Copy Markdown
Owner

Thanks for this — the core design here (resolve .cmd shims to their JS entry and spawn process.execPath, named-pipe broker, taskkill tree-kill, the OMB_USER_DATA contract) is the right approach, and it's the foundation we're going with for the Windows port.

A deep review turned up a few things that block merging as-is:

  1. Empty-env probes (merge-blocker): cliExec builds env: { ...opts.env, ...runEnv } — when no env is passed (as in cliVersion) that's an empty environment, not "inherit". Verified locally: execFile("claude", ["--version"], { env: {} }) ENOENTs because PATH is gone, so claude/codex report "CLI not found" and the model picker empties — on macOS too, in dev and packaged. Fix is ...(opts.env ?? process.env).
  2. The shell: true fallback for a .cmd that can't be unwrapped routes args through cmd.exe — the exact injection class (%VAR% expands even inside quotes) the file's header says it avoids, and it also breaks for shim paths containing spaces.
  3. Shim regex misses %~dp0 (no trailing %) — the yarn / older cmd-shim format — so those installs fall into the fallback paths.
  4. The no-pwsh fallback can hit Node's synchronous spawn EINVAL for .cmd targets (CVE-2024-27980 hardening), escaping the child.on("error") path.
  5. server/drivers/grokagent.ts (landed via Grok via subscription: grokAgent driver (Grok Build CLI over ACP) — no API key #6 after you branched) still raw-spawns and process.kill(-pid)s — grok wouldn't spawn on Windows.
  6. Smaller: spawnSync for where/taskkill blocks the event loop; resolvePwsh misses winget-user/PATH installs; pwsh re-emits CLI stdout in the OEM codepage (mojibake risk for non-ASCII stream-json); cua.mjs can write a .cmd as mcpCommand; ms-settings:privacy-broadcasting is the game-broadcasting pane.

Rather than round-tripping all of that, I've pushed #10 — your branch with all commits preserved, merged with current main, plus the fixes above and a portable test file for the spawn layer (server/drivers/cli.test.ts); main now has a 3-OS CI matrix and #10's windows leg is running it. If you can give #10 a hands-on pass on your Windows setup (packaged NSIS build, an agent turn, local computer use), that's the last thing standing between this port and merge. Thanks again — this was a well-researched contribution, especially the console-flashing analysis around the pwsh wrapper.

@milind-soni

Copy link
Copy Markdown
Owner

Current main already contains the Windows desktop shell, NSIS packaging, platform-gated macOS features, named-pipe permission broker, process-tree cleanup, and the portable capability contract. This branch predates those implementations and still routes npm shims through cmd.exe, which conflicts with the current no-shell security rule. It should not be merged; please close it, and move any still-missing narrowly scoped behavior into a fresh PR against current main.

@milind-soni

Copy link
Copy Markdown
Owner

Thank you for this — you were first to map out what Windows support actually required, and that shaped everything that followed.

Closing as superseded: Windows support landed in #17 (f3bac3a), which covers the same ground — server/procs.ts resolves npm .cmd/.bat shims before spawning and uses taskkill /T where POSIX process groups don't exist, the permission broker binds a named pipe on win32 (procs.ts:105), and electron-builder.yml gained an NSIS target. Windows installers now also build from CI on a real Windows runner, so every release ships both platforms from one commit.

Not a reflection on the work — three parallel Windows ports arrived and only one could land. If you spot something yours handled that main still gets wrong, please open a focused PR against current main and I'll take it seriously.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants