@bastani/atomic 0.9.56-alpha.1 → 0.9.57-alpha.1

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +1 -1
  4. package/dist/builtin/subagents/CHANGELOG.md +24 -0
  5. package/dist/builtin/subagents/package.json +1 -1
  6. package/dist/builtin/subagents/skills/agent-desktop/SKILL.md +83 -0
  7. package/dist/builtin/web-access/package.json +1 -1
  8. package/dist/builtin/workflows/CHANGELOG.md +12 -0
  9. package/dist/builtin/workflows/builtin/adversarial-verification.js +3 -3
  10. package/dist/builtin/workflows/builtin/{chunk-49kkbdpr.js → chunk-2f1shjah.js} +1 -1
  11. package/dist/builtin/workflows/builtin/{chunk-dcxscfnb.js → chunk-37gm3bzk.js} +1 -1
  12. package/dist/builtin/workflows/builtin/{chunk-2jfst7e4.js → chunk-45fn5q5p.js} +1 -1
  13. package/dist/builtin/workflows/builtin/{chunk-hx8v4p97.js → chunk-4ek3v2wm.js} +1 -1
  14. package/dist/builtin/workflows/builtin/{chunk-z3pajrfs.js → chunk-8d40nyz9.js} +1 -1
  15. package/dist/builtin/workflows/builtin/{chunk-qhm35ntx.js → chunk-g3b2zf8r.js} +2 -2
  16. package/dist/builtin/workflows/builtin/{chunk-6jpwc09k.js → chunk-h6bt0r0g.js} +2 -2
  17. package/dist/builtin/workflows/builtin/{chunk-9cm2ckq2.js → chunk-x25rjw4y.js} +4 -4
  18. package/dist/builtin/workflows/builtin/{chunk-apagcs4r.js → chunk-xqcy5nve.js} +1 -1
  19. package/dist/builtin/workflows/builtin/{chunk-w4xp1w1f.js → chunk-yb55w6me.js} +1 -1
  20. package/dist/builtin/workflows/builtin/{chunk-k5q3t7je.js → chunk-yjgfnbgd.js} +1 -1
  21. package/dist/builtin/workflows/builtin/classify-and-act.js +3 -3
  22. package/dist/builtin/workflows/builtin/fan-out-and-synthesize.js +3 -3
  23. package/dist/builtin/workflows/builtin/generate-and-filter.js +3 -3
  24. package/dist/builtin/workflows/builtin/goal.js +3 -3
  25. package/dist/builtin/workflows/builtin/index.js +11 -11
  26. package/dist/builtin/workflows/builtin/loop-until-done.js +3 -3
  27. package/dist/builtin/workflows/builtin/open-claude-design.js +3 -3
  28. package/dist/builtin/workflows/builtin/ralph.js +3 -3
  29. package/dist/builtin/workflows/builtin/steering-context.js +1 -1
  30. package/dist/builtin/workflows/builtin/tournament.js +3 -3
  31. package/dist/builtin/workflows/package.json +1 -1
  32. package/dist/builtin/workflows/src/extension/index.bundle.mjs +7 -7
  33. package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +4 -4
  34. package/dist/builtin/workflows/src/index.js +4 -4
  35. package/dist/builtin/workflows/src/local-worker-entry.mjs +4 -4
  36. package/dist/core/system-prompt.d.ts.map +1 -1
  37. package/dist/core/system-prompt.js +2 -3
  38. package/dist/core/system-prompt.js.map +1 -1
  39. package/docs/computer-use.md +119 -98
  40. package/docs/getting-started/first-session.md +1 -1
  41. package/docs/skills.md +2 -2
  42. package/docs/usage.md +1 -1
  43. package/docs/workflows/authoring.md +105 -141
  44. package/docs/workflows/verification.md +14 -13
  45. package/npm-shrinkwrap.json +32 -32
  46. package/package.json +3 -3
  47. package/dist/builtin/subagents/skills/cua-driver/BROWSER.md +0 -562
  48. package/dist/builtin/subagents/skills/cua-driver/EMBEDDING.md +0 -685
  49. package/dist/builtin/subagents/skills/cua-driver/LINUX.md +0 -412
  50. package/dist/builtin/subagents/skills/cua-driver/MACOS.md +0 -996
  51. package/dist/builtin/subagents/skills/cua-driver/README.md +0 -123
  52. package/dist/builtin/subagents/skills/cua-driver/RECORDING.md +0 -132
  53. package/dist/builtin/subagents/skills/cua-driver/RUNTIME.md +0 -115
  54. package/dist/builtin/subagents/skills/cua-driver/SKILL.md +0 -108
  55. package/dist/builtin/subagents/skills/cua-driver/VISUAL.md +0 -125
  56. package/dist/builtin/subagents/skills/cua-driver/WINDOWS.md +0 -832
  57. package/dist/builtin/subagents/skills/cua-driver/WORKFLOW.md +0 -226
