The complete capability matrix for WebDriver BiDi — every module, command, and event mapped against the bl engine, its user intention, CLI/MCP exposure, Skill workflow, and Chrome support.
Every WebDriver BiDi capability, tracked against the bl engine in public.
Expand a row (+) for the implementation description, source
evidence, arguments, risk, and verification detail.
Spec capabilities
89
65 commands · 24 events
Engine coverage
100%
89 covered · 0 gaps
How it's covered
89 native
0 shimmed / consumed / lifecycle
Verification baseline
82 integrated
82 verified · 0 awaiting runtime verification · 0 not integrated · 7 other
Engine answers "can the bl engine reach this capability?" — that is
the parity score. CLI / MCP answer "does a curated surface exist for
it?" — a deliberate product decision, not a mirror of the spec. Internal
in a surface column is not a gap; Missing in the engine column is.
Verified on Chrome is evidence-gated: it only reads "Verified" when a
direct engine parity test passed against Chrome. The expanded row carries
the full record — ID, Skill workflow (the Skill teaches the CLI and
implements no browser capability of its own), engine test status,
Chrome's own BiDi support, and the evidence behind each claim.
Showing 89 of 89 capabilities · engine parity is tracked separately from CLI/MCP exposure
Verified live 2026-07-09 on Chrome for Testing / ChromeDriver 150.0.7871.49: session.status -> {ready: true, message: 'ChromeDriver ready for new sessions.'}. Direct parity tests bidi_001_session_status_{wire_and_result, error_propagation, live_chrome} (src/bidi/session.rs); CLI smoke drives the hidden bidi-test diagnostic. CLI kept internal 2026-08-05: protocol-health probes are engine bookkeeping, not automation surface (all three protocol diagnostics unadvertised, still invocable). MCP kept internal: remote-end readiness is engine bookkeeping, not an agent intention.
Native session.new: the pre-Mux raw_session_new handshake (every local launch and remote connect) and the Mux-path Client::session_new both send {capabilities: CapabilitiesRequest} and decode {sessionId, capabilities} with the six spec-required capability members type-validated (userAgent tolerated as absent — ChromeDriver 150 omits it); optional/extensible members are preserved verbatim (userDataDir).
Source evidence
src/bidi/connect.rs:58; src/bidi/session.rs:191
Arguments / defaults
params.capabilities is required (session.CapabilitiesRequest; alwaysMatch/firstMatch both optional — {} is valid) and is built from launch flags/config; result {sessionId, capabilities} validates the required members and types, with the documented userAgent-absent tolerance for ChromeDriver. Session id and WebSocket setup are engine-managed.
Exposure decision
Semantic public
CLI
bl start · Shipped · Public
MCP
browser_start · Shipped · Public · profile: core
Skill workflow
Documented · bl start (teaches the CLI; not a separate implementation)
Verified live 2026-07-09 on Chrome for Testing / ChromeDriver 150.0.7871.49: launch handshake session.new -> non-empty sessionId + matched capabilities {acceptInsecureCerts, browserName, browserVersion, platformName, setWindowRect, proxy, unhandledPromptBehavior, webSocketUrl, userDataDir, ...} — the spec-required userAgent is MISSING from Chrome's result (hence browser support 'Partial'; the decoder tolerates exactly that absence, documented in SessionNewCapabilities). A second session.new on the active session -> 'session not created: session already exists' (spec §7.1.3.2 remote end step 1). Direct parity tests bidi_002_session_new_{handshake_wire_and_result, mux_wire_and_result, malformed_result_rejected, missing_capability_member_rejected, wrong_capability_type_rejected, error_propagation, live_chrome} (src/bidi/session.rs). CLI/MCP reuse the existing bl start / browser_start intentions; no new surface.
Native session.end: Client::session_end sends EmptyParams and validates the success result against EmptyResult (object or the prose’s null accepted; anything else rejected as malformed); remote/connect teardown uses it with a 5s bound (process_pool). Local teardown closes the owned chromedriver process instead — Browserlane owns that lifecycle.
No parameters (EmptyParams); returns EmptyResult — validated as an extensible map, with the spec prose’s "success with data null" also accepted; any other result value is rejected as malformed. The current named session is derived; protocol cleanup is engine-managed (local teardown closes the owned browser process instead of sending session.end).
Exposure decision
Semantic public
CLI
bl stop · Shipped · Public
MCP
browser_stop · Shipped · Public · profile: core
Skill workflow
Documented · bl stop (teaches the CLI; not a separate implementation)
Verified live 2026-07-09 on Chrome for Testing / ChromeDriver 150.0.7871.49: session.end -> result {} (captured and asserted against the EmptyResult policy), then the remote end cleans up and tears the connection down (a post-end command fails). Direct parity tests bidi_003_session_end_{wire_and_result, result_policy, malformed_result_rejected, error_propagation, live_chrome} (src/bidi/session.rs). CLI/MCP reuse the existing bl stop / browser_stop intentions; no new surface.
Native session.subscribe: Client::subscribe_events sends {events, contexts?/userContexts?} — the engine's Scope enum (global/context/userContext) makes the spec's invalid contexts+userContexts combination unrepresentable — with per-connection refcounting so only 0→1 transitions hit the wire; an async lifecycle lock serializes add/send/commit operations, and the SubscribeResult subscription id is decoded strictly (non-empty text required when present; an empty/absent result is the documented pre-id remote-end tolerance) and captured for id-based unsubscribe. Recovery is transactional and owned by the method: every failure releases the FULL original request (overlap bumps included, surviving claims untouched); an ambiguous global outcome is compensated with the spec-valid attributes form and the connection is invalidated if cleanup cannot be confirmed; an ambiguous scoped outcome is invalidated immediately because no unsubscribe is expressible without the missing id. Mux ConnLost recovery then establishes a clean process/session rather than retaining an orphan. The engine router and WebSocket monitor drive the same command for their event lanes.
params.events is required ([+text]); contexts/userContexts are optional scoping members driven by the engine's Scope — never sent together (the spec returns invalid argument for that combination). Result {subscription: text} is decoded and retained internally; subscription ids and refcounts are never a user concern.
Native session.unsubscribe: Client::unsubscribe_events refcounts so only 1→0 drops hit the wire, using the spec's UnsubscribeByAttributesRequest {events} for global subscriptions and UnsubscribeByIDRequest {subscriptions} with ids captured at subscribe time for scoped ones (the current spec's attributes form removes global subscriptions only); the EmptyResult is validated and protocol errors (invalid argument) propagate. Bookkeeping is two-phase (begin/commit/abort in the SubscriptionManager): removal commits only after the remote end executed the command, so a failed unsubscribe restores the claims — ids intact — and a retry re-sends.
Both spec request forms are engine-driven: the attributes form {events: [+text]} for global drops and the id form {subscriptions: [+session.Subscription]} for scoped drops — users never pass subscription ids. Returns EmptyResult, validated (object or the prose's null accepted; anything else rejected as malformed).
Verified live 2026-07-10 on Chrome for Testing / ChromeDriver 150.0.7871.49: after unsubscribe, a wait:'complete' navigation emits NO browsingContext.load — proven order-deterministically (re-subscribe + a third navigation delivered only the third's event, and BiDi events on one connection are ordered); unsubscribing a not-subscribed event -> 'invalid argument - No subscription found' (spec §7.1.3.5). Direct parity tests bidi_005_session_unsubscribe_{attributes_wire_and_result, by_id_wire, malformed_result_rejected, error_propagation, live_chrome} (src/bidi/session.rs), with the error/malformed tests also proving retry-after-failure re-sends and retry-after-commit no-ops. Exposure stays internal; subscription cleanup is engine-owned.
Native browser.close: Client::close_browser sends the spec's EmptyParams (exactly {} on the wire) and validates the CloseResult = EmptyResult success (the remote end prose returns 'success with data null'; Chrome replies result: {}). Per the remote end steps the requesting session is ended BEFORE the response is written and the browser then closes its top-level traversables without prompting to unload and shuts down its OS processes — so a success means the connection is about to die: expected lifecycle, not a fault (the Mux observes the shutdown as its natural ConnLost after the command has already resolved). The spec's 'unable to close browser' error (§3.5, returnable while other sessions are active) propagates verbatim as a definitive protocol error. Ambiguity policy: like setClientWindowState and unlike createUserContext, a malformed success or timeout does NOT invalidate the connection — the command creates no browser-side resource whose only handle lives in the lost response; if the close executed the socket dies on its own, and if it did not the connection is genuinely usable. The daemon CONSUMES the command: BrowserProcessHost::close asks every OWNED browser process to shut itself down via browser.close (5s teardown bound, best-effort) before the process-tree kill that stays as the guaranteed backstop (and reaps chromedriver itself) — the path behind the runtime's own daemon-wide shutdown, an --isolated session's close/stop, and pool eviction. Remote connections (connect_url) deliberately keep session.end instead: browser.close would terminate a browser instance the daemon does not own.
EmptyParams — the spec defines no members, so nothing is user-suppliable and the engine puts exactly {} on the wire. No dedicated CLI command or MCP tool: terminating the owned browser is already an expressed intention — the runtime's internal daemon-wide shutdown closes every owned process, and closing/stopping an --isolated session (CLI session close/stop; MCP browser_session_close/browser_stop) closes its dedicated one — all through the same engine path, with the teardown's 5s bound and the unconditional process reap engine-internal. Remote endpoints keep session.end (the far side owns that browser's lifecycle).
Exposure decision
Internal
CLI
Internal · Internal
MCP
browser_session_close · Shipped (partial) · Public · profile: core
Skill workflow
Not applicable — no user-facing CLI workflow
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (WPT has no bidi/browser close suite — closing the browser ends the harness session — so the direct live test is the evidence)
Verified live 2026-07-11 on Chrome for Testing / ChromeDriver 150.0.7871.49: browser.close over the launcher's BiDi session.new connection answers the spec's EmptyResult success — proving the response is delivered before shutdown — then the Chrome main process parented by this launch's chromedriver observably exits (live tab included) and further use of the ended session fails (Chrome 150 resets the socket without a closing handshake; recorded as-is). Direct parity tests bidi_006_close_{wire_and_result, result_policy, success_then_remote_shutdown, error_propagation, malformed_result_keeps_connection_usable, timeout_keeps_connection_usable, live_chrome} (src/bidi/browser.rs) — success_then_remote_shutdown pins the spec's expected success-then-socket-death lifecycle; the two *_keeps_connection_usable tests pin the no-invalidation ambiguity policy (termination leaves nothing to leak; the lifecycle layer's unconditional reap owns the guarantee). Daemon-branch regressions bidi_006_host_close_{sends_browser_close_to_owned_process, keeps_session_end_for_remote_process} (src/daemon/process_pool.rs) pin the ownership rule at the wire: an owned process gets the graceful browser.close before the reap, a remote endpoint keeps session.end and never sees browser.close. Exposure: the prior recommended-raw browser_bidi_call idea is dropped — the capability now natively powers the existing lifecycle surfaces (the runtime's internal daemon-wide shutdown; --isolated session close/stop per-browser, CLI and MCP browser_session_close/browser_stop alike), so MCP is partial-semantic via the session tools and the daemon-wide stop stays internal to the runtime's own lifecycle.
Native browser.createUserContext: Client::create_user_context sends the spec's CreateUserContextParameters — every optional member (acceptInsecureCerts, proxy, unhandledPromptBehavior; §7.2.4.2) is engine-representable and serializes with its exact spec name, the default serializing to {} (byte-identical to the pre-typed wire traffic); proxy/unhandledPromptBehavior are validated as maps before anything reaches the browser. The result decodes strictly per browser.UserContextInfo (§7.2.3.4): userContext must be non-empty text (extensible extra members tolerated; a missing/empty/wrong-typed id is malformed), and protocol errors — notably the spec's unsupported operation for an unhonorable acceptInsecureCerts/proxy — propagate. Recovery treats the command as the mutation it is (the spec appends the context to the set of user contexts BEFORE returning success, and without the returned id no removeUserContext is expressible): a definitive BiDi error created nothing and propagates with the connection intact, while a malformed success (executed, id unknowable) or a timeout (may still execute) is an ambiguous mutating outcome that invalidates the connection — the Mux emits ConnLost and the daemon recycles the process/session into a clean epoch instead of retaining an unreachable context in a warm browser. The daemon orchestrator's named-session creation (shared and --isolated; every named session gets its own created user context) drives the typed method — a definitive failure unwinds (isolated reap; a shared session created nothing) — and the engine proxy route browserlane:browser.newContext decodes through the same value-level parser and surfaces BiDi errors instead of masking them as an empty-id success.
All spec CreateUserContextParameters members are optional and engine-representable: acceptInsecureCerts (bool), proxy (session.ProxyConfiguration map, preserved verbatim), unhandledPromptBehavior (session.UserPromptHandler map, preserved verbatim); the spec default — omit a member, record no per-context override — is the engine default and serializes to {}. bl session new / browser_session_create intentionally send the defaults; the browser-generated userContext id stays internal, mapped to the stable Browserlane session name.
Exposure decision
Semantic public
CLI
bl session new <name> · Shipped · Public
MCP
browser_session_create · Shipped · Public · profile: core
Skill workflow
Documented · bl session new <name> (teaches the CLI; not a separate implementation)
Verified live 2026-07-10 on Chrome for Testing / ChromeDriver 150.0.7871.49: default-params create returned UserContextInfo {userContext: 'A466A6736FF4…'}; the context is observably real — a tab created with that userContext reports it in browsingContext.getTree; acceptInsecureCerts:true was accepted and returned a second distinct context. proxy/unhandledPromptBehavior are wire-verified (exact spec member names, nothing else on the frame) but not exercised live. Direct parity tests bidi_007_create_user_context_{wire_and_result, optional_params_wire, invalid_params_rejected_before_send, result_policy, error_propagation, malformed_result_invalidates_connection, timeout_invalidates_connection, bidi_error_keeps_connection_usable, live_chrome} (src/bidi/browser.rs) — the last three are the ambiguous-mutating-outcome regressions: malformed success and timeout invalidate the connection (ConnLost published, tainted client refuses further use), a definitive BiDi error does not. Exposure unchanged: bl session new <name> [--isolated] / browser_session_create; the per-context TLS/proxy/prompt overrides stay engine-internal until they represent a distinct user intention.
Native browser.getClientWindows: Client::get_client_windows sends the spec's EmptyParams and decodes GetClientWindowsResult strictly — clientWindows must be a list (the CDDL's * occurrence permits an empty one, unlike getUserContexts' +) of ClientWindowInfo maps carrying all seven required §7.2.3.2 members (active bool, non-empty clientWindow id, state one of fullscreen/maximized/minimized/normal, js-uint width/height, js-int x/y; extensible extra members tolerated); a malformed row is an error, never a zero-valued default — the pre-typed lenient decode could produce an empty clientWindow id and then target setClientWindowState at it. The semantic path engine::get_window (MCP browser_get_window, the engine-internal CLI window path, proxy route browserlane:page.window) decodes through the same value-level parser and keeps its long-standing selection — the first reported window — mapped to the public WindowInfo shape {state,x,y,width,height}; the clientWindow id and active flag stay engine-internal.
EmptyParams — nothing is user-supplied. The engine returns every reported window (deduplicated by the remote end); browser_get_window (and the internal CLI window path) intentionally answers for the session's first window, and the browser-generated clientWindow ids stay internal as setClientWindowState targets (per the 2026-07-02 multi-window verdict, enumeration is deliberately unexposed).
Exposure decision
Semantic public
CLI
Internal · Internal
MCP
browser_get_window · Shipped · Public · profile: testing
Verified live 2026-07-10 on Chrome for Testing / ChromeDriver 150.0.7871.49: a launched headless browser reports one ClientWindowInfo with all seven spec members present and typed (active:false under headless, state 'normal', real geometry); every top-level context's clientWindow in browsingContext.getTree appears in the list; the spec's dedup step is observable — a second TAB does not grow the list; and the WPT open-and-close contract holds for windows — a browsingContext.create type:'window' adds a second, DISTINCT clientWindow id (1→2; observed id 918596466 alongside 918596463, cascaded at 44,44) and closing that context restores the original list (2→1). Direct parity tests bidi_008_get_client_windows_{wire_and_result, result_policy, error_propagation, live_chrome} (src/bidi/browser.rs) plus engine-lane strict-decode regressions get_window_{maps_spec_info_to_public_shape, rejects_malformed_window_row} (src/engine/handlers_emulation.rs). Exposure: browser_get_window (MCP) is the public surface and reports the first window; the CLI-side window path is internal. The full list, ids and active flag are engine-internal.
Native browser.getUserContexts: Client::get_user_contexts sends EmptyParams and decodes the spec's GetUserContextsResult strictly — userContexts must be a non-empty list (the CDDL's + occurrence: the default user context always exists) of UserContextInfo maps whose userContext is non-empty text; a malformed result is an error, never a silently-empty or lossy list. The daemon's per-process user-context reconciliation audit (leaked/missing drift vs the Session Pool, 2s bound per launched process, surfaced by the runtime's internal status reporting) consumes the typed method, and a decode failure now yields an explicit audit-unavailable row instead of false 'missing' drift. bl session list / browser_session_list enumerate the session registry by design — the browser-truth list backs the reconciliation audit that keeps the registry honest.
EmptyParams — nothing is user-supplied. The result's browser-generated userContext ids stay internal: the daemon diffs them against session-owned ids for drift diagnostics ('default' never counts as leaked), and bl session list reports stable Browserlane session names with their userContext ids alongside.
Exposure decision
Semantic public
CLI
bl session list · Shipped · Public
MCP
browser_session_list · Shipped · Public · profile: core
Skill workflow
Documented · bl session list (teaches the CLI; not a separate implementation)
Verified live 2026-07-10 on Chrome for Testing / ChromeDriver 150.0.7871.49: a fresh browser reports exactly ['default'] (the spec's + occurrence is real); after two createUserContext calls the list contains default plus both new ids and nothing else. Direct parity tests bidi_009_get_user_contexts_{wire_and_result, result_policy, error_propagation, live_chrome} (src/bidi/browser.rs). Exposure unchanged: bl session list / browser_session_list list Browserlane sessions from the registry; the raw browser-truth list feeds the runtime's internal reconciliation audit.
Native browser.removeUserContext: Client::remove_user_context sends the spec's RemoveUserContextParameters {userContext} (an empty id is rejected client-side before send) and validates the EmptyResult ({} or the prose's null accepted, anything else rejected); the spec's protocol errors propagate — invalid argument for the protected 'default' id (§7.2.4.5 step 2), no such user context for an unknown one (step 4). Named shared-session teardown (bl session close/reset, browser_session_close, idle eviction) and partial-create unwind drive the 5s-bounded variant best-effort — the browser's default user context is never removed (OQ-8: the default session closes contexts individually) and a leaked context is surfaced by the getUserContexts audit. The engine proxy route browserlane:context.close validates the same shapes through the shared helpers and surfaces BiDi errors instead of reporting success.
userContext is required (browser.UserContext text) and engine-derived: callers supply the stable Browserlane session name, never the browser id. An empty id is rejected before anything is sent; teardown ordering and the 'default' guard (OQ-8) are internal, while the spec's invalid-argument error for 'default' propagates where the id is caller-controlled (proxy lane).
Exposure decision
Semantic public
CLI
bl session close <name> · Shipped · Public
MCP
browser_session_close · Shipped · Public · profile: core
Skill workflow
Documented · bl session close <name> (teaches the CLI; not a separate implementation)
Verified live 2026-07-10 on Chrome for Testing / ChromeDriver 150.0.7871.49: removing a created context returned the spec EmptyResult and was observably effective — its tab left browsingContext.getTree and its id left browser.getUserContexts; removing it again -> 'no such user context - Failed to find context with id …' (spec step 4); removing 'default' -> 'invalid argument - `default` user context cannot be removed' (spec step 2). Direct parity tests bidi_010_remove_user_context_{wire_and_result, malformed_result_rejected, error_propagation, rejects_empty_id_before_send, live_chrome} (src/bidi/browser.rs). Exposure unchanged: bl session close <name> / browser_session_close.
Native browser.setClientWindowState: Client::set_client_window_state sends the spec's SetClientWindowStateParameters — clientWindow plus the CDDL group choice ClientWindowNamedState (fullscreen/maximized/minimized: exactly {clientWindow, state} on the wire) or ClientWindowRectState (state 'normal' plus only the geometry members that were given; the spec default — omit ⇒ leave that dimension untouched — serializes as absence, never null). An empty window id or geometry outside the protocol's js-uint/js-int bounds is rejected client-side before anything reaches the browser. The result decodes strictly per SetClientWindowStateResult = browser.ClientWindowInfo (§7.2.4.6: the post-change info under the spec's synchronous model), and protocol errors propagate (unsupported operation, the unknown-window error, invalid argument) WITHOUT connection invalidation — unlike createUserContext no browser-side resource's only handle lives in the response; window state is re-queryable and re-settable. The semantic path engine::set_window (MCP browser_set_window, the engine-internal CLI window path, proxy route browserlane:page.setWindow) targets the first reported window through the same wire builder and validates the result through the same parser; the classic-HTTP chromedriver fallback remains for LOCAL endpoints predating the command (unsupported operation / unknown command only) while remote errors surface as-is.
clientWindow is required and engine-derived (the first window from getClientWindows — browser-generated ids are never user-supplied); geometry-only input implies the rect state ('normal'); width/height (js-uint) and x/y (js-int) are optional, omitted members leave that dimension untouched per spec, and bounds are validated before send (negatives rejected with a clear message instead of a raw browser invalid argument). Named states intentionally carry no geometry — the CDDL group has none, and caller geometry is not silently forwarded.
Exposure decision
Semantic public
CLI
Internal · Internal
MCP
browser_set_window · Shipped · Public · profile: testing
Verified live 2026-07-10 on Chrome for Testing / ChromeDriver 150.0.7871.49: the rect state applied exactly (normal 1004x768@7,42 echoed in the returned ClientWindowInfo and confirmed by a subsequent getClientWindows); every named state echoes in the result (maximized 800x600@0,0, fullscreen 800x675@0,0, minimized — the headless virtual screen). Two Chrome deviations recorded: the DIRECT fullscreen→minimized transition is rejected ('unknown error - To minimize a fullscreen window, restore it to normal state first.'; WPT's transition matrix includes that pair), and an unknown window id answers code 'invalid argument' with message 'no such client window' rather than the spec's dedicated no-such-client-window code (WPT currently expects 'unknown error'; Chrome does neither). Direct parity tests bidi_011_set_client_window_state_{wire_named_states, wire_rect_state, invalid_params_rejected_before_send, result_policy, error_propagation, malformed_result_keeps_connection_usable, timeout_keeps_connection_usable, live_chrome} (src/bidi/browser.rs) plus engine-lane regressions set_window_bidi_rejects_{negative_geometry_before_send, malformed_set_result} (src/engine/handlers_emulation.rs) — the two *_keeps_connection_usable tests pin the ambiguity policy: a malformed success and a post-send timeout surface as errors with NO connection invalidation (Mux live, no ConnLost, the same client re-queries), the deliberate contrast with BIDI-007's response-only-handle recovery. Exposure: browser_set_window (MCP) is the public surface; the CLI-side window path is internal.
Native browser.setDownloadBehavior: Client::set_download_behavior sends the spec's SetDownloadBehaviorParameters — the required-but-nullable downloadBehavior member is always present (the CDDL group choice DownloadBehaviorAllowed {type:'allowed', destinationFolder} / DownloadBehaviorDenied {type:'denied'}, or the literal null that RESETS: per the remote end steps it clears the default download behavior, or removes the named contexts' overrides when userContexts is given), and the optional userContexts list serializes only when given — the spec default (omit ⇒ the DEFAULT download behavior changes) is the engine default. The CDDL's + occurrence makes only an empty userContexts list structurally invalid; destinationFolder and browser.UserContext are unrestricted text, so even empty strings are preserved verbatim for the remote end to interpret. The result decodes strictly per SetDownloadBehaviorResult = EmptyResult and protocol errors propagate (invalid argument, the 'get valid user contexts' step's no such user context, unsupported operation) WITHOUT connection invalidation — like setClientWindowState and unlike createUserContext, download behavior is fully re-settable and no browser-side resource's only handle lives in the response, so an ambiguous outcome (malformed success or timeout) surfaces as a plain error and callers re-issue. Both semantic lanes drive the shared typed layer: the CLI/MCP handler browser_download_set_dir calls the typed Client method (named sessions scope to their user context via userContexts; the default session stays process-wide), and the proxy router's session bring-up (setup_downloads: every session auto-allows downloads into a per-session temp dir) builds its wire params through the same struct and validates the raw envelope through check_bidi_error + the same value-level EmptyResult parser — before this, a BiDi ERROR envelope on that lane resolved Ok and was silently treated as success, now pinned by direct router-response regressions.
All spec SetDownloadBehaviorParameters members are engine-representable: downloadBehavior is required-but-nullable (allowed{destinationFolder} / denied / null = reset per the remote end steps) and userContexts ([+browser.UserContext]) is optional — omitted, the default download behavior changes. bl download dir <path> / browser_download_set_dir intentionally expose the one human intention, set where downloads go: the destination is caller-supplied (created and absolutized first), userContexts is engine-derived (a named session scopes to its own user context; ids are never user-supplied), and the denied/null arms stay engine-internal until they represent a distinct user intention. The engine preserves every spec text value verbatim, including empty destinationFolder and user-context strings; only an empty userContexts list is rejected before send because [+] requires at least one entry.
Verified live 2026-07-11 on Chrome for Testing / ChromeDriver 150.0.7871.49: allowed{destinationFolder} observably steers a triggered <a download> download into the folder (downloadWillBegin + downloadEnd status 'complete', filepath inside the canonicalized dir, fixture bytes on disk); denied cancels it (downloadEnd status 'canceled', no file written) on both the global and per-user-context lanes; a per-context override wins over the global default without leaking to other contexts, and the null reset removes the override (observable fall-back to the still-set global allowed dir); the global null reset answers the spec's EmptyResult; empty destinationFolder text reaches Chrome and succeeds, while an empty user-context text reaches the get-valid-user-contexts step and answers no such user context; another unknown user-context id answers the same spec error ('User context bl-no-such-user-context not found'). Two Chrome 150 observations recorded: downloadWillBegin/downloadEnd params carry NO 'download' id member (the WD 2026-06-29 productions include download: browsingContext.Download), and Chrome throttles repeated downloads from a stale document — the live fixture navigates to a fresh page per trigger, exactly like the WPT harness. Direct parity tests bidi_012_set_download_behavior_{wire_allowed_and_result, wire_denied_null_and_contexts, empty_text_values_reach_wire, invalid_params_rejected_before_send, result_policy, error_propagation, malformed_result_keeps_connection_usable, timeout_keeps_connection_usable, live_chrome} (src/bidi/browser.rs), plus router regressions set_download_behavior_response_{rejects_bidi_error_envelope, applies_empty_result_policy} (src/engine/handlers_download.rs). The two *_keeps_connection_usable tests pin the ambiguity policy shared with BIDI-011 (no invalidation: a re-settable property). Exposure unchanged: bl download dir <path> / browser_download_set_dir; denied/null and multi-context scoping stay engine-internal.
Native browsingContext.activate: ActivateParameters represents the command's sole required context text member and Client::activate_context sends the exact method, preserves the context text unchanged, validates ActivateResult = EmptyResult, and propagates no such frame, invalid argument, and unsupported operation without invalidating the connection. The existing semantic bl tab switch <tab> and browser_tab_switch surfaces resolve a session-scoped tab index/URL to its browser-generated context id and call engine::switch_tab; engine::switch_tab and the proxy router's browserlane:tab.activate handler now build the same typed wire shape and validate both raw BiDi error envelopes and the shared EmptyResult policy. This fixes the previous router behavior where send_internal_command returned an error envelope as Ok and activation could be reported as success.
ActivateParameters has exactly one required member, context (browsingContext.BrowsingContext text), and no optional parameters or defaults; ActivateResult is EmptyResult. The engine preserves any text value verbatim so the remote end produces the specified no such frame response for unknown/empty ids. The semantic CLI/MCP surfaces derive the context id from the session-scoped tab list after the caller chooses a tab by index or URL substring, so users do not coordinate raw browser-generated ids.
Exposure decision
Semantic public
CLI
bl tab switch <tab> · Shipped · Public
MCP
browser_tab_switch · Shipped · Public · profile: core
Skill workflow
Documented · bl tab switch <tab> (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_013_activate_live_chrome)
Verified live 2026-07-11 on Chrome for Testing / ChromeDriver 150.0.7871.49: activating a background tab changed document status from hidden:false to visible:true; the tab gained system focus while preserving document.activeElement ('kept-focused'); repeated activation remained successful and preserved focus. Empty and unknown context ids answered no such frame ('Context … not found'); activating a child iframe answered invalid argument ('Activation is only supported on the top-level context'). Direct Client→Mux→Connection parity tests bidi_013_activate_{wire_and_result, result_policy, empty_context_reaches_wire, error_propagation, live_chrome} cover exact wire shape, EmptyResult decoding, no such frame, invalid argument, unsupported operation, and connection reuse. engine-lane tests bidi_013_switch_tab_{wire_and_result_policy, rejects_bidi_error_envelope, empty_context_reaches_wire} prove the existing CLI/MCP semantic path shares the typed shape and no longer silently accepts raw error envelopes. Exposure remains semantic-public: bl tab switch <tab> and browser_tab_switch are distinct, useful human/agent intentions; Skill remains documented from the existing CLI workflow.
Native browsingContext.captureScreenshot: CaptureScreenshotParameters represents required context and every optional production — origin (viewport/document), ImageFormat (unrestricted MIME type plus quality 0..1), and both BoxClipRectangle and ElementClipRectangle with the complete script.SharedReference (sharedId plus optional handle). Optional members serialize only when supplied so the remote defaults remain authoritative (origin viewport, format PNG, clip equal to the full origin rectangle); finite float validation rejects values JSON cannot represent while negative box dimensions remain valid for the spec's normalization algorithm. Client::capture_screenshot_with sends the exact method and strictly decodes CaptureScreenshotResult {data:text}; protocol errors propagate without invalidating the reusable connection. The existing bl screenshot / browser_screenshot semantic paths derive the active session context and call engine::screenshot, which now builds the same typed params and shares strict raw-envelope/result validation with the proxy page and element screenshot routes, fixing malformed successes that previously became empty data.
All CaptureScreenshotParameters members are engine-representable: required context; optional origin ('viewport'/'document', default viewport); optional format {type:text, quality?:0..1} (omitted means image/png); and optional clip as either box {x,y,width,height floats} or element {element: script.SharedReference{sharedId,?handle}} (omitted means the complete origin rectangle). bl screenshot / browser_screenshot expose the common semantic choices: active context is engine-derived, default capture is viewport PNG, and --full-page/fullPage selects document origin; annotation, file saving, and direct image return remain Browserlane semantics. MIME/quality and raw clip/reference primitives remain engine-accessible rather than expanding the curated public surface until a distinct user workflow requires them.
Exposure decision
Semantic public
CLI
bl screenshot [url] [--full-page] [--annotate] · Shipped · Public
MCP
browser_screenshot · Shipped · Public · profile: core
Skill workflow
Documented · bl screenshot [url] [--full-page] [--annotate] (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_014_capture_screenshot_live_chrome)
Verified live 2026-07-11 on Chrome for Testing / ChromeDriver 150.0.7871.49 without activating the background capture tab: default capture returned a decodable 756x469 PNG viewport; document origin produced a taller image; image/jpeg returned JPEG bytes and quality 1 encoded more data than quality 0.1; a 120x80 CSS-pixel box and the same element via its real script.SharedReference produced equal physical dimensions at the observed DPR. Unknown context answered no such frame, unknown sharedId answered no such node, and a zero-area box answered unable to capture screen. Direct tests bidi_014_capture_screenshot_{wire_defaults_and_result, wire_optional_parameters, parameter_validation, result_policy, error_propagation, live_chrome} cover exact wire/defaults, every optional production, strict result decoding, finite/range validation, normative errors, and connection reuse; engine::handlers_capture::bidi_014_capture_response_validation pins raw-envelope and malformed-success behavior. Exposure remains semantic-public via bl screenshot and browser_screenshot; existing viewport/fullPage choices are reused unchanged, so Skill remains documented.
Native browsingContext.close: Client::close_context sends the spec's CloseParameters — the required context is unrestricted browsingContext.BrowsingContext text and is preserved verbatim (including an empty string, which reaches the remote end's no such frame semantics), while optional promptUnload serializes only when given so the spec default (omit ⇒ close WITHOUT prompting to unload) is the engine default. The success validates strictly per CloseResult = EmptyResult and the spec's protocol errors propagate (no such frame for an unknown context, invalid argument for a non-top-level navigable) WITHOUT connection invalidation — like setClientWindowState and unlike createUserContext, closing is re-issuable, no browser-side resource's only handle lives in the response, and an already-executed close answers a definitive no such frame on the retry. Every lane drives the shared typed layer: the MCP/CLI handler browser_tab_close resolves the tab index and calls engine::close_tab, which builds its wire params through CloseParameters and validates the raw envelope through check_bidi_error + the same value-level EmptyResult parser (before this, the proxy router's send_internal_command resolved a BiDi ERROR envelope as Ok and handle_tab_close/close_tab silently treated a no such frame as success — now pinned by direct regressions); the proxy router's handle_tab_close shares the identical validation; and the daemon orchestrator's teardown/launch-tab-cleanup paths use the typed close_context_with_timeout (5s teardown bound).
All spec CloseParameters members are engine-representable: context (required browsingContext.BrowsingContext text, preserved verbatim even when empty) and promptUnload (optional bool, .default false — omitted, the navigable closes without prompting to unload). bl tab close [index] / browser_tab_close intentionally expose the one human intention, close a tab: the context id is engine-derived from the session's tab list (never user-supplied), the active tab is the default target, and promptUnload stays engine-internal until prompting-on-close represents a distinct user intention (the spec default is exactly today's behavior).
Exposure decision
Semantic public
CLI
bl tab close · Shipped · Public
MCP
browser_tab_close · Shipped · Public · profile: core
Skill workflow
Documented · bl tab close (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_015_close_live_chrome; WPT wpt.fyi bidi/browsing_context suites corroborate but the live test is the evidence)
Verified live 2026-07-11 on Chrome for Testing / ChromeDriver 150.0.7871.49: close answers the spec's EmptyResult and observably removes the top-level traversable from browsingContext.getTree; a repeat close of the same id answers the spec's no such frame ('Context … not found'); an empty context string also reaches Chrome and answers no such frame; closing a child frame (iframe fixture via a data: URL) answers the spec's invalid argument ('Non top-level browsing context … cannot be closed.'); an explicit promptUnload: true closes a tab showing a plain page. Direct parity tests bidi_015_close_{wire_and_result, wire_prompt_unload, result_policy, empty_context_reaches_wire, error_propagation, live_chrome} (src/bidi/browsingcontext.rs), plus engine-lane regressions bidi_015_close_tab_{wire_and_result_policy, rejects_bidi_error_envelope, empty_context_reaches_wire} (src/engine/handlers_lifecycle.rs) — rejects_bidi_error_envelope pins the silent-error fix: the proxy router's send_internal_command resolves a BiDi ERROR envelope as Ok, and before this task handle_tab_close/close_tab reported success on a no such frame. Exposure unchanged: bl tab close / browser_tab_close; promptUnload stays engine-internal (the spec default false is exactly the surface's semantics).
Native browsingContext.create: Client::create_context sends the spec's CreateParameters — the required type ('tab' / 'window') plus the three optionals referenceContext, background, and userContext, each serialized only when given so every spec default (omit referenceContext ⇒ default/userContext's user context; omit background (.default false) ⇒ the new navigable is activated; omit userContext ⇒ the reference navigable's, else the default, user context) is the engine default. referenceContext and userContext are unrestricted spec text and are preserved verbatim, including empty strings that reach the remote end's no such frame / no such user context semantics. The result decodes strictly per CreateResult {context, ?userContext}: context must be non-empty text, a PRESENT userContext must be non-empty text, and a missing userContext alone is tolerated (optional in the CDDL — Chrome 150 omits it). Protocol errors propagate (no such frame for an unknown referenceContext, invalid argument for a non-top-level reference, no such user context, unsupported operation) WITHOUT connection invalidation. The deliberate contrast with createUserContext is ownership, not mere enumerability: browsingContext.getTree reports every navigable together with its EXISTING userContext, so Browserlane can deterministically reattach it to that session's ctx_index and Mux event lanes; browser.getUserContexts can enumerate an orphan new user-context id but cannot attribute it to the Browserlane session request whose response was lost. The malformed-result and timeout tests prove the lost-response navigable remains visible in getTree, while bidi_016_default_context_reconciliation_adopts_tree_discovered_context proves the daemon adopts both registry ownership and the event route. Every lane drives the shared typed layer: Client::create_tab and the daemon orchestrator's session bring-up/rotation route through create_context; engine::new_tab (the MCP/CLI handler browser_tab_new's engine path) and the proxy router's handle_browser_tab_new/handle_context_new_tab build wire params through CreateParameters and validate the raw envelope through check_bidi_error + the same value-level CreateResult parser — before this, a BiDi error envelope on the router lane degraded to a meaningless 'no context in create response'.
All spec CreateParameters members are engine-representable: type ('tab' / 'window', required), referenceContext (optional browsingContext.BrowsingContext text, preserved verbatim even when empty), background (optional bool, .default false — omitted, the new navigable is activated), and userContext (optional browser.UserContext text, likewise preserved verbatim). bl tab new [url] / browser_tab_new intentionally expose the one human intention, open a new tab: type is 'tab' (the everyday shape), userContext is engine-derived (a named session scopes to its own user context via the ''/'default'-implicit convention for byte-identical default-path wire traffic; ids are never user-supplied), the URL is a caller choice navigated after creation, and window/referenceContext/background stay engine-internal until they represent distinct user intentions.
Exposure decision
Semantic public
CLI
bl tab new [url] · Shipped · Public
MCP
browser_tab_new · Shipped · Public · profile: core
Skill workflow
Documented · bl tab new [url] (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_016_create_live_chrome; WPT wpt.fyi bidi/browsing_context suites corroborate but the live test is the evidence)
Verified live 2026-07-11 on Chrome for Testing / ChromeDriver 150.0.7871.49: create type=tab, type=window, background: true, referenceContext, and a created userContext each return a fresh navigable id that observably appears as a new top-level context in browsingContext.getTree with the expected user context (default for the tab/window/background/reference cases; the created user context for the scoped case); unknown and empty userContext strings answer the spec's no such user context, while unknown and empty referenceContext strings answer the spec's no such frame. One Chrome 150 observation recorded: ChromeDriver omits the CreateResult's optional userContext member entirely (the WD 2026-06-29 remote end steps set it; the CDDL marks it optional) — the engine tolerates absence and the tree proves the scoping. Direct parity tests bidi_016_create_{wire_tab_defaults_and_result, wire_window_and_optional_params, tab_params_convention, empty_text_values_reach_wire, result_policy, error_propagation, malformed_result_keeps_connection_usable, timeout_keeps_connection_usable, live_chrome} (src/bidi/browsingcontext.rs) — both ambiguous-outcome tests query getTree and prove the lost-response resource is discoverable without invalidation — plus daemon regression bidi_016_default_context_reconciliation_adopts_tree_discovered_context (src/daemon/orchestrator.rs), which proves ctx_index ownership and the Mux event route are restored, and engine-lane regressions bidi_016_new_tab_{wire_and_strict_result, rejects_bidi_error_envelope, rejects_malformed_result} (src/engine/handlers_lifecycle.rs). Exposure unchanged: bl tab new [url] / browser_tab_new; window/referenceContext/background stay engine-internal.
Native browsingContext.getTree: GetTreeParameters represents both optional members, root (unrestricted BrowsingContext text) and maxDepth (validated js-uint 0..2^53-1), serializing only supplied members so omission preserves the remote defaults (all top-level traversables, unlimited descendants). Client::get_tree_with sends the exact method and strictly decodes GetTreeResult and every required browsingContext.Info member: children preserves null (depth truncation) versus an empty list, clientWindow, context, originalOpener preserves null/text, url, userContext, and optional nullable parent preserves absent/null/text. Protocol errors propagate without invalidating the reusable connection. Tab-list, context-tree, router context selection, storage context lookup, and both router/standalone frame lanes share typed params plus the strict raw-envelope/result parser instead of silently defaulting malformed fields. A dedicated PublicContextInfo projection keeps bl tabs --tree --json / browser_context_tree byte-shape compatible with the historical contract: spec-only clientWindow/originalOpener remain internal, nullable/empty children remain omitted publicly, and the existing context/url/children/parent/userContext keys retain their prior rules.
Both GetTreeParameters members are engine-representable: root is optional browsingContext.BrowsingContext text (omitted means all top-level traversables; an empty/unknown id reaches the remote no-such-frame semantics), and maxDepth is optional js-uint 0..2^53-1 (omitted means unlimited descendants; 0 returns selected roots with children:null). bl tabs / browser_tab_list intentionally present session-scoped top-level tabs; bl tabs --tree / browser_context_tree present the complete session-scoped hierarchy. Raw root ids and depth limits remain engine-accessible protocol controls rather than public coordination requirements because the semantic surfaces derive session ownership and return the useful complete view.
Exposure decision
Semantic public
CLI
bl tabs / bl tabs --tree · Shipped · Public
MCP
browser_tab_list / browser_context_tree · Shipped · Public · profile: core
Skill workflow
Documented · bl tabs / bl tabs --tree (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_017_get_tree_live_chrome)
Verified live 2026-07-11 on Chrome for Testing / ChromeDriver 150.0.7871.49 with a deterministic two-level iframe fixture: rooted unlimited getTree returned root→child→grandchild with complete Info data; maxDepth 0 returned root children:null; maxDepth 1 returned the child with children:null; querying the child as root reported parent equal to the top-level context; the maximum js-uint depth was accepted; a global tree included both default and created-user-context tabs with correct userContext attribution. Empty and unknown roots answered no such frame. Direct tests bidi_017_get_tree_{wire_defaults_and_result, wire_optional_parameters, max_depth_validation, result_policy, error_propagation, live_chrome} cover exact defaults/wire, both options and boundaries, strict required Info decoding, nullable-state preservation, protocol errors, and connection reuse. API regressions bidi_017_get_tree_response_validation, bidi_017_{router_frame_tree,standalone_list_frames}_rejects_malformed_response, and bidi_017_public_context_tree_preserves_historical_json pin every semantic lane plus the stable public JSON projection. Exposure remains semantic-public through existing tabs/context-tree workflows; Skill remains documented.
Native browsingContext.handleUserPrompt: HandleUserPromptParameters represents required context and the independent optional accept and userText members, preserving explicit false and empty text while omission leaves the remote defaults authoritative (accept=true, userText=''). Client::handle_user_prompt sends the exact method, validates HandleUserPromptResult = EmptyResult, and propagates no such frame/no such alert without invalidating the reusable connection. The existing bl dialog accept [text] / dismiss and browser_dialog_accept / browser_dialog_dismiss semantic paths derive the active session context and share the typed wire builder plus raw-envelope/EmptyResult validation with the proxy router. Prompt-open/closed events remain the existing internal inspection/wait mechanism and are not conflated with this command.
All HandleUserPromptParameters members are engine-representable: required context (BrowsingContext text), optional accept bool (omitted defaults true), and optional userText text (omitted defaults empty string; explicit empty text remains representable). bl dialog accept [text] / browser_dialog_accept expose positive handling and optional prompt text; bl dialog dismiss / browser_dialog_dismiss expose negative handling; context is engine-derived from the active session tab. Dialog info/wait use prompt events rather than inventing a query mode for this command.
Verified live 2026-07-11 on Chrome for Testing / ChromeDriver 150.0.7871.49 under unhandledPromptBehavior=ignore: alert handling with accept/userText omitted closed the alert and resumed script; confirm accept=true/false produced true/false; prompt acceptance returned supplied text and dismissal returned null; attempting to handle the open prompt from another top-level context answered no such alert; no-open-prompt and unknown-context calls answered no such alert and no such frame. Prompt opened/closed events made each action deterministic and the live test unsubscribed/closed both fixture tabs. Direct tests bidi_018_handle_user_prompt_{wire_defaults_and_result, wire_optional_parameters, result_policy, error_propagation, live_chrome} cover defaults, every parameter state, strict EmptyResult, protocol errors, observable effects, and cleanup; API tests bidi_018_dialog_semantic_wire_and_result and bidi_018_handle_user_prompt_response_validation pin the shared semantic/router lanes. Exposure remains semantic-public through existing dialog accept/dismiss workflows; Skill remains documented.
Resolve semantic locators and durable element references
Native
bl find / bl map(partial)
browser_find / browser_map(partial)
Verified
Implementation
Native browsingContext.locateNodes: LocateNodesParameters represents required context and all five Locator variants (accessibility name/role, CSS, child context, innerText with ignoreCase/matchType/maxDepth, and XPath), plus optional maxNodeCount (validated js-uint >=1), complete script.SerializationOptions with absent/null depth distinctions and shadow-tree mode, and non-empty script.SharedReference startNodes including optional handles. Client::locate_nodes sends the exact method, preserves raw NodeRemoteValue result objects after strict nodes-array/type validation, and propagates no such frame/invalid selector/no such node errors. Existing bl find/map and browser_find/map remain curated injected-script semantic workflows; the native protocol primitive is engine-internal rather than exposing shared-reference bookkeeping.
All LocateNodesParameters members are engine-representable: required context and Locator; optional maxNodeCount (js-uint >=1); optional serializationOptions {maxDomDepth?:js-uint|null default 0, maxObjectDepth?:js-uint|null default null, includeShadowTree?:none/open/all default none}; and optional non-empty startNodes list of SharedReference {sharedId,?handle}. Every locator member/default is preserved. Native options remain internal because public find/map derive context and element identity and intentionally present semantic selectors rather than raw shared-reference lifecycle.
Verified live 2026-07-11 on Chrome for Testing / ChromeDriver 150.0.7871.49: native CSS located two real NodeRemoteValues with sharedId; maxNodeCount limited results to one; XPath located both divs; innerText and accessibility role locators returned matches; startNodes scoped a CSS search to one located node; serializationOptions includeShadowTree=all/maxDomDepth=1 exposed an open shadow root. Invalid CSS returned invalid selector and an unknown context returned no such frame. Direct tests bidi_019_locate_nodes_{wire_required_and_result, all_parameters_and_validation, result_policy, error_propagation, live_chrome} cover exact wire shape, all locator/optional parameter families, validation, result decoding, protocol errors and observable Chrome behavior. Exposure decision remains internal for the native primitive; existing public find/map workflows remain partial semantic shims and Skill remains documented from those CLI workflows.
Native browsingContext.navigate: NavigateParameters represents required context/url and optional wait (none/interactive/complete), omitting wait when absent so the protocol's committed behavior remains reachable. Client::navigate_with strictly decodes NavigateResult {navigation:text|null,url:text}; the compatibility and semantic API paths intentionally select wait=complete while sharing typed serialization, protocol-error handling, and result validation.
context and url are required; wait is optional (none/interactive/complete). Omission remains engine-representable and means committed protocol behavior. bl open/go/goto/navigate and browser_open derive context and intentionally wait for complete.
Exposure decision
Semantic public
CLI
bl open <url> · Shipped · Public
MCP
browser_open · Shipped · Public · profile: core
Skill workflow
Documented · bl open <url> (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_020_022_navigation_live_chrome)
Verified live 2026-07-12 on Chrome for Testing 150.0.7871.49: navigation changed the document URL/title, returned a non-null navigation id and exact data URL, and none/interactive/complete waits succeeded; unknown context returned no such frame. Direct tests bidi_020_022_navigation_{wire_and_result,result_policy,error_propagation,live_chrome} cover wire options, strict nullable result decoding, errors, and observable effects.
Native browsingContext.print: PdfPrintOptions represents every optional spec member and nested margin/page group, validates orientation, scale, dimensions, margins and page-range grammar before send, and omits absent members so all protocol defaults remain authoritative. Proxy, CLI and MCP routes share this model and strict PrintResult {data:text} plus protocol-error validation.
context is required and engine-derived publicly. All optional members are represented: background default false; margins default 1cm; orientation portrait; page 21.59x27.94cm; pageRanges default all (explicit [] and the text range '-' are valid and also mean all pages; range strings follow WebDriver whitespace trimming without a js-uint ceiling; numeric entries are limited to js-uint); scale 1; shrinkToFit true. bl pdf/browser_pdf expose these validated print intentions and output handling.
Verified live 2026-07-12 on Chrome for Testing 150.0.7871.49: default, explicit pageRanges ['-'], and option-rich landscape/background/scale/margin/page/pageRanges output each decoded to a real %PDF artifact; unknown context returned no such frame. Existing option tests cover every parameter/default and invalid boundary; pageRanges regressions prove [] and '-' are accepted as all pages, ' 2 - 4 ' is accepted unchanged, text ranges are not js-uint-limited, and numeric entries above 9007199254740991 are rejected; the MCP schema permits an empty array. bidi_021_print_result_validation rejects malformed success/error envelopes; bidi_021_print_live_chrome provides direct browser evidence.
Native browsingContext.reload: ReloadParameters represents required context plus optional ignoreCache and wait, preserving omitted versus explicit false and all readiness states. Client::reload_context strictly decodes the shared NavigateResult. Existing bl reload/browser_reload semantics intentionally default wait=complete and omit ignoreCache unless requested, while sharing typed serialization, errors, and strict result validation.
context is required; ignoreCache is optional and defaults false; wait is optional (none/interactive/complete). All omitted/explicit states are engine-representable. bl reload/browser_reload derive context, default to complete, and expose cache bypass.
Exposure decision
Semantic public
CLI
bl reload · Shipped · Public
MCP
browser_reload · Shipped · Public · profile: core
Skill workflow
Documented · bl reload (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_020_022_navigation_live_chrome)
Verified live 2026-07-12 on Chrome for Testing 150.0.7871.49: reload with ignoreCache=true/wait=complete returned a navigation id and observably reran page script (title 1→2); omitted/false cache and wait states are wire-tested; unknown context returned no such frame. Shared direct tests bidi_020_022_navigation_{wire_and_result,result_policy,error_propagation,live_chrome} cover both commands.
Temporarily bypass Content Security Policy for debugging
Native
Internal
Internal
Not verified
Implementation
Native browsingContext.setBypassCSP: SetBypassCspParameters models the CDDL `bypass: true / null` exactly — the CspBypass enum's single Enabled variant emits true and None emits null (clearing the stored configuration), so `false` is inexpressible by construction, the emulation.setScriptingEnabled idiom — plus the mutually-exclusive contexts/userContexts scopes (CDDL [+] lists rejected empty client-side; exclusivity and global storage the browser's via the shared WebDriver-configuration store); Client::set_bypass_csp validates the extensible EmptyResult with the fully-re-settable non-invalidation policy.
bypass is required but nullable: true bypasses every CSP directive and null clears back to unset — false is CDDL-invalid and inexpressible by construction. The optional contexts/userContexts scopes are mutually exclusive (remote-end invalid argument) and omitting both stores the configuration globally. Never enabled by default anywhere in the engine; no surface derives arguments today.
Chrome for Testing 150.0.7871.49 predates the command (direct parity test bidi_023_set_bypass_csp_live_chrome): every parameter form (global enable/clear, context-scoped, user-context-scoped, both scopes) answers "unknown command - Unknown command 'browsingContext.setBypassCSP'." and the connection stays usable. Recorded as a Chrome limitation, not an engine gap.
Verified 2026-07-16 against Chrome for Testing 150.0.7871.49, which predates the command: every parameter form answers `unknown command` (pinned live with the exact message), so Chrome support is Unsupported and BL-on-Chrome stays Not Verified — the BIDI-050 precedent. The live test also pre-proves the observable probe for the Chrome that implements it: the fixture's script-src 'self' policy demonstrably enforces (inline script does not run; a DOM-injected inline script is refused) while script.evaluate-initiated eval() is recorded as CSP-exempt in Chrome — the reason a DOM-injection probe, not eval, is the observable signal. Exposure revised raw-only → Internal: the typed engine layer exists and no dedicated surface is justified for a debugging primitive Chrome cannot run; promotion is a future decision once Chrome implements the command. Direct tests bidi_023_set_bypass_csp_{wire_and_result,result_policy,invalid_params_rejected_before_send,error_propagation,malformed_result_keeps_connection_usable,timeout_keeps_connection_usable,live_chrome} cover wire shape for every parameter form, the strict EmptyResult, client-side [+] list validation, the spec's error lanes, the non-invalidation policy, and the live Chrome record.
Exactly one target is required: context or non-empty userContexts. viewport and devicePixelRatio are independently optional and nullable; null resets the override. Viewport width/height are js-uint and DPR must be finite >0. Public viewport/set/reset derives context; user-context-wide targeting remains engine-internal.
Exposure decision
Semantic public
CLI
bl viewport <width> <height> · Shipped · Public
MCP
browser_set_viewport · Shipped · Public · profile: testing
Skill workflow
Documented · bl viewport <width> <height> (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_024_set_viewport_live_chrome)
Verified live 2026-07-12 on Chrome for Testing 150.0.7871.49: context-scoped 640x480@2 was observable through innerWidth/innerHeight/devicePixelRatio; null viewport/DPR reset succeeded; user-context-wide 700x500@1.5 applied to a created context; unknown user context returned no such user context. Direct tests bidi_024_set_viewport_{all_parameter_forms,wire_result_and_errors,live_chrome} cover every target/null/omission form, validation, EmptyResult, errors, observable behavior, and cleanup.
context is required; mimeType is optional unrestricted text; video width/height/frameRate are optional js-uint including zero; audio is optional and defaults false, with omitted and explicit false preserved distinctly.
Documented · bl screencast start (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
unsupported (browser-side, tracked independently)
Chrome evidence
Chrome for Testing 150.0.7871.49 returned unknown command for browsingContext.startScreencast (direct probe bidi_025_026_screencast_live_chrome_unsupported)
Engine path verified by direct BIDI-025 serialization, js-uint boundary, omitted-versus-explicit-audio, strict-result, and protocol-error tests. A direct live probe on Chrome for Testing 150.0.7871.49 returned unknown command for browsingContext.startScreencast, so Chrome browser support is unsupported and Browserlane-on-Chrome is not marked verified.
Native browsingContext.stopScreencast: stop_screencast sends the required screencast id, strictly decodes required path and optional text error, preserves absent plus empty/non-empty successful-stop write errors exactly, rejects a non-text error, and propagates no such screencast or clean unsupported-endpoint errors.
Source evidence
src/engine/handlers_screencast.rs:167
Arguments / defaults
screencast is required and engine-managed from the validated start result. Stop returns required path and optional text write error; absence, empty text, and non-empty text remain distinct. The browser-created file is retained.
Documented · bl screencast stop (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
unsupported (browser-side, tracked independently)
Chrome evidence
Chrome for Testing 150.0.7871.49 returned unknown command for browsingContext.stopScreencast with a synthetic id (direct probe bidi_025_026_screencast_live_chrome_unsupported)
Engine path verified by direct BIDI-026 wire, required-result, optional-error preservation, malformed-success, no-such-screencast, unsupported-mapping, and protocol-error tests. A direct live probe on Chrome for Testing 150.0.7871.49 returned unknown command for browsingContext.stopScreencast, so Chrome browser support is unsupported and Browserlane-on-Chrome remains not verified.
Native browsingContext.traverseHistory: TraverseHistoryParameters represents the required context and required js-int delta (validated to the CDDL range before send; negative back, positive forward, zero the current entry), so every spec delta is engine-representable. Client::traverse_history and the shared engine::traverse_history primitive send the exact method and strictly validate the extensible EmptyResult; the back/forward intentions derive delta ±1 on top and keep their post-traversal readiness wait.
context and delta are both required with no spec defaults; delta is a js-int (client-validated to ±9007199254740991) and any value is engine-representable, with the context required to be a top-level traversable. bl back/forward and browser_back/browser_forward derive context and delta ±1.
Exposure decision
Semantic public
CLI
bl back / bl forward · Shipped · Public
MCP
browser_back / browser_forward · Shipped · Public · profile: core
Skill workflow
Documented · bl back / bl forward (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_027_traverse_history_live_chrome)
Verified live 2026-07-12 on Chrome for Testing 150.0.7871.49: deltas -2/+2/-1/+1 observably moved the document across three history entries, and delta 0 succeeded without navigating away; out-of-range deltas returned no such history entry, an unknown context no such frame, and an iframe context invalid argument (top-level-only enforcement). Direct tests bidi_027_traverse_history_{wire_and_result,delta_validation,result_policy,error_propagation,live_chrome} cover wire shape, js-int bounds, EmptyResult policy, protocol errors, and observable behavior.
Native browsingContext event integration: BROWSING_CONTEXT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events/unsubscribe_events provides global, browsing-context and user-context scope with refcounted cleanup; the Mux's single reader preserves the exact raw JSON plus structured method/params, while the production daemon recursively registers top-level and child navigables so iframe events reach the owning session without stealing events from overlapping consumers. With BL_TRACE=forensic, the session subscribes to this same complete 14-event registry and writes every event as normalized, redacted lifecycle evidence; debug retains its curated lower-noise subset. The process-wide critical subscription and Orchestrator consume contextCreated to register new context routes and keep tab/session discovery coherent.
Events have no caller-supplied event parameters, defaults, command result, or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates session.subscribe/session.unsubscribe protocol errors. Forensic traces normalize the payload at write time: URLs/text are scrubbed and bounded, prompt userText is always redacted, and downloadEnd stores only the filepath basename rather than the full local path. Payload is browsingContext.Info with required children, clientWindow, context, originalOpener, url and userContext plus optional parent; subscribe steps also emit existing matching contexts.
Exposure decision
Internal
CLI
bl tabs · Internal · Public
MCP
browser_tab_list · Internal · Public · profile: core
Skill workflow
Not applicable — no user-facing CLI workflow
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 by bidi_028_through_041_browsing_context_events_live_chrome plus the production-daemon child routing test bidi_028_041_child_events_reach_production_daemon_live_chrome
Direct parity test bidi_028_context_created_delivery_and_payload proves exact-name reception, context routing and raw/structured payload preservation; the grouped live test proves real subscription, deterministic delivery, payload fields, context scoping, cleanup, post-unsubscribe silence and compatibility with the existing event consumers.
Native browsingContext event integration: BROWSING_CONTEXT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events/unsubscribe_events provides global, browsing-context and user-context scope with refcounted cleanup; the Mux's single reader preserves the exact raw JSON plus structured method/params, while the production daemon recursively registers top-level and child navigables so iframe events reach the owning session without stealing events from overlapping consumers. With BL_TRACE=forensic, the session subscribes to this same complete 14-event registry and writes every event as normalized, redacted lifecycle evidence; debug retains its curated lower-noise subset. contextDestroyed is CriticalRouting: it reaches the session actor before the Mux and Orchestrator garbage-collect the context route/index, clears stale active-context and prompt state, and remains observable to waiters/recorders first.
Events have no caller-supplied event parameters, defaults, command result, or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates session.subscribe/session.unsubscribe protocol errors. Forensic traces normalize the payload at write time: URLs/text are scrubbed and bounded, prompt userText is always redacted, and downloadEnd stores only the filepath basename rather than the full local path. Payload is the destroyed context's browsingContext.Info; route cleanup happens only after delivery.
Exposure decision
Internal
CLI
bl tabs · Internal · Public
MCP
browser_tab_list · Internal · Public · profile: core
Skill workflow
Not applicable — no user-facing CLI workflow
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 by bidi_028_through_041_browsing_context_events_live_chrome plus the production-daemon child routing test bidi_028_041_child_events_reach_production_daemon_live_chrome
Direct parity test bidi_029_context_destroyed_delivery_and_payload proves exact-name reception, context routing and raw/structured payload preservation; the grouped live test proves real subscription, deterministic delivery, payload fields, context scoping, cleanup, post-unsubscribe silence and compatibility with the existing event consumers.
Native browsingContext event integration: BROWSING_CONTEXT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events/unsubscribe_events provides global, browsing-context and user-context scope with refcounted cleanup; the Mux's single reader preserves the exact raw JSON plus structured method/params, while the production daemon recursively registers top-level and child navigables so iframe events reach the owning session without stealing events from overlapping consumers. With BL_TRACE=forensic, the session subscribes to this same complete 14-event registry and writes every event as normalized, redacted lifecycle evidence; debug retains its curated lower-noise subset.
Events have no caller-supplied event parameters, defaults, command result, or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates session.subscribe/session.unsubscribe protocol errors. Forensic traces normalize the payload at write time: URLs/text are scrubbed and bounded, prompt userText is always redacted, and downloadEnd stores only the filepath basename rather than the full local path. Payload is NavigationInfo {context, navigation: text|null, timestamp: js-uint, url, userContext?}.
Verified live on Chrome for Testing 150.0.7871.49 by bidi_028_through_041_browsing_context_events_live_chrome plus the production-daemon child routing test bidi_028_041_child_events_reach_production_daemon_live_chrome
Direct parity test bidi_030_navigation_started_delivery_and_payload proves exact-name reception, context routing and raw/structured payload preservation; the grouped live test proves real subscription, deterministic delivery, payload fields, context scoping, cleanup, post-unsubscribe silence and compatibility with the existing event consumers.
Native browsingContext event integration: BROWSING_CONTEXT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events/unsubscribe_events provides global, browsing-context and user-context scope with refcounted cleanup; the Mux's single reader preserves the exact raw JSON plus structured method/params, while the production daemon recursively registers top-level and child navigables so iframe events reach the owning session without stealing events from overlapping consumers. With BL_TRACE=forensic, the session subscribes to this same complete 14-event registry and writes every event as normalized, redacted lifecycle evidence; debug retains its curated lower-noise subset. Recorder/trace consumers also use fragmentNavigated to retain same-document navigation evidence.
Events have no caller-supplied event parameters, defaults, command result, or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates session.subscribe/session.unsubscribe protocol errors. Forensic traces normalize the payload at write time: URLs/text are scrubbed and bounded, prompt userText is always redacted, and downloadEnd stores only the filepath basename rather than the full local path. Payload is NavigationInfo {context, navigation: text|null, timestamp: js-uint, url, userContext?}.
Verified live on Chrome for Testing 150.0.7871.49 by bidi_028_through_041_browsing_context_events_live_chrome plus the production-daemon child routing test bidi_028_041_child_events_reach_production_daemon_live_chrome
Direct parity test bidi_031_fragment_navigated_delivery_and_payload proves exact-name reception, context routing and raw/structured payload preservation; the grouped live test proves real subscription, deterministic delivery, payload fields, context scoping, cleanup, post-unsubscribe silence and compatibility with the existing event consumers.
Native browsingContext event integration: BROWSING_CONTEXT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events/unsubscribe_events provides global, browsing-context and user-context scope with refcounted cleanup; the Mux's single reader preserves the exact raw JSON plus structured method/params, while the production daemon recursively registers top-level and child navigables so iframe events reach the owning session without stealing events from overlapping consumers. With BL_TRACE=forensic, the session subscribes to this same complete 14-event registry and writes every event as normalized, redacted lifecycle evidence; debug retains its curated lower-noise subset.
Events have no caller-supplied event parameters, defaults, command result, or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates session.subscribe/session.unsubscribe protocol errors. Forensic traces normalize the payload at write time: URLs/text are scrubbed and bounded, prompt userText is always redacted, and downloadEnd stores only the filepath basename rather than the full local path. Payload is HistoryUpdatedParameters {context, timestamp: js-uint, url, userContext?} and deliberately has no navigation member.
Verified live on Chrome for Testing 150.0.7871.49 by bidi_028_through_041_browsing_context_events_live_chrome plus the production-daemon child routing test bidi_028_041_child_events_reach_production_daemon_live_chrome
Direct parity test bidi_032_history_updated_delivery_and_payload proves exact-name reception, context routing and raw/structured payload preservation; the grouped live test proves real subscription, deterministic delivery, payload fields, context scoping, cleanup, post-unsubscribe silence and compatibility with the existing event consumers.
Native browsingContext event integration: BROWSING_CONTEXT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events/unsubscribe_events provides global, browsing-context and user-context scope with refcounted cleanup; the Mux's single reader preserves the exact raw JSON plus structured method/params, while the production daemon recursively registers top-level and child navigables so iframe events reach the owning session without stealing events from overlapping consumers. With BL_TRACE=forensic, the session subscribes to this same complete 14-event registry and writes every event as normalized, redacted lifecycle evidence; debug retains its curated lower-noise subset.
Events have no caller-supplied event parameters, defaults, command result, or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates session.subscribe/session.unsubscribe protocol errors. Forensic traces normalize the payload at write time: URLs/text are scrubbed and bounded, prompt userText is always redacted, and downloadEnd stores only the filepath basename rather than the full local path. Payload is NavigationInfo {context, navigation: text|null, timestamp: js-uint, url, userContext?}.
Verified live on Chrome for Testing 150.0.7871.49 by bidi_028_through_041_browsing_context_events_live_chrome plus the production-daemon child routing test bidi_028_041_child_events_reach_production_daemon_live_chrome
Direct parity test bidi_033_dom_content_loaded_delivery_and_payload proves exact-name reception, context routing and raw/structured payload preservation; the grouped live test proves real subscription, deterministic delivery, payload fields, context scoping, cleanup, post-unsubscribe silence and compatibility with the existing event consumers.
Native browsingContext event integration: BROWSING_CONTEXT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events/unsubscribe_events provides global, browsing-context and user-context scope with refcounted cleanup; the Mux's single reader preserves the exact raw JSON plus structured method/params, while the production daemon recursively registers top-level and child navigables so iframe events reach the owning session without stealing events from overlapping consumers. With BL_TRACE=forensic, the session subscribes to this same complete 14-event registry and writes every event as normalized, redacted lifecycle evidence; debug retains its curated lower-noise subset. Recorder/trace consumers use load for navigation evidence; the router also updates its last-known URL from load/fragment events.
Events have no caller-supplied event parameters, defaults, command result, or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates session.subscribe/session.unsubscribe protocol errors. Forensic traces normalize the payload at write time: URLs/text are scrubbed and bounded, prompt userText is always redacted, and downloadEnd stores only the filepath basename rather than the full local path. Payload is NavigationInfo {context, navigation: text|null, timestamp: js-uint, url, userContext?}.
Verified live on Chrome for Testing 150.0.7871.49 by bidi_028_through_041_browsing_context_events_live_chrome plus the production-daemon child routing test bidi_028_041_child_events_reach_production_daemon_live_chrome
Direct parity test bidi_034_load_delivery_and_payload proves exact-name reception, context routing and raw/structured payload preservation; the grouped live test proves real subscription, deterministic delivery, payload fields, context scoping, cleanup, post-unsubscribe silence and compatibility with the existing event consumers.
Native browsingContext event integration: BROWSING_CONTEXT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events/unsubscribe_events provides global, browsing-context and user-context scope with refcounted cleanup; the Mux's single reader preserves the exact raw JSON plus structured method/params, while the production daemon recursively registers top-level and child navigables so iframe events reach the owning session without stealing events from overlapping consumers. With BL_TRACE=forensic, the session subscribes to this same complete 14-event registry and writes every event as normalized, redacted lifecycle evidence; debug retains its curated lower-noise subset. downloadWillBegin is an always-on CriticalEphemeral event and also feeds recording/trace evidence without exposing its protocol bookkeeping as a separate command.
Events have no caller-supplied event parameters, defaults, command result, or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates session.subscribe/session.unsubscribe protocol errors. Forensic traces normalize the payload at write time: URLs/text are scrubbed and bounded, prompt userText is always redacted, and downloadEnd stores only the filepath basename rather than the full local path. Spec payload adds required download and suggestedFilename to NavigationInfo. For the <a download> case, versioned WPT requires navigation:null. Chrome 150.0.7871.49 omits download and emits a non-null navigation id; Browserlane preserves and correlates that partial payload exactly.
Verified live on Chrome for Testing 150.0.7871.49 by bidi_028_through_041_browsing_context_events_live_chrome plus the production-daemon child routing test bidi_028_041_child_events_reach_production_daemon_live_chrome; Chrome omitted the required download UUID and emitted a non-null navigation id where the versioned WPT download_attribute case requires null
Chrome 150.0.7871.49 delivered downloadWillBegin for the deterministic <a download> fixture with context, suggestedFilename, timestamp and url, but omitted the WD-required download UUID and emitted a non-null navigation id where the versioned WPT download_attribute test requires null. Browserlane preserved the exact partial event and the file completed normally. Direct test bidi_035_download_will_begin_delivery_and_payload pins the complete spec shape; bidi_028_through_041_browsing_context_events_live_chrome records both Chrome limitations.
Native browsingContext event integration: BROWSING_CONTEXT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events/unsubscribe_events provides global, browsing-context and user-context scope with refcounted cleanup; the Mux's single reader preserves the exact raw JSON plus structured method/params, while the production daemon recursively registers top-level and child navigables so iframe events reach the owning session without stealing events from overlapping consumers. With BL_TRACE=forensic, the session subscribes to this same complete 14-event registry and writes every event as normalized, redacted lifecycle evidence; debug retains its curated lower-noise subset. downloadEnd is always-on CriticalEphemeral traffic routed through the session actor and buffered in a bounded per-session completion queue, so a later bl download wait or browser_wait_download deterministically consumes the next unclaimed completion and returns structured status, URL and filepath without exposing subscription ids or raw context ids.
Event subscription, context, download correlation and subscription id are engine-managed. bl download wait / browser_wait_download accept only timeout milliseconds (non-negative integer, default 30000) and return structured status + URL + filepath when present. Spec payload is complete or canceled: both require download plus NavigationInfo; complete also requires filepath text|null. For the <a download> case, versioned WPT requires navigation:null. Chrome 150 omits download and emits a non-null navigation id; Browserlane preserves that partial payload and verifies begin/end navigation equality even without the UUID.
Exposure decision
Semantic public
CLI
bl download wait [--timeout <ms>] · Shipped · Public
MCP
browser_wait_download · Shipped · Public · profile: core
Skill workflow
Documented · bl download wait [--timeout <ms>] (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
partial (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 by bidi_028_through_041_browsing_context_events_live_chrome plus the production-daemon child routing test bidi_028_041_child_events_reach_production_daemon_live_chrome; Chrome completed the download and reported filepath/status but omitted the required download UUID and emitted a non-null navigation id where versioned WPT requires null
Chrome 150.0.7871.49 delivered downloadEnd status complete with a filepath inside the configured temporary directory; the downloaded bytes exactly matched the fixture, but the WD-required download UUID was omitted consistently with downloadWillBegin and the navigation id was non-null where the versioned WPT download_attribute test requires null. Direct tests bidi_036_download_end_delivery_and_payload, bidi_036_download_wait_tool_is_strict_and_session_scoped, bidi_036_download_wait_rejects_invalid_timeout, download_wait_consumes_an_already_bookkept_completion_once, download_end_consumed_while_idle_is_buffered_for_later_wait and bidi_028_through_041_browsing_context_events_live_chrome cover payload preservation, strict MCP schema, timeout validation, buffered completion, correlation, scope and cleanup. Public exposure promotes the existing actor waiter; no duplicate event implementation was added.
Native browsingContext event integration: BROWSING_CONTEXT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events/unsubscribe_events provides global, browsing-context and user-context scope with refcounted cleanup; the Mux's single reader preserves the exact raw JSON plus structured method/params, while the production daemon recursively registers top-level and child navigables so iframe events reach the owning session without stealing events from overlapping consumers. With BL_TRACE=forensic, the session subscribes to this same complete 14-event registry and writes every event as normalized, redacted lifecycle evidence; debug retains its curated lower-noise subset.
Events have no caller-supplied event parameters, defaults, command result, or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates session.subscribe/session.unsubscribe protocol errors. Forensic traces normalize the payload at write time: URLs/text are scrubbed and bounded, prompt userText is always redacted, and downloadEnd stores only the filepath basename rather than the full local path. Payload is NavigationInfo {context, navigation: text|null, timestamp: js-uint, url, userContext?}; live verification interrupts a deterministic local streaming response with a second navigation.
Verified live on Chrome for Testing 150.0.7871.49 by bidi_028_through_041_browsing_context_events_live_chrome plus the production-daemon child routing test bidi_028_041_child_events_reach_production_daemon_live_chrome
Direct parity test bidi_037_navigation_aborted_delivery_and_payload proves exact-name reception, context routing and raw/structured payload preservation; the grouped live test proves real subscription, deterministic delivery, payload fields, context scoping, cleanup, post-unsubscribe silence and compatibility with the existing event consumers.
Native browsingContext event integration: BROWSING_CONTEXT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events/unsubscribe_events provides global, browsing-context and user-context scope with refcounted cleanup; the Mux's single reader preserves the exact raw JSON plus structured method/params, while the production daemon recursively registers top-level and child navigables so iframe events reach the owning session without stealing events from overlapping consumers. With BL_TRACE=forensic, the session subscribes to this same complete 14-event registry and writes every event as normalized, redacted lifecycle evidence; debug retains its curated lower-noise subset.
Events have no caller-supplied event parameters, defaults, command result, or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates session.subscribe/session.unsubscribe protocol errors. Forensic traces normalize the payload at write time: URLs/text are scrubbed and bounded, prompt userText is always redacted, and downloadEnd stores only the filepath basename rather than the full local path. Payload is NavigationInfo {context, navigation: text|null, timestamp: js-uint, url, userContext?}.
Verified live on Chrome for Testing 150.0.7871.49 by bidi_028_through_041_browsing_context_events_live_chrome plus the production-daemon child routing test bidi_028_041_child_events_reach_production_daemon_live_chrome
Direct parity test bidi_038_navigation_committed_delivery_and_payload proves exact-name reception, context routing and raw/structured payload preservation; the grouped live test proves real subscription, deterministic delivery, payload fields, context scoping, cleanup, post-unsubscribe silence and compatibility with the existing event consumers.
Native browsingContext event integration: BROWSING_CONTEXT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events/unsubscribe_events provides global, browsing-context and user-context scope with refcounted cleanup; the Mux's single reader preserves the exact raw JSON plus structured method/params, while the production daemon recursively registers top-level and child navigables so iframe events reach the owning session without stealing events from overlapping consumers. With BL_TRACE=forensic, the session subscribes to this same complete 14-event registry and writes every event as normalized, redacted lifecycle evidence; debug retains its curated lower-noise subset.
Events have no caller-supplied event parameters, defaults, command result, or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates session.subscribe/session.unsubscribe protocol errors. Forensic traces normalize the payload at write time: URLs/text are scrubbed and bounded, prompt userText is always redacted, and downloadEnd stores only the filepath basename rather than the full local path. Payload is NavigationInfo {context, navigation: text|null, timestamp: js-uint, url, userContext?}; live verification uses the canonical WPT CSP-blocked child-navigation trigger.
Verified live on Chrome for Testing 150.0.7871.49 by bidi_028_through_041_browsing_context_events_live_chrome plus the production-daemon child routing test bidi_028_041_child_events_reach_production_daemon_live_chrome
Direct parity test bidi_039_navigation_failed_delivery_and_payload proves exact-name reception, context routing and raw/structured payload preservation; the grouped live test proves real subscription, deterministic delivery, payload fields, context scoping, cleanup, post-unsubscribe silence and compatibility with the existing event consumers.
Clear completed prompt state and unblock internal waits
Native
Internal
Internal
Verified
Implementation
Native browsingContext event integration: BROWSING_CONTEXT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events/unsubscribe_events provides global, browsing-context and user-context scope with refcounted cleanup; the Mux's single reader preserves the exact raw JSON plus structured method/params, while the production daemon recursively registers top-level and child navigables so iframe events reach the owning session without stealing events from overlapping consumers. With BL_TRACE=forensic, the session subscribes to this same complete 14-event registry and writes every event as normalized, redacted lifecycle evidence; debug retains its curated lower-noise subset. userPromptClosed is always-on CriticalEphemeral traffic; the session actor removes only the matching context's tracked prompt so dialog info/wait never returns stale state.
Events have no caller-supplied event parameters, defaults, command result, or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates session.subscribe/session.unsubscribe protocol errors. Forensic traces normalize the payload at write time: URLs/text are scrubbed and bounded, prompt userText is always redacted, and downloadEnd stores only the filepath basename rather than the full local path. Payload requires context, accepted and prompt type; userContext and userText are optional.
Verified live on Chrome for Testing 150.0.7871.49 by bidi_028_through_041_browsing_context_events_live_chrome plus the production-daemon child routing test bidi_028_041_child_events_reach_production_daemon_live_chrome
Direct parity test bidi_040_user_prompt_closed_delivery_and_payload proves exact-name reception, context routing and raw/structured payload preservation; the grouped live test proves real subscription, deterministic delivery, payload fields, context scoping, cleanup, post-unsubscribe silence and compatibility with the existing event consumers.
Native browsingContext event integration: BROWSING_CONTEXT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events/unsubscribe_events provides global, browsing-context and user-context scope with refcounted cleanup; the Mux's single reader preserves the exact raw JSON plus structured method/params, while the production daemon recursively registers top-level and child navigables so iframe events reach the owning session without stealing events from overlapping consumers. With BL_TRACE=forensic, the session subscribes to this same complete 14-event registry and writes every event as normalized, redacted lifecycle evidence; debug retains its curated lower-noise subset. userPromptOpened is always-on CriticalEphemeral traffic; the session actor tracks its typed details per context and powers the existing dialog wait/info semantic surfaces without exposing subscription mechanics.
Events have no caller-supplied event parameters, defaults, command result, or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates session.subscribe/session.unsubscribe protocol errors. Forensic traces normalize the payload at write time: URLs/text are scrubbed and bounded, prompt userText is always redacted, and downloadEnd stores only the filepath basename rather than the full local path. Payload requires context, handler, message and prompt type; userContext and defaultValue are optional. Dialog wait accepts timeout_ms (default 30000); dialog info has no parameters.
Exposure decision
Semantic public
CLI
bl dialog wait|info · Shipped · Public
MCP
browser_dialog_wait / browser_dialog_info · Shipped · Public · profile: core
Skill workflow
Documented · bl dialog wait|info (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 by bidi_028_through_041_browsing_context_events_live_chrome plus the production-daemon child routing test bidi_028_041_child_events_reach_production_daemon_live_chrome
Direct parity test bidi_041_user_prompt_opened_delivery_and_payload proves exact-name reception, context routing and raw/structured payload preservation; the grouped live test proves real subscription, deterministic delivery, payload fields, context scoping, cleanup, post-unsubscribe silence and compatibility with the existing event consumers.
Native emulation.setForcedColorsModeThemeOverride: SetForcedColorsModeThemeOverrideParameters carries the required-but-nullable theme ("light"/"dark" Rust-enum inexpressible otherwise; None emits theme:null to clear) plus the mutually-exclusive contexts/userContexts scopes (CDDL [+] lists rejected empty client-side, exclusivity and global storage the browser's via the shared WebDriver-configuration store); Client::set_forced_colors_mode_theme_override validates the extensible EmptyResult with the module's non-invalidation policy. The curated bl media --forced-colors / browser_emulate_media surface keeps its matchMedia JS shim for the media-query intent — the shim fakes matchMedia answers only, while the native command (once Chrome implements it) forces real forced-colors rendering.
theme is required but nullable ("light"/"dark"; null clears); contexts and userContexts are optional and mutually exclusive — omitting both stores the override globally (shared WebDriver-configuration store). CDDL [+] scope lists are rejected empty client-side; everything else is the browser's.
Exposure decision
Semantic public
CLI
bl media --forced-colors <mode> · Shipped (shim) · Advanced
Documented · bl media --forced-colors <mode> (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
unsupported (browser-side, tracked independently)
Chrome evidence
Chrome for Testing 150.0.7871.49 does NOT implement the command (direct parity test bidi_042_set_forced_colors_mode_theme_override_live_chrome): every parameter form (dark/light/null, context-scoped) answers "unsupported operation - Method emulation.setForcedColorsModeThemeOverride is not implemented."; the rejection leaves page state untouched ((forced-colors: active) stays false) and the connection usable. Recorded as a Chrome limitation, not an engine gap.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_042_set_forced_colors_mode_theme_override_wire_and_result, _invalid_params_rejected_before_send, _result_policy, _error_propagation and live _live_chrome cover the exact method name, all three theme forms, both scopes, CDDL client validation, EmptyResult policy and the live unsupported-operation outcome. Chrome 150 answers unsupported operation, so BL-on-Chrome stays Not Verified until a Chrome release implements the command. Engine status Native (revised from shimmed): the typed command now exists; the bl media --forced-colors shim keeps serving the curated media-query intent unchanged.
Native emulation.setGeolocationOverride: SetGeolocationOverrideParameters models the CDDL group choice as the GeolocationOverride enum — Coordinates(Some(GeolocationCoordinates)) with the full seven-member surface (latitude/longitude required; accuracy CDDL-default 1.0; altitude/altitudeAccuracy/heading/speed nullable-default-null, emitted in the omitted form), Coordinates(None) emitting coordinates:null to clear, and PositionUnavailable emitting error:{type:"positionUnavailable"} — so the mixed forms are inexpressible by construction. Client-side validation is CDDL-only: the coordinate ranges (lat ±90, lon ±180, accuracy/altitudeAccuracy/speed ≥ 0, heading 0..<360), finiteness (JSON cannot carry NaN/Infinity), and non-empty [+] scope lists; the altitudeAccuracy-requires-altitude rule stays the browser's. Client::set_geolocation_override validates the extensible EmptyResult. The curated bl geolocation / browser_set_geolocation surface keeps its navigator.geolocation JS shim — the shim needs no permission grant, so rewiring it to the native override would change user-visible behavior.
Documented · bl geolocation <lat> <lng> (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_043_set_geolocation_override_live_chrome, loopback-origin fixture + permissions.setPermission granted + browsingContext.activate): minimal coordinates were observable via getCurrentPosition with the CDDL accuracy default 1.0 applied and every nullable member null; the full seven-member set round-tripped field by field (lat -33.87, lon 151.21, acc 5.5, alt 120.5, altAcc 0.5, heading 90, speed 2.25); the error arm surfaced POSITION_UNAVAILABLE (code 2) to the page; and the spec's errors answered live — invalid argument for altitudeAccuracy without altitude ("Geolocation altitudeAccuracy can be set only with altitude") and for contexts+userContexts together.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_043_set_geolocation_override_wire_and_result, _invalid_params_rejected_before_send (nine CDDL range/finiteness lanes), _result_policy, _error_propagation and live _live_chrome cover both group-choice arms, the nullable clear, the CDDL defaults observably applied, and the spec error lanes. Engine status Native (revised from shimmed): the typed command now exists. Surface decision (2026-07-15): bl geolocation / browser_set_geolocation deliberately stay on the permission-free JS shim — rewiring to the native override would require an origin-specific permission grant, a user-visible security-state change deferred until an explicit permission ownership/grant/reset design; no implementation-oriented --native switch. The command is intentionally NOT part of the grouped bl emulate environment surface.
Native emulation.setLocaleOverride: SetLocaleOverrideParameters carries the required-but-nullable locale (None emits locale:null to clear) plus the mutually-exclusive contexts/userContexts scopes; structural language-tag validity (IsStructurallyValidLanguageTag) and the exactly-one-scope rule stay the browser's; Client::set_locale_override validates the extensible EmptyResult with the module's non-invalidation policy. Promoted through the curated environment surface: engine::emulate_environment applies it through the typed Session method (strict EmptyResult validation on both proxy/router and AgentSession lanes), scoped to the selected session's user context (never unscoped, never another session's), behind bl emulate environment --locale / bl emulate reset locale and the strict browser_emulate_environment MCP tool.
locale is required but nullable (null clears); a non-null value must be a structurally valid language tag (the browser's check). Exactly one of contexts/userContexts must be present (the browser's check); the CDDL [+] lists are rejected empty client-side.
Documented · bl emulate environment --locale <tag> (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_044_set_locale_override_live_chrome): a context-scoped "fr-CA" override was observable via Intl.DateTimeFormat().resolvedOptions().locale and the clear restored the default (en-US); a userContexts-scoped "de-CH" override covered a tab in a fresh user context while leaving the default context untouched; and the spec's errors answered live — invalid argument for neither scope ("Either user contexts or browsing contexts must be provided"), for both scopes together, and for a malformed language tag ("Invalid locale \"not a locale!\""). Surface verified live 2026-07-15 on Chrome for Testing 150.0.7871.49: the direct test emulate_environment_live_chrome (src/engine/handlers_emulation.rs) proves the full override set applies observably to one user context without touching another, the literal "default" user-context scope works, and resets restore each scope independently; cli-smoke "emulate isolation (session-scoped)" proves the same through the real daemon with a named session (fr-CA in default, de-CH in the named session, neither leaking), plus reset-path and empty-call-rejected checks.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_044_set_locale_override_wire_and_result, _invalid_params_rejected_before_send, _result_policy, _error_propagation and live _live_chrome cover both scope forms, the nullable clear, user-context isolation and three spec error lanes. Exposure Semantic Public (revised from the interim internal): the curated grouped surface — bl emulate environment --locale <tag> / bl emulate reset locale and browser_emulate_environment {locale} ("" clears) — promotes the locale intention, session-scoped via userContexts. The curated lane's malformed-success regression covers all five promoted members; emulate_environment_live_chrome and cli-smoke prove isolation, explicit reset, and that session reset default clears the permanent default user context's environment configuration.
Native emulation.setNetworkConditions: SetNetworkConditionsParameters carries the required-but-nullable networkConditions — the NetworkConditions enum's single Offline variant emits {type:"offline"} (the pinned spec defines only emulation.NetworkConditionsOffline), None emits null to clear — plus the mutually-exclusive contexts/userContexts scopes. Promoted through the curated environment surface: engine::emulate_environment applies it through the typed Session method (strict EmptyResult validation on both proxy/router and AgentSession lanes), scoped to the selected session's user context (never the session-global default, which other sessions' tabs would inherit), behind bl emulate environment --offline / bl emulate reset network and browser_emulate_environment {network: "offline"|"default"}.
networkConditions is required but nullable — {type:"offline"} is the only condition the pinned spec defines (the Rust enum admits nothing else); null clears. contexts/userContexts optional and mutually exclusive; omitting both sets the session default. CDDL [+] lists rejected empty client-side.
Documented · bl emulate environment --offline (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_045_set_network_conditions_live_chrome): a context-scoped offline condition flipped navigator.onLine to false and its clear restored true; the session-global form (both scopes omitted) did the same; and the spec's invalid argument answered live for contexts+userContexts together. Surface verified live 2026-07-15 on Chrome for Testing 150.0.7871.49: the direct test emulate_environment_live_chrome (src/engine/handlers_emulation.rs) proves the full override set applies observably to one user context without touching another, the literal "default" user-context scope works, and resets restore each scope independently; cli-smoke "emulate isolation (session-scoped)" proves the same through the real daemon with a named session (fr-CA in default, de-CH in the named session, neither leaking), plus reset-path and empty-call-rejected checks.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_045_set_network_conditions_wire_and_result, _invalid_params_rejected_before_send, _result_policy, _error_propagation and live _live_chrome cover the offline condition, the global and context scopes, the nullable clear and the exclusivity error. Exposure Semantic Public (revised from the interim internal): the grouped surface promotes the "go offline" intention — bl emulate environment --offline / bl emulate reset network and browser_emulate_environment {network} — session-scoped via userContexts so one agent's offline emulation never severs another's (emulate_environment_live_chrome observes navigator.onLine false in the overridden user context only).
Native emulation.setScreenSettingsOverride: SetScreenSettingsOverrideParameters carries the required-but-nullable screenArea ({width, height} both CDDL js-uint, bounds enforced client-side; None emits screenArea:null to clear; the remote end forces the area origin to 0,0) plus the mutually-exclusive contexts/userContexts scopes (exactly-one required by the remote end); Client::set_screen_settings_override validates the extensible EmptyResult with the module's non-invalidation policy. The curated bl viewport / browser_set_viewport surface keeps covering the adjacent viewport intent via browsingContext.setViewport — the real screen-area override is now natively reachable engine-side.
screenArea is required but nullable ({width, height} js-uint; null clears; x/y forced to 0 by the remote end). Exactly one of contexts/userContexts must be present (the browser's check); js-uint bounds and empty [+] lists rejected client-side.
Documented · bl viewport (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_046_set_screen_settings_override_live_chrome): a context-scoped 812x634 screenArea was observable via screen.width/height/availWidth/availHeight (all four followed the override) and the clear restored the real 800x600 headless screen; the spec's errors answered live — invalid argument for neither scope and for both scopes together.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_046_set_screen_settings_override_wire_and_result, _invalid_params_rejected_before_send, _result_policy, _error_propagation and live _live_chrome cover the js-uint bounds, both scope forms, the nullable clear and both spec error lanes. Engine status Native (revised from lifecycle-alternative): the real screen-settings override now exists alongside the viewport surface, which keeps its partial-semantic role for the viewport intent. Surface decision (2026-07-15): no direct screen-settings command — the primitive is deferred with orientation and touch until a coherent bl emulate device workflow can coordinate viewport, screen, DPR, orientation, and touch; it is deliberately NOT part of bl emulate environment.
Native emulation.setScreenOrientationOverride: SetScreenOrientationOverrideParameters carries the required-but-nullable screenOrientation — a ScreenOrientation pair of ScreenOrientationNatural (portrait/landscape) and ScreenOrientationType (the four Screen Orientation API types, wire member `type`), all spellings Rust-enum-enforced; None emits null to clear — plus the mutually-exclusive contexts/userContexts scopes (exactly-one required by the remote end); Client::set_screen_orientation_override validates the extensible EmptyResult with the module's non-invalidation policy.
screenOrientation is required but nullable — {natural: portrait/landscape, type: one of the four Screen Orientation API types} (both members type-enforced); null clears. Exactly one of contexts/userContexts must be present (the browser's check); the CDDL [+] lists are rejected empty client-side.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_047_set_screen_orientation_override_live_chrome): landscape-secondary under a naturally-landscape screen surfaced screen.orientation {type: landscape-secondary, angle: 180} while the same type under a naturally-portrait screen surfaced angle 270 — proving the natural member observably participates in the spec's angle mapping; the clear restored the default {landscape-primary, 0}; and the spec's errors answered live — invalid argument for neither scope and for both scopes together.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_047_set_screen_orientation_override_wire_and_result (all eight natural x type spellings on the wire), _invalid_params_rejected_before_send, _result_policy, _error_propagation and live _live_chrome cover the full enum surface, the angle mapping, the nullable clear and both spec error lanes. Exposure Internal, reaffirmed with the curated-surface decision (2026-07-15): screen orientation stays engine-internal, deferred with touch and screen settings until a coherent bl emulate device workflow can coordinate viewport, screen, DPR, orientation, and touch; it is deliberately NOT part of bl emulate environment.
Native emulation.setUserAgentOverride: SetUserAgentOverrideParameters carries the required-but-nullable userAgent (any text per the CDDL; None emits null to clear) plus the mutually-exclusive contexts/userContexts scopes (the spec's three-level navigable > user context > session-default resolution). Promoted through the curated environment surface: engine::emulate_environment applies it through the typed Session method (strict EmptyResult validation on both proxy/router and AgentSession lanes), scoped to the selected session's user context — never the session default, which every other session's tabs would fall back to — behind bl emulate environment --user-agent / bl emulate reset user-agent and browser_emulate_environment {userAgent} ("" clears).
userAgent is required but nullable (any text; null clears the addressed level). contexts/userContexts optional and mutually exclusive; omitting both sets the session-global default, and per-navigable overrides shadow per-user-context and global ones. CDDL [+] lists rejected empty client-side.
Documented · bl emulate environment --user-agent <text> (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_048_set_user_agent_override_live_chrome): a session-global override was observable via navigator.userAgent, a per-context override shadowed it in its tab only, clearing the context override fell back to the global one and clearing the global restored the real HeadlessChrome UA; the spec's errors answered live — no such frame, no such user context, invalid argument for both scopes together. Surface verified live 2026-07-15 on Chrome for Testing 150.0.7871.49: the direct test emulate_environment_live_chrome (src/engine/handlers_emulation.rs) proves the full override set applies observably to one user context without touching another, the literal "default" user-context scope works, and resets restore each scope independently; cli-smoke "emulate isolation (session-scoped)" proves the same through the real daemon with a named session (fr-CA in default, de-CH in the named session, neither leaking), plus reset-path and empty-call-rejected checks.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_048_set_user_agent_override_wire_and_result, _invalid_params_rejected_before_send, _result_policy, _error_propagation and live _live_chrome cover all three scope levels, the shadowing/fallback order, both clears and three spec error lanes. Exposure Semantic Public (revised from the interim internal): the grouped surface promotes the User-Agent intention — bl emulate environment --user-agent <text> / bl emulate reset user-agent and browser_emulate_environment {userAgent} — session-scoped via userContexts (emulate_environment_live_chrome observes the override in one user context with the other untouched; no sticky open/session-new flags per the curated-surface decision).
Native emulation.setScriptingEnabled: SetScriptingEnabledParameters models the CDDL `enabled: false / null` exactly — the ScriptingEnabled enum's single Disabled variant emits false and None emits null, so `true` is inexpressible by construction (the spec only supports emulating disabled JavaScript). Promoted through the curated environment surface: engine::emulate_environment applies it through the typed Session method (strict EmptyResult validation on both proxy/router and AgentSession lanes), scoped to the selected session's user context, behind bl emulate environment --no-scripting / bl emulate reset scripting and browser_emulate_environment {scripting: "disabled"|"default"}.
enabled admits exactly false (disable) or null (clear) — the CDDL has no true, and the typed layer cannot express one. Exactly one of contexts/userContexts must be present (the browser's check); the CDDL [+] lists are rejected empty client-side.
Documented · bl emulate environment --no-scripting (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_049_set_scripting_enabled_live_chrome): disabling scripting on a context was observable through noscript parsing semantics while automation script.evaluate kept running, and the clear restored normal parsing; the spec's errors answered live — invalid argument for neither scope and for both scopes together. Surface verified live 2026-07-15 on Chrome for Testing 150.0.7871.49: the direct test emulate_environment_live_chrome (src/engine/handlers_emulation.rs) proves the full override set applies observably to one user context without touching another, the literal "default" user-context scope works, and resets restore each scope independently; cli-smoke "emulate isolation (session-scoped)" proves the same through the real daemon with a named session (fr-CA in default, de-CH in the named session, neither leaking), plus reset-path and empty-call-rejected checks.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_049_set_scripting_enabled_wire_and_result, _invalid_params_rejected_before_send, _result_policy, _error_propagation and live _live_chrome cover the false/null wire forms, the inexpressibility of true, both scope forms and both spec error lanes. Exposure Semantic Public (revised from the interim internal): the grouped surface promotes the "disable JavaScript" intention — bl emulate environment --no-scripting / bl emulate reset scripting and browser_emulate_environment {scripting} — session-scoped via userContexts (emulate_environment_live_chrome observes noscript rendering flip in the overridden user context only).
Native emulation.setScrollbarTypeOverride: SetScrollbarTypeOverrideParameters carries the required-but-nullable scrollbarType (the ScrollbarType enum's classic/overlay, spelling type-enforced; None emits null to clear) plus the mutually-exclusive contexts/userContexts scopes (CDDL [+] lists rejected empty client-side, exclusivity and global storage the browser's via the shared WebDriver-configuration store); Client::set_scrollbar_type_override validates the extensible EmptyResult with the module's non-invalidation policy.
scrollbarType is required but nullable ("classic"/"overlay"; null clears); contexts and userContexts are optional and mutually exclusive — omitting both stores the override globally (shared WebDriver-configuration store). CDDL [+] scope lists are rejected empty client-side.
Chrome for Testing 150.0.7871.49 predates the command (direct parity test bidi_050_set_scrollbar_type_override_live_chrome): every parameter form (classic/overlay/null, context-scoped) answers "unknown command - Unknown command 'emulation.setScrollbarTypeOverride'." and the connection stays usable. Recorded as a Chrome limitation, not an engine gap.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_050_set_scrollbar_type_override_wire_and_result, _invalid_params_rejected_before_send, _result_policy, _error_propagation and live _live_chrome cover both scrollbar types, the nullable clear, all scope forms and the live unknown-command outcome. Chrome 150 answers unknown command, so BL-on-Chrome stays Not Verified until a Chrome release implements the command. Exposure Internal (revised from advanced-public).
Native emulation.setTimezoneOverride, now typed: SetTimezoneOverrideParameters carries the required-but-nullable timezone (a named IANA identifier or a time-zone offset string; None emits null to clear; validity is the browser's) plus the mutually-exclusive contexts/userContexts scopes; Client::set_timezone_override validates the extensible EmptyResult with the module's non-invalidation policy. Two shipping lanes serialize through the same typed parameters: the per-tab page_clock_set_timezone / clock.setTimezone lane (contexts-scoped via handlers_clock::timezone_override_params, retained unchanged for compatibility) and the session-wide environment lane (userContexts-scoped through the typed Session method, with strict EmptyResult validation on both proxy/router and AgentSession lanes, behind bl emulate environment --timezone / bl emulate reset timezone and browser_emulate_environment {timezone}). Per the spec's resolution order the per-tab override wins over the session-wide one in that tab.
timezone is required but nullable (named IANA zone or offset string like "+05:30"; null clears; validity is the browser's — IsTimeZoneOffsetString / AvailableNamedTimeZoneIdentifiers). Exactly one of contexts/userContexts must be present (the browser's check); the CDDL [+] lists are rejected empty client-side. The clock surface derives the context and always sends the contexts form.
Documented · bl emulate environment --timezone <zone> (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_051_set_timezone_override_live_chrome): a context-scoped Asia/Tokyo override was observable via Intl.DateTimeFormat().resolvedOptions().timeZone, the offset-string form "+05:30" surfaced verbatim, the clear restored the system zone, and a userContexts-scoped America/Chicago override covered a fresh user context's tab while leaving the default context untouched; the spec's errors answered live — invalid argument for neither scope, for both scopes together, and for an unknown zone. Surface verified live 2026-07-15 on Chrome for Testing 150.0.7871.49: the direct test emulate_environment_live_chrome (src/engine/handlers_emulation.rs) proves the full override set applies observably to one user context without touching another, the literal "default" user-context scope works, and resets restore each scope independently; cli-smoke "emulate isolation (session-scoped)" proves the same through the real daemon with a named session (fr-CA in default, de-CH in the named session, neither leaking), plus reset-path and empty-call-rejected checks.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_051_set_timezone_override_wire_and_result, _invalid_params_rejected_before_send, _result_policy, _error_propagation, _malformed_result_keeps_connection_usable, _timeout_keeps_connection_usable, live _live_chrome, and bidi_051_timezone_lanes_send_spec_wire_shape (the clock lane's wire-identity through the typed parameters) cover both timezone forms, both scopes, the clear, three spec error lanes and the ambiguous-outcome policy. CLI Existing Semantic (revised from not-exposed): timezone joined the grouped bl emulate environment surface for a coherent environment workflow; page_clock_set_timezone is retained unchanged for compatibility (its per-tab contexts scope wins over the session-wide userContexts scope in that tab, per spec resolution).
Native emulation.setTouchOverride: SetTouchOverrideParameters carries the required-but-nullable maxTouchPoints (CDDL (js-uint .ge 1), both bounds enforced client-side; None emits null to clear) plus the mutually-exclusive contexts/userContexts scopes; omitting both sets the session's default emulated maxTouchPoints (the spec's three-level navigable > user context > session-default resolution). Client::set_touch_override validates the extensible EmptyResult with the module's non-invalidation policy.
maxTouchPoints is required but nullable — a non-null value is js-uint ≥ 1 (zero and beyond-2^53-1 rejected client-side); null clears the addressed level. contexts/userContexts optional and mutually exclusive; omitting both sets the session-global default, and per-navigable overrides shadow per-user-context and global ones. CDDL [+] lists rejected empty client-side.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_052_set_touch_override_live_chrome): a session-global maxTouchPoints of 5 was observable via navigator.maxTouchPoints (default 0 on the headless host), a per-context override of 3 shadowed it in its tab, clearing the context override fell back to 5 and clearing the global restored 0; the spec's invalid argument answered live for contexts+userContexts together.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_052_set_touch_override_wire_and_result, _invalid_params_rejected_before_send (the .ge 1 floor, the js-uint ceiling, empty scopes), _result_policy, _error_propagation and live _live_chrome cover all three scope levels, the shadowing/fallback order, both clears and the exclusivity error. Exposure Internal, reaffirmed with the curated-surface decision (2026-07-15): touch emulation stays engine-internal, deferred with screen orientation and screen settings until a coherent bl emulate device workflow can coordinate viewport, screen, DPR, orientation, and touch; it is deliberately NOT part of bl emulate environment.
Capture request or response bodies for forensic evidence
Native
Internal
Internal
Verified
Implementation
Native network.addDataCollector: AddDataCollectorParameters carries the full spec surface — the [+network.DataType] list, maxEncodedDataSize, the CDDL-defaulted collectorType ("blob"), and the mutually-exclusive contexts/userContexts scopes — validating only the CDDL constraints client-side (non-empty [+] lists, js-uint bound) and leaving the remote-end constraints (mutual exclusivity, top-level-only contexts, size budget, id resolution) to the browser; Client::add_data_collector strictly decodes the {collector} UUID-handle result. It is a mutating handle-creating command, so an ambiguous outcome (malformed success / timeout) invalidates the connection like browser.createUserContext — an orphaned collector would buffer traffic against the remote end's budget with no expressible removal.
dataTypes and maxEncodedDataSize are required; collectorType (remote-end default "blob"), contexts and userContexts are optional. Omitting both scopes registers a session-global collector; the CDDL [+] lists are rejected empty client-side, the size bound and scope resolution stay the browser's. The collector UUID is engine-managed and feeds network.getData / network.disownData / network.removeDataCollector.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_053_add_data_collector_live_chrome): a registered blob collector's UUID handle made a real response body retrievable via network.getData, and the spec's registration errors answered live — invalid argument for maxEncodedDataSize 0 ("Max encoded data size should be between 1 and 200000000") and for contexts+userContexts together, no such frame for an unknown context, no such user context for an unknown user context.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_053_add_data_collector_wire_and_result, bidi_053_add_data_collector_invalid_params_rejected_before_send, bidi_053_add_data_collector_result_policy, bidi_053_add_data_collector_error_propagation, bidi_053_add_data_collector_malformed_result_invalidates_connection, bidi_053_add_data_collector_timeout_invalidates_connection and live bidi_053_add_data_collector_live_chrome cover the exact method name, all parameter forms, strict result decode, CDDL-level client validation, the createUserContext-style ambiguous-outcome invalidation and observable body capture. Exposure Internal: a data collector is a forensic-capture primitive; the useful "capture request/response bodies" intention would ride the trace/recording family, not a per-primitive command.
Native network.addIntercept: AddInterceptParameters carries the [+network.InterceptPhase] list, the optional top-level-only contexts, and the [*network.UrlPattern] list with both UrlPatternPattern (per-component text) and UrlPatternString variants, validating only the CDDL [+] non-empty phases/contexts client-side and leaving URL-pattern parsing and context resolution to the browser; Client::add_intercept strictly decodes the {intercept} UUID-handle result and, like addDataCollector, invalidates the connection on an ambiguous outcome (an orphaned intercept would block matching traffic with no expressible removal). The pre-existing engine-internal router page.route / page.setHeaders shims keep sending the untyped addIntercept blob.
phases is required; contexts and urlPatterns are optional — omitting contexts scopes to all contexts, and an omitted or empty urlPatterns list matches all URLs (CDDL [*]). The intercept UUID is engine-managed and feeds network.removeIntercept.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_054_add_intercept_live_chrome): a string-pattern intercept blocked exactly its URL (network.beforeRequestSent isBlocked true with the intercept id listed in intercepts), a non-matching URL flowed unblocked, a context-scoped pattern-form intercept blocked its path, and the spec's registration errors answered live — invalid argument for a malformed URL pattern ("Forbidden characters"), no such frame for an unknown context.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_054_add_intercept_wire_and_result, bidi_054_add_intercept_invalid_params_rejected_before_send, bidi_054_add_intercept_result_policy, bidi_054_add_intercept_error_propagation, bidi_054_add_intercept_malformed_result_invalidates_connection, bidi_054_add_intercept_timeout_invalidates_connection and live bidi_054_add_intercept_live_chrome cover both UrlPattern variants, the empty-list defaults, strict decode, ambiguous-outcome invalidation and observable blocking. Exposure Internal (revised from advanced-public): registering an intercept coordinates an engine-managed id and blocked-request lifecycle with no standalone human intention; a curated "mock a route" surface remains a follow-up. The router page.route shim stays engine-internal.
Native network.continueRequest: ContinueRequestParameters carries request plus the optional body (network.BytesValue), cookies ([*network.CookieHeader]), headers ([*network.Header]), method and url — every omitted member leaves the blocked request unmodified, headers replaces the header list wholesale and cookies overwrites the Cookie header — with header/url/method validity left to the browser; Client::continue_request validates the extensible EmptyResult. It is request-identified, so an ambiguous outcome (malformed success / timeout) surfaces as a plain error with the connection intact. The engine-internal router network.continue shim keeps its untyped blob.
request is required; body, cookies, headers, method and url are optional and modify only what they name. Legal only for a request blocked in the beforeRequestSent phase; request ids come from the network event bus and stay engine-managed.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_055_continue_request_live_chrome): a plain resume reached the loopback fixture untouched; a rewrite of method+url+headers was observable end to end (the fixture received POST /rewritten-055 with the replaced X-BL-Continue header and the response events carried the new URL); and the spec's errors answered live — no such request for an unknown id ("Network request ... doesn't exist") and invalid argument for a responseStarted-phase request ("is in 'responseStarted' phase").
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_055_continue_request_wire_and_result, bidi_055_continue_request_result_policy, bidi_055_continue_request_error_propagation, bidi_055_continue_request_malformed_result_keeps_connection_usable, bidi_055_continue_request_timeout_keeps_connection_usable and live bidi_055_continue_request_live_chrome cover the required-only and complete wire forms, EmptyResult policy, phase/id errors, the non-invalidation policy and observable request rewriting. Exposure Internal (revised from advanced-public): resuming/rewriting an intercepted request is interception plumbing with no standalone intention; the router network.continue shim stays engine-internal.
Native network.continueResponse: ContinueResponseParameters carries request plus the optional cookies ([*network.SetCookieHeader] with the full domain/httpOnly/expiry/maxAge/path/sameSite/secure surface), credentials (network.AuthCredentials, the CDDL-literal "password" type emitted by the engine), headers, reasonPhrase and statusCode — validating only the js-uint/js-int/sameSite CDDL bounds client-side; Client::continue_response validates the extensible EmptyResult with the request-identified non-invalidation policy.
Source evidence
src/bidi/network.rs:1191; src/bidi/network.rs:637
Arguments / defaults
request is required; cookies, credentials, headers, reasonPhrase and statusCode are optional and modify only what they name. Legal for requests blocked in the responseStarted or authRequired phases; request ids are engine-managed.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_056_continue_response_live_chrome): a responseStarted-blocked request had its status/reason overridden to 418 Teapot — both the network.responseCompleted event and the page's own fetch promise observed "418:Teapot" — and the spec's errors answered live: no such request for an unknown id, invalid argument for a beforeRequestSent-phase request ("is in 'beforeRequestSent' phase").
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_056_continue_response_wire_and_result, bidi_056_continue_response_invalid_params_rejected_before_send, bidi_056_continue_response_result_policy, bidi_056_continue_response_error_propagation and live bidi_056_continue_response_live_chrome cover the SetCookieHeader/credentials surface, the CDDL client validation (statusCode js-uint, maxAge js-int, sameSite enum), phase/id errors and observable status override. Exposure Internal: response modification is interception plumbing with no standalone intention (no router shim exists for it either).
Native network.continueWithAuth: ContinueWithAuthParameters models the CDDL group choice as a Rust enum — ProvideCredentials(AuthCredentials) emits {action:"provideCredentials", credentials}, Default/Cancel emit the bare action with credentials inexpressible by construction — so the schema's inconsistent forms cannot be built; Client::continue_with_auth validates the extensible EmptyResult with the request-identified non-invalidation policy.
Source evidence
src/bidi/network.rs:1207; src/bidi/network.rs:716
Arguments / defaults
request and action are required; credentials is required exactly for the provideCredentials action (the type enforces it). Legal only for a request blocked in the authRequired phase; request ids are engine-managed.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_057_continue_with_auth_live_chrome): an authRequired-blocked challenge was cancelled (network.responseCompleted status 401) and answered with provideCredentials (the loopback fixture received Authorization: Basic ..., responseCompleted 200); the spec's errors answered live — no such request for an unknown id, invalid argument for a beforeRequestSent-phase request ("is in 'beforeRequestSent' phase").
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_057_continue_with_auth_wire_and_result, bidi_057_continue_with_auth_result_policy, bidi_057_continue_with_auth_error_propagation and live bidi_057_continue_with_auth_live_chrome cover all three group-choice forms, the EmptyResult policy, phase/id errors and observable credential/cancel behavior (the live test challenges the cancel lane before caching so Chrome's per-origin Basic-credential replay does not mask it). Exposure Internal: answering an auth challenge is interception plumbing with no standalone intention.
Native network.disownData: DisownDataParameters carries the three required members (dataType, collector, request; no optional members, no defaults); Client::disown_data validates the extensible EmptyResult. Releasing the last owning collector frees the collected bytes at the remote end; a collector that never collected that entry is a spec-level silent no-op, so only collector/data resolution errors propagate.
Source evidence
src/bidi/network.rs:1224; src/bidi/network.rs:754
Arguments / defaults
dataType, collector and request are all required with no defaults. Collector and request ids are engine-managed (the collector from network.addDataCollector, the request from the network event bus).
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_058_disown_data_live_chrome): disowning the last collector's ownership froze the data (a later network.getData answered no such network data — "Collector ... didn't collect response data"), and the spec's errors answered live — no such network collector for an unknown collector ("Unknown collector ..."), no such network data for a never-collected request.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_058_disown_data_wire_and_result, bidi_058_disown_data_result_policy, bidi_058_disown_data_error_propagation and live bidi_058_disown_data_live_chrome cover both data types, the EmptyResult policy, the collector-first / data-second error ordering and observable release. Exposure Internal: collected-data lifecycle is protocol bookkeeping with no human or agent intention.
Native network.failRequest: FailRequestParameters carries the single required request member; Client::fail_request validates the extensible EmptyResult with the request-identified non-invalidation policy. The engine-internal router network.abort shim keeps its untyped blob.
request is the only member — required, no optional members, no defaults. Legal for requests blocked in the beforeRequestSent or responseStarted phases; request ids are engine-managed.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_059_fail_request_live_chrome): a beforeRequestSent-blocked fetch was failed with a network error (network.fetchError fired with errorText net::ERR_FAILED and the page promise rejected with a TypeError), and the spec's errors answered live — no such request for an unknown id, invalid argument for an authRequired-blocked request ("in 'authRequired' phase cannot be failed").
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_059_fail_request_wire_and_result, bidi_059_fail_request_result_policy, bidi_059_fail_request_error_propagation and live bidi_059_fail_request_live_chrome cover the wire shape, the EmptyResult policy, phase/id errors and the observable fetchError/rejection. Exposure Internal (revised from advanced-public): aborting an intercepted request is interception plumbing; the router network.abort shim stays engine-internal.
Native network.getData: GetDataParameters carries dataType and request plus the optional collector and disown (remote-end default false); Client::get_data strictly decodes the {bytes: network.BytesValue} result, preserving the string form for valid UTF-8 and the base64 form otherwise. Without disown it is a pure read; disown mutates ownership, but the ids live in the request so an ambiguous outcome never invalidates the connection.
dataType and request are required; collector and disown are optional — omitting disown reads without releasing (remote-end default false), and disown:true requires a collector (a remote-end check). Collector and request ids are engine-managed.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_060_get_data_live_chrome): a text response body decoded to the string form byte-exact, a non-UTF-8 body to the base64 form (/wCc/g==), disown:true released the last owner (a second read answered no such network data), and the spec's errors answered live — invalid argument for disown without a collector ("Cannot disown collected data without collector ID"), no such network collector for an unknown collector, no such network data for a never-collected request.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_060_get_data_wire_and_result, bidi_060_get_data_result_policy, bidi_060_get_data_error_propagation and live bidi_060_get_data_live_chrome cover both BytesValue result forms, the disown default, the four error lanes and observable retrieval/release. Exposure Internal: retrieving a collected body is a forensic-capture primitive that would ride the trace/recording family, not a per-primitive command. The unavailable-network-data lane is covered by unit assertions (the pending-body await path is not deterministically reproducible live).
Native network.provideResponse: ProvideResponseParameters carries request plus the optional body, cookies ([*network.SetCookieHeader]), headers, reasonPhrase and statusCode (no credentials member) — legal in every blocking phase — validating only the js-uint/js-int/sameSite CDDL bounds client-side; Client::provide_response validates the extensible EmptyResult with the request-identified non-invalidation policy. The engine-internal router network.fulfill shim keeps its untyped blob.
request is required; body, cookies, headers, reasonPhrase and statusCode are optional. Legal in every blocking phase (beforeRequestSent/responseStarted/authRequired); request ids are engine-managed.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_061_provide_response_live_chrome): a beforeRequestSent-blocked fetch was fulfilled with a fabricated 200 body — the page observed "200:bl-mocked-body" and the loopback origin never received the request — and no such request answered for an unknown id.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_061_provide_response_wire_and_result, bidi_061_provide_response_invalid_params_rejected_before_send, bidi_061_provide_response_result_policy, bidi_061_provide_response_error_propagation and live bidi_061_provide_response_live_chrome cover the full body/cookies/headers surface, the CDDL client validation, the no-such-request lane and observable mocking (the origin is never hit). Exposure Internal (revised from advanced-public): fulfilling a request is interception plumbing; the router network.fulfill shim stays engine-internal, and a curated "mock a route" surface remains a follow-up.
Native network.removeDataCollector: RemoveDataCollectorParameters carries the single required collector member; Client::remove_data_collector validates the extensible EmptyResult. Removal releases the collector's ownership of every entry it holds, freeing entries with no remaining owner. Removal is idempotently re-issuable, so ambiguous outcomes never invalidate the connection.
Source evidence
src/bidi/network.rs:1281; src/bidi/network.rs:895
Arguments / defaults
collector is the only member — required, no optional members, no defaults. The collector id comes from network.addDataCollector and is engine-managed.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_062_remove_data_collector_live_chrome): removing the last owning collector froze its collected data (a later network.getData answered no such network data) and a second removal answered no such network collector ("Collector ... does not exist").
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_062_remove_data_collector_wire_and_result, bidi_062_remove_data_collector_result_policy, bidi_062_remove_data_collector_error_propagation and live bidi_062_remove_data_collector_live_chrome cover the wire shape, the EmptyResult policy, the no-such-collector lane and observable data release. Exposure Internal: collector lifecycle is protocol bookkeeping and the cleanup half of the addDataCollector primitive.
Native network.removeIntercept: RemoveInterceptParameters carries the single required intercept member; Client::remove_intercept validates the extensible EmptyResult. Removal affects future requests (and future phases of in-flight ones) only — already-blocked requests stay blocked. Removal is idempotently re-issuable, so ambiguous outcomes never invalidate the connection. The engine-internal router page.unroute shim keeps its untyped blob.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_063_remove_intercept_live_chrome): after removal a URL that blocked before flowed with isBlocked false, and a second removal answered no such intercept ("Intercept '...' does not exist.").
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_063_remove_intercept_wire_and_result, bidi_063_remove_intercept_result_policy, bidi_063_remove_intercept_error_propagation and live bidi_063_remove_intercept_live_chrome cover the wire shape, the EmptyResult policy, the no-such-intercept lane and observable unblocking. Exposure Internal (revised from advanced-public): removing an intercept is the cleanup half of the addIntercept primitive; the router page.unroute shim stays engine-internal.
Native network.setCacheBehavior: SetCacheBehaviorParameters models the CDDL enum as a fieldless CacheBehavior (Default/Bypass — an out-of-enum value inexpressible) plus the optional top-level-only contexts, validating the CDDL [+] non-empty contexts list client-side; Client::set_cache_behavior validates the extensible EmptyResult. Without contexts the behavior becomes the remote end's global default and clears per-context overrides; with contexts it records per-context deviations. Fully re-settable, so ambiguous outcomes never invalidate the connection.
Source evidence
src/bidi/network.rs:1314; src/bidi/network.rs:937
Arguments / defaults
cacheBehavior is required ("default" / "bypass"); contexts is optional — omitting it sets the global default and clears per-context overrides, the CDDL [+] list is rejected empty client-side, context resolution stays the browser's.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_064_set_cache_behavior_live_chrome): a fresh max-age subresource replayed from cache under the default behavior (network.responseCompleted fromCache true, no origin hit), hit the network under a global bypass (fromCache false, one origin hit), a per-context bypass scoped to its tab while the other tab kept caching, and the spec's no such frame answered for an unknown context.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_064_set_cache_behavior_wire_and_result, bidi_064_set_cache_behavior_invalid_params_rejected_before_send, bidi_064_set_cache_behavior_result_policy, bidi_064_set_cache_behavior_error_propagation and live bidi_064_set_cache_behavior_live_chrome cover both behaviors, the global/per-context forms, the EmptyResult policy, the no-such-frame lane and observable cache gating via responseCompleted fromCache. Exposure Internal: persistent cache-behavior control is distinct from the single-reload `bl reload --ignore-cache` option and has no curated surface today; a follow-up could expose it if a workflow needs it.
Native network.setExtraHeaders: SetExtraHeadersParameters carries the required [*network.Header] list plus the mutually-exclusive contexts/userContexts scopes; Client::set_extra_headers validates the extensible EmptyResult. Each call REPLACES the stored header list for its scope (global / per user context / per top-level context), so an empty list clears the scope; header-name/value productions and mutual exclusivity stay the browser's. This is the native replacement for the previously-shimmed behavior — the engine-internal router page.setHeaders shim still injects headers client-side via an addIntercept, but BIDI-065 is now covered by the real command.
headers is required (the empty list clears the scope); contexts and userContexts are optional and mutually exclusive — omitting both sets the session-global default. The CDDL [+] scope lists are rejected empty client-side; mutual exclusivity and header productions stay the browser's.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_065_set_extra_headers_live_chrome): a global extra header reached the loopback fixture, an empty list cleared the scope, a per-context header scoped to its tab (an unscoped tab never carried it), and the spec's errors answered live — invalid argument for contexts+userContexts together ("User contexts and browsing contexts are mutually exclusive"), no such user context / no such frame for unknown ids.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_065_set_extra_headers_wire_and_result, bidi_065_set_extra_headers_invalid_params_rejected_before_send, bidi_065_set_extra_headers_result_policy, bidi_065_set_extra_headers_error_propagation and live bidi_065_set_extra_headers_live_chrome cover all three scope forms, the clear-on-empty-list semantics, the CDDL client validation, the three error lanes and observable header injection/scoping. Engine status Native (revised from shimmed): the real network.setExtraHeaders now backs the capability. Exposure Internal (revised from semantic-public): the safe header-set intention would be a curated CLI/MCP surface built on this primitive, not the raw scope-coordinating command; that surface remains a follow-up.
Native network.authRequired: NETWORK_AUTH_REQUIRED pins the exact event name and parse_auth_required_params strictly decodes AuthRequiredParameters (network.BaseParameters + response: network.ResponseData) — the shared BaseParameters/RequestData/ResponseData/FetchTimingInfo/AuthChallenge parsers, the isBlocked↔intercepts invariant, and a required-but-nullable model for context/navigation/bodySize/headersSize. The event classifies Bulk in the Mux and routes through the existing Client::subscribe_events refcounting; it is the blocking authRequired phase, answered by network.continueWithAuth / continueResponse.
Event payload only — no caller parameters beyond the subscription (event name + optional context/user-context scope, session-managed). authChallenges is spec-guaranteed for this event but Chrome-omitted; intercepts is present exactly when isBlocked is true.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_066_auth_required_live_chrome): an authRequired intercept blocked a 401 challenge and the typed payload decoded with isBlocked true and the matching intercept id listed; a wrong-credential continueWithAuth re-fired the event for the same request id; an unchallenged request emitted nothing. Chrome deviations: the authRequired event carries a degenerate placeholder response — status -1 (not the spec js-uint) and no authChallenges (the spec asserts they are present) — and omits the CDDL-optional userContext; the typed decoder tolerates all three, and the challenge scheme/realm are observable on the responseStarted/responseCompleted payload and the fixture Authorization header instead.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_066_auth_required_registry_and_classification, bidi_066_auth_required_params_policy and live bidi_066_auth_required_live_chrome cover the exact event name, Bulk classification, strict payload decode (incl. the Chrome placeholder), the isBlocked/intercepts invariant and observable blocking/retry. Exposure Internal (revised advanced-public -> internal): authRequired feeds the interception lifecycle and observability, not a per-event public command. Chrome Browser BiDi Partial: the authRequired response placeholder (status -1, no authChallenges) and the omitted userContext deviate from the pinned spec.
Native network.beforeRequestSent: NETWORK_BEFORE_REQUEST_SENT pins the exact event name and parse_before_request_sent_params strictly decodes BeforeRequestSentParameters (network.BaseParameters + the optional network.Initiator, whose stackTrace is preserved verbatim) — the full RequestData with typed headers/cookies/timings, the nullable context/navigation/bodySize/initiatorType, the optional-and-nullable userContext, and the isBlocked↔intercepts invariant. The event classifies Bulk and rides the existing consumed routing: the engine router's session subscription (router.rs:266), the recorder's HAR pending-request map, and the forensic trace writer's network.request draft — all untouched.
Event payload only. The whole initiator member is omitted for parser/other-initiated requests; intercepts is present exactly when isBlocked is true; beforeRequestSent is guaranteed to precede every other network event for its (request id, redirectCount) hop.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_067_before_request_sent_live_chrome): a fetch decoded with null navigation, the empty destination, initiatorType "fetch" and a script initiator stack; a navigation decoded with a non-null navigation id and the "document" destination; a 302 redirect fired one event per hop reusing the request id with redirectCount 0 -> 1; a context-scoped subscription delivered only its tab's requests; and delivery stopped after unsubscribing. Chrome omits the CDDL-optional userContext member.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_067_before_request_sent_registry_and_classification, bidi_067_before_request_sent_params_policy, bidi_066_to_070_network_events_subscribe_wire_and_cleanup and live bidi_067_before_request_sent_live_chrome cover the exact name, Bulk classification, strict decode (fetch/navigation/redirect/blocked forms), subscription refcounting + scoping + cleanup, and no disruption to the recorder/trace consumers. Exposure Internal (revised advanced-public -> internal). Chrome Browser BiDi Partial: the omitted userContext deviates from the pinned spec.
Native network.fetchError: NETWORK_FETCH_ERROR pins the exact event name and parse_fetch_error_params strictly decodes FetchErrorParameters (network.BaseParameters + the required errorText). The event classifies Bulk and rides the existing consumed routing (the recorder's HAR pop at recording.rs:672 and the trace writer's network.failed draft at writer.rs:508); it is never a blocking phase (fetchError is not an intercept phase), and the remote end self-heals ordering by emitting the hop's beforeRequestSent first.
Event payload only. errorText is an implementation-defined failure description; the event is a hop's terminal outcome (a request ends in exactly one of responseCompleted or fetchError).
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_068_fetch_error_live_chrome): a connection-refused fetch (a bound-then-dropped loopback port) decoded with errorText net::ERR_CONNECTION_REFUSED, redirectCount 0, isBlocked false, strictly after the hop's beforeRequestSent with the same request id; a page-side AbortController abort decoded with errorText net::ERR_ABORTED; delivery stopped after unsubscribing. Chrome omits the CDDL-optional userContext member.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_068_fetch_error_registry_and_classification, bidi_068_fetch_error_params_policy and live bidi_068_fetch_error_live_chrome cover the exact name, Bulk classification, strict decode, the beforeRequestSent-precedes-fetchError ordering and observable connection-refused/abort failures. Exposure Internal (revised advanced-public -> internal). Chrome Browser BiDi Partial: the omitted userContext deviates from the pinned spec.
Native network.responseCompleted: NETWORK_RESPONSE_COMPLETED pins the exact event name and parse_response_completed_params strictly decodes ResponseCompletedParameters (network.BaseParameters + response: network.ResponseData) — the full ResponseData with the nullable headersSize/bodySize, the decoded content.size, fromCache, and the optional authChallenges. The event classifies Bulk and rides the existing consumed routing (the recorder's HAR entry at recording.rs:661, the trace writer's network.response draft at writer.rs:507, the engine router subscription at router.rs:267); it is never a blocking phase, so isBlocked is always false and intercepts always omitted — the shared invariant enforces exactly that.
Event payload only. responseCompleted is never interceptable (isBlocked always false, intercepts always omitted); fromCache is true only for the local cache state; content.size is the decoded body size while bodySize is the encoded one (both js-uint/null).
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_069_response_completed_live_chrome): a fetch decoded third in its hop's order behind beforeRequestSent and responseStarted with one request id, status 200, isBlocked false, positive bytesReceived and a decoded content.size; a fresh Cache-Control max-age subresource decoded fromCache false on the fill and fromCache true on the replay; delivery stopped after unsubscribing. Chrome omits the CDDL-optional userContext member.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_069_response_completed_registry_and_classification, bidi_069_response_completed_params_policy and live bidi_069_response_completed_live_chrome cover the exact name, Bulk classification, strict ResponseData decode, the never-blocking invariant, the per-hop ordering and the observable fromCache tri-state. Exposure Internal (revised advanced-public -> internal). Chrome Browser BiDi Partial: the omitted userContext deviates from the pinned spec.
Native network.responseStarted: NETWORK_RESPONSE_STARTED pins the exact event name and parse_response_started_params strictly decodes ResponseStartedParameters (network.BaseParameters + response: network.ResponseData), sharing the ResponseData/AuthChallenge parsers with responseCompleted/authRequired. The event classifies Bulk and rides the existing Client::subscribe_events routing; it is the last interceptable phase (responseStarted intercepts block here, answered by continueResponse / provideResponse / failRequest), fired after headers arrive and after this hop's beforeRequestSent.
Event payload only. responseStarted is the last interceptable phase; intercepts is present exactly when isBlocked is true; the payload carries the response headers/status before the body completes.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_070_response_started_live_chrome): a fetch decoded after its beforeRequestSent with one request id, status 200, http/1.1 protocol, the text/html mimeType, fromCache false and a non-empty header list; a 302 redirect decoded its own responseStarted (status 302, redirectCount 0) plus the target (status 200, redirectCount 1) reusing the request id; delivery stopped after unsubscribing. Chrome omits the CDDL-optional userContext member.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_070_response_started_registry_and_classification, bidi_070_response_started_params_policy and live bidi_070_response_started_live_chrome cover the exact name, Bulk classification, strict ResponseData decode (incl. the blocked form and a 401 authChallenges shape), the beforeRequestSent-precedes-responseStarted ordering and the redirect-hop request-id reuse. Exposure Internal (revised advanced-public -> internal). Chrome Browser BiDi Partial: the omitted userContext deviates from the pinned spec.
Native script.addPreloadScript: AddPreloadScriptParameters carries the full spec surface — functionDeclaration, channel-only arguments, contexts, userContexts, sandbox — validating only the CDDL constraints client-side (non-empty [+] lists, channel-typed arguments) and leaving the remote-end constraints (contexts/userContexts mutual exclusivity, top-level-only contexts, id resolution) to the browser; Client::add_preload_script sends the exact method name and strictly decodes the {script} UUID-handle result. The engine router's raw lanes keep powering the semantic features built on the primitive (clock install, WebSocket monitoring, the proxy-only context.addInitScript lane).
functionDeclaration is required; arguments (channel values only), contexts, userContexts and sandbox are optional with no CDDL defaults — omitting them registers a global main-world script with no channel arguments. The engine rejects only structural CDDL violations (empty [+] lists, non-channel arguments) client-side; the preload-script UUID is engine-managed and feeds script.removePreloadScript.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_071_add_preload_script_live_chrome): a page script observed the preload marker (the preload ran first), a sandboxed registration stayed invisible to the main world while the sandbox realm saw it, a contexts-scoped registration left an unlisted tab untouched, and the spec's registration errors answered live — invalid argument for contexts+userContexts together, no such frame for an unknown context, no such user context for an unknown user context.
Verified live 2026-07-14 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_071_add_preload_script_wire_and_result, bidi_071_add_preload_script_invalid_params_rejected_before_send, bidi_071_add_preload_script_result_policy, bidi_071_add_preload_script_error_propagation and live bidi_071_add_preload_script_live_chrome cover the exact method name, all three parameter forms, strict result decode, CDDL-level client validation and observable run-before-page-script / sandbox / context-scoping behavior. Exposure revised advanced-public -> internal: installing an init script is a low-level primitive already powering semantic features (clock install, WebSocket observation, the proxy-only context.addInitScript router lane); a curated init-script surface (add+remove as one intention) remains a follow-up candidate rather than a per-primitive command.
Native script.disown: DisownParameters carries the required handle list (CDDL [*script.Handle] — an empty list is a valid no-op) and both script.Target forms (context+optional sandbox / realm); Client::disown sends the exact method name and strictly validates the extensible EmptyResult. Unknown or already-disowned handles are a spec-level silent no-op, so only target-resolution errors propagate.
Source evidence
src/bidi/script.rs:915
Arguments / defaults
handles and target are both required with no optional members and no spec defaults; handles may be empty (a no-op). Normal callers never manage handles — the engine's semantic lanes run with resultOwnership none, and handle release stays an engine cleanup primitive.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_072_disown_live_chrome): a root-ownership handle worked as a RemoteObjectReference argument until disowned and then answered no such handle, an unknown handle disowned silently per spec, the realm-target form released a second handle just the same, and an unknown target context answered no such frame.
Verified live 2026-07-14 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_072_disown_wire_and_result, bidi_072_disown_result_policy, bidi_072_disown_error_propagation and live bidi_072_disown_live_chrome cover both target forms, the empty-list no-op, the strict EmptyResult policy and observable handle release. Exposure internal (risk raised read -> write: disown mutates the realm's handle object map): handle lifecycle is protocol bookkeeping with no human or agent intention.
Native script.callFunction: CallFunctionParameters carries the full spec surface — functionDeclaration, the required awaitPromise, both target forms, script.LocalValue arguments and this (verbatim wire values, structurally validated), resultOwnership, SerializationOptions (nullable depths with js-uint validation), userActivation — and Client::call_function_with strictly decodes the shared script.EvaluateResult union with fully-typed ExceptionDetails/StackTrace; a malformed success deliberately does not invalidate the connection (the only resource at stake is a realm-bounded handle). The pre-typed Client::call_function wrapper (the MCP agent's lane) keeps its exact wire shape on top, and the engine router's raw callFunction lanes (element resolution, a11y, emulation, interaction) are unchanged.
functionDeclaration, awaitPromise and target are required; arguments (empty list), resultOwnership (none), serializationOptions (maxDomDepth 0, maxObjectDepth null, includeShadowTree none), this (null) and userActivation (false) default at the remote end when omitted — the typed layer omits absent members so the defaults stay the browser's. Semantic lanes keep function, realm, ownership and handles internal.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_073_call_function_live_chrome): argument and this deserialization round-tripped, awaitPromise true unwrapped an async function while false serialized the promise RemoteValue, a thrown Error decoded as typed ExceptionDetails with stack frames, resultOwnership root retained a handle while the default did not, maxObjectDepth 0 elided object contents while an explicit null serialized full nesting, and userActivation true made navigator.userActivation.isActive observable. Deviation: a non-callable functionDeclaration answers an EvaluateResultException (TypeError: f.apply is not a function) instead of the spec's invalid argument protocol error.
Verified live 2026-07-14 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_073_call_function_wire_and_result, bidi_073_call_function_exception_result_decode, bidi_073_call_function_result_policy, bidi_073_call_function_invalid_params_rejected_before_send, bidi_073_call_function_error_propagation, bidi_073_call_function_malformed_result_keeps_connection_usable, bidi_073_call_function_compat_wrapper_wire and live bidi_073_call_function_live_chrome cover the required-only and complete wire forms, strict EvaluateResult/ExceptionDetails decode, protocol errors, the non-invalidation policy and the byte-stable pre-typed wrapper. Exposure internal: the primitive under bl eval, element operations and wait workflows — never a dedicated surface.
Native script.evaluate: EvaluateParameters carries expression, the required awaitPromise, both target forms including named sandboxes, resultOwnership, SerializationOptions and userActivation; Client::evaluate_with strictly decodes the shared script.EvaluateResult union. The pre-typed Client::evaluate wrapper keeps the bl eval / browser_evaluate contract (awaitPromise true, resultOwnership none, first-context fallback, bare-value flattening) and now surfaces the spec's exception text instead of a null detail; the engine router's page.eval raw lane is unchanged.
expression, target and awaitPromise are required; resultOwnership (none), serializationOptions (spec defaults) and userActivation (false) default at the remote end when omitted. The semantic surface (bl eval / browser_evaluate) keeps awaitPromise true, retains no handles and flattens the RemoteValue to its bare value; a dedicated sandbox stays available through the typed engine layer.
Documented · bl eval <expression> (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_074_evaluate_live_chrome): expressions evaluated in the default realm and again via the realm target it reported, a named sandbox isolated globals in both directions while the empty-string sandbox was the default realm per spec, awaitPromise true resolved a promise while false serialized it, a thrown Error decoded as typed ExceptionDetails, and both an unknown context and an unknown realm answered no such frame (matching the pinned spec, whose realm-lookup error code carries an acknowledged wrong-error-code issue marker).
Verified live 2026-07-14 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_074_evaluate_wire_and_result, bidi_074_evaluate_error_propagation, bidi_074_evaluate_compat_wrapper_wire (plus the shared bidi_073 result-policy suite) and live bidi_074_evaluate_live_chrome cover the required-only and complete wire forms, sandbox/realm targeting, strict decode and the byte-stable pre-typed wrapper behind bl eval / browser_evaluate. Exposure remains semantic-public via the existing bl eval command and browser_evaluate tool — no new surface needed.
Native script.getRealms: GetRealmsParameters carries the optional context and RealmType filters (exactly {} when both are absent); Client::get_realms strictly decodes every script.RealmInfo against the eight pinned variants — window realms with context/userContext/sandbox (sandbox realms report type window with the sandbox name), dedicated workers with owners, the base realm+origin for the rest — tolerating extension members and rejecting unknown realm types.
Source evidence
src/bidi/script.rs:955; src/bidi/script.rs:747
Arguments / defaults
context and type are both optional with no defaults — omitting them enumerates every execution-ready realm; the filters combine freely and a filter matching nothing is an empty list, never an error. Realm ids are browser-generated and stay engine-managed (risk lowered dangerous -> read: enumeration executes nothing).
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_075_get_realms_live_chrome): the default enumeration contained the same realm id script.evaluate reported with the fixture's context, a sandbox realm appeared as a window realm carrying its sandbox name, the context filter excluded a second tab's realm, a type filter matching nothing returned an empty list, and an unknown context answered no such frame. Deviation: Chrome omits the optional userContext member the spec's get-the-realm-info steps set on window realms (CDDL-optional, so the payload stays production-valid).
Verified live 2026-07-14 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_075_get_realms_wire_and_result, bidi_075_get_realms_result_policy, bidi_075_get_realms_error_propagation and live bidi_075_get_realms_live_chrome cover all four filter forms, strict decode across four RealmInfo variants, unknown-type rejection and observable enumeration/filtering. Exposure revised advanced-public -> internal: realm ids are browser-generated identifiers for engine targeting (sandboxes, workers) with no curated human or agent intention today; a diagnostic surface would ride a future protocol-debugging profile, not a per-primitive command.
Native script.removePreloadScript: RemovePreloadScriptParameters carries the single required preload-script UUID handle; Client::remove_preload_script sends the exact method name and strictly validates the extensible EmptyResult, completing the addPreloadScript lifecycle (the engine can now retire the preload registrations it installs).
Source evidence
src/bidi/script.rs:969
Arguments / defaults
script is the only member — required, with no optional members and no spec defaults; the UUID handle comes from script.addPreloadScript and stays engine-managed. Removal affects subsequently created documents only.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_076_remove_preload_script_live_chrome): an installed preload script's marker appeared on navigation, disappeared on the navigation after removal, and removing the same UUID again answered the spec's no such script.
Verified live 2026-07-14 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_076_remove_preload_script_wire_and_result, bidi_076_remove_preload_script_result_policy, bidi_076_remove_preload_script_error_propagation and live bidi_076_remove_preload_script_live_chrome cover the wire shape, the EmptyResult policy, no-such-script propagation and the observable stop-running-after-removal behavior. Exposure revised advanced-public -> internal: removal is the cleanup half of the addPreloadScript primitive and would surface together with a future curated init-script intention, not as its own command. Follow-up: the session-stored clock/WebSocket preload ids are never removed today — a clock uninstall/reset flow could now use this primitive.
Native script.message event integration: SCRIPT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events provides global, browsing-context and user-context scope with refcounted, id-based cleanup; the Mux's single reader preserves the exact raw JSON plus structured method/params, routes on the payload's source.context (documentless worker sources stay on the raw orphan path) and classifies the event Bulk beside log.entryAdded (observability data, no parked engine waiter); parse_message_params decodes the typed script.MessageParameters — required channel, the data RemoteValue preserved verbatim, and the typed script.Source with its optional context/userContext. The WebSocket monitor keeps consuming the event on the Mux-free proxy lane with its own raw session.subscribe and browserlane-ws channel filter, untouched by the typed layer.
Events have no caller-supplied parameters, defaults, results or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates subscribe/unsubscribe protocol errors. Payload requires channel, data and source (realm required; context/userContext optional — set together only for document-owned realms); data is serialized at emit time with the channel properties' serializationOptions (spec defaults) and ownership (none).
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_077_script_message_live_chrome): a script.callFunction channel argument and a preload-script channel argument both delivered the typed {channel, data, source} payload with source.realm equal to the invoking command's result realm and source.context equal to the fixture context; emit-time channel properties applied (ownership root retained a handle on the data, maxObjectDepth 0 elided object contents); a context-scoped subscription stayed silent for a foreign tab's channel; a concurrent log.entryAdded consumer kept composing; unsubscribing stopped delivery. Deviation: Chrome omits the optional userContext member the spec's get-the-source steps set alongside context (CDDL-optional, so the payload stays production-valid).
Verified live 2026-07-14 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_077_script_message_registry_and_classification, bidi_077_script_message_params_policy, bidi_077_078_079_script_events_subscribe_wire_and_cleanup, bidi_077_script_message_delivery_and_payload + bidi_077_contextless_script_message_stays_on_raw_orphan_path (bidi::mux — bulk-lane routing on source.context, raw-orphan path for documentless sources, exact raw preservation) and live bidi_077_script_message_live_chrome cover the pinned name, typed payload policy, subscription machinery and real delivery/options/scoping/cleanup on Chrome. Exposure revised advanced-public -> internal: channel messages are engine plumbing consumed by the WebSocket monitor (proxy lane) and future semantic features; the raw watch surface ('bl events tail') remains the BIDI-004 follow-up.
Native script.realmCreated event integration: SCRIPT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events provides global, browsing-context and user-context scope with refcounted, id-based cleanup; the params are a bare script.RealmInfo, so the Mux routes window realms on the top-level context member (the script-prefix fallback in extract_context), leaves worker/worklet realm infos on the raw orphan path, and classifies the event Bulk (realm state is reconstructible on demand via script.getRealms — never CriticalRouting, whose resync rebuilds context routes, not realms); parse_realm_created_params decodes the payload against the same eight pinned RealmInfo variants as script.getRealms.
Events have no caller-supplied parameters, defaults, results or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates subscribe/unsubscribe protocol errors. The payload is a bare script.RealmInfo (window realms carry context and optionally userContext/sandbox; dedicated workers carry exactly one owner realm); per the spec's subscribe steps a new subscription replays every execution-ready realm in scope.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_078_realm_created_live_chrome): subscribing replayed the already-existing execution-ready window realm per the spec's priority-2 subscribe steps; a navigation fired the event with the fresh window RealmInfo whose realm id equalled a follow-up script.evaluate's realm; creating a sandbox realm fired the event carrying the sandbox name and the sandbox evaluate's realm id; a context-scoped subscription stayed silent for a foreign tab's realms; unsubscribing stopped delivery. Deviation: Chrome omits the optional userContext member the spec's get-the-realm-info steps set on window realms (CDDL-optional, so the payload stays production-valid — the same omission recorded on BIDI-075).
Verified live 2026-07-14 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_078_realm_created_registry_and_classification, bidi_078_realm_created_params_policy, bidi_077_078_079_script_events_subscribe_wire_and_cleanup, bidi_078_realm_created_delivery_and_payload (bidi::mux — window realms route on the top-level context, worker realm infos stay on the raw orphan path, exact raw preservation) and live bidi_078_realm_created_live_chrome cover the pinned name, RealmInfo payload policy, the subscribe-replay behavior and real delivery/scoping/cleanup on Chrome. Exposure revised advanced-public -> internal: realm lifecycle is engine bookkeeping reconstructible via script.getRealms, with no curated human or agent intention today; the raw watch surface ('bl events tail') remains the BIDI-004 follow-up. Worker-realm events are decoded and orphan-routed by the typed layer but not exercised live (no worker fixture — data: URLs cannot spawn workers).
Native script.realmDestroyed event integration: SCRIPT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events provides global, browsing-context and user-context scope with refcounted, id-based cleanup; the payload carries only the destroyed realm id — no browsing context — so the Mux deliberately keeps the event on the raw orphan path (byte-identical preservation, never guessed onto a session lane) and classifies it Bulk (realm state is reconstructible via script.getRealms); parse_realm_destroyed_params decodes the typed single-member script.RealmDestroyedParameters.
Events have no caller-supplied parameters, defaults, results or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates subscribe/unsubscribe protocol errors. The payload's single required member is the destroyed realm id; the event carries no browsing context, so session-level attribution is deliberately not guessed (raw orphan path).
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_079_realm_destroyed_live_chrome): a navigation fired the event for BOTH the unloaded document's window realm and its named sandbox realm (each payload exactly the pinned realm id captured before the navigation); closing a second tab fired the event with exactly that tab's realm id; unsubscribing stopped delivery.
Verified live 2026-07-14 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_079_realm_destroyed_registry_and_classification, bidi_079_realm_destroyed_params_policy, bidi_077_078_079_script_events_subscribe_wire_and_cleanup, bidi_079_realm_destroyed_stays_on_raw_orphan_path (bidi::mux — contextless orphan routing with exact raw preservation) and live bidi_079_realm_destroyed_live_chrome cover the pinned name, payload policy, subscription machinery and real navigation/close-triggered delivery plus cleanup on Chrome. Exposure revised advanced-public -> internal: realm teardown is engine bookkeeping with no curated human or agent intention; the raw watch surface ('bl events tail') remains the BIDI-004 follow-up. Worker-termination delivery is decoded by the typed layer but not exercised live (no worker fixture).
Native storage.getCookies through the typed storage module: GetCookiesParameters carries the full spec CookieFilter (name, string/base64 BytesValue value, domain, path, size, httpOnly, secure, sameSite, expiry) and both PartitionDescriptor variants (context; storageKey with userContext and sourceOrigin), serialized member-exact with js-uint and network.SameSite bounds validated client-side. Client::get_cookies and the shared value-level parser decode the result strictly — required cookies list and partitionKey, spec-complete network.Cookie members, session-cookie expiry omission, extension members tolerated — and the engine lanes (the proxy's storageKey lane and the dyn-Session context lane behind bl cookies / browser_cookie_*) route through the same types and parser.
filter and partition are both optional with the spec defaults (no filter matches every cookie; no partition resolves to the remote end's default storage partition); every filter member and both partition descriptor variants are engine-representable. bl cookies list/get and browser_cookie_list/get derive the context partition from the session and keep their client-side name/domain matching on top.
Verified live 2026-07-13 on Chrome for Testing 150.0.7871.49: the default partition returned its cookies with partitionKey {userContext: default}; a name filter matched exactly (bl_foo, not bl_foo_2); the string and base64 forms of one value matched the same cookie (deserialize-bytes equivalence); a context partition resolved to the default partition; a fresh user context's partition was isolated and echoed in partitionKey.userContext; an unknown context returned no such frame. Chrome deviation: an unknown storageKey userContext returns no such user context eagerly (the spec's expand algorithm would defer to no such storage partition). Direct tests bidi_080_get_cookies_{wire_and_result,result_policy,invalid_params_rejected_before_send,error_propagation,live_chrome}. Further Chrome deviation (pinned by storage_snapshot_binary_cookie_roundtrip_live_chrome): non-UTF-8 cookie bytes set via the base64 form are reported back as a StringValue of their latin1 interpretation ([0xE2,0x28,0xA1] read back as "â(¡") — Chrome 150 never answers with a Base64Value, where the spec's serialize-protocol-bytes would fall back to base64.
Native storage.setCookie through the typed storage module: SetCookieParameters carries the full spec PartialCookie (required name/value/domain with string or base64 BytesValue; optional path, httpOnly, secure, sameSite, expiry — CDDL-faithfully, empty name/domain stay the remote end's unable-to-set-cookie call) and both partition descriptors, and the {partitionKey} result is decoded strictly by Client::set_cookie and the shared parser. Ambiguous outcomes (malformed success, timeout) do NOT invalidate the connection — the cookie's identity lives in the request, so callers re-query or re-issue; regression tests pin the policy. Drives bl cookies set / browser_cookie_set (context lane), the proxy setCookies/setStorage storageKey lane, and the storage-snapshot restore.
cookie (name/value/domain) is required — domain never defaults from a document, per spec; path/httpOnly/secure/sameSite/expiry are optional with the spec defaults (path /, false, false, no policy, session cookie), partition optional. All members are engine-representable, and the public surfaces expose them: bl cookies set takes --secure/--http-only/--same-site/--expiry alongside name/value/--domain/--path, and browser_cookie_set (plus the browser_set_cookie alias) takes the matching secure/httpOnly/sameSite/expiry properties, all deriving the session's context partition. sameSite=none without secure is refused at the shared surface layer — Chrome answers such a setCookie with success while silently storing nothing (verified live).
Verified live 2026-07-13 on Chrome for Testing 150.0.7871.49: a minimal cookie stored with the spec defaults (path /, not secure, not httpOnly, session lifetime); full attributes round-tripped with the exact requested expiry; a base64 value deserialized to its bytes (aGVsbG8= read back as string hello); the cookies observably reached a real navigation's Cookie: header while document.cookie hid the httpOnly one; a past expiry succeeded but stored nothing; a __Secure- prefix without secure returned unable to set cookie. Chrome deviations: an unspecified sameSite is reported as none (the spec's serialize-cookie would report default); sameSite=none without secure is accepted with success but silently stores nothing (the shared bl surface refuses it up front); sameSite=default input is accepted and reported back as lax. Direct tests bidi_081_set_cookie_{wire_and_result,result_policy,invalid_params_rejected_before_send,error_propagation,malformed_result_keeps_connection_usable,timeout_keeps_connection_usable,live_chrome}. Surface extension same day: --secure/--http-only/--same-site/--expiry on bl cookies set + matching browser_cookie_set/browser_set_cookie properties, routed through the shared engine::set_cookie.
Native storage.deleteCookies through the typed storage module: DeleteCookiesParameters shares the full CookieFilter and both partition descriptors with getCookies (no filter = the spec's clear-the-partition form), and the {partitionKey} result is decoded strictly by Client::delete_cookies and the shared parser. Deletion is idempotent, so ambiguous outcomes keep the connection usable (pinned by regression). Drives bl cookies delete/clear / browser_cookie_delete|clear (per-cookie exact {name,domain,path} identity deletes), the proxy clearCookies lane (typed partition + verbatim extensible filter passthrough), and the daemon's session-reset rotation (delete_cookies_with_timeout, 5s best-effort bound).
filter and partition are both optional with the spec defaults; omitting the filter deletes every cookie in the resolved partition, so the granular surfaces always send an explicit identity filter — bl cookies delete resolves each matching cookie to an exact {name, domain, path} filter and bl cookies clear without --domain is the only whole-partition form.
Verified live 2026-07-13 on Chrome for Testing 150.0.7871.49: a name-filtered delete removed exactly bl_a while bl_b survived in both storage.getCookies and a real page's document.cookie; deleting zero matches succeeded; deleting a fresh user context's storageKey partition removed its cookie, echoed the userContext in partitionKey, and left the default partition untouched; the no-filter form emptied the default partition; an unknown context returned no such frame. Direct tests bidi_082_delete_cookies_{wire_and_result,result_policy,error_propagation,malformed_result_keeps_connection_usable,timeout_keeps_connection_usable,live_chrome}.
Native log.entryAdded integration: LOG_EVENTS registers the exact pinned-spec event name; Client::subscribe_events/unsubscribe_events provides global, browsing-context and user-context scope with refcounted cleanup and no invented event parameters. The parity payload validator checks the pinned outer log.Entry shape (required type/level/source/text/timestamp, the Level enum, js-uint timestamp, required source.realm with optional context/userContext, optional StackTrace, and console method/args) while preserving RemoteValue args and the complete raw JSON. The Mux classifies the event as Bulk and routes source.context; a contextless entry with source.userContext is delivered only to that session, while a fully unscoped entry stays on the raw Mux orphan/standalone Client handler path and is not broadcast across daemon sessions. Every Browser Process always-on-subscribes PROCESS_OBSERVABILITY_EVENTS so sessions buffer console/page-error entries for bl console / browser_console_* without requiring recording. Recorder and forensic-trace subscriptions overlap through the same refcounted path; the session actor's wait_for_event matches Bulk traffic so console wait does not starve behind Critical-only pumps.
Events have no caller-supplied event parameters, defaults, command result, or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, validates the pinned outer log.Entry shape for parity assertions, preserves the complete extensible payload, and propagates session.subscribe/session.unsubscribe protocol errors. Semantic bl console list|wait|clear / browser_console_* expose optional level (debug|info|warn|error), text substring, type (console|javascript), and wait timeout_ms (default 30000); subscription bookkeeping stays internal. The per-session console buffer holds 256 entries and drops the oldest on overflow. Trace drafting length-bounds console text (not redacted).
Documented · bl console list|wait|clear (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 by bidi_083_log_entry_added_live_chrome: console.log, console.error, and uncaught Error payloads delivered with the required BaseLogEntry plus ConsoleLogEntry/JavascriptLogEntry fields; an explicit foreign marker was absent during a context-scoped subscription before a same-context marker was accepted; an explicit post-unsubscribe marker was absent.
Direct parity tests bidi_083_log_events_registry_is_exact, bidi_083_log_entry_shape_policy, bidi_083_log_entry_added_delivery_and_payload, bidi_083_contextless_log_entry_stays_on_raw_orphan_path, bidi_083_contextless_log_routes_by_user_context_without_leakage, bidi_083_javascript_log_entry_shape, bidi_083_subscribe_unsubscribe_wire_and_cleanup, bidi_083_console_tools_are_strict_and_session_scoped, and live bidi_083_log_entry_added_live_chrome. Only console and javascript variants were exercised live; generic/contextless shapes and routing were verified on wire/unit paths. Fully unscoped entries cannot be assigned to one daemon session and therefore do not appear in semantic console buffers. Exposure remains semantic-public/advanced: humans and agents ask to inspect console output; raw subscription mechanics remain internal. The docs-site CLI/MCP reference MDX remains release-generated by gen:reference.
Native input.performActions: PerformActionsParameters and the typed SourceActions model cover the pinned command's complete CDDL — none, key, pointer and wheel sources; pause, keyDown/keyUp, pointerDown/pointerUp/pointerMove and scroll actions; mouse/pen/touch pointer types; viewport/pointer/element origins; every optional pointer property and duration; and exact js-uint/js-int/finite-float range validation before send. Client::perform_actions and the shared Session::perform_actions lane (proxy/router raw-envelope validation by default, AgentSession overriding with the typed Client method) send the exact method and strictly validate the extensible EmptyResult. Existing click/type/key/mouse/touch/drag/scroll primitives on CLI and MCP build this typed command rather than maintaining a second protocol path.
context and actions are required; the source list and every source's action list may be empty. Source ids, browsing-context ids and key values are spec text preserved verbatim. Pointer parameters and pointerType are optional (pointerType defaults to mouse); pause and action durations are optional js-uint values; wheel origin defaults to viewport; pointer-move origin is optional; element origins carry script.SharedReference. Pointer buttons/properties, wheel coordinates/deltas and all float ranges are validated before send. Public CLI/MCP intentions generate source arrays, coordinates and timing; the raw generic command remains engine-internal.
Documented · bl click|type|mouse|drag (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
partial (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_084_perform_actions_live_chrome): an element-origin default-mouse sequence clicked a real button; parallel none+key sources typed into a focused input; pen and touch pointer actions delivered their pointer types and pressure; a default-viewport wheel action scrolled the page; another browsing context remained unchanged; an unknown context returned no such frame; and an invalid multi-grapheme key returned invalid argument. Chrome deviations: the supplied pen contact width 9 surfaced as 1, and Chrome rejected the pinned CDDL's wheel pointer origin as invalid argument even though input.Origin includes "pointer".
Verified live 2026-07-14 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_084_perform_actions_wire_parameters_defaults_and_result, bidi_084_perform_actions_invalid_parameters_rejected_before_send, bidi_084_perform_actions_result_policy, bidi_084_perform_actions_protocol_error_propagation, bidi_084_perform_actions_proxy_session_response_validation, bidi_084_semantic_mouse_coordinates_preserve_float_wire_values, bidi_084_input_integer_schemas_match_protocol_ranges, bidi_084_input_js_int_arguments_reject_fractional_values, and live bidi_084_perform_actions_live_chrome cover the complete typed wire model, numeric validation, EmptyResult, both engine lanes, surface schema/argument alignment and observable Chrome behavior. Chrome support is partial because Chrome 150 normalizes pen width and rejects the spec-valid wheel pointer origin. Exposure remains internal for raw action sequences; existing semantic CLI/MCP click, type, key, mouse, touch, drag and scroll intentions remain public and the Skill documents the CLI workflows.
Native input.releaseActions: ReleaseActionsParameters represents the spec's single required context (no optional members, no defaults) and serializes exactly {context}; the context is spec text preserved verbatim so the remote end supplies the semantic no such frame error. Client::release_actions and the shared Session::release_actions lane (proxy/router raw-envelope validation by default, AgentSession overriding with the typed Client method — the same split as input.performActions) send the exact method name and strictly validate the extensible EmptyResult, so the input-state cleanup primitive is reachable from both engine lanes for cleanup/finally flows.
context is the only parameter — required, with no optional members and no spec defaults; it is preserved verbatim so the remote end supplies no such frame, and the result is the extensible EmptyResult. Semantic bl keyboard up / bl mouse up and browser_keyboard_up / browser_mouse_up cover the user-facing release intention; the whole-state reset stays a cleanup/finally engine primitive that never requires user invocation.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_085_release_actions_live_chrome): a held keyDown "a" and held pointerDown 0 created via input.performActions were undone in reverse input-cancel-list order (pointerup:0 then keyup:a), the reset made a second release a clean no-op, releasing another context left the primary’s held state untouched, and an unknown context returned no such frame.
Verified live 2026-07-14 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_085_release_actions_wire_parameters_and_result, bidi_085_release_actions_result_policy, bidi_085_release_actions_protocol_error_propagation, bidi_085_release_actions_proxy_session_response_validation, and live bidi_085_release_actions_live_chrome cover wire shape, EmptyResult policy, protocol errors on the genuine Client→Mux→Connection path, the proxy/router envelope lane, and observable undo/reset/scoping behavior. Exposure remains internal: resetting input state is protocol cleanup, not a distinct human or agent intention — no dedicated CLI command or MCP tool.
Native input.setFiles: SetFilesParameters represents the spec's three required members — context, element (script.SharedReference with sharedId plus optional handle), and files ([*text], including the empty list) — and serializes exactly {context, element, files}; every value is spec text preserved verbatim so the remote end supplies the semantic errors (no such frame, no such element / no such node, unable to set file input, unsupported operation). Client::set_files and the shared Session::set_files lane (proxy/router raw-envelope validation by default, AgentSession overriding with the typed Client method — the same split as input.performActions) send the exact method name and strictly validate the extensible EmptyResult. The upload() primitive, browserlane:element.setFiles router handler, bl upload, and browser_upload all route through this typed path instead of ignoring BiDi error envelopes.
context, element (SharedReference), and files are all required with no optional members and no spec defaults; files may be the empty list ([*text]). Semantic bl upload <locator> <files...> and browser_upload derive context and element handle from the locator and resolve file paths to absolute before send; path existence is enforced at the CLI/MCP layer (Chrome refuses paths it cannot stat). Result is the extensible EmptyResult.
Exposure decision
Semantic public
CLI
bl upload <locator> <files...> · Shipped · Public
MCP
browser_upload · Shipped · Public · profile: core
Skill workflow
Documented · bl upload <locator> <files...> (teaches the CLI; not a separate implementation)
Engine test
Verified
Chrome browser BiDi support
supported (browser-side, tracked independently)
Chrome evidence
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_086_set_files_live_chrome): a real temp file appeared in the FileList with input+change events, an identical re-selection fired cancel, an empty files list cleared the selection, multiple accepted two files while a single input rejected them with unable to set file input (as did a text input and a disabled file input), an unknown sharedId returned no such node, and an unknown context returned no such frame.
Verified live 2026-07-14 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_086_set_files_wire_parameters_and_result, bidi_086_set_files_result_policy, bidi_086_set_files_protocol_error_propagation, bidi_086_set_files_proxy_session_response_validation, and live bidi_086_set_files_live_chrome cover wire shape, EmptyResult policy, all four definitive protocol error codes on the genuine Client→Mux→Connection path, the proxy/router envelope lane (the regression where unable to set file input silently passed as success), and observable selection/cancel/clear/multiple/error behavior. Exposure remains semantic-public via bl upload / browser_upload; no new CLI command or MCP tool.
Native input event integration: INPUT_EVENTS registers the exact pinned-spec event name; Client::subscribe_events/unsubscribe_events provides global, browsing-context and user-context scope with refcounted, id-based cleanup; the Mux's single reader preserves the exact raw JSON plus structured method/params, routes on the payload's top-level context, and classifies the event CriticalEphemeral next to the prompt/download lifecycle (open-dialog state has no query command to reconstruct it); parse_file_dialog_opened_params decodes the typed input.FileDialogInfo — required context/multiple, optional userContext and element (script.SharedReference). The subscription deliberately stays off product sessions: Chrome auto-cancels a subscribed session's picker, which would break headed human file choosing, and the semantic file-chooser answer remains input.setFiles (bl upload / browser_upload).
Events have no caller-supplied event parameters, defaults, command result, or event-specific errors. The engine supplies session.subscribe's events and optional context/user-context scope, derives subscription ids, refcounts and routing context, preserves the complete extensible payload, and propagates session.subscribe/session.unsubscribe protocol errors. Payload requires context and multiple; userContext and element (SharedReference sharedId plus optional handle) are optional. Dialog disposition is governed by the session's unhandledPromptBehavior file/default handler, not by this event.
Verified live on Chrome for Testing 150.0.7871.49 (direct parity test bidi_087_file_dialog_opened_live_chrome): a genuine pointer click on a file input delivered the event on both the product {default: ignore} session and a {file: dismiss} session, with context, multiple (false/true for plain/multiple inputs) and the same element sharedId locateNodes resolves; a context-scoped subscription delivered only its own context, the id-based scoped unsubscribe (and the global attributes-form one) stopped delivery, and browsingContext.userPromptOpened kept flowing on the same connection. Deviations: the payload omits the userContext the spec's remote-end steps set (CDDL-optional, so still production-valid), and Chrome auto-cancels the picker for a subscribed session even when the file handler resolves to ignore (the spec's ignore path leaves it open) — with file: dismiss the cancel is the spec's dismissed path.
Verified live 2026-07-14 on Chrome for Testing 150.0.7871.49. Direct parity tests bidi_087_file_dialog_opened_subscription_wire_and_cleanup, bidi_087_file_dialog_opened_payload_policy, bidi_087_file_dialog_opened_delivery_and_payload (bidi::mux — critical-lane routing and exact raw preservation) and live bidi_087_file_dialog_opened_live_chrome cover the pinned name through the refcounted subscribe/unsubscribe machinery, the typed FileDialogInfo decode policy, and real delivery/scoping/cleanup on Chrome. Exposure revised recommended-semantic -> internal: subscribing makes Chrome auto-cancel every picker in the session (an always-on watch surface would break headed human file choosing), and the file-chooser intention is already semantic-public via bl upload / browser_upload (BIDI-086); a raw watch surface remains the BIDI-004 'bl events tail' follow-up.
Native webExtension.install: InstallParameters carries the required extensionData member as the three-variant ExtensionData choice (path / archivePath / base64, each serializing to exactly its type tag plus one text member, preserved verbatim — CDDL leaves nothing to validate locally), Client::install_web_extension sends the exact method and strictly decodes the {extension} InstallResult to a non-empty extension id. The id is the response's only handle and the module has no list command, so an ambiguous outcome (malformed success or timeout) invalidates the connection, the browser.createUserContext policy.
extensionData is required with no optional members and no spec defaults; the engine represents all three data variants. Chrome 150 installs only the unpacked-directory "path" variant — "archivePath"/"base64" answer the spec-sanctioned unsupported operation — and only functions on a session whose Chrome launch arguments permit extension work; no curated surface derives arguments today.
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49: on an extension-capable session, install from an unpacked directory returned the extension id and the extension's content script observably stamped a real page; archivePath/base64 answered `unsupported operation` ("Archived and Base64 extensions are not supported") — Chrome support is Partial. Chrome deviation: unresolvable data (nonexistent path, dir without a manifest) answers `unknown error` where the spec's steps resolve to `invalid web extension`. Exposure is Internal, not the previously recommended advanced surface: under browserlane's standard launch arguments (--disable-extensions) Chrome 150 answers install with a phantom success — an id is returned but the extension never functions and uninstall does not know the id — so a dedicated CLI/MCP surface would claim success while doing nothing on every product session; an extension-capable launch profile is a separate product decision. --disable-extensions is the one launch argument that matters: dropping it alone enables the full lifecycle (no --enable-unsafe-extension-debugging is needed on chromedriver-launched Chrome 150, verified both ways). Direct tests bidi_088_install_{wire_and_result,result_policy,error_propagation,malformed_result_invalidates_connection,timeout_invalidates_connection} and the shared live scenario cover wire shape, result policy, protocol errors, the invalidate-on-ambiguous policy, and observable behavior.
Native webExtension.uninstall: UninstallParameters carries the required extension id (spec text, preserved verbatim — the remote end owns no such web extension), Client::uninstall_web_extension sends the exact method and strictly validates the extensible EmptyResult. The identity lives in the request and re-issuing is safe, so an ambiguous outcome surfaces as a plain error with the connection intact — the deliberate contrast with install's invalidate-on-ambiguous policy.
extension (the id webExtension.install returned) is required with no spec defaults; unknown ids — a repeat uninstall included — answer no such web extension. No curated surface derives arguments today (see BIDI-088).
Verified live 2026-07-15 on Chrome for Testing 150.0.7871.49: on an extension-capable session, uninstall of the installed id succeeded and the extension's content script observably stopped running on a fresh load; a repeat uninstall of the removed id and a never-installed id both answered `no such web extension`. Exposure is Internal alongside BIDI-088: under browserlane's standard launch arguments (--disable-extensions) install only phantom-succeeds and uninstall does not know the returned id, so no curated surface exists today. Direct tests bidi_089_uninstall_{wire_and_result,error_propagation,malformed_result_keeps_connection_usable,timeout_keeps_connection_usable} and the shared live scenario cover wire shape, the strict EmptyResult, protocol errors, the non-invalidation policy, and observable behavior.
Capability names link to their section in the tracked specification snapshot;
source evidence cites the engine file and line the audit verified (the engine
is closed-source, so the citation itself is the evidence). See
the status model for what each label means.