Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions docs/content/docs/concepts/how-permission-policies-work.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,13 @@ description: How the Cua Driver permission policy engine evaluates YAML and Rego

import { Callout } from 'fumadocs-ui/components/callout';

Cua Driver's permission policy engine sits between an MCP client and the tool implementation. Before the driver executes any tool call it asks the policy engine whether the call is allowed. This page explains how the engine is structured, when it is active, and what guarantees it provides.
Cua Driver's permission policy engine sits between daemon clients (MCP, CLI, or direct socket clients) and the tool implementation. Before the daemon executes any tool call it asks the policy engine whether the call is allowed. This page explains how the engine is structured, when it is active, and what guarantees it provides.

## The enforcement point

Every MCP tool call passes through one of two dispatch paths:
Every tool call reaches a running `cua-driver serve` daemon. A thin `cua-driver mcp` process proxies MCP calls over the local socket; `cua-driver call` sends its one request to the same daemon.

- **In-process dispatch.** The MCP server receives the call directly, normalizes arguments, and executes the tool in the same process.
- **Daemon-proxy dispatch.** A thin `cua-driver mcp` process proxies the call to a running `cua-driver serve` daemon over a Unix socket.

The policy engine is invoked at the in-process MCP handler and at the proxy layer: once in the proxy before it forwards the call, and once in the daemon before it executes it. A policy denial at either point returns an error to the client; the tool implementation is never reached.
The policy engine is always invoked in the daemon before tool execution. The MCP proxy also evaluates the policy when `CUA_DRIVER_POLICY_FILE` is present in its environment, providing an earlier defense-in-depth check. A denial at either point returns an error to the client; the tool implementation is never reached.

## Deny-by-default

Expand All @@ -24,7 +21,7 @@ The deny-by-default behavior applies only when a policy is active. When `CUA_DRI

## Process-lifetime snapshot

The policy file is loaded once when the daemon or in-process server starts. All subsequent calls share the same immutable policy object for the lifetime of that process. There is no reload endpoint and no hot-swap path. Changing the policy takes effect only after the process restarts.
The policy file is loaded once when the daemon starts. All subsequent calls share the same immutable policy object for the lifetime of that process. There is no reload endpoint and no hot-swap path. Changing the policy takes effect only after the daemon restarts.

This makes the policy a reliable static contract: the same rule that was in effect when the daemon started will still be in effect when the last tool call of the session runs.

Expand Down Expand Up @@ -57,7 +54,7 @@ At evaluation time, the driver:
3. Sets the input and evaluates `data.cua.policy.allow`.
4. Maps the result: `true` → Allow, `false` or `undefined` → Deny, error → Error.

Because Regorus runs fully in-process and does not spawn a subprocess, there is no IPC overhead for each call.
Because Regorus runs inside the Cua Driver daemon and does not spawn a policy subprocess, there is no additional policy IPC per call.

## Argument sanitization before evaluation

Expand Down Expand Up @@ -87,4 +84,4 @@ For remote agents connecting through the daemon's network socket, the policy pro

- [Restrict tool access with permission policies](/how-to-guides/driver/restrict-tool-access): step-by-step setup guide
- [Permission policies](/reference/cua-driver/permission-policies): YAML schema and Rego input interface
- [Process model](/reference/cua-driver/process-model): how the daemon and in-process MCP server relate
- [Process model](/reference/cua-driver/process-model): how CLI and MCP clients reach the daemon
2 changes: 1 addition & 1 deletion docs/content/docs/how-to-guides/driver/keep-running.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: Register Cua Driver as a persistent daemon that starts automaticall
import { Tabs, Tab } from 'fumadocs-ui/components/tabs';
import { Callout } from 'fumadocs-ui/components/callout';

Use a **persistent daemon** for element-indexed workflows. The per-pid element cache lives inside the process, so one-shot CLI invocations *drop that cache between calls*. The daemon also gives macOS the right TCC attribution and gives Windows an interactive-session proxy.
Cua Driver requires a daemon for tool execution. The daemon owns the per-pid element cache, permission policy, recording and configuration state, macOS TCC attribution, and the Windows interactive-session context. MCP and one-shot CLI calls fail if they cannot reach it.

<Tabs items={['macOS', 'Windows', 'Linux']}>
<Tab value="macOS">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ cua-driver serve --cursor-shape teardrop # explicit; same as the default
cua-driver serve --cursor-shape arrow # opt into the procedural arrow
```

