Skip to content

feat(platform-macos): port PermissionsGate CLI flow for first-launch UX (#1527) - #1529

Merged
f-trycua merged 4 commits into
mainfrom
feat/permissions-gate-cli-1527
May 16, 2026
Merged

feat(platform-macos): port PermissionsGate CLI flow for first-launch UX (#1527)#1529
f-trycua merged 4 commits into
mainfrom
feat/permissions-gate-cli-1527

Conversation

@f-trycua

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

Copy link
Copy Markdown
Collaborator

Summary

Ports Swift PermissionsGate to cua-driver-rs as a terminal-driven first-launch flow. Closes #1527.

When cua-driver serve starts on macOS and TCC grants for Accessibility or Screen Recording are missing, the gate now:

  • prints a banner listing exactly which grant is missing and why cua-driver needs it
  • raises the system TCC prompts (no-op when already granted)
  • auto-opens the matching System Settings → Privacy & Security pane(s) via x-apple.systempreferences: URLs (the same URLs Swift's PermissionsGate uses)
  • polls TCC state every 1s, prints "still waiting on X" every 5s
  • auto-continues startup the moment everything flips green

Already-granted users see no change — the gate is a transparent no-op when both grants are active.

Why CLI and not SwiftUI

  • Rust port already drives an AppKit run loop on the main thread for the cursor overlay; grafting another window onto it invites main-thread deadlocks.
  • Primary deployment shape is cua-driver serve from a shell (Claude Code, Cursor, Codex) — terminal is already attached.
  • Headless / CI needs an opt-out anyway; a terminal-driven flow with --no-permissions-gate is the straight-line approach.

A native NSAlert via objc2 is tracked as a possible follow-up. CLI is the MVP.

Opt-out

Signal Effect
cua-driver serve --no-permissions-gate gate skipped
CUA_DRIVER_RS_PERMISSIONS_GATE=0 env-var gate skipped
(any other env-var value, including unset) gate active

Commits

  1. feat(platform-macos): permissions gate (CLI flow) — new permissions/{mod,status,gate}.rs, 7 unit tests, check_permissions tool refactored to share the new status module.
  2. feat(cli): wire permissions gate into serve startup--no-permissions-gate flag + Serve arm call.
  3. docs: permissions gate (PARITY.md + installation + CLI reference) — PARITY.md gets a new section under check_permissions; installation.mdx and cli-reference.mdx get the CI / headless opt-out recipe.

Test plan

  • cargo build --release clean (warnings unchanged from baseline).
  • cargo test -p platform-macos --lib permissions:: — all 7 unit tests pass (opt-out logic, env-var parsing, missing-permission ordering, settings URLs match Swift verbatim).
  • Manual: cua-driver serve on a fresh macOS box with grants revoked → banner appears, Settings opens, gate auto-continues on grant.
  • Manual: cua-driver serve with both grants already active → no banner, no delay, normal startup.
  • Manual: cua-driver serve --no-permissions-gate on a fresh box → no banner, daemon starts, tool calls fail with normal TCC errors.
  • Manual: CUA_DRIVER_RS_PERMISSIONS_GATE=0 cua-driver serve → same behaviour as the flag.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a macOS first-launch permissions gate that requests required Accessibility and Screen Recording permissions before cua-driver serve starts.
    • Added --no-permissions-gate flag and CUA_DRIVER_RS_PERMISSIONS_GATE environment variable to bypass the permissions prompt for CI/headless environments.
  • Documentation

    • Updated installation and CLI reference guides with permissions gate behavior and opt-out options.

Review Change Stack

@vercel

vercel Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment May 16, 2026 10:19pm

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: 6a259451-382e-4723-b1b2-413e7e1f35bc

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 brings the Swift PermissionsGate interactive first-launch experience to the Rust CLI port. It adds a configurable macOS-only gate that checks for required Accessibility and Screen Recording permissions before serve startup, displays a user-friendly banner when permissions are missing, optionally opens System Settings panes, and polls until grants are approved—all with opt-out flags and environment variables for CI/headless use.

Changes

macOS First-Launch Permissions Gate for cua-driver-rs

Layer / File(s) Summary
Permission status probes and TCC utilities
libs/cua-driver-rs/crates/platform-macos/src/permissions/status.rs
PermissionsStatus struct captures Accessibility and Screen Recording grant states via AXIsProcessTrusted and CGPreflightScreenCaptureAccess, with helper functions to check current status and request prompts via macOS FFI.
Permissions module structure and exports
libs/cua-driver-rs/crates/platform-macos/src/lib.rs, libs/cua-driver-rs/crates/platform-macos/src/permissions/mod.rs
Declares the new permissions module and re-exports PermissionsStatus, current_status, GateOpts, MissingPermission, and run_if_needed for cross-crate visibility.
Gate orchestration and interactive flow
libs/cua-driver-rs/crates/platform-macos/src/permissions/gate.rs
MissingPermission enum maps TCC grants to labels/rationales/Settings URLs; GateOpts controls opt-out and polling; run_if_needed orchestrates the flow (print banner, request prompts, open Settings, wait for grants); unit tests verify opt-out semantics and Settings URL parity with Swift.
CLI argument parsing and serve dispatch
libs/cua-driver-rs/crates/cua-driver/src/cli.rs, libs/cua-driver-rs/crates/cua-driver/src/main.rs
Command::Serve gains no_permissions_gate field; parse_command() scans argv for --no-permissions-gate; macOS serve dispatch invokes the gate with env/flag options before socket bind; non-macOS dispatch accepts and ignores the flag for CLI uniformity.
Refactor check_permissions tool to use shared status module
libs/cua-driver-rs/crates/platform-macos/src/tools/check_permissions.rs
Delegates Accessibility and Screen Recording checks to permissions::status probe functions; removes duplicate in-file permission helpers to avoid code duplication.
User documentation and parity notes
docs/content/docs/cua-driver/guide/getting-started/installation.mdx, docs/content/docs/cua-driver/reference/cli-reference.mdx, libs/cua-driver-rs/PARITY.md
Installation guide and CLI reference document the gate behavior, the --no-permissions-gate flag, and CUA_DRIVER_RS_PERMISSIONS_GATE environment variable for opting out; PARITY.md describes Rust vs Swift differences (CLI banner vs SwiftUI panel) and timeout semantics.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • trycua/cua#1368: Adds macOS TCC status plumbing and permission-checking logic that the installer guidance depends on for the same Accessibility/Screen Recording prompts during post-install setup.

Poem

🐰 A gate stands tall on startup's road,
Permissions asked, a gentle load,
Settings open with a bound,
Grant access and move around!
Flags and env vars set it free—
For CI's headless harmony. 🔐

🚥 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 clearly summarizes the main change: porting the PermissionsGate CLI flow to macOS platform for first-launch UX, which aligns directly with the changeset's primary objective.
Linked Issues check ✅ Passed All three acceptance criteria from issue #1527 are met: the PermissionsGate equivalent is implemented in Rust, it blocks serve startup before socket binding, and the interactive gate with polling/auto-dismiss matches the Swift UX (albeit terminal-driven vs SwiftUI).
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the permissions gate: CLI flag parsing, macOS permission status checks, gate logic, documentation, and refactoring the check_permissions tool to use shared permission checks—no unrelated modifications.
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/permissions-gate-cli-1527

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

🤖 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 `@docs/content/docs/cua-driver/reference/cli-reference.mdx`:
- Line 163: Update the description for `--no-permissions-gate` to list all
accepted env-var disable values for CUA_DRIVER_RS_PERMISSIONS_GATE (e.g., 0,
false, no, off — case-insensitive) instead of only `=0`, so the text clearly
states that the gate can also be disabled via environment variable values like
`CUA_DRIVER_RS_PERMISSIONS_GATE=0|false|no|off` (or any case variant).

In `@libs/cua-driver-rs/crates/platform-macos/src/permissions/gate.rs`:
- Around line 310-311: print_banner currently always prints "Opening System
Settings → Privacy & Security now." which is misleading when run_if_needed is
called with open_settings=false; update print_banner (or its caller) to accept
an open_settings boolean (or otherwise check the open_settings flag) and only
print the "Opening System Settings…" line when open_settings is true, otherwise
print a message indicating that the user should manually open Settings or omit
that line; locate the banner logic in print_banner and the call site in
run_if_needed to pass the flag or conditionally call the variant that includes
the open-settings text.
- Around line 351-389: Tests mutate the global CUA_DRIVER_RS_PERMISSIONS_GATE
env var and can race when cargo runs tests in parallel; wrap all env-var
reads/sets in a global test mutex to serialize them. Add a static
OnceLock<Mutex<()>> (or Lazy/OnceCell) and acquire its MutexGuard at the start
of each test (env_var_disables_gate, flag_disables_gate,
neither_flag_nor_env_does_not_opt_out, env_var_truthy_values_do_not_opt_out)
before calling GateOpts::from_env_and_flag or mutating std::env so the
set_var/remove_var and assertions are executed under the lock, then drop the
guard before returning. Ensure the mutex is initialized once and used across
tests to prevent race conditions on the CUA_DRIVER_RS_PERMISSIONS_GATE
environment variable.
🪄 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: 6ea7b1d0-8d98-48bc-bb29-39db8d4fb293

📥 Commits

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

⛔ Files ignored due to path filters (1)
  • libs/cua-driver-rs/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • docs/content/docs/cua-driver/guide/getting-started/installation.mdx
  • docs/content/docs/cua-driver/reference/cli-reference.mdx
  • libs/cua-driver-rs/PARITY.md
  • 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/platform-macos/src/lib.rs
  • libs/cua-driver-rs/crates/platform-macos/src/permissions/gate.rs
  • libs/cua-driver-rs/crates/platform-macos/src/permissions/mod.rs
  • libs/cua-driver-rs/crates/platform-macos/src/permissions/status.rs
  • libs/cua-driver-rs/crates/platform-macos/src/tools/check_permissions.rs

| Name | Description |
| ---- | ----------- |
| `--no-relaunch` | Stay in the current process instead of re-execing via `open -n -g -a CuaDriver`. |
| `--no-permissions-gate` | Skip the macOS TCC permissions gate at startup. Use for CI / headless runners where blocking on user input would deadlock the process. Also toggleable via `CUA_DRIVER_RS_PERMISSIONS_GATE=0`. |

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

Document all accepted env-var disable values, not just =0.

Line 163 currently implies only CUA_DRIVER_RS_PERMISSIONS_GATE=0 disables the gate, while other docs also allow false, no, and off. Please align this line to avoid conflicting operator guidance.

🤖 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 `@docs/content/docs/cua-driver/reference/cli-reference.mdx` at line 163, Update
the description for `--no-permissions-gate` to list all accepted env-var disable
values for CUA_DRIVER_RS_PERMISSIONS_GATE (e.g., 0, false, no, off —
case-insensitive) instead of only `=0`, so the text clearly states that the gate
can also be disabled via environment variable values like
`CUA_DRIVER_RS_PERMISSIONS_GATE=0|false|no|off` (or any case variant).

Comment on lines +310 to +311
println!(" Opening System Settings → Privacy & Security now.");
println!(" Grant each item, then this prompt will auto-continue.");

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

Banner text is inaccurate when open_settings is disabled.

print_banner always says Settings is being opened, but run_if_needed can be configured with open_settings=false. Make this line conditional (or parameterize banner rendering) to avoid misleading output.

🤖 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/platform-macos/src/permissions/gate.rs` around
lines 310 - 311, print_banner currently always prints "Opening System Settings →
Privacy & Security now." which is misleading when run_if_needed is called with
open_settings=false; update print_banner (or its caller) to accept an
open_settings boolean (or otherwise check the open_settings flag) and only print
the "Opening System Settings…" line when open_settings is true, otherwise print
a message indicating that the user should manually open Settings or omit that
line; locate the banner logic in print_banner and the call site in run_if_needed
to pass the flag or conditionally call the variant that includes the
open-settings text.

Comment on lines +351 to +389
fn env_var_disables_gate() {
// Mutating the env in a test is generally suspect (parallel tests
// racing) but cargo runs unit tests in this crate sequentially and
// the var name is unique enough to avoid collisions.
std::env::set_var("CUA_DRIVER_RS_PERMISSIONS_GATE", "0");
let opts = GateOpts::from_env_and_flag(false);
assert!(opts.opt_out, "env=0 must opt out");
std::env::remove_var("CUA_DRIVER_RS_PERMISSIONS_GATE");
}

#[test]
fn flag_disables_gate() {
std::env::remove_var("CUA_DRIVER_RS_PERMISSIONS_GATE");
let opts = GateOpts::from_env_and_flag(true);
assert!(opts.opt_out, "--no-permissions-gate must opt out");
}

#[test]
fn neither_flag_nor_env_does_not_opt_out() {
std::env::remove_var("CUA_DRIVER_RS_PERMISSIONS_GATE");
let opts = GateOpts::from_env_and_flag(false);
assert!(!opts.opt_out);
}

#[test]
fn env_var_truthy_values_do_not_opt_out() {
// Only the explicit "off" sentinels disable the gate. Anything
// else (including empty string or unknown garbage) leaves the gate
// active — fail-safe default for first-launch UX.
for v in &["1", "true", "yes", "on", "garbage", ""] {
std::env::set_var("CUA_DRIVER_RS_PERMISSIONS_GATE", v);
let opts = GateOpts::from_env_and_flag(false);
assert!(
!opts.opt_out,
"env={v:?} must not opt out (only 0/false/no/off do)"
);
}
std::env::remove_var("CUA_DRIVER_RS_PERMISSIONS_GATE");
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="libs/cua-driver-rs/crates/platform-macos/src/permissions/gate.rs"

echo "== Env-var mutation sites in tests =="
rg -n --type rust 'std::env::set_var|std::env::remove_var' "$FILE"

echo
echo "== Existing serialization primitives in test module (if any) =="
rg -n --type rust 'Mutex|OnceLock|serial_test' "$FILE"

echo
echo "== Relevant test block context =="
nl -ba "$FILE" | sed -n '345,395p'

Repository: trycua/cua

Length of output: 571


🏁 Script executed:

# Check for test parallelism configuration in the crate's Cargo.toml
fd -n Cargo.toml | xargs grep -l "cua-driver-rs-platform-macos" 2>/dev/null | head -5

Repository: trycua/cua

Length of output: 285


🏁 Script executed:

# Check the actual Cargo.toml for the platform-macos crate
find . -path "*platform-macos/Cargo.toml" -type f

Repository: trycua/cua

Length of output: 108


🏁 Script executed:

# Search for any other usages of the CUA_DRIVER_RS_PERMISSIONS_GATE env var in the entire codebase
rg -i "CUA_DRIVER_RS_PERMISSIONS_GATE" --type rust

Repository: trycua/cua

Length of output: 1631


🏁 Script executed:

# Check if there are any test configuration attributes (like #[test] ordering) in the test module
rg -B5 -A15 "fn env_var_disables_gate" libs/cua-driver-rs/crates/platform-macos/src/permissions/gate.rs | head -30

Repository: trycua/cua

Length of output: 856


🏁 Script executed:

cat -n ./libs/cua-driver-rs/crates/platform-macos/Cargo.toml

Repository: trycua/cua

Length of output: 1822


🏁 Script executed:

# Check if there's a global test configuration or workspace setting
rg -n "test-threads|profile.test" --type toml libs/cua-driver-rs/

Repository: trycua/cua

Length of output: 36


🏁 Script executed:

# Check parent workspace Cargo.toml
find . -name "Cargo.toml" -path "*/cua-driver-rs/*" | head -3 | xargs cat

Repository: trycua/cua

Length of output: 2584


🏁 Script executed:

# Verify the suggested lock pattern is correct Rust syntax
cat > /tmp/test_lock.rs << 'EOF'
use std::sync::{Mutex, OnceLock};

fn env_lock() -> std::sync::MutexGuard<'static, ()> {
    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    LOCK.get_or_init(|| Mutex::new(())).lock().expect("env lock poisoned")
}

#[test]
fn test_lock() {
    let _guard = env_lock();
    println!("lock acquired");
}
EOF

# Just check if it's syntactically valid by trying to parse it
rustfmt --check /tmp/test_lock.rs 2>&1 | head -20 || echo "Syntax check completed"

Repository: trycua/cua

Length of output: 101


🏁 Script executed:

# Double-check: Look at the exact test functions to see if there are any other synchronization mechanisms
rg -A20 "^mod.*test" libs/cua-driver-rs/crates/platform-macos/src/permissions/gate.rs | tail -50

Repository: trycua/cua

Length of output: 835


Add test synchronization to prevent race conditions on env-var mutations.

The four test functions in this module mutate the global CUA_DRIVER_RS_PERMISSIONS_GATE environment variable without synchronization. Although the existing comment claims tests run sequentially in this crate, there's no Cargo.toml configuration enforcing this—Cargo runs tests in parallel by default. These tests will race and produce nondeterministic failures when run concurrently.

Use a static Mutex + OnceLock to serialize access across all four test functions:

Suggested implementation
+    use std::sync::{Mutex, OnceLock};
+
+    fn env_lock() -> std::sync::MutexGuard<'static, ()> {
+        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
+        LOCK.get_or_init(|| Mutex::new(())).lock().expect("env lock poisoned")
+    }
+
     #[test]
     fn env_var_disables_gate() {
+        let _guard = env_lock();
         std::env::set_var("CUA_DRIVER_RS_PERMISSIONS_GATE", "0");
         let opts = GateOpts::from_env_and_flag(false);
         assert!(opts.opt_out, "env=0 must opt out");
         std::env::remove_var("CUA_DRIVER_RS_PERMISSIONS_GATE");
     }

     #[test]
     fn flag_disables_gate() {
+        let _guard = env_lock();
         std::env::remove_var("CUA_DRIVER_RS_PERMISSIONS_GATE");
         let opts = GateOpts::from_env_and_flag(true);
         assert!(opts.opt_out, "--no-permissions-gate must opt out");
     }

     #[test]
     fn neither_flag_nor_env_does_not_opt_out() {
+        let _guard = env_lock();
         std::env::remove_var("CUA_DRIVER_RS_PERMISSIONS_GATE");
         let opts = GateOpts::from_env_and_flag(false);
         assert!(!opts.opt_out);
     }

     #[test]
     fn env_var_truthy_values_do_not_opt_out() {
+        let _guard = env_lock();
         for v in &["1", "true", "yes", "on", "garbage", ""] {
             std::env::set_var("CUA_DRIVER_RS_PERMISSIONS_GATE", v);
             let opts = GateOpts::from_env_and_flag(false);
             assert!(
                 !opts.opt_out,
                 "env={v:?} must not opt out (only 0/false/no/off do)"
             );
         }
         std::env::remove_var("CUA_DRIVER_RS_PERMISSIONS_GATE");
     }
📝 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
fn env_var_disables_gate() {
// Mutating the env in a test is generally suspect (parallel tests
// racing) but cargo runs unit tests in this crate sequentially and
// the var name is unique enough to avoid collisions.
std::env::set_var("CUA_DRIVER_RS_PERMISSIONS_GATE", "0");
let opts = GateOpts::from_env_and_flag(false);
assert!(opts.opt_out, "env=0 must opt out");
std::env::remove_var("CUA_DRIVER_RS_PERMISSIONS_GATE");
}
#[test]
fn flag_disables_gate() {
std::env::remove_var("CUA_DRIVER_RS_PERMISSIONS_GATE");
let opts = GateOpts::from_env_and_flag(true);
assert!(opts.opt_out, "--no-permissions-gate must opt out");
}
#[test]
fn neither_flag_nor_env_does_not_opt_out() {
std::env::remove_var("CUA_DRIVER_RS_PERMISSIONS_GATE");
let opts = GateOpts::from_env_and_flag(false);
assert!(!opts.opt_out);
}
#[test]
fn env_var_truthy_values_do_not_opt_out() {
// Only the explicit "off" sentinels disable the gate. Anything
// else (including empty string or unknown garbage) leaves the gate
// active — fail-safe default for first-launch UX.
for v in &["1", "true", "yes", "on", "garbage", ""] {
std::env::set_var("CUA_DRIVER_RS_PERMISSIONS_GATE", v);
let opts = GateOpts::from_env_and_flag(false);
assert!(
!opts.opt_out,
"env={v:?} must not opt out (only 0/false/no/off do)"
);
}
std::env::remove_var("CUA_DRIVER_RS_PERMISSIONS_GATE");
}
use std::sync::{Mutex, OnceLock};
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(())).lock().expect("env lock poisoned")
}
#[test]
fn env_var_disables_gate() {
let _guard = env_lock();
// Mutating the env in a test is generally suspect (parallel tests
// racing) but cargo runs unit tests in this crate sequentially and
// the var name is unique enough to avoid collisions.
std::env::set_var("CUA_DRIVER_RS_PERMISSIONS_GATE", "0");
let opts = GateOpts::from_env_and_flag(false);
assert!(opts.opt_out, "env=0 must opt out");
std::env::remove_var("CUA_DRIVER_RS_PERMISSIONS_GATE");
}
#[test]
fn flag_disables_gate() {
let _guard = env_lock();
std::env::remove_var("CUA_DRIVER_RS_PERMISSIONS_GATE");
let opts = GateOpts::from_env_and_flag(true);
assert!(opts.opt_out, "--no-permissions-gate must opt out");
}
#[test]
fn neither_flag_nor_env_does_not_opt_out() {
let _guard = env_lock();
std::env::remove_var("CUA_DRIVER_RS_PERMISSIONS_GATE");
let opts = GateOpts::from_env_and_flag(false);
assert!(!opts.opt_out);
}
#[test]
fn env_var_truthy_values_do_not_opt_out() {
let _guard = env_lock();
// Only the explicit "off" sentinels disable the gate. Anything
// else (including empty string or unknown garbage) leaves the gate
// active — fail-safe default for first-launch UX.
for v in &["1", "true", "yes", "on", "garbage", ""] {
std::env::set_var("CUA_DRIVER_RS_PERMISSIONS_GATE", v);
let opts = GateOpts::from_env_and_flag(false);
assert!(
!opts.opt_out,
"env={v:?} must not opt out (only 0/false/no/off do)"
);
}
std::env::remove_var("CUA_DRIVER_RS_PERMISSIONS_GATE");
}
🤖 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/platform-macos/src/permissions/gate.rs` around
lines 351 - 389, Tests mutate the global CUA_DRIVER_RS_PERMISSIONS_GATE env var
and can race when cargo runs tests in parallel; wrap all env-var reads/sets in a
global test mutex to serialize them. Add a static OnceLock<Mutex<()>> (or
Lazy/OnceCell) and acquire its MutexGuard at the start of each test
(env_var_disables_gate, flag_disables_gate,
neither_flag_nor_env_does_not_opt_out, env_var_truthy_values_do_not_opt_out)
before calling GateOpts::from_env_and_flag or mutating std::env so the
set_var/remove_var and assertions are executed under the lock, then drop the
guard before returning. Ensure the mutex is initialized once and used across
tests to prevent race conditions on the CUA_DRIVER_RS_PERMISSIONS_GATE
environment variable.

f-trycua added a commit that referenced this pull request May 16, 2026
Three findings from the review on the permissions-gate-cli branch:

1. `print_banner` no longer claims "Opening System Settings…" when the
   caller passed `open_settings: false`. The flag is threaded through
   and the false branch prints the equivalent `open "x-apple.systempreferences:…"`
   command(s) instead, so users still know how to grant manually.

2. The 7 unit tests that mutate `CUA_DRIVER_RS_PERMISSIONS_GATE` now
   serialize through a static `OnceLock<Mutex<()>>`. Cargo runs unit
   tests in parallel by default, so without the lock the env-var
   tests race on a process-global. Standard pattern; recovers from
   mutex poisoning so a panicking test doesn't wedge the rest of
   the suite.

3. `CUA_DRIVER_RS_PERMISSIONS_GATE` matching is now case-insensitive
   (`0` / `false` / `no` / `off` in any case), and the CLI reference
   + installation docs + module/banner text all describe the full
   off-sentinel set instead of only mentioning `=0`. New test
   `env_var_off_sentinels_are_case_insensitive` covers the matrix.

Verified: 8/8 platform-macos::permissions tests pass across 5
repeated runs; `cargo build --release` stays clean.

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

Copy link
Copy Markdown
Collaborator Author

Addressed the 3 CodeRabbit findings in commit 9661390e:

  1. gate.rs:310-311 (banner ignores open_settings=false)print_banner now takes the open_settings flag and only prints "Opening System Settings → Privacy & Security now." when it's true. When false it prints the equivalent open "x-apple.systempreferences:…" command(s) for each missing pane so users still know how to grant manually.

  2. gate.rs:351-389 (env-var tests race under parallel cargo test) — added a static TEST_ENV_MUTEX: OnceLock<Mutex<()>> and an env_lock() helper. Every test that touches CUA_DRIVER_RS_PERMISSIONS_GATE acquires the guard for the duration of the test; mutex poisoning is recovered with into_inner() so a panicking test doesn't wedge the suite.

  3. cli-reference.mdx:163 (docs only mention =0) — the env-var description now lists all off-sentinels (0, false, no, off) and notes they're case-insensitive. To make that promise truthful I also updated from_env_and_flag to lowercase the input before matching (was strict case-sensitive), and added env_var_off_sentinels_are_case_insensitive covering FALSE, Off, NO, TrUe, etc. Same case-insensitive wording added to installation.mdx and the module/banner text.

Verification: cargo build --release clean; cargo test -p platform-macos permissions:: shows 8/8 pass across 5 repeated runs (was 7 before the new case-insensitivity test).

@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

🏁 Script executed:

#!/bin/bash
# Fetch the current state of the key files after the fix commit

# gate.rs - check print_banner, TEST_ENV_MUTEX, env_lock, from_env_and_flag, and tests
fd "gate.rs" --exec cat {}

# cli-reference.mdx
fd "cli-reference.mdx" --exec cat {}

# installation.mdx
fd "installation.mdx" --exec cat {}

Length of output: 86960


@f-trycua All three fixes in commit 9661390e look correct — here's what I verified:


Fix 1 — print_banner respects open_settings=false

print_banner now takes open_settings: bool and branches correctly:

  • true → prints "Opening System Settings → Privacy & Security now."
  • false → prints the manual open "x-apple.systempreferences:…" # <label> lines, one per missing permission

The call site print_banner(&missing, opts.open_settings) correctly forwards the flag.


Fix 2 — Env-var test serialization via TEST_ENV_MUTEX

The OnceLock<Mutex<()>> + env_lock() helper with poison recovery (unwrap_or_else(|e| e.into_inner())) is the right pattern. All five env-touching tests (env_var_disables_gate, flag_disables_gate, neither_flag_nor_env_does_not_opt_out, env_var_truthy_values_do_not_opt_out, env_var_off_sentinels_are_case_insensitive) acquire the guard at the top — no test is left unprotected.


Fix 3 — Case-insensitive off-sentinels in code and docs

  • from_env_and_flag now lowercases via .to_ascii_lowercase() before matching — implementation matches the documented promise.
  • The new env_var_off_sentinels_are_case_insensitive test covers FALSE, False, NO, No, OFF, Off, and includes TrUe specifically to assert it does not opt out — a clean boundary check.
  • cli-reference.mdx and installation.mdx both list all four sentinels with explicit case-insensitive wording and concrete examples (FALSE, Off, NO).
  • The module-level doc comment matches too.

Everything is internally consistent across the implementation, tests, and docs.

[approve]

🐇 ✓

f-trycua and others added 4 commits May 17, 2026 00:17
Port Swift's PermissionsGate first-launch UX to cua-driver-rs as a
terminal-driven flow.  When `serve` starts and TCC permissions
(Accessibility / Screen Recording) are missing the gate now:

  - prints a clear banner explaining which grant is missing and why
    cua-driver needs it
  - raises the system TCC prompts (no-op when already granted)
  - auto-opens the matching `System Settings → Privacy & Security`
    pane via the `x-apple.systempreferences:` URL scheme
  - polls TCC state every 1s and emits a "still waiting on X" line
    every 5s
  - returns to normal startup once everything flips green

No SwiftUI / GUI dependency is introduced — the Rust port intentionally
keeps the gate terminal-only so it works headlessly under CI when paired
with the opt-out (added in the next commit).

`check_permissions.rs` now delegates its TCC probes to the new
`permissions::status` module so both the MCP tool and the gate share
the same code paths.

Refs #1527

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Call `platform_macos::permissions::run_if_needed()` at the top of
`cua-driver serve` on macOS, before the tool registry is built or the
Unix socket is bound.  When permissions are already granted the gate
is a transparent no-op — the existing happy path is unchanged.

Adds a `--no-permissions-gate` bare flag that short-circuits the gate
for CI / headless automation.  `CUA_DRIVER_RS_PERMISSIONS_GATE=0`
(checked inside the gate) is the env-var alternative; either signal
is sufficient to opt out.

If the gate returns an error (deadline elapsed) the daemon continues
to start so that affected tool calls fail with their existing TCC
error messages, mirroring Swift's "user closed the panel" fallback.

Refs #1527

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- PARITY.md: new "Startup flow: permissions gate (`serve`)" section
  documenting the macOS port, the intentional UX divergence from
  Swift's SwiftUI panel, and the full opt-out signal table.

- installation.mdx: call out the gate in a Callout under "Grant TCC
  permissions", with the CI / headless opt-out recipe (flag + env-var).

- cli-reference.mdx: add a `serve` paragraph describing the gate and
  document `--no-permissions-gate` in the flags table.

Refs #1527

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three findings from the review on the permissions-gate-cli branch:

1. `print_banner` no longer claims "Opening System Settings…" when the
   caller passed `open_settings: false`. The flag is threaded through
   and the false branch prints the equivalent `open "x-apple.systempreferences:…"`
   command(s) instead, so users still know how to grant manually.

2. The 7 unit tests that mutate `CUA_DRIVER_RS_PERMISSIONS_GATE` now
   serialize through a static `OnceLock<Mutex<()>>`. Cargo runs unit
   tests in parallel by default, so without the lock the env-var
   tests race on a process-global. Standard pattern; recovers from
   mutex poisoning so a panicking test doesn't wedge the rest of
   the suite.

3. `CUA_DRIVER_RS_PERMISSIONS_GATE` matching is now case-insensitive
   (`0` / `false` / `no` / `off` in any case), and the CLI reference
   + installation docs + module/banner text all describe the full
   off-sentinel set instead of only mentioning `=0`. New test
   `env_var_off_sentinels_are_case_insensitive` covers the matrix.

Verified: 8/8 platform-macos::permissions tests pass across 5
repeated runs; `cargo build --release` stays clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <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 interactive PermissionsGate (first-launch UX)

1 participant