diff --git a/.github/workflows/cd-rust-cua-driver.yml b/.github/workflows/cd-rust-cua-driver.yml index 5a8b00694e..fcdfe1539e 100644 --- a/.github/workflows/cd-rust-cua-driver.yml +++ b/.github/workflows/cd-rust-cua-driver.yml @@ -1,4 +1,4 @@ -name: "CD: Cua Driver RS (cross-platform)" +name: "CD: Cua Driver (cross-platform)" # Matches the artifact convention from cd-swift-cua-driver.yml: # - macOS: ONE universal binary tarball + bare universal binary @@ -245,13 +245,13 @@ jobs: codesign --force --timestamp --options runtime \ --sign "$IDENTITY" release/universal/cua-driver codesign --verify --strict --verbose=2 release/universal/cua-driver - - name: Assemble CuaDriverRs.app bundle + - name: Assemble CuaDriver.app bundle working-directory: libs/cua-driver-rs run: | # Copy the bundle skeleton (Info.plist) from scripts/ and drop # the universal binary into Contents/MacOS/cua-driver. The # assembled bundle goes into every directory tarball so - # install.sh can `ditto` it to /Applications/CuaDriverRs.app + # install.sh can `ditto` it to /Applications/CuaDriver.app # for the TCC auto-relaunch path. # # No codesigning at this layer — the bundle ships ad-hoc @@ -263,26 +263,26 @@ jobs: # change that wires up the notarization script the way the # Swift driver does. VERSION="${{ steps.version.outputs.version }}" - mkdir -p release/CuaDriverRs.app - cp -R scripts/CuaDriverRs.app/Contents release/CuaDriverRs.app/Contents + mkdir -p release/CuaDriver.app + cp -R scripts/CuaDriver.app/Contents release/CuaDriver.app/Contents cp release/universal/cua-driver \ - release/CuaDriverRs.app/Contents/MacOS/cua-driver - chmod +x release/CuaDriverRs.app/Contents/MacOS/cua-driver + release/CuaDriver.app/Contents/MacOS/cua-driver + chmod +x release/CuaDriver.app/Contents/MacOS/cua-driver # Stamp the release version into Info.plist so the bundle # version tracks the tag instead of whatever was last # checked in. Without this the in-tree Info.plist's # CFBundleShortVersionString drifts from the release tag on # every cut. CodeRabbit #4. plutil -replace CFBundleShortVersionString -string "$VERSION" \ - release/CuaDriverRs.app/Contents/Info.plist + release/CuaDriver.app/Contents/Info.plist plutil -replace CFBundleVersion -string "$VERSION" \ - release/CuaDriverRs.app/Contents/Info.plist + release/CuaDriver.app/Contents/Info.plist # Remove the .gitkeep we use in source control — it's not # part of the runtime bundle. - rm -f release/CuaDriverRs.app/Contents/MacOS/.gitkeep - ls -la release/CuaDriverRs.app/Contents/MacOS - plutil -p release/CuaDriverRs.app/Contents/Info.plist | grep -E 'CFBundle(Short)?Version' - - name: Codesign + notarize + staple CuaDriverRs.app + rm -f release/CuaDriver.app/Contents/MacOS/.gitkeep + ls -la release/CuaDriver.app/Contents/MacOS + plutil -p release/CuaDriver.app/Contents/Info.plist | grep -E 'CFBundle(Short)?Version' + - name: Codesign + notarize + staple CuaDriver.app if: env.DO_NOTARIZE == 'true' working-directory: libs/cua-driver-rs env: @@ -298,26 +298,26 @@ jobs: # is essentially a no-op on the binary and a fresh signature # on the bundle wrapper. codesign --force --deep --timestamp --options runtime \ - --sign "$IDENTITY" release/CuaDriverRs.app - codesign --verify --strict --verbose=2 release/CuaDriverRs.app + --sign "$IDENTITY" release/CuaDriver.app + codesign --verify --strict --verbose=2 release/CuaDriver.app # notarytool wants a zip (or .dmg / .pkg). Build one next to # the .app, submit, wait, then staple the bundle in place. - /usr/bin/ditto -c -k --keepParent release/CuaDriverRs.app \ - release/CuaDriverRs.app.zip + /usr/bin/ditto -c -k --keepParent release/CuaDriver.app \ + release/CuaDriver.app.zip - xcrun notarytool submit release/CuaDriverRs.app.zip \ + xcrun notarytool submit release/CuaDriver.app.zip \ --apple-id "$APPLE_ID" \ --team-id "$TEAM_ID" \ --password "$APP_SPECIFIC_PASSWORD" \ --wait --timeout 20m - xcrun stapler staple release/CuaDriverRs.app - spctl -a -vv -t exec release/CuaDriverRs.app + xcrun stapler staple release/CuaDriver.app + spctl -a -vv -t exec release/CuaDriver.app # The zip we used to submit is throwaway; the packaging step # below builds the final tarballs from the stapled .app. - rm -f release/CuaDriverRs.app.zip + rm -f release/CuaDriver.app.zip - name: Package working-directory: libs/cua-driver-rs run: | @@ -330,10 +330,10 @@ jobs: # (matches the Swift `cd-swift-cua-driver.yml` convention). cp release/universal/cua-driver "release/${STAGE}/" # Ship the .app bundle in every macOS tarball so install.sh - # can drop it into /Applications/CuaDriverRs.app for TCC + # can drop it into /Applications/CuaDriver.app for TCC # attribution (issue #1525). Tarball callers that want only # the bare binary can grab the *-binary.tar.gz below. - cp -R release/CuaDriverRs.app "release/${STAGE}/CuaDriverRs.app" + cp -R release/CuaDriver.app "release/${STAGE}/CuaDriver.app" cp ../../LICENSE.md "release/${STAGE}/LICENSE" 2>/dev/null || true (cd release && tar -czf "${STAGE}.tar.gz" "${STAGE}") done @@ -546,7 +546,7 @@ jobs: ### Artifacts **macOS (universal — arm64 + x86_64 in one binary, like the Swift cua-driver)** - - `cua-driver-rs-${{ steps.version.outputs.version }}-darwin-universal.tar.gz` — directory tarball with LICENSE + `CuaDriverRs.app` bundle (install.sh expects this layout) + - `cua-driver-rs-${{ steps.version.outputs.version }}-darwin-universal.tar.gz` — directory tarball with LICENSE + `CuaDriver.app` bundle (install.sh expects this layout) - `cua-driver-rs-${{ steps.version.outputs.version }}-darwin-arm64.tar.gz` — same payload, named for arm64 callers - `cua-driver-rs-${{ steps.version.outputs.version }}-darwin-x86_64.tar.gz` — same payload, named for x86_64 callers - `cua-driver-rs-${{ steps.version.outputs.version }}-darwin-universal-binary.tar.gz` — bare universal binary (single file at archive root; **no** .app — bypasses the TCC auto-relaunch path) diff --git a/docs/content/docs/cua-driver/guide/getting-started/installation.mdx b/docs/content/docs/cua-driver/guide/getting-started/installation.mdx index bc9c802ddc..716f1f82f2 100644 --- a/docs/content/docs/cua-driver/guide/getting-started/installation.mdx +++ b/docs/content/docs/cua-driver/guide/getting-started/installation.mdx @@ -41,18 +41,15 @@ The install runs **without sudo**: `/Applications` is user-writable on personal The flag delegates to [`libs/cua-driver/scripts/_install-rust.sh`](https://github.com/trycua/cua/blob/main/libs/cua-driver/scripts/_install-rust.sh) (a private helper colocated with the canonical `install.sh`), which: -- Installs `CuaDriverRs.app` to `/Applications/` — separate bundle id (`com.trycua.cuadriverrs`) from the Swift driver's `com.trycua.driver`. Both can coexist on the same machine with independent TCC grants and independent telemetry IDs. -- Points `~/.local/bin/cua-driver` at the Rust binary. The Swift `CuaDriver.app` (if previously installed) is left untouched and still launchable via its bundle path (`/Applications/CuaDriver.app/Contents/MacOS/cua-driver`). +- Installs `CuaDriver.app` to `/Applications/` with bundle id `com.trycua.driver` — the same bundle id the Swift driver uses. If `/Applications/CuaDriver.app` already holds the Swift bundle, the installer replaces it in place. macOS TCC keys grants on bundle id, so the user's prior Accessibility / Screen Recording grants survive the upgrade and apply to the new binary; a one-time re-grant prompt may fire on first action because the cdhash differs. +- Points `~/.local/bin/cua-driver` at the Rust binary. - Same `--bin-dir` / `--no-modify-path` flags as the Swift installer — anything you put after `--experimental-rust` is forwarded to the Rust installer verbatim. -**Switch back to Swift:** +**Switch back to Swift:** the Swift driver still ships under its own release-tag prefix (`cua-driver-v*`). Re-run the canonical install script without `--experimental-rust` and it'll fetch the latest Swift release and replace the `.app` at `/Applications/CuaDriver.app` the same way (TCC grants survive, since bundle id is shared). ```bash -rm -rf /Applications/CuaDriverRs.app /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh)" ``` - -The second command rewires the `~/.local/bin/cua-driver` symlink back to the Swift bundle. ### Install `cua-driver-rs` directly (Linux / Windows / macOS) @@ -81,7 +78,7 @@ The install layout is wired with NTFS directory junctions (`IO_REPARSE_TAG_MOUNT #### Versioned-dirs install layout -Linux and Windows installs land in a three-tier layout that makes upgrades and rollbacks an atomic retarget of one link, never a file overwrite. The macOS install intentionally stays on a different layout (`/Applications/CuaDriverRs.app`) — see the [macOS asymmetry note](#why-macos-uses-a-different-layout) below. +Linux and Windows installs land in a three-tier layout that makes upgrades and rollbacks an atomic retarget of one link, never a file overwrite. The macOS install intentionally stays on a different layout (`/Applications/CuaDriver.app`) — see the [macOS asymmetry note](#why-macos-uses-a-different-layout) below. **Linux** @@ -161,7 +158,7 @@ So an API outage, an unauthenticated-rate-limit (60 req/hr per IP), or a transie -**Why macOS uses a different layout.** On macOS the install still drops `CuaDriverRs.app` into `/Applications` and symlinks `~/.local/bin/cua-driver` into the bundle. The `.app` placement is the anchor for both **TCC attribution** (cdhash + bundle id) and **LaunchServices** (`open -a CuaDriverRs`); symlinking the `.app` from `/Applications` to a versioned dir under `$CUA_DRIVER_RS_HOME` would break both. The asymmetry is intentional — rollback on macOS = reinstall an older release tag with `CUA_DRIVER_RS_VERSION=`. +**Why macOS uses a different layout.** On macOS the install still drops `CuaDriver.app` into `/Applications` and symlinks `~/.local/bin/cua-driver` into the bundle. The `.app` placement is the anchor for both **TCC attribution** (cdhash + bundle id) and **LaunchServices** (`open -a CuaDriver`); symlinking the `.app` from `/Applications` to a versioned dir under `$CUA_DRIVER_RS_HOME` would break both. The asymmetry is intentional — rollback on macOS = reinstall an older release tag with `CUA_DRIVER_RS_VERSION=`. @@ -178,7 +175,7 @@ Two invariants worth knowing: - **The active install is always preserved**, even if it would otherwise fall outside the keep window (e.g. you rolled back to an older version). Worst-case on-disk count is `keep + 1`. - **Per-target filtering** — a multi-arch dev with both `aarch64-apple-darwin` and `x86_64-unknown-linux-gnu` directories under the same `$CUA_DRIVER_RS_HOME` (rare, but possible with shared/NFS homes) gets each target's history GC'd independently of the other. -The macOS install path is unaffected — `/Applications/CuaDriverRs.app` is an in-place replacement with no per-version accumulation. +The macOS install path is unaffected — `/Applications/CuaDriver.app` is an in-place replacement with no per-version accumulation. diff --git a/libs/cua-driver-rs/PARITY.md b/libs/cua-driver-rs/PARITY.md index e0f4a0ba43..2293c3608b 100644 --- a/libs/cua-driver-rs/PARITY.md +++ b/libs/cua-driver-rs/PARITY.md @@ -1280,7 +1280,7 @@ Swift. `CallTool` over the daemon UDS). - Rust: - `libs/cua-driver-rs/crates/cua-driver/src/bundle.rs` — - `is_executable_inside_cuadriverrs_app`, + `is_executable_inside_cuadriver_app`, `parent_is_not_launchd`, `is_env_truthy`. - `libs/cua-driver-rs/crates/cua-driver/src/cli.rs` — `should_use_daemon_proxy`, `launch_daemon_and_wait`, @@ -1288,10 +1288,10 @@ Swift. - `libs/cua-driver-rs/crates/cua-driver/src/proxy.rs` — `run_proxy` (the stdio loop forwarding `tools/list` and `tools/call` through the daemon socket). - - `libs/cua-driver-rs/scripts/CuaDriverRs.app/Contents/Info.plist` — + - `libs/cua-driver-rs/scripts/CuaDriver.app/Contents/Info.plist` — the bundle the auto-relaunch path lands in. - `libs/cua-driver-rs/scripts/install.sh` — drops the bundle to - `/Applications/CuaDriverRs.app` and symlinks the bin into it. + `/Applications/CuaDriver.app` and symlinks the bin into it. - Status: implemented on macOS (issue #1525); smoke-tested manually before merge. @@ -1299,7 +1299,7 @@ Swift. When `cua-driver-rs mcp` is invoked from an IDE terminal (Claude Code, Cursor, VS Code, Warp), macOS attributes the spawned process to the parent terminal's TCC responsibility chain — *not* to -`com.trycua.cuadriverrs`. AX probes against the process silently +`com.trycua.driver`. AX probes against the process silently fail because the user granted Accessibility to the bundle, not to the IDE terminal. The Swift driver hit the same pathology and fixed it in PR #1479; the Rust port hit it on the macOS GA flip path and @@ -1307,7 +1307,7 @@ fixed it here. See issue #1525 for the full background. ### Bundle id divergence (intentional) Swift `CuaDriver.app` → `com.trycua.driver`. -Rust `CuaDriverRs.app` → `com.trycua.cuadriverrs`. +Rust `CuaDriver.app` → `com.trycua.driver`. The two bundles coexist on disk and in TCC; a user can grant Accessibility + Screen Recording to each independently. The Rust port has its own bundle name + identifier so: @@ -2191,7 +2191,7 @@ post-install GC pass to trim oldest dirs back to a configurable cap. `Invoke-OldReleasesGc` (ps1) is invoked only after `current` has been retargeted at the new install, so the about-to-be-active version is never a deletion candidate. -4. **macOS path unchanged** — the macOS `/Applications/CuaDriverRs.app` +4. **macOS path unchanged** — the macOS `/Applications/CuaDriver.app` install is an in-place replacement (no per-version directory accumulation), so the GC pass is a no-op there by construction (the Darwin branch never enters the versioned-dirs install path). diff --git a/libs/cua-driver-rs/crates/cua-driver/src/bundle.rs b/libs/cua-driver-rs/crates/cua-driver/src/bundle.rs index 02cc841bcd..bea611bd63 100644 --- a/libs/cua-driver-rs/crates/cua-driver/src/bundle.rs +++ b/libs/cua-driver-rs/crates/cua-driver/src/bundle.rs @@ -5,14 +5,14 @@ //! whether `cua-driver-rs mcp` was spawned from an IDE terminal as a //! bare CLI symlinked into our .app bundle. When true and the parent //! isn't launchd, we re-launch the daemon via `open -n -g -a -//! CuaDriverRs --args serve` so it picks up the bundle's TCC grants, +//! CuaDriver --args serve` so it picks up the bundle's TCC grants, //! then proxy stdio MCP traffic through the daemon's Unix socket. //! //! Non-macOS targets compile to no-ops so the cross-platform call //! sites stay tidy. /// Returns `true` when the currently-running binary resolves into an -/// installed `CuaDriverRs.app` bundle (Rust port). The check is the +/// installed `CuaDriver.app` bundle (Rust port). The check is the /// same shape as the Swift driver's `isExecutableInsideCuaDriverApp` /// (`/CuaDriver.app/Contents/MacOS/`) but keyed on the Rust port's /// distinct bundle name so the two installs don't collide. @@ -25,12 +25,12 @@ /// 1. Resolve `std::env::current_exe()` (preferred; absolute path /// to the running image). /// 2. Walk symlinks via `std::fs::canonicalize` — the install layout -/// is `~/.local/bin/cua-driver` → `/Applications/CuaDriverRs.app/ +/// is `~/.local/bin/cua-driver` → `/Applications/CuaDriver.app/ /// Contents/MacOS/cua-driver`, so without the canonicalize step /// we'd see the bare symlink path and miss the bundle. /// 3. Substring-match the canonical path for the bundle marker. #[cfg(target_os = "macos")] -pub fn is_executable_inside_cuadriverrs_app() -> bool { +pub fn is_executable_inside_cuadriver_app() -> bool { let exe = match std::env::current_exe() { Ok(p) => p, Err(_) => return false, @@ -43,16 +43,16 @@ pub fn is_executable_inside_cuadriverrs_app() -> bool { Some(s) => s, None => return false, }; - s.contains("/CuaDriverRs.app/Contents/MacOS/") + s.contains("/CuaDriver.app/Contents/MacOS/") } #[cfg(not(target_os = "macos"))] -pub fn is_executable_inside_cuadriverrs_app() -> bool { +pub fn is_executable_inside_cuadriver_app() -> bool { false } /// Returns `true` when the parent process is *not* `launchd` (pid 1). -/// Combined with [`is_executable_inside_cuadriverrs_app`], a `true` +/// Combined with [`is_executable_inside_cuadriver_app`], a `true` /// here means the binary was spawned from a shell / IDE terminal that /// inherits the wrong TCC responsibility — i.e. the case we want to /// auto-relaunch from. @@ -107,7 +107,7 @@ mod tests { // deps/`, never inside a .app bundle. Should always return // false in CI / local dev, which is exactly the behavior we // want so `cargo run` callers stay in-process. - assert!(!is_executable_inside_cuadriverrs_app()); + assert!(!is_executable_inside_cuadriver_app()); } #[test] 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 31a54b4f78..6768779667 100644 --- a/libs/cua-driver-rs/crates/cua-driver/src/cli.rs +++ b/libs/cua-driver-rs/crates/cua-driver/src/cli.rs @@ -19,10 +19,10 @@ use mcp_server::{protocol::Content, tool::ToolRegistry}; pub enum Command { Mcp { /// Force in-process MCP execution — skip the TCC auto-relaunch - /// path that would spawn a daemon via `open -n -g -a CuaDriverRs + /// path that would spawn a daemon via `open -n -g -a CuaDriver /// --args serve` and proxy stdio MCP requests through its Unix /// socket. Useful when the calling context already has the right - /// TCC grants (CuaDriverRs.app launched us directly), or when + /// TCC grants (CuaDriver.app launched us directly), or when /// diagnosing in-process failures. Also toggleable via /// `CUA_DRIVER_RS_MCP_NO_RELAUNCH=1`. no_daemon_relaunch: bool, @@ -127,7 +127,7 @@ pub fn parse_command() -> Command { println!(" --from main (install only) Fetch latest from main branch instead of the tagged release."); println!(); println!("mcp options (macOS):"); - println!(" --no-daemon-relaunch Stay in-process; skip auto-launching the CuaDriverRs daemon."); + println!(" --no-daemon-relaunch Stay in-process; skip auto-launching the CuaDriver daemon."); println!(" Also: CUA_DRIVER_RS_MCP_NO_RELAUNCH=1"); println!(" --socket Override the daemon UDS path used by the proxy fallback."); println!(); @@ -328,7 +328,7 @@ pub fn run_describe(registry: &ToolRegistry, name: &str) { /// /// Mirrors Swift `MCPCommand.shouldUseDaemonProxy` in spirit: /// the trigger is "shell-spawned bare binary that resolves into an -/// installed `CuaDriverRs.app` bundle, with a non-launchd parent". +/// installed `CuaDriver.app` bundle, with a non-launchd parent". /// When any of those conditions fails — explicit opt-out, dev-mode /// `cargo run` invocation, already-relaunched-via-launchd — we stay /// in-process. The proxy path is purely additive. @@ -337,7 +337,7 @@ pub fn run_describe(registry: &ToolRegistry, name: &str) { /// there's no `open -a` equivalent on Linux / Windows. #[cfg(target_os = "macos")] pub fn should_use_daemon_proxy(no_daemon_relaunch: bool) -> bool { - use crate::bundle::{is_env_truthy, is_executable_inside_cuadriverrs_app, parent_is_not_launchd}; + use crate::bundle::{is_env_truthy, is_executable_inside_cuadriver_app, parent_is_not_launchd}; if no_daemon_relaunch { return false; } @@ -345,7 +345,7 @@ pub fn should_use_daemon_proxy(no_daemon_relaunch: bool) -> bool { return false; } // Hidden test/escape hook: force proxy mode without requiring the - // executable to live inside CuaDriverRs.app. Used by the + // executable to live inside CuaDriver.app. Used by the // integration test (which spawns a daemon manually) and by users // who've wrapped the binary in a custom bundle. Skips the // launch_daemon_and_wait `open -a` step too — caller is expected @@ -353,7 +353,7 @@ pub fn should_use_daemon_proxy(no_daemon_relaunch: bool) -> bool { if is_env_truthy("CUA_DRIVER_RS_MCP_FORCE_PROXY") { return true; } - if !is_executable_inside_cuadriverrs_app() { + if !is_executable_inside_cuadriver_app() { // Raw `cargo run` / dev binary — no installed bundle to land // in, so relaunching would fail. Stay in-process. return false; @@ -371,7 +371,7 @@ pub fn should_use_daemon_proxy(_no_daemon_relaunch: bool) -> bool { false } -/// Spawn `/usr/bin/open -n -g -a CuaDriverRs --args serve` to launch +/// Spawn `/usr/bin/open -n -g -a CuaDriver --args serve` to launch /// the daemon under `LaunchServices` (so it inherits the bundle's /// TCC attribution), then poll the socket for up to `timeout_secs` /// seconds. Returns Err with a diagnostic message if `open` failed @@ -393,14 +393,14 @@ pub fn launch_daemon_and_wait(socket_path: &str, timeout_secs: u64) -> anyhow::R // actually differs from the default, so the common case keeps the // shorter `open` argv (and matches Swift's invocation byte-for-byte). let pass_socket = socket_path != crate::serve::default_socket_path(); - let mut open_args: Vec<&str> = vec!["-n", "-g", "-a", "CuaDriverRs", "--args", "serve"]; + let mut open_args: Vec<&str> = vec!["-n", "-g", "-a", "CuaDriver", "--args", "serve"]; if pass_socket { open_args.push("--socket"); open_args.push(socket_path); } let status = Cmd::new("/usr/bin/open") - // `-n` forces a new instance: CuaDriverRs.app might already be + // `-n` forces a new instance: CuaDriver.app might already be // running from a previous MCP session, and without `-n`, `open // -a` would re-use it and drop our `--args serve`, leaving no // daemon up. `-g` keeps the new instance backgrounded — @@ -419,8 +419,8 @@ pub fn launch_daemon_and_wait(socket_path: &str, timeout_secs: u64) -> anyhow::R if !status.success() { anyhow::bail!( - "`open -n -g -a CuaDriverRs --args serve{}` exited {:?}. \ - Check that `/Applications/CuaDriverRs.app` is installed, or \ + "`open -n -g -a CuaDriver --args serve{}` exited {:?}. \ + Check that `/Applications/CuaDriver.app` is installed, or \ pass --no-daemon-relaunch to bypass.", if pass_socket { format!(" --socket {socket_path}") } else { String::new() }, status.code() @@ -440,7 +440,7 @@ pub fn launch_daemon_and_wait(socket_path: &str, timeout_secs: u64) -> anyhow::R anyhow::bail!( "daemon did not appear on {socket_path} within {timeout_secs}s. If this \ is the first launch, grant Accessibility + Screen Recording to \ - CuaDriverRs.app in System Settings and retry. Pass --no-daemon-relaunch \ + CuaDriver.app in System Settings and retry. Pass --no-daemon-relaunch \ to stay in-process." ); } @@ -456,7 +456,7 @@ pub fn run_mcp_via_daemon_proxy(socket: Option) -> anyhow::Result<()> { 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 CuaDriverRs.app to + // 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!( @@ -471,8 +471,8 @@ pub fn run_mcp_via_daemon_proxy(socket: Option) -> anyhow::Result<()> { String::new() }; eprintln!( - "cua-driver-rs: mcp launched without CuaDriverRs.app's TCC grants; \ - auto-launching the daemon via `open -n -g -a CuaDriverRs --args serve{socket_suffix}` \ + "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)?; 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 6fae35b873..aa635d9e75 100644 --- a/libs/cua-driver-rs/crates/cua-driver/src/main.rs +++ b/libs/cua-driver-rs/crates/cua-driver/src/main.rs @@ -220,7 +220,7 @@ fn main() { // banner can land on stderr in either dispatch path. version_check::maybe_announce_update(); // TCC sidestep: if we're a shell-spawned bare binary that - // resolves into /Applications/CuaDriverRs.app, run the + // resolves into /Applications/CuaDriver.app, run the // proxy path instead of the in-process MCP server. The // proxy ensures a daemon is up under the bundle's TCC // attribution and forwards stdio MCP through its socket. diff --git a/libs/cua-driver-rs/crates/cua-driver/src/proxy.rs b/libs/cua-driver-rs/crates/cua-driver/src/proxy.rs index 09be4ee832..15b2144735 100644 --- a/libs/cua-driver-rs/crates/cua-driver/src/proxy.rs +++ b/libs/cua-driver-rs/crates/cua-driver/src/proxy.rs @@ -5,7 +5,7 @@ //! mirror of Swift PR #1479). When `cua-driver-rs mcp` is invoked from //! an IDE terminal — Claude Code, Cursor, VS Code, Warp — macOS TCC //! attributes the process to the calling terminal, not to -//! `CuaDriverRs.app`. The MCP client side sees a normal stdio server, +//! `CuaDriver.app`. The MCP client side sees a normal stdio server, //! but every AX probe silently fails because the binary is running //! against the wrong bundle id. //! @@ -48,7 +48,7 @@ pub async fn run_proxy(socket_path: String) -> anyhow::Result<()> { if !is_daemon_listening(&socket_path) { anyhow::bail!( "cua-driver-rs daemon not reachable on {socket_path}. Start it \ - with `open -n -g -a CuaDriverRs --args serve` and retry." + with `open -n -g -a CuaDriver --args serve` and retry." ); } diff --git a/libs/cua-driver-rs/scripts/CuaDriver.app/Contents/Info.plist b/libs/cua-driver-rs/scripts/CuaDriver.app/Contents/Info.plist new file mode 100644 index 0000000000..feec768b06 --- /dev/null +++ b/libs/cua-driver-rs/scripts/CuaDriver.app/Contents/Info.plist @@ -0,0 +1,69 @@ + + + + + + CFBundleIdentifier + com.trycua.driver + CFBundleName + Cua Driver + CFBundleDisplayName + Cua Driver + CFBundleExecutable + cua-driver + CFBundlePackageType + APPL + + CFBundleShortVersionString + 0.0.0-dev + CFBundleVersion + 0 + LSMinimumSystemVersion + 13.0 + LSUIElement + + NSHighResolutionCapable + + NSSupportsAutomaticTermination + + + diff --git a/libs/cua-driver-rs/scripts/CuaDriverRs.app/Contents/MacOS/.gitkeep b/libs/cua-driver-rs/scripts/CuaDriver.app/Contents/MacOS/.gitkeep similarity index 90% rename from libs/cua-driver-rs/scripts/CuaDriverRs.app/Contents/MacOS/.gitkeep rename to libs/cua-driver-rs/scripts/CuaDriver.app/Contents/MacOS/.gitkeep index 32cec09275..cc98bb02cb 100644 --- a/libs/cua-driver-rs/scripts/CuaDriverRs.app/Contents/MacOS/.gitkeep +++ b/libs/cua-driver-rs/scripts/CuaDriver.app/Contents/MacOS/.gitkeep @@ -8,7 +8,7 @@ # See: # - libs/cua-driver-rs/scripts/install.sh (downloads the binary # from GitHub Releases, copies it into Contents/MacOS, installs -# the bundle to /Applications/CuaDriverRs.app, and creates the +# the bundle to /Applications/CuaDriver.app, and creates the # ~/.local/bin/cua-driver symlink pointing into the bundle) # - .github/workflows/cd-rust-cua-driver.yml (bakes the assembled # .app into the darwin release tarball at CD time) diff --git a/libs/cua-driver-rs/scripts/CuaDriverRs.app/Contents/Info.plist b/libs/cua-driver-rs/scripts/CuaDriverRs.app/Contents/Info.plist deleted file mode 100644 index 552dad462e..0000000000 --- a/libs/cua-driver-rs/scripts/CuaDriverRs.app/Contents/Info.plist +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - CFBundleIdentifier - com.trycua.cuadriverrs - CFBundleName - Cua Driver RS - CFBundleDisplayName - Cua Driver RS - CFBundleExecutable - cua-driver - CFBundlePackageType - APPL - CFBundleShortVersionString - 0.1.3 - CFBundleVersion - 1 - LSMinimumSystemVersion - 13.0 - LSUIElement - - NSHighResolutionCapable - - NSSupportsAutomaticTermination - - - diff --git a/libs/cua-driver-rs/scripts/install-local.sh b/libs/cua-driver-rs/scripts/install-local.sh index 65b7686065..9432eff6f5 100755 --- a/libs/cua-driver-rs/scripts/install-local.sh +++ b/libs/cua-driver-rs/scripts/install-local.sh @@ -25,8 +25,8 @@ # -> ../current/cua-driver # # macOS layout produced: -# /Applications/CuaDriverRs.app/Contents/MacOS/cua-driver (bundle replaced wholesale) -# $HOME/.local/bin/cua-driver -> .../CuaDriverRs.app/Contents/MacOS/cua-driver +# /Applications/CuaDriver.app/Contents/MacOS/cua-driver (bundle replaced wholesale) +# $HOME/.local/bin/cua-driver -> .../CuaDriver.app/Contents/MacOS/cua-driver # # The version string carries `-local-debug` / `-local-release` so it # never collides with a real release dir and is trivial to GC. diff --git a/libs/cua-driver/scripts/_install-rust.sh b/libs/cua-driver/scripts/_install-rust.sh index 33e78e2bb2..ea114617d2 100644 --- a/libs/cua-driver/scripts/_install-rust.sh +++ b/libs/cua-driver/scripts/_install-rust.sh @@ -89,13 +89,14 @@ KEEP_VERSIONS_DEFAULT=5 KEEP_VERSIONS="${CUA_DRIVER_RS_KEEP_VERSIONS:-$KEEP_VERSIONS_DEFAULT}" # macOS-only: name and install location of the .app bundle that wraps -# the bare binary so the TCC auto-relaunch path in `cua-driver-rs mcp` -# has a stable bundle id (com.trycua.cuadriverrs) to attribute the -# daemon to. See libs/cua-driver-rs/scripts/CuaDriverRs.app/Contents/ -# Info.plist and the matching docs on `cua-driver-rs mcp`'s auto- -# relaunch behavior. Distinct from the Swift driver's CuaDriver.app -# (com.trycua.driver) so the two installs coexist on the same machine. -APP_NAME="CuaDriverRs.app" +# the bare binary so the TCC auto-relaunch path in `cua-driver mcp` has +# a stable bundle id (com.trycua.driver) to attribute the daemon to. +# See libs/cua-driver-rs/scripts/CuaDriver.app/Contents/Info.plist and +# the matching docs on `cua-driver mcp`'s auto-relaunch behavior. +# Identical to the Swift driver's CuaDriver.app + com.trycua.driver +# pair — the Rust port replaces the Swift install at this path, +# preserving TCC grants (they're keyed on bundle id, which we share). +APP_NAME="CuaDriver.app" APP_DEST="/Applications/$APP_NAME" while [[ $# -gt 0 ]]; do @@ -422,10 +423,10 @@ VERSION="${TAG#${TAG_PREFIX}}" # Tarball selection: # # macOS — fetch the directory tarball (cua-driver-rs-vN-darwin-universal.tar.gz). -# The directory layout includes `CuaDriverRs.app/` alongside the bare +# The directory layout includes `CuaDriver.app/` alongside the bare # binary, which we need to install into /Applications so the TCC # auto-relaunch path in `cua-driver-rs mcp` can resolve -# `com.trycua.cuadriverrs` via `open -n -g -a CuaDriverRs`. The +# `com.trycua.driver` via `open -n -g -a CuaDriver`. The # directory variant carries the same universal binary as the # bare-binary tarball, so users on both Apple Silicon and Intel # get a working install from one download. @@ -451,7 +452,7 @@ tar -xzf "$TMP_DIR/$TARBALL" -C "$TMP_DIR" # macOS dir tarball expands to: # cua-driver-rs-${VERSION}-darwin-universal/ # ├── cua-driver (bare universal binary) -# ├── CuaDriverRs.app/ (minimal bundle; copy of the same binary +# ├── CuaDriver.app/ (minimal bundle; copy of the same binary # │ lives at Contents/MacOS/cua-driver) # └── LICENSE # Linux bare-binary tarball expands to: @@ -479,17 +480,21 @@ mkdir -p "$BIN_DIR" # macOS: install the .app to /Applications first, then symlink the # bin into the bundle so `~/.local/bin/cua-driver` resolves into -# `/Applications/CuaDriverRs.app/Contents/MacOS/cua-driver`. The -# `realpath` walk in `is_executable_inside_cuadriverrs_app()` keys on +# `/Applications/CuaDriver.app/Contents/MacOS/cua-driver`. The +# `realpath` walk in `is_executable_inside_cuadriver_app()` keys on # that resolved path to know whether the auto-relaunch heuristic -# should fire. Same shape as the Swift `cua-driver` install path — -# different bundle id (com.trycua.cuadriverrs) so the two coexist. +# should fire. Same path and same bundle id as the Swift `cua-driver` +# install (`/Applications/CuaDriver.app`, `com.trycua.driver`), so an +# install over an existing Swift bundle is an in-place takeover — +# TCC grants attributed to the shared bundle id survive the swap and +# the new binary inherits them (macOS may re-prompt once on first +# action because the cdhash differs; after that the grants persist). # # The macOS path intentionally does NOT use the # $HOME_DIR/packages/releases// + current symlink layout used on -# Linux. Reason: /Applications/CuaDriverRs.app placement is the +# Linux. Reason: /Applications/CuaDriver.app placement is the # anchor for both TCC attribution (cdhash + bundle id) and -# LaunchServices' `open -a CuaDriverRs` discovery — symlinking the +# LaunchServices' `open -a CuaDriver` discovery — symlinking the # .app from /Applications to a versioned dir under $HOME_DIR breaks # both. The asymmetry is deliberate; rollback on macOS = reinstall # an older release tag. @@ -506,7 +511,7 @@ mkdir -p "$BIN_DIR" if [[ "$OS" == "Darwin" ]]; then if [[ -z "${SRC_APP:-}" || ! -d "$SRC_APP" ]]; then err "macOS install requires the .app bundle (SRC_APP not found at ${SRC_APP:-})" - err " This usually means the downloaded tarball is missing CuaDriverRs.app — re-run the installer or" + err " This usually means the downloaded tarball is missing CuaDriver.app — re-run the installer or" err " pin a known-good release via CUA_DRIVER_RS_VERSION=." exit 1 fi @@ -517,8 +522,27 @@ if [[ "$OS" == "Darwin" && -n "$SRC_APP" && -d "$SRC_APP" ]]; then err " Without the .app bundle, \`cua-driver-rs mcp\` from an IDE terminal will not auto-relaunch into a TCC-correct daemon." exit 1 fi + # The Rust port and the legacy Swift driver both live at + # /Applications/CuaDriver.app with bundle id `com.trycua.driver` — + # bundle-id-identical so TCC grants survive the upgrade. When we + # detect a prior Swift bundle at the install path we log it for + # transparency, but no `tccutil reset` is needed; grants transfer + # automatically because they're keyed on bundle id. macOS may + # surface a one-time re-prompt on first action because the cdhash + # of the new binary doesn't match the old one — that's a TCC + # cdhash-pairing detail, not a grant loss. + REPLACED_SWIFT=0 if [[ -e "$APP_DEST" ]]; then - log "removing existing $APP_DEST" + PREV_BUNDLE_ID=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP_DEST/Contents/Info.plist" 2>/dev/null || true) + PREV_BUNDLE_VERSION=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP_DEST/Contents/Info.plist" 2>/dev/null || true) + if [[ "$PREV_BUNDLE_ID" == "com.trycua.driver" ]] && [[ -n "$PREV_BUNDLE_VERSION" ]]; then + log "replacing existing cua-driver at $APP_DEST (${PREV_BUNDLE_ID}, version ${PREV_BUNDLE_VERSION})" + REPLACED_SWIFT=1 + elif [[ -n "$PREV_BUNDLE_ID" ]]; then + log "replacing existing $APP_DEST (bundle id $PREV_BUNDLE_ID)" + else + log "removing existing $APP_DEST" + fi rm -rf "$APP_DEST" fi log "installing $APP_DEST" @@ -631,6 +655,17 @@ fi echo "" echo "cua-driver-rs $VERSION installed." echo "" + +if [[ "${REPLACED_SWIFT:-0}" == "1" ]]; then + echo "Upgraded the cua-driver bundle that was previously at $APP_DEST." + echo "TCC grants (Accessibility, Screen Recording) are keyed on the bundle id" + echo "(com.trycua.driver) — which is preserved — so they transfer to the new" + echo "binary automatically. macOS may surface a one-time re-grant prompt on" + echo "first action because the new binary's cdhash doesn't match the old" + echo "one's; approve once and the grants persist." + echo "" +fi + echo "Try it:" echo " $BIN_LINK list-tools" echo " $BIN_LINK list_apps" diff --git a/libs/cua-driver/scripts/install.sh b/libs/cua-driver/scripts/install.sh index e2c9b5b267..1def4942d2 100755 --- a/libs/cua-driver/scripts/install.sh +++ b/libs/cua-driver/scripts/install.sh @@ -16,7 +16,7 @@ # backend instead of the Swift binary. Delegates to # libs/cua-driver/scripts/_install-rust.sh — see that # script for backend-specific env vars. Installs to a -# separate bundle (CuaDriverRs.app) so the Swift +# separate bundle (CuaDriver.app) so the Swift # binary is left untouched. Also accepted as # --backend=rust. # @@ -113,7 +113,7 @@ fi # rest of argv to _install-rust.sh and exit. The Swift # install path below is never touched in this case, so the Swift binary # (if present) is left exactly as-is — users can roll back by deleting -# /Applications/CuaDriverRs.app and re-running this script without the flag. +# /Applications/CuaDriver.app and re-running this script without the flag. if [[ "$USE_RUST_BACKEND" == "1" ]]; then if [[ "$AUTO_RUST" == "0" ]]; then # Explicit opt-in on macOS — flag the "experimental" framing.