@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 +511 -2
- package/dist/agentconnect-desktop.mjs +111438 -0
- package/dist/agentconnect-desktop.mjs.LEGAL.txt +145 -0
- package/package.json +54 -4
package/README.md
CHANGED
|
@@ -1,3 +1,512 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @agentconnect.md/desktop
|
|
2
2
|
|
|
3
|
-
|
|
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.
|