@agentconnect.md/desktop 0.0.0-stage → 2.6.0-rc.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,512 @@
1
- # Temporary Holding Version
1
+ # @agentconnect.md/desktop
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The **computer (desktop) daemon** from the computer-use design
4
+ ([`docs/designs/computer-use.md`](../../docs/designs/computer-use.md), tracked in [#3182](https://github.com/agentconnect-md/agentconnect/issues/3182)). It runs inside the
5
+ logged-in desktop session, embeds [Cua Driver](https://github.com/trycua/cua/tree/main/libs/cua-driver)
6
+ (MIT) for screenshots and input, and exposes the design's `computer_*` tools as an MCP
7
+ server that Claude Code (or any MCP client) can use.
8
+
9
+ It runs standalone with local client tokens, or registered with a Control Plane as a Computer
10
+ (see [Register with a Control Plane](#register-with-a-control-plane)). No relay or AgentConnect
11
+ daemon is involved yet.
12
+
13
+ ## Status
14
+
15
+ This package started as the prototype merged in milestone M0 of the design's development plan (§14.3).
16
+ It is published to npm with each AgentConnect release (a self-contained bundle; Cua Driver and ffmpeg
17
+ are pinned downloads at setup) and installed, upgraded and diagnosed by `@agentconnect.md/cli` as
18
+ `agentconnect computer …` (see [Run on Linux](#run-on-linux)). The `desktop` container image is
19
+ published with each release too (see [Run in Docker](#run-in-docker)).
20
+
21
+ **What works today**
22
+
23
+ - The `computer_*` MCP tools over Streamable HTTP (bearer token per client) or stdio, with one
24
+ control lease, human-activity arbitration, an owner pause, no-replay `uncertain` results, and a
25
+ local audit log.
26
+ - The `/view` browser remote desktop: WebRTC video (ffmpeg + werift) with a screenshot-polling
27
+ fallback, and human takeover.
28
+ - `setup` / `doctor` on macOS, Linux (X11 and GNOME Wayland) and Windows; a macOS LaunchAgent
29
+ (`.app` mode) and a Linux systemd user unit; Windows logon autostart.
30
+ - A Docker image (Xvfb + Xfce + Firefox ESR) that registers from its environment, amd64 and arm64.
31
+
32
+ **Limits**
33
+
34
+ - Registration with a Control Plane and its Computer channel work (status, capabilities, `doctor`,
35
+ revoke). `ComputerPolicy` is enforced only for the lease limits. Approvals, per-agent
36
+ authorization, the data plane and object-store screenshots are still missing. Local client
37
+ tokens stand in for them, and removing a token takes effect only after a restart.
38
+ - Plain HTTP with a bearer token. The daemon listens on `127.0.0.1` by default; other machines
39
+ can reach it only when you pass `--host 0.0.0.0` (or another address) explicitly, and then the
40
+ token is the only protection.
41
+ - One display, no audio, no clipboard sync; no screen border. Keyboard activity, the tray icon
42
+ and the global stop hotkey exist on X11 only (Wayland is #3221; macOS and Windows have none).
43
+ - Builds are unsigned (macOS `.app` is ad-hoc signed; no Windows installer), so OS grants do not
44
+ survive a rebuild.
45
+ - CI covers X11 under Xvfb only (see [Development](#development)); the real-session matrix of design
46
+ §13 is still run by hand. `agentconnect computer` manages the npm package on Linux; macOS (`.app`) and
47
+ Windows packaging are M3 / M4, so there the bundle below is still installed by hand.
48
+
49
+ ```
50
+ Claude Code ──HTTP (Bearer token)──▶ agentconnect-desktop ──stdio MCP──▶ cua-driver mcp --direct ──▶ desktop
51
+ (any machine) (user's logon session) (pinned 0.35.2, sha256-checked)
52
+ Browser ─────WebRTC video + input───▶ │ ffmpeg screen capture → H.264 → werift (WebRTC)
53
+ ```
54
+
55
+ It also serves a browser remote desktop at `/view` so a person can watch the screen
56
+ and take over from agents (see [Remote desktop viewer](#remote-desktop-viewer)).
57
+
58
+ ## Install: `setup`, then `doctor`
59
+
60
+ Everything that needs a download or a person's approval happens once, in `setup`, run **on the
61
+ desktop itself** (not over SSH or RDP-from-a-service):
62
+
63
+ 1. Config and a client token. The token is printed once, together with the `claude mcp add`
64
+ command and the `/view` link.
65
+ 2. Pinned Cua Driver and ffmpeg, both sha256-checked.
66
+ 3. OS approvals:
67
+ - **macOS**: requests Accessibility and Screen Recording for the app running setup (usually
68
+ Terminal). It opens the matching System Settings pane and waits until each is switched on.
69
+ Then a test ffmpeg capture raises the separate "bypass the private window picker" consent.
70
+ Screen Recording takes effect after quitting Terminal (Cmd+Q) and reopening it.
71
+ - **Windows**, only when listening beyond loopback (`--host 0.0.0.0`): adds the inbound
72
+ firewall rule "AgentConnect Desktop (Node.js)" for this `node.exe`, covering all network
73
+ profiles (Tailscale is often _Public_), after one administrator prompt. It disables any
74
+ block rule that Windows created when someone pressed Cancel on its own prompt.
75
+ 4. Runs `doctor`.
76
+
77
+ `doctor` is read-only and can be run any time. It exits 1 when a required check fails, and every
78
+ failure comes with its fix. It checks:
79
+
80
+ - config and tokens;
81
+ - the desktop session (SSH or Session 0 cannot capture; disconnecting RDP locks the session);
82
+ - that Cua Driver starts;
83
+ - the macOS permissions;
84
+ - a real screenshot;
85
+ - display sleep;
86
+ - ffmpeg and a real one-frame capture;
87
+ - the Windows Firewall rules for Node.js;
88
+ - whether a daemon is already running.
89
+
90
+ `run` also keeps the display awake while it runs (`"keepAwake": true`; macOS `caffeinate`, Windows
91
+ `SetThreadExecutionState`). A sleeping display cannot be captured: macOS even removes its capture
92
+ devices.
93
+
94
+ ## Run on Windows
95
+
96
+ Requirements: Windows 10/11 x64 or arm64, a logged-in desktop session, Node.js ≥ 24.12
97
+ (`winget install OpenJS.NodeJS`).
98
+
99
+ 1. Build the bundle on a dev machine (`pnpm --filter @agentconnect.md/desktop bundle`) and copy
100
+ the `bundle/` directory (`agentconnect-desktop.mjs`, `install.ps1`, `README.md`) to the
101
+ Windows machine.
102
+ 2. In a normal (non-admin) PowerShell on the Windows desktop, run:
103
+
104
+ ```powershell
105
+ .\install.ps1 # copies to %LOCALAPPDATA%\AgentConnect\desktop\app and runs setup
106
+ node "$env:LOCALAPPDATA\AgentConnect\desktop\app\agentconnect-desktop.mjs" run
107
+ ```
108
+
109
+ The daemon then listens on `127.0.0.1` only. To reach it from another machine, run
110
+ `.\install.ps1 -ListenHost 0.0.0.0` instead: `setup` saves the address and adds the inbound
111
+ firewall rule, which a loopback-only install skips.
112
+
113
+ `run` is a plain foreground process: `Ctrl+C` stops it and closes the port. Keep the console
114
+ window open: it is the indicator and emergency stop. Running `node agentconnect-desktop.mjs run`
115
+ straight from the copied `bundle/` folder also works; it does the downloads itself but skips the
116
+ firewall step.
117
+
118
+ Nothing is installed as a service. With `-Autostart`, `install.ps1` also adds a per-user
119
+ `HKCU\...\Run` entry that opens the daemon console at logon (`autostart disable` removes it). A real
120
+ Windows service would not work anyway: services live in Session 0 and cannot see the user desktop.
121
+
122
+ To start over (new token), delete `%LOCALAPPDATA%\AgentConnect\desktop\config.json`, or run
123
+ `node agentconnect-desktop.mjs token add claude` to rotate the token.
124
+
125
+ ## Run on macOS
126
+
127
+ Requirements: Node.js ≥ 24.12. No Homebrew or sudo is needed: the official tarball unpacked to
128
+ `~/.local/node` works. In **Terminal on the Mac** (not over SSH: macOS grants the permissions to
129
+ the app that starts the daemon), copy the `bundle/` directory to the Mac and run:
130
+
131
+ ```sh
132
+ ./install.sh # copies to ~/.agentconnect/desktop/app, runs setup (permission dialogs), then doctor
133
+ open ~/.agentconnect/desktop/app/"AgentConnect Desktop.command" # starts the daemon in Terminal
134
+ ```
135
+
136
+ On macOS 27, Accessibility is named _Device Control and Data Access_. Being listed there is not
137
+ enough: the switch must be on. Without it, clicks and keys are silently dropped, though pointer
138
+ moves still work. Cmd shortcuts are allowed on macOS; only lock screen, log out, and force quit are
139
+ refused. ffmpeg is a pinned static 6.0 build (eugeneware/ffmpeg-static b6.1.1).
140
+
141
+ ### macOS app mode (prototype): the app holds the permissions, not Terminal
142
+
143
+ `pnpm bundle:macos` builds `AgentConnect Desktop.app` (`md.agentconnect.desktop`): the daemon as a
144
+ Node single executable app on the official, pinned Node.js, ad-hoc signed. Install it and run
145
+ `setup` from the app's binary. This works from Terminal or over SSH:
146
+
147
+ ```sh
148
+ ditto -x -k AgentConnect-Desktop-macos-arm64.zip ~/Applications/
149
+ ~/Applications/"AgentConnect Desktop.app"/Contents/MacOS/agentconnect-desktop setup
150
+ ```
151
+
152
+ - `setup` installs a per-user LaunchAgent (`~/Library/LaunchAgents/md.agentconnect.desktop.plist`,
153
+ Aqua session, starts at login, restarts on crash) and starts it. launchd starts the app itself,
154
+ so macOS attributes Accessibility and Screen Recording to **AgentConnect Desktop**. Cua Driver
155
+ and ffmpeg run as its children and inherit the grants. The app raises the dialogs itself.
156
+ `setup` opens the System Settings panes, waits, and restarts the service until a new Screen
157
+ Recording grant takes effect. Then it runs `doctor` inside the service.
158
+ - `doctor` asks the running service to run the checks as its own child (`/control/doctor`, loopback
159
+ only), so the result reflects the app's grants. `--here` checks the calling process instead.
160
+ - `service start | stop | restart | status | uninstall` manage the LaunchAgent. The log is
161
+ `~/.agentconnect/desktop/daemon.log`.
162
+ - Ad-hoc signing ties the grants to this exact build: each rebuild must be granted again.
163
+ Distribution needs a Developer ID signature plus notarization (`CODESIGN_IDENTITY=...`; the
164
+ hardened runtime then needs the JIT entitlements for Node). Unsigned zips downloaded through a
165
+ browser are quarantined and blocked by Gatekeeper.
166
+
167
+ ## Run on Linux
168
+
169
+ **Install** (what the console's **Computers → Add Computer** shows for Linux): run it in a terminal
170
+ inside the desktop session, as the desktop user, not root. It needs Node.js ≥ 24.12 (the official
171
+ tarball or your distribution's package).
172
+
173
+ ```bash
174
+ npx -y @agentconnect.md/cli computer install --register <token> --cp-url <exchange URL>
175
+ ```
176
+
177
+ The CLI downloads `@agentconnect.md/desktop` from npm, verifies its integrity, unpacks it into
178
+ `~/.agentconnect/desktop/versions/<version>`, points `~/.agentconnect/desktop/current` at it, and
179
+ runs `setup --register` (Cua Driver, ffmpeg, grants, the systemd user service on `current`, then
180
+ `doctor` inside the service). `--name`, `--no-service` and other `setup` options pass through; run
181
+ over SSH, it installs the service, which then starts at the next desktop login. Then:
182
+
183
+ ```bash
184
+ agentconnect computer doctor # the installed version and every check, from inside the service
185
+ agentconnect computer status # version, service, and whether the daemon answers
186
+ agentconnect computer upgrade # install the channel latest (--version / --channel), restart, roll back if unhealthy
187
+ agentconnect computer versions # installed versions
188
+ agentconnect computer stop | start | restart
189
+ agentconnect computer uninstall # service, autostart entry and versions; --purge also deletes config and the key
190
+ ```
191
+
192
+ (`npx -y @agentconnect.md/cli` stands in for `agentconnect` without a global install.) Config, the
193
+ Computer key and the grants stay in `~/.agentconnect/desktop`, so an upgrade keeps the
194
+ registration; a desktop installed by the earlier `install.sh` keeps its data, and `computer install`
195
+ moves its service onto the store. `AGENTCONNECT_NPM_REGISTRY` points the CLI at another registry.
196
+
197
+ Requirements for running from source: Node.js ≥ 24.12 (the official tarball in `~/.local/node` works), a logged-in
198
+ graphical session, and `setup` run inside it. Over SSH, export the session's `WAYLAND_DISPLAY`,
199
+ `XDG_RUNTIME_DIR`, and `DBUS_SESSION_BUS_ADDRESS` first. `setup` downloads Cua Driver and a pinned
200
+ static ffmpeg (6.0, libx264).
201
+
202
+ - **X11**: Cua Driver captures and injects through X11/XTest, and the viewer uses ffmpeg `x11grab`.
203
+ Nothing needs approving.
204
+ - **GNOME Wayland**, tested on Ubuntu 26.04 / GNOME 50. X11 grabbers only see Xwayland clients
205
+ there, so:
206
+ - Cua Driver runs in its Wayland mode (`CUA_DRIVER_RS_ENABLE_WAYLAND=1`, set by the daemon). It
207
+ needs its GNOME Shell helper extension `winrects@cua` for pixel geometry, capture, and safe
208
+ input. `setup` installs it; **log out and back in once** so GNOME loads it.
209
+ - Input goes through the xdg-desktop-portal **RemoteDesktop** portal plus libei. `setup` presses
210
+ Shift once so GNOME asks; switch on _Allow Remote Interaction_ and keep _Remember This
211
+ Selection_. The grant is kept in `~/.config/cua-driver/libei-persistent.token`.
212
+ - Video uses the **ScreenCast** portal plus PipeWire. A small Python helper (`python3-gi`) holds
213
+ the portal session and runs GStreamer (`pipewiresrc ! videorate ! y4menc`) into the pinned
214
+ ffmpeg, because distro GStreamer often has no H.264 encoder. `setup` asks once; the restore
215
+ token is kept in `screencast.token`.
216
+ - GNOME shows its screen-sharing indicator in the top bar while either session is active, and
217
+ `run` holds an idle inhibitor (`gnome-session-inhibit`) so the screen does not blank or lock.
218
+ - Known limits: libei types key codes, so text that needs an input method (e.g. Chinese) is
219
+ dropped. KDE Plasma Wayland lacks Cua Driver's activation adapter.
220
+ - **Background service**: `setup` installs and starts a systemd user unit
221
+ (`~/.config/systemd/user/agentconnect-desktop.service`). It is wanted by
222
+ `graphical-session.target` (GNOME, KDE) and also started by an XDG autostart entry
223
+ (`~/.config/autostart/agentconnect-desktop.desktop`). That entry imports the session's display
224
+ variables first, for desktops such as Xfce that never activate the target.
225
+ - The unit runs a copy of the bundle from the data dir, restarts on failure, and stops with the
226
+ session. Xfce never stops `graphical-session.target`, so the autostart entry also records the
227
+ logind session id and the daemon exits when that session closes; the next login starts it
228
+ again with the new display.
229
+ - `ExecCondition` skips it when there is no display, so it never crash-loops outside a session.
230
+ - Cua Driver runs in its own `systemd-run --scope`, so apps an agent launched survive service
231
+ restarts and upgrades.
232
+ - Commands: `service status | restart | stop | uninstall`; logs with
233
+ `journalctl --user -u agentconnect-desktop`. `setup --no-service` skips the unit.
234
+ - X11 types characters missing from the keymap by remapping a spare keycode. On real Xorg,
235
+ back-to-back remaps raced (`你好` became `好好`), so non-ASCII text is sent one character at a
236
+ time. Desktops other than GNOME get `xset s off -dpms` instead of an idle inhibitor.
237
+ - Tested: Ubuntu 26.04 arm64 with GNOME 50 Wayland, and with Xfce 4.20 on Xorg (LightDM, because
238
+ GDM 50 no longer starts X11 sessions) and on Xvfb.
239
+
240
+ ## Run in Docker
241
+
242
+ The `desktop` image (`ghcr.io/agentconnect-md/desktop`, amd64 and arm64, published with each release) is a
243
+ Debian trixie desktop with Xvfb, a lean Xfce, Firefox ESR, Noto CJK fonts, the pinned Node.js, and the daemon
244
+ with Cua Driver preinstalled. ffmpeg (GPL) is not in the image: the daemon downloads the pinned, sha256-checked
245
+ build on first start into the volume, and the viewer polls screenshots until it is there. The entrypoint starts Xvfb on `:1` (`RESOLUTION`, default
246
+ `1600x900x24`), a session D-Bus, Xfce and a maximized browser, then runs `run` in the foreground as user
247
+ `desktop`. The browser is restarted when it exits or crashes (`BROWSER`, default `firefox-esr`; `none` turns it
248
+ off; `BROWSER_URL` opens a page). `docker stop` stops the daemon, closes the browser, then the session, with exit
249
+ code 0. `/home/desktop` is a volume: the Computer key, config, audit log, and the browser profile survive a new
250
+ container. A `HEALTHCHECK` runs `doctor --json --daemon` inside the running daemon.
251
+
252
+ Register it as a Computer: create a token in the console under **Computers → Add Computer** (pick **Docker** for
253
+ this command), then
254
+
255
+ ```sh
256
+ docker run -d --name desktop --hostname desktop-1 --shm-size=1g --restart unless-stopped \
257
+ -v desktop-home:/home/desktop \
258
+ -e AGENTCONNECT_CP_URL='https://<control plane>' -e AGENTCONNECT_REGISTER_TOKEN='<token>' \
259
+ ghcr.io/agentconnect-md/desktop:latest
260
+ ```
261
+
262
+ | Variable | Meaning |
263
+ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
264
+ | `AGENTCONNECT_REGISTER_TOKEN` | the one-time registration token; the session, browser and driver never see it, and it is never printed (`docker inspect` and `docker exec` still do: prefer the file) |
265
+ | `AGENTCONNECT_REGISTER_TOKEN_FILE` | a file holding the token instead, such as a Docker or Kubernetes secret under `/run/secrets` |
266
+ | `AGENTCONNECT_CP_URL` | the Control Plane URL the console prints with the token |
267
+ | `AGENTCONNECT_COMPUTER_NAME` | the name to register with when the console gave none; defaults to the container hostname |
268
+ | `PUBLIC_URL` | how clients outside reach the published port, printed instead of the container address |
269
+ | `RTC_ADDRESS`, `RTC_PORTS` | the public IP offered as the only WebRTC candidate (defaults to `PUBLIC_URL`'s host), and a fixed UDP range |
270
+ | `RESOLUTION`, `LISTEN_HOST`, `PORT` | the screen, and where the daemon listens inside the container (`0.0.0.0:7681`) |
271
+
272
+ For a server, [`docker/compose.yaml`](docker/compose.yaml) with [`docker/compose.env.example`](docker/compose.env.example)
273
+ does the same with its settings in `.env`; the guide is
274
+ [docs/self-host-desktop-container.md](../../docs/self-host-desktop-container.md), including TURN for the viewer.
275
+
276
+ The token is spent on the first start; the stored key remembers its sha256, so later starts with the same token
277
+ and Control Plane leave it unused (keep the volume). A different token, such as a new one after a revoke or for
278
+ another org, is spent and replaces the stored key. Without a token the
279
+ daemon runs in local mode and writes its first client token to `~/.agentconnect/desktop/client-token` instead of
280
+ the log (`docker exec desktop cat /home/desktop/.agentconnect/desktop/client-token`).
281
+
282
+ The console's viewer reaches a registered container through the relay and needs no published port. For a viewer
283
+ or MCP client that connects directly, publish the port and the UDP range and say where they are:
284
+
285
+ ```sh
286
+ docker run -d --name desktop --shm-size=1g -v desktop-home:/home/desktop \
287
+ -p 7681:7681 -p 7700-7709:7700-7709/udp \
288
+ -e PUBLIC_URL="http://$HOST_IP:7681" -e RTC_PORTS=7700-7709 ghcr.io/agentconnect-md/desktop:latest
289
+ ```
290
+
291
+ WebRTC behind NAT: by default the daemon offers the address the browser reached it on, which in bridge
292
+ networking is the container IP. `--rtc-address` (`rtcAddress`) replaces it as the only host candidate,
293
+ and `--rtc-ports MIN-MAX` (`rtcPortRange`) pins the UDP ports so they can be published. Each viewer uses
294
+ up to two ports from the range, so publish at least 8. With `--network host` on a Linux host neither is
295
+ needed.
296
+
297
+ Build it locally from the repository root, and smoke-test it the way CI does:
298
+
299
+ ```sh
300
+ docker build -f packages/desktop/docker/Dockerfile -t agentconnect-desktop .
301
+ packages/desktop/docker/smoke.sh agentconnect-desktop
302
+ ```
303
+
304
+ ## Connect Claude Code
305
+
306
+ The first `run` (or `init`) prints the exact command, for example:
307
+
308
+ ```text
309
+ claude mcp add --transport http computer http://127.0.0.1:7681/mcp --header "Authorization: Bearer acd_..."
310
+ ```
311
+
312
+ The daemon listens on `127.0.0.1` unless you ask otherwise, so by default only clients on the
313
+ same machine (or through an `ssh -L` tunnel) reach it. `--host 0.0.0.0` on `setup`, `init`, or
314
+ `run` listens on every interface; `setup` and `init` save it in `config.json` (`"host"`) for the
315
+ service. Traffic is plain HTTP and the token is the only protection, so do that on a trusted
316
+ network only. A `config.json` written by an earlier prototype build may still say `"0.0.0.0"`.
317
+
318
+ If Claude Code runs on the same Windows machine you can use stdio instead:
319
+ `claude mcp add computer -- node <path>\agentconnect-desktop.mjs stdio`.
320
+
321
+ ## Remote desktop viewer
322
+
323
+ Open `http://<host>:7681/view` in any modern browser and sign in with a client token.
324
+ The first `run` prints a ready link of the form `/view#token=...`. The token is in the URL fragment,
325
+ so it never reaches the server or proxy logs, and the page keeps it in `localStorage`.
326
+
327
+ - **Video**: ffmpeg captures the primary display (Windows: `ddagrab`, falling back to
328
+ `gdigrab`; macOS: `avfoundation`; Linux: `x11grab`). It encodes H.264 constrained baseline
329
+ (`libx264 ultrafast/zerolatency`, `streamFps` 30, `streamMaxWidth` 1920,
330
+ `streamBitrateKbps` 2500, 1 s keyframe interval, keyframes capped by a small VBV buffer), and [werift](https://github.com/shinyoshiaki/werift-webrtc)
331
+ (MIT, pure TypeScript) sends it over WebRTC. One capture is shared by every viewer and stops
332
+ when the last viewer leaves. Packets are paced at 1.5x the bitrate, because an unpaced keyframe
333
+ burst overflows thin links (e.g. Tailscale over the internet) and is lost whole. On slow links,
334
+ lower `streamBitrateKbps` / `streamMaxWidth`.
335
+ - **Signaling** uses the same port and token (`POST /view/api/rtc/offer` / `answer`). The
336
+ daemon is ICE-lite with host candidates only: no STUN/TURN and no external server. It
337
+ advertises the address the browser reached it on, so Tailscale and LAN both work. Media runs
338
+ over UDP on random ports, so the Windows firewall must allow Node.js (accept the prompt).
339
+ - **ffmpeg**: on Windows, the first `run` downloads the pinned gyan.dev build
340
+ (9.0.2 essentials, GPL, ~110 MB, sha256-checked) in the background, or run `install-ffmpeg`
341
+ ahead of time. Elsewhere it uses `ffmpeg` from PATH. `ffmpegPath` overrides it, and
342
+ `"webrtc": false` disables it.
343
+ - **Fallback**: without ffmpeg, or when WebRTC cannot connect, the page polls
344
+ screenshots (2–5 fps). It switches to WebRTC once that is ready.
345
+ - **Control**: watching never blocks agents. _Take control_ blocks agents with
346
+ `human_in_control` (like an owner pause) until _Release_. Control is also released 15 s after
347
+ the browser goes away. Mouse, wheel, keyboard, double-click, drag, and right-click go over a
348
+ WebRTC DataChannel to Cua Driver's desktop-scope input. A Mac's Cmd shortcuts map to Ctrl on
349
+ Windows. Use the text box for IME / pasted text. Viewer actions are audited as `viewer_*`
350
+ (pointer moves are not).
351
+ - Limits: the UAC secure desktop and the lock screen cannot be captured or controlled from
352
+ the user session. `gdigrab` captures the whole virtual desktop, so with several monitors input
353
+ mapping is only right when `ddagrab` (primary display) works. There is no audio and no
354
+ clipboard sync.
355
+
356
+ ## Tools
357
+
358
+ | Tool | Lease | Notes |
359
+ | --------------------------------------- | ----- | ----------------------------------------------------------------------------------------------------------- |
360
+ | `computer_list` | no | OS, status, lease holder, display size |
361
+ | `computer_screenshot` | no | full primary display, long edge `maxImageDimension` (1280) |
362
+ | `computer_cursor_position` | no | in screenshot pixels |
363
+ | `computer_click` | yes | `{x, y, button?, count?, modifiers?}` |
364
+ | `computer_move` / `computer_drag` | yes | `{x, y}` / `{from, to}` |
365
+ | `computer_scroll` | yes | `{x, y, direction, amount?}` |
366
+ | `computer_type` | yes | `{text}` ≤ 4096 characters, into the frontmost app |
367
+ | `computer_key` | yes | `{keys}` e.g. `["ctrl","s"]`; Windows: Win and Ctrl+Alt+Del refused; macOS: lock/log out/force quit refused |
368
+ | `computer_launch` | yes | `{app}` name, or `.exe` path on Windows |
369
+ | `computer_wait` | no | `{ms}` ≤ 10000, then screenshot |
370
+ | `computer_acquire` / `computer_release` | — | explicit lease control |
371
+
372
+ Coordinates are pixels of the latest screenshot; Cua Driver maps them back to the
373
+ screen. Input tools return a screenshot taken `actionSettleMs` after the action.
374
+
375
+ `--expose-driver-tools` additionally exposes Cua Driver's own tools as `cua_*`
376
+ (window-scoped background input, accessibility `element_token`s, `get_window_state`, ...).
377
+ Read-only ones skip the lease; the rest need it. Script, config, extension, recording,
378
+ process-kill, clipboard, and file-transfer tools are never exposed.
379
+
380
+ ## Safety behavior
381
+
382
+ - **Control lease**: one client (token name) controls input at a time; others get
383
+ `computer_busy`. The lease is taken on first input, expires after `leaseIdleSeconds`
384
+ (120) idle or `leaseMaxSeconds` (1800), and is freed by `computer_release`.
385
+ - **Human first**: pointer movement the agent did not cause blocks input for
386
+ `humanIdleSeconds` (5) with `human_active`. On X11, a key or mouse button pressed on a real
387
+ device does too (XInput2 raw events; XTEST input from the driver, the viewer or xdotool is
388
+ ignored, and no key is recorded).
389
+ - **Emergency stop**: press `p` in the daemon console, run `agentconnect-desktop pause`, or on
390
+ X11 press `Ctrl+Alt+Shift+Escape` (`stopHotkey` in `config.json`, `""` turns it off) or
391
+ choose _Stop all agent input_ in the tray icon. Input then returns `human_in_control` until
392
+ `r` / `agentconnect-desktop resume`, or the tray's _Resume agent input_, which appears only
393
+ for a pause made on this machine; screenshots keep working. The hotkey and the tray can only
394
+ stop: an agent that sends the hotkey stops itself, and no agent input reaches the tray while
395
+ paused.
396
+ - **Indicator** (X11): a tray icon shows green when ready, amber with the holder while an agent
397
+ controls the desktop, red when paused, and grey without the Control Plane. It needs a status
398
+ tray in the panel (Xfce's Status Tray plugin); `doctor` warns when there is none.
399
+ - **No replay**: a timeout, driver crash, or cancellation returns `uncertain` with a fresh
400
+ screenshot. The action is never retried automatically.
401
+ - **Audit**: every call is appended to `audit.log` in the data dir; typed text is
402
+ recorded by length only. Screenshots are not stored.
403
+
404
+ ## Register with a Control Plane
405
+
406
+ In the console, create a Computer registration. It gives you a one-time token and an exchange URL.
407
+ Then run this on the desktop:
408
+
409
+ ```
410
+ agentconnect-desktop setup --register TOKEN --cp-url URL [--name NAME]
411
+ ```
412
+
413
+ This runs the whole `setup` and also exchanges the token for this desktop's Computer key. The key
414
+ is stored in `computer-key.json` in the data dir with mode 0600. The keychain or credential
415
+ manager comes later. No local client token is minted. `register TOKEN --cp-url URL` does only the
416
+ exchange, and you restart the daemon afterwards.
417
+
418
+ A registered daemon dials the Computer channel and reports its status, capabilities and `doctor`
419
+ results there. The design
420
+ ([computer-use.md §3.6, §8.2](../../docs/designs/computer-use.md#36-registration-and-pairing-design))
421
+ describes this. The daemon acts only while it holds a channel key that the Control Plane renews,
422
+ and this applies to local-token MCP clients and `/view` too. If it loses the Control Plane, it keeps
423
+ working until that key expires (5 minutes by default) and then refuses with `unavailable` until it
424
+ reconnects. If the Computer is revoked or deleted, the daemon refuses input, screenshots and
425
+ viewers at once. It also forgets the key and stays refused after restarts, until you register it
426
+ again. `doctor` shows the registration as the `control plane` check. `stdio` mode has no channel,
427
+ so a registered desktop refuses it.
428
+
429
+ ## Commands
430
+
431
+ ```
432
+ agentconnect-desktop setup [--client NAME] [--host H] [--port P] (once, on the desktop: downloads + OS approvals + doctor)
433
+ agentconnect-desktop setup --register TOKEN --cp-url URL [--name NAME] (the same, registered as a Computer)
434
+ agentconnect-desktop register TOKEN --cp-url URL [--name NAME] (only the registration; restart the daemon after)
435
+ agentconnect-desktop init [--client NAME] [--host H] [--port P] (non-interactive part: config + token + driver)
436
+ agentconnect-desktop install-ffmpeg (Windows; run does it in the background)
437
+ agentconnect-desktop run [--host H] [--port P] [--expose-driver-tools] [--rtc-address A] [--rtc-ports MIN-MAX]
438
+ agentconnect-desktop stdio [--expose-driver-tools]
439
+ agentconnect-desktop token add NAME | token list | token remove NAME
440
+ agentconnect-desktop status | pause | resume
441
+ agentconnect-desktop doctor [--here] [--json] (read-only checks; exit 1 on failure)
442
+ agentconnect-desktop service install|uninstall|start|stop|restart|status (macOS .app LaunchAgent, Linux systemd user unit)
443
+ agentconnect-desktop autostart enable | disable | start (Windows)
444
+ ```
445
+
446
+ With the AgentConnect CLI installed, every command also runs as `agentconnect computer <command>`
447
+ (for example `agentconnect computer token list`; `desktop` is a hidden alias). The CLI runs
448
+ `AgentConnect Desktop.app` when it is in `~/Applications` or `/Applications` (macOS), otherwise the
449
+ version store's `current/dist/agentconnect-desktop.mjs`, otherwise `app/agentconnect-desktop.mjs` in
450
+ the data dir, where the bundle's install scripts put it; `AGENTCONNECT_DESKTOP_ENTRY` points it at a
451
+ development build instead. See
452
+ [cli-daemon-split.md §1.2](../../docs/designs/cli-daemon-split.md#12-computer-desktop-daemon).
453
+
454
+ Data dir: `%LOCALAPPDATA%\AgentConnect\desktop` on Windows, `~/.agentconnect/desktop`
455
+ elsewhere (`AGENTCONNECT_DESKTOP_HOME` overrides). `config.json` also holds
456
+ `maxImageDimension`, `leaseIdleSeconds`, `leaseMaxSeconds`, `humanIdleSeconds`,
457
+ `actionScreenshot`, `actionSettleMs`, `allowSystemKeys`, `stopHotkey`, `exposeDriverTools`,
458
+ `driverPath`, `driverCallTimeoutMs`, `keepAwake`, and the viewer's `webrtc`, `ffmpegPath`, `streamFps`,
459
+ `streamMaxWidth`, `streamBitrateKbps`, `rtcAddress`, and `rtcPortRange`.
460
+
461
+ ## Development
462
+
463
+ ```bash
464
+ pnpm --filter @agentconnect.md/desktop test # fake driver, no desktop needed
465
+ pnpm --filter @agentconnect.md/desktop typecheck
466
+ pnpm --filter @agentconnect.md/desktop dev # tsx src/cli.ts run
467
+ pnpm --filter @agentconnect.md/desktop test:xvfb # bundle, then the Xvfb suite (needs DISPLAY, xterm, xdotool)
468
+ pnpm --filter @agentconnect.md/desktop license-scan # scan bundle/ after `bundle` (and `bundle:macos`)
469
+ pnpm --filter @agentconnect.md/desktop build # the npm package's dist/ (what a release publishes)
470
+ pnpm --filter @agentconnect.md/desktop license-scan dist
471
+ ```
472
+
473
+ CI runs two more jobs on pull requests that touch this package (`.github/workflows/test.yaml`):
474
+
475
+ - **Desktop (Xvfb)**: `xvfb-run` at 1600x900, then `test/xvfb.test.ts` against the bundled daemon. `init` and
476
+ `install-ffmpeg` download the pinned, sha256-checked Cua Driver and ffmpeg; the suite checks a screenshot, a pointer
477
+ move against X's own pointer, a click plus typing into an `xterm`, a `/view` screenshot frame, and one H.264 keyframe
478
+ received over WebRTC and decoded by ffmpeg.
479
+ - **Desktop licenses**: `scripts/license-scan.ts` reads the esbuild metafiles `bundle` and `bundle:macos` write to
480
+ `bundle/.meta/`, attributes every bundled input to its npm package, and fails on AGPL / FSL / BSL (Business Source) /
481
+ SSPL / GPL, on a license missing from its allowlist, on forbidden licenses in the extracted `*.LEGAL.txt`, on
482
+ ffmpeg or Cua Driver inside an artifact, and on any undeclared executable or archive. The macOS app is scanned the
483
+ same way when built (CI builds only the Linux bundle). The Docker image carries Debian packages under their own
484
+ licenses but no ffmpeg, which it downloads on first start while the distribution question of design §11 is open
485
+ with counsel.
486
+
487
+ A release builds `dist/` with the release version (`scripts/publish-desktop-if-changed.sh`), scans it with the same
488
+ script, and publishes it to npm with the agent daemon's channels (`latest` / `rc`). To try the install flow without
489
+ npm, publish to a local registry (for example verdaccio) and set `npm_config_registry` and `AGENTCONNECT_NPM_REGISTRY`
490
+ to it.
491
+
492
+ macOS and Linux also work for local development (`init` installs the matching driver
493
+ build). On macOS, `--direct` attributes Accessibility and Screen Recording to the terminal
494
+ that starts the daemon.
495
+
496
+ ## Differences from the design (deliberate for M0)
497
+
498
+ - One Computer per daemon. Registration and the Computer channel exist, but the data plane,
499
+ `ComputerPolicy` beyond the lease limits, approvals, object-store screenshots and per-agent
500
+ authorization do not, so agents still connect with local client tokens.
501
+ - `computer_screenshot` has no `display`, `window`, `region`, or `maxDimension`
502
+ parameters: the image size is fixed per daemon so the driver's capture mapping stays
503
+ consistent. Window-level capture is available through `cua_get_window_state`.
504
+ - `computer_scroll` uses Cua Driver's `direction` + `amount` instead of `dx/dy`.
505
+ - `computer_*` input uses Cua Driver's desktop scope, which moves the real pointer.
506
+ Background, window-scoped input (design §6) is reachable through the `cua_*` tools.
507
+ - No screen-border indicator, and the tray icon and global hotkey are X11 only: elsewhere the
508
+ console window and its title serve as indicator and stop control.
509
+ - The human takeover path is the built-in `/view` page (WebRTC). The design does not specify
510
+ this part.
511
+ - Cua Driver runs as a `cua-driver mcp --direct` child process (design open question 2),
512
+ which isolates driver crashes and keeps MCP as the boundary.