Skip to content

feat(cua-driver-rs)(macos): permissions UX — source labeling + live probe + permissions status|grant CLI verb - #1765

Merged
f-trycua merged 2 commits into
mainfrom
feat/check-permissions-source-and-probe
May 30, 2026
Merged

feat(cua-driver-rs)(macos): permissions UX — source labeling + live probe + permissions status|grant CLI verb#1765
f-trycua merged 2 commits into
mainfrom
feat/check-permissions-source-and-probe

Conversation

@f-trycua

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

Copy link
Copy Markdown
Collaborator

Problem (reported live)

cua-driver call check_permissions from a terminal can return accessibility: true, screen_recording: true while tccutil … com.trycua.driver reports no record for the driver. The preflight APIs (CGPreflightScreenCaptureAccess, AXIsProcessTrusted) answer for the macOS responsible process (the launching app), so a standalone call reports the terminal's grants — a false positive for the driver.

Fix — Peekaboo's two techniques, without touching the daemon path

Peekaboo (a) doesn't trust the preflight API — it probes with SCShareableContent — and (b) prints a Source: line so you know which identity the status reflects. Both adopted here:

  • (A) screen_recording_capturable — a live SCShareableContent::get() probe (screencapturekit was already a dep for recording). Only true when the answering process can genuinely capture. When it disagrees with the preflight screen_recording, the text summary flags it — catching the stale-cache false positive (cua-driver check_permissions reports stale accessibility=true after tccutil reset Accessibility com.trycua.driver (macOS) #1561) too.
  • (B) source{ attribution: "driver-daemon" | "caller", pid, responsible_ppid, executable, note }. driver-daemon when we're the bundle binary reparented to launchd (the real com.trycua.driver status — the forced bundle/daemon path), caller otherwise, with a note + the open -n -g -a CuaDriver --args serve hint.

status.rs and the startup permissions gate are untouched — zero risk to the daemon path.

Verified live (macOS 26)

$ cua-driver call check_permissions '{"prompt":false}'
{
  "accessibility": true,
  "screen_recording": true,
  "screen_recording_capturable": true,
  "source": {
    "attribution": "caller",
    "note": "These booleans reflect the TCC identity of the app that launched this process … NOT the CuaDriver daemon (com.trycua.driver) …",
    "pid": 9294, "responsible_ppid": 9139,
    "executable": "/Applications/CuaDriver.app/Contents/MacOS/cua-driver"
  }
}

Refs

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Enhanced permissions detection with live screen recording capability checks
    • Added permission source attribution to identify where permissions originate
    • Improved warnings when permission state conflicts are detected
    • Extended output with additional permission details
  • Documentation

    • Updated tool documentation to reflect new response fields and attribution semantics

Review Change Stack

… + live capture probe

`check_permissions` answered in-process trusts CGPreflightScreenCaptureAccess
/ AXIsProcessTrusted, which answer for the macOS *responsible process* (the
LaunchServices launching app), not the executable. So a standalone
`cua-driver call check_permissions` from a terminal reports the TERMINAL's
grants — it can read `accessibility: true, screen_recording: true` while
`tccutil … com.trycua.driver` reports no record for the driver. A real
false positive (reported live).

Borrowing Peekaboo's two techniques, without changing the daemon path:

- **(A) Live probe.** Add `screen_recording_capturable` from a
  `SCShareableContent::get()` query (screencapturekit is already a dep).
  Unlike the CGPreflight cache — which goes stale after `tccutil reset`
  (#1561) and is unreliable for CLI/child processes — this only returns
  displays when the answering process can genuinely capture. A preflight
  `true` with a probe `false` is the false-positive tell; the text summary
  flags the disagreement.

- **(B) Source identity.** Add a `source` object: `attribution`
  (`driver-daemon` when we're the bundle binary reparented to launchd —
  the real driver status — vs `caller` otherwise), pid, responsible_ppid,
  executable, and a note explaining the booleans belong to the launching
  app, not com.trycua.driver, with the `open -n -g -a CuaDriver --args
  serve` path for the driver's own status.

status.rs + the startup gate are untouched (zero risk to the forced
bundle/daemon path). Verified live: a terminal `call check_permissions`
now returns attribution=caller + the note; the daemon path reports
attribution=driver-daemon.

Refs #1491, #1561.
@vercel

vercel Bot commented May 30, 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 30, 2026 3:44am

Request Review

@coderabbitai

coderabbitai Bot commented May 30, 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: 23fcc9f7-9421-4f04-8388-24419bbecfc8

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 enhances the macOS check_permissions tool by adding live ScreenCaptureKit display capture probing, process attribution logic to distinguish daemon-initiated vs. caller-initiated permission checks, and enriched reporting with conflict warnings and source metadata in both text and JSON output.

Changes

Permission Check Enhancement

Layer / File(s) Summary
Live capture and attribution probes
libs/cua-driver/rust/crates/platform-macos/src/tools/check_permissions.rs
screen_recording_capturable() performs live ScreenCaptureKit query to test display captureability. permission_source() collects PID/PPID and executable identity to determine whether the checker is the CuaDriver daemon (ppid==1) or a caller process, returning attribution details and context.
Tool description documentation
libs/cua-driver/rust/crates/platform-macos/src/tools/check_permissions.rs
Updates ToolDef description string to document the new screen_recording_capturable and source response fields and explain the attribution semantics for daemon vs. caller contexts.
Invoke integration and output enrichment
libs/cua-driver/rust/crates/platform-macos/src/tools/check_permissions.rs
invoke() now computes live capture status and source attribution, builds a richer summary with warnings when preflight grant and live probe disagree, appends caller-attribution notes, and extends JSON output to include screen_recording_capturable and source alongside existing permission fields.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related issues

  • trycua/cua#1561: The new permission_source() function directly addresses AXIsProcessTrusted and screen-recording permission attribution by distinguishing whether the CuaDriver daemon or the launching caller is responsible for the reported permission state.

Possibly related PRs

  • trycua/cua#1529: Adds shared permissions::status and startup gate that the main PR's enhanced check_permissions tool now consumes and extends with live probing and attribution reporting.
  • trycua/cua#1562: Adjusts underlying screen_recording_granted() preflight behavior by removing a fallback, directly impacting the results and conflict-detection logic that the main PR's live screen_recording_capturable() probe now reports.
  • trycua/cua#1760: Suppresses prompting during check_permissions invocation to prevent terminal-attributed TCC prompts, which works in tandem with the main PR's new source attribution to clarify permission ownership.

Poem

🐰 Live probes and daemon dreams align,
Who asked the system?—Caller or mine?
With ScreenCaptureKit's truthful gaze,
We chase the permission's haze.
Rich JSON tales of source and sight! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately describes the main changes: adding source labeling and live capture probe functionality to the permissions check tool, with specific focus on the CLI verb improvements mentioned.

✏️ 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/check-permissions-source-and-probe

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/rust/crates/platform-macos/src/tools/check_permissions.rs`:
- Around line 21-25: The probe function screen_recording_capturable() currently
swallows SCShareableContent::get() errors by using unwrap_or(false), making
transient probe failures indistinguishable from a true permission-denied result;
change it to return a Result<bool, E> (or an enum) so callers (e.g., invoke())
can distinguish probe errors from an actual non-capturable state and
log/propagate the underlying error instead of serializing false. Also, in the
source construction, don’t call the field responsible_ppid with libc::getppid()
if you intend the LaunchServices/TCC “responsible process” PID; rename the field
to immediate_ppid or add a new field (e.g., responsible_process_pid) that
attempts to resolve the true responsible PID, keeping libc::getppid() only for
the immediate parent identity.
🪄 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: 793728f1-92e1-4175-a2e1-c7ec91c85351

📥 Commits

Reviewing files that changed from the base of the PR and between 0ff8ea6 and 3fde1fd.

📒 Files selected for processing (1)
  • libs/cua-driver/rust/crates/platform-macos/src/tools/check_permissions.rs

Comment on lines +21 to +25
fn screen_recording_capturable() -> bool {
use screencapturekit::prelude::SCShareableContent;
SCShareableContent::get()
.map(|c| !c.displays().is_empty())
.unwrap_or(false)

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
set -euo pipefail

FILE="libs/cua-driver/rust/crates/platform-macos/src/tools/check_permissions.rs"

echo "== File =="
ls -l "$FILE"

echo "== Relevant function: screen_recording_capturable() =="
rg -n "fn screen_recording_capturable" -n "$FILE" || true
# Print around it (small window)
python3 - <<'PY'
import itertools,sys,os
path="libs/cua-driver/rust/crates/platform-macos/src/tools/check_permissions.rs"
start=1
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()
# find line numbers for the function
for i,l in enumerate(lines, start=1):
    if "fn screen_recording_capturable" in l:
        s=i-5
        e=i+30
        print(f"== lines {s}-{e} ==")
        for j in range(max(1,s), min(len(lines),e)+1):
            print(f"{j:4d}:{lines[j-1].rstrip()}")
        break
PY

echo "== CheckPermissionsTool::invoke() warning + structured payload =="
python3 - <<'PY'
path="libs/cua-driver/rust/crates/platform-macos/src/tools/check_permissions.rs"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()
def show_around(substr, window=120):
    for i,l in enumerate(lines, start=1):
        if substr in l:
            s=i-20
            e=i+window
            print(f"\n== around line {i} ({substr}) => {s}-{e} ==")
            for j in range(max(1,s), min(len(lines),e)+1):
                print(f"{j:4d}:{lines[j-1].rstrip()}")
            return True
    return False

show_around("⚠️  Screen Recording reads granted")
show_around("screen_recording_capturable")
show_around("responsible_ppid")
PY

echo "== permission_source() responsible_ppid field =="
rg -n "permission_source\\(" "$FILE" || true
python3 - <<'PY'
path="libs/cua-driver/rust/crates/platform-macos/src/tools/check_permissions.rs"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()
for i,l in enumerate(lines, start=1):
    if "fn permission_source" in l:
        s=i-5
        e=i+80
        print(f"\n== lines {s}-{e} ===============")
        for j in range(max(1,s), min(len(lines),e)+1):
            print(f"{j:4d}:{lines[j-1].rstrip()}")
        break
PY

Repository: trycua/cua

Length of output: 21994


Don’t collapse ScreenCaptureKit probe errors into false

  • screen_recording_capturable() turns any SCShareableContent::get() error into false (unwrap_or(false)), so transient probe failures are indistinguishable from a true permission miss; invoke() can then emit the “reads granted but a live capture probe failed” warning and serialize screen_recording_capturable: false. (libs/cua-driver/rust/crates/platform-macos/src/tools/check_permissions.rs:21-26, 145-165)
  • source sets responsible_ppid to libc::getppid() (immediate parent PID). If the intent is the LaunchServices/TCC “responsible process” PID, this can be wrong for indirect spawn/reparenting cases—rename or add the accurate identity. (40-75)
Possible fix
-fn screen_recording_capturable() -> bool {
+fn screen_recording_capturable() -> Result<bool, String> {
     use screencapturekit::prelude::SCShareableContent;
     SCShareableContent::get()
         .map(|c| !c.displays().is_empty())
-        .unwrap_or(false)
+        .map_err(|err| err.to_string())
 }
-        let screen_recording_capturable = screen_recording_capturable();
+        let screen_recording_capturable = screen_recording_capturable();
+        let screen_recording_capturable_value =
+            screen_recording_capturable.as_ref().ok().copied();
 
-        if screen_recording && !screen_recording_capturable {
+        if screen_recording && screen_recording_capturable_value == Some(false) {
             summary.push_str(
                 "\n⚠️  Screen Recording reads granted but a live capture probe failed — \
                  the grant likely belongs to a different process, not this one.",
             );
         }
 
         ToolResult::text(summary)
             .with_structured(serde_json::json!({
                 "accessibility":               accessibility,
                 "screen_recording":            screen_recording,
-                "screen_recording_capturable": screen_recording_capturable,
+                "screen_recording_capturable": screen_recording_capturable_value,
+                "screen_recording_capturable_error": screen_recording_capturable.err(),
                 "source":                      source,
             }))
🤖 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/rust/crates/platform-macos/src/tools/check_permissions.rs`
around lines 21 - 25, The probe function screen_recording_capturable() currently
swallows SCShareableContent::get() errors by using unwrap_or(false), making
transient probe failures indistinguishable from a true permission-denied result;
change it to return a Result<bool, E> (or an enum) so callers (e.g., invoke())
can distinguish probe errors from an actual non-capturable state and
log/propagate the underlying error instead of serializing false. Also, in the
source construction, don’t call the field responsible_ppid with libc::getppid()
if you intend the LaunchServices/TCC “responsible process” PID; rename the field
to immediate_ppid or add a new field (e.g., responsible_process_pid) that
attempts to resolve the true responsible PID, keeping libc::getppid() only for
the immediate parent identity.

Promote permission checking/granting from the clunky
`cua-driver call check_permissions '{"prompt":false}'` to a discoverable
verb group (mirrors `recording` / `skills` / `autostart`):

- `cua-driver permissions status [--json]` — report Accessibility + Screen
  Recording. Read-only (never prompts). Routes through a running daemon
  when one is up so the answer carries the com.trycua.driver identity;
  otherwise reads in-process and labels the result as the caller's
  (`source.attribution`). Human-formatted by default, `--json` for the raw
  payload (incl. the `screen_recording_capturable` probe + `source`).
- `cua-driver permissions grant` — launch CuaDriver via LaunchServices
  (`open -n -g -a CuaDriver --args serve`) so the dialog attributes to
  com.trycua.driver, wait (user-paced, 180s) for the daemon's socket — it
  only appears once the gate passes, i.e. the grant was given — then
  confirm the driver's own status. The blessed grant path, no longer
  tribal knowledge.

Wired into `--help`, telemetry (`cua_driver_permissions`), and both macOS
dispatch sites. The MCP `check_permissions` tool is unchanged. Updated the
tool's `source.note` to point at `cua-driver permissions grant` instead of
the raw `open` incantation.

Verified live: `permissions status` reports caller-attributed status + the
grant hint with no prompt/daemon; `--json` emits the full payload;
`permissions grant` finds/launches the daemon and confirms.
@f-trycua f-trycua changed the title feat(cua-driver-rs)(macos): check_permissions reports source identity + live capture probe feat(cua-driver-rs)(macos): permissions UX — source labeling + live probe + permissions status|grant CLI verb May 30, 2026
@f-trycua
f-trycua merged commit 8ba5cd0 into main May 30, 2026
10 checks passed
@f-trycua
f-trycua deleted the feat/check-permissions-source-and-probe branch May 30, 2026 03:45
f-trycua added a commit that referenced this pull request May 30, 2026
…nce) (#1766)

The install output + autostart docs pitched `--autostart` as bare
"auto-start (optional)". On macOS it's more than convenience: a
launchd-started daemon is attributed to com.trycua.driver (not the
terminal that would otherwise spawn it), so permission prompts say
"Cua Driver", you grant Accessibility + Screen Recording ONCE, and the
grants persist — every `cua-driver call`/`mcp` then routes through the
correctly-attributed always-on daemon. This is the clean fix for the
terminal-attribution problem (#1491, PRs #1758#1765).

Changes:
- install-local-rust.sh: macOS hint now "recommended on macOS" and
  explains the TCC attribution win; the --autostart --help text too.
- autostart.mdx: new callout on the macOS TCC benefit; fixed the stale
  plist name (com.trycua.cua-driver-rs.plist → com.trycua.cua-driver.plist,
  matching what the script actually writes) and pointed at the new
  `cua-driver permissions status|grant` verbs.

Kept opt-in (a standing computer-control daemon is a deliberate choice).
Linux/systemd messaging unchanged — TCC is macOS-only.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants