* Native Rust rewrite of agent-browser daemon

Single-binary Rust implementation replacing the Node.js/Playwright daemon
with direct CDP (Chrome DevTools Protocol) communication. Includes full
command parity, WebDriver/Safari/iOS backend routing, request tracking,
frame context management, CDP protocol codegen, and comprehensive tests.

* improvements

* fix ci

* fixes

* faster builds
This commit is contained in:
Chris Tate
2026-03-03 15:15:57 -06:00
committed by GitHub
parent 857c0b25df
commit 51f5fa484c
52 changed files with 55160 additions and 278 deletions
+80 -53
View File
@@ -114,10 +114,12 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
let mut nav_cmd = json!({ "id": id, "action": "navigate", "url": url });
// If --headers flag is set, include headers (scoped to this origin)
if let Some(ref headers_json) = flags.headers {
let headers = serde_json::from_str::<serde_json::Value>(headers_json)
.map_err(|_| ParseError::InvalidValue {
message: format!("Invalid JSON for --headers: {}", headers_json),
usage: "open <url> --headers '{\"Key\": \"Value\"}'",
let headers =
serde_json::from_str::<serde_json::Value>(headers_json).map_err(|_| {
ParseError::InvalidValue {
message: format!("Invalid JSON for --headers: {}", headers_json),
usage: "open <url> --headers '{\"Key\": \"Value\"}'",
}
})?;
nav_cmd["headers"] = headers;
}
@@ -290,7 +292,9 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
usage: "keyboard inserttext <text>",
});
}
Ok(json!({ "id": id, "action": "keyboard", "subaction": "insertText", "text": text }))
Ok(
json!({ "id": id, "action": "keyboard", "subaction": "insertText", "text": text }),
)
}
_ => Err(ParseError::UnknownSubcommand {
subcommand: sub.to_string(),
@@ -585,13 +589,33 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
let mut j = 2;
while j < rest.len() {
match rest[j].as_ref() {
"--url" => { url = rest.get(j + 1).cloned(); j += 1; }
"--username" => { username = rest.get(j + 1).cloned(); j += 1; }
"--password" => { password = rest.get(j + 1).cloned(); j += 1; }
"--password-stdin" => { password_stdin = true; }
"--username-selector" => { username_selector = rest.get(j + 1).cloned(); j += 1; }
"--password-selector" => { password_selector = rest.get(j + 1).cloned(); j += 1; }
"--submit-selector" => { submit_selector = rest.get(j + 1).cloned(); j += 1; }
"--url" => {
url = rest.get(j + 1).cloned();
j += 1;
}
"--username" => {
username = rest.get(j + 1).cloned();
j += 1;
}
"--password" => {
password = rest.get(j + 1).cloned();
j += 1;
}
"--password-stdin" => {
password_stdin = true;
}
"--username-selector" => {
username_selector = rest.get(j + 1).cloned();
j += 1;
}
"--password-selector" => {
password_selector = rest.get(j + 1).cloned();
j += 1;
}
"--submit-selector" => {
submit_selector = rest.get(j + 1).cloned();
j += 1;
}
other => {
if other.starts_with("--") {
return Err(ParseError::InvalidValue {
@@ -1093,9 +1117,7 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
})?;
Ok(json!({ "id": id, "action": "state_load", "path": path }))
}
Some("list") => {
Ok(json!({ "id": id, "action": "state_list" }))
}
Some("list") => Ok(json!({ "id": id, "action": "state_list" })),
Some("clear") => {
let mut session_name: Option<&str> = None;
let mut all = false;
@@ -1116,7 +1138,9 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
if let Some(name) = session_name {
if !is_valid_session_name(name) {
return Err(ParseError::InvalidSessionName { name: name.to_string() });
return Err(ParseError::InvalidSessionName {
name: name.to_string(),
});
}
}
@@ -1170,13 +1194,19 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
let new_name = new_name.trim_end_matches(".json");
if !is_valid_session_name(old_name) {
return Err(ParseError::InvalidSessionName { name: old_name.to_string() });
return Err(ParseError::InvalidSessionName {
name: old_name.to_string(),
});
}
if !is_valid_session_name(new_name) {
return Err(ParseError::InvalidSessionName { name: new_name.to_string() });
return Err(ParseError::InvalidSessionName {
name: new_name.to_string(),
});
}
Ok(json!({ "id": id, "action": "state_rename", "oldName": old_name, "newName": new_name }))
Ok(
json!({ "id": id, "action": "state_rename", "oldName": old_name, "newName": new_name }),
)
}
Some(sub) => Err(ParseError::UnknownSubcommand {
subcommand: sub.to_string(),
@@ -1285,7 +1315,10 @@ fn parse_diff(rest: &[&str], id: &str, flags: &Flags) -> Result<Value, ParseErro
}
Err(_) => {
return Err(ParseError::InvalidValue {
message: format!("Depth must be a non-negative integer, got: {}", d),
message: format!(
"Depth must be a non-negative integer, got: {}",
d
),
usage: "diff snapshot --depth <n>",
});
}
@@ -1351,7 +1384,10 @@ fn parse_diff(rest: &[&str], id: &str, flags: &Flags) -> Result<Value, ParseErro
}
Ok(n) => {
return Err(ParseError::InvalidValue {
message: format!("Threshold must be between 0 and 1, got {}", n),
message: format!(
"Threshold must be between 0 and 1, got {}",
n
),
usage: "diff screenshot --threshold <0-1>",
});
}
@@ -1468,7 +1504,10 @@ fn parse_diff(rest: &[&str], id: &str, flags: &Flags) -> Result<Value, ParseErro
}
Err(_) => {
return Err(ParseError::InvalidValue {
message: format!("Depth must be a non-negative integer, got: {}", d),
message: format!(
"Depth must be a non-negative integer, got: {}",
d
),
usage: "diff url <url1> <url2> --depth <n>",
});
}
@@ -2070,7 +2109,7 @@ mod tests {
action_policy: None,
confirm_actions: None,
confirm_interactive: false,
native: false,
}
}
@@ -2374,16 +2413,12 @@ mod tests {
)
.unwrap();
assert_eq!(cmd["action"], "navigate");
assert_eq!(
cmd["url"],
"chrome-extension://abcdefghijklmnop/popup.html"
);
assert_eq!(cmd["url"], "chrome-extension://abcdefghijklmnop/popup.html");
}
#[test]
fn test_navigate_chrome_url() {
let cmd =
parse_command(&args("open chrome://extensions"), &default_flags()).unwrap();
let cmd = parse_command(&args("open chrome://extensions"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "navigate");
assert_eq!(cmd["url"], "chrome://extensions");
}
@@ -3236,8 +3271,11 @@ mod tests {
#[test]
fn test_diff_snapshot_baseline() {
let cmd =
parse_command(&args("diff snapshot --baseline before.txt"), &default_flags()).unwrap();
let cmd = parse_command(
&args("diff snapshot --baseline before.txt"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "diff_snapshot");
assert_eq!(cmd["baseline"], "before.txt");
}
@@ -3257,9 +3295,11 @@ mod tests {
#[test]
fn test_diff_snapshot_short_flags() {
let cmd =
parse_command(&args("diff snapshot -b snap.txt -s .content -c -d 2"), &default_flags())
.unwrap();
let cmd = parse_command(
&args("diff snapshot -b snap.txt -s .content -c -d 2"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "diff_snapshot");
assert_eq!(cmd["baseline"], "snap.txt");
assert_eq!(cmd["selector"], ".content");
@@ -3307,8 +3347,7 @@ mod tests {
fn test_diff_screenshot_global_full_flag() {
let mut flags = default_flags();
flags.full = true;
let cmd =
parse_command(&args("diff screenshot --baseline b.png"), &flags).unwrap();
let cmd = parse_command(&args("diff screenshot --baseline b.png"), &flags).unwrap();
assert_eq!(cmd["action"], "diff_screenshot");
assert_eq!(cmd["fullPage"], true);
}
@@ -3352,8 +3391,7 @@ mod tests {
fn test_diff_url_global_full_flag() {
let mut flags = default_flags();
flags.full = true;
let cmd =
parse_command(&args("diff url https://a.com https://b.com"), &flags).unwrap();
let cmd = parse_command(&args("diff url https://a.com https://b.com"), &flags).unwrap();
assert_eq!(cmd["fullPage"], true);
}
@@ -3676,11 +3714,7 @@ mod tests {
#[test]
fn test_scroll_with_selector_short_flag() {
let cmd = parse_command(
&args("scroll left 100 -s .sidebar"),
&default_flags(),
)
.unwrap();
let cmd = parse_command(&args("scroll left 100 -s .sidebar"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "scroll");
assert_eq!(cmd["direction"], "left");
assert_eq!(cmd["amount"], 100);
@@ -3689,11 +3723,8 @@ mod tests {
#[test]
fn test_scroll_selector_before_positional() {
let cmd = parse_command(
&args("scroll --selector .panel down 400"),
&default_flags(),
)
.unwrap();
let cmd =
parse_command(&args("scroll --selector .panel down 400"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "scroll");
assert_eq!(cmd["direction"], "down");
assert_eq!(cmd["amount"], 400);
@@ -3702,11 +3733,7 @@ mod tests {
#[test]
fn test_scroll_selector_only() {
let cmd = parse_command(
&args("scroll --selector .content"),
&default_flags(),
)
.unwrap();
let cmd = parse_command(&args("scroll --selector .content"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "scroll");
assert_eq!(cmd["direction"], "down");
assert_eq!(cmd["amount"], 300);