@@ -1,685 +0,0 @@
1
- # Embedding cua-driver in your application without introducing new permissions
2
-
3
- This guide is for teams shipping a macOS app (an "agent harness") that wants
4
- cua-driver's background computer-use and agent-cursor overlay **inside their
5
- own app**, without shipping a second app bundle and without their users ever
6
- seeing a second macOS permission prompt. Your app requests Accessibility and
7
- Screen Recording once; the embedded driver inherits those grants.
8
-
9
- A working daemon-host reference lives in the cua repo at
10
- `libs/cua-driver/rust/examples/embedded-host-macos/`
11
- (https://github.com/trycua/cua). This doc ships standalone in the skill
12
- pack, so the path is given rather than a relative link.
13
-
14
- ## How macOS attributes these permissions (what you must know)
15
-
16
- macOS TCC (the privacy system behind System Settings → Privacy & Security)
17
- does not attribute Accessibility or Screen Recording to an executable path.
18
- It attributes them to the **responsible process**: the app at the top of the
19
- process's launch chain, as tracked by the kernel/LaunchServices. When your
20
- signed app spawns a child with `posix_spawn`, `NSTask`/`Process`, or plain
21
- `fork`/`exec`, that child stays inside _your_ responsibility chain: TCC
22
- checks made by the child are answered with **your app's** grants, and any
23
- prompt it triggered would name **your app**. This is exactly the behavior
24
- embedding relies on: grant once to the host, and every well-behaved child
25
- inherits. (Apple documents the attribution chain; you can watch it live with
26
- `log stream --debug --predicate 'subsystem == "com.apple.TCC" AND eventMessage BEGINSWITH "AttributionChain"'`.)
27
-
28
- Two things break the chain, and both are things the embedded driver must
29
- _not_ do (and, in embedded mode, does not do). First, launching via
30
- LaunchServices (`open -a …`, `NSWorkspace.open`) makes the launched app its
31
- own responsible process. Second, a process can explicitly _disclaim_
32
- responsibility for a child (`responsibility_spawnattrs_setdisclaim`), making
33
- the child its own responsible process: standalone cua-driver does this on
34
- purpose so its permissions attach to a stable `com.trycua.driver` identity
35
- instead of whatever terminal launched it. Embedded mode turns that off.
36
-
37
- Note this is TCC **responsibility** inheritance: it is unrelated to App
38
- Sandbox inheritance (`com.apple.security.inherit`). This guide assumes a
39
- non-sandboxed host, which is typical for agent harnesses; a sandboxed host
40
- spawning a non-sandboxed helper raises separate App Sandbox questions that
41
- embedded mode does not address.
42
-
43
- ## Preferred application SDK: same-process runtime
44
-
45
- Python and TypeScript applications should normally import the packaged SDK and
46
- create `CuaDriver` directly. This path does not start an executable or open a
47
- socket, and TCC checks execute as the importing application:
48
-
49
- ```ts
50
- import { CuaDriver } from '@trycua/cua-driver';
51
-
52
- const driver = CuaDriver.create(undefined);
53
- try {
54
- const metadata = await driver.metadata();
55
- // Invoke typed driver operations here.
56
- } finally {
57
- await driver.shutdown();
58
- driver.uniffiDestroy();
59
- }
60
- ```
61
-
62
- The direct runtime never presents macOS permission UI. Even
63
- `check_permissions({prompt: true})` is forced into a read-only check and
64
- reports the host as the responsible permission owner. After the host changes
65
- Accessibility or Screen Recording grants, fully relaunch the host before
66
- creating a replacement runtime.
67
-
68
- The AppKit agent-cursor overlay is not available in an arbitrary direct
69
- runtime. Until a host installs a certified main-thread UI adapter, overlay
70
- methods return structured `facility_unavailable` results. Use the private
71
- worker or daemon-backed host when the visible overlay is required.
72
-
73
- Use the daemon-backed host below only when the application must also provide a
74
- stable MCP endpoint to an external agent, coordinate external clients, or keep
75
- the automation runtime isolated from the application process.
76
-
77
- ## Launching the daemon-backed host
78
-
79
- ```sh
80
- # env var form: set by the host on the child process
81
- CUA_DRIVER_EMBEDDED=1 CUA_DRIVER_HOST_BUNDLE_ID=com.yourco.yourapp \
82
- cua-driver serve --socket /tmp/yourapp-cua.sock
83
-
84
- # after the daemon socket is ready, start the stdio MCP proxy
85
- CUA_DRIVER_EMBEDDED=1 cua-driver mcp --socket /tmp/yourapp-cua.sock
86
- ```
87
-
88
- Requirements on the host side:
89
-
90
- - **Spawn `cua-driver serve --embedded` directly** as a child process
91
- (`Process`/`NSTask`, `posix_spawn`, `exec` from your own code). Do
92
- **not** launch the daemon via `open(1)` or `NSWorkspace`: that hands it
93
- to LaunchServices and breaks inheritance.
94
- - Give the daemon a private socket and wait until it is accepting connections.
95
- - Spawn `cua-driver mcp --embedded --socket <path>` and speak MCP over that
96
- proxy's stdin/stdout (line-delimited JSON-RPC). The proxy never executes
97
- tools; the host-owned daemon does.
98
- - Request Accessibility and Screen Recording **from your app** before (or
99
- after: the driver just reports "not granted" until then) starting the
100
- driver, using `AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt: true])`
101
- and `CGRequestScreenCaptureAccess()`.
102
-
103
- Only the exact value `CUA_DRIVER_EMBEDDED=1` enables embedded mode; anything
104
- else is ignored (fail-safe). `--host-bundle-id` is an advisory label echoed
105
- in `check_permissions` output and logs: it is **not** a trust signal; trust
106
- comes from the OS responsibility chain, so there is nothing to spoof by
107
- setting it.
108
-
109
- ## Choosing the agent permission mode
110
-
111
- Embedded hosts own their user-facing permission experience, but they must
112
- select Cua Driver's immutable runtime mode at trusted launch. The choices are:
113
-
114
- - `standard`: promptless routine automation, with explicit host authorization
115
- only for residual boundaries such as an existing logged-in Chromium profile.
116
- - `bounded`: unattended work inside an approved tool and resource manifest.
117
- - `unrestricted`: no Cua runtime approvals; use only when the host accepts the
118
- consequences of prompt injection and unintended actions.
119
-
120
- Direct SDK hosts may install `DriverAuthorizationHost` for residual
121
- standard-mode decisions and `DriverActivityObserver` for content-free action
122
- events. Cua Driver does not render its own authorization modal or banner.
123
-
124
- For unrestricted embedding, use the explicit two-part environment contract:
125
-
126
- ```sh
127
- CUA_DRIVER_EMBEDDED=1 \
128
- CUA_DRIVER_PERMISSION_MODE=unrestricted \
129
- CUA_DRIVER_DANGEROUSLY_BYPASS_APPROVALS=1 \
130
- cua-driver serve --embedded --socket /tmp/yourapp-cua.sock
131
- ```
132
-
133
- Both values are required and contradictory values fail before the daemon
134
- binds. They belong in trusted launcher configuration; never expose either as
135
- an agent-settable MCP or raw-socket argument. Interactive operators can use
136
- the equivalent single CLI shortcut, `--dangerously-bypass-approvals`, which
137
- selects unrestricted mode and records the acknowledgement. The older
138
- `autonomous` mode name remains accepted as an alias for `bounded` during
139
- migration.
140
-
141
- ### Node and Electron daemon hosts
142
-
143
- Use the embedded host in `@trycua/cua-driver` instead of implementing process
144
- and socket management in every host. It starts a private daemon directly, waits
145
- until its socket accepts connections, returns SDK and MCP connection details,
146
- and owns restart and cleanup:
147
-
148
- ```ts
149
- import { CuaDriver, EmbeddedCuaDriverHost } from '@trycua/cua-driver';
150
-
151
- const embedded = new EmbeddedCuaDriverHost(
152
- '/path/inside/YourApp.app/Contents/Resources/cua-driver',
153
- 'com.example.your-app'
154
- );
155
- const connection = await embedded.start();
156
- const driver = CuaDriver.connect(connection.socketPath);
157
- // Application calls use driver; an agent runtime uses connection.mcp.
158
- driver.uniffiDestroy();
159
- await embedded.stop();
160
- embedded.uniffiDestroy();
161
- ```
162
-
163
- The package does not install or bundle cua-driver. Ship a compatible executable
164
- outside Electron's ASAR archive, preserve its executable bit, and sign the
165
- nested executable before signing and notarizing the enclosing macOS app.
166
- Electron main processes can use the package's `/electron` entry point for
167
- low-level Accessibility and Screen Recording requests after `app.whenReady()`;
168
- the calls run as the importing host, not the child driver. The host still owns
169
- permission UI, status, and restart policy. These functions use the same
170
- generated Rust SDK; there is no second native FFI dependency. Some macOS
171
- releases refuse to raise a Screen Recording prompt; in that case, open the
172
- Screen Recording settings pane with
173
- `openMacOSScreenRecordingSettings()`, ask the user to add the host app, and
174
- start the driver only after both checks return true.
175
-
176
- Destroy the SDK client and call `await embedded.stop()` from every orderly
177
- shutdown path. If grants change, destroy the SDK client, call
178
- `embedded.restart()`, and reconnect. Electron hosts must defer their first
179
- `before-quit` event until cleanup completes because asynchronous cleanup cannot
180
- run after the host process exits. Normal OpenClaw
181
- gateway and Hermes YAML configurations remain standalone integrations; use the
182
- package only when their signed Node or Electron app process directly owns the
183
- daemon child.
184
-
185
- ### Lifecycle rules
186
-
187
- - Concurrent `start()` calls coalesce into one daemon generation.
188
- - Treat `connection` as generation-scoped. After `restart()`, destroy old SDK
189
- clients and MCP proxies and reconnect from the newly returned descriptor.
190
- - Stop new work, end sessions, close proxies/clients, then await `stop()`.
191
- `stop()` is idempotent and cancels an in-progress start.
192
- - Observe unexpected termination with `waitForExit(generation)` in Node or
193
- `wait_for_exit(generation)` in Python. Never blindly replay an action whose
194
- completion is unknown.
195
- - The Rust owner holds a parent-liveness pipe, so host death closes the daemon;
196
- orderly shutdown should still await `stop()`.
197
- - Capture modality belongs to each observation or action target, not to the
198
- lifecycle session. One embedded daemon can concurrently serve exact window
199
- and desktop calls without changing session state.
200
- - Permission changes require destroying clients, restarting the daemon, and
201
- reconnecting. A connection from the old generation is never reusable.
202
-
203
- ## Bounding the post-action window observation
204
-
205
- After an input action, the driver watches the window list for a short time so
206
- it can report a menu, dialog, or new window that the action opened. On macOS,
207
- the watch lasts up to 1000 ms. The action's result waits for at most the first
208
- 300 ms of it; when nothing has changed by then, the result returns and the rest
209
- of the watch runs detached, still holding the focus-steal lease. A window that
210
- opens during that remainder is reported on the next action's result as
211
- `After the previous action returned, it opened new window(s): …`. The next
212
- action, or any activation the driver performs, ends the detached remainder
213
- first. On Linux X11 the result waits for the whole watch. A host that already observes
214
- its target continuously can shorten it through two variables set at trusted
215
- launch, in the environment of the `serve --embedded` child (or of the host
216
- process when you use the same-process runtime). `EmbeddedCuaDriverHost` starts
217
- that child from an allowlisted environment that admits both variables, so pass
218
- them in its `environment` option or set them in the host process:
219
-
220
- | Variable | Meaning | Default | Accepted range |
221
- | ------------------------------------- | ---------------------------------------------------- | ------------------------------- | ----------------------------------------- |
222
- | `CUA_DRIVER_WINDOW_CHANGE_TIMEOUT_MS` | Longest wait for a window change after each action. | 1000 on macOS, 800 on Linux X11 | 0 to 10000; larger values are clamped |
223
- | `CUA_DRIVER_WINDOW_CHANGE_POLL_MS` | Interval between window-list reads during that wait. | 50 | 5 to 1000, and never longer than the wait |
224
-
225
- Unset, empty, or unparsable values, such as `-1`, `1.5`, or `100ms`, keep the
226
- default, so a daemon launched without these variables behaves exactly as
227
- before. No tool argument can change the bound. Every ingress strips
228
- underscore-prefixed arguments, so an agent cannot shorten its own focus
229
- protection.
230
-
231
- ```sh
232
- CUA_DRIVER_EMBEDDED=1 \
233
- CUA_DRIVER_WINDOW_CHANGE_TIMEOUT_MS=200 \
234
- cua-driver serve --embedded --socket /tmp/yourapp-cua.sock
235
- ```
236
-
237
- A shorter wait costs something on each platform. Choose a value knowingly.
238
-
239
- - **macOS: less focus protection.** The watch also holds a focus-steal lease:
240
- while it runs, the driver reactivates the app that was frontmost before the
241
- action if any other app activates, such as a browser opened by a link. The
242
- lease ends when the watch ends. With a nonzero value, cross-app activations
243
- are reverted only for that long after the action returns. With `0`, the lease
244
- is released as soon as the action returns. The separate target-pid guard
245
- remains, and it still covers the action plus a 50 ms settle when the target
246
- was not frontmost. An app that activates later stays frontmost, so the host
247
- must detect and correct that itself.
248
- - **macOS: missing result suffixes.** A window that appears after the wait
249
- ends is not reported. With `0`, results never carry
250
- `Action opened new window(s): …` or
251
- `Action caused a different app to become frontmost.`
252
- - **Linux X11 foreground delivery: weaker evidence.** A foreground-delivery
253
- action (`"delivery_mode": "foreground"`) reports `effect: "confirmed"` when
254
- the target opened or closed a window during the wait. A dialog that maps after a shorter wait
255
- leaves the action `unverifiable`. Qt file dialogs can take 300 to 600 ms to
256
- map. With `0`, foreground actions never report window-change evidence. Focus
257
- checks and `suspected_noop` detection are unchanged.
258
- - **Windows: no effect.** The Windows adapter does not wait for window changes
259
- after an action, so it ignores both variables.
260
-
261
- Measured on macOS with Calculator in the background and Terminal frontmost, a
262
- background AX click took 1153 ms with the default, 325 ms with `200`, and 78 ms
263
- with `0`.
264
-
265
- ### Steps inside `run_steps` and `run_script` (macOS)
266
-
267
- Two waits make up most of a macOS action's latency: the agent cursor's glide to
268
- the target (600 ms or more with the default `steady` timing) and the first 300 ms
269
- of the window watch above. A call that is one step of a `run_steps` batch or a
270
- `run_script` program is followed at once by another step, so the driver shortens
271
- both for it: the step waits at most 120 ms for the cursor to arrive and 100 ms
272
- for a window change. The cursor keeps gliding and the watch keeps running
273
- detached, as above, so a window that opens late is still reported on the next
274
- action's result. The last step of a `run_steps` batch, and every direct call,
275
- keep the full waits, so the batch's final result still reports what it left
276
- open. A script has no last step the driver can know in advance; read the window
277
- with `getState()` or `waitFor()` after the action that opens something.
278
-
279
- An embedding host that wants every step to play the full glide and watch, for
280
- example to record a demo, sets `CUA_DRIVER_BATCH_PACING=full` in the same
281
- environment as the variables above. Any other value, or none, applies the
282
- shortened waits. No tool argument can change it.
283
-
284
- ## Pacing synthesized keystrokes (macOS)
285
-
286
- The macOS keyboard adapter waits a fixed gap between each key down and up and
287
- between consecutive keys. For `type_text`, `press_key`, and `hotkey`, that gap
288
- is most of the call's latency. A host whose targets accept faster input can
289
- shorten it at trusted launch with one variable, in the same environment as the
290
- window-observation variables above. `EmbeddedCuaDriverHost` admits it too:
291
-
292
- | Variable | Meaning | Default | Accepted range |
293
- | ----------------------- | ----------------------------------------------- | ------- | ----------------------------------------------------- |
294
- | `CUA_DRIVER_KEY_GAP_MS` | Gap between synthesized key events, per event. | 8 | 2 to 100; smaller values are raised, larger clamped |
295
-
296
- Unset, empty, or unparsable values keep the default. No tool argument can
297
- change the gap. An explicit `type_text` `delay_ms` still applies on top of it.
298
-
299
- - **macOS only.** Linux keeps its fixed 10 ms key delay and Windows its own
300
- pacing; neither reads this variable.
301
- - **What a shorter gap costs.** Some targets coalesce or drop key events that
302
- arrive back to back, most often remote-input forwarders and apps under heavy
303
- load. Read the field back after typing before relying on a low value for a
304
- new target. The 2 ms floor exists because `0` posts events with no gap at all.
305
-
306
- Measured on macOS 27 with TextEdit in the background, typing a 185-character
307
- ASCII sentence through the PID-routed key path took about 3650 ms at the default
308
- and about 980 ms at `2`. All ten read-backs (five per value) matched exactly.
309
-
310
- ## What embedded mode changes (and what it doesn't)
311
-
312
- | | Standalone | Embedded (`CUA_DRIVER_EMBEDDED=1`) |
313
- | --------------------------------------------- | ----------------------------- | -------------------------------------- |
314
- | Responsibility disclaim re-exec | ON (owns its TCC identity) | OFF (stays in the host's chain) |
315
- | Tool execution process | `serve` daemon | host-spawned `serve --embedded` daemon |
316
- | Daemon auto-relaunch of the installed app | Yes, when installed | Never (would leave the host's chain) |
317
- | TCC identity | `com.trycua.driver` | the host app |
318
- | Permission prompts / startup gate | May prompt once | **Never prompts** |
319
- | Settings → Privacy & Security entries | CuaDriver | your app only |
320
- | `check_permissions` `source.attribution` | `driver-daemon` (or `caller`) | `host` |
321
- | Overlay, background input, capture, all tools | full | full, identical |
322
-
323
- Everything else is unchanged: the agent-cursor overlay, background (no-focus-steal)
324
- clicking and typing, AX tree reads, and per-window screenshots.
325
- When embedded mode is off, nothing in this feature is active: standalone
326
- behavior is byte-for-byte what it was.
327
-
328
- ## The responsibility-chain requirement, exactly
329
-
330
- The host must be the responsible process for the driver. That holds
331
- automatically when you spawn the `serve` daemon directly and embedded mode
332
- is on. If the daemon were allowed to disclaim (standalone behavior), macOS
333
- would treat it as its own responsible process: your user would get a _second_ prompt
334
- attributed to the driver binary, a second Settings entry, and capture/AX
335
- would fail until that second grant. That is the exact experience embedding exists
336
- to eliminate. Embedded mode short-circuits the disclaim re-exec
337
- (`responsibility.rs`) and the installed-app daemon relaunch. MCP is
338
- always a proxy, so the embedded daemon remains the single process that checks
339
- TCC and executes tools.
340
-
341
- ### App + gateway architectures
342
-
343
- `--embedded` does not transfer a GUI app's permissions to the driver; it
344
- only keeps the driver inside its **spawner's** TCC responsibility chain. If
345
- your product has a GUI app that owns the macOS grants and a separate
346
- gateway, daemon, or Node process that spawns MCP servers, registering
347
- `cua-driver serve --embedded` with the gateway makes the daemon inherit the
348
- gateway's identity, not the app's. Spawn the daemon from the GUI app itself;
349
- gateways may connect an MCP proxy to the app-owned private socket.
350
-
351
- ```text
352
- Wrong (inherits the gateway's identity): Right:
353
-
354
- gateway / node daemon YourApp.app
355
- └─ cua-driver serve --embedded ├─ cua-driver serve --embedded
356
- └─ cua-driver mcp --embedded
357
- --socket <private-path>
358
- ```
359
-
360
- `health_report(include=["bundle_identity"])` detects this wiring error by
361
- resolving the daemon's direct parent through macOS. It fails when the parent
362
- is not an identifiable app or when its observed bundle identifier differs
363
- from `CUA_DRIVER_HOST_BUNDLE_ID`. `check_permissions.source.attribution`
364
- still describes the configured mode; use the two reports together when
365
- diagnosing embedding.
366
-
367
- ## Reading `check_permissions` from the host
368
-
369
- Call the `check_permissions` tool over MCP. In embedded mode it never raises
370
- a dialog (the `prompt` argument is ignored) and returns:
371
-
372
- ```json
373
- {
374
- "accessibility": true,
375
- "screen_recording": true,
376
- "screen_recording_capturable": null,
377
- "direct_capture_status": "not_checked",
378
- "source": {
379
- "attribution": "host",
380
- "host_bundle_id": "com.yourco.yourapp",
381
- "embedded": true,
382
- "pid": 12345,
383
- "responsible_ppid": 12300,
384
- "executable": "/path/to/cua-driver",
385
- "disclaim_env": false,
386
- "note": "Embedded mode: these booleans reflect the HOST app's TCC grant…"
387
- }
388
- }
389
- ```
390
-
391
- - `accessibility` / `screen_recording`: the live TCC state _of your app's
392
- grant_, answered from inside the driver process (which shares your
393
- identity). If both are true, it is safe to drive the desktop.
394
- - `screen_recording_capturable` / `direct_capture_status`: embedded
395
- `check_permissions` is read-only and never runs Tahoe's prompt-capable
396
- ScreenCaptureKit probe, so these are `null` / `not_checked`. The host owns
397
- the permission UX and should verify pixels with an explicit screenshot or
398
- capture operation after explaining the prompt.
399
- - `source.attribution` values:
400
- - `host`: embedded mode; booleans reflect the host's grant. What you
401
- should always see when embedding.
402
- - `driver-daemon`: standalone daemon owning `com.trycua.driver`. If you
403
- see this while embedding, embedded mode is not actually set.
404
- - `caller`: a non-embedded, non-bundle launch (e.g. someone ran the
405
- binary from a terminal); booleans reflect the terminal's grants.
406
-
407
- If a permission is missing, the correct reaction is: **the host requests
408
- it** (the two API calls above), then re-calls `check_permissions`. The
409
- driver never owns the host's OS permission experience.
410
-
411
- Heads-up on grant timing: macOS caches TCC answers per process. If your app
412
- requests/receives the grants _after_ the driver child is already running,
413
- restart the driver child so it re-queries with a fresh cache.
414
-
415
- ## Minimal host example (copy-paste)
416
-
417
- The file below is the complete reference host: mirrored verbatim from
418
- `libs/cua-driver/rust/examples/embedded-host-macos/ExampleAgentHarness.swift`
419
- in the cua repo (which also has a build-and-run `demo.sh` covering the
420
- TCC-reset flow).
421
- It requests the two grants as the host, spawns an embedded daemon plus an MCP
422
- proxy, and runs the whole demo sequence: attribution check, background screenshot,
423
- background AX read, agent-cursor glide.
424
-
425
- `ExampleAgentHarness.swift`:
426
-
427
- ```swift
428
- // SPDX-License-Identifier: MIT
429
- // Copyright (c) 2026 Cua AI, Inc.
430
-
431
- // ExampleAgentHarness: minimal reference host for embedding cua-driver.
432
- // Mirrored verbatim in Skills/cua-driver/EMBEDDING.md ("Minimal host
433
- // example"): keep the two in sync.
434
- //
435
- // Runs the one-grant demo sequence from EMBEDDING.md end to end:
436
- // 1. Requests Accessibility + Screen Recording AS THE HOST (the only
437
- // prompts the user ever sees), then
438
- // 2. spawns an embedded cua-driver daemon plus its stdio MCP proxy and
439
- // verifies attribution, takes a background screenshot,
440
- // reads a background app's window state, and glides the agent-cursor
441
- // overlay, with zero driver-side prompts.
442
- //
443
- // Launched via `open` (see demo.sh) the app has no terminal, so all
444
- // output also goes to /tmp/cua-embedded-demo.log.
445
-
446
- import Foundation
447
- import ApplicationServices
448
- import CoreGraphics
449
-
450
- let logPath = "/tmp/cua-embedded-demo.log"
451
- FileManager.default.createFile(atPath: logPath, contents: nil)
452
- let logFile = FileHandle(forWritingAtPath: logPath)!
453
- func log(_ line: String) {
454
- print(line)
455
- logFile.write((line + "\n").data(using: .utf8)!)
456
- }
457
-
458
- // 1. Request both grants AS THE HOST: the only prompts in the whole flow.
459
- let axOpts = ["AXTrustedCheckOptionPrompt": true] as CFDictionary
460
- let ax = AXIsProcessTrustedWithOptions(axOpts)
461
- let sr = CGRequestScreenCaptureAccess()
462
- log("host grants, accessibility: \(ax), screen recording: \(sr)")
463
- // Keep going even without grants: the run registers BOTH rows in one pass
464
- // (the AX request above, plus, on newer macOS, where the app only appears
465
- // in the Screen Recording pane after a real ScreenCaptureKit attempt, the
466
- // embedded driver's live probe below, registered as THE HOST, which is the
467
- // point of embedding). Grant both in one Settings visit, then re-run.
468
- if !ax || !sr {
469
- log("after this run: grant the missing item(s) in System Settings, then re-run")
470
- }
471
-
472
- // 2. Spawn the daemon as a DIRECT child (never via `open`/NSWorkspace:
473
- // that breaks responsibility inheritance), then attach an MCP proxy.
474
- let driverPath = ProcessInfo.processInfo.environment["CUA_DRIVER_PATH"]
475
- ?? "/usr/local/bin/cua-driver"
476
- let socketPath = "/tmp/cua-embedded-\(ProcessInfo.processInfo.processIdentifier).sock"
477
- var env = ProcessInfo.processInfo.environment
478
- env["CUA_DRIVER_EMBEDDED"] = "1"
479
- env["CUA_DRIVER_HOST_BUNDLE_ID"] = Bundle.main.bundleIdentifier ?? ""
480
-
481
- let daemon = Process()
482
- daemon.executableURL = URL(fileURLWithPath: driverPath)
483
- daemon.arguments = ["serve", "--embedded", "--socket", socketPath]
484
- daemon.environment = env
485
- daemon.standardOutput = logFile
486
- daemon.standardError = logFile
487
- try daemon.run()
488
-
489
- let deadline = Date().addingTimeInterval(10)
490
- while !FileManager.default.fileExists(atPath: socketPath) && Date() < deadline {
491
- Thread.sleep(forTimeInterval: 0.05)
492
- }
493
- guard FileManager.default.fileExists(atPath: socketPath) else {
494
- log("embedded daemon did not create \(socketPath)")
495
- daemon.terminate()
496
- exit(1)
497
- }
498
-
499
- let driver = Process()
500
- driver.executableURL = URL(fileURLWithPath: driverPath)
501
- driver.arguments = ["mcp", "--embedded", "--socket", socketPath]
502
- driver.environment = env
503
- let toDriver = Pipe(), fromDriver = Pipe()
504
- driver.standardInput = toDriver
505
- driver.standardOutput = fromDriver
506
- try driver.run()
507
-
508
- // 3. Line-delimited JSON-RPC 2.0 over the child's stdio.
509
- var buffer = Data()
510
- func send(_ obj: [String: Any]) {
511
- var data = try! JSONSerialization.data(withJSONObject: obj)
512
- data.append(0x0A)
513
- toDriver.fileHandleForWriting.write(data)
514
- }
515
- func readMessage() -> [String: Any] {
516
- while true {
517
- if let nl = buffer.firstIndex(of: 0x0A) {
518
- let line = buffer.subdata(in: buffer.startIndex..<nl)
519
- buffer.removeSubrange(buffer.startIndex...nl)
520
- if line.isEmpty { continue }
521
- return (try? JSONSerialization.jsonObject(with: line)) as? [String: Any] ?? [:]
522
- }
523
- let chunk = fromDriver.fileHandleForReading.availableData
524
- if chunk.isEmpty { log("driver exited unexpectedly"); exit(1) }
525
- buffer.append(chunk)
526
- }
527
- }
528
- var nextId = 0
529
- func call(_ tool: String, _ args: [String: Any] = [:]) -> [String: Any] {
530
- nextId += 1
531
- send(["jsonrpc": "2.0", "id": nextId, "method": "tools/call",
532
- "params": ["name": tool, "arguments": args]])
533
- while true {
534
- let msg = readMessage()
535
- if msg["id"] as? Int == nextId {
536
- if let error = msg["error"] as? [String: Any] {
537
- log("RPC error for \(tool): \(error)")
538
- }
539
- return msg["result"] as? [String: Any] ?? [:]
540
- }
541
- }
542
- }
543
-
544
- nextId += 1
545
- send(["jsonrpc": "2.0", "id": nextId, "method": "initialize", "params": [
546
- "protocolVersion": "2024-11-05", "capabilities": [:],
547
- "clientInfo": ["name": "ExampleAgentHarness", "version": "0.1"]]])
548
- _ = readMessage()
549
- send(["jsonrpc": "2.0", "method": "notifications/initialized"])
550
- log("embedded cua-driver daemon + proxy started (\(driverPath)): no driver prompt should have appeared")
551
-
552
- // 4. health_report must observe this actual parent app, and
553
- // check_permissions must report host attribution and matching TCC results.
554
- let health = call("health_report", ["include": ["bundle_identity"]])
555
- let healthStructured = health["structuredContent"] as? [String: Any] ?? [:]
556
- let healthChecks = healthStructured["checks"] as? [[String: Any]] ?? []
557
- let identity = healthChecks.first { $0["name"] as? String == "bundle_identity" } ?? [:]
558
- let identityData = identity["data"] as? [String: Any] ?? [:]
559
- let hostBundleId = Bundle.main.bundleIdentifier ?? ""
560
- let identityOk = identity["status"] as? String == "pass" &&
561
- identityData["bundle_identifier"] as? String == hostBundleId &&
562
- identityData["identity_source"] as? String == "parent_application" &&
563
- (identityData["parent_process_id"] as? Int) == Int(ProcessInfo.processInfo.processIdentifier)
564
- log("health_report: bundle_identity: \(identity["status"] ?? "?"), " +
565
- "observed host: \(identityData["bundle_identifier"] ?? "?") (want: \(hostBundleId))")
566
-
567
- let perms = call("check_permissions")
568
- let structured = perms["structuredContent"] as? [String: Any] ?? [:]
569
- let source = structured["source"] as? [String: Any] ?? [:]
570
- let attribution = source["attribution"] as? String ?? "?"
571
- let permissionsMatchHost = structured["accessibility"] as? Bool == ax &&
572
- structured["screen_recording"] as? Bool == sr
573
- log("check_permissions: attribution: \(attribution) (want: host), " +
574
- "TCC matches host: \(permissionsMatchHost), " +
575
- "capturable: \(structured["screen_recording_capturable"] ?? "?")")
576
-
577
- // 5. Background AX read + window screenshot: proves both grants
578
- // inherited without focusing anything. launch_app resolves pid +
579
- // windows without foregrounding; get_window_state returns the AX
580
- // element tree AND a screenshot of the (background) window.
581
- let launch = call("launch_app", ["bundle_id": "com.apple.finder"])
582
- let launched = launch["structuredContent"] as? [String: Any] ?? [:]
583
- let pid = launched["pid"] as? Int ?? 0
584
- let windows = launched["windows"] as? [[String: Any]] ?? []
585
- let windowId = windows.first?["window_id"] as? Int ?? 0
586
- log("launch_app(Finder): pid: \(pid), windows: \(windows.count)")
587
-
588
- let state = call("get_window_state", ["pid": pid, "window_id": windowId])
589
- let images = (state["content"] as? [[String: Any]] ?? [])
590
- .filter { $0["type"] as? String == "image" }
591
- let hasTree = (state["structuredContent"] as? [String: Any])?["elements"] != nil
592
- log("get_window_state(Finder): tree: \(hasTree ? "ok" : "EMPTY"), " +
593
- "screenshot: \(images.count) image(s) (want: ≥1)")
594
-
595
- // 6. Agent-cursor glide: shows the overlay, no real-pointer move.
596
- log("watch the agent cursor glide now (no real-pointer move)…")
597
- let cursor1 = call("move_cursor", ["x": 200, "y": 200])
598
- Thread.sleep(forTimeInterval: 2)
599
- let cursor2 = call("move_cursor", ["x": 900, "y": 500])
600
- Thread.sleep(forTimeInterval: 2)
601
- let cursorOk = (cursor1["isError"] as? Bool) != true &&
602
- (cursor2["isError"] as? Bool) != true
603
- log("move_cursor: \(cursorOk ? "ok" : "FAILED")")
604
-
605
- let pass = identityOk && attribution == "host" && permissionsMatchHost &&
606
- !images.isEmpty && hasTree && cursorOk
607
- log(pass ? "DEMO COMPLETE: PASS" : "DEMO COMPLETE: FAIL")
608
- driver.terminate()
609
- daemon.terminate()
610
- exit(pass ? 0 : 1)
611
- ```
612
-
613
- Build it as a signed app bundle (a stable signing identity is what keys
614
- the TCC grant rows to your app):
615
-
616
- ```sh
617
- mkdir -p ExampleAgentHarness.app/Contents/MacOS
618
- swiftc -O ExampleAgentHarness.swift \
619
- -o ExampleAgentHarness.app/Contents/MacOS/ExampleAgentHarness \
620
- -framework ApplicationServices
621
- printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>' \
622
- '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \
623
- '<plist version="1.0"><dict>' \
624
- '<key>CFBundleExecutable</key><string>ExampleAgentHarness</string>' \
625
- '<key>CFBundleIdentifier</key><string>com.trycua.example-agent-harness</string>' \
626
- '<key>CFBundlePackageType</key><string>APPL</string>' \
627
- '</dict></plist>' > ExampleAgentHarness.app/Contents/Info.plist
628
- codesign --force --sign - ExampleAgentHarness.app # use your Developer ID in production
629
- open ExampleAgentHarness.app # `open` is correct HERE: the HOST must be its own responsible process
630
- tail -f /tmp/cua-embedded-demo.log
631
- ```
632
-
633
- ## Troubleshooting
634
-
635
- **"I still get a second permission prompt / a second Settings entry."**
636
- Embedded mode is not in effect for the process doing the TCC check. Causes,
637
- in order of likelihood: (a) `CUA_DRIVER_EMBEDDED` is not exactly `1`, or was
638
- set on your app but not passed into the daemon child's environment; (b) the daemon
639
- was launched via `open(1)` / `NSWorkspace` instead of spawned directly, so
640
- it is its own responsible process; (c) the MCP proxy connected to an old standalone `CuaDriver.app` daemon. Check
641
- `check_permissions` → `source.attribution` (must be `host`) and verify
642
- that the proxy uses the host's private socket. To see exactly which identity macOS is charging,
643
- run: `log stream --debug --predicate 'subsystem == "com.apple.TCC" AND
644
- eventMessage BEGINSWITH "AttributionChain"'` and trigger the action again.
645
-
646
- **"Screenshots come back black even though `screen_recording: true`."**
647
- The read-only permission check cannot verify direct ScreenCaptureKit access
648
- without risking a system dialog. Exercise an explicit screenshot only after
649
- the host has explained the OS permission. If that fails, the grant may not
650
- belong to the driver's current responsible identity, may have been reset, or
651
- the driver may have escaped the host's chain (see the previous item). Restart
652
- the driver child after any grant change: TCC answers are cached per process.
653
-
654
- **"The AX tree comes back empty / clicks do nothing."**
655
- `AXIsProcessTrusted()` is false for the effective identity. The host hasn't
656
- been granted Accessibility, or was granted it _after_ the driver child
657
- started (per-process cache again: restart the child), or the app was
658
- re-signed/moved so the existing grant row no longer matches it (remove and
659
- re-add it in System Settings, or `tccutil reset Accessibility <your-bundle-id>`
660
- and re-grant).
661
-
662
- **"It worked, then stopped after I updated/re-signed my app."**
663
- TCC grant rows are keyed to the app's code-signing identity. A signature
664
- change can orphan the old row. Reset and re-grant:
665
- `tccutil reset Accessibility com.yourco.yourapp && tccutil reset ScreenCapture com.yourco.yourapp`.
666
-
667
- ## Platform notes (Windows / Linux)
668
-
669
- Embedding also works on Windows and Linux (X11) with **no per-app permission
670
- ceremony**. The host still spawns a daemon in the intended interactive session
671
- or desktop, then points MCP and CLI adapters at its private socket. The
672
- one-grant inheritance story in this guide is macOS-specific because macOS is
673
- the platform where Accessibility and Screen Recording grants follow the
674
- responsibility chain.
675
-
676
- Two known exceptions:
677
-
678
- - **Windows, elevated / UWP targets**: pixel or SendInput delivery into a
679
- higher-integrity target requires an interactively launched High-IL daemon
680
- (the installed autostart task uses `RunLevel=Highest`). The
681
- `cua-driver-uia` pipe is a reserved, default-off daemon-internal boundary;
682
- embedding hosts and other public clients must not launch or connect to it.
683
- - **Linux Wayland** (compositor-specific): capture goes through XDG desktop
684
- portals, which prompt per-session at capture time and cannot be
685
- pre-granted by the host. X11 has no portal gate.