native (#594)
* 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:
+95
-49
@@ -41,6 +41,7 @@ pub struct Config {
|
||||
pub action_policy: Option<String>,
|
||||
pub confirm_actions: Option<String>,
|
||||
pub confirm_interactive: Option<bool>,
|
||||
pub native: Option<bool>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -82,6 +83,7 @@ impl Config {
|
||||
action_policy: other.action_policy.or(self.action_policy),
|
||||
confirm_actions: other.confirm_actions.or(self.confirm_actions),
|
||||
confirm_interactive: other.confirm_interactive.or(self.confirm_interactive),
|
||||
native: other.native.or(self.native),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,8 +182,7 @@ pub fn load_config(args: &[String]) -> Result<Config, String> {
|
||||
});
|
||||
|
||||
if let Some((source, maybe_path)) = explicit {
|
||||
let path_str =
|
||||
maybe_path.ok_or_else(|| format!("{} requires a file path", source))?;
|
||||
let path_str = maybe_path.ok_or_else(|| format!("{} requires a file path", source))?;
|
||||
let path = PathBuf::from(&path_str);
|
||||
if !path.exists() {
|
||||
return Err(format!("config file not found: {}", path_str));
|
||||
@@ -234,6 +235,7 @@ pub struct Flags {
|
||||
pub action_policy: Option<String>,
|
||||
pub confirm_actions: Option<String>,
|
||||
pub confirm_interactive: bool,
|
||||
pub native: bool,
|
||||
|
||||
// Track which launch-time options were explicitly passed via CLI
|
||||
// (as opposed to being set only via environment variables)
|
||||
@@ -273,66 +275,72 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
};
|
||||
|
||||
let mut flags = Flags {
|
||||
json: env_var_is_truthy("AGENT_BROWSER_JSON")
|
||||
|| config.json.unwrap_or(false),
|
||||
full: env_var_is_truthy("AGENT_BROWSER_FULL")
|
||||
|| config.full.unwrap_or(false),
|
||||
headed: env_var_is_truthy("AGENT_BROWSER_HEADED")
|
||||
|| config.headed.unwrap_or(false),
|
||||
debug: env_var_is_truthy("AGENT_BROWSER_DEBUG")
|
||||
|| config.debug.unwrap_or(false),
|
||||
session: env::var("AGENT_BROWSER_SESSION").ok()
|
||||
json: env_var_is_truthy("AGENT_BROWSER_JSON") || config.json.unwrap_or(false),
|
||||
full: env_var_is_truthy("AGENT_BROWSER_FULL") || config.full.unwrap_or(false),
|
||||
headed: env_var_is_truthy("AGENT_BROWSER_HEADED") || config.headed.unwrap_or(false),
|
||||
debug: env_var_is_truthy("AGENT_BROWSER_DEBUG") || config.debug.unwrap_or(false),
|
||||
session: env::var("AGENT_BROWSER_SESSION")
|
||||
.ok()
|
||||
.or(config.session)
|
||||
.unwrap_or_else(|| "default".to_string()),
|
||||
headers: config.headers,
|
||||
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok()
|
||||
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH")
|
||||
.ok()
|
||||
.or(config.executable_path),
|
||||
cdp: config.cdp,
|
||||
extensions,
|
||||
profile: env::var("AGENT_BROWSER_PROFILE").ok()
|
||||
.or(config.profile),
|
||||
state: env::var("AGENT_BROWSER_STATE").ok()
|
||||
.or(config.state),
|
||||
proxy: env::var("AGENT_BROWSER_PROXY").ok()
|
||||
.or(config.proxy),
|
||||
proxy_bypass: env::var("AGENT_BROWSER_PROXY_BYPASS").ok()
|
||||
profile: env::var("AGENT_BROWSER_PROFILE").ok().or(config.profile),
|
||||
state: env::var("AGENT_BROWSER_STATE").ok().or(config.state),
|
||||
proxy: env::var("AGENT_BROWSER_PROXY").ok().or(config.proxy),
|
||||
proxy_bypass: env::var("AGENT_BROWSER_PROXY_BYPASS")
|
||||
.ok()
|
||||
.or(config.proxy_bypass),
|
||||
args: env::var("AGENT_BROWSER_ARGS").ok()
|
||||
.or(config.args),
|
||||
user_agent: env::var("AGENT_BROWSER_USER_AGENT").ok()
|
||||
args: env::var("AGENT_BROWSER_ARGS").ok().or(config.args),
|
||||
user_agent: env::var("AGENT_BROWSER_USER_AGENT")
|
||||
.ok()
|
||||
.or(config.user_agent),
|
||||
provider: env::var("AGENT_BROWSER_PROVIDER").ok()
|
||||
.or(config.provider),
|
||||
provider: env::var("AGENT_BROWSER_PROVIDER").ok().or(config.provider),
|
||||
ignore_https_errors: env_var_is_truthy("AGENT_BROWSER_IGNORE_HTTPS_ERRORS")
|
||||
|| config.ignore_https_errors.unwrap_or(false),
|
||||
allow_file_access: env_var_is_truthy("AGENT_BROWSER_ALLOW_FILE_ACCESS")
|
||||
|| config.allow_file_access.unwrap_or(false),
|
||||
device: env::var("AGENT_BROWSER_IOS_DEVICE").ok()
|
||||
.or(config.device),
|
||||
device: env::var("AGENT_BROWSER_IOS_DEVICE").ok().or(config.device),
|
||||
auto_connect: env_var_is_truthy("AGENT_BROWSER_AUTO_CONNECT")
|
||||
|| config.auto_connect.unwrap_or(false),
|
||||
session_name: env::var("AGENT_BROWSER_SESSION_NAME").ok()
|
||||
session_name: env::var("AGENT_BROWSER_SESSION_NAME")
|
||||
.ok()
|
||||
.or(config.session_name),
|
||||
annotate: env_var_is_truthy("AGENT_BROWSER_ANNOTATE")
|
||||
|| config.annotate.unwrap_or(false),
|
||||
color_scheme: env::var("AGENT_BROWSER_COLOR_SCHEME").ok()
|
||||
annotate: env_var_is_truthy("AGENT_BROWSER_ANNOTATE") || config.annotate.unwrap_or(false),
|
||||
color_scheme: env::var("AGENT_BROWSER_COLOR_SCHEME")
|
||||
.ok()
|
||||
.or(config.color_scheme),
|
||||
download_path: env::var("AGENT_BROWSER_DOWNLOAD_PATH").ok()
|
||||
download_path: env::var("AGENT_BROWSER_DOWNLOAD_PATH")
|
||||
.ok()
|
||||
.or(config.download_path),
|
||||
content_boundaries: env_var_is_truthy("AGENT_BROWSER_CONTENT_BOUNDARIES")
|
||||
|| config.content_boundaries.unwrap_or(false),
|
||||
max_output: env::var("AGENT_BROWSER_MAX_OUTPUT").ok()
|
||||
max_output: env::var("AGENT_BROWSER_MAX_OUTPUT")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.or(config.max_output),
|
||||
allowed_domains: env::var("AGENT_BROWSER_ALLOWED_DOMAINS").ok()
|
||||
.map(|s| s.split(',').map(|d| d.trim().to_lowercase()).filter(|d| !d.is_empty()).collect())
|
||||
allowed_domains: env::var("AGENT_BROWSER_ALLOWED_DOMAINS")
|
||||
.ok()
|
||||
.map(|s| {
|
||||
s.split(',')
|
||||
.map(|d| d.trim().to_lowercase())
|
||||
.filter(|d| !d.is_empty())
|
||||
.collect()
|
||||
})
|
||||
.or(config.allowed_domains),
|
||||
action_policy: env::var("AGENT_BROWSER_ACTION_POLICY").ok()
|
||||
action_policy: env::var("AGENT_BROWSER_ACTION_POLICY")
|
||||
.ok()
|
||||
.or(config.action_policy),
|
||||
confirm_actions: env::var("AGENT_BROWSER_CONFIRM_ACTIONS").ok()
|
||||
confirm_actions: env::var("AGENT_BROWSER_CONFIRM_ACTIONS")
|
||||
.ok()
|
||||
.or(config.confirm_actions),
|
||||
confirm_interactive: env_var_is_truthy("AGENT_BROWSER_CONFIRM_INTERACTIVE")
|
||||
|| config.confirm_interactive.unwrap_or(false),
|
||||
native: env_var_is_truthy("AGENT_BROWSER_NATIVE") || config.native.unwrap_or(false),
|
||||
cli_executable_path: false,
|
||||
cli_extensions: false,
|
||||
cli_profile: false,
|
||||
@@ -352,22 +360,30 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
"--json" => {
|
||||
let (val, consumed) = parse_bool_arg(args, i);
|
||||
flags.json = val;
|
||||
if consumed { i += 1; }
|
||||
if consumed {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--full" | "-f" => {
|
||||
let (val, consumed) = parse_bool_arg(args, i);
|
||||
flags.full = val;
|
||||
if consumed { i += 1; }
|
||||
if consumed {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--headed" => {
|
||||
let (val, consumed) = parse_bool_arg(args, i);
|
||||
flags.headed = val;
|
||||
if consumed { i += 1; }
|
||||
if consumed {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--debug" => {
|
||||
let (val, consumed) = parse_bool_arg(args, i);
|
||||
flags.debug = val;
|
||||
if consumed { i += 1; }
|
||||
if consumed {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--session" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
@@ -452,13 +468,17 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
"--ignore-https-errors" => {
|
||||
let (val, consumed) = parse_bool_arg(args, i);
|
||||
flags.ignore_https_errors = val;
|
||||
if consumed { i += 1; }
|
||||
if consumed {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--allow-file-access" => {
|
||||
let (val, consumed) = parse_bool_arg(args, i);
|
||||
flags.allow_file_access = val;
|
||||
flags.cli_allow_file_access = true;
|
||||
if consumed { i += 1; }
|
||||
if consumed {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--device" => {
|
||||
if let Some(d) = args.get(i + 1) {
|
||||
@@ -469,7 +489,9 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
"--auto-connect" => {
|
||||
let (val, consumed) = parse_bool_arg(args, i);
|
||||
flags.auto_connect = val;
|
||||
if consumed { i += 1; }
|
||||
if consumed {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--session-name" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
@@ -481,7 +503,9 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
let (val, consumed) = parse_bool_arg(args, i);
|
||||
flags.annotate = val;
|
||||
flags.cli_annotate = true;
|
||||
if consumed { i += 1; }
|
||||
if consumed {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--color-scheme" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
@@ -499,7 +523,9 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
"--content-boundaries" => {
|
||||
let (val, consumed) = parse_bool_arg(args, i);
|
||||
flags.content_boundaries = val;
|
||||
if consumed { i += 1; }
|
||||
if consumed {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--max-output" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
@@ -512,7 +538,10 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
"--allowed-domains" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
flags.allowed_domains = Some(
|
||||
s.split(',').map(|d| d.trim().to_lowercase()).filter(|d| !d.is_empty()).collect()
|
||||
s.split(',')
|
||||
.map(|d| d.trim().to_lowercase())
|
||||
.filter(|d| !d.is_empty())
|
||||
.collect(),
|
||||
);
|
||||
i += 1;
|
||||
}
|
||||
@@ -532,7 +561,16 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
"--confirm-interactive" => {
|
||||
let (val, consumed) = parse_bool_arg(args, i);
|
||||
flags.confirm_interactive = val;
|
||||
if consumed { i += 1; }
|
||||
if consumed {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--native" => {
|
||||
let (val, consumed) = parse_bool_arg(args, i);
|
||||
flags.native = val;
|
||||
if consumed {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--config" => {
|
||||
// Already handled by load_config(); skip the value
|
||||
@@ -561,6 +599,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
||||
"--annotate",
|
||||
"--content-boundaries",
|
||||
"--confirm-interactive",
|
||||
"--native",
|
||||
];
|
||||
// Global flags that always take a value (need to skip the next arg too)
|
||||
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[
|
||||
@@ -835,7 +874,10 @@ mod tests {
|
||||
assert_eq!(config.session.as_deref(), Some("test-session"));
|
||||
assert_eq!(config.session_name.as_deref(), Some("my-app"));
|
||||
assert_eq!(config.executable_path.as_deref(), Some("/usr/bin/chromium"));
|
||||
assert_eq!(config.extensions, Some(vec!["/ext1".to_string(), "/ext2".to_string()]));
|
||||
assert_eq!(
|
||||
config.extensions,
|
||||
Some(vec!["/ext1".to_string(), "/ext2".to_string()])
|
||||
);
|
||||
assert_eq!(config.profile.as_deref(), Some("/tmp/profile"));
|
||||
assert_eq!(config.state.as_deref(), Some("/tmp/state.json"));
|
||||
assert_eq!(config.proxy.as_deref(), Some("http://proxy:8080"));
|
||||
@@ -1144,7 +1186,11 @@ mod tests {
|
||||
let merged = user.merge(project);
|
||||
assert_eq!(
|
||||
merged.extensions,
|
||||
Some(vec!["/ext1".to_string(), "/ext2".to_string(), "/ext3".to_string()])
|
||||
Some(vec![
|
||||
"/ext1".to_string(),
|
||||
"/ext2".to_string(),
|
||||
"/ext3".to_string()
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user