Skip to content

fix(cua-driver-rs): warn + force prompt:false for check_permissions in CLI mode - #1575

Closed
hippoley wants to merge 2 commits into
trycua:mainfrom
hippoley:fix/check-permissions-cli-caveat
Closed

fix(cua-driver-rs): warn + force prompt:false for check_permissions in CLI mode#1575
hippoley wants to merge 2 commits into
trycua:mainfrom
hippoley:fix/check-permissions-cli-caveat

Conversation

@hippoley

@hippoley hippoley commented May 19, 2026

Copy link
Copy Markdown
Contributor

Problem

cua-driver check_permissions reports stale/wrong results after tccutil reset Accessibility com.trycua.driver when called from a shell or IDE terminal without a running daemon.

Root cause: macOS attributes the calling CLI process to the responsible parent application (Terminal.app, VS Code, Cursor, etc.) rather than to CuaDriver.app. AXIsProcessTrusted() therefore reflects the parent's Accessibility grant, not CuaDriver.app's.

The Swift binary already handles this in CallCommand.swift lines 148-171. The Rust CLI had no equivalent guard.

Fix

Ports the Swift fix to the Rust CLI:

  • cli.rs: when no daemon is listening and tool == check_permissions, emit a stderr warning and force prompt:false. Prevents a TCC dialog being attributed to the wrong bundle identity.
  • check_permissions.rs: add the caveat to the tool description so MCP clients and cua-driver describe surface the limitation.
  • status.rs: document the AXIsProcessTrusted() attribution caveat in the accessibility_granted() doc comment.

No behaviour change when the daemon is running (requests are forwarded through the Unix socket and run inside CuaDriver.app where attribution is correct).

Fixes #1561

Summary by CodeRabbit

  • Documentation
    • Added warning messages when running permission checks without the daemon, clarifying that results may be inaccurate
    • Updated tool descriptions to explain daemon requirements for correct macOS TCC permission attribution

Review Change Stack

…n CLI mode

Fixes trycua#1561.

When cua-driver check_permissions is called from a shell or IDE terminal
without a running daemon, macOS attributes the calling process to the
parent application (Terminal.app, VS Code, Cursor, etc.) rather than to
CuaDriver.app. AXIsProcessTrusted() therefore reflects the parent's
Accessibility grant, not CuaDriver.app's — so results can be stale or
misleading after tccutil reset Accessibility com.trycua.driver.

This is the same bug that exists in the Swift binary; Swift's
CallCommand.swift already handles it (lines 148-171). This commit ports
that fix to the Rust CLI.

Changes:
- cli.rs: when no daemon is listening and tool == check_permissions,
  emit a stderr warning and force prompt:false (same logic as Swift).
  Prevents a TCC dialog being attributed to the wrong bundle identity.
- check_permissions.rs: add the caveat to the tool description so MCP
  clients and cua-driver describe output surface the limitation.
- status.rs: document the AXIsProcessTrusted() attribution caveat in
  the accessibility_granted() doc comment.

The fix is purely additive — no behaviour change when the daemon is
running (requests are forwarded through the Unix socket and run inside
CuaDriver.app where attribution is correct).
@vercel

vercel Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

@nishantpurohit04 is attempting to deploy a commit to the Cua Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 19, 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: 8221f5bc-53bd-43aa-aaac-2d9ec6eea510

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

The PR addresses a macOS TCC attribution bug where check_permissions reports stale accessibility status after tccutil reset. When no daemon is running, AXIsProcessTrusted() may report the parent process's (terminal/IDE) trust state rather than the bundle's. The fix documents the caveat and forces prompt: false in the CLI path to avoid misattributed prompts.

Changes

TCC Attribution Caveat for check_permissions

Layer / File(s) Summary
Root cause documentation
libs/cua-driver-rs/crates/platform-macos/src/permissions/status.rs
Doc comment on accessibility_granted() explains that CLI-spawned processes may be attributed to parent terminals, causing stale results, and notes that daemon-mediated checks run under correct attribution.
Tool description caveat
libs/cua-driver-rs/crates/platform-macos/src/tools/check_permissions.rs
ToolDef.description expanded to warn users that without a running daemon, TCC attribution may route incorrectly to parent processes, making results unreliable after tccutil reset; instructs users to start the daemon.
In-process CLI mitigation
libs/cua-driver-rs/crates/cua-driver/src/cli.rs
run_call special-cases check_permissions: prints a stderr warning about TCC attribution and forces prompt: false in JSON args to prevent permission prompts from a misattributed process.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

  • trycua/cua#1562: Removes the screen_recording_granted() window-enum fallback in permissions/status.rs that was masking stale results.
  • trycua/cua#1368: Updates install.sh to start the daemon before running check_permissions for authoritative permission checks.
  • trycua/cua#1396: Adjusts check_permissions default behavior regarding the prompt argument handling.

Poem

