Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -346,12 +346,21 @@ If a grant still reads `NOT granted` after granting in the dialog, open **System
<Callout type="info">
**First-launch permissions gate (`cua-driver serve`).** On the Rust port,
`cua-driver serve` runs an interactive permissions gate at startup. If
Accessibility or Screen Recording is missing it prints a banner, auto-opens
the matching System Settings pane, and polls until you grant the missing
items. When both grants are already active the gate is a transparent no-op.

**CI / headless runners** should skip the gate so the daemon does not block
waiting for a TTY-attached human:
Accessibility or Screen Recording is missing it presents a small native
`CuaDriver Permissions` window listing the missing grants with **Open
System Settings** and **Continue anyway** buttons, then polls until you
grant the missing items. When both grants are already active the gate is
a transparent no-op.

**Headless / terminal-only fallback.** The native window only shows when
the daemon is launched from the bundled `.app` (e.g.
`open -n -g -a CuaDriver --args serve`). Bare-binary launches
(`./target/release/cua-driver serve`), `CUA_DRIVER_RS_PERMISSIONS_PANEL=0`,
and environments with no graphical session fall back to the historical
terminal banner + polling flow.

**CI / headless runners** should skip the entire gate so the daemon does
not block waiting for a TTY-attached human:

```bash
# As a flag …
Expand All @@ -361,9 +370,9 @@ If a grant still reads `NOT granted` after granting in the dialog, open **System
CUA_DRIVER_RS_PERMISSIONS_GATE=0 cua-driver serve
```

Accepted "off" values for the env-var (case-insensitive): `0`, `false`,
Accepted "off" values for both env-vars (case-insensitive): `0`, `false`,
`no`, `off` — so `FALSE`, `Off`, `NO` etc. all work. Any other value
(including unset) leaves the gate active.
(including unset) leaves the corresponding behaviour active.
</Callout>

<Callout type="info">
Expand Down
59 changes: 57 additions & 2 deletions libs/cua-driver-rs/crates/platform-macos/src/permissions/gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,28 @@ pub fn run_if_needed(opts: GateOpts) -> Result<()> {
}

let missing = missing_from_status(initial);
print_banner(&missing, opts.open_settings);

// Phase 1: try to present a native NSPanel before falling back to
// the terminal banner. The presentation result tells us how to
// sequence the rest of the flow:
//
// * `NotShown` — historical CLI path: print the banner, then auto-
// open Settings (when `open_settings` is true).
// * `ShownOpenSettings` — the user clicked the primary button; we
// auto-open Settings on their behalf and skip the banner.
// * `ShownDismissed` — the user clicked "Continue anyway" or the
// red dot; we skip both the banner AND the auto-open since the
// user explicitly declined the guided flow. They can still
// grant in their own time and the polling loop will pick it up.
let presentation = present_panel_if_available(&missing);
let should_auto_open_settings = match presentation {
PanelPresentation::NotShown => {
print_banner(&missing, opts.open_settings);
opts.open_settings
}
PanelPresentation::ShownOpenSettings => opts.open_settings,
PanelPresentation::ShownDismissed => false,
};

if opts.also_raise_prompts {
// These are no-ops when the grant is already active and are the
Expand All @@ -231,7 +252,7 @@ pub fn run_if_needed(opts: GateOpts) -> Result<()> {
}
}

if opts.open_settings {
if should_auto_open_settings {
// Open *both* missing panes up front. System Settings collapses
// duplicate-open requests to a single navigation, so this isn't
// disruptive even when only one grant is needed.
Expand All @@ -245,6 +266,40 @@ pub fn run_if_needed(opts: GateOpts) -> Result<()> {
wait_for_grants(&opts)
}

/// Outcome of a panel-present attempt. Drives the subsequent flow in
/// [`run_if_needed`] — see comments at the call site.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PanelPresentation {
/// Panel could not be shown (opt-out env var, bare-binary launch,
/// headless, etc.). Caller should fall back to the terminal banner.
NotShown,
/// Panel shown; user clicked "Open System Settings".
ShownOpenSettings,
/// Panel shown; user clicked "Continue anyway" or closed the window.
ShownDismissed,
}

fn present_panel_if_available(missing: &[MissingPermission]) -> PanelPresentation {
#[cfg(target_os = "macos")]
{
use crate::permissions::panel;
if !panel::panel_enabled() {
return PanelPresentation::NotShown;
}
match panel::show_modal(panel::PanelOpts {
missing: missing.to_vec(),
}) {
panel::PanelOutcome::OpenSettings => PanelPresentation::ShownOpenSettings,
panel::PanelOutcome::Dismissed => PanelPresentation::ShownDismissed,
}
}
#[cfg(not(target_os = "macos"))]
{
let _ = missing;
PanelPresentation::NotShown
}
}

/// Block until all required permissions are granted or the deadline
/// elapses. Emits a status line every `opts.status_interval` while
/// waiting so the user has feedback.
Expand Down
14 changes: 10 additions & 4 deletions libs/cua-driver-rs/crates/platform-macos/src/permissions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@
//! - [`gate`] — startup-time interactive flow that walks the user through
//! granting the missing permissions before `serve` binds.
//!
//! The gate is a Rust port of Swift's `PermissionsGate` (SwiftUI panel),
//! re-implemented as a terminal-only flow so the Rust port does not pull in
//! a GUI framework. See `gate.rs` for the rationale + UX shape.
//! The gate is a Rust port of Swift's `PermissionsGate` (SwiftUI panel).
//! Two presentation surfaces:
//! - native NSPanel (`panel`, Phase 1+) — used when the daemon is launched
//! from the bundled `.app` and the env-var opt-out is not set;
//! - terminal banner (`gate::wait_for_grants`) — fallback for bare-binary
//! invocations, headless environments, CI, and explicit opt-outs.
//!
//! Mirrors `libs/cua-driver/Sources/CuaDriverCore/Permissions/`:
//! - `Permissions.swift` → `permissions::status`
//! - `PermissionsGate.swift` → `permissions::gate` (CLI-only)
//! - `PermissionsGate.swift` → `permissions::gate` + `permissions::panel`

pub mod gate;
pub mod status;

#[cfg(target_os = "macos")]
pub mod panel;

pub use status::{PermissionsStatus, current_status};
pub use gate::{GateOpts, MissingPermission, run_if_needed};
Loading
Loading