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
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,29 @@ cua-driver check_permissions

If a grant still reads `NOT granted` after granting in the dialog, open **System Settings → Privacy & Security**, find `CuaDriver.app` under Accessibility and Screen Recording, and flip the toggle.

<Callout type="info">
**First-launch permissions gate (`cua-driver serve`).** On the Rust port,
`cua-driver serve` runs an interactive permissions gate at startup. If
Accessibility or Screen Recording is missing it prints a banner, auto-opens
the matching System Settings pane, and polls until you grant the missing
items. When both grants are already active the gate is a transparent no-op.

**CI / headless runners** should skip the gate so the daemon does not block
waiting for a TTY-attached human:

```bash
# As a flag …
cua-driver serve --no-permissions-gate

# … or as an env-var.
CUA_DRIVER_RS_PERMISSIONS_GATE=0 cua-driver serve
```

Accepted "off" values for the env-var (case-insensitive): `0`, `false`,
`no`, `off` — so `FALSE`, `Off`, `NO` etc. all work. Any other value
(including unset) leaves the gate active.
</Callout>

## Requirements

- macOS 14 (Sonoma) or later
Expand Down
8 changes: 8 additions & 0 deletions docs/content/docs/cua-driver/reference/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,13 @@ Subsequent `cua-driver call/list-tools/describe` invocations auto-detect
the socket and forward their requests, so the AppStateEngine's per-pid
element_index cache survives across CLI calls.

On macOS, `serve` runs a **first-launch permissions gate** before binding
the socket. When TCC grants for Accessibility or Screen Recording are
missing, it prints a banner listing exactly what is missing, auto-opens
the matching `System Settings → Privacy & Security` pane(s), and polls
every second until the user grants both. When grants are already active
the gate is a transparent no-op.

**Options:**

| Name | Type | Default | Description |
Expand All @@ -153,6 +160,7 @@ element_index cache survives across CLI calls.
| Name | Description |
| ---- | ----------- |
| `--no-relaunch` | Stay in the current process instead of re-execing via `open -n -g -a CuaDriver`. |
| `--no-permissions-gate` | Skip the macOS TCC permissions gate at startup. Use for CI / headless runners where blocking on user input would deadlock the process. Also toggleable by setting `CUA_DRIVER_RS_PERMISSIONS_GATE` to any of `0`, `false`, `no`, or `off` (case-insensitive — e.g. `CUA_DRIVER_RS_PERMISSIONS_GATE=FALSE` works too). |

### cua-driver stop

Expand Down
60 changes: 60 additions & 0 deletions libs/cua-driver-rs/PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,66 @@ status).

---

## Startup flow: permissions gate (`serve`)
- Swift: `libs/cua-driver/Sources/CuaDriverCore/Permissions/PermissionsGate.swift`
(SwiftUI panel + AppKit window + 1 Hz polling)
- Rust:
- macos=`crates/platform-macos/src/permissions/gate.rs`
(CLI banner + auto-open System Settings + 1 Hz polling)
- windows / linux: N/A — TCC is a macOS concept; the `--no-permissions-gate`
flag is accepted and silently ignored on those platforms for CLI uniformity.
- Status:
- macos: PORTED with intentional UX divergence (CLI, not SwiftUI)
- windows / linux: N/A

### Intentional UX divergence

Swift surfaces a branded SwiftUI window on first launch. The Rust port
ships a terminal-driven banner instead. Rationale:

1. cua-driver-rs already drives an AppKit run loop on the main thread
for the cursor overlay; bolting on a second window invites
main-thread deadlocks.
2. The Rust binary's primary deployment shape is the daemon under
`cua-driver serve` from a shell (Claude Code, Cursor, Codex), which
already has a terminal attached.
3. Headless / CI use cases need an opt-out; a CLI flow with
`--no-permissions-gate` + `CUA_DRIVER_RS_PERMISSIONS_GATE=0` is the
straight-line approach. Replicating Swift's window only to suppress
it under headless would be more code with no UX upside.

The CLI gate still preserves the substantive Swift behaviours:

- Lists exactly which TCC grants are missing (Accessibility / Screen
Recording), with the same rationale strings the SwiftUI panel uses.
- Opens both `x-apple.systempreferences:` URLs at once so the user can
grant both in a single Settings visit. (Swift's "chain to next pane
when one flips green" trick is unnecessary when both panes are
pre-opened.)
- Polls at 1 Hz, identical cadence to the Swift `Timer`.
- Auto-continues startup the moment all required grants are green.

### Opt-out signals

