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
54 changes: 49 additions & 5 deletions .github/workflows/cd-rust-cua-driver.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,43 @@ jobs:
mkdir -p release/universal
lipo -create "$ARM64" "$X86" -output release/universal/cua-driver
lipo -info release/universal/cua-driver
- name: Assemble CuaDriverRs.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
# for the TCC auto-relaunch path.
#
# No codesigning at this layer — the bundle ships ad-hoc
# signed (the bare binary inherits whatever signature was
# applied at build/notarize time, currently none for the
# Rust port). TCC keys grants on the cdhash of the binary
# the user grants permission to, so ad-hoc is fine for the
# BETA release; production signing will land in a separate
# 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
cp release/universal/cua-driver \
release/CuaDriverRs.app/Contents/MacOS/cua-driver
chmod +x release/CuaDriverRs.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
plutil -replace CFBundleVersion -string "$VERSION" \
release/CuaDriverRs.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: Package
Comment on lines +179 to 216

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Bundle version metadata should be stamped at package time.

The workflow copies a static Info.plist, so CFBundleShortVersionString / CFBundleVersion can drift from the actual release tag. Please stamp both from ${{ steps.version.outputs.version }} during bundle assembly.

Suggested patch
       - name: Assemble CuaDriverRs.app bundle
         working-directory: libs/cua-driver-rs
         run: |
+          VERSION="${{ steps.version.outputs.version }}"
           mkdir -p release/CuaDriverRs.app
           cp -R scripts/CuaDriverRs.app/Contents release/CuaDriverRs.app/Contents
+          /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" \
+            release/CuaDriverRs.app/Contents/Info.plist
+          /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $VERSION" \
+            release/CuaDriverRs.app/Contents/Info.plist
           cp release/universal/cua-driver \
              release/CuaDriverRs.app/Contents/MacOS/cua-driver
           chmod +x release/CuaDriverRs.app/Contents/MacOS/cua-driver
🤖 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 179 - 205, The static
Info.plist copied into release/CuaDriverRs.app can drift from the release tag;
update the "Assemble CuaDriverRs.app bundle" step to stamp
CFBundleShortVersionString and CFBundleVersion in
release/CuaDriverRs.app/Contents/Info.plist with the value from ${{
steps.version.outputs.version }} (use a plist editor such as
/usr/libexec/PlistBuddy or xcrun defaults/plutil equivalent) after copying the
skeleton and before packaging so both keys reflect the actual release tag;
ensure you reference the Info.plist path and the keys CFBundleShortVersionString
and CFBundleVersion in the step.

