Skip to content

feat(cua-driver): embedded mode — inherit the host app's TCC grants, never prompt#2102

Merged
f-trycua merged 4 commits into
trycua:mainfrom
injaneity:feat/cua-driver-embedded-mode
Jul 6, 2026
Merged

feat(cua-driver): embedded mode — inherit the host app's TCC grants, never prompt#2102
f-trycua merged 4 commits into
trycua:mainfrom
injaneity:feat/cua-driver-embedded-mode

Conversation

@injaneity

@injaneity injaneity commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Adds an explicit embedded mode to cua-driver-rs so a host macOS app (an agent harness) can run the driver as a child process and have it inherit the host's Accessibility + Screen Recording grants — one permission prompt total, one entry in System Settings, no com.trycua.driver dialogs. Activated via CUA_DRIVER_EMBEDDED=1 or cua-driver mcp --embedded [--host-bundle-id <id>].

Why

Standalone cua-driver deliberately makes itself its own TCC responsible process (disclaim re-exec + open -a CuaDriver relaunch) so grants attach to a stable bundle identity. That's right for standalone, wrong for embedding: a harness that already holds both grants ships a second app identity and its users hit a second set of permission prompts. macOS attributes TCC to the responsible process, so a directly-spawned child inherits the host's grants for free — the driver just has to stop opting out.

What changed

