Skip to content
Open
Show file tree
Hide file tree
Changes from 79 commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
ebdd3a4
feat(firefox): attach to a running Firefox over WebDriver BiDi
lahfir Oct 30, 2026
956c4c5
fix(firefox): reject local file moves from attached browsers (#2384)
vercel-security-reviewer-slack[bot] Nov 3, 2026
74bfa68
fix(firefox): stop blaming --engine firefox for a missing BiDi endpoint
lahfir Nov 5, 2026
566109b
fix(firefox): complete native HTML5 drag gestures
lahfir Nov 5, 2026
9cab167
fix(firefox): read the active page through BiDi
lahfir Nov 5, 2026
f47e6a6
fix(firefox): retain HAR request bodies and elapsed time
lahfir Nov 5, 2026
be03e56
fix(firefox): complete HTML5 drags with DOM events
lahfir Nov 6, 2026
055b290
Merge updated Firefox live view into attach support
lahfir Nov 6, 2026
e3a1b8f
Merge commit 'd728f294' into HEAD
lahfir Nov 6, 2026
f615ae1
Align Firefox attach documentation with its route contract
lahfir Nov 6, 2026
12aa67d
Merge commit '25f08ba0eaef2a348b0bfc5e9b0f838accdc9a11' into HEAD
lahfir Nov 6, 2026
515e914
Merge commit '6d74b840fcba0b5a8e49adb3756aa1343fcf935e' into HEAD
lahfir Nov 6, 2026
969a7e8
Merge commit '24224a317705a9ede6041178fb2903e3d36829dc' into HEAD
lahfir Nov 6, 2026
c6ac5e3
Merge Firefox dashboard modifier fix into attach branch
lahfir Nov 6, 2026
cebd656
fix(firefox): accept bare WebSocket endpoints and preserve CDP fallback
lahfir Nov 6, 2026
b7dc8fe
fix: initialize network controls before resuming new pages
lahfir Nov 6, 2026
5d3349f
fix: enable the current Chrome WebMCP feature
lahfir Nov 6, 2026
41741bf
Merge verified Firefox fixes into attach support
lahfir Nov 6, 2026
b32b4b8
fix: align regression tests with integrated browser code
lahfir Nov 6, 2026
9926c7f
Merge commit 'f0567c0dd85028606ef74d7018d567b90e6aef38' into HEAD
lahfir Nov 6, 2026
821c1ff
style: format integrated Firefox regressions
lahfir Nov 6, 2026
4545b25
Merge commit 'e105fbf2c1ba67b2249b560ba011b14e82b6a86c' into HEAD
lahfir Nov 6, 2026
6822591
fix: correct docs fixtures and bound popup regression waits
lahfir Nov 6, 2026
89a9918
Merge commit 'a71d0e2e2abd0cdf7af24f4c71f76e15aa32fad0' into HEAD
lahfir Nov 6, 2026
6bf5647
Merge commit '37324d1661c0c3d18360abb179bf5d0492fd9255' into HEAD
lahfir Nov 6, 2026
5d89c51
Merge Firefox launch, tab and storage fixes into attach
lahfir Nov 6, 2026
82151e8
fix(firefox): verify restore recovery and document capture limits
lahfir Nov 6, 2026
eb3b770
Merge final Firefox checks into attach
lahfir Nov 6, 2026
a4d8baf
Merge commit '90e6ada2339f26e0cd4217e9d75037205cf508e0' into HEAD
lahfir Nov 6, 2026
f8382ed
Merge commit '8314cc4cccfc83a2948b625e22623ccd45931e5f' into HEAD
lahfir Nov 6, 2026
1265028
Merge commit '54a605ffd5d544643ae917a5d494eebba600f9fc' into HEAD
lahfir Nov 6, 2026
ba88c3b
test(firefox): accept the command deadline for idle waits
lahfir Nov 6, 2026
9cd7033
Merge commit 'fc020a1132215f9aa26cf2cc70f1d9ff5c3985c2' into HEAD
lahfir Nov 6, 2026
b2342fa
Merge the pre-attach regression correction
lahfir Nov 6, 2026
95c9ec9
Merge commit '5c9057aef52e3c552712af8e9fa707c69c7d9e18' into HEAD
lahfir Nov 6, 2026
2a0cd6d
Merge commit '1b76723e2ed5a9bb23754c9aa6d75c77190dad45' into HEAD
lahfir Nov 6, 2026
2cdb44c
merge: update Firefox attach against current main
lahfir Nov 6, 2026
0f9bc46
test(firefox): use the current daemon stream constructor
lahfir Nov 6, 2026
a29d016
Merge commit '18f4b3098970505b027f11d2b8076290e7d15fa4' into HEAD
lahfir Nov 6, 2026
e2ae420
fix(firefox): keep inferred attachment engines out of settings
lahfir Nov 6, 2026
b531619
Merge commit '7ce354f' into HEAD
lahfir Nov 6, 2026
f806c57
Merge commit '7b6e331' into HEAD
lahfir Nov 6, 2026
dcbad8a
style(firefox): format attachment routing regressions
lahfir Nov 6, 2026
075f406
Merge commit '701bf241' into HEAD
lahfir Nov 6, 2026
5a255c4
Merge commit '619b7ee8' into HEAD
lahfir Nov 6, 2026
2bccbb1
Merge commit '06bfc66a' into HEAD
lahfir Nov 6, 2026
6d86a7d
Merge commit 'fb69f09' into HEAD
lahfir Nov 6, 2026
3257f14
Merge Firefox networkidle tab scoping fix
lahfir Nov 6, 2026
83de658
Merge commit '916242119701349fcb27431d02bb6145dc9cf1c2' into HEAD
lahfir Nov 6, 2026
f9f13a0
Merge Firefox dialog and pointer fixes into attach support
lahfir Nov 7, 2026
d4ad648
Merge duplicate-test cleanup into attach support
lahfir Nov 7, 2026
941282e
Merge Firefox review fixes into attach support
lahfir Nov 7, 2026
9c2d5c4
Merge storage regression formatting into attach support
lahfir Nov 7, 2026
c45f047
Merge dispatcher cleanup into attach support
lahfir Nov 7, 2026
3baf5f7
Merge dispatcher cleanup into attach support
lahfir Nov 7, 2026
8112e8e
Merge PR Firefox test gating into attach support
lahfir Nov 7, 2026
1209276
Merge request, proxy, and popup fixes into attach support
lahfir Nov 7, 2026
585eff7
Merge request, proxy, and popup fixes into attach support
lahfir Nov 7, 2026
677eea5
Merge request scope formatting into attach support
lahfir Nov 7, 2026
3e76339
Merge request scope compile fixes into attach support
lahfir Nov 7, 2026
409890b
Merge request scope formatting into attach support
lahfir Nov 7, 2026
a64a832
Treat slow Firefox attach probes as healthy
lahfir Nov 7, 2026
1af303b
Merge DevTools and session fixes into attach support
lahfir Nov 7, 2026
3639302
Merge snapshot formatting into attach support
lahfir Nov 7, 2026
e6c6f97
Merge latest BiDi compile fixes into attach support
lahfir Nov 7, 2026
4a7eadb
Merge snapshot result fix into attach support
lahfir Nov 7, 2026
d185245
Apply RDP timeout and liveness fixes to attach support
lahfir Nov 7, 2026
c14893a
Merge final interaction fixes into attach support
lahfir Nov 7, 2026
de4827b
Fail closed after a Firefox DevTools timeout
lahfir Nov 7, 2026
14f4bc2
Merge fail-closed RDP timeout into attach support
lahfir Nov 7, 2026
5e74e67
Merge RDP timeout formatting into attach support
lahfir Nov 7, 2026
ac154aa
Keep standard RDP replies after fail-closed timeouts
lahfir Nov 7, 2026
f68a4ba
Merge standard RDP reply handling into attach support
lahfir Nov 7, 2026
c0f2e28
Merge RDP assertion formatting into attach support
lahfir Nov 7, 2026
1a82824
Keep attach test IO imports after merge
lahfir Nov 7, 2026
5df85e8
Merge duplicate DevTools cleanup into attach support
lahfir Nov 7, 2026
b26ad17
Update attached Firefox liveness expectations
lahfir Nov 7, 2026
4572568
Merge laf/bidi-live-view-docs into laf/bidi-attach
lahfir Nov 8, 2026
cf7a6e0
Merge laf/bidi-live-view-docs into laf/bidi-attach
lahfir Nov 8, 2026
d86864f
refactor(firefox): the attached session records what it applied and u…
lahfir Nov 8, 2026
3dcb99b
Merge laf/bidi-live-view-docs into laf/bidi-attach
lahfir Nov 8, 2026
f68465a
Merge remote-tracking branch 'origin/laf/bidi-live-view-docs' into HEAD
lahfir Nov 8, 2026
3b056c9
Merge remote-tracking branch 'origin/laf/bidi-live-view-docs' into HEAD
lahfir Nov 8, 2026
f6074f5
Merge remote-tracking branch 'origin/laf/bidi-live-view-docs' into HEAD
lahfir Nov 8, 2026
f54e394
Merge remote-tracking branch 'origin/laf/bidi-live-view-docs' into HEAD
lahfir Nov 8, 2026
6badbc3
Merge remote-tracking branch 'origin/laf/bidi-live-view-docs' into HEAD
lahfir Nov 8, 2026
b517ff2
Merge remote-tracking branch 'origin/laf/bidi-live-view-docs' into HEAD
lahfir Nov 8, 2026
c6cea77
Merge laf/bidi-live-view-docs into laf/bidi-attach
lahfir Nov 8, 2026
e3839ef
Merge remote-tracking branch 'origin/laf/bidi-live-view-docs' into HEAD
lahfir Nov 8, 2026
013113b
Merge laf/bidi-live-view-docs into laf/bidi-attach
lahfir Nov 9, 2026
a02abab
Merge remote-tracking branch 'origin/laf/bidi-live-view-docs' into HEAD
lahfir Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ The CLI sends each command to a background daemon that starts on the first comma

Chrome for Testing is the default browser. You can also attach to a running Chrome with `--cdp` or `--auto-connect` ([CDP mode](https://agent-browser.dev/cdp-mode)), select the [Lightpanda](https://agent-browser.dev/engines/lightpanda) or [Obscura](https://agent-browser.dev/engines/obscura) engine with `--engine`, drive Mobile Safari in the [iOS Simulator](https://agent-browser.dev/ios), or use a cloud browser provider such as [Browserbase](https://agent-browser.dev/providers/browserbase), [Browserless](https://agent-browser.dev/providers/browserless), [Browser Use](https://agent-browser.dev/providers/browser-use), [Kernel](https://agent-browser.dev/providers/kernel), or [AgentCore](https://agent-browser.dev/providers/agentcore).

Select Firefox with `--engine firefox`. It requires Firefox 150 or newer, uses WebDriver BiDi and Firefox's DevTools accessibility tree for snapshots, and starts with a fresh temporary profile. See the [Firefox engine guide](https://agent-browser.dev/engines/firefox).
Select Firefox with `--engine firefox`. It requires Firefox 150 or newer, uses WebDriver BiDi and Firefox's DevTools accessibility tree for snapshots, and starts with a fresh temporary profile. You can also attach to a running Firefox with `connect` or `--cdp`. See [CDP mode](https://agent-browser.dev/cdp-mode) for attach setup.

## For AI agents

Expand Down
41 changes: 41 additions & 0 deletions cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,24 @@ fn reject_batch_global_flag(batch_item: bool, arg: &str) -> Result<(), ParseErro
}
}

pub fn attach_allowed_domains_to_launch_command(cmd: &mut Value, flags: &Flags) {
if let Some(ref domains) = flags.allowed_domains {
cmd["allowedDomains"] = json!(domains);
}
}

/// Adds the launch settings that also apply when attaching to a running browser.
pub fn apply_attach_launch_options(cmd: &mut Value, flags: &Flags) {
attach_allowed_domains_to_launch_command(cmd, flags);
if let Some(ref path) = flags.download_path {
cmd["downloadPath"] = json!(path);
}
if let Some(ref engine) = flags.engine {
cmd["engine"] = json!(engine);
}
}

/// Stamps `timeout` on a wait-family command that has none.
/// Stamps `timeout` on a wait-family command or `flow_save` that has none,
/// so the client read timeout follows it.
pub fn inherit_wait_timeout(cmd: &mut Value, timeout: Option<u64>) {
Expand Down Expand Up @@ -1393,6 +1411,7 @@ fn parse_command_inner(
|| endpoint.starts_with("https://")
{
let mut cmd = json!({ "id": id, "action": "launch", "cdpUrl": endpoint });
apply_attach_launch_options(&mut cmd, flags);
if let Some(ref raw) = flags.cdp_headers {
cmd["cdpHeaders"] =
parse_cdp_headers(raw).map_err(|message| ParseError::InvalidValue {
Expand Down Expand Up @@ -1431,6 +1450,7 @@ fn parse_command_inner(
}
};
let mut cmd = json!({ "id": id, "action": "launch", "cdpPort": port });
apply_attach_launch_options(&mut cmd, flags);
if let Some(ref raw) = flags.cdp_headers {
cmd["cdpHeaders"] =
parse_cdp_headers(raw).map_err(|message| ParseError::InvalidValue {
Expand Down Expand Up @@ -6585,6 +6605,27 @@ mod tests {
assert_eq!(cmd["cdpPort"], 1);
}

#[test]
fn test_connect_forwards_attach_launch_options() {
let mut flags = default_flags();
flags.download_path = Some("/tmp/downloads".to_string());
flags.allowed_domains = Some(vec!["example.com".to_string()]);
flags.engine = Some("firefox".to_string());
flags.profile = Some("/tmp/profile".to_string());
flags.init_scripts = vec!["/tmp/init.js".to_string()];

let port = parse_command(&args("connect 9222"), &flags).unwrap();
assert_eq!(port["downloadPath"], "/tmp/downloads");
assert_eq!(port["allowedDomains"], json!(["example.com"]));
assert_eq!(port["engine"], "firefox");
assert!(port.get("profile").is_none());
assert!(port.get("initScripts").is_none());

let url = parse_command(&args("connect ws://127.0.0.1:9222/session"), &flags).unwrap();
assert_eq!(url["downloadPath"], "/tmp/downloads");
assert_eq!(url["engine"], "firefox");
}

// === Runtime stream control tests ===

#[test]
Expand Down
21 changes: 21 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2324,6 +2324,27 @@ mod tests {
assert_eq!(wire["headed"], true);
}

#[test]
fn firefox_cdp_flags_travel_with_the_command_settings() {
let (command, wire) = settings_of(&[
"--engine",
"firefox",
"--cdp",
"9222",
"--download-path",
"/tmp/downloads",
"--allowed-domains",
"example.com",
"get",
"url",
]);
assert_eq!(command["action"], "url");
assert_eq!(wire["engine"], "firefox");
assert_eq!(wire["cdp"], "9222");
assert_eq!(wire["download-path"], "/tmp/downloads");
assert_eq!(wire["allowed-domains"], json!(["example.com"]));
}

#[test]
fn settings_distinguish_ca_trust_set_omitted_and_cleared() {
let (_, omitted) = settings_of(&["get", "url"]);
Expand Down
37 changes: 31 additions & 6 deletions cli/src/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1862,11 +1862,11 @@ fn parity_tools() -> Vec<Value> {
),
tool(
TOOL_CONNECT,
"Connect CDP",
"Connect to a browser over CDP. With pinTab, the session is strictly bound to its own tab: it re-binds by target id after restarts and fails with a tab_gone error instead of adopting another tab. Structured errors include code=tab_gone, data.targetId, and optional sanitized data.lastUrl. Pass pinTab: false to explicitly disable a sticky pin; omit it to keep the current state. The session stays attached until agent_browser_close, so later tools without a connection target use the same browser.",
"Connect to browser via CDP or Firefox WebDriver BiDi",
"Connect to a browser over CDP or to a running Firefox over WebDriver BiDi. A ws:// URL whose path is /session attaches to Firefox directly; an http(s):// URL or bare port falls back to BiDi when CDP discovery finds nothing. Add ?devtools=<port> to point at the Firefox DevTools server (default 6000). Firefox's RDP companion is unencrypted, so remote Firefox ports must be forwarded through an encrypted tunnel and the connection must use 127.0.0.1. With pinTab on a Chrome CDP session, the session is strictly bound to its own tab: it re-binds by target id after restarts and fails with a tab_gone error instead of adopting another tab. The pinTab binding and its tab_gone guarantee are CDP-only; Firefox attach rejects pinTab. Structured errors include code=tab_gone, data.targetId, and optional sanitized data.lastUrl. Pass pinTab: false to explicitly disable a sticky pin; omit it to keep the current state. The session stays attached until agent_browser_close, so later tools without a connection target use the same browser.",
json!({
"target": { "type": "string", "description": "CDP port or URL." },
"pinTab": { "type": "boolean", "description": "Strict session-to-tab binding (sticky for the session). Explicit false disables a sticky pin; omitted leaves it unchanged." }
"target": { "type": "string", "description": "CDP port or URL, or a Firefox WebDriver BiDi endpoint such as ws://127.0.0.1:9333/session or http://127.0.0.1:9333?devtools=6080." },
"pinTab": { "type": "boolean", "description": "Strict session-to-tab binding (sticky for the session; CDP only, rejected for Firefox attach). Explicit false disables a sticky pin; omitted leaves it unchanged." }
}),
&["target"],
),
Expand Down Expand Up @@ -2853,15 +2853,19 @@ fn call_one_string(arguments: &Value, command: &str, key: &str) -> Result<Value,
call_cli_tool(arguments, args, None)
}

fn call_connect(arguments: &Value) -> Result<Value, ProtocolError> {
fn connect_args(arguments: &Value) -> Result<Vec<String>, ProtocolError> {
let mut args = vec!["connect".to_string()];
args.push(required_string(arguments, "target")?);
match arguments.get("pinTab").and_then(|v| v.as_bool()) {
Some(true) => args.push("--pin-tab".to_string()),
Some(false) => args.push("--no-pin-tab".to_string()),
None => {}
}
call_cli_tool(arguments, args, None)
Ok(args)
}

fn call_connect(arguments: &Value) -> Result<Value, ProtocolError> {
call_cli_tool(arguments, connect_args(arguments)?, None)
}

fn call_optional_one(arguments: &Value, parts: &[&str], key: &str) -> Result<Value, ProtocolError> {
Expand Down Expand Up @@ -4843,6 +4847,27 @@ mod tests {
}
}

#[test]
fn connect_forwards_a_firefox_bidi_endpoint_to_the_cli_parser() {
for target in [
"9333",
"ws://127.0.0.1:9333/session",
"ws://127.0.0.1:9333/session?devtools=6080",
"http://127.0.0.1:9333?devtools=6080",
] {
let args = connect_args(&json!({ "target": target })).unwrap();
assert_eq!(args, vec!["connect", target]);
let flags = crate::flags::parse_flags(&args);
let command = crate::commands::parse_command(&args, &flags).unwrap();
assert_eq!(command["action"], "launch");
if target == "9333" {
assert_eq!(command["cdpPort"], 9333);
} else {
assert_eq!(command["cdpUrl"], target);
}
}
}

#[test]
fn conditional_screenshot_scope_matches_cli_parser() {
for scope in [
Expand Down
Loading
Loading