From 8de8943dd7999110c0f5d79f2a5789e77427fbd3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 12 Jan 2026 11:00:08 -0600 Subject: [PATCH] add --executable-path --- README.md | 34 ++++++++++++++++++++++++++++++++++ cli/src/commands.rs | 1 + cli/src/connection.rs | 10 +++++++++- cli/src/flags.rs | 12 +++++++++++- cli/src/main.rs | 2 +- cli/src/output.rs | 1 + src/browser.ts | 1 + src/daemon.ts | 7 ++++++- src/types.ts | 1 + 9 files changed, 65 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4e2702c..47963d2 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,7 @@ agent-browser snapshot -i -c -d 5 # Combine options | Option | Description | |--------|-------------| | `--session ` | Use isolated session (or `AGENT_BROWSER_SESSION` env) | +| `--executable-path ` | Custom browser executable (or `AGENT_BROWSER_EXECUTABLE_PATH` env) | | `--json` | JSON output (for agents) | | `--full, -f` | Full page screenshot | | `--name, -n` | Locator name filter | @@ -387,6 +388,39 @@ agent-browser open example.com --headed This opens a visible browser window instead of running headless. +## Custom Browser Executable + +Use a custom browser executable instead of the bundled Chromium. This is useful for: +- **Serverless deployment**: Use lightweight Chromium builds like `@sparticuz/chromium` (~50MB vs ~684MB) +- **System browsers**: Use an existing Chrome/Chromium installation +- **Custom builds**: Use modified browser builds + +### CLI Usage + +```bash +# Via flag +agent-browser --executable-path /path/to/chromium open example.com + +# Via environment variable +AGENT_BROWSER_EXECUTABLE_PATH=/path/to/chromium agent-browser open example.com +``` + +### Serverless Example (Vercel/AWS Lambda) + +```typescript +import chromium from '@sparticuz/chromium'; +import { BrowserManager } from 'agent-browser'; + +export async function handler() { + const browser = new BrowserManager(); + await browser.launch({ + executablePath: await chromium.executablePath(), + headless: true, + }); + // ... use browser +} +``` + ## Architecture agent-browser uses a client-daemon architecture: diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 405f753..3ff4125 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -886,6 +886,7 @@ mod tests { full: false, headed: false, debug: false, + executable_path: None, } } diff --git a/cli/src/connection.rs b/cli/src/connection.rs index 2cfa83c..9d0ef40 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -153,7 +153,7 @@ fn daemon_ready(session: &str) -> bool { } } -pub fn ensure_daemon(session: &str, headed: bool) -> Result<(), String> { +pub fn ensure_daemon(session: &str, headed: bool, executable_path: Option<&str>) -> Result<(), String> { if is_daemon_running(session) && daemon_ready(session) { return Ok(()); } @@ -186,6 +186,10 @@ pub fn ensure_daemon(session: &str, headed: bool) -> Result<(), String> { cmd.env("AGENT_BROWSER_HEADED", "1"); } + if let Some(path) = executable_path { + cmd.env("AGENT_BROWSER_EXECUTABLE_PATH", path); + } + // Create new process group and session to fully detach unsafe { cmd.pre_exec(|| { @@ -220,6 +224,10 @@ pub fn ensure_daemon(session: &str, headed: bool) -> Result<(), String> { cmd.env("AGENT_BROWSER_HEADED", "1"); } + if let Some(path) = executable_path { + cmd.env("AGENT_BROWSER_EXECUTABLE_PATH", path); + } + // CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; const DETACHED_PROCESS: u32 = 0x00000008; diff --git a/cli/src/flags.rs b/cli/src/flags.rs index fc2749b..bd1da0d 100644 --- a/cli/src/flags.rs +++ b/cli/src/flags.rs @@ -6,6 +6,7 @@ pub struct Flags { pub headed: bool, pub debug: bool, pub session: String, + pub executable_path: Option, } pub fn parse_flags(args: &[String]) -> Flags { @@ -15,6 +16,7 @@ pub fn parse_flags(args: &[String]) -> Flags { headed: false, debug: false, session: env::var("AGENT_BROWSER_SESSION").unwrap_or_else(|_| "default".to_string()), + executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok(), }; let mut i = 0; @@ -30,6 +32,12 @@ pub fn parse_flags(args: &[String]) -> Flags { i += 1; } } + "--executable-path" => { + if let Some(s) = args.get(i + 1) { + flags.executable_path = Some(s.clone()); + i += 1; + } + } _ => {} } i += 1; @@ -43,13 +51,15 @@ pub fn clean_args(args: &[String]) -> Vec { // Global flags that should be stripped from command args const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug"]; + // Flags that take a value (skip both the flag and the next arg) + const VALUE_FLAGS: &[&str] = &["--session", "--executable-path"]; for arg in args.iter() { if skip_next { skip_next = false; continue; } - if arg == "--session" { + if VALUE_FLAGS.contains(&arg.as_str()) { skip_next = true; continue; } diff --git a/cli/src/main.rs b/cli/src/main.rs index 2ea8044..06dc06f 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -149,7 +149,7 @@ fn main() { } }; - if let Err(e) = ensure_daemon(&flags.session, flags.headed) { + if let Err(e) = ensure_daemon(&flags.session, flags.headed, flags.executable_path.as_deref()) { if flags.json { println!(r#"{{"success":false,"error":"{}"}}"#, e); } else { diff --git a/cli/src/output.rs b/cli/src/output.rs index 4f4dadc..68e7fbe 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -1186,6 +1186,7 @@ Snapshot Options: Options: --session Isolated session (or AGENT_BROWSER_SESSION env) + --executable-path Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH) --json JSON output --full, -f Full page screenshot --headed Show browser window (not headless) diff --git a/src/browser.ts b/src/browser.ts index f51e2bc..8409112 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -520,6 +520,7 @@ export class BrowserManager { // Launch browser this.browser = await launcher.launch({ headless: options.headless ?? true, + executablePath: options.executablePath, }); // Create context with viewport diff --git a/src/daemon.ts b/src/daemon.ts index c7e52be..61ecc07 100644 --- a/src/daemon.ts +++ b/src/daemon.ts @@ -158,7 +158,12 @@ export async function startDaemon(): Promise { parseResult.command.action !== 'launch' && parseResult.command.action !== 'close' ) { - await browser.launch({ id: 'auto', action: 'launch', headless: true }); + await browser.launch({ + id: 'auto', + action: 'launch', + headless: true, + executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH, + }); } // Handle close command specially diff --git a/src/types.ts b/src/types.ts index 64f1a66..e9caac1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,6 +12,7 @@ export interface LaunchCommand extends BaseCommand { headless?: boolean; viewport?: { width: number; height: number }; browser?: 'chromium' | 'firefox' | 'webkit'; + executablePath?: string; } export interface NavigateCommand extends BaseCommand {