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

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,514 @@
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@latest computer install --register <token> --cp-url <exchange URL>
175
+ # against a release-candidate Control Plane, which the console detects and shows:
176
+ npx -y @agentconnect.md/cli@rc computer install --channel rc --register <token> --cp-url <exchange URL>
177
+ ```
178
+
179
+ The CLI downloads `@agentconnect.md/desktop` from npm, verifies its integrity, unpacks it into
180
+ `~/.agentconnect/desktop/versions/<version>`, points `~/.agentconnect/desktop/current` at it, and
181
+ runs `setup --register` (Cua Driver, ffmpeg, grants, the systemd user service on `current`, then
182
+ `doctor` inside the service). `--name`, `--no-service` and other `setup` options pass through; run
183
+ over SSH, it installs the service, which then starts at the next desktop login. Then:
184
+
185
+ ```bash
186
+ agentconnect computer doctor # the installed version and every check, from inside the service
187
+ agentconnect computer status # version, service, and whether the daemon answers
188
+ agentconnect computer upgrade # install the channel latest (--version / --channel), restart, roll back if unhealthy
189
+ agentconnect computer versions # installed versions
190
+ agentconnect computer stop | start | restart
191
+ agentconnect computer uninstall # service, autostart entry and versions; --purge also deletes config and the key
192
+ ```
193
+
194
+ (`npx -y @agentconnect.md/cli` stands in for `agentconnect` without a global install.) Config, the
195
+ Computer key and the grants stay in `~/.agentconnect/desktop`, so an upgrade keeps the
196
+ registration; a desktop installed by the retired Control Plane `install.sh` keeps its data, and `computer install`
197
+ moves its service onto the store. `AGENTCONNECT_NPM_REGISTRY` points the CLI at another registry.
198
+
199
+ Requirements for running from source: Node.js ≥ 24.12 (the official tarball in `~/.local/node` works), a logged-in
200
+ graphical session, and `setup` run inside it. Over SSH, export the session's `WAYLAND_DISPLAY`,
201
+ `XDG_RUNTIME_DIR`, and `DBUS_SESSION_BUS_ADDRESS` first. `setup` downloads Cua Driver and a pinned
202
+ static ffmpeg (6.0, libx264).
203
+
204
+ - **X11**: Cua Driver captures and injects through X11/XTest, and the viewer uses ffmpeg `x11grab`.
205
+ Nothing needs approving.
206
+ - **GNOME Wayland**, tested on Ubuntu 26.04 / GNOME 50. X11 grabbers only see Xwayland clients
207
+ there, so:
208
+ - Cua Driver runs in its Wayland mode (`CUA_DRIVER_RS_ENABLE_WAYLAND=1`, set by the daemon). It
209
+ needs its GNOME Shell helper extension `winrects@cua` for pixel geometry, capture, and safe
210
+ input. `setup` installs it; **log out and back in once** so GNOME loads it.
211
+ - Input goes through the xdg-desktop-portal **RemoteDesktop** portal plus libei. `setup` presses
212
+ Shift once so GNOME asks; switch on _Allow Remote Interaction_ and keep _Remember This
213
+ Selection_. The grant is kept in `~/.config/cua-driver/libei-persistent.token`.
214
+ - Video uses the **ScreenCast** portal plus PipeWire. A small Python helper (`python3-gi`) holds
215
+ the portal session and runs GStreamer (`pipewiresrc ! videorate ! y4menc`) into the pinned
216
+ ffmpeg, because distro GStreamer often has no H.264 encoder. `setup` asks once; the restore
217
+ token is kept in `screencast.token`.
218
+ - GNOME shows its screen-sharing indicator in the top bar while either session is active, and
219
+ `run` holds an idle inhibitor (`gnome-session-inhibit`) so the screen does not blank or lock.
220
+ - Known limits: libei types key codes, so text that needs an input method (e.g. Chinese) is
221
+ dropped. KDE Plasma Wayland lacks Cua Driver's activation adapter.
222
+ - **Background service**: `setup` installs and starts a systemd user unit
223
+ (`~/.config/systemd/user/agentconnect-desktop.service`). It is wanted by
224
+ `graphical-session.target` (GNOME, KDE) and also started by an XDG autostart entry
225
+ (`~/.config/autostart/agentconnect-desktop.desktop`). That entry imports the session's display
226
+ variables first, for desktops such as Xfce that never activate the target.
227
+ - The unit runs a copy of the bundle from the data dir, restarts on failure, and stops with the
228
+ session. Xfce never stops `graphical-session.target`, so the autostart entry also records the
229
+ logind session id and the daemon exits when that session closes; the next login starts it
230
+ again with the new display.
231
+ - `ExecCondition` skips it when there is no display, so it never crash-loops outside a session.
232
+ - Cua Driver runs in its own `systemd-run --scope`, so apps an agent launched survive service
233
+ restarts and upgrades.
234
+ - Commands: `service status | restart | stop | uninstall`; logs with
235
+ `journalctl --user -u agentconnect-desktop`. `setup --no-service` skips the unit.
236
+ - X11 types characters missing from the keymap by remapping a spare keycode. On real Xorg,
237
+ back-to-back remaps raced (`你好` became `好好`), so non-ASCII text is sent one character at a
238
+ time. Desktops other than GNOME get `xset s off -dpms` instead of an idle inhibitor.
239
+ - Tested: Ubuntu 26.04 arm64 with GNOME 50 Wayland, and with Xfce 4.20 on Xorg (LightDM, because
240
+ GDM 50 no longer starts X11 sessions) and on Xvfb.
241
+
242
+ ## Run in Docker
243
+
244
+ The `desktop` image (`ghcr.io/agentconnect-md/desktop`, amd64 and arm64, published with each release) is a
245
+ Debian trixie desktop with Xvfb, a lean Xfce, Firefox ESR, Noto CJK fonts, the pinned Node.js, and the daemon
246
+ with Cua Driver preinstalled. ffmpeg (GPL) is not in the image: the daemon downloads the pinned, sha256-checked
247
+ build on first start into the volume, and the viewer polls screenshots until it is there. The entrypoint starts Xvfb on `:1` (`RESOLUTION`, default
248
+ `1600x900x24`), a session D-Bus, Xfce and a maximized browser, then runs `run` in the foreground as user
249
+ `desktop`. The browser is restarted when it exits or crashes (`BROWSER`, default `firefox-esr`; `none` turns it
250
+ off; `BROWSER_URL` opens a page). `docker stop` stops the daemon, closes the browser, then the session, with exit
251
+ code 0. `/home/desktop` is a volume: the Computer key, config, audit log, and the browser profile survive a new
252
+ container. A `HEALTHCHECK` runs `doctor --json --daemon` inside the running daemon.
253
+
254
+ Register it as a Computer: create a token in the console under **Computers → Add Computer** (pick **Docker** for
255
+ this command), then
256
+
257
+ ```sh
258
+ docker run -d --name desktop --hostname desktop-1 --shm-size=1g --restart unless-stopped \
259
+ -v desktop-home:/home/desktop \
260
+ -e AGENTCONNECT_CP_URL='https://<control plane>' -e AGENTCONNECT_REGISTER_TOKEN='<token>' \
261
+ ghcr.io/agentconnect-md/desktop:latest
262
+ ```
263
+
264
+ | Variable | Meaning |
265
+ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
266
+ | `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) |
267
+ | `AGENTCONNECT_REGISTER_TOKEN_FILE` | a file holding the token instead, such as a Docker or Kubernetes secret under `/run/secrets` |
268
+ | `AGENTCONNECT_CP_URL` | the Control Plane URL the console prints with the token |
269
+ | `AGENTCONNECT_COMPUTER_NAME` | the name to register with when the console gave none; defaults to the container hostname |
270
+ | `PUBLIC_URL` | how clients outside reach the published port, printed instead of the container address |
271
+ | `RTC_ADDRESS`, `RTC_PORTS` | the public IP offered as the only WebRTC candidate (defaults to `PUBLIC_URL`'s host), and a fixed UDP range |
272
+ | `RESOLUTION`, `LISTEN_HOST`, `PORT` | the screen, and where the daemon listens inside the container (`0.0.0.0:7681`) |
273
+
274
+ For a server, [`docker/compose.yaml`](docker/compose.yaml) with [`docker/compose.env.example`](docker/compose.env.example)
275
+ does the same with its settings in `.env`; the guide is
276
+ [docs/self-host-desktop-container.md](../../docs/self-host-desktop-container.md), including TURN for the viewer.
277
+
278
+ The token is spent on the first start; the stored key remembers its sha256, so later starts with the same token
279
+ and Control Plane leave it unused (keep the volume). A different token, such as a new one after a revoke or for
280
+ another org, is spent and replaces the stored key. Without a token the
281
+ daemon runs in local mode and writes its first client token to `~/.agentconnect/desktop/client-token` instead of
282
+ the log (`docker exec desktop cat /home/desktop/.agentconnect/desktop/client-token`).
283
+
284
+ The console's viewer reaches a registered container through the relay and needs no published port. For a viewer
285
+ or MCP client that connects directly, publish the port and the UDP range and say where they are:
286
+
287
+ ```sh
288
+ docker run -d --name desktop --shm-size=1g -v desktop-home:/home/desktop \
289
+ -p 7681:7681 -p 7700-7709:7700-7709/udp \
290
+ -e PUBLIC_URL="http://$HOST_IP:7681" -e RTC_PORTS=7700-7709 ghcr.io/agentconnect-md/desktop:latest
291
+ ```
292
+
293
+ WebRTC behind NAT: by default the daemon offers the address the browser reached it on, which in bridge
294
+ networking is the container IP. `--rtc-address` (`rtcAddress`) replaces it as the only host candidate,
295
+ and `--rtc-ports MIN-MAX` (`rtcPortRange`) pins the UDP ports so they can be published. Each viewer uses
296
+ up to two ports from the range, so publish at least 8. With `--network host` on a Linux host neither is
297
+ needed.
298
+
299
+ Build it locally from the repository root, and smoke-test it the way CI does:
300
+
301
+ ```sh
302
+ docker build -f packages/desktop/docker/Dockerfile -t agentconnect-desktop .
303
+ packages/desktop/docker/smoke.sh agentconnect-desktop
304
+ ```
305
+
306
+ ## Connect Claude Code
307
+
308
+ The first `run` (or `init`) prints the exact command, for example:
309
+
310
+ ```text
311
+ claude mcp add --transport http computer http://127.0.0.1:7681/mcp --header "Authorization: Bearer acd_..."
312
+ ```
313
+
314
+ The daemon listens on `127.0.0.1` unless you ask otherwise, so by default only clients on the
315
+ same machine (or through an `ssh -L` tunnel) reach it. `--host 0.0.0.0` on `setup`, `init`, or
316
+ `run` listens on every interface; `setup` and `init` save it in `config.json` (`"host"`) for the
317
+ service. Traffic is plain HTTP and the token is the only protection, so do that on a trusted
318
+ network only. A `config.json` written by an earlier prototype build may still say `"0.0.0.0"`.
319
+
320
+ If Claude Code runs on the same Windows machine you can use stdio instead:
321
+ `claude mcp add computer -- node <path>\agentconnect-desktop.mjs stdio`.
322
+
323
+ ## Remote desktop viewer
324
+
325
+ Open `http://<host>:7681/view` in any modern browser and sign in with a client token.
326
+ The first `run` prints a ready link of the form `/view#token=...`. The token is in the URL fragment,
327
+ so it never reaches the server or proxy logs, and the page keeps it in `localStorage`.
328
+
329
+ - **Video**: ffmpeg captures the primary display (Windows: `ddagrab`, falling back to
330
+ `gdigrab`; macOS: `avfoundation`; Linux: `x11grab`). It encodes H.264 constrained baseline
331
+ (`libx264 ultrafast/zerolatency`, `streamFps` 30, `streamMaxWidth` 1920,
332
+ `streamBitrateKbps` 2500, 1 s keyframe interval, keyframes capped by a small VBV buffer), and [werift](https://github.com/shinyoshiaki/werift-webrtc)
333
+ (MIT, pure TypeScript) sends it over WebRTC. One capture is shared by every viewer and stops
334
+ when the last viewer leaves. Packets are paced at 1.5x the bitrate, because an unpaced keyframe
335
+ burst overflows thin links (e.g. Tailscale over the internet) and is lost whole. On slow links,
336
+ lower `streamBitrateKbps` / `streamMaxWidth`.
337
+ - **Signaling** uses the same port and token (`POST /view/api/rtc/offer` / `answer`). The
338
+ daemon is ICE-lite with host candidates only: no STUN/TURN and no external server. It
339
+ advertises the address the browser reached it on, so Tailscale and LAN both work. Media runs
340
+ over UDP on random ports, so the Windows firewall must allow Node.js (accept the prompt).
341
+ - **ffmpeg**: on Windows, the first `run` downloads the pinned gyan.dev build
342
+ (9.0.2 essentials, GPL, ~110 MB, sha256-checked) in the background, or run `install-ffmpeg`
343
+ ahead of time. Elsewhere it uses `ffmpeg` from PATH. `ffmpegPath` overrides it, and
344
+ `"webrtc": false` disables it.
345
+ - **Fallback**: without ffmpeg, or when WebRTC cannot connect, the page polls
346
+ screenshots (2–5 fps). It switches to WebRTC once that is ready.
347
+ - **Control**: watching never blocks agents. _Take control_ blocks agents with
348
+ `human_in_control` (like an owner pause) until _Release_. Control is also released 15 s after
349
+ the browser goes away. Mouse, wheel, keyboard, double-click, drag, and right-click go over a
350
+ WebRTC DataChannel to Cua Driver's desktop-scope input. A Mac's Cmd shortcuts map to Ctrl on
351
+ Windows. Use the text box for IME / pasted text. Viewer actions are audited as `viewer_*`
352
+ (pointer moves are not).
353
+ - Limits: the UAC secure desktop and the lock screen cannot be captured or controlled from
354
+ the user session. `gdigrab` captures the whole virtual desktop, so with several monitors input
355
+ mapping is only right when `ddagrab` (primary display) works. There is no audio and no
356
+ clipboard sync.
357
+
358
+ ## Tools
359
+
360
+ | Tool | Lease | Notes |
361
+ | --------------------------------------- | ----- | ----------------------------------------------------------------------------------------------------------- |
362
+ | `computer_list` | no | OS, status, lease holder, display size |
363
+ | `computer_screenshot` | no | full primary display, long edge `maxImageDimension` (1280) |
364
+ | `computer_cursor_position` | no | in screenshot pixels |
365
+ | `computer_click` | yes | `{x, y, button?, count?, modifiers?}` |
366
+ | `computer_move` / `computer_drag` | yes | `{x, y}` / `{from, to}` |
367
+ | `computer_scroll` | yes | `{x, y, direction, amount?}` |
368
+ | `computer_type` | yes | `{text}` ≤ 4096 characters, into the frontmost app |
369
+ | `computer_key` | yes | `{keys}` e.g. `["ctrl","s"]`; Windows: Win and Ctrl+Alt+Del refused; macOS: lock/log out/force quit refused |
370
+ | `computer_launch` | yes | `{app}` name, or `.exe` path on Windows |
371
+ | `computer_wait` | no | `{ms}` ≤ 10000, then screenshot |
372
+ | `computer_acquire` / `computer_release` | — | explicit lease control |
373
+
374
+ Coordinates are pixels of the latest screenshot; Cua Driver maps them back to the
375
+ screen. Input tools return a screenshot taken `actionSettleMs` after the action.
376
+
377
+ `--expose-driver-tools` additionally exposes Cua Driver's own tools as `cua_*`
378
+ (window-scoped background input, accessibility `element_token`s, `get_window_state`, ...).
379
+ Read-only ones skip the lease; the rest need it. Script, config, extension, recording,
380
+ process-kill, clipboard, and file-transfer tools are never exposed.
381
+
382
+ ## Safety behavior
383
+
384
+ - **Control lease**: one client (token name) controls input at a time; others get
385
+ `computer_busy`. The lease is taken on first input, expires after `leaseIdleSeconds`
386
+ (120) idle or `leaseMaxSeconds` (1800), and is freed by `computer_release`.
387
+ - **Human first**: pointer movement the agent did not cause blocks input for
388
+ `humanIdleSeconds` (5) with `human_active`. On X11, a key or mouse button pressed on a real
389
+ device does too (XInput2 raw events; XTEST input from the driver, the viewer or xdotool is
390
+ ignored, and no key is recorded).
391
+ - **Emergency stop**: press `p` in the daemon console, run `agentconnect-desktop pause`, or on
392
+ X11 press `Ctrl+Alt+Shift+Escape` (`stopHotkey` in `config.json`, `""` turns it off) or
393
+ choose _Stop all agent input_ in the tray icon. Input then returns `human_in_control` until
394
+ `r` / `agentconnect-desktop resume`, or the tray's _Resume agent input_, which appears only
395
+ for a pause made on this machine; screenshots keep working. The hotkey and the tray can only
396
+ stop: an agent that sends the hotkey stops itself, and no agent input reaches the tray while
397
+ paused.
398
+ - **Indicator** (X11): a tray icon shows green when ready, amber with the holder while an agent
399
+ controls the desktop, red when paused, and grey without the Control Plane. It needs a status
400
+ tray in the panel (Xfce's Status Tray plugin); `doctor` warns when there is none.
401
+ - **No replay**: a timeout, driver crash, or cancellation returns `uncertain` with a fresh
402
+ screenshot. The action is never retried automatically.
403
+ - **Audit**: every call is appended to `audit.log` in the data dir; typed text is
404
+ recorded by length only. Screenshots are not stored.
405
+
406
+ ## Register with a Control Plane
407
+
408
+ In the console, create a Computer registration. It gives you a one-time token and an exchange URL.
409
+ Then run this on the desktop:
410
+
411
+ ```
412
+ agentconnect-desktop setup --register TOKEN --cp-url URL [--name NAME]
413
+ ```
414
+
415
+ This runs the whole `setup` and also exchanges the token for this desktop's Computer key. The key
416
+ is stored in `computer-key.json` in the data dir with mode 0600. The keychain or credential
417
+ manager comes later. No local client token is minted. `register TOKEN --cp-url URL` does only the
418
+ exchange, and you restart the daemon afterwards.
419
+
420
+ A registered daemon dials the Computer channel and reports its status, capabilities and `doctor`
421
+ results there. The design
422
+ ([computer-use.md §3.6, §8.2](../../docs/designs/computer-use.md#36-registration-and-pairing-design))
423
+ describes this. The daemon acts only while it holds a channel key that the Control Plane renews,
424
+ and this applies to local-token MCP clients and `/view` too. If it loses the Control Plane, it keeps
425
+ working until that key expires (5 minutes by default) and then refuses with `unavailable` until it
426
+ reconnects. If the Computer is revoked or deleted, the daemon refuses input, screenshots and
427
+ viewers at once. It also forgets the key and stays refused after restarts, until you register it
428
+ again. `doctor` shows the registration as the `control plane` check. `stdio` mode has no channel,
429
+ so a registered desktop refuses it.
430
+
431
+ ## Commands
432
+
433
+ ```
434
+ agentconnect-desktop setup [--client NAME] [--host H] [--port P] (once, on the desktop: downloads + OS approvals + doctor)
435
+ agentconnect-desktop setup --register TOKEN --cp-url URL [--name NAME] (the same, registered as a Computer)
436
+ agentconnect-desktop register TOKEN --cp-url URL [--name NAME] (only the registration; restart the daemon after)
437
+ agentconnect-desktop init [--client NAME] [--host H] [--port P] (non-interactive part: config + token + driver)
438
+ agentconnect-desktop install-ffmpeg (Windows; run does it in the background)
439
+ agentconnect-desktop run [--host H] [--port P] [--expose-driver-tools] [--rtc-address A] [--rtc-ports MIN-MAX]
440
+ agentconnect-desktop stdio [--expose-driver-tools]
441
+ agentconnect-desktop token add NAME | token list | token remove NAME
442
+ agentconnect-desktop status | pause | resume
443
+ agentconnect-desktop doctor [--here] [--json] (read-only checks; exit 1 on failure)
444
+ agentconnect-desktop service install|uninstall|start|stop|restart|status (macOS .app LaunchAgent, Linux systemd user unit)
445
+ agentconnect-desktop autostart enable | disable | start (Windows)
446
+ ```
447
+
448
+ With the AgentConnect CLI installed, every command also runs as `agentconnect computer <command>`
449
+ (for example `agentconnect computer token list`; `desktop` is a hidden alias). The CLI runs
450
+ `AgentConnect Desktop.app` when it is in `~/Applications` or `/Applications` (macOS), otherwise the
451
+ version store's `current/dist/agentconnect-desktop.mjs`, otherwise `app/agentconnect-desktop.mjs` in
452
+ the data dir, where the bundle's install scripts put it; `AGENTCONNECT_DESKTOP_ENTRY` points it at a
453
+ development build instead. See
454
+ [cli-daemon-split.md §1.2](../../docs/designs/cli-daemon-split.md#12-computer-desktop-daemon).
455
+
456
+ Data dir: `%LOCALAPPDATA%\AgentConnect\desktop` on Windows, `~/.agentconnect/desktop`
457
+ elsewhere (`AGENTCONNECT_DESKTOP_HOME` overrides). `config.json` also holds
458
+ `maxImageDimension`, `leaseIdleSeconds`, `leaseMaxSeconds`, `humanIdleSeconds`,
459
+ `actionScreenshot`, `actionSettleMs`, `allowSystemKeys`, `stopHotkey`, `exposeDriverTools`,
460
+ `driverPath`, `driverCallTimeoutMs`, `keepAwake`, and the viewer's `webrtc`, `ffmpegPath`, `streamFps`,
461
+ `streamMaxWidth`, `streamBitrateKbps`, `rtcAddress`, and `rtcPortRange`.
462
+
463
+ ## Development
464
+
465
+ ```bash
466
+ pnpm --filter @agentconnect.md/desktop test # fake driver, no desktop needed
467
+ pnpm --filter @agentconnect.md/desktop typecheck
468
+ pnpm --filter @agentconnect.md/desktop dev # tsx src/cli.ts run
469
+ pnpm --filter @agentconnect.md/desktop test:xvfb # bundle, then the Xvfb suite (needs DISPLAY, xterm, xdotool)
470
+ pnpm --filter @agentconnect.md/desktop license-scan # scan bundle/ after `bundle` (and `bundle:macos`)
471
+ pnpm --filter @agentconnect.md/desktop build # the npm package's dist/ (what a release publishes)
472
+ pnpm --filter @agentconnect.md/desktop license-scan dist
473
+ ```
474
+
475
+ CI runs two more jobs on pull requests that touch this package (`.github/workflows/test.yaml`):
476
+
477
+ - **Desktop (Xvfb)**: `xvfb-run` at 1600x900, then `test/xvfb.test.ts` against the bundled daemon. `init` and
478
+ `install-ffmpeg` download the pinned, sha256-checked Cua Driver and ffmpeg; the suite checks a screenshot, a pointer
479
+ move against X's own pointer, a click plus typing into an `xterm`, a `/view` screenshot frame, and one H.264 keyframe
480
+ received over WebRTC and decoded by ffmpeg.
481
+ - **Desktop licenses**: `scripts/license-scan.ts` reads the esbuild metafiles `bundle` and `bundle:macos` write to
482
+ `bundle/.meta/`, attributes every bundled input to its npm package, and fails on AGPL / FSL / BSL (Business Source) /
483
+ SSPL / GPL, on a license missing from its allowlist, on forbidden licenses in the extracted `*.LEGAL.txt`, on
484
+ ffmpeg or Cua Driver inside an artifact, and on any undeclared executable or archive. The macOS app is scanned the
485
+ same way when built (CI builds only the Linux bundle). The Docker image carries Debian packages under their own
486
+ licenses but no ffmpeg, which it downloads on first start while the distribution question of design §11 is open
487
+ with counsel.
488
+
489
+ A release builds `dist/` with the release version (`scripts/publish-desktop-if-changed.sh`), scans it with the same
490
+ script, and publishes it to npm with the agent daemon's channels (`latest` / `rc`). To try the install flow without
491
+ npm, publish to a local registry (for example verdaccio) and set `npm_config_registry` and `AGENTCONNECT_NPM_REGISTRY`
492
+ to it.
493
+
494
+ macOS and Linux also work for local development (`init` installs the matching driver
495
+ build). On macOS, `--direct` attributes Accessibility and Screen Recording to the terminal
496
+ that starts the daemon.
497
+
498
+ ## Differences from the design (deliberate for M0)
499
+
500
+ - One Computer per daemon. Registration and the Computer channel exist, but the data plane,
501
+ `ComputerPolicy` beyond the lease limits, approvals, object-store screenshots and per-agent
502
+ authorization do not, so agents still connect with local client tokens.
503
+ - `computer_screenshot` has no `display`, `window`, `region`, or `maxDimension`
504
+ parameters: the image size is fixed per daemon so the driver's capture mapping stays
505
+ consistent. Window-level capture is available through `cua_get_window_state`.
506
+ - `computer_scroll` uses Cua Driver's `direction` + `amount` instead of `dx/dy`.
507
+ - `computer_*` input uses Cua Driver's desktop scope, which moves the real pointer.
508
+ Background, window-scoped input (design §6) is reachable through the `cua_*` tools.
509
+ - No screen-border indicator, and the tray icon and global hotkey are X11 only: elsewhere the
510
+ console window and its title serve as indicator and stop control.
511
+ - The human takeover path is the built-in `/view` page (WebRTC). The design does not specify
512
+ this part.
513
+ - Cua Driver runs as a `cua-driver mcp --direct` child process (design open question 2),
514
+ which isolates driver crashes and keeps MCP as the boundary.