Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c7e68a6
test(driver): run element-token protocol through daemon
Jul 18, 2026
7d14a71
test(driver): model capture scope protocol gates
Jul 18, 2026
ccc5d98
test(driver): record compositor canaries after setup
Jul 18, 2026
70bc104
test(driver): drain sentinel setup input
Jul 18, 2026
b18bbe1
docs(driver): regenerate daemon-backed references
Jul 18, 2026
4f13057
test(driver): keep compositor sentinel responsive
Jul 18, 2026
41a1da9
test(driver): assert daemon policy error envelope
Jul 18, 2026
ddbff5d
test(driver): drive compositor heartbeat over IPC
Jul 18, 2026
779f237
test(driver): probe Windows daemon over named pipe
Jul 18, 2026
7f447aa
fix(driver): wait for real DevTools endpoint
Jul 18, 2026
23764ee
fix(driver): stabilize daemon-backed E2E contracts
Jul 18, 2026
0484421
test(driver): isolate compositor heartbeat loop
Jul 18, 2026
790754a
fix(driver): repair remaining hosted E2E regressions
Jul 18, 2026
b02c261
test(driver): use supported compositor leak canary
Jul 18, 2026
97abfef
fix(driver): restore Chromium approval endpoint
Jul 18, 2026
c94a0e2
fix(driver): target nested compositor windows by pid
Jul 18, 2026
0d20e27
fix(driver): use explicit pid for compositor input
Jul 18, 2026
0a13208
fix(driver): target compositor process families
Jul 18, 2026
d54102c
fix(driver): bound compositor recording snapshots
Jul 18, 2026
d1df026
fix(driver): avoid compositor recording re-resolution
Jul 18, 2026
12bb33a
fix(driver): preserve focused compositor keyboard delivery
Jul 18, 2026
533d107
fix(driver): avoid redundant compositor focus request
Jul 18, 2026
3244be3
test(driver): require renderer heartbeat in compositor E2E
Jul 18, 2026
fa20214
fix(driver): map compositor sentinel before page load
Jul 18, 2026
fdc4022
fix(driver): avoid headless activation configure
Jul 18, 2026
6bbac10
fix(driver): configure compositor activation once
Jul 18, 2026
635735d
fix(driver): pair initial compositor activation
Jul 18, 2026
ba10f98
fix(driver): defer headless seat focus until activation
Jul 18, 2026
620b264
fix(driver): attach virtual compositor keyboard
Jul 18, 2026
f19b65d
Merge remote-tracking branch 'origin/main' into codex/post-merge-daem…
Jul 18, 2026
13b62dc
fix(driver): restore compositor state evidence
Jul 19, 2026
990d8da
fix(driver): complete Wayland browser routing
Jul 19, 2026
1258e14
fix(driver): complete Chromium Wayland protocols
Jul 19, 2026
542f9b5
fix(driver): avoid double-counting Chromium document offsets
Jul 19, 2026
2aaa3ab
fix(driver): focus desktop compositor pointer events
Jul 19, 2026
b5f0702
fix(driver): preserve background child-window focus
Jul 19, 2026
161bc60
fix(driver): declare compositor toplevel type
Jul 19, 2026
8ab163a
fix(driver): declare compositor focus helper
Jul 19, 2026
6acd882
fix(driver): fall back for inert Edge consent action
Jul 19, 2026
f48ac5e
test(driver): retry transient sentinel startup
Jul 19, 2026
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
10 changes: 5 additions & 5 deletions docs/content/docs/reference/cua-driver/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ Print a tool's full description and JSON input schema.

### `cua-driver call`

Invoke an MCP tool directly from the shell.
Invoke an MCP tool through the running daemon.

Sends the tool request to the required Cua Driver daemon. JSON arguments may be passed as a positional JSON object or through stdin. If the daemon is unavailable, the command fails; it never executes the tool in the CLI process.
Requires a Cua Driver daemon. JSON arguments may be passed as a positional JSON object or through stdin.

**Arguments:**

Expand All @@ -62,9 +62,9 @@ Sends the tool request to the required Cua Driver daemon. JSON arguments may be

### `cua-driver mcp`

Run the stdio MCP server.
Run the daemon-backed stdio MCP proxy.

Every MCP process is a stdio proxy to a Cua Driver daemon. On macOS it can auto-launch the CuaDriver.app daemon so TCC grants attach to the bundle. On Windows and Linux, the daemon must already be running.
Every MCP tool call is forwarded to a Cua Driver daemon. On macOS the proxy can auto-launch CuaDriver.app; on Windows and Linux the daemon must already be running.

**Options:**

Expand Down Expand Up @@ -140,7 +140,7 @@ Supported clients include claude, codex, cursor, antigravity, openclaw, opencode

Control trajectory recording on a running daemon.

Recording state lives in the daemon and is shared across daemon-backed clients.
Recording state lives in the required daemon and survives client reconnects.

