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
48 changes: 37 additions & 11 deletions libs/cua-driver/rust/crates/cua-driver/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1559,19 +1559,45 @@ fn run_permissions_grant() {
process::exit(1);
}
}
// Since #1761 the daemon binds its socket IMMEDIATELY — before the
// permissions gate completes — so the first `check_permissions`
// query returns "pending" while the grant is still missing. Poll
// the daemon until both grants flip true (success) or we time out.
//
// The gate re-execs the daemon (~every 25s) to pick up an
// Accessibility grant — `AXIsProcessTrusted` is cached per process
// and only a fresh process image sees a later grant. During each
// restart the socket briefly disappears, so tolerate transient
// connection failures rather than bailing on the first one.
let req = crate::serve::DaemonRequest {
method: "call".into(),
name: Some("check_permissions".into()),
args: Some(serde_json::json!({ "prompt": false })),
};
let structured = crate::serve::send_request(&socket, &req)
.ok()
.filter(|r| r.ok)
.and_then(|r| r.result)
.and_then(|res| res.get("structuredContent").cloned())
.unwrap_or_else(|| serde_json::json!({}));
let ax = structured.get("accessibility").and_then(|v| v.as_bool()).unwrap_or(false);
let sr = structured.get("screen_recording").and_then(|v| v.as_bool()).unwrap_or(false);
let poll_deadline =
std::time::Instant::now() + std::time::Duration::from_secs(180);
let mut ax = false;
let mut sr = false;
loop {
if let Some(structured) = crate::serve::send_request(&socket, &req)
.ok()
.filter(|r| r.ok)
.and_then(|r| r.result)
.and_then(|res| res.get("structuredContent").cloned())
{
ax = structured.get("accessibility").and_then(|v| v.as_bool()).unwrap_or(false);
sr = structured.get("screen_recording").and_then(|v| v.as_bool()).unwrap_or(false);
if ax && sr {
break;
}
}
// `send_request` failing (None / !ok) means the daemon is
// mid-restart (re-exec) or briefly down — keep polling.
if std::time::Instant::now() >= poll_deadline {
break;
}
std::thread::sleep(std::time::Duration::from_secs(2));
}
if ax && sr {
println!("\n✅ CuaDriver has Accessibility + Screen Recording. You're set.");
} else {
Expand All @@ -1581,10 +1607,10 @@ fn run_permissions_grant() {
(true, false) => "Screen Recording",
(true, true) => unreachable!(),
};
println!("\n⚠️ CuaDriver is running but still missing: {missing}.");
println!("\n⚠️ Timed out waiting on: {missing}.");
println!(
"Approve it for \u{201c}Cua Driver\u{201d} in System Settings \u{2192} Privacy & \
Security, then re-run `cua-driver permissions status`."
"Approve CuaDriver for \u{201c}Cua Driver\u{201d} in System Settings \u{2192} \
Privacy & Security, then re-run `cua-driver permissions grant`."
);
}
}
Expand Down
88 changes: 56 additions & 32 deletions libs/cua-driver/rust/crates/cua-driver/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,24 +229,9 @@ fn main() {
// before any blocking work so the banner can land on stderr
// early in the serve lifecycle.
version_check::maybe_announce_update();
// 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.");
}
cua_driver_core::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 @@ -278,27 +263,66 @@ fn main() {
reg.init_self_weak();
let sp = socket.unwrap_or_else(serve::default_socket_path);
let pid_path = serve::default_pid_file_path();

// Bind the Unix socket FIRST, on a background thread, BEFORE
// running the (blocking) permissions gate (#1761).
//
// The gate's `wait_for_grants` blocks while `com.trycua.driver`
// is ungranted — it prompts and re-exec-loops until the user
// grants or the deadline elapses. If serve ran after the gate,
// the daemon's socket wouldn't appear for minutes on first
// launch, so `permissions grant` / MCP clients launched via
// `open -n -g -a CuaDriver --args serve` (the correct-TCC-
// attribution path) couldn't reach the daemon to even report
// "pending". Binding the socket first makes the daemon
// reachable within ~1s while the gate works toward the grant.
//
// A Unix socket + tokio accept loop has no main-thread
// requirement, so serve runs on a background thread. The gate
// stays on the MAIN thread: its prompt APIs
// (`request_accessibility` / `request_screen_recording`) and
// the NSPanel must run on main. On grant, the gate's
// `reexec_self()` execvp's the whole daemon — the socket
// re-binds fast on restart (run_serve unlinks the stale socket
// file first) and stabilizes once the grant sticks.
let serve_handle = std::thread::Builder::new()
.name("cua-serve".into())
.spawn(move || {
serve::run_serve_cmd(reg, &sp, Some(&pid_path));
std::process::exit(0);
})
.expect("spawn serve thread");

// Socket is binding/bound now → daemon reachable while we 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 serve — individual tool calls
// will then fail with the underlying TCC error, mirroring
// Swift's "user closed the panel" fallback.
if let Err(e) = platform_macos::permissions::run_if_needed(gate_opts) {
eprintln!("[cua-driver] permissions gate: {e}");
eprintln!("[cua-driver] continuing — tool calls touching AX or \
Screen Recording fail until you grant the missing TCC \
permissions.");
}

// Keep the main thread alive for the daemon.
//
// PiP needs the AppKit main run loop to process the
// dispatch_async_f calls that push frames into NSImageView.
// When --experimental-pip is on, move the serve loop onto
// a background thread and park the main thread in
// NSApplication.run() so the dispatched blocks actually
// execute. Without this, frames queue forever and the
// window stays blank. The non-PiP path keeps the original
// run-on-main semantics so we don't change behaviour for
// existing users.
// dispatch_async_f calls that push frames into NSImageView;
// park main in NSApplication.run() when --experimental-pip is
// on. Otherwise just join the serve thread so the process
// stays up as long as the daemon does.
if pip_cfg.enabled {
std::thread::Builder::new()
.name("cua-serve".into())
.spawn(move || {
serve::run_serve_cmd(reg, &sp, Some(&pid_path));
std::process::exit(0);
})
.expect("spawn serve thread");
platform_macos::pip::run_appkit_main_loop();
return;
} else {
let _ = serve_handle.join();
}
serve::run_serve_cmd(reg, &sp, Some(&pid_path));
return;
}
cli::Command::Stop { socket } => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -384,14 +384,22 @@ pub fn wait_for_grants(opts: &GateOpts) -> Result<()> {
// On macOS, when polling has gone several iterations without
// any change, re-exec the binary to invalidate the per-process
// TCC cache. See the function-level doc for the rationale.
// Threshold of 5 polls (~5s) is tuned to feel snappy without
// exec-spinning when the user is mid-grant.
//
// Since #1761 the serve loop runs concurrently with this gate on
// a background thread, so every `reexec_self()` restarts the whole
// daemon — including re-binding the Unix socket. A tight re-exec
// cadence would therefore make the socket flap (clients see brief
// connection failures on each restart). We trade grant-detection
// latency (fine for the grant flow — the user is clicking through
// System Settings on a human timescale) for socket stability:
// ~25 polls (~25s) between re-execs instead of ~5s.
#[cfg(target_os = "macos")]
{
const EXEC_AFTER_POLLS: u32 = 5;
const EXEC_AFTER_POLLS: u32 = 25;
if polls_without_change >= EXEC_AFTER_POLLS {
println!(
"[cua-driver] rechecking permissions (still missing: {})",
"[cua-driver] rechecking permissions — restarting daemon \
(still missing: {})",
fmt_missing(&last_missing)
);
let _ = std::io::stdout().flush();
Expand Down
Loading