| Signal | Effect |
| --------------------------------------- | ----------- |
| `--no-permissions-gate` flag | gate skipped |
| `CUA_DRIVER_RS_PERMISSIONS_GATE=0` | gate skipped |
| `CUA_DRIVER_RS_PERMISSIONS_GATE=false` | gate skipped |
| `CUA_DRIVER_RS_PERMISSIONS_GATE=no` | gate skipped |
| `CUA_DRIVER_RS_PERMISSIONS_GATE=off` | gate skipped |
| any other env value | gate active |

Default deadline is 10 minutes; on timeout the gate logs an error and
`serve` continues to start, mirroring the Swift "user closed the
panel" path (individual tool calls then fail with the underlying TCC
error).

A native `NSAlert` via objc2 is tracked as a follow-up if the
terminal-only flow proves insufficient; the CLI is the MVP.

---

## MCP tool: `list_apps`
- Swift: `libs/cua-driver/Sources/CuaDriverServer/Tools/ListAppsTool.swift:6-71`
+ `libs/cua-driver/Sources/CuaDriverCore/Apps/AppInfo.swift`
Expand Down
15 changes: 13 additions & 2 deletions libs/cua-driver-rs/crates/cua-driver/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ pub enum Command {
Describe(String),
Call { tool: String, json_args: Option<serde_json::Value>, screenshot_out_file: Option<String> },
McpConfig { client: Option<String> },
Serve { socket: Option<String> },
Serve {
socket: Option<String>,
/// True when `--no-permissions-gate` is on argv. The env-var
/// `CUA_DRIVER_RS_PERMISSIONS_GATE=0` short-circuits the gate too
/// (checked inside the gate itself), so the flag is only one of
/// two opt-out signals.
no_permissions_gate: bool,
},
Stop { socket: Option<String> },
Status { socket: Option<String> },
Recording { subcommand: String, args: Vec<String>, socket: Option<String> },
Expand Down Expand Up @@ -93,7 +100,11 @@ pub fn parse_command() -> Command {
None | Some("mcp") => Command::Mcp,
Some("list-tools") => Command::ListTools,
Some("mcp-config") => Command::McpConfig { client: mcp_client },
Some("serve") => Command::Serve { socket },
Some("serve") => Command::Serve {
socket,
// Bare flag — present anywhere on argv counts as "skip the gate".
no_permissions_gate: args.iter().any(|a| a == "--no-permissions-gate"),
},
Some("stop") => Command::Stop { socket },
Some("status") => Command::Status { socket },
Some("recording") => {
Expand Down
26 changes: 24 additions & 2 deletions libs/cua-driver-rs/crates/cua-driver/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,25 @@ fn main() {
cli::run_call(reg, &tool, json_args, screenshot_out_file);
return;
}
cli::Command::Serve { socket } => {
cli::Command::Serve { socket, no_permissions_gate } => {
// First-launch permissions gate (Swift PermissionsGate parity).
// Runs on every `serve` start; no-op when both grants are
// already active. Honors --no-permissions-gate and
// CUA_DRIVER_RS_PERMISSIONS_GATE=0 for CI / headless.
//
// Failures (e.g. deadline elapsed without grants) are logged
// and the daemon continues to start — individual tool calls
// will then fail with the underlying TCC error, mirroring
// Swift's "user closed the panel" fallback.
let gate_opts = platform_macos::permissions::GateOpts::from_env_and_flag(
no_permissions_gate,
);
if let Err(e) = platform_macos::permissions::run_if_needed(gate_opts) {
eprintln!("[cua-driver] permissions gate: {e}");
eprintln!("[cua-driver] continuing serve startup anyway — \
expect tool calls touching AX or Screen Recording \
to fail until you grant the missing TCC permissions.");
}
mcp_server::recording::set_screenshot_fn(|window_id, pid| {
if let Some(wid) = window_id {
platform_macos::capture::screenshot_window_bytes(wid as u32).ok()
Expand Down Expand Up @@ -251,7 +269,11 @@ fn main() -> anyhow::Result<()> {
}).join().ok();
return Ok(());
}
cli::Command::Serve { socket } => {
cli::Command::Serve { socket, no_permissions_gate } => {
// The Rust permissions gate is macOS-only (TCC concept).
// On Windows / Linux the flag is silently accepted for
// CLI uniformity and ignored.
let _ = no_permissions_gate;
// Serve mode needs the cursor overlay just like MCP mode.
let cursor_cfg = cursor_overlay::CursorConfig::from_args();
let reg = Arc::new(build_registry(cursor_cfg));
Expand Down
2 changes: 2 additions & 0 deletions libs/cua-driver-rs/crates/platform-macos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ pub mod browser;
#[cfg(target_os = "macos")]
pub mod focus_steal;
#[cfg(target_os = "macos")]
pub mod permissions;
#[cfg(target_os = "macos")]
pub mod tools;

use mcp_server::tool::ToolRegistry;
Expand Down
Loading
Loading