Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3c7ea8d
fix(cua-driver): complete browser and nested Wayland E2E
Jul 24, 2026
396528b
test(cua-driver): verify browser scale without fixed viewport
Jul 24, 2026
2e4bcc3
feat(cua-driver): implement SDK-owned runtime RFC
Jul 25, 2026
bbae99e
fix(cua-driver): test UIAccess authorization without elevation
Jul 25, 2026
249d6e0
test(cua-driver): use portable embedded host fixture path
Jul 25, 2026
108fae7
test(cua-driver): validate native Wayland worker scope
Jul 25, 2026
102a78a
test(cua-driver): bound Windows high-DPI browser geometry
Jul 25, 2026
493d161
fix(cua-driver): attest Windows browser process trees
Jul 25, 2026
b5975e9
fix(cua-driver): preserve promoted browser runtime identity
Jul 25, 2026
5e950af
fix(cua-driver): contain macOS browser setup fallback
Jul 25, 2026
9c49326
fix(cua-driver): bind macOS setup fallback evidence
Jul 25, 2026
28907dd
fix(cua-driver): close macOS setup evidence gaps
Jul 25, 2026
8c553d0
test(cua-driver): scope pixel fallback expectation
Jul 25, 2026
fd07cf2
test(cua-driver): keep fallback evidence route-neutral
Jul 25, 2026
8c980e2
fix(cua-driver): close final RFC validation gaps
Jul 25, 2026
cc7afdb
fix(cua-driver): harden Windows browser endpoint ownership
Jul 25, 2026
516d749
fix(cua-driver): separate spawned browser attestation
Jul 25, 2026
991cfa0
docs(cua-driver): describe Windows service transport
Jul 25, 2026
0413b0e
test(cua-driver): isolate macOS browser setup consent
Jul 25, 2026
9cf8db8
fix(cua-driver): disambiguate macOS browser setup checkbox
Jul 25, 2026
03f2556
Merge remote-tracking branch 'origin/main' into codex/fix-e2e-unrestr…
Jul 25, 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
42 changes: 40 additions & 2 deletions .github/workflows/ci-cua-driver-contract-clients.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ jobs:
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
workspaces: "libs/cua-driver/rust -> target"
- name: Record Windows runner session
if: runner.os == 'Windows'
id: windows-session
shell: pwsh
run: |
$sessionId = (Get-Process -Id $PID).SessionId
"session_id=$sessionId" >> $env:GITHUB_OUTPUT
"Windows runner process session id: $sessionId"
- name: Install Linux build dependencies
if: runner.os == 'Linux'
run: |
Expand All @@ -84,21 +92,47 @@ jobs:
clang pkg-config libdbus-1-dev libpipewire-0.3-dev libspa-0.2-dev \
libei-dev libxkbcommon-dev libx11-dev libxi-dev libxtst-dev libxext-dev
- name: Prove portable contracts match the live registry
if: runner.os != 'Windows' || steps.windows-session.outputs.session_id != '0'
working-directory: libs/cua-driver/rust
run: cargo test --locked -p cua-driver --test schema_consistency_test portable_desktop_contracts_are_accepted_by_active_backend

- name: Verify released CLI and MCP compatibility
if: runner.os != 'Windows' || steps.windows-session.outputs.session_id != '0'
working-directory: libs/cua-driver/rust
run: cargo test --locked -p cua-driver --test compatibility_contract_test

- name: Compile the UniFFI SDK library
working-directory: libs/cua-driver/rust
run: cargo check --locked -p cua-driver-sdk

- name: Compile the frozen previous-release Rust application
run: cargo check --manifest-path libs/cua-driver/compat-fixtures/apps/rust/Cargo.toml

- name: Run the frozen previous-release Rust application
if: runner.os == 'Linux'
run: cargo run --manifest-path libs/cua-driver/compat-fixtures/apps/rust/Cargo.toml

- name: Prove the embedded host serves SDK and MCP clients
if: runner.os != 'Windows' || steps.windows-session.outputs.session_id != '0'
working-directory: libs/cua-driver/rust
run: cargo test --locked -p cua-driver --test embedded_host_sdk_mcp_test

- name: Prove Windows Session 0 refusal and desktop-free metadata
if: runner.os == 'Windows' && steps.windows-session.outputs.session_id == '0'
working-directory: libs/cua-driver/rust
shell: pwsh
run: |
cargo test --locked -p platform-windows diagnostics::tests:: --lib
if ($LASTEXITCODE -ne 0) { throw "Windows diagnostics tests failed with exit $LASTEXITCODE" }
cargo test --locked -p cua-driver-sdk session_zero_refuses_runtime_creation_before_platform_dispatch --lib
if ($LASTEXITCODE -ne 0) { throw "Session 0 refusal test failed with exit $LASTEXITCODE" }
cargo run --locked -p cua-driver -- list-tools | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Session 0 list-tools failed with exit $LASTEXITCODE" }
cargo run --locked -p cua-driver -- describe click | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Session 0 describe failed with exit $LASTEXITCODE" }
cargo run --locked -p cua-driver -- dump-docs --type cli | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Session 0 dump-docs failed with exit $LASTEXITCODE" }

