add --executable-path

This commit is contained in:
Chris Tate
2026-01-12 11:00:08 -06:00
parent 3cd0ab468f
commit 8de8943dd7
9 changed files with 65 additions and 4 deletions
+34
View File
@@ -293,6 +293,7 @@ agent-browser snapshot -i -c -d 5 # Combine options
| Option | Description | | Option | Description |
|--------|-------------| |--------|-------------|
| `--session <name>` | Use isolated session (or `AGENT_BROWSER_SESSION` env) | | `--session <name>` | Use isolated session (or `AGENT_BROWSER_SESSION` env) |
| `--executable-path <path>` | Custom browser executable (or `AGENT_BROWSER_EXECUTABLE_PATH` env) |
| `--json` | JSON output (for agents) | | `--json` | JSON output (for agents) |
| `--full, -f` | Full page screenshot | | `--full, -f` | Full page screenshot |
| `--name, -n` | Locator name filter | | `--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. 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 ## Architecture
agent-browser uses a client-daemon architecture: agent-browser uses a client-daemon architecture:
+1
View File
@@ -886,6 +886,7 @@ mod tests {
full: false, full: false,
headed: false, headed: false,
debug: false, debug: false,
executable_path: None,
} }
} }
+9 -1
View File
@@ -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) { if is_daemon_running(session) && daemon_ready(session) {
return Ok(()); return Ok(());
} }
@@ -186,6 +186,10 @@ pub fn ensure_daemon(session: &str, headed: bool) -> Result<(), String> {
cmd.env("AGENT_BROWSER_HEADED", "1"); 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 // Create new process group and session to fully detach
unsafe { unsafe {
cmd.pre_exec(|| { cmd.pre_exec(|| {
@@ -220,6 +224,10 @@ pub fn ensure_daemon(session: &str, headed: bool) -> Result<(), String> {
cmd.env("AGENT_BROWSER_HEADED", "1"); 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 // CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
const DETACHED_PROCESS: u32 = 0x00000008; const DETACHED_PROCESS: u32 = 0x00000008;
+11 -1
View File
@@ -6,6 +6,7 @@ pub struct Flags {
pub headed: bool, pub headed: bool,
pub debug: bool, pub debug: bool,
pub session: String, pub session: String,
pub executable_path: Option<String>,
} }
pub fn parse_flags(args: &[String]) -> Flags { pub fn parse_flags(args: &[String]) -> Flags {
@@ -15,6 +16,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
headed: false, headed: false,
debug: false, debug: false,
session: env::var("AGENT_BROWSER_SESSION").unwrap_or_else(|_| "default".to_string()), 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; let mut i = 0;
@@ -30,6 +32,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1; i += 1;
} }
} }
"--executable-path" => {
if let Some(s) = args.get(i + 1) {
flags.executable_path = Some(s.clone());
i += 1;
}
}
_ => {} _ => {}
} }
i += 1; i += 1;
@@ -43,13 +51,15 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
// Global flags that should be stripped from command args // Global flags that should be stripped from command args
const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug"]; 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() { for arg in args.iter() {
if skip_next { if skip_next {
skip_next = false; skip_next = false;
continue; continue;
} }
if arg == "--session" { if VALUE_FLAGS.contains(&arg.as_str()) {
skip_next = true; skip_next = true;
continue; continue;
} }
+1 -1
View File
@@ -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 { if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, e); println!(r#"{{"success":false,"error":"{}"}}"#, e);
} else { } else {
+1
View File
@@ -1186,6 +1186,7 @@ Snapshot Options:
Options: Options:
--session <name> Isolated session (or AGENT_BROWSER_SESSION env) --session <name> Isolated session (or AGENT_BROWSER_SESSION env)
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
--json JSON output --json JSON output
--full, -f Full page screenshot --full, -f Full page screenshot
--headed Show browser window (not headless) --headed Show browser window (not headless)
+1
View File
@@ -520,6 +520,7 @@ export class BrowserManager {
// Launch browser // Launch browser
this.browser = await launcher.launch({ this.browser = await launcher.launch({
headless: options.headless ?? true, headless: options.headless ?? true,
executablePath: options.executablePath,
}); });
// Create context with viewport // Create context with viewport
+6 -1
View File
@@ -158,7 +158,12 @@ export async function startDaemon(): Promise<void> {
parseResult.command.action !== 'launch' && parseResult.command.action !== 'launch' &&
parseResult.command.action !== 'close' 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 // Handle close command specially
+1
View File
@@ -12,6 +12,7 @@ export interface LaunchCommand extends BaseCommand {
headless?: boolean; headless?: boolean;
viewport?: { width: number; height: number }; viewport?: { width: number; height: number };
browser?: 'chromium' | 'firefox' | 'webkit'; browser?: 'chromium' | 'firefox' | 'webkit';
executablePath?: string;
} }
export interface NavigateCommand extends BaseCommand { export interface NavigateCommand extends BaseCommand {