**Options:**

Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/reference/cua-driver/mcp-tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ Turn folders are named `turn-00001/`, `turn-00002/`, etc. Turn numbering restar

**Windows + Linux use an ffmpeg subprocess** (`gdigrab` / `x11grab` + libx264). Requires ffmpeg on PATH (winget install Gyan.FFmpeg / apt install ffmpeg); when ffmpeg is missing or fails on startup the per-turn capture (screenshots + action.json) still runs and the session's `last_error` field carries the diagnostic.

State persists for the life of the daemon / MCP session; a restart resets to disabled with no on-disk state. Call `stop_recording` to disable + finalize the mp4.
State persists for the life of the daemon; a restart resets to disabled with no on-disk state. Call `stop_recording` to disable + finalize the mp4.

**Arguments:**

Expand Down
46 changes: 22 additions & 24 deletions libs/cua-driver/rust/crates/cua-driver-core/src/page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ pub struct ClickElementResult {
pub trait PageBackend: Send + Sync {
/// Returns the visible text of the page (rough analog of
/// `document.body.innerText`).
async fn get_text(&self, pid: i32, window_id: u32) -> anyhow::Result<String>;
async fn get_text(&self, pid: i32, window_id: u64) -> anyhow::Result<String>;

/// Find elements matching `css_selector` and return a formatted-text
/// response (same human-readable shape macOS already emits).
async fn query_dom(
&self,
pid: i32,
window_id: u32,
window_id: u64,
css_selector: &str,
attributes: &[String],
) -> anyhow::Result<String>;
Expand All @@ -68,7 +68,7 @@ pub trait PageBackend: Send + Sync {
async fn execute_javascript(
&self,
pid: i32,
window_id: u32,
window_id: u64,
javascript: &str,
) -> anyhow::Result<String>;

Expand All @@ -80,7 +80,7 @@ pub trait PageBackend: Send + Sync {
async fn execute_javascript_targeted(
&self,
pid: i32,
window_id: u32,
window_id: u64,
javascript: &str,
cdp_port: Option<u16>,
target_url_contains: Option<&str>,
Expand Down Expand Up @@ -122,7 +122,7 @@ pub trait PageBackend: Send + Sync {
async fn click_element(
&self,
_pid: i32,
_window_id: u32,
_window_id: u64,
_selector: &str,
) -> anyhow::Result<ClickElementResult> {
anyhow::bail!(
Expand Down Expand Up @@ -162,7 +162,7 @@ pub trait PageBackend: Send + Sync {
async fn type_keystrokes(
&self,
_pid: i32,
_window_id: u32,
_window_id: u64,
_text: &str,
_cdp_port: Option<u16>,
_target_url_contains: Option<&str>,
Expand Down Expand Up @@ -191,7 +191,7 @@ pub trait PageBackend: Send + Sync {
async fn insert_text(
&self,
_pid: i32,
_window_id: u32,
_window_id: u64,
_text: &str,
_cdp_port: Option<u16>,
_target_url_contains: Option<&str>,
Expand Down Expand Up @@ -327,27 +327,24 @@ impl Tool for PageTool {
// `pid` / `window_id` are resolved per-action: every action except
// `enable_javascript_apple_events` needs both. We resolve once here
// so each arm can `?` on the Result and we get matching error text.
// Narrowing casts use `TryFrom` so out-of-range JSON numbers fail
// with an actionable error instead of silently truncating to the
// wrong process / window.
// PID narrowing uses `TryFrom` so out-of-range JSON numbers fail
// instead of silently truncating. Window IDs remain u64 because
// native Wayland accessibility providers can legitimately exceed u32.
let resolve_pid = |args: &Value| -> Result<i32, String> {
let raw = args
.get("pid")
.and_then(|v| v.as_i64())
.ok_or_else(|| "Missing required parameter: pid".to_owned())?;
i32::try_from(raw).map_err(|_| format!("Invalid parameter: pid {raw} out of i32 range"))
};
let resolve_window_id = |args: &Value| -> Result<u32, String> {
let raw = args
.get("window_id")
let resolve_window_id = |args: &Value| -> Result<u64, String> {
args.get("window_id")
.and_then(|v| v.as_u64())
.ok_or_else(|| "Missing required parameter: window_id".to_owned())?;
u32::try_from(raw)
.map_err(|_| format!("Invalid parameter: window_id {raw} out of u32 range"))
.ok_or_else(|| "Missing required parameter: window_id".to_owned())
};

let (pid, window_id) = if action == "enable_javascript_apple_events" {
(0i32, 0u32) // unused
(0i32, 0u64) // unused
} else {
let pid = match resolve_pid(&args) {
Ok(v) => v,
Expand Down Expand Up @@ -533,7 +530,7 @@ mod tests {
use super::*;
use std::sync::Mutex;

type TargetedCall = (i32, u32, String, Option<u16>, Option<String>);
type TargetedCall = (i32, u64, String, Option<u16>, Option<String>);

#[derive(Default)]
struct RecordingBackend {
Expand All @@ -542,14 +539,14 @@ mod tests {

#[async_trait]
impl PageBackend for RecordingBackend {
async fn get_text(&self, _pid: i32, _window_id: u32) -> anyhow::Result<String> {
async fn get_text(&self, _pid: i32, _window_id: u64) -> anyhow::Result<String> {
Ok(String::new())
}

async fn query_dom(
&self,
_pid: i32,
_window_id: u32,
_window_id: u64,
_css_selector: &str,
_attributes: &[String],
) -> anyhow::Result<String> {
Expand All @@ -559,7 +556,7 @@ mod tests {
async fn execute_javascript(
&self,
_pid: i32,
_window_id: u32,
_window_id: u64,
_javascript: &str,
) -> anyhow::Result<String> {
anyhow::bail!("untargeted execute must not be used")
Expand All @@ -568,7 +565,7 @@ mod tests {
async fn execute_javascript_targeted(
&self,
pid: i32,
window_id: u32,
window_id: u64,
javascript: &str,
cdp_port: Option<u16>,
target_url_contains: Option<&str>,
Expand All @@ -588,11 +585,12 @@ mod tests {
async fn execute_javascript_forwards_explicit_page_target() {
let backend = Arc::new(RecordingBackend::default());
let tool = PageTool::new(backend.clone());
let synthetic_wayland_window_id = u64::from(u32::MAX) + 0x1234;

let result = tool
.invoke(serde_json::json!({
"pid": 42,
"window_id": 7,
"window_id": synthetic_wayland_window_id,
"action": "execute_javascript",
"javascript": "document.title",
"cdp_port": 9333,
Expand All @@ -605,7 +603,7 @@ mod tests {
*backend.targeted.lock().unwrap(),
Some((
42,
7,
synthetic_wayland_window_id,
"document.title".to_owned(),
Some(9333),
Some("#window-b".to_owned()),
Expand Down
39 changes: 38 additions & 1 deletion libs/cua-driver/rust/crates/cua-driver-testkit/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,44 @@ fn daemon_is_listening(_binary: &Path, socket: &str) -> bool {
std::os::unix::net::UnixStream::connect(socket).is_ok()
}

#[cfg(not(unix))]
#[cfg(target_os = "windows")]
fn daemon_is_listening(_binary: &Path, socket: &str) -> bool {
use std::io::{BufRead, BufReader, Write};

// Exercise the real named-pipe protocol instead of spawning `status`.
// A finite CLI command is wrapped by the telemetry completion observer,
// which makes it an unnecessarily heavy and timing-sensitive readiness
// probe on hosted Windows runners. Completing `list` also proves that the
// server has progressed past pipe creation and can service the connection.
let Ok(pipe) = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(socket)
else {
return false;
};
let Ok(mut writer) = pipe.try_clone() else {
return false;
};
if writer
.write_all(b"{\"method\":\"list\"}\n")
.and_then(|()| writer.flush())
.is_err()
{
return false;
}

let mut response = String::new();
if BufReader::new(pipe).read_line(&mut response).is_err() {
return false;
}
serde_json::from_str::<serde_json::Value>(&response)
.ok()
.and_then(|value| value.get("ok").and_then(serde_json::Value::as_bool))
== Some(true)
}

#[cfg(not(any(unix, target_os = "windows")))]
fn daemon_is_listening(binary: &Path, socket: &str) -> bool {
Command::new(binary)
.args(["status", "--socket", socket])
Expand Down
19 changes: 18 additions & 1 deletion libs/cua-driver/rust/crates/cua-driver-testkit/src/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ pub enum Scope {
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DriverRoute {
CaptureScopeGate,
AxRead,
WindowState,
UiaInvoke,
Expand Down Expand Up @@ -1361,7 +1362,7 @@ impl CatalogPolicy {
fn case_requires_action_turn(case: &CaseSpec) -> bool {
!matches!(
case.driver_route,
DriverRoute::AxRead | DriverRoute::WindowState
DriverRoute::CaptureScopeGate | DriverRoute::AxRead | DriverRoute::WindowState
) && case.action != "screenshot"
}

Expand Down Expand Up @@ -2016,6 +2017,22 @@ mod tests {
.any(|error| error.contains("missing turn evidence")));
}

#[test]
fn strict_capture_scope_gate_does_not_invent_an_action_turn() {
let case = CaseSpec::delivered(
"window-scope-gate",
"desktop",
"x11",
"window_scope_gate",
Targeting::Px,
Delivery::NotApplicable,
Scope::Window,
DriverRoute::CaptureScopeGate,
vec![OracleKind::Protocol],
);
assert!(!case_requires_action_turn(&case));
}

#[test]
fn validator_exposes_missing_legacy_modal_images() {
let (root, case, result, turn) = complete_turn_fixture();
Expand Down
Loading
Loading