verify:
name: Generated contract and SDK bindings
runs-on: ubuntu-latest
Expand Down Expand Up @@ -215,8 +249,10 @@ jobs:
python -m build --wheel --outdir /tmp/cua-driver-wheels python
unzip -l /tmp/cua-driver-wheels/*.whl | grep -F 'cua_driver/libcua_driver_sdk.so'
python -m pip install --target /tmp/cua-driver-wheel-smoke /tmp/cua-driver-wheels/*.whl
PYTHONPATH=/tmp/cua-driver-wheel-smoke python \
compat-fixtures/apps/python/app.py
PYTHONPATH=/tmp/cua-driver-wheel-smoke python -c \
"from cua_driver import CuaDriver, EmbeddedCuaDriverHost; print(CuaDriver.connect(None).socket_path(), EmbeddedCuaDriverHost)"
"from cua_driver import EmbeddedCuaDriverHost; print(EmbeddedCuaDriverHost)"

- name: Verify Node native package contents
working-directory: libs/cua-driver/typescript
Expand All @@ -243,8 +279,10 @@ jobs:
"/tmp/cua-driver-npm/trycua-cua-driver-$VERSION.tgz" \
"/tmp/cua-driver-npm/trycua-cua-driver-linux-x64-gnu-$VERSION.tgz"
cd /tmp/cua-driver-npm/smoke
cp "$GITHUB_WORKSPACE/libs/cua-driver/compat-fixtures/apps/typescript/app.mjs" .
node app.mjs
node --input-type=module -e \
"const sdk = await import('@trycua/cua-driver'); const driver = sdk.CuaDriver.connect(undefined); console.log(driver.socketPath()); driver.uniffiDestroy(); const embedded = await import('@trycua/cua-driver/embedded'); if (embedded.EmbeddedCuaDriverHost !== sdk.EmbeddedCuaDriverHost) process.exit(1)"
"const sdk = await import('@trycua/cua-driver'); const embedded = await import('@trycua/cua-driver/embedded'); if (embedded.EmbeddedCuaDriverHost !== sdk.EmbeddedCuaDriverHost) process.exit(1)"

- name: Install Python agent SDK example dependencies
working-directory: libs/cua-driver/examples/agent-sdks
Expand Down
41 changes: 39 additions & 2 deletions .github/workflows/ci-rust-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ jobs:
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
workspaces: "libs/cua-driver/rust -> target"
- name: Record Windows runner session
id: runner-session
shell: pwsh
run: |
$sessionId = (Get-Process -Id $PID).SessionId
"session_id=$sessionId" >> $env:GITHUB_OUTPUT
"Windows runner process session id: $sessionId"
- name: Run Windows Rust tests
working-directory: libs/cua-driver/rust
# Compile every Rust target without executing desktop-dependent integration
Expand All @@ -61,11 +68,41 @@ jobs:
working-directory: libs/cua-driver/rust
run: |
cargo test -p cua-driver-core session_authorization::tests:: --lib --locked
cargo test -p cua-driver-sdk --lib --locked
if ($LASTEXITCODE -ne 0) { throw "session authorization tests failed with exit $LASTEXITCODE" }
cargo test -p cua-driver-sdk abi::tests:: --lib --locked
if ($LASTEXITCODE -ne 0) { throw "SDK ABI tests failed with exit $LASTEXITCODE" }
cargo test -p cua-driver-sdk remote_ --lib --locked
if ($LASTEXITCODE -ne 0) { throw "remote SDK tests failed with exit $LASTEXITCODE" }
cargo test -p cua-driver --bin cua-driver named_pipe_authentication_tests:: --locked
if ($LASTEXITCODE -ne 0) { throw "named-pipe authentication tests failed with exit $LASTEXITCODE" }
# The production binary embeds uiAccess=true and Windows correctly
# refuses to launch it unelevated. Run the same pure security policy
# from the manifest-free library test target.
cargo test -p cua-driver-uia --lib authorization_tests:: --locked
if ($LASTEXITCODE -ne 0) { throw "UIAccess authorization tests failed with exit $LASTEXITCODE" }
cargo test -p platform-windows diagnostics::tests:: --lib --locked
if ($LASTEXITCODE -ne 0) { throw "Windows desktop classification tests failed with exit $LASTEXITCODE" }
- name: Run full SDK unit suite in an interactive runner session
if: steps.runner-session.outputs.session_id != '0'
working-directory: libs/cua-driver/rust
run: cargo test -p cua-driver-sdk --lib --locked -- --test-threads=1
- name: Prove Session 0 refusal
if: steps.runner-session.outputs.session_id == '0'
working-directory: libs/cua-driver/rust
run: cargo test -p cua-driver-sdk session_zero_refuses_runtime_creation_before_platform_dispatch --lib --locked
- name: Prove metadata commands remain desktop-free
working-directory: libs/cua-driver/rust
run: |
cargo run -p cua-driver --locked -- list-tools | Out-Null
if ($LASTEXITCODE -ne 0) { throw "list-tools failed with exit $LASTEXITCODE" }
cargo run -p cua-driver --locked -- describe click | Out-Null
if ($LASTEXITCODE -ne 0) { throw "describe failed with exit $LASTEXITCODE" }
cargo run -p cua-driver --locked -- dump-docs --type cli | Out-Null
if ($LASTEXITCODE -ne 0) { throw "dump-docs failed with exit $LASTEXITCODE" }
- name: Run Windows browser platform unit tests
working-directory: libs/cua-driver/rust
run: 'cargo test -p platform-windows browser_platform::tests:: --lib --locked'
run: "cargo test -p platform-windows browser_platform::tests:: --lib --locked"
- name: Run Windows protocol schema contract
if: steps.runner-session.outputs.session_id != '0'
working-directory: libs/cua-driver/rust
run: cargo test -p cua-driver --test protocol_schema_test --locked
3 changes: 3 additions & 0 deletions .github/workflows/e2e-rust-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ jobs:
with:
ref: ${{ needs.source.outputs.sha }}
- uses: dtolnay/rust-toolchain@stable
- name: Verify installer runner is interactive
shell: pwsh
run: .\scripts\ci\windows\verify-user-session.ps1
- name: Install into an isolated local namespace
shell: pwsh
env:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,20 @@ files, restarts, or terminates the selected process, and it reports its
temporary-tab and setting effects. Remote-debugging arguments passed through
`launch_app` are refused.

Current macOS Chrome can expose the native address field and selected internal
tab while withholding that page's web AX subtree. In that case, the macOS
adapter uses only the temporary tab that it created and navigated to the fixed
internal URL, requires the committed address value and expected selected-tab
title with no active omnibox edit, then requires one unique checkbox-shaped
control inside a bounded setup-page region. The click is PID-routed to the
revalidated unchanged browser window and the same control's state transition is
verified. Because macOS delivers that bounded pixel action through global input,
the driver may briefly foreground the exact approved window, then restore the
previous frontmost app. The result reports both the foreground and global-input
effects. Unsupported appearance, scale, zoom, window-size, or toolbar geometry
is refused without a click. It does not generalize that fallback to web pages or
arbitrary dialogs.

The setup transition and protocol attachment are separate proofs. A listener
must be loopback-only, attributed to the approved pid, and either discoverable
as DevTools or correlated with the exact approved checkbox transition. The
Expand Down
56 changes: 42 additions & 14 deletions docs/content/docs/concepts/how-permission-policies-work.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,51 @@ description: How the Cua Driver permission policy engine evaluates YAML and Rego

import { Callout } from 'fumadocs-ui/components/callout';

Cua Driver's permission policy engine sits between daemon clients (MCP, CLI, or direct socket clients) and the tool implementation. Before the daemon executes any tool call it asks the policy engine whether the call is allowed. This page explains how the engine is structured, when it is active, and what guarantees it provides.
Cua Driver's permission policy engine sits at the native runtime dispatch
boundary, between every public caller and the tool implementation. Before a
direct SDK runtime, private worker, MCP process, or daemon executes a tool
call, it asks the same policy engine whether the call is allowed.

## The enforcement point

Every tool call reaches a running `cua-driver serve` daemon. A thin `cua-driver mcp` process proxies MCP calls over the local socket; `cua-driver call` sends its one request to the same daemon.

The authorization coordinator is always invoked in the daemon before tool execution. It evaluates the built-in risk map, managed policy, user policy, and autonomous session manifest in order. The MCP proxy may repeat the user-policy check as an earlier defense-in-depth check. A denial at either point returns an error to the client; the tool implementation is never reached.
Every public path reaches the authorization coordinator before platform
dispatch. It evaluates the built-in risk map, managed policy, user policy, and
bounded session manifest in order. Adapters may repeat a check earlier as
defense in depth, but they cannot authorize a request the runtime denies. A
denial returns an error to the client and the tool implementation is never
reached.

## Deny-by-default

The engine is deny-by-default. A tool that is not explicitly mentioned in the policy is blocked. This means adding a new tool to the driver does not automatically expose it to agents; each tool must be explicitly permitted.

The deny-by-default behavior applies within each configured policy. When `CUA_DRIVER_POLICY_FILE` is unset, that layer is absent for compatibility. The reviewed built-in tool and risk map still rejects unknown tools, and the default permission mode remains `standard`.

An explicitly configured policy path is an operator assertion that the layer must exist. If the path is missing, unreadable, empty, or invalid, the daemon exits before binding its action socket.
An explicitly configured policy path is an operator assertion that the layer
must exist. If the path is missing, unreadable, empty, or invalid, runtime
construction fails before tools are registered or a service binds its action
endpoint.

## Policy composition and modes

`CUA_DRIVER_MANAGED_POLICY_FILE` loads an administrator ceiling in the same YAML or Rego formats. A call must pass both the managed and user layers. The daemon hashes each immutable policy snapshot and includes those hashes in protected approval requests and status output.
`CUA_DRIVER_MANAGED_POLICY_FILE` loads an administrator ceiling in the same
YAML or Rego formats. A call must pass both the managed and user layers. The
runtime hashes each immutable policy snapshot and includes those hashes in
protected approval requests and status output.

Permission mode is separate from capability policy. Policy answers whether a call is inside the allowed ceiling. Mode answers whether an allowed operation must stop for protected human approval. Approval and unrestricted mode cannot widen the policy ceiling. See [Permission modes and bounded autonomy](/reference/cua-driver/permission-modes).

## Process-lifetime snapshot
## Runtime-lifetime snapshot

The policy file is loaded once when the daemon starts. All subsequent calls share the same immutable policy object for the lifetime of that process. There is no reload endpoint and no hot-swap path. Changing the policy takes effect only after the daemon restarts.
The policy file is loaded once when the runtime starts. All subsequent calls
through that runtime generation share the same immutable policy object. There
is no reload endpoint and no hot-swap path. Changing the policy takes effect
only after a direct runtime, private worker, MCP process, or daemon is
restarted.

This makes the policy a reliable static contract: the same rule that was in effect when the daemon started will still be in effect when the last tool call of the session runs.
This makes the policy a reliable static contract: the same rule that was in
effect when the runtime started remains in effect through its last admitted
call.

## YAML evaluation

Expand Down Expand Up @@ -62,13 +80,16 @@ At evaluation time, the driver:
3. Sets the input and evaluates `data.cua.policy.allow`.
4. Maps the result: `true` → Allow, `false` or `undefined` → Deny, error → Error.

Because Regorus runs inside the Cua Driver daemon and does not spawn a policy subprocess, there is no additional policy IPC per call.
Because Regorus runs inside the runtime owner and does not spawn a policy
subprocess, there is no additional policy IPC per call.

## Argument sanitization before evaluation

Two transformations happen before the arguments reach the policy engine:

- **`_session_id` removal.** The daemon injects a `_session_id` field for session tracking. This is an internal implementation detail that is not relevant to policy decisions, so it is stripped before evaluation.
- **Internal session-field removal.** Runtime and transport adapters may inject
reserved session fields for lifecycle tracking. These are stripped before
evaluation so a caller-controlled label cannot change policy authority.
- **Tool name canonicalization.** The deprecated `type_text_chars` alias is normalized to `type_text` before any rule is consulted, so policies written against the canonical name cover both forms automatically.

## What the engine does not cover
Expand All @@ -84,13 +105,20 @@ A policy that allows `screenshot` permits an agent to take an unlimited number o

## Trust model

The policy is evaluated in the same process as the tool implementation. An agent that can replace or inject code into the daemon can bypass it. Local shell access alone does not turn ordinary MCP arguments, files, TTY input, or environment values into protected consent. Use a trusted launcher to own daemon startup settings and OS isolation to keep the agent from replacing the daemon.
The policy is evaluated in the same process as the tool implementation. An
agent that can replace or inject code into the runtime-owning process can
bypass it. Local shell access alone does not turn ordinary MCP arguments,
files, TTY input, or environment values into protected consent. Use a trusted
host to construct direct runtimes, or a trusted launcher and OS isolation for
workers and services.

For remote agents connecting through the daemon's network socket, the policy provides a meaningful boundary: the daemon will not execute a tool that the policy blocks, regardless of what the agent sends.
For remote agents connecting through an authenticated service, the policy
provides a meaningful boundary: the service runtime will not execute a tool
that the policy blocks, regardless of what the agent sends.

## Related

- [Restrict tool access with permission policies](/how-to-guides/driver/restrict-tool-access): step-by-step setup guide
- [Permission policies](/reference/cua-driver/permission-policies): YAML schema and Rego input interface
- [Permission modes and bounded autonomy](/reference/cua-driver/permission-modes): how mode, consent grants, and policy layers compose
- [Process model](/reference/cua-driver/process-model): how CLI and MCP clients reach the daemon
- [Process model](/reference/cua-driver/process-model): direct, worker, MCP, and service ownership
Loading
Loading