diff --git a/libs/cua-driver-rs/crates/cua-driver/src/cli.rs b/libs/cua-driver-rs/crates/cua-driver/src/cli.rs index 6768779667..5fb5267bc8 100644 --- a/libs/cua-driver-rs/crates/cua-driver/src/cli.rs +++ b/libs/cua-driver-rs/crates/cua-driver/src/cli.rs @@ -366,9 +366,38 @@ pub fn should_use_daemon_proxy(no_daemon_relaunch: bool) -> bool { true } +/// Non-macOS targets don't have TCC, but they DO have the equivalent +/// problem of session attribution on Windows (Session 0 vs the user's +/// interactive Session 1+). When the CLI is spawned via SSH or a +/// Windows service, it lands in Session 0 where the desktop, window +/// APIs, and UI Automation return empty. A daemon running in the +/// interactive session (via `cua-driver autostart enable && kick`, +/// or any other Session-1+ launch) can answer tool calls correctly — +/// so when one is up, we proxy through it. +/// +/// Behaviour: +/// * `--no-daemon-relaunch` or `CUA_DRIVER_RS_MCP_NO_RELAUNCH=1` +/// forces in-process (matches macOS opt-out). +/// * `CUA_DRIVER_RS_MCP_FORCE_PROXY=1` always proxies, even with +/// no daemon up — the caller is responsible for having one +/// already. +/// * Otherwise we probe `is_daemon_listening` on the default +/// socket: a live daemon means proxy through it; nothing +/// listening means run in-process (no autospawn equivalent on +/// Linux/Windows — there's no `open -a CuaDriver` analog). #[cfg(not(target_os = "macos"))] -pub fn should_use_daemon_proxy(_no_daemon_relaunch: bool) -> bool { - false +pub fn should_use_daemon_proxy(no_daemon_relaunch: bool) -> bool { + use crate::bundle::is_env_truthy; + if no_daemon_relaunch { + return false; + } + if is_env_truthy("CUA_DRIVER_RS_MCP_NO_RELAUNCH") { + return false; + } + if is_env_truthy("CUA_DRIVER_RS_MCP_FORCE_PROXY") { + return true; + } + crate::serve::is_daemon_listening(&crate::serve::default_socket_path()) } /// Spawn `/usr/bin/open -n -g -a CuaDriver --args serve` to launch @@ -449,15 +478,15 @@ pub fn launch_daemon_and_wait(socket_path: &str, timeout_secs: u64) -> anyhow::R /// `open` if needed), then `crate::proxy::run_proxy` against its /// socket. Builds its own tokio runtime — same shape as the other /// `run_*` helpers in this file that own their event loop. -#[cfg(target_os = "macos")] pub fn run_mcp_via_daemon_proxy(socket: Option) -> anyhow::Result<()> { let socket_path = socket.unwrap_or_else(crate::serve::default_socket_path); if !crate::serve::is_daemon_listening(&socket_path) { // CUA_DRIVER_RS_MCP_FORCE_PROXY callers (test harness, custom - // bundle setups) supply their own daemon — skip the `open -a` - // step, since they don't have an installed CuaDriver.app to - // relaunch into. Fail fast if no daemon is up at this point. + // bundle setups) supply their own daemon — skip the auto- + // launch step, since they don't have an installed + // CuaDriver.app to relaunch into. Fail fast if no daemon is + // up at this point. if crate::bundle::is_env_truthy("CUA_DRIVER_RS_MCP_FORCE_PROXY") { anyhow::bail!( "CUA_DRIVER_RS_MCP_FORCE_PROXY=1 but no daemon listening on \ @@ -465,17 +494,40 @@ pub fn run_mcp_via_daemon_proxy(socket: Option) -> anyhow::Result<()> { and retry." ); } - let socket_suffix = if socket_path != crate::serve::default_socket_path() { - format!(" --socket {socket_path}") - } else { - String::new() - }; - eprintln!( - "cua-driver-rs: mcp launched without CuaDriver.app's TCC grants; \ - auto-launching the daemon via `open -n -g -a CuaDriver --args serve{socket_suffix}` \ - and proxying MCP requests through it. Pass --no-daemon-relaunch to stay in-process." - ); - launch_daemon_and_wait(&socket_path, 10)?; + #[cfg(target_os = "macos")] + { + let socket_suffix = if socket_path != crate::serve::default_socket_path() { + format!(" --socket {socket_path}") + } else { + String::new() + }; + eprintln!( + "cua-driver-rs: mcp launched without CuaDriver.app's TCC grants; \ + auto-launching the daemon via `open -n -g -a CuaDriver --args serve{socket_suffix}` \ + and proxying MCP requests through it. Pass --no-daemon-relaunch to stay in-process." + ); + launch_daemon_and_wait(&socket_path, 10)?; + } + // On Linux / Windows there's no equivalent `open -a CuaDriver` + // mechanism to spawn a daemon attributed to the user's + // interactive session. The caller is expected to have one + // running already (e.g. via `cua-driver autostart enable && kick` + // on Windows). Bail with an actionable error rather than + // silently falling back to an in-process server that would + // be attributed to whatever session spawned us (typically + // Session 0 over SSH). + #[cfg(not(target_os = "macos"))] + { + anyhow::bail!( + "no cua-driver daemon listening on {socket_path}. Start one in \ + your interactive session — on Windows run \ + `cua-driver autostart enable && cua-driver autostart kick`; \ + on Linux run `cua-driver serve &` in the user's session. \ + Then re-run `cua-driver mcp`. To skip the proxy and run \ + in-process anyway (Session 0 attribution, GUI tools will \ + return empty), pass --no-daemon-relaunch." + ); + } } let rt = tokio::runtime::Builder::new_multi_thread() diff --git a/libs/cua-driver-rs/crates/cua-driver/src/main.rs b/libs/cua-driver-rs/crates/cua-driver/src/main.rs index aa635d9e75..8f6632fc13 100644 --- a/libs/cua-driver-rs/crates/cua-driver/src/main.rs +++ b/libs/cua-driver-rs/crates/cua-driver/src/main.rs @@ -431,12 +431,30 @@ fn main() -> anyhow::Result<()> { } cli::Command::Mcp { no_daemon_relaunch, socket } => { // Long-running MCP server — kick off the background update - // check before falling through to the in-process server. + // check before any daemon-proxy decisions. version_check::maybe_announce_update(); - // Non-macOS: TCC doesn't exist, no daemon proxy path. The - // flags parse cleanly so cross-platform MCP config - // snippets work, but we ignore them and run in-process. - let _ = (no_daemon_relaunch, socket); + // Daemon-proxy sidestep for Windows Session 0 attribution + // (and equivalent on Linux when a daemon is up): if a + // daemon is listening on the default socket, forward + // stdio MCP through it instead of running the server + // in-process. The proxy preserves the daemon's session + // identity (typically Session 1+ on Windows) so window / + // UIA / screen tools see the user's actual desktop — + // without this, an `cua-driver mcp` spawned by Claude + // Code over SSH lands in Session 0 and every desktop + // tool returns empty. See `cli::should_use_daemon_proxy`. + if cli::should_use_daemon_proxy(no_daemon_relaunch) { + if let Err(e) = cli::run_mcp_via_daemon_proxy(socket) { + eprintln!("cua-driver-rs: {e}"); + std::process::exit(1); + } + return Ok(()); + } + // Fall through to the in-process MCP server below. The + // `socket` flag is daemon-proxy-only; ignored on the + // in-process path (mirrors the macOS arm's drop-on-floor + // behaviour). + let _ = socket; } }