`--cursor-shape` is parsed on `serve` and `mcp`. It has no effect on one-shot CLI calls like `cua-driver call`, because those do not keep the long-lived UI runloop the overlay needs.
`--cursor-shape` is a daemon startup option. Passing it to an MCP proxy or one-shot CLI adapter does not change the already-running daemon's overlay; set it on `cua-driver serve`.

`--cursor-icon <path>` always wins over `--cursor-shape`: if you pass both, the custom file is what renders.

Expand Down
13 changes: 2 additions & 11 deletions docs/content/docs/how-to-guides/driver/windows-ssh.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,6 @@ Check these items before opening an issue:
2. Run `cua-driver status` from SSH and confirm it reports a running daemon. If it does not, use `cua-driver autostart status` to see whether the Scheduled Task is registered.
3. Run `query session` and confirm your user has a row in `Active` or `Disc` state.
4. Run `cua-driver doctor` from RDP and confirm it reports `[ok] interactive session: session N has an attached interactive desktop`.
5. Confirm that you did not pass `--no-daemon-relaunch` and that `CUA_DRIVER_RS_MCP_NO_RELAUNCH` is unset.
5. Confirm that the MCP configuration points to the same daemon socket reported by `cua-driver status`.

## Opt out of proxying

To keep `cua-driver mcp` in the current process, such as in a CI runner that already owns an interactive session, use either opt-out:

```powershell
cua-driver mcp --no-daemon-relaunch # per-invocation flag
$env:CUA_DRIVER_RS_MCP_NO_RELAUNCH = "1" # per-shell env var
```

With either form, tool calls run directly against the current session.
There is no in-process opt-out. If the interactive-session daemon is unavailable, MCP startup fails instead of attempting GUI work from the SSH session.
11 changes: 5 additions & 6 deletions docs/content/docs/reference/cua-driver/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Print a tool's full description and JSON input schema.

Invoke an MCP tool directly from the shell.

Runs the same handler the MCP server uses. JSON arguments may be passed as a positional JSON object or through stdin.
Sends the tool request to the required Cua Driver daemon. JSON arguments may be passed as a positional JSON object or through stdin. If the daemon is unavailable, the command fails; it never executes the tool in the CLI process.

**Arguments:**

Expand All @@ -64,22 +64,21 @@ Runs the same handler the MCP server uses. JSON arguments may be passed as a pos

Run the stdio MCP server.

On macOS, shell-spawned MCP processes can auto-launch and proxy through a CuaDriver.app daemon so TCC grants attach to the bundle. On Windows and Linux, MCP proxies through an already-running daemon when one is listening.
Every MCP process is a stdio proxy to a Cua Driver daemon. On macOS it can auto-launch the CuaDriver.app daemon so TCC grants attach to the bundle. On Windows and Linux, the daemon must already be running.

**Options:**

| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `--socket` | String | — | Override the daemon socket or named-pipe path used by the proxy fallback. |
| `--socket` | String | — | Override the required daemon socket or named-pipe path. |
| `--host-bundle-id` | String | — | Advisory host bundle id label echoed in check_permissions output (embedded mode). |

**Flags:**

| Name | Description |
| ---- | ----------- |
| `--no-daemon-relaunch` | Stay in-process instead of proxying through a daemon. |
| `--claude-code-computer-use-compat` | Expose the Claude Code computer-use compatibility screenshot surface. |
| `--embedded` | Run embedded inside a host app: inherit the host's TCC grants, never prompt or relaunch. Also CUA_DRIVER_EMBEDDED=1. |
| `--embedded` | Require a daemon spawned by the embedding host instead of auto-launching the standalone app. |

### `cua-driver serve`

Expand Down Expand Up @@ -141,7 +140,7 @@ Supported clients include claude, codex, cursor, antigravity, openclaw, opencode

Control trajectory recording on a running daemon.

Recording state lives in-process, so use a daemon for multi-call sessions.
Recording state lives in the daemon and is shared across daemon-backed clients.

**Options:**

Expand Down
36 changes: 19 additions & 17 deletions docs/content/docs/reference/cua-driver/embedding.mdx
Original file line number Diff line number Diff line change
@@ -1,56 +1,58 @@
---
title: Embedding
description: Run cua-driver as a direct child of your host app instead of handing off to a standalone daemon.
description: Run a Cua Driver daemon as a direct child of your host app and connect an MCP proxy to it.
---

Embedding runs `cua-driver` as a direct child of your host app instead of relaunching or proxying through a standalone daemon. On macOS, this also lets the driver inherit the host app's Accessibility and Screen Recording grants, so users only approve your app. On Windows and Linux, embedded mode keeps driver execution inside the host process tree and avoids daemon handoff.
Embedding runs a dedicated `cua-driver serve` daemon as a direct child of your host app instead of launching the standalone `CuaDriver.app`. On macOS, the daemon inherits the host app's Accessibility and Screen Recording grants, so users only approve your app. A second `cua-driver mcp` child proxies stdio MCP traffic to that daemon; it never executes tools itself.

