How Cua Driver works
Observation, targets, the background-first action ladder, verification, choosing an integration, who owns the runtime, and the optional perception extension.
Cua Driver is the UI layer of a computer-use agent. The harness runs the model and the loop; Cua Driver turns each proposed step into an observation or an action on a real desktop, and reports what happened.
model -> agent harness -> Cua Driver (MCP, CLI, or SDK) -> OS accessibility + input -> appsObserve: tree and pixels together#
get_window_state(pid, window_id) returns the window's accessibility tree and
a screenshot in one call. The tree says what is actionable (roles, labels, an
element_token per element); the screenshot says which
one, and shows what the tree omits or gets wrong. Pass
include_screenshot: false to skip the image when you only need to re-index.
get_desktop_state captures the whole primary display.
When neither the tree nor typed browser state exposes the target, the optional perception extension can parse that same screenshot into labelled regions.
Act: one exact target per call#
Every input tool (click, type_text, press_key, hotkey, scroll,
drag, move_cursor) names its target:
| Target | Coordinates | Delivery |
|---|---|---|
{kind: "window", pid, window_id} | Elements from that window's snapshot, or pixels in its screenshot | background (default) or foreground |
{kind: "desktop", display_id: "primary"} | Screen pixels from get_desktop_state | Foreground only |
The target is chosen per call; nothing is locked into a session. A malformed or ambiguous target fails before anything is sent.
The action ladder#
Background delivery means the driver does not raise the window, move the real pointer, or switch the frontmost app. The agent gets its own cursor overlay.
- Element, background. Act by
element_token: UI Automation Invoke on Windows,AXPerformActionon macOS, AT-SPI actions on Linux. This is the only rung the driver can verify itself. - Pixel, background. Act by
x, yread from the same screenshot. For keyboard tools,x, yfirst clicks to give the field renderer focus, which is how to type into Chromium and Electron inputs. - Page. For browser tabs, switch to the browser tools, which act on the DOM over CDP without focus.
- Foreground. Retry only that action with
delivery_mode: "foreground": the window is raised, the input lands, and the previous app is restored. Needed for games, canvas apps such as Blender, and focus-polling apps.
Each response says whether to climb:
| Field | Values |
|---|---|
effect | confirmed (read back through accessibility), unverifiable, suspected_noop, partial, refused |
escalation | {recommended: "px" | "page" | "foreground", reason} when a next rung exists |
verified | true only for accessibility read-back |
A delivered event is not an applied change: Electron, Catalyst, and web
content can echo a write they did not apply, so the driver reports those as
unverifiable and recommends the next rung. After anything short of
confirmed, observe again and check. Refusals carry a code and a recovery
hint; stale_element_token means snapshot again, not that the element route is
broken.
Platform mechanisms#
| Semantic route | Background input and capture | Main limit | |
|---|---|---|---|
| macOS | Accessibility API | Scoped CoreGraphics and SkyLight events; ScreenCaptureKit per window | Off-Space SwiftUI windows lose their tree; needs TCC grants |
| Windows | UI Automation | Window messages to the target HWND | The daemon must run in the interactive session, not Session 0 |
| Linux X11 | AT-SPI | Window-addressed X11 input and capture | Some toolkits reject synthetic background events |
| Linux Wayland | AT-SPI | Compositor-specific; no general raw input to other windows | Raw background keys refuse; use AT-SPI, XWayland, or foreground |
On Wayland, knowing where a window is does not change which window the
compositor's seat sends input to, so the driver refuses rather than risk typing
into the wrong app. A structured background_unavailable is part of the
contract, not a failure to hide.
Choose an integration#
Pick by who owns the runtime: the process that holds the OS permissions, sessions and browser connections.
| You are building | Use | Runtime owner |
|---|---|---|
| An agent or harness (Claude Code, Codex, Cursor, …) | MCP: cua-driver mcp | The MCP process; on macOS, the CuaDriver.app daemon |
| An agent that can only run shell commands | CLI: cua-driver call <tool> | The cua-driver serve daemon |
| An app with built-in computer use | SDK: CuaDriver.create() | On macOS, the CuaDriver.app daemon; elsewhere your app process |
| A signed desktop app that serves an external agent | App-hosted service | The permission-owning desktop app |
| Several clients sharing one desktop | cua-driver serve --socket <path> plus cua-driver mcp --socket <path> | The daemon |
Inside a Cua Sandbox you do not install Cua Driver: the sandbox's guest service bundles its tools.
Processes and sessions#
The runtime owns permissions, sessions, element caches, recordings, and browser connections. Where it lives depends on how you connect:
| Entry point | Runtime |
|---|---|
CuaDriver.create() | Inside your process |
create_private_worker() | A supervised child over inherited pipes |
cua-driver mcp on Windows and Linux | The MCP process itself (exits on stdin EOF) |
cua-driver mcp on macOS | Proxies to the CuaDriver.app daemon (--direct to own it instead) |
cua-driver serve + mcp --socket / call / connect() | A long-lived daemon |
The daemon exists for identity. On macOS, Accessibility and Screen Recording
grants belong to CuaDriver.app (com.trycua.driver), not to whatever
terminal spawned a cua-driver process. On Windows, an SSH process in Session
0 cannot see the desktop, so a daemon in the user's session does the work.
Each MCP connection or SDK runtime gets one implicit session, reused by
unnamed calls and cleaned up when the connection closes, on end_session, or
after five idle minutes. After idle expiry, the next unnamed call starts a new
implicit session, and element tokens and snapshots from the ended one stay
invalid, so observe again before acting. An ended named session refuses calls
with session_ended until start_session restarts it. Pass a session label on each call for a readable
name and a cursor badge; the label is not a credential. One-shot
cua-driver call commands each get a disposable session. All sessions still
share one screen, keyboard, and focus, and native input is admitted one action
at a time.
The optional HTTP MCP listener is off unless CUA_DRIVER_RS_MCP_HTTP_PORT is
set, and then requires CUA_DRIVER_RS_MCP_HTTP_TOKEN (32 to 4096 characters,
sent as a bearer token). Windows named pipes accept only the daemon owner's
user.
Permissions sit below every transport#
CLI, MCP, SDK, and daemon calls all pass the same authorization check inside the runtime. The launcher fixes the mode, manifest, and grants at startup; an agent cannot widen them. See Set permissions.
The optional perception extension#
Canvas apps, custom-drawn views, games, remote desktops, and some web pages
have no useful accessibility tree or page structure, so an agent has nothing to
address by role or label. The optional cua-perception extension gives those
screens a structure: it parses one screenshot into labelled regions (text,
icons, controls) on your own machine, without sending the screenshot to a
remote model. Cua Driver then acts on exactly the region the agent chose.
It never replaces the tree and never captures on its own. Without it installed,
parse_visual_regions returns not_installed, and agents continue with the
accessibility tree or page structure. To run the loop step by step, see
Parse visual regions.
Every action is bound to one screenshot#
A region only means something on the screen it was found on, so Cua Driver ties each perception-based action to its capture:
| Situation | Result |
|---|---|
The click carries the capture_id of the screenshot the region came from | Delivered |
| The same capture is reused for another action | Refused with capture_not_found |
| The capture is older than 60 seconds | Refused with capture_expired |
Agents receive region bounds and text, never screenshot bytes, and Cua Driver keeps the authority to capture and to act. A stale or reused screen is refused instead of guessed at.
What it contains#
Each release packs local models and a CPU runtime, so parsing needs no GPU and no network access.
| Component | Role | License |
|---|---|---|
| OmniParser icon detector | Finds icons and controls | AGPL-3.0-only |
| PP-OCR detector and recognizer | Finds and reads on-screen text | Apache-2.0 |
| ONNX Runtime (CPU) | Runs the models locally | Shipped with its own notices |
Releases are published as cua-perception-v<version> GitHub releases for macOS
arm64, Linux x64, and Windows x64. Each target has a signed catalog, the
archive, an SBOM, provenance, a runtime contract, and an install check, plus
one SHA256SUMS. Cua Driver verifies the signed catalog before it installs
anything, and a verified install reports publisher-verified.
The extension is not MIT licensed. Its OmniParser icon detector is AGPL-3.0-only, and Cua does
not relicense it. Installing the extension does not change Cua Driver's MIT license, but
redistributing the extension, or offering it to users over a network (for example, inside a
hosted product), can require you to provide the AGPL corresponding source. If your organization
does not accept AGPL components, skip the extension: Cua Driver keeps working, and
parse_visual_regions returns not_installed. This is not legal advice. See the third-party
notices and
precautions.
Install and speed#
Capture, parsing, Cua Driver startup, and update checks never install the
extension. You inspect a catalog first, then install, check, update, or remove
the extension with cua-driver extension commands. A running Driver picks up
an install or removal without a restart.
Parsing runs on the CPU. Measured per screenshot on the canonical harnesses: about 2 to 2.5 seconds on macOS, 3.5 to 4 seconds on Linux, and 8 to 9 seconds on Windows. Prefer accessibility or page targets when they exist, and keep perception for surfaces that expose nothing addressable.
How it is validated#
A tool returning ok is not evidence. Each supported behavior is a cell in one
Rust catalog (action × element/pixel × background/foreground × window/desktop
× surface), run against source-built fixture apps in a real desktop session.
A cell passes only when state the app or desktop owns changed, and, for
background cells, when focus, z-order, the real cursor, and the foreground app
stayed untouched. An exact refusal also passes; a silent success does not.
Runs keep per-cell video and logs tied to the source commit. Current results
are in Platform support.