Behavior (~25 lines, all gated on the flag; standalone is unchanged when it's off):

  • responsibility.rs — skip the disclaim re-exec (stay in the host's chain). Decision factored into a pure should_skip_disclaim() seam for testability.
  • cli.rs should_use_daemon_proxy — never auto-relaunch via the CuaDriver.app bundle (both platform branches; an embedded driver answers in-process).
  • Permissions gate (gate.rs) — embedded mode opts out; the driver never raises its own prompts or panel.
  • check_permissionsprompt:true is ignored (this + the gate are the only two request_* call sites, so prompts are unreachable), and permission_source() reports attribution: "host" with the advisory host bundle id and a disclaim_env debug field. The live SCShareableContent capture probe is untouched.

Docs + example:

  • Skills/cua-driver/EMBEDDING.md — integration guide: TCC responsibility model, launch contract, standalone-vs-embedded table, check_permissions attribution semantics, copy-paste host example, troubleshooting (second prompt / black screenshot / empty AX / re-sign), Windows/Linux notes. Ships in the skill pack, so it links by repo path, not relative links.
  • examples/embedded-host-macos/ExampleAgentHarness.swift, a 122-line no-UI reference host mirrored verbatim in the guide (grants → embedded spawn → attribution check → background screenshot → background AX read → agent-cursor glide), plus a self-verifying demo.sh (build + sign + tccutil reset + tail-until-DEMO COMPLETE: PASS).
  • --embedded / --host-bundle-id added to --help, manifest, and dump-docs; cli-reference.mdx regenerated via scripts/docs-generators/cua-driver.ts (no hand edits to generated docs).

Security notes

  • The env var is caller-controlled, and that is safe by construction: embedded mode only ever removes capability claims (suppresses prompting, downgrades attribution to host). It never feeds the driver-daemon attribution decision, which stays keyed on the non-spoofable bundle-path signal. New tests pin this, including embedded + disclaim env set together never yielding driver-daemon.
  • Only the exact value 1 enables the mode (fail-safe); --host-bundle-id is an advisory label, not a trust signal — trust comes from the OS responsibility chain.
  • The host must spawn the driver directly (Process/posix_spawn); launching via open(1)/NSWorkspace breaks inheritance. Documented, with the TCC AttributionChain log-stream command for debugging.

Testing

  • 20/20 unit tests pass (release profile): gate opt-out incl. exact-value rule, host attribution, fail-closed spoofing cases, disclaim truth table. Env-mutating tests across gate and check_permissions now share one crate-wide lock (they race under separate per-module mutexes).
  • Example host + the guide's copy-paste block both compile with swiftc; verified byte-identical to each other.
  • End-to-end one-grant demo verified on a real Mac (macOS Darwin 25.5, TCC reset via tccutil): host granted once, embedded driver started with zero driver-side prompts, check_permissions reported attribution: host with the live capture probe true, background Finder AX tree + window screenshot returned, agent-cursor overlay glided, DEMO COMPLETE: PASS. Settings listed only the harness — no CuaDriver entry. Still not CI-able (needs interactive grants).
  • The manual run surfaced and fixed real issues: macOS 26 only registers an app in the Screen Recording pane after an actual ScreenCaptureKit attempt (the embedded driver's live probe is that attempt, attributed to the host — so the demo registers both rows in one pass); ad-hoc re-signing orphans grant rows (demo.sh now reuses the built bundle and skips tccutil reset on re-runs); get_window_state needs pid+window_id via launch_app; stock macOS has no timeout(1).

Not in this PR

  • Nothing further is needed for Windows/Linux (X11) embedding: those platforms have no per-app permission grants, so a directly-spawned driver child inherits everything by plain process inheritance, and the flag already keeps it in-process. Two documented exceptions remain out of scope: driving elevated/UWP windows from an embedded host on Windows (embedded mode does not route through the uiAccess cua-driver-uia worker — the host must manage it), and Wayland capture (XDG portals prompt per-session and cannot be pre-granted; X11 is the supported path).
  • CI automation of the TCC demo (requires interactive grant).

Summary by CodeRabbit

  • New Features

    • Added support for embedded mode in cua-driver, including a host bundle ID label and updated permission behavior for host-owned grants.
    • Added a macOS embedding guide and an end-to-end example host app for running the driver inside another app.
    • Expanded the macOS demo script to build, sign, run, and verify the embedded setup.
  • Documentation

    • Updated CLI reference and README links to cover the new embedded workflow and command flags.

@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

@injaneity 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 Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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: 89fbc138-adbb-43dd-bb6e-c109e2b495e2

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 adds an "embedded mode" to cua-driver allowing it to run inside a macOS host app and inherit the host's Accessibility and Screen Recording TCC grants. Changes span core config constants, CLI flags/docs, responsibility disclaim logic, permissions gate opt-out, check_permissions attribution, an example Swift host harness with demo script, and documentation.

Changes

Embedded mode implementation

Layer / File(s) Summary
Core embedded-mode constants
libs/cua-driver/rust/crates/cua-driver-core/src/lib.rs
Adds EMBEDDED_ENV, HOST_BUNDLE_ID_ENV constants and embedded_mode() helper checking for exact value "1".
CLI flags, help, manifest, docs JSON
libs/cua-driver/rust/crates/cua-driver/src/cli.rs
Adds --embedded/--host-bundle-id parsing, env export, help text, manifest and cli_docs_json entries; short-circuits should_use_daemon_proxy to false when embedded.
Responsibility disclaim skip logic
libs/cua-driver/rust/crates/cua-driver/src/responsibility.rs
Adds should_skip_disclaim helper and updates reexec_disclaimed_if_needed to skip when embedded; adds unit test.
Permissions gate opt-out
libs/cua-driver/rust/crates/platform-macos/src/permissions/gate.rs, .../permissions/mod.rs
Extends gate opt-out condition to include embedded_mode(); refactors test env locking via shared test_env_lock(); adds tests.
check_permissions attribution
libs/cua-driver/rust/crates/platform-macos/src/tools/check_permissions.rs
Returns attribution: "host" and embedded: true in embedded mode, adds disclaim_env field, suppresses prompting, adds host attribution summary note, reworks tests.
Example macOS host and demo
libs/cua-driver/rust/examples/embedded-host-macos/*
Adds Swift harness that spawns cua-driver as a direct embedded child over JSON-RPC and verifies attribution/screenshots/AX tree, plus a demo.sh build/run/verify script and .gitignore.
Documentation
libs/cua-driver/rust/Skills/cua-driver/EMBEDDING.md, .../README.md, docs/content/docs/reference/cua-driver/cli-reference.mdx
Adds embedding guide, updates reading order, and documents --embedded/--host-bundle-id in CLI reference.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Host as Host App
  participant Driver as cua-driver (embedded)
  participant Tool as CheckPermissionsTool

  Host->>Driver: spawn as direct child (CUA_DRIVER_EMBEDDED=1)
  Host->>Driver: initialize / notifications/initialized
  Host->>Driver: check_permissions(prompt=true)
  Driver->>Tool: invoke()
  Tool->>Tool: embedded_mode() true, suppress prompt
  Tool->>Tool: permission_source() -> attribution "host"
  Tool-->>Driver: source.attribution=host, embedded=true
  Driver-->>Host: JSON-RPC response
  Host->>Driver: launch_app, get_window_state, move_agent_cursor
  Driver-->>Host: screenshots, AX tree, cursor moves
Loading

Possibly related PRs

  • trycua/cua#1738: Both PRs modify cli_docs_json/manifest generation in cli.rs, with this PR adding embedded/host-bundle-id fields to that same machinery.
  • trycua/cua#1765: Both PRs directly modify permission_source()/invoke() in check_permissions.rs to change structured attribution output and prompting behavior.
  • trycua/cua#1956: Both PRs modify the macOS responsibility disclaimer flow (responsibility.rs) and its effect on check_permissions attribution.

Poem

One flag, one grant, no double ask,
the host now shares its TCC mask 🐰
Hop in, dear driver, stdio wide,
no second prompt, no daemon ride.
Cursor glides, the tree unfurls,
PASS logged loud — this bunny twirls! 🥕✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: embedded mode to inherit host TCC grants and avoid prompts.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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.

@injaneity injaneity force-pushed the feat/cua-driver-embedded-mode branch from 7ec28d2 to bd110a4 Compare July 2, 2026 08:40
@injaneity injaneity marked this pull request as ready for review July 2, 2026 08:44
Adds explicit embedded mode (CUA_DRIVER_EMBEDDED=1 / --embedded
[--host-bundle-id <id>]) so a host app (e.g. OpenClaw) can embed the
driver as a direct child and have macOS attribute Accessibility +
Screen Recording to the host it already trusted — one grant, one
Settings entry, no com.trycua.driver prompts.

In embedded mode the driver:
- skips the responsibility-disclaim re-exec (stays in the host's chain)
- skips the daemon auto-relaunch via the CuaDriver.app bundle
- opts out of the startup permissions gate and never calls
  request_accessibility / request_screen_recording (check_permissions
  ignores prompt=true)
- reports check_permissions source.attribution = "host" with the
  advisory host bundle id, keeping the live SCShareableContent probe

Fail-closed property preserved: the caller-controlled env var only ever
downgrades attribution (host, never driver-daemon); daemon attribution
stays keyed on the non-spoofable bundle-path signal. Standalone
behavior is unchanged when the flag is off.

Ships Skills/cua-driver/EMBEDDING.md (integration guide +
troubleshooting) and examples/embedded-host-macos/ (signed AppKit
reference host + one-grant demo script).
@injaneity injaneity force-pushed the feat/cua-driver-embedded-mode branch from bd110a4 to 826fbef Compare July 2, 2026 08:49

@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: 2

🧹 Nitpick comments (2)
libs/cua-driver/rust/Skills/cua-driver/EMBEDDING.md (1)

149-295: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Manual doc/code sync has no automated guardrail.

This code block is a manual mirror of ExampleAgentHarness.swift, kept in sync only by comment convention on both sides. Consider a small CI check that extracts this fenced block and diffs it against the actual source file, so drift fails the build instead of silently accumulating.

🤖 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/Skills/cua-driver/EMBEDDING.md` around lines 149 - 295,
The EMBEDDING.md mirrored ExampleAgentHarness.swift block can drift from the
real source because there is no automated sync check. Add a small CI guardrail
that extracts the fenced “Minimal host example” block from EMBEDDING.md and
diffs it against ExampleAgentHarness.swift, so any mismatch fails the build;
keep the check keyed to the shared ExampleAgentHarness reference and the
markdown block itself.
libs/cua-driver/rust/examples/embedded-host-macos/ExampleAgentHarness.swift (1)

79-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

call() silently swallows JSON-RPC error responses.

If the driver returns a JSON-RPC error instead of result (e.g. a bad tool call), call() returns [:] with no indication of what failed. Given this harness exists specifically to surface embedding problems, logging the error payload would make failures far easier to diagnose.

♻️ Suggested diff
     while true {
         let msg = readMessage()
         if msg["id"] as? Int == nextId {
-            return msg["result"] as? [String: Any] ?? [:]
+            if let error = msg["error"] as? [String: Any] {
+                log("RPC error for \(tool): \(error)")
+            }
+            return msg["result"] as? [String: Any] ?? [:]
         }
     }
🤖 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/examples/embedded-host-macos/ExampleAgentHarness.swift`
around lines 79 - 89, The call(_:_:) helper currently ignores JSON-RPC error
responses and returns an empty dictionary, which hides tool-call failures.
Update ExampleAgentHarness.call to inspect each matching message for an error
payload as well as result, and when tools/call returns an error, log or print
the error details before continuing or failing the call path. Use the existing
call, readMessage, and send flow to surface the JSON-RPC error information
instead of silently returning [:].
🤖 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/examples/embedded-host-macos/demo.sh`:
- Around line 73-76: The wait loop in the demo harness is watching for a log
string that never appears, so it can’t exit early on the missing-grant branch.
Update the grep condition in the loop around the demo completion check to match
the actual harness log text emitted for missing items (the “grant the missing
item(s)...” message), while still preserving the existing DEMO COMPLETE exit
path. Locate this in the shell loop that polls "$LOG" so the early break
triggers for both completion and missing-grant cases.

In `@libs/cua-driver/rust/examples/embedded-host-macos/ExampleAgentHarness.swift`:
- Around line 1-15: Add the required SPDX license header to ExampleAgentHarness
so CI recognizes the file’s license metadata; place the SPDX identifier at the
top of the file before the existing introductory comments, and keep the
EMBEDDING.md mirrored example content unchanged otherwise.

---

Nitpick comments:
In `@libs/cua-driver/rust/examples/embedded-host-macos/ExampleAgentHarness.swift`:
- Around line 79-89: The call(_:_:) helper currently ignores JSON-RPC error
responses and returns an empty dictionary, which hides tool-call failures.
Update ExampleAgentHarness.call to inspect each matching message for an error
payload as well as result, and when tools/call returns an error, log or print
the error details before continuing or failing the call path. Use the existing
call, readMessage, and send flow to surface the JSON-RPC error information
instead of silently returning [:].

In `@libs/cua-driver/rust/Skills/cua-driver/EMBEDDING.md`:
- Around line 149-295: The EMBEDDING.md mirrored ExampleAgentHarness.swift block
can drift from the real source because there is no automated sync check. Add a
small CI guardrail that extracts the fenced “Minimal host example” block from
EMBEDDING.md and diffs it against ExampleAgentHarness.swift, so any mismatch
fails the build; keep the check keyed to the shared ExampleAgentHarness
reference and the markdown block itself.
🪄 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: 7e2966c5-b66e-4753-8555-bc5f962dff3c

📥 Commits

Reviewing files that changed from the base of the PR and between 73fe822 and bd110a4.

📒 Files selected for processing (12)
  • docs/content/docs/reference/cua-driver/cli-reference.mdx
  • libs/cua-driver/rust/Skills/cua-driver/EMBEDDING.md
  • libs/cua-driver/rust/Skills/cua-driver/README.md
  • libs/cua-driver/rust/crates/cua-driver-core/src/lib.rs
  • libs/cua-driver/rust/crates/cua-driver/src/cli.rs
  • libs/cua-driver/rust/crates/cua-driver/src/responsibility.rs
  • libs/cua-driver/rust/crates/platform-macos/src/permissions/gate.rs
  • libs/cua-driver/rust/crates/platform-macos/src/permissions/mod.rs
  • libs/cua-driver/rust/crates/platform-macos/src/tools/check_permissions.rs
  • libs/cua-driver/rust/examples/embedded-host-macos/.gitignore
  • libs/cua-driver/rust/examples/embedded-host-macos/ExampleAgentHarness.swift
  • libs/cua-driver/rust/examples/embedded-host-macos/demo.sh

Comment thread libs/cua-driver/rust/examples/embedded-host-macos/demo.sh
@injaneity injaneity force-pushed the feat/cua-driver-embedded-mode branch from 2cb4061 to 71272fc Compare July 6, 2026 08:27
@f-trycua f-trycua merged commit b654f27 into trycua:main Jul 6, 2026
13 of 67 checks passed
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.

2 participants