🐰 A little rabbit hops with care,
To warn of TCC's tricky snare—
When parent processes rule the day,
Daemon brings the truth, hooray!
Prompt disabled, wisdom shared,
Accurate permissions, fairly faired!

🚥 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 accurately describes the main change: warning users and forcing prompt:false for check_permissions in CLI mode to address the TCC attribution issue.
Linked Issues check ✅ Passed The PR directly addresses #1561 by implementing the short-term remediation: warning when check_permissions runs without a daemon and forcing prompt:false to prevent misleading TCC dialogs attributed to parent processes.
Out of Scope Changes check ✅ Passed All changes are scoped to addressing the check_permissions CLI caveat through documentation and forcing prompt:false; no unrelated modifications are present.
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

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: 1

🤖 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 `@libs/cua-driver-rs/crates/cua-driver/src/cli.rs`:
- Around line 674-679: The current code only sets prompt:false when json_args is
an Object, leaving non-object json_args (e.g., true, [], "str") unchanged and
causing check_permissions to treat them as prompt:true; change the coercion so
that regardless of the shape of json_args the returned value is an Object with
prompt:false: capture the original json_args, create a serde_json::Map, insert
("prompt", serde_json::Value::Bool(false)), then if the original was an Object
merge its entries into the map else insert the original under a new key (e.g.,
"value") before returning Some(serde_json::Value::Object(map)); update the code
around the variables json_args and coerced to implement this behavior.
🪄 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: 81fa220e-dbfe-4a36-986d-f773fe3fb071

📥 Commits

Reviewing files that changed from the base of the PR and between 0f834c6 and 30e7756.

📒 Files selected for processing (3)
  • libs/cua-driver-rs/crates/cua-driver/src/cli.rs
  • libs/cua-driver-rs/crates/platform-macos/src/permissions/status.rs
  • libs/cua-driver-rs/crates/platform-macos/src/tools/check_permissions.rs

Comment on lines +674 to +679
let mut coerced = json_args
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
if let serde_json::Value::Object(ref mut map) = coerced {
map.insert("prompt".into(), serde_json::Value::Bool(false));
}
Some(coerced)

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the current coercion only mutates object-shaped args and that tool default is prompt=true.
rg -n -C3 'if let serde_json::Value::Object\(ref mut map\)' libs/cua-driver-rs/crates/cua-driver/src/cli.rs
rg -n -C3 'unwrap_or\(true\)' libs/cua-driver-rs/crates/platform-macos/src/tools/check_permissions.rs

Repository: trycua/cua

Length of output: 1526


🏁 Script executed:

#!/bin/bash
# Check where json_args comes from and if non-object values are realistic
rg -n -B15 'let mut coerced = json_args' libs/cua-driver-rs/crates/cua-driver/src/cli.rs | head -40

Repository: trycua/cua

Length of output: 973


🏁 Script executed:

#!/bin/bash
# See initial json_args assignment and full context of this block
sed -n '630,690p' libs/cua-driver-rs/crates/cua-driver/src/cli.rs

Repository: trycua/cua

Length of output: 2648


🏁 Script executed:

#!/bin/bash
# Find function signature to see json_args parameter type/source
rg -n -B20 'let json_args = if tool == "check_permissions"' libs/cua-driver-rs/crates/cua-driver/src/cli.rs | grep -E '(fn|json_args)'

Repository: trycua/cua

Length of output: 111


🏁 Script executed:

#!/bin/bash
# Find the function signature containing this code
sed -n '600,680p' libs/cua-driver-rs/crates/cua-driver/src/cli.rs | head -40

Repository: trycua/cua

Length of output: 2237


🏁 Script executed:

#!/bin/bash
# Find the function containing this code block
rg -n 'fn.*json_args|fn main|pub fn' libs/cua-driver-rs/crates/cua-driver/src/cli.rs | head -10

Repository: trycua/cua

Length of output: 691


🏁 Script executed:

#!/bin/bash
# Get the run_call function signature and initial lines
sed -n '576,620p' libs/cua-driver-rs/crates/cua-driver/src/cli.rs

Repository: trycua/cua

Length of output: 2476


🏁 Script executed:

#!/bin/bash
# Find where run_call is invoked to see what json_args values are passed
rg -n 'run_call\(' libs/cua-driver-rs/crates/cua-driver/src/cli.rs

Repository: trycua/cua

Length of output: 75


🏁 Script executed:

#!/bin/bash
# Search main and command handlers for where run_call is invoked
rg -n 'run_call' libs/cua-driver-rs/crates/cua-driver/src/cli.rs

Repository: trycua/cua

Length of output: 75


🏁 Script executed:

#!/bin/bash
# Search entire crate for invocations of run_call
rg -n 'run_call' libs/cua-driver-rs/crates/cua-driver/src/

Repository: trycua/cua

Length of output: 510


🏁 Script executed:

#!/bin/bash
# Check main.rs to see where json_args comes from
sed -n '100,130p' libs/cua-driver-rs/crates/cua-driver/src/main.rs
sed -n '340,360p' libs/cua-driver-rs/crates/cua-driver/src/main.rs

