Skip to content

feat(platform-macos): TCC auto-relaunch for cua-driver-rs mcp (#1525) - #1530

Merged
f-trycua merged 10 commits into
mainfrom
feat/cua-driver-rs-tcc-auto-relaunch-1525
May 16, 2026
Merged

feat(platform-macos): TCC auto-relaunch for cua-driver-rs mcp (#1525)#1530
f-trycua merged 10 commits into
mainfrom
feat/cua-driver-rs-tcc-auto-relaunch-1525

Conversation

@f-trycua

@f-trycua f-trycua commented May 16, 2026

Copy link
Copy Markdown
Collaborator

Closes #1525.

Ports Swift PR #1479's TCC auto-relaunch / daemon-proxy fix to
cua-driver-rs. 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 — so AX probes silently fail against
the wrong bundle id. This PR mirrors the Swift fix: detect that
context, spawn a daemon under LaunchServices (which gets the bundle's
TCC attribution), then proxy stdio MCP traffic through the daemon's
Unix socket. MCP clients see an ordinary stdio server; no Python
bridge required.

Packaging decision

The Swift driver ships as /Applications/CuaDriver.app so its
auto-relaunch uses open -n -g -a CuaDriver --args serve. The Rust
port currently ships as a bare binary at ~/.local/bin/cua-driver,
so open -a can't be the trigger as-is.

Chosen approach: ship a minimal .app bundle for the Rust port too.

  • New libs/cua-driver-rs/scripts/CuaDriverRs.app/Contents/Info.plist
    with bundle id com.trycua.cuadriverrsdistinct from Swift's
    com.trycua.driver so the two installs coexist in TCC and a user
    can grant Accessibility + Screen Recording to each independently.
  • install.sh installs the bundle to /Applications/CuaDriverRs.app
    via ditto and symlinks ~/.local/bin/cua-driver into
    Contents/MacOS/cua-driver (same shape as the Swift driver).
  • .github/workflows/cd-rust-cua-driver.yml assembles the bundle at
    CD time and bakes it into every macOS directory tarball. The
    bare-binary tarball is preserved for callers who explicitly want
    only the binary.

No new signing or notarization setup — the bundle inherits whatever
signature the binary has (currently ad-hoc on the BETA Rust port,
same as before this PR). Production signing is out of scope for
#1525 and would land in a separate change.

Commits

SHA Title
35172da feat(cua-driver-rs): package as macOS .app bundle for TCC attribution
fb1e105 feat(platform-macos): bundle-context detection helper
4b24a31 feat(mcp): daemon-proxy mode for stdio MCP
200648a feat(cli): auto-relaunch mcp from IDE terminal context
abc1794 docs(parity): document TCC auto-relaunch / daemon-proxy path for mcp

What changed

New modules (Rust):

  • crates/cua-driver/src/bundle.rsis_executable_inside_cuadriverrs_app()
    (resolves current_exe() through symlinks via canonicalize,
    substring-matches /CuaDriverRs.app/Contents/MacOS/),
    parent_is_not_launchd() (libc::getppid() != 1), is_env_truthy().
  • crates/cua-driver/src/proxy.rsrun_proxy(): stdio MCP server
    whose tools/list (cached) and tools/call (via
    tokio::task::spawn_blocking) forward through the daemon socket.
    Fails fast at startup if the daemon isn't reachable — matches
    Swift makeProxy's fetchProxyToolList contract.
  • crates/cua-driver/src/cli.rs: should_use_daemon_proxy(),
    launch_daemon_and_wait() (/usr/bin/open -n -g -a CuaDriverRs --args serve),
    run_mcp_via_daemon_proxy().

Modified:

  • crates/cua-driver/src/cli.rs::Command::Mcp is now a struct
    variant carrying no_daemon_relaunch and socket. New CLI flags:
    --no-daemon-relaunch, --socket <path>.
  • crates/cua-driver/src/main.rs (macOS): dispatch through the
    proxy path when should_use_daemon_proxy is true; else fall
    through to the existing in-process MCP server.
  • crates/cua-driver/src/serve.rs: daemon's list method now
    returns full ToolDef (input_schema + annotation hints) so the
    proxy can build tools/list in one round-trip. Backwards
    compatible (older clients ignore the extra fields).
  • crates/cua-driver/Cargo.toml: libc = \"0.2\" for getppid.
  • scripts/install.sh: macOS path downloads the directory tarball,
    installs CuaDriverRs.app to /Applications, symlinks bin into
    the bundle. Linux/WSL path unchanged.
  • .github/workflows/cd-rust-cua-driver.yml: assemble the bundle
    at CD time from scripts/CuaDriverRs.app/Contents/Info.plist +
    the universal binary, ship it inside every macOS directory tarball.
  • PARITY.md: new "CLI subcommand: `mcp` (TCC auto-relaunch /
    daemon proxy)" entry under the lifecycle/process-model section,
    linking Swift's MCPCommand + BundleHelpers + makeProxy to
    the new Rust modules. Documents the bundle-id divergence
    (intentional), the four escape hatches, the daemon protocol
    extension, and a manual smoke-test recipe.

Escape hatches

  • --no-daemon-relaunch flag (matches Swift)
  • CUA_DRIVER_RS_MCP_NO_RELAUNCH=1 env (Rust-specific name; Swift
    uses CUA_DRIVER_MCP_NO_RELAUNCH)
  • --socket <path> flag — override daemon UDS path
  • CUA_DRIVER_RS_MCP_FORCE_PROXY=1 env (Rust-only) — force proxy
    mode without the bundle-context check. Useful for custom bundles
    or manual smoke-testing. Skips the open -a step entirely;
    caller must supply a daemon on --socket.

Verification

  • cargo build --release clean on macOS (one pre-existing warning
    in cli.rs::run_dump_docs, unchanged by this PR).
  • cargo test -p cua-driver — bundle.rs unit tests (4) pass; the
    one pre-existing failure (test_type_text_chars_tool) is
    environmental (CGEvent insertion against whatever has focus),
    not introduced by this PR.
  • Manual smoke test recipe (also in PARITY.md):
    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, drive initialize → tools/list →
      tools/call get_screen_size; expect identical envelope shape
      to the in-process path.
    4. Without spawning the daemon first, repeat (2); expect non-zero
      exit + "daemon not reachable" diagnostic on stderr.

Test plan

  • On a clean macOS machine: run scripts/install.sh, verify
    /Applications/CuaDriverRs.app/Contents/MacOS/cua-driver exists
    and ~/.local/bin/cua-driver is a symlink that resolves into it.
  • Grant Accessibility + Screen Recording to
    /Applications/CuaDriverRs.app in System Settings.
  • From an IDE terminal (Claude Code / Cursor / VS Code / Warp),
    configure cua-driver mcp as an MCP server. Confirm a TCC-correct
    daemon spawns (visible in Activity Monitor as CuaDriverRs) and
    tool calls succeed.
  • Run with --no-daemon-relaunch; confirm we stay in-process
    (no daemon spawned, AX calls fail against the wrong bundle if TCC
    isn't granted to the IDE terminal).
  • Set CUA_DRIVER_RS_MCP_NO_RELAUNCH=1 env; same as above.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

New Features

  • macOS app bundle packaging enables native system integration and streamlined installation
  • MCP proxy functionality for forwarding requests through daemon service
  • Enhanced daemon responses with complete tool definitions
  • New control options for daemon auto-launch behavior

Documentation

  • Added comprehensive macOS integration documentation including daemon proxy setup and manual smoke test procedures

Review Change Stack

f-trycua and others added 5 commits May 16, 2026 23:09
Adds the .app bundle skeleton needed for issue #1525's TCC auto-relaunch
path. The Rust port currently ships as a bare binary at ~/.local/bin/
cua-driver, which inherits the calling shell/IDE-terminal's TCC
responsibility when invoked as an MCP stdio server — the same pathology
the Swift driver hit before #1479. The fix mirrors the Swift approach:
ship a minimal .app bundle (CuaDriverRs.app, bundle id
com.trycua.cuadriverrs) wrapping the same universal binary, and resolve
the bare CLI symlink into it. Future commits wire up the detection +
relaunch + proxy logic.

This commit does not change runtime behavior yet. It only:
- Adds libs/cua-driver-rs/scripts/CuaDriverRs.app/Contents/Info.plist
  with the bundle id, LSUIElement=true (headless), distinct from the
  Swift driver's com.trycua.driver so both installs coexist.
- Updates scripts/install.sh on macOS to download the directory
  tarball (which carries the .app), ditto it to /Applications/
  CuaDriverRs.app, and symlink ~/.local/bin/cua-driver into the
  bundle (matching the Swift install layout).
- Updates .github/workflows/cd-rust-cua-driver.yml to assemble the
  .app at release time, drop it into every macOS directory tarball,
  and keep the existing bare-binary tarball untouched (so users who
  explicitly want only the binary can still grab it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds crates/cua-driver/src/bundle.rs with three small helpers used by
the upcoming TCC auto-relaunch path:

- `is_executable_inside_cuadriverrs_app()` — Rust mirror of Swift's
  `isExecutableInsideCuaDriverApp()`. Resolves `current_exe()` through
  symlinks via `canonicalize` and substring-matches
  `/CuaDriverRs.app/Contents/MacOS/`. False for raw `cargo run` / dev
  invocations, true for the installed `~/.local/bin/cua-driver` symlink
  resolving into `/Applications/CuaDriverRs.app/Contents/MacOS/`.

- `parent_is_not_launchd()` — `unsafe { libc::getppid() } != 1`. When
  the parent is launchd, TCC attribution is already correct (we're
  the daemon LaunchServices spawned). Otherwise we're shell-spawned
  and need to relaunch.

- `is_env_truthy(name)` — recognizes `1|true|yes|on`. Used for
  `CUA_DRIVER_RS_MCP_NO_RELAUNCH=1` escape hatch.

Unit tests cover all three. The dead-code allow at the top of the
file silences warnings until commit 4 wires the helpers into
`MCPCommand::run`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds crates/cua-driver/src/proxy.rs — a stdio MCP server whose
`tools/list` and `tools/call` handlers forward through a running
`cua-driver-rs serve` daemon over its Unix socket. This is the
runtime half of the TCC auto-relaunch path (issue #1525, mirror of
Swift PR #1479's `CuaDriverMCPServer.makeProxy`).

The proxy lives in `cua-driver` (not `mcp-server`) because the daemon
protocol is owned by `crate::serve` — `mcp-server` already speaks
JSON-RPC against an in-process registry, the proxy speaks the same
protocol on the client side but the server side is the daemon's UDS
protocol. Putting it here avoids `mcp-server → cua-driver` reverse
coupling.

Behavior:
- Fails fast at startup if the daemon isn't reachable, so MCP clients
  see a clear error rather than a successful handshake that
  advertises zero tools (matches Swift `fetchProxyToolList`).
- Caches the daemon's tool list once at startup (registry is static
  for the daemon's lifetime).
- Forwards `tools/call` via `tokio::task::spawn_blocking` so the
  sync UDS client doesn't block the reactor during AX-heavy calls
  like `screenshot` / `get_window_state`.
- Reshapes the daemon's `{name, description, input_schema, ...}`
  envelope into MCP's `{name, description, inputSchema, annotations:
  {...}}` shape, identical to `ToolDef::to_list_entry`'s in-process
  output.

Drive-by: extend the daemon's `list` handler (both Unix + Windows
paths) to include `input_schema` + annotation hints so proxy callers
can 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.

Wired into `MCPCommand::run` in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires up the TCC auto-relaunch path so `cua-driver-rs mcp` invoked
from an IDE terminal (Claude Code, Cursor, VS Code, Warp) transparently
delegates to a daemon running under CuaDriverRs.app's TCC attribution.
This is the user-facing payoff for issue #1525 — the equivalent of
Swift PR #1479's `MCPCommand` for the Rust port.

Changes:
- `cli::Command::Mcp` becomes a struct variant carrying
  `no_daemon_relaunch: bool` and `socket: Option<String>` (new CLI
  flags `--no-daemon-relaunch` and `--socket <path>`).
- `cli::should_use_daemon_proxy()` — Rust mirror of Swift's
  `shouldUseDaemonProxy`: returns true only when (1) opt-out flag/env
  not set, (2) bundle-context detection fires, (3) ppid != 1.
- `cli::launch_daemon_and_wait()` — `Command::new("/usr/bin/open")
  .args(["-n", "-g", "-a", "CuaDriverRs", "--args", "serve"])` then
  poll the socket up to 10s. Same flags Swift uses; -n forces a new
  instance, -g keeps it backgrounded.
- `cli::run_mcp_via_daemon_proxy()` — orchestrate: ensure daemon is
  up, then `proxy::run_proxy` against its socket on a fresh tokio
  runtime.
- `main.rs` (macOS): dispatch through the proxy path when
  `should_use_daemon_proxy` is true, otherwise fall through to the
  in-process MCP server exactly as before. Non-macOS targets parse
  the flags cleanly so cross-platform MCP configs work, but ignore
  them (no TCC, no proxy).
- Drop the `#[allow(dead_code)]` shims from bundle.rs and proxy.rs
  now that the helpers are wired up.

Escape hatches: `--no-daemon-relaunch` flag or
`CUA_DRIVER_RS_MCP_NO_RELAUNCH=1` env var.

Build verified clean; existing tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a PARITY.md entry under the lifecycle/process-model section
linking Swift's `MCPCommand` (in CuaDriverCommand.swift) + Bundle
helpers + `CuaDriverMCPServer.makeProxy` to the new Rust modules
(`bundle.rs`, `cli.rs::{should_use_daemon_proxy,
launch_daemon_and_wait, run_mcp_via_daemon_proxy}`, `proxy.rs`).

Documents:
- Why the bundle id intentionally diverges from Swift
  (`com.trycua.cuadriverrs` vs `com.trycua.driver`) so the two
  installs coexist in TCC.
- All four escape hatches: `--no-daemon-relaunch` flag,
  `CUA_DRIVER_RS_MCP_NO_RELAUNCH=1`, `--socket <path>`, and the
  Rust-only `CUA_DRIVER_RS_MCP_FORCE_PROXY=1` for users who've
  wrapped the binary in a custom bundle or want to smoke-test the
  proxy against a manually-started daemon.
- The daemon `list` protocol extension (now returns full ToolDef
  so the proxy can build `tools/list` in one round-trip).
- A manual smoke-test recipe to verify the path end-to-end on
  macOS.

Also wires up the `CUA_DRIVER_RS_MCP_FORCE_PROXY=1` knob in
`cli::should_use_daemon_proxy` + `cli::run_mcp_via_daemon_proxy` so
the proxy path can be exercised without an installed `.app` bundle
(skips both the bundle-context check and the `open -a` daemon
spawn — caller must supply a daemon on `--socket`).

Integration test deferred per coordinator request — the substantive
detection + proxy + relaunch logic ships in commits 1–4; this PR
will be smoke-tested manually before merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Ignored Ignored Preview May 16, 2026 9:46pm

Request Review

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3e326cec-03e1-4e0b-96fe-f19cf1e80eb1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements macOS TCC auto-relaunch for cua-driver-rs mcp by adding daemon proxy forwarding. The change detects when the binary runs in an IDE terminal (wrong TCC context), automatically launches a daemon via a CuaDriverRs.app bundle, and proxies all MCP requests through a Unix socket to the daemon. An app bundle skeleton, updated CI packaging steps, and installer changes deliver the bundle to /Applications on macOS.

Changes

macOS MCP daemon proxy

Layer / File(s) Summary
Bundle context and parent-process detection
libs/cua-driver-rs/crates/cua-driver/Cargo.toml, libs/cua-driver-rs/crates/cua-driver/src/bundle.rs
is_executable_inside_cuadriverrs_app() resolves and canonicalizes the running executable path to check for the Rust app bundle marker (macOS-only); parent_is_not_launchd() checks ppid != 1 on Unix; is_env_truthy() parses environment variables as truthy when lowercase value is 1|true|yes|on. All include unit tests and platform stubs.
CLI flags and daemon proxy decision logic
libs/cua-driver-rs/crates/cua-driver/src/cli.rs
Command::Mcp refactored to carry no_daemon_relaunch and optional socket parameters; parse_command() now recognizes --no-daemon-relaunch and --socket <path> flags; should_use_daemon_proxy() decides between in-process and proxy modes based on flags, env vars, executable-bundle location, and launch context.
Daemon launch and proxy initialization
libs/cua-driver-rs/crates/cua-driver/src/cli.rs, libs/cua-driver-rs/crates/cua-driver/src/main.rs
launch_daemon_and_wait() spawns the daemon via /usr/bin/open -n -g -a CuaDriverRs --args serve and polls the socket until ready or timeout; run_mcp_via_daemon_proxy() ensures reachability, builds a Tokio runtime, and starts the proxy server; proxy module is declared in main.rs.
MCP proxy server implementation
libs/cua-driver-rs/crates/cua-driver/src/proxy.rs
run_proxy() reads line-delimited JSON-RPC requests from stdin, caches the daemon tool list at startup, dispatches tools/list and tools/call to handlers that forward or transform daemon responses, and writes JSON-RPC responses to stdout; spawn_blocking wraps synchronous daemon socket I/O.
CLI dispatch to proxy or in-process MCP
libs/cua-driver-rs/crates/cua-driver/src/main.rs
Routes Command::Mcp to daemon proxy (on macOS when should_use_daemon_proxy is true) or falls through to in-process MCP; errors from proxy mode print to stderr and exit with code 1; non-macOS platforms ignore proxy fields and continue in-process.
Daemon list response with full tool metadata
libs/cua-driver-rs/crates/cua-driver/src/serve.rs
Updates the "list" handler in both Unix and Windows implementations to return complete tool definitions (input_schema, read_only, destructive, idempotent, open_world) instead of only name and description, enabling one-round-trip proxy responses.
macOS app bundle skeleton and packaging
libs/cua-driver-rs/scripts/CuaDriverRs.app/Contents/Info.plist, libs/cua-driver-rs/scripts/CuaDriverRs.app/Contents/MacOS/.gitkeep, .github/workflows/cd-rust-cua-driver.yml
Introduces CuaDriverRs.app bundle with bundle id com.trycua.cuadriverrs, minimum macOS 13.0, and headless mode flags; CI adds a step to copy the skeleton, install the universal binary into Contents/MacOS, and copy the assembled bundle into each macOS tarball stage; release notes document bare universal-binary tarball as TCC-bypass option.
macOS app bundle installation and bin-dir symlink
libs/cua-driver-rs/scripts/install.sh
Installer downloads the directory tarball on macOS, detects extracted layout, installs CuaDriverRs.app to /Applications via ditto, validates internal binary, and symlinks ~/.local/bin/cua-driver to the bundle executable; non-macOS continues prior direct binary installation.
MCP parity documentation for macOS TCC behavior
libs/cua-driver-rs/PARITY.md
Documents Swift/Rust MCP parity mapping, bundle-id divergence, escape-hatch flags/env vars (--no-daemon-relaunch, --socket, CUA_DRIVER_RS_MCP_NO_RELAUNCH, CUA_DRIVER_RS_MCP_FORCE_PROXY), daemon protocol divergence (full ToolDef in list response), and manual macOS smoke-test procedure.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • trycua/cua#1479: Swift equivalent that implements the same macOS TCC auto-relaunch and daemon-proxy pattern for the original cua-driver binary.
  • trycua/cua#1413: Swift CI changes to produce separate bare universal binary tarball distinct from the .app bundle; this PR's approach mirrors the pattern.
  • trycua/cua#1518: Earlier tarball naming changes to darwin-universal-binary.tar.gz; this PR switches installer to the new directory tarball format.

Poem

🐰 A daemon wakes at last! Through app-bundle magic,
MCP requests now dance the daemon proxy path—
TCC grins at /Applications, no more tragic
IDE terminal blues; the Rust port finds its path! 🚀

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(platform-macos): TCC auto-relaunch for cua-driver-rs mcp (#1525)' directly and clearly describes the main change: adding TCC auto-relaunch functionality for the macOS MCP command in the Rust cua-driver port.
Linked Issues check ✅ Passed The PR implementation fully addresses all coding objectives from issue #1525: TCC detection heuristics (bundle.rs), daemon auto-relaunch via LaunchServices (cli.rs, main.rs), MCP proxy server forwarding to daemon socket (proxy.rs), escape hatches (--no-daemon-relaunch flag, env vars), daemon list metadata extension (serve.rs), .app bundle packaging (Info.plist, install.sh), and comprehensive documentation (PARITY.md).
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #1525: TCC auto-relaunch implementation for the Rust cua-driver mcp path. The workflow, installer, bundling, proxy logic, CLI flags, and documentation all serve the stated objective of achieving macOS TCC parity with the Swift implementation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cua-driver-rs-tcc-auto-relaunch-1525

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

🧹 Nitpick comments (1)
libs/cua-driver-rs/PARITY.md (1)

1116-1125: ⚡ Quick win

Consider adding expected output examples to the smoke test.

The smoke test procedure is actionable but would be clearer with concrete expected outputs. For example:

  • Step 3: Show a sample tools/list response snippet or confirm specific tools appear
  • Step 4: Include the actual error message text that "daemon not reachable" produces

This would make the smoke test easier to execute correctly and help verify the proxy path is working as intended.

📝 Suggested enhancement
 ### 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.
+   shape to the in-process path. The `tools/list` response should
+   include all registered tools (e.g., `move_cursor`, `click`,
+   `get_screen_size`, etc.) with full `inputSchema` definitions.
 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`).
+   non-zero exit and an error message on stderr indicating the
+   daemon socket is not reachable (the fail-fast contract that
+   matches Swift `makeProxy`). Example error: "Failed to connect
+   to daemon at /tmp/test.sock".
🤖 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 `@libs/cua-driver-rs/PARITY.md` around lines 1116 - 1125, Add concrete expected
output examples to the Manual smoke test: show a sample tools/list JSON snippet
(or at least expected tool names) for step 3 and a sample envelope shape for the
tools/call get_screen_size handshake, and for step 4 include the exact stderr
text returned when the daemon is unreachable (the "daemon not reachable" message
produced when running `CUA_DRIVER_RS_MCP_FORCE_PROXY=1 cua-driver mcp --socket
/tmp/test.sock` without a running `cua-driver serve`), referencing the existing
commands `cua-driver serve`, `CUA_DRIVER_RS_MCP_FORCE_PROXY`, `cua-driver mcp`,
`tools/list`, and `tools/call get_screen_size` so readers can match their
outputs against the expected snippets.
🤖 Prompt for all review comments with 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.

Inline comments:
In @.github/workflows/cd-rust-cua-driver.yml:
- Around line 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.

In `@libs/cua-driver-rs/crates/cua-driver/src/cli.rs`:
- Around line 286-298: In launch_daemon_and_wait, the relaunched daemon is
invoked with args ["serve"] only, so when a custom socket is requested the new
process still listens on the default socket; modify the Command built in
launch_daemon_and_wait (and the equivalent invocation used at the other block
around the later spawn) to include the socket override flag and value (the same
"--socket" and socket_path variable) in the .args list so the relaunched
CuaDriverRs receives the custom socket path and readiness polling matches where
the daemon actually listens.

In `@libs/cua-driver-rs/crates/cua-driver/src/proxy.rs`:
- Around line 249-256: The current branch turning daemon failures into a
JSON-RPC internal error should instead wrap tool failures in a successful
JSON-RPC response; in the !resp.ok branch (using resp, resp.error,
resp.exit_code and id) replace the Response::error(...) call with a
Response::ok(...) that returns a payload indicating a tool-level failure (e.g.,
include isError: true, the error message from resp.error.unwrap_or(...), and the
original exit code or None). Keep special-case mapping for exit_code == Some(64)
only in the payload metadata if needed, but do not emit a JSON-RPC error code;
return a normal Response::ok to preserve transport-level success while signaling
the tool failure in the payload.

In `@libs/cua-driver-rs/scripts/install.sh`:
- Around line 178-203: The Darwin install branch currently allows falling
through to the non-macOS install when the app bundle is missing; change the
logic so on macOS (OS == "Darwin") we require SRC_APP to be set and a directory
and fail fast with a clear error if it's absent or not a bundle (do this where
the if [[ "$OS" == "Darwin" && -n "$SRC_APP" && -d "$SRC_APP" ]] check is,
emitting an err message referencing SRC_APP/APP_DEST and exit 1), instead of
letting the script continue to the else path that installs a bare binary
(BIN_LINK), ensuring we do not silently bypass the TCC relaunch path.

---

Nitpick comments:
In `@libs/cua-driver-rs/PARITY.md`:
- Around line 1116-1125: Add concrete expected output examples to the Manual
smoke test: show a sample tools/list JSON snippet (or at least expected tool
names) for step 3 and a sample envelope shape for the tools/call get_screen_size
handshake, and for step 4 include the exact stderr text returned when the daemon
is unreachable (the "daemon not reachable" message produced when running
`CUA_DRIVER_RS_MCP_FORCE_PROXY=1 cua-driver mcp --socket /tmp/test.sock` without
a running `cua-driver serve`), referencing the existing commands `cua-driver
serve`, `CUA_DRIVER_RS_MCP_FORCE_PROXY`, `cua-driver mcp`, `tools/list`, and
`tools/call get_screen_size` so readers can match their outputs against the
expected snippets.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dc6b8fd4-4e7a-48fb-acd2-5e99f60afbfd

📥 Commits

Reviewing files that changed from the base of the PR and between 63be0af and abc1794.

⛔ Files ignored due to path filters (1)
  • libs/cua-driver-rs/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • .github/workflows/cd-rust-cua-driver.yml
  • libs/cua-driver-rs/PARITY.md
  • libs/cua-driver-rs/crates/cua-driver/Cargo.toml
  • libs/cua-driver-rs/crates/cua-driver/src/bundle.rs
  • libs/cua-driver-rs/crates/cua-driver/src/cli.rs
  • libs/cua-driver-rs/crates/cua-driver/src/main.rs
  • libs/cua-driver-rs/crates/cua-driver/src/proxy.rs
  • libs/cua-driver-rs/crates/cua-driver/src/serve.rs
  • libs/cua-driver-rs/scripts/CuaDriverRs.app/Contents/Info.plist
  • libs/cua-driver-rs/scripts/CuaDriverRs.app/Contents/MacOS/.gitkeep
  • libs/cua-driver-rs/scripts/install.sh

Comment on lines +179 to 205
- 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.
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
# 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
- name: Package

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.

Comment on lines +286 to +298
pub fn launch_daemon_and_wait(socket_path: &str, timeout_secs: u64) -> anyhow::Result<()> {
use std::process::{Command as Cmd, Stdio};
use std::time::{Duration, Instant};

let status = Cmd::new("/usr/bin/open")
// `-n` forces a new instance: CuaDriverRs.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 —
// LSUIElement=true in Info.plist already does this but the
// flag makes it explicit and matches Swift's invocation.
.args(["-n", "-g", "-a", "CuaDriverRs", "--args", "serve"])
.stdout(Stdio::null())

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 | 🟠 Major | ⚡ Quick win

Pass the socket override into the relaunched daemon command.

When mcp --socket <custom> hits auto-relaunch, the daemon is started on the default socket (serve only), but readiness is polled on the custom socket path. That makes proxy bootstrap fail consistently for custom socket users.

💡 Suggested fix
 #[cfg(target_os = "macos")]
 pub fn launch_daemon_and_wait(socket_path: &str, timeout_secs: u64) -> anyhow::Result<()> {
@@
     let status = Cmd::new("/usr/bin/open")
@@
-        .args(["-n", "-g", "-a", "CuaDriverRs", "--args", "serve"])
+        .args([
+            "-n",
+            "-g",
+            "-a",
+            "CuaDriverRs",
+            "--args",
+            "serve",
+            "--socket",
+            socket_path,
+        ])
         .stdout(Stdio::null())
         .stderr(Stdio::null())
         .status();

Also applies to: 340-361

🤖 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 `@libs/cua-driver-rs/crates/cua-driver/src/cli.rs` around lines 286 - 298, In
launch_daemon_and_wait, the relaunched daemon is invoked with args ["serve"]
only, so when a custom socket is requested the new process still listens on the
default socket; modify the Command built in launch_daemon_and_wait (and the
equivalent invocation used at the other block around the later spawn) to include
the socket override flag and value (the same "--socket" and socket_path
variable) in the .args list so the relaunched CuaDriverRs receives the custom
socket path and readiness polling matches where the daemon actually listens.

Comment on lines +249 to +256
if !resp.ok {
let msg = resp.error.unwrap_or_else(|| "daemon reported failure".into());
// exit_code 64 is EX_USAGE — bad params, surfaces as a
// JSON-RPC InvalidParams. Any other non-zero is treated as
// an internal error.
let code = if resp.exit_code == Some(64) { -32602 } else { -32603 };
return Response::error(id, code, msg);
}

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 | 🟠 Major | ⚡ Quick win

Keep tool failures in tools/call results instead of JSON-RPC internal errors.

This branch converts daemon call failures into -32603, which makes normal tool failures look like transport/protocol failures. For MCP compatibility, tool failures should return Response::ok with isError: true payload.

💡 Suggested fix
     if !resp.ok {
         let msg = resp.error.unwrap_or_else(|| "daemon reported failure".into());
-        // exit_code 64 is EX_USAGE — bad params, surfaces as a
-        // JSON-RPC InvalidParams. Any other non-zero is treated as
-        // an internal error.
-        let code = if resp.exit_code == Some(64) { -32602 } else { -32603 };
-        return Response::error(id, code, msg);
+        // Keep argument/usage failures as JSON-RPC InvalidParams.
+        if resp.exit_code == Some(64) {
+            return Response::error(id, -32602, msg);
+        }
+        // Tool-level failures should remain MCP CallTool results.
+        return Response::ok(id, serde_json::json!({
+            "content": [{ "type": "text", "text": msg }],
+            "isError": true
+        }));
     }
🤖 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 `@libs/cua-driver-rs/crates/cua-driver/src/proxy.rs` around lines 249 - 256,
The current branch turning daemon failures into a JSON-RPC internal error should
instead wrap tool failures in a successful JSON-RPC response; in the !resp.ok
branch (using resp, resp.error, resp.exit_code and id) replace the
Response::error(...) call with a Response::ok(...) that returns a payload
indicating a tool-level failure (e.g., include isError: true, the error message
from resp.error.unwrap_or(...), and the original exit code or None). Keep
special-case mapping for exit_code == Some(64) only in the payload metadata if
needed, but do not emit a JSON-RPC error code; return a normal Response::ok to
preserve transport-level success while signaling the tool failure in the
payload.

Comment on lines +178 to +203
if [[ "$OS" == "Darwin" && -n "$SRC_APP" && -d "$SRC_APP" ]]; then
if [[ ! -w "/Applications" ]]; then
err "/Applications is not writable. Re-run this installer in a shell where it is, or grant write access."
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
if [[ -e "$APP_DEST" ]]; then
log "removing existing $APP_DEST"
rm -rf "$APP_DEST"
fi
log "installing $APP_DEST"
# `ditto` preserves the bundle's metadata + nested symlinks the way
# Apple's installer would. `cp -R` works but doesn't preserve as
# much, and ditto is always present on macOS.
ditto "$SRC_APP" "$APP_DEST"
APP_BINARY="$APP_DEST/Contents/MacOS/$BINARY_NAME"
if [[ ! -x "$APP_BINARY" ]]; then
err "binary missing at $APP_BINARY (refusing to create broken symlink)"
exit 1
fi
ln -sf "$APP_BINARY" "$BIN_LINK"
log "symlinked $BIN_LINK -> $APP_BINARY"
else
install -m 0755 "$SRC" "$BIN_LINK"
log "installed $BIN_LINK (version $VERSION)"
fi

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

Avoid silent bare-binary fallback on macOS when the app bundle is missing.

On Darwin, if SRC_APP is absent, the script currently succeeds via the non-macOS branch. That can silently bypass the TCC relaunch path. Prefer failing fast with a clear error in the Darwin path.

Suggested patch
-if [[ "$OS" == "Darwin" && -n "$SRC_APP" && -d "$SRC_APP" ]]; then
+if [[ "$OS" == "Darwin" ]]; then
+    if [[ -z "$SRC_APP" || ! -d "$SRC_APP" ]]; then
+        err "expected $APP_NAME in macOS tarball but didn't find it"
+        err "refusing bare-binary fallback on macOS because it disables TCC-correct relaunch behavior"
+        exit 1
+    fi
     if [[ ! -w "/Applications" ]]; then
         err "/Applications is not writable. Re-run this installer in a shell where it is, or grant write access."
         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
@@
-else
+else
     install -m 0755 "$SRC" "$BIN_LINK"
     log "installed $BIN_LINK (version $VERSION)"
 fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if [[ "$OS" == "Darwin" && -n "$SRC_APP" && -d "$SRC_APP" ]]; then
if [[ ! -w "/Applications" ]]; then
err "/Applications is not writable. Re-run this installer in a shell where it is, or grant write access."
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
if [[ -e "$APP_DEST" ]]; then
log "removing existing $APP_DEST"
rm -rf "$APP_DEST"
fi
log "installing $APP_DEST"
# `ditto` preserves the bundle's metadata + nested symlinks the way
# Apple's installer would. `cp -R` works but doesn't preserve as
# much, and ditto is always present on macOS.
ditto "$SRC_APP" "$APP_DEST"
APP_BINARY="$APP_DEST/Contents/MacOS/$BINARY_NAME"
if [[ ! -x "$APP_BINARY" ]]; then
err "binary missing at $APP_BINARY (refusing to create broken symlink)"
exit 1
fi
ln -sf "$APP_BINARY" "$BIN_LINK"
log "symlinked $BIN_LINK -> $APP_BINARY"
else
install -m 0755 "$SRC" "$BIN_LINK"
log "installed $BIN_LINK (version $VERSION)"
fi
if [[ "$OS" == "Darwin" ]]; then
if [[ -z "$SRC_APP" || ! -d "$SRC_APP" ]]; then
err "expected $APP_NAME in macOS tarball but didn't find it"
err "refusing bare-binary fallback on macOS because it disables TCC-correct relaunch behavior"
exit 1
fi
if [[ ! -w "/Applications" ]]; then
err "/Applications is not writable. Re-run this installer in a shell where it is, or grant write access."
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
if [[ -e "$APP_DEST" ]]; then
log "removing existing $APP_DEST"
rm -rf "$APP_DEST"
fi
log "installing $APP_DEST"
# `ditto` preserves the bundle's metadata + nested symlinks the way
# Apple's installer would. `cp -R` works but doesn't preserve as
# much, and ditto is always present on macOS.
ditto "$SRC_APP" "$APP_DEST"
APP_BINARY="$APP_DEST/Contents/MacOS/$BINARY_NAME"
if [[ ! -x "$APP_BINARY" ]]; then
err "binary missing at $APP_BINARY (refusing to create broken symlink)"
exit 1
fi
ln -sf "$APP_BINARY" "$BIN_LINK"
log "symlinked $BIN_LINK -> $APP_BINARY"
else
install -m 0755 "$SRC" "$BIN_LINK"
log "installed $BIN_LINK (version $VERSION)"
fi
🤖 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 `@libs/cua-driver-rs/scripts/install.sh` around lines 178 - 203, The Darwin
install branch currently allows falling through to the non-macOS install when
the app bundle is missing; change the logic so on macOS (OS == "Darwin") we
require SRC_APP to be set and a directory and fail fast with a clear error if
it's absent or not a bundle (do this where the if [[ "$OS" == "Darwin" && -n
"$SRC_APP" && -d "$SRC_APP" ]] check is, emitting an err message referencing
SRC_APP/APP_DEST and exit 1), instead of letting the script continue to the else
path that installs a bare binary (BIN_LINK), ensuring we do not silently bypass
the TCC relaunch path.

f-trycua and others added 5 commits May 16, 2026 23:42
When the caller passed `cua-driver mcp --socket /custom/path`, the
auto-relaunched daemon was still listening on `default_socket_path()`,
so the proxy would block waiting for a daemon that never came up on
the user-supplied path.

Append `--socket <path>` to the `open -n -g -a CuaDriverRs --args
serve` argv when the socket differs from the default. Keep the common
case (default socket) byte-for-byte identical to Swift's invocation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…bit #2)

When the daemon returned `!resp.ok`, the proxy was building a
`Response::error(...)` (JSON-RPC envelope error). MCP separates two
failure modes:
  - JSON-RPC errors → transport / protocol failures (unreachable
    socket, decode error, unknown method).
  - Tool-level errors → tool ran but returned `isError: true` with
    the error text in `content[]`. JSON-RPC envelope stays success.

A non-`ok` daemon response means the tool reached the daemon and the
daemon reported the tool returned an error. That's tool-level, so
`Response::ok(...)` with `isError: true` is the right shape — same
envelope the in-process `mcp_server::server` path returns.

Transport failures (UDS gone, decode error, join panic) still surface
as JSON-RPC `-32603` errors, since the client really does need to
distinguish "tool said no" from "I couldn't reach the tool."

Adds two unit tests pinning the serialized shape of the tool-error
envelope so a regression to `Response::error` fails fast in CI on
every platform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#3)

When SRC_APP is unset or not a directory on macOS, the installer was
falling through to the bare-binary `install -m 0755` branch — leaving
a working CLI but no /Applications/CuaDriverRs.app, which silently
breaks the TCC auto-relaunch path in `cua-driver-rs mcp`.

Now exits 1 with a diagnostic before touching BIN_DIR. Linux / WSL
path is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…abbit #4)

The in-tree Info.plist's CFBundleShortVersionString / CFBundleVersion
drifted from the release tag on every cut because the workflow just
copied the skeleton verbatim.

Use `plutil -replace` to stamp ${{ steps.version.outputs.version }}
into both keys after copying the skeleton. Echoes the resulting
values back for build-log auditing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Rabbit #5)

Adds:
- representative tools/list response envelope so the reader knows
  what "identical envelope shape to the in-process path" looks like
  in practice
- tools/call get_screen_size request + response showing the
  structuredContent + text mirror the proxy passes through
- exact stderr text both `main.rs` (unreachable) and `cli.rs`
  (CUA_DRIVER_RS_MCP_FORCE_PROXY) emit when no daemon is up, plus
  the exit status

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@f-trycua

Copy link
Copy Markdown
Collaborator Author

Pushed 5 fixup commits addressing the CodeRabbit review:

# Commit Finding
1 e520ccab fix(cli): forward --socket to the relaunched daemon so a custom cua-driver mcp --socket /path actually reaches a daemon listening on that path.
2 b82d8c4a fix(proxy): wrap daemon tool failures as MCP CallTool.Result with isError: true instead of a JSON-RPC Response::error. Transport failures still surface as -32603 JSON-RPC errors. Adds 2 unit tests pinning the serialized envelope.
3 980b89e5 fix(install): fail fast on Darwin if the .app bundle is missing from the tarball (no more silent fall-through to the bare-binary install that would break TCC auto-relaunch).
4 5a88ecad fix(ci): stamp the release version into CuaDriverRs.app/Contents/Info.plist (CFBundleShortVersionString + CFBundleVersion) via plutil -replace so it tracks the tag instead of whatever was last checked in.
5 41892dce docs(parity): smoke-test recipe now shows concrete expected tools/list + tools/call get_screen_size envelopes and the exact stderr text the daemon-unreachable path emits, with exit status.

Build + tests on macOS arm64: cargo build --release green, cargo test -p cua-driver green except the pre-existing environmental test_type_text_chars_tool (unrelated). The 2 new proxy::tests unit tests pass.

@coderabbitai please re-review.

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@f-trycua, thank you for the detailed fixup summary! Let me pull the current state of the PR for a thorough re-review.


🏁 Script executed:

#!/bin/bash
# Get the PR details and list of files changed
gh pr view 1530 --repo trycua/cua --json title,state,headRefName,baseRefName,files 2>&1 | head -100

echo "=== PR DIFF (summarized) ==="
gh pr diff 1530 --repo trycua/cua 2>&1 | head -2000

Length of output: 63302


@f-trycua, all five fixups look solid. Here's the post-fixup pass:


✅ Addressed cleanly

# Finding Status
1 --socket forwarded to relaunched daemon via pass_socket guard in launch_daemon_and_wait ✓ Correct — only appended when path deviates from the default, matching Swift's invocation
2 Daemon tool failures now wrapped as CallTool.Result { isError: true } instead of -32603; transport failures still surface as JSON-RPC errors ✓ Correct separation; two unit tests pin the envelopes
3 Darwin install now exits 1 immediately when SRC_APP is missing/empty before falling through ✓ Fail-fast path verified; the second if [[ "$OS" == "Darwin" && -n … ]] block is reachable only after the guard, so no silent fall-through
4 plutil -replace CFBundleShortVersionString + CFBundleVersion stamps the release tag into Info.plist at CI time ✓ In-tree placeholder 0.1.3 / 1 is overwritten on every cut
5 Smoke-test recipe in PARITY.md now has concrete JSON envelopes and exact stderr text ✓ Matches code in proxy.rs and main.rs

Minor nit (non-blocking)

fetch_tools_list_from_daemon is a blocking call inside the async run_proxy

// proxy.rs – line ~64
let cached_tools_list = fetch_tools_list_from_daemon(&socket_path)?;

fetch_tools_list_from_daemon calls send_request (synchronous UDS I/O) directly in an async fn, which can stall the tokio reactor thread. It's a startup-only, one-shot call on a local socket so the wall-clock impact is negligible, and forward_tool_call already uses spawn_blocking for the hot path. Still, for consistency you could wrap it:

let sp = socket_path.clone();
let cached_tools_list = tokio::task::spawn_blocking(move || fetch_tools_list_from_daemon(&sp)).await??;

This is a pure quality-of-life nit — the current code works fine in practice.


One CFBundleVersion note (informational, not a bug): plutil -replace CFBundleVersion -string "$VERSION" will set it to a semver string like "0.1.4". Apple's convention for CFBundleVersion is a monotonically increasing integer (or dot-separated integers). macOS won't reject the bundle for a headless LSUIElement daemon, but App Store review and some tooling (agvtool, stapler) would complain. Since you're not distributing via the App Store and there's no notarization in this PR, it's fine for the beta scope — just worth tracking for when notarization lands.


Overall this is in great shape. The TCC auto-relaunch flow, proxy error handling, bundle packaging, and installer hardening all look correct. Ready to merge once you're happy with the nit above.

[approve]

@f-trycua
f-trycua merged commit 7bf0908 into main May 16, 2026
5 checks passed
@f-trycua
f-trycua deleted the feat/cua-driver-rs-tcc-auto-relaunch-1525 branch May 16, 2026 22:18
f-trycua added a commit that referenced this pull request May 16, 2026
…#1532)

* feat(telemetry): TelemetryClient with PostHog integration + opt-out

Port the Swift `TelemetryClient` from `libs/cua-driver/.../TelemetryClient.swift`
to a new `crates/cua-driver/src/telemetry.rs` module. Same PostHog
ingest endpoint, same event names, same payload shape — so Rust and
Swift telemetry aggregate cleanly on the same dashboards.

Public API:
- `telemetry::capture(event, properties)` — fire-and-forget HTTP POST,
  no-op when opt-out is active.
- `telemetry::capture_install()` — one-shot install ping guarded by a
  marker file, **only** path that bypasses the opt-out check (for
  adoption counting parity with Swift).
- `telemetry::is_enabled()` — single env-var check.
- `telemetry::event::*` constants — canonical event names mirrored 1:1
  from Swift's `TelemetryEvent` enum.

Differences from Swift (deliberate, documented in module docs):
- Install ID at `~/.cua-driver-rs/.telemetry_id` (Swift uses
  `~/.cua-driver/`). Independent so opting out of one port doesn't
  silence the other.
- Opt-out env var is `CUA_DRIVER_RS_TELEMETRY_ENABLED=false` (Swift
  uses `CUA_DRIVER_TELEMETRY_ENABLED`). Same independence rationale.
- `$lib = "cua-driver-rs"` so dashboards split Rust vs Swift adoption.
- No persisted config flag (YAGNI — env var only).

HTTP client: `ureq` v3 with rustls (default features). One transitive
dep tree, no system OpenSSL needed. POST runs on `spawn_blocking` when
a tokio runtime is live, otherwise a short-lived OS thread — covers
both the async MCP server and sync CLI subcommands. 3s timeout, all
errors logged via `tracing::debug!` only.

Tests cover: env-var bool parsing, opt-out default semantics, CI
detection, payload shape (incl. privacy assertion that no usernames /
paths / argv leak into the envelope), default-envelope precedence on
key collision, install-ID idempotent persistence, ISO-8601 format.

No call-site wiring yet — commit 2 adds telemetry emission at the
mcp/serve/call CLI entry points.

Refs #1528.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): emit telemetry events from mcp/serve/call subcommands

Wire `telemetry::capture(...)` at the CLI dispatch boundary so every
invocation emits its entry-event (e.g. `cua_driver_mcp`,
`cua_driver_api_click`) once before any work starts. Mirrors Swift's
`TelemetryClient.shared.record(event: entryEvent)` at the top of
`CuaDriverCommand.main()`.

- New `cli::telemetry_entry_event(&Command) -> Option<String>` maps each
  parsed subcommand to its canonical event name. `call <tool>` reports
  as `cua_driver_api_<tool>` so per-tool adoption is visible without
  ever recording the args. Implicit-call form (`cua-driver <tool>`)
  reuses the same path via `Command::Call`.
- New hidden `cua-driver telemetry install-event` subcommand
  (`Command::TelemetryInstallEvent`) — installer-only entry point that
  fires the one-shot `cua_driver_install` ping via
  `telemetry::capture_install()`. Bypasses opt-out (only path that
  does so); guarded by the `.installation_recorded` marker file so
  repeat invocations are no-ops.

Both macOS and non-macOS `main()` paths now call `emit_entry_telemetry`
right after `parse_command()` and before dispatch — fire-and-forget,
respects the env-var opt-out, never blocks the actual work.

Verified locally on macOS:
- `cua-driver list-tools` with `CUA_DRIVER_RS_TELEMETRY_ENABLED=false`
  silently skips the POST.
- `cua-driver list-tools` with debug enabled prints
  `[telemetry] sending event: cua_driver_list_tools`.
- `cua-driver telemetry install-event` returns PostHog HTTP 200 on
  first call; second call is silent (marker file present).
- `cua-driver --version` still exits cleanly without firing anything
  (handled before parse_command).

Refs #1528.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(install.sh): emit cua_driver_install event post-install

After dropping the binary into place, fire `cua-driver telemetry
install-event` once in the background. Bypasses the
`CUA_DRIVER_RS_TELEMETRY_ENABLED` opt-out by design so we count
adoption even from users who immediately disable telemetry (every
subsequent event from the binary respects the opt-out normally).

The binary's own `.installation_recorded` marker guards against
re-sends, so re-running `install.sh` (e.g. after `cua-driver update`)
is a no-op for telemetry.

Run in the background with `&` + `disown` so a slow or failed POST
can never delay the install — keeps the user-facing "installed" log
line snappy.

Refs #1528.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(PARITY.md): document telemetry parity + opt-out

New "Telemetry (PostHog)" section. Covers:

- Endpoint + event names (identical to Swift so dashboards aggregate)
- Payload shape table (every key + its source)
- Privacy posture: explicit list of what we DO NOT send, backed by
  a unit-test assertion in build_payload_contains_required_keys.
- Opt-out env var (CUA_DRIVER_RS_TELEMETRY_ENABLED) and the single
  exception (install ping bypasses for adoption counting).
- Independence-from-Swift table: separate marker dir + UUID + env var
  so opting out of one port doesn't silence the other.
- HTTP client choice (ureq v3 + rustls) and timeout/error-handling.
- Intentional divergences (no persisted config flag, no GUI launch
  emission, env-var-only CI detection).

Refs #1528.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): sanitize tool name before building API telemetry event name

Per-tool `call <tool>` events were concatenating the raw user-provided
tool string onto `cua_driver_api_`, so path-like or non-ASCII tool names
would flow verbatim into PostHog event names (privacy + dashboard pollution).

Add `sanitize_tool_name` that lowercases, keeps only `[a-z0-9_]`, caps at
64 chars, and falls back to `"unknown"` when the input strips to empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(telemetry): synchronous install POST; only write marker on HTTP success

Previously `capture_install()` fired the install event via the async
`spawn_capture` path and immediately wrote the `.installation_recorded`
marker — so a failed POST (network, PostHog outage, non-2xx) silently
dropped the only adoption signal because the marker still prevented
retries on the next launch.

Switch the install path to a synchronous POST via a new internal
`capture_install_with_poster` seam (the seam exists for testability —
public callers use `capture_install`). The marker is only written when
the POST returns HTTP 2xx; any other outcome (Err, 4xx, 5xx) leaves the
marker absent so the next `cua-driver` launch retries.

Other telemetry paths still use the async `spawn_capture` fire-and-forget
flow — only the install one-shot blocks. Bypass-opt-out semantics are
preserved (install path still skips `is_enabled()`).

Drops the now-unused 2s sleep in `main.rs` (the comment claimed it was
waiting for a spawned thread; the POST is sync now).

Adds two unit tests verifying the marker is not written on Err or on
non-2xx responses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): update Command::Mcp pattern after #1530 merge

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
f-trycua added a commit that referenced this pull request May 31, 2026
…nstead of SIGABRT-ing (#1724) (#1781)

When `cua-driver mcp` falls through to the in-process server (dev binary
not inside CuaDriver.app, `--no-daemon-relaunch`, or a launchd parent),
the cursor overlay brings up AppKit on the main thread. `+[NSApplication
sharedApplication]` registers the process with the Window Server, and that
registration **aborts the whole process** (SIGABRT in `_RegisterApplication`)
when the process has no graphic-session access — e.g. `mcp` spawned as a
stdio child from an SSH session, a LaunchDaemon, or a headless CI runner.
The crash happens *before* the existing `mainScreen.is_null()` headless
guard, so that guard never gets a chance to run. Reported in #1724.

Probe `SessionGetInfo`'s `sessionHasGraphicAccess` bit — which answers
"can this session talk to the Window Server?" without touching AppKit —
and skip the overlay when it's unset, parking the main thread exactly as
the overlay-disabled path already does. The MCP server keeps serving on
its background thread, so `mcp` degrades to headless instead of dying.
This is the macOS analogue of the Windows Session-0 short-circuit guard.

Note: the *primary* repro from #1724 (a bundle-resolved `mcp` spawned by
a client) is already handled on current releases by the daemon-proxy
re-exec (#1525/#1530), which routes that case away from AppKit entirely.
This change hardens the remaining in-process path.

- `platform-macos/src/session.rs` — `has_graphic_access()` via SessionGetInfo
- `platform-macos/src/cursor/overlay.rs` — gate AppKit init on it
- `platform-macos/src/lib.rs` — expose the module

Verified: builds + links the Security framework; the probe returns true in
a GUI session (attrs 0x6030, graphic bit set) and the smoke test passes.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cua-driver-rs: missing TCC auto-relaunch (#1479 equivalent) — mcp from IDE terminals fails

1 participant