A complete macOS reference host and demo live in the repo at `libs/cua-driver/rust/examples/embedded-host-macos`.

## Launch embedded

Set embedded mode on the driver process your host spawns:
Choose a private socket and start the embedded daemon first:

```sh
CUA_DRIVER_EMBEDDED=1 \
CUA_DRIVER_HOST_BUNDLE_ID=com.yourco.yourapp \
cua-driver mcp
cua-driver serve --socket /tmp/yourapp-cua.sock
```

Or use the equivalent flags:
Then start the MCP proxy against that socket:

```sh
cua-driver mcp --embedded --host-bundle-id com.yourco.yourapp
cua-driver mcp --embedded --socket /tmp/yourapp-cua.sock \
--host-bundle-id com.yourco.yourapp
```

Only the exact value `CUA_DRIVER_EMBEDDED=1` enables embedded mode. The host bundle id is an advisory label echoed in `check_permissions`; trust still comes from macOS's responsibility chain.
You can pass `--embedded --host-bundle-id com.yourco.yourapp` to `serve` instead of the environment variables. Only the exact value `CUA_DRIVER_EMBEDDED=1` enables environment-based embedded mode. The host bundle id is an advisory label echoed in `check_permissions`; trust still comes from macOS's responsibility chain.

## Host requirements

- Spawn `cua-driver` directly from your app, for example with `Process` / `NSTask`, `posix_spawn`, or `fork` / `exec`.
- Speak MCP over the child's stdin/stdout.
- Spawn `cua-driver serve --embedded` directly from your app, for example with `Process` / `NSTask`, `posix_spawn`, or `fork` / `exec`.
- Wait for its private socket to become ready, then spawn `cua-driver mcp --embedded --socket <path>` and speak MCP over the proxy's stdin/stdout.
- On macOS, do not launch the driver with `open(1)` or `NSWorkspace.open`; LaunchServices makes the launched app its own responsible process and breaks permission inheritance.
- On macOS, request Accessibility and Screen Recording from the host app with `AXIsProcessTrustedWithOptions` and `CGRequestScreenCaptureAccess`.

If macOS grants are added after the driver child has started, restart the child so TCC is re-queried with a fresh per-process cache.
If macOS grants are added after the daemon has started, restart the daemon so TCC is re-queried with a fresh per-process cache.

## App + gateway architectures

`--embedded` does not transfer a GUI app's grants to the driver; it only keeps the driver inside its spawner's macOS responsibility chain. If a separate gateway, daemon, or Node process spawns MCP servers, registering `cua-driver mcp --embedded` there makes the driver inherit the gateway's identity, not the app's. Spawn the driver from the app process, or bridge MCP from the gateway to an app-spawned child.
`--embedded` does not transfer a GUI app's grants to the driver; it only keeps the daemon inside its spawner's macOS responsibility chain. If a separate gateway or Node process spawns the daemon, the daemon inherits the gateway's identity, not the app's. Spawn `cua-driver serve --embedded` from the app process.

```text
Wrong (inherits the gateway's identity): Right:

gateway / node daemon YourApp.app
└─ cua-driver --embedded └─ cua-driver --embedded
└─ cua-driver serve --embedded ├─ cua-driver serve --embedded
└─ cua-driver mcp --socket <private>
```

## What changes

| Behavior | Standalone | Embedded |
| --- | --- | --- |
| Process model | May proxy through a daemon | Direct child / in-process path |
| Daemon relaunch | May proxy through app daemon | Disabled |
| Process model | Standalone daemon + proxy | Host-spawned daemon + proxy |
| Daemon launch | May auto-launch CuaDriver.app | Host starts private daemon |
| macOS TCC identity | `com.trycua.driver` or caller | Host app |
| macOS permission prompts | Driver may prompt | Driver never prompts |
| macOS Settings entries | CuaDriver | Host app only |
Expand All @@ -60,7 +62,7 @@ Driver tools, screenshots, AX tree reads, background input, and the agent cursor

## macOS permission check

Call the `check_permissions` MCP tool after starting the embedded driver. On macOS, embedded mode ignores prompt requests and should return `source.attribution: "host"`:
Call the `check_permissions` MCP tool after the proxy connects to the embedded daemon. On macOS, embedded mode ignores prompt requests and should return `source.attribution: "host"`:

