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: 24 additions & 24 deletions .github/workflows/cd-rust-cua-driver.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Comment on lines 276 to +285

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Assert the renamed bundle identifier here too.

Line 284 only checks that the version keys exist, so a stale CFBundleIdentifier would still pass this job even though the Swift→Rust takeover path depends on that value. Please fail fast on com.trycua.cuadriver in the assembled plist as part of this smoke check.

Suggested change
           plutil -replace CFBundleVersion -string "$VERSION" \
             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/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'
+          /usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \
+            release/CuaDriver.app/Contents/Info.plist | grep -qx 'com.trycua.cuadriver'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/cd-rust-cua-driver.yml around lines 276 - 285, Add a check
that the assembled Info.plist contains the expected bundle identifier
"com.trycua.cuadriver" in the same smoke-test step that currently prints
CFBundle(Short)?Version; after plutil -p
release/CuaDriver.app/Contents/Info.plist | grep -E 'CFBundle(Short)?Version'
run a plutil/grep (or plutil + jq) assertion on CFBundleIdentifier and exit
non‑zero (failing the job) if the value is not exactly "com.trycua.cuadriver";
update the step named "Codesign + notarize + staple CuaDriver.app" to include
this assertion so stale identifiers fail fast.

if: env.DO_NOTARIZE == 'true'
working-directory: libs/cua-driver-rs
env:
Expand All @@ -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: |
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</Callout>

### Install `cua-driver-rs` directly (Linux / Windows / macOS)
Expand Down Expand Up @@ -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**

Expand Down Expand Up @@ -161,7 +158,7 @@ So an API outage, an unauthenticated-rate-limit (60 req/hr per IP), or a transie
</Callout>

<Callout type="info">
**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=<x.y.z>`.
**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=<x.y.z>`.
</Callout>

<Callout type="info">
Expand All @@ -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.
</Callout>

<Callout type="info">
Expand Down
12 changes: 6 additions & 6 deletions libs/cua-driver-rs/PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -1280,34 +1280,34 @@ 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`,
`run_mcp_via_daemon_proxy`.
- `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.

### Why this exists
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
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:
Expand Down Expand Up @@ -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).
Expand Down
16 changes: 8 additions & 8 deletions libs/cua-driver-rs/crates/cua-driver/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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]
Expand Down
Loading
Loading