feat: add Browser Use cloud browser as available provider (#138)

* feat: add Browser Use cloud browser
  integration

* feat: enhance Browser Use integration with provider flag support

- Updated README to reflect new usage instructions for enabling Browser Use with the `-p` flag.
- Modified CLI to parse and handle the `-p` flag for specifying the provider.
- Implemented logic in the main application to launch with the specified cloud provider.
- Adjusted BrowserManager to connect to Browser Use based on the provider flag or environment variable.
- Updated types and protocol schemas to include provider information.

* feat: add validation for mutually exclusive CLI options

- Implemented checks to prevent the use of both --cdp and --provider flags simultaneously.
- Added validation to ensure --extension cannot be used with the --provider flag.
- Enhanced error handling to provide clear feedback in both JSON and console output formats.
This commit is contained in:
Aitor
2026-01-21 18:01:19 -06:00
committed by GitHub
parent 7123d46e7f
commit c4139fa389
6 changed files with 194 additions and 7 deletions
+23
View File
@@ -674,6 +674,29 @@ When both variables are set, agent-browser automatically connects to a Browserba
Get your API key and project ID from the [Browserbase Dashboard](https://browserbase.com/overview). Get your API key and project ID from the [Browserbase Dashboard](https://browserbase.com/overview).
### Browser Use
[Browser Use](https://browser-use.com) provides cloud browser infrastructure for AI agents. Use it when running agent-browser in environments where a local browser isn't available (serverless, CI/CD, etc.).
To enable Browser Use, use the `-p` flag:
```bash
export BROWSER_USE_API_KEY="your-api-key"
agent-browser -p browseruse open https://example.com
```
Or use environment variables for CI/scripts:
```bash
export AGENT_BROWSER_PROVIDER=browseruse
export BROWSER_USE_API_KEY="your-api-key"
agent-browser open https://example.com
```
When enabled, agent-browser connects to a Browser Use cloud session instead of launching a local browser. All commands work identically.
Get your API key from the [Browser Use Cloud Dashboard](https://cloud.browser-use.com/settings?tab=api-keys). Free credits are available to get started, with pay-as-you-go pricing after.
## License ## License
Apache-2.0 Apache-2.0
+9 -1
View File
@@ -11,6 +11,7 @@ pub struct Flags {
pub cdp: Option<String>, pub cdp: Option<String>,
pub extensions: Vec<String>, pub extensions: Vec<String>,
pub proxy: Option<String>, pub proxy: Option<String>,
pub provider: Option<String>,
} }
pub fn parse_flags(args: &[String]) -> Flags { pub fn parse_flags(args: &[String]) -> Flags {
@@ -30,6 +31,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
cdp: None, cdp: None,
extensions: extensions_env, extensions: extensions_env,
proxy: None, proxy: None,
provider: env::var("AGENT_BROWSER_PROVIDER").ok(),
}; };
let mut i = 0; let mut i = 0;
@@ -75,6 +77,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1; i += 1;
} }
} }
"-p" | "--provider" => {
if let Some(p) = args.get(i + 1) {
flags.provider = Some(p.clone());
i += 1;
}
}
_ => {} _ => {}
} }
i += 1; i += 1;
@@ -89,7 +97,7 @@ 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"];
// Global flags that take a value (need to skip the next arg too) // Global flags that take a value (need to skip the next arg too)
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &["--session", "--headers", "--executable-path", "--cdp", "--extension", "--proxy"]; const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &["--session", "--headers", "--executable-path", "--cdp", "--extension", "--proxy", "-p", "--provider"];
for arg in args.iter() { for arg in args.iter() {
if skip_next { if skip_next {
+47 -2
View File
@@ -216,6 +216,27 @@ fn main() {
} }
} }
// Validate mutually exclusive options
if flags.cdp.is_some() && flags.provider.is_some() {
let msg = "Cannot use --cdp and -p/--provider together";
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
} else {
eprintln!("\x1b[31m✗\x1b[0m {}", msg);
}
exit(1);
}
if flags.provider.is_some() && !flags.extensions.is_empty() {
let msg = "Cannot use --extension with -p/--provider (extensions require local browser)";
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
} else {
eprintln!("\x1b[31m✗\x1b[0m {}", msg);
}
exit(1);
}
// Connect via CDP if --cdp flag is set // Connect via CDP if --cdp flag is set
if let Some(ref port) = flags.cdp { if let Some(ref port) = flags.cdp {
let cdp_port: u16 = match port.parse::<u32>() { let cdp_port: u16 = match port.parse::<u32>() {
@@ -271,8 +292,32 @@ fn main() {
} }
} }
// Launch headed browser or proxy if flags are set (without CDP) // Launch with cloud provider if -p flag is set
if (flags.headed || flags.proxy.is_some()) && flags.cdp.is_none() { if let Some(ref provider) = flags.provider {
let launch_cmd = json!({
"id": gen_id(),
"action": "launch",
"provider": provider
});
let err = match send_command(launch_cmd, &flags.session) {
Ok(resp) if resp.success => None,
Ok(resp) => Some(resp.error.unwrap_or_else(|| "Provider connection failed".to_string())),
Err(e) => Some(e.to_string()),
};
if let Some(msg) = err {
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
} else {
eprintln!("\x1b[31m✗\x1b[0m {}", msg);
}
exit(1);
}
}
// Launch headed browser or proxy if flags are set (without CDP or provider)
if (flags.headed || flags.proxy.is_some()) && flags.cdp.is_none() && flags.provider.is_none() {
let mut launch_cmd = json!({ let mut launch_cmd = json!({
"id": gen_id(), "id": gen_id(),
"action": "launch", "action": "launch",
+112 -3
View File
@@ -72,6 +72,8 @@ export class BrowserManager {
private isPersistentContext: boolean = false; private isPersistentContext: boolean = false;
private browserbaseSessionId: string | null = null; private browserbaseSessionId: string | null = null;
private browserbaseApiKey: string | null = null; private browserbaseApiKey: string | null = null;
private browserUseSessionId: string | null = null;
private browserUseApiKey: string | null = null;
private contexts: BrowserContext[] = []; private contexts: BrowserContext[] = [];
private pages: Page[] = []; private pages: Page[] = [];
private activePageIndex: number = 0; private activePageIndex: number = 0;
@@ -656,6 +658,24 @@ export class BrowserManager {
}); });
} }
/**
* Close a Browser Use session via API
*/
private async closeBrowserUseSession(sessionId: string, apiKey: string): Promise<void> {
const response = await fetch(`https://api.browser-use.com/api/v2/browsers/${sessionId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-Browser-Use-API-Key': apiKey,
},
body: JSON.stringify({ action: 'stop' }),
});
if (!response.ok) {
throw new Error(`Failed to close Browser Use session: ${response.statusText}`);
}
}
/** /**
* Connect to Browserbase remote browser via CDP. * Connect to Browserbase remote browser via CDP.
* Returns true if connected, false if credentials not available. * Returns true if connected, false if credentials not available.
@@ -683,7 +703,7 @@ export class BrowserManager {
throw new Error(`Failed to create Browserbase session: ${response.statusText}`); throw new Error(`Failed to create Browserbase session: ${response.statusText}`);
} }
const session = await response.json() as { id: string; connectUrl: string }; const session = (await response.json()) as { id: string; connectUrl: string };
const browser = await chromium.connectOverCDP(session.connectUrl).catch(() => { const browser = await chromium.connectOverCDP(session.connectUrl).catch(() => {
throw new Error('Failed to connect to Browserbase session via CDP'); throw new Error('Failed to connect to Browserbase session via CDP');
@@ -717,6 +737,79 @@ export class BrowserManager {
} }
} }
/**
* Connect to Browser Use remote browser via CDP.
* Requires BROWSER_USE_API_KEY environment variable.
*/
private async connectToBrowserUse(): Promise<void> {
const browserUseApiKey = process.env.BROWSER_USE_API_KEY;
if (!browserUseApiKey) {
throw new Error('BROWSER_USE_API_KEY is required when using browseruse as a provider');
}
const response = await fetch('https://api.browser-use.com/api/v2/browsers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Browser-Use-API-Key': browserUseApiKey,
},
body: JSON.stringify({}),
});
if (!response.ok) {
throw new Error(`Failed to create Browser Use session: ${response.statusText}`);
}
let session: { id: string; cdpUrl: string };
try {
session = (await response.json()) as { id: string; cdpUrl: string };
} catch (error) {
throw new Error(
`Failed to parse Browser Use session response: ${error instanceof Error ? error.message : String(error)}`
);
}
if (!session.id || !session.cdpUrl) {
throw new Error(
`Invalid Browser Use session response: missing ${!session.id ? 'id' : 'cdpUrl'}`
);
}
const browser = await chromium.connectOverCDP(session.cdpUrl).catch(() => {
throw new Error('Failed to connect to Browser Use session via CDP');
});
try {
const contexts = browser.contexts();
let context: BrowserContext;
let page: Page;
if (contexts.length === 0) {
context = await browser.newContext();
page = await context.newPage();
} else {
context = contexts[0];
const pages = context.pages();
page = pages[0] ?? (await context.newPage());
}
this.browserUseSessionId = session.id;
this.browserUseApiKey = browserUseApiKey;
this.browser = browser;
context.setDefaultTimeout(60000);
this.contexts.push(context);
this.pages.push(page);
this.activePageIndex = 0;
this.setupPageTracking(page);
this.setupContextTracking(context);
} catch (error) {
await this.closeBrowserUseSession(session.id, browserUseApiKey).catch((sessionError) => {
console.error('Failed to close Browser Use session during cleanup:', sessionError);
});
throw error;
}
}
/** /**
* Launch the browser with the specified options * Launch the browser with the specified options
* If already launched, this is a no-op (browser stays open) * If already launched, this is a no-op (browser stays open)
@@ -744,12 +837,19 @@ export class BrowserManager {
return; return;
} }
// Try connecting to Browserbase if credentials are available // Try connecting to cloud browser providers if configured
// Browserbase: auto-connects when BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID are set
if (await this.connectToBrowserbase()) { if (await this.connectToBrowserbase()) {
return; return;
} }
// Select browser type // Browser Use: requires explicit opt-in via -p browseruse flag or AGENT_BROWSER_PROVIDER=browseruse
const provider = options.provider ?? process.env.AGENT_BROWSER_PROVIDER;
if (provider === 'browseruse') {
await this.connectToBrowserUse();
return;
}
const browserType = options.browser ?? 'chromium'; const browserType = options.browser ?? 'chromium';
if (hasExtensions && browserType !== 'chromium') { if (hasExtensions && browserType !== 'chromium') {
throw new Error('Extensions are only supported in Chromium'); throw new Error('Extensions are only supported in Chromium');
@@ -1447,6 +1547,13 @@ export class BrowserManager {
} }
); );
this.browser = null; this.browser = null;
} else if (this.browserUseSessionId && this.browserUseApiKey) {
await this.closeBrowserUseSession(this.browserUseSessionId, this.browserUseApiKey).catch(
(error) => {
console.error('Failed to close Browser Use session:', error);
}
);
this.browser = null;
} else if (this.cdpPort !== null) { } else if (this.cdpPort !== null) {
// CDP: only disconnect, don't close external app's pages // CDP: only disconnect, don't close external app's pages
if (this.browser) { if (this.browser) {
@@ -1472,6 +1579,8 @@ export class BrowserManager {
this.cdpPort = null; this.cdpPort = null;
this.browserbaseSessionId = null; this.browserbaseSessionId = null;
this.browserbaseApiKey = null; this.browserbaseApiKey = null;
this.browserUseSessionId = null;
this.browserUseApiKey = null;
this.isPersistentContext = false; this.isPersistentContext = false;
this.activePageIndex = 0; this.activePageIndex = 0;
this.refMap = {}; this.refMap = {};
+1
View File
@@ -30,6 +30,7 @@ const launchSchema = baseCommandSchema.extend({
password: z.string().optional(), password: z.string().optional(),
}) })
.optional(), .optional(),
provider: z.string().optional(),
}); });
const navigateSchema = baseCommandSchema.extend({ const navigateSchema = baseCommandSchema.extend({
+1
View File
@@ -22,6 +22,7 @@ export interface LaunchCommand extends BaseCommand {
username?: string; username?: string;
password?: string; password?: string;
}; };
provider?: string;
} }
export interface NavigateCommand extends BaseCommand { export interface NavigateCommand extends BaseCommand {