```json
{
Expand All @@ -75,6 +77,6 @@ Call the `check_permissions` MCP tool after starting the embedded driver. On mac
}
```

If `source.attribution` is not `host` on macOS, embedded mode is not active for the process handling your MCP calls. Check that `CUA_DRIVER_EMBEDDED=1` is passed to the child, that the child was spawned directly, and that you are not accidentally talking to an old standalone daemon.
If `source.attribution` is not `host` on macOS, embedded mode is not active in the daemon handling your MCP calls. Check that `CUA_DRIVER_EMBEDDED=1` is passed to the `serve` child, that the daemon was spawned directly, and that the proxy uses the intended private socket.

`source.attribution: "host"` means the driver process is running in embedded mode; it does not prove that your GUI app is the responsible process. If a gateway, daemon, or Node process spawned the child, the reported grant state still belongs to that spawner. Spawn `cua-driver` from the app process that owns the macOS grants, or bridge MCP to an app-spawned child.
`source.attribution: "host"` means the daemon is running in embedded mode; it does not prove that your GUI app is the responsible process. If a gateway or Node process spawned the daemon, the reported grant state still belongs to that spawner.
4 changes: 2 additions & 2 deletions docs/content/docs/reference/cua-driver/mcp-tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ For the cross-cutting parameter contract (shared parameters, required-parameter
</Callout>

<Callout type="info">
**TCC auto-delegation.** When an MCP client spawns `cua-driver mcp` from an IDE terminal (Claude Code, Cursor, VS Code, Warp), macOS attributes the subprocess to the parent terminal — not `CuaDriver.app` — so AX probes fail against the wrong bundle id. `mcp` detects this and auto-launches a `cua-driver serve` daemon via `open -n -g -a CuaDriver --args serve`, then proxies every tool call through the daemon's Unix socket. Tool semantics are identical to the in-process path; no Python bridge is needed. Pass `--no-daemon-relaunch` (or set `CUA_DRIVER_MCP_NO_RELAUNCH=1`) to force in-process execution. See the [process model](/reference/cua-driver/process-model) for the full lifecycle, failure modes, and wrapper-author guidance.
**Daemon delegation.** `cua-driver mcp` is always a stdio proxy to a `cua-driver serve` daemon. On macOS it can auto-launch the daemon via `open -n -g -a CuaDriver --args serve` so AX and Screen Recording grants attach to the app bundle. On Windows and Linux the daemon must already be running. See the [process model](/reference/cua-driver/process-model) for the full lifecycle and wrapper-author guidance.
</Callout>

## Inspection tools
Expand Down Expand Up @@ -520,7 +520,7 @@ Turn folders are named `turn-00001/`, `turn-00002/`, etc. Turn numbering restar

**Video is off by default.** Pass `record_video: true` to also capture the main display to `<output_dir>/recording.mp4` (H.264 / 30 fps) for the lifetime of the session. The recording is torn down automatically when the MCP client disconnects.

**macOS uses native ScreenCaptureKit** (in-process SCStream + SCRecordingOutput) so video inherits Cua Driver's own Screen Recording grant — no extra TCC prompt, no ffmpeg subprocess. Requires macOS 15.0+.
**macOS uses native ScreenCaptureKit** (daemon-owned SCStream + SCRecordingOutput) so video inherits the daemon's Screen Recording grant — no extra TCC prompt, no ffmpeg subprocess. Requires macOS 15.0+.

**Windows + Linux use an ffmpeg subprocess** (`gdigrab` / `x11grab` + libx264). Requires ffmpeg on PATH (winget install Gyan.FFmpeg / apt install ffmpeg); when ffmpeg is missing or fails on startup the per-turn capture (screenshots + action.json) still runs and the session's `last_error` field carries the diagnostic.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: YAML and Rego permission policy schema, environment variable, evalu

import { Callout } from 'fumadocs-ui/components/callout';

Cua Driver evaluates a permission policy for every MCP tool call when `CUA_DRIVER_POLICY_FILE` is set. The policy is loaded once at process startup and applies to both in-process MCP dispatch and daemon-proxy dispatch. This page is the reference for the file format, evaluation rules, and Rego interface.
Cua Driver evaluates a permission policy for every daemon tool call when `CUA_DRIVER_POLICY_FILE` is set. The daemon loads the policy once at process startup; the MCP proxy also evaluates the same policy when it is configured in the proxy environment. CLI and MCP calls therefore share the daemon enforcement point. This page is the reference for the file format, evaluation rules, and Rego interface.

---

Expand Down
Loading
Loading