Repository: trycua/cua

Length of output: 2810


🏁 Script executed:

#!/bin/bash
# Find the Command enum definition to see json_args type
rg -n 'enum Command|struct Call' libs/cua-driver-rs/crates/cua-driver/src/cli.rs | head -20

Repository: trycua/cua

Length of output: 76


🏁 Script executed:

#!/bin/bash
# Show the Command enum definition
sed -n '19,100p' libs/cua-driver-rs/crates/cua-driver/src/cli.rs

Repository: trycua/cua

Length of output: 4037


🏁 Script executed:

#!/bin/bash
# Find where json_args is parsed from command line arguments
rg -n 'json_args' libs/cua-driver-rs/crates/cua-driver/src/cli.rs | head -20

Repository: trycua/cua

Length of output: 695


🏁 Script executed:

#!/bin/bash
# See how json_args is parsed from command line input
sed -n '200,220p' libs/cua-driver-rs/crates/cua-driver/src/cli.rs
sed -n '245,265p' libs/cua-driver-rs/crates/cua-driver/src/cli.rs

Repository: trycua/cua

Length of output: 1773


🏁 Script executed:

#!/bin/bash
# Verify the suggested fix logic is sound
cat <<'EOF' > /tmp/test_fix.rs
// Current buggy code:
let json_args_buggy = Some(serde_json::json!(true)); // or [], "str", etc.
let mut coerced = json_args_buggy
    .unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
if let serde_json::Value::Object(ref mut map) = coerced {
    map.insert("prompt".into(), serde_json::Value::Bool(false));
}
// Result: coerced is still true, no "prompt" key inserted

// Suggested fixed code:
let json_args_fixed = Some(serde_json::json!(true)); // or [], "str", etc.
let mut map = match json_args_fixed {
    Some(serde_json::Value::Object(map)) => map,
    _ => serde_json::Map::new(),
};
map.insert("prompt".into(), serde_json::Value::Bool(false));
let result = Some(serde_json::Value::Object(map));
// Result: always a new Object with "prompt": false
EOF
cat /tmp/test_fix.rs

Repository: trycua/cua

Length of output: 833


Force prompt: false for all arg shapes, not only object args.

When json_args is a non-object JSON value (e.g., true, [], or "string"), this branch leaves it unchanged, and check_permissions falls back to prompt=true. That re-enables the misattributed TCC prompt in exactly the path this PR is trying to prevent. Users can pass non-object JSON via the command line, so this is not an edge case.

Suggested fix
-        let mut coerced = json_args
-            .unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
-        if let serde_json::Value::Object(ref mut map) = coerced {
-            map.insert("prompt".into(), serde_json::Value::Bool(false));
-        }
-        Some(coerced)
+        let mut map = match json_args {
+            Some(serde_json::Value::Object(map)) => map,
+            _ => serde_json::Map::new(),
+        };
+        map.insert("prompt".into(), serde_json::Value::Bool(false));
+        Some(serde_json::Value::Object(map))
📝 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
let mut coerced = json_args
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
if let serde_json::Value::Object(ref mut map) = coerced {
map.insert("prompt".into(), serde_json::Value::Bool(false));
}
Some(coerced)
let mut map = match json_args {
Some(serde_json::Value::Object(map)) => map,
_ => serde_json::Map::new(),
};
map.insert("prompt".into(), serde_json::Value::Bool(false));
Some(serde_json::Value::Object(map))
🤖 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 674 - 679, The
current code only sets prompt:false when json_args is an Object, leaving
non-object json_args (e.g., true, [], "str") unchanged and causing
check_permissions to treat them as prompt:true; change the coercion so that
regardless of the shape of json_args the returned value is an Object with
prompt:false: capture the original json_args, create a serde_json::Map, insert
("prompt", serde_json::Value::Bool(false)), then if the original was an Object
merge its entries into the map else insert the original under a new key (e.g.,
"value") before returning Some(serde_json::Value::Object(map)); update the code
around the variables json_args and coerced to implement this behavior.

…sions

Address CodeRabbit review on PR trycua#1575.

The previous coercion used 'if let Object' which silently skipped
non-object json_args (e.g. true, [], "str"), leaving prompt defaulting
to true and re-enabling the misattributed TCC dialog.

Replace with a match that always produces a fresh Object with
prompt:false, regardless of the original arg shape.
@hippoley

Copy link
Copy Markdown
Contributor Author

Good catch — fixed in 992e512.

Replaced the if let Object guard with a match that always produces a fresh Object with prompt: false, regardless of the original arg shape. Non-object inputs (true, [], "str") now correctly get coerced instead of passing through unchanged.

@injaneity

Copy link
Copy Markdown
Collaborator

closed as superseded by #1760, #1774, and #2337

@injaneity injaneity closed this Jul 20, 2026
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 check_permissions reports stale accessibility=true after tccutil reset Accessibility com.trycua.driver (macOS)

2 participants