packages/cua-driver/docs/browser-tool-implementation-plan.md
codex/browser-tool-v1 (local only)The accepted roadmap now covers the five typed tools, strict endpoint ownership,
session capabilities, exact-or-refused Chromium mutation, standalone Chromium
adversarial coverage, isolated browser_prepare, composed-frame refs,
event-capable CDP transport, embedded-webview positive/refusal rows, and the
legacy page transport migration. The body below preserves the reviewed design
and phase gates; the implementation journal
records what actually shipped and the retained evidence. Safari/WebKit mutation,
Firefox BiDi, split-process WebView2 binding, and mutation on generic Wayland
without exact compositor geometry remain explicit limitations.
Add a first-class browser control surface to Cua Driver that lets an agent start from the existing native discovery flow, bind an exact browser page when the platform can prove that relationship, inspect compact browser state, and perform typed browser actions without losing Cua Driver's native desktop, background-delivery, focus-preservation, and truthful-refusal guarantees.
The intended agent flow is:
start_session
list_apps
list_windows
get_browser_state
browser_prepare # only when get_browser_state reports requires_setup
get_browser_state
browser_navigate / browser_click / browser_type
get_browser_state # verify
get_window_state # browser chrome, prompts, downloads, pickers, native fallback
The implementation must improve exact targeting and agent ergonomics. It must not hide browser setup inside a getter, silently guess a tab, or create a second copy of native window state.
This proposal comes from recent cross-platform validation of browser automation on macOS, Windows, Linux X11, and Linux Wayland.
Cua Driver currently separates several capture and delivery modalities: AX and PX targeting, background and foreground delivery, and window and desktop scope. The agent action ladder starts with background AX, then tries background PX, the page tool, and finally foreground delivery. This ordering remains the right safety model because the agent begins with the most semantic and least disruptive route, then escalates only when the previous route is unavailable or cannot be verified. The current OS-specific boundaries are listed in platform support.
Web content does not always fit this ladder cleanly. Some Chromium actions cannot be delivered reliably to an occluded or unfocused page through native synthetic pointer or keyboard routes. The same operations can often run in the background through the Chrome DevTools Protocol (CDP) or, on macOS, Apple Events. The current page workflow exposes some of these routes, but its capabilities vary by operating system, it does not reliably bind the native pid and window_id to one exact browser tab, and its setup requirements are difficult for an agent to discover before an action fails.
This leaves some browser actions with best-effort background behavior even when a browser-native route could provide full-background execution. Shared-workspace agents need the stronger contract: supported page actions must avoid raising the browser, moving the cursor, stealing focus, or leaking input into the user's active application.
The proposal keeps the existing action ladder and makes its browser rung first-class and capability-aware. get_browser_state(pid, window_id) identifies the exact tab when the relationship can be proven and reports the available routes. Supported page actions can then use CDP or Apple Events before falling back to native PX or foreground delivery. get_window_state remains the source of truth for browser chrome, permission prompts, downloads, file pickers, and native fallback.
The first implementation step is a binding feasibility spike. If Cua Driver cannot reliably bind, revalidate, or safely refuse multi-window Chromium targets across supported operating systems, the design will change before the public API is committed.
The browser tool is ready for a supported route when all of the following are true:
pid and window_id discovered through Cua Driver.get_browser_state reports browser identity, available routes, setup requirements, tabs, and compact page references without causing visible or persistent side effects.browser_target_id, exact tab_id, and snapshot-scoped page reference where applicable.page clients continue working during migration.This project will not:
get_window_state or the native AX/PX action surface;browser_wait, browser_close, or tab lifecycle operations in v1;page before the new browser surface has accepted cross-platform evidence.The browser API begins with the exact native target already used by Cua Driver:
{
"session": "research-1",
"pid": 1234,
"window_id": 5678
}
pid plus window_id is the connection anchor, not a guarantee that a browser tab can be derived. The browser binding response must state whether the relationship is exact, heuristic, or unavailable.
get_browser_state is strictly read-onlyget_browser_state may:
/json-style target metadata;It may not:
This is required for truthful MCP readOnlyHint, host approval behavior, and trajectory recording.
browser_prepare owns all setup that can mutate state or require user consent.
It is statically marked mutating, destructive, and non-idempotent. Acting setup
requires either MCP-host approval or a short-lived single-use token minted by
the interactive browser-approve command. It launches a separate browser with
a driver-owned isolated profile and never copies, modifies, restarts, or
terminates the selected user profile.
get_window_state remains the source of truth for:
get_browser_state owns only what the native state cannot express reliably:
Screenshot is opt-in and must use the same capture provenance as get_window_state. AX-only surfaces do not get a duplicate browser ref namespace; they direct the caller back to get_window_state.
A browser_target_id is not a portable identifier. It is a capability owned by one Cua Driver session and one live driver instance. It must not be serialized into trajectories, replayed, or accepted after session cleanup.
Every stateful browser tool requires an explicit public session. Internal connection identity may support transport bookkeeping but must not replace the caller-visible session contract.
Read-only state may report binding.confidence as:
exact: the driver can prove the native target, browser endpoint, browser window, and page relationship needed for the requested operation;heuristic: useful metadata exists, but exact mutation is unsafe;none: the relationship cannot be established.V1 browser mutation requires exact. It must never automatically promote a heuristic binding. A future explicit product policy may permit narrowly defined heuristic reads, but not writes.
Do not add generic browser_act or browser_get tools. They would reproduce the current page problem: unrelated read and mutation semantics forced into one schema and one set of MCP annotations.
V1 uses a small set of typed tools:
get_browser_state
browser_prepare
browser_navigate
browser_click
browser_type
Every browser input result identifies one of:
trusted_input: a trusted browser input route such as CDP Input domain dispatch;dom_event: an in-page JavaScript operation such as element.click();native_ax: a native accessibility action delegated to Cua Driver;native_px: a native pixel action delegated to Cua Driver;unavailable.The driver must not represent a DOM event as a trusted pointer or keyboard event.
get_browser_statePurpose: discover a browser route, bind an exact page when possible, return compact page state, and refresh existing bindings without side effects.
Implemented request:
{
"session": "research-1",
"pid": 1234,
"window_id": 5678,
"tab_id": "t-a1",
"include_refs": true,
"refs": "interactive",
"max_refs": 200,
"include_screenshot": false
}
Rules:
session, pid, and window_id are required for initial discovery.browser_target_id plus tab_id for refresh.tab_id is allowed for state discovery and tab listing.tab_id is supplied.refs accepts interactive, all, or none; default is interactive.max_refs and a tool-level text budget.include_screenshot defaults to false.browser_prepare implicitly.Proposed response:
{
"browser_target_id": "bt-3f",
"native_target": {
"pid": 1234,
"window_id": 5678
},
"binding": {
"method": "cdp_window_bounds",
"confidence": "exact",
"port_owner_verified": true,
"revalidated": true
},
"browser": {
"kind": "chrome",
"engine": "chromium",
"profile_kind": "default"
},
"tabs": [
{
"tab_id": "t-a1",
"url": "https://example.com",
"title": "Example",
"active": true,
"load_state": "complete"
}
],
"page": {
"tab_id": "t-a1",
"refs_snapshot": "p1b3",
"ref_source": "dom",
"refs": [
{
"ref": "p1b3:4",
"role": "button",
"name": "Submit",
"frame": [112, 340, 88, 32]
}
]
},
"routes": {
"read": "cdp",
"navigate": "cdp",
"input_trusted": "cdp",
"input_dom": "cdp"
},
"requires_setup": [],
"limitations": []
}
No universal state_revision is included in v1. Snapshot staleness is represented by refs_snapshot; tab and binding validity are checked directly.
browser_preparePurpose: perform explicit consent-bearing or mutating setup needed to expose a safe browser route.
Proposed request:
{
"session": "research-1",
"pid": 1234,
"allow_launch": true,
"profile": {
"mode": "isolated_new"
}
}
Supported setup operations are deliberately narrow:
isolated_new or persistent driver-owned
isolated_named profile;DevToolsActivePort and
operating-system socket ownership;The result returns prepared_pid, endpoint-ownership metadata, and explicit
side effects, or a structured refusal. The caller discovers that new process's
window and binds it normally. Ending the owning session terminates the managed
process and removes an isolated_new profile.
Public Boolean consent fields are intentionally absent because a model can forge them. MCP approval is transported through a private host-to-driver marker; direct and raw callers must present a five-minute, pid/profile-bound, single-use approval token minted in an interactive terminal.
browser_navigatePurpose: navigate an exact tab.
Proposed request:
{
"session": "research-1",
"browser_target_id": "bt-3f",
"tab_id": "t-a1",
"operation": "goto",
"url": "https://example.com/dashboard",
"wait_until": "domcontentloaded",
"timeout_ms": 25000
}
Operations:
gotobackforwardreloadwait_until is supported only on routes with reliable navigation events. Other routes return a structured route refusal rather than imitating a load event with an undocumented poll.
browser_clickPurpose: click a snapshot-scoped page reference using an explicitly requested trust class.
Proposed request:
{
"session": "research-1",
"browser_target_id": "bt-3f",
"tab_id": "t-a1",
"ref": "p1b3:4",
"input": "trusted"
}
Rules:
input defaults to trusted.element.click().dom_event for applications where that semantic is acceptable.get_browser_state call.browser_typePurpose: insert text or dispatch browser keystrokes to an exact page target.
Proposed request:
{
"session": "research-1",
"browser_target_id": "bt-3f",
"tab_id": "t-a1",
"ref": "p1b3:8",
"text": "[email protected]",
"mode": "insert"
}
Modes:
insert: direct text insertion without individual key events;keystrokes: trusted key event dispatch;dom_value: explicit DOM value mutation, if added after v1 validation.The result reports the actual mode and trust class. Browser-tool v1 requires a
fresh ref and proves that it resolves to the focused editable element before
typing; it refuses rather than reporting success against an unproven focus.
Add browser-specific refusal codes to the shared structured result vocabulary:
browser_route_unavailable
browser_requires_setup
browser_binding_ambiguous
browser_binding_stale
browser_wrong_target_refused
browser_tab_required
browser_tab_not_found
browser_ref_stale
browser_input_trust_unavailable
browser_endpoint_owner_mismatch
browser_consent_required
Every refusal includes:
requires_setup entry when applicable;BrowserEngineCreate a shared browser subsystem in cua-driver-core rather than extending the current platform-specific PageBackend divergence.
Proposed responsibilities:
cua-driver-core/src/browser/
mod.rs public internal API and engine composition
types.rs targets, tabs, routes, setup, results, refusals
discovery.rs endpoint and browser target discovery interfaces
binding.rs native-window-to-browser-window/page correlation
cdp.rs shared CDP transport and protocol operations
pool.rs browser socket and flat-session lifecycle
refs.rs DOM snapshot refs and stale-ref validation
session_store.rs session-scoped browser targets and cleanup
Final paths may follow existing crate conventions, but ownership boundaries must remain:
page becomes a compatibility adapter over the shared engine.Each platform supplies:
classify_browser(pid)
validate_window_owner(pid, window_id)
discover_pid_owned_endpoints(pid)
correlate_browser_window(pid, window_id, browser_window_metadata)
prepare_browser_route(request)
capture_provenance(pid, window_id)
Windows' existing HWND-to-PID validation becomes the behavioral baseline. Equivalent fail-closed validation is required on macOS and Linux wherever platform APIs expose ownership.
Each target record contains:
session
browser_target_id (opaque and unguessable)
pid
window_id
browser kind and engine
endpoint identity and owner proof
browser WebSocket UUID where applicable
binding method and confidence
binding fingerprint
tab mappings
created_at and last_used_at
Lifecycle rules:
end_session, connection teardown, and idle TTL remove records and page refs;Replace the current mixture of one-shot core calls and macOS-only cached mutable targeting with one shared model:
This removes the cross-session risk of a process-global socket with mutable "current target" state.
Reuse the existing snapshot-token concepts but keep browser refs in a distinct namespace:
native ref: s<snapshot>:<index>
browser ref: p<snapshot>:<index>
Rules:
For Chromium, generate refs from the browser accessibility tree and DOM identity rather than inventing selectors:
Page refs must carry frame_id. Main-frame support is required. Open shadow
roots and same-process iframes are implemented; out-of-process iframes are
included only after a capability-tested flattened CDP attachment. Unsupported
frame routes are omitted with a limitation rather than flattened into an
incorrect main-frame target.
Before any CDP or inspector connection, the driver must prove that the local listening endpoint belongs to the expected browser process or an allowed browser process in its verified parent/child process tree.
Platform work:
| Platform | Ownership strategy |
|---|---|
| macOS | Existing PID-to-port discovery via lsof, tightened to verify endpoint and process tree |
| Windows | GetExtendedTcpTable plus process-tree validation |
| Linux | /proc/net/tcp* socket inode mapping plus /proc/<pid>/fd and process-tree validation |
Security requirements:
The preferred binding algorithm is:
pid and window_id relationship.Tab drag is a required invalidation scenario: a CDP target may survive while moving to another browser window. The next action must re-correlate or refuse.
heuristic or none and do not permit browser mutation.| Surface | Implemented state | Mutation | Setup / limitation | Confidence |
|---|---|---|---|---|
| Chrome/Edge/Brave macOS | CDP metadata and refs where approved | Navigate, ref typing, explicit DOM click; trusted pointer refuses | Existing owned endpoint or approved isolated profile | Exact or refuse |
| Electron macOS | CDP metadata and refs | Navigate and input | Inspector exposure through prepare when needed | Exact for single-window fixture |
| Safari macOS | Classified; native/legacy read routes remain | Typed mutation deferred | No exact WebKit route | None for typed mutation |
| Chrome/Edge Windows | CDP metadata and refs | Navigate, ref typing, trusted and explicit DOM click | Existing owned endpoint or approved isolated profile | Exact or refuse |
| WebView2 Windows | Native host and renderer split are detected | Structured refusal | Exact cross-process correlation deferred | None for typed mutation |
| Firefox Windows | Identity and unavailable routes | Native get_window_state only | None in v1 | None |
| Chromium Linux X11 | CDP metadata and refs | Navigate, ref typing, explicit DOM click; trusted pointer refuses | Existing owned endpoint or approved isolated profile | Exact or refuse |
| Chromium Linux Wayland | CDP metadata; native correlation when available | Only exact binding | Add launch CDP support and compositor metadata | Exact or refuse |
| Electron/Tauri Linux | Electron exact where bounded; Tauri classified | Electron typed mutation; Tauri structured refusal | Tauri/WebKit inspector correlation deferred | Exact or refuse |
| Firefox Linux | Identity and unavailable routes | Native get_window_state only | None in v1 | None |
Support documentation must distinguish browser identity, page read, DOM refs, navigation, trusted input, DOM input, native background input, and native foreground input. A single "browser supported" label is insufficient.
Purpose: prove or disprove the window-anchored design before exposing a public API.
Work:
Required scenarios:
/json endpoint on a common debugging port.Exit gate:
Kill criterion:
If multi-window Chromium cannot be deterministically bound or honestly refused on all three OS families, stop the window-bound API implementation. Pivot to tab-first targeting, with native-window correlation exposed only as advisory metadata.
Work:
launch_app on every platform;browser_prepare;Exit gate:
page tests still pass unchanged;Work:
Exit gate:
get_browser_stateWork:
requires_setup;Exit gate:
browser_prepareImplementation status: Complete for safe isolated Chromium launch.
Work:
Exit gate:
Work:
browser_navigate for exact CDP targets;browser_click through browser input dispatch;dom_event click as a separate requested trust class;browser_type insert and keystroke modes;Exit gate:
page compatibility facadeImplementation status: Complete at the transport boundary. Legacy public selection, arguments, result formatting, and native fallbacks remain unchanged.
Work:
CdpConnection;Exit gate:
page tests and downstream schema tests pass;page calls share the CDP transport and event demux
without pretending their target-selection semantics are identical;page behavior regresses;Implementation status: Complete. The accepted implementation and runner changes are frozen before the final exact-head replay. The canonical evidence manifests record the full source SHA, environment, row outcome, oracles, and video for every accepted route.
Work:
page;Exit gate:
page remains available for at least two releases after formal deprecation.Keep each PR independently reviewable and avoid exposing incomplete public tools.
| PR | Scope | User-visible change | Merge requirement |
|---|---|---|---|
| 1 | Chromium binding spike and real-browser discriminators | None | Kill criterion passes |
| 2 | Shared CDP transport, endpoint ownership, and launch parity | More truthful/hardened existing CDP behavior | Existing page E2E green |
| 3 | Session target store, flat-mode routing, refs, and refusals | None or additive structured internals | Concurrency and cleanup tests green |
| 4 | Read-only get_browser_state | New state tool | Cross-platform state matrix accepted |
| 5 | browser_prepare and consent/profile flows | New mutating setup tool | Consent and restart evidence accepted |
| 6 | browser_navigate | New typed navigation | Real-browser navigation lanes green |
| 7 | browser_click and browser_type | New typed input tools | Trust/focus/leak oracles green |
| 8 | Route legacy page through BrowserEngine | No intentional behavior change | Compatibility suite green |
| 9 | Docs, skills, release matrix, and deprecation notice | Public rollout | Exact-SHA evidence linked |
Do not merge PR 4 merely to allow later CI to run unless the state tool's own acceptance gate is complete. Use test-only probes or feature-gated internal code for the feasibility stage.
Run automatically on affected OS paths:
page compatibility.Extend the existing harnesses without weakening their oracles:
| Host | Platforms | Required browser-tool coverage |
|---|---|---|
| Electron fixture | macOS, Windows, Linux X11, Linux Wayland | Discovery, exact single-window binding, refs, navigate, click, type, stale refs, concurrent sessions |
| Tauri fixture | macOS, Windows, Linux X11, Linux Wayland | Capability reporting, exact route where available, native fallback boundaries |
| WebView2 fixture | Windows | Split-process route detection and side-effect-free structured refusal |
| WKWebView fixture | macOS | Identity, capability limitations, native-state handoff |
| WebKitGTK fixture | Linux | Identity, capability limitations, native-state handoff |
Repository fixtures remain the canonical deterministic behavior matrix. They do not replace real-browser evidence.
| Platform | Required v2 browser evidence |
|---|---|
| Windows interactive | Chrome and Edge standalone; Electron delivery; Tauri and WebView2 exact refusals |
| macOS authorized VM | Chrome standalone; Electron delivery; Tauri and WKWebView exact refusals |
| Linux X11 | Chromium standalone; Electron delivery; Tauri exact refusal |
| Linux Wayland Sway | Native Ozone Chromium; Electron delivery; Tauri exact refusal |
| Other Wayland desktops | Capability reporting and refusal unless the compositor proves exact pid + geometry |
Every real-browser lane records:
page call and new browser call against the same browser process.A release that advertises the browser tool requires:
page facade.pagepage remains functional throughout v1.page gains optional session and browser_target_id fields additively.page responses gain route metadata without removing existing text.During coexistence, docs should say:
get_window_state for browser chrome and native dialogs;page only for compatibility with existing clients;The implementation should coordinate with these existing issues:
| Issue | Relationship to this plan |
|---|---|
| #2200 | Exact PID/window targeting and ambiguous-target refusal |
| #2176 | Browser/page escalation before native foreground fallback |
| #2192 | Authenticated Chrome profile preparation and consent |
| #2084 | Shared CDP typing and Windows/Linux parity |
| #2201 | Windows Chrome background native input remains a separate route risk |
| #2202 | macOS Chrome native drag remains separate from browser DOM/CDP input |
| #1616 | Real Chromium accessibility exposure and native fallback |
| #2101 | Firefox capability and minimized/native-state limitations |
| #2194 | Wayland cursor-preservation evidence remains platform-limited |
Create one browser-tool umbrella issue before implementation, then link each PR and any newly discovered platform bug to the relevant capability row rather than treating all browser work as one undifferentiated feature.
Use the Diataxis structure for public documentation:
requires_setup;Bundled skills must derive their examples from the same platform-specific schemas and must not claim unsupported actions.
Add privacy-preserving counters and structured diagnostics for:
Do not record URLs, page text, selectors, cookies, profile paths, auth state, endpoint tokens, or JavaScript payloads in aggregate telemetry.
doctor should report browser support readiness without mutating state:
| Risk | Mitigation |
|---|---|
| Wrong native window mapped to a tab | Owner proof, exact correlation, per-call revalidation, ambiguity refusal |
| Consent prompt triggered by read-only state | Hard separation between state and prepare; reuse approved sockets |
| Cross-session CDP delivery | Flat mode and one CDP session ID per Cua session/tab |
| Browser tab moved after binding | Re-check browser window relationship before mutation |
| DOM click mistaken for trusted input | Required input_trust result and explicit caller choice |
| Duplicate native/browser context | Browser state excludes native tree and screenshot by default |
| Wayland missing identity/geometry | Confidence none, structured refusal, native semantic fallback only |
| Existing profile compromised | Explicit consent, loopback owner proof, TTL, no target replay, secret-safe logs |
| Public API outruns evidence | Chromium-only v1 mutation and exact-SHA release matrix |
page migration regresses users | Compatibility facade, retained tests, multi-release deprecation |
| Tool surface overwhelms MCP context | Five typed v1 tools, compact refs, bounded output, granular registration |
These decisions remain intentionally deferred after implementation evidence:
browser_wait is valuable enough to become a separate typed tool.state_revision should exist only for event-backed targets or remain omitted in favor of snapshot staleness.page deprecation timeline after adoption data exists. Transport
duplication is removed, but the compatibility facade is not deprecated yet.The browser tool project is complete when:
get_browser_state is demonstrably side-effect-free;get_window_state;page behavior remains compatible through the shared engine;Reviewers approved these four points before implementation:
get_browser_state is strictly read-only, with all setup delegated to browser_prepare.Phase 0 proved the window-anchored design. The later phases proceeded only after the exact-or-refused binding and ambiguity tests passed.