@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.
- package/CHANGELOG.md +12 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +24 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/skills/agent-desktop/SKILL.md +83 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +12 -0
- package/dist/builtin/workflows/builtin/adversarial-verification.js +3 -3
- package/dist/builtin/workflows/builtin/{chunk-49kkbdpr.js → chunk-2f1shjah.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-dcxscfnb.js → chunk-37gm3bzk.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-2jfst7e4.js → chunk-45fn5q5p.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-hx8v4p97.js → chunk-4ek3v2wm.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-z3pajrfs.js → chunk-8d40nyz9.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-qhm35ntx.js → chunk-g3b2zf8r.js} +2 -2
- package/dist/builtin/workflows/builtin/{chunk-6jpwc09k.js → chunk-h6bt0r0g.js} +2 -2
- package/dist/builtin/workflows/builtin/{chunk-9cm2ckq2.js → chunk-x25rjw4y.js} +4 -4
- package/dist/builtin/workflows/builtin/{chunk-apagcs4r.js → chunk-xqcy5nve.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-w4xp1w1f.js → chunk-yb55w6me.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-k5q3t7je.js → chunk-yjgfnbgd.js} +1 -1
- package/dist/builtin/workflows/builtin/classify-and-act.js +3 -3
- package/dist/builtin/workflows/builtin/fan-out-and-synthesize.js +3 -3
- package/dist/builtin/workflows/builtin/generate-and-filter.js +3 -3
- package/dist/builtin/workflows/builtin/goal.js +3 -3
- package/dist/builtin/workflows/builtin/index.js +11 -11
- package/dist/builtin/workflows/builtin/loop-until-done.js +3 -3
- package/dist/builtin/workflows/builtin/open-claude-design.js +3 -3
- package/dist/builtin/workflows/builtin/ralph.js +3 -3
- package/dist/builtin/workflows/builtin/steering-context.js +1 -1
- package/dist/builtin/workflows/builtin/tournament.js +3 -3
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +7 -7
- package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +4 -4
- package/dist/builtin/workflows/src/index.js +4 -4
- package/dist/builtin/workflows/src/local-worker-entry.mjs +4 -4
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +2 -3
- package/dist/core/system-prompt.js.map +1 -1
- package/docs/computer-use.md +119 -98
- package/docs/getting-started/first-session.md +1 -1
- package/docs/skills.md +2 -2
- package/docs/usage.md +1 -1
- package/docs/workflows/authoring.md +105 -141
- package/docs/workflows/verification.md +14 -13
- package/npm-shrinkwrap.json +32 -32
- package/package.json +3 -3
- package/dist/builtin/subagents/skills/cua-driver/BROWSER.md +0 -562
- package/dist/builtin/subagents/skills/cua-driver/EMBEDDING.md +0 -685
- package/dist/builtin/subagents/skills/cua-driver/LINUX.md +0 -412
- package/dist/builtin/subagents/skills/cua-driver/MACOS.md +0 -996
- package/dist/builtin/subagents/skills/cua-driver/README.md +0 -123
- package/dist/builtin/subagents/skills/cua-driver/RECORDING.md +0 -132
- package/dist/builtin/subagents/skills/cua-driver/RUNTIME.md +0 -115
- package/dist/builtin/subagents/skills/cua-driver/SKILL.md +0 -108
- package/dist/builtin/subagents/skills/cua-driver/VISUAL.md +0 -125
- package/dist/builtin/subagents/skills/cua-driver/WINDOWS.md +0 -832
- 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.
|