working-directory: libs/cua-driver-rs
run: |
Expand All @@ -187,11 +224,18 @@ jobs:
# that download by arch still get the universal slice
# (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
# 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 ../../LICENSE.md "release/${STAGE}/LICENSE" 2>/dev/null || true
(cd release && tar -czf "${STAGE}.tar.gz" "${STAGE}")
done
# Bare universal binary — single-file tarball matching Swift's
# `cua-driver-${VERSION}-darwin-universal-binary.tar.gz`.
# `cua-driver-${VERSION}-darwin-universal-binary.tar.gz`. NO
# bundle here: callers that fetch the bare tarball deliberately
# skipped the .app workflow.
tar -czf "release/cua-driver-rs-${VERSION}-darwin-universal-binary.tar.gz" \
-C release/universal cua-driver
ls -lh release/*.tar.gz
Expand Down Expand Up @@ -311,10 +355,10 @@ 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
- `cua-driver-rs-${{ steps.version.outputs.version }}-darwin-arm64.tar.gz` — same universal binary, named for arm64 callers
- `cua-driver-rs-${{ steps.version.outputs.version }}-darwin-x86_64.tar.gz` — same universal binary, 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)
- `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-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)

**Linux**
- `cua-driver-rs-${{ steps.version.outputs.version }}-linux-x86_64.tar.gz` — directory tarball
Expand Down
15 changes: 8 additions & 7 deletions libs/cua-driver-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

155 changes: 155 additions & 0 deletions libs/cua-driver-rs/PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,161 @@ Swift.

---

## CLI subcommand: `mcp` (TCC auto-relaunch / daemon proxy)
- Swift:
- `libs/cua-driver/Sources/CuaDriverCLI/CuaDriverCommand.swift` —
`MCPCommand`, `shouldUseDaemonProxy`, `runViaDaemonProxy`,
`launchDaemonViaOpen`, `waitForDaemon`.
- `libs/cua-driver/Sources/CuaDriverCLI/BundleHelpers.swift` —
`isExecutableInsideCuaDriverApp()`.
- `libs/cua-driver/Sources/CuaDriverServer/CuaDriverMCPServer.swift` —
`makeProxy` (the actor that re-implements `ListTools` /
`CallTool` over the daemon UDS).
- Rust:
- `libs/cua-driver-rs/crates/cua-driver/src/bundle.rs` —
`is_executable_inside_cuadriverrs_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` —
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.
- 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
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`.
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:
- `open -n -g -a CuaDriverRs --args serve` never accidentally
relaunches into the Swift bundle (and vice versa).
- TCC grants are per-cdhash, so granting one doesn't carry into
the other — users explicitly opt in to each binary.

### Escape hatches
- `--no-daemon-relaunch` flag — same flag Swift exposes.
- `CUA_DRIVER_RS_MCP_NO_RELAUNCH=1` env var — Rust-specific name
(Swift uses `CUA_DRIVER_MCP_NO_RELAUNCH`).
- `--socket <path>` flag — override the daemon UDS path used by the
proxy.
- `CUA_DRIVER_RS_MCP_FORCE_PROXY=1` env var (Rust-only) — force
proxy mode without the bundle-context check. Useful when wrapping
the binary in a custom .app, or for manual smoke-testing of the
proxy path against a daemon you've already started by hand. Skips
the `open -a` step entirely; caller must supply a daemon on
`--socket`.

### Daemon protocol divergence
The daemon's `list` method now returns full `ToolDef`
(`input_schema` + annotation hints), not just `{name, description}`.
The proxy uses this to build a complete `tools/list` from one
round-trip instead of N+1 list+describe calls. Backwards compatible:
older clients that only read name/description still work.

### Manual smoke test (macOS)
1. `cua-driver serve --socket /tmp/test.sock &`
2. `CUA_DRIVER_RS_MCP_FORCE_PROXY=1 cua-driver mcp --socket /tmp/test.sock`
3. From an MCP client, run the standard initialize → tools/list →
tools/call get_screen_size handshake. Expect identical envelope
shape to the in-process path. Concretely:

`tools/list` response (the daemon caches and returns it once at
proxy startup — same shape as the in-process server's `tools/list`):

```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{ "name": "browser_eval", "description": "…", "inputSchema": {…}, "annotations": {…} },
{ "name": "check_permissions", "description": "…", "inputSchema": {…}, "annotations": {…} },
{ "name": "click", "description": "…", "inputSchema": {…}, "annotations": {…} },
{ "name": "double_click", "…": "…" },
{ "name": "drag", "…": "…" },
{ "name": "get_accessibility_tree", "…": "…" },
{ "name": "get_config", "…": "…" },
{ "name": "get_cursor_position", "…": "…" },
{ "name": "get_recording_state", "…": "…" },
{ "name": "get_screen_size", "…": "…" },
{ "name": "get_window_state", "…": "…" },
{ "name": "hotkey", "…": "…" },
{ "name": "launch_app", "…": "…" },
{ "name": "list_apps", "…": "…" },
{ "name": "list_windows", "…": "…" },
{ "name": "page", "…": "…" },
{ "name": "press_key", "…": "…" },
{ "name": "replay_trajectory", "…": "…" },
{ "name": "right_click", "…": "…" },
{ "name": "screenshot", "…": "…" },
{ "name": "scroll", "…": "…" },
{ "name": "set_config", "…": "…" },
{ "name": "set_recording", "…": "…" },
{ "name": "set_value", "…": "…" },
{ "name": "type_text", "…": "…" },
{ "name": "zoom", "…": "…" }
// …plus the agent_cursor.* family when overlay is enabled.
// For an exact snapshot run: `cua-driver list-tools`
]
}
}
```

`tools/call get_screen_size` request + response:

```json
// → stdin
{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"get_screen_size","arguments":{}}}

// ← stdout
{"jsonrpc":"2.0","id":2,"result":{
"content":[{"type":"text","text":"{\"width\":1920,\"height\":1080}"}],
"structuredContent":{"width":1920,"height":1080},
"isError":false
}}
```

The `result` envelope is identical to the in-process path —
structuredContent + text mirror, no proxy-specific wrapping.

4. Without spawning the daemon first, repeat step 2. Expect
non-zero exit and a "daemon not reachable" diagnostic on stderr
(the fail-fast contract that matches Swift `makeProxy`). Exact
stderr text emitted by `main.rs`'s proxy-error branch (wrapping
`proxy::run_proxy`'s pre-check):

```
cua-driver-rs: cua-driver-rs daemon not reachable on /tmp/test.sock. Start it with `open -n -g -a CuaDriverRs --args serve` and retry.
```

Process exits with status `1` before reading any MCP request on
stdin. With `CUA_DRIVER_RS_MCP_FORCE_PROXY=1` set, `cli.rs`'s
`run_mcp_via_daemon_proxy` emits the more specific:

```
cua-driver-rs: CUA_DRIVER_RS_MCP_FORCE_PROXY=1 but no daemon listening on /tmp/test.sock. Start one with `cua-driver serve --socket /tmp/test.sock` and retry.
```

---

## CLI subcommands: `status` + `stop`
- Swift: `libs/cua-driver/Sources/CuaDriverCLI/ServeCommand.swift:368-470`
- Rust: `libs/cua-driver-rs/crates/cua-driver/src/serve.rs::run_status_cmd, run_stop_cmd`
Expand Down
6 changes: 6 additions & 0 deletions libs/cua-driver-rs/crates/cua-driver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ cursor-overlay = { path = "../cursor-overlay" }
async-trait = "0.1"
base64 = { workspace = true }

# Used by crate::bundle::parent_is_not_launchd() for the TCC
# auto-relaunch detection path on Unix (only the macOS heuristic
# actually fires, but the crate compiles on Linux too).
[target.'cfg(unix)'.dependencies]
libc = "0.2"

[target.'cfg(target_os = "macos")'.dependencies]
platform-macos = { path = "../platform-macos" }

Expand Down
Loading
Loading