* 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
+274
View File
@@ -0,0 +1,274 @@
//! Browser provider connections for remote CDP sessions.
//!
//! Supports Browserbase, Browser Use, and Kernel providers. Each provider
//! returns a CDP WebSocket URL for connecting via BrowserManager.
use serde_json::{json, Value};
use std::env;
/// Provider session info for cleanup on failure.
pub struct ProviderSession {
pub provider: String,
pub session_id: String,
}
/// Connects to the specified browser provider and returns a CDP WebSocket URL
/// along with session info for cleanup on failure.
pub async fn connect_provider(
provider_name: &str,
) -> Result<(String, Option<ProviderSession>), String> {
match provider_name.to_lowercase().as_str() {
"browserbase" => connect_browserbase().await,
"browser-use" | "browseruse" => connect_browser_use().await,
"kernel" => connect_kernel().await,
_ => Err(format!(
"Unknown provider '{}'. Supported: browserbase, browser-use, kernel",
provider_name
)),
}
}
/// Close a provider session (call on CDP connect failure).
pub async fn close_provider_session(session: &ProviderSession) {
let client = reqwest::Client::new();
match session.provider.as_str() {
"browserbase" => {
if let Ok(api_key) = env::var("BROWSERBASE_API_KEY") {
let _ = client
.delete(format!(
"https://api.browserbase.com/v1/sessions/{}",
session.session_id
))
.header("X-BB-API-Key", &api_key)
.send()
.await;
}
}
"browser-use" => {
if let Ok(api_key) = env::var("BROWSER_USE_API_KEY") {
let _ = client
.patch(format!(
"https://api.browser-use.com/api/v2/browsers/{}",
session.session_id
))
.header("X-Browser-Use-API-Key", &api_key)
.header("Content-Type", "application/json")
.json(&json!({ "action": "stop" }))
.send()
.await;
}
}
"kernel" => {
if let Ok(api_key) = env::var("KERNEL_API_KEY") {
let endpoint = env::var("KERNEL_ENDPOINT")
.unwrap_or_else(|_| "https://api.onkernel.com".to_string());
let _ = client
.delete(format!(
"{}/browsers/{}",
endpoint.trim_end_matches('/'),
session.session_id
))
.header("Authorization", format!("Bearer {}", api_key))
.send()
.await;
}
}
_ => {}
}
}
async fn connect_browserbase() -> Result<(String, Option<ProviderSession>), String> {
let api_key = env::var("BROWSERBASE_API_KEY")
.map_err(|_| "BROWSERBASE_API_KEY environment variable is not set")?;
let project_id = env::var("BROWSERBASE_PROJECT_ID")
.map_err(|_| "BROWSERBASE_PROJECT_ID environment variable is not set")?;
let client = reqwest::Client::new();
let response = client
.post("https://api.browserbase.com/v1/sessions")
.header("Content-Type", "application/json")
.header("X-BB-API-Key", &api_key)
.json(&json!({ "projectId": project_id }))
.send()
.await
.map_err(|e| format!("Browserbase request failed: {}", e))?;
let status = response.status();
let body = response
.text()
.await
.map_err(|e| format!("Failed to read Browserbase response: {}", e))?;
if !status.is_success() {
return Err(format!(
"Browserbase API error ({}): {}",
status.as_u16(),
body
));
}
let json: Value =
serde_json::from_str(&body).map_err(|e| format!("Invalid Browserbase response: {}", e))?;
let session_id = json
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let ws_url = json
.get("connectUrl")
.and_then(|v| v.as_str())
.map(String::from)
.ok_or_else(|| "Browserbase response missing connectUrl".to_string())?;
Ok((
ws_url,
Some(ProviderSession {
provider: "browserbase".to_string(),
session_id,
}),
))
}
async fn connect_browser_use() -> Result<(String, Option<ProviderSession>), String> {
let api_key = env::var("BROWSER_USE_API_KEY")
.map_err(|_| "BROWSER_USE_API_KEY environment variable is not set")?;
let client = reqwest::Client::new();
let response = client
.post("https://api.browser-use.com/api/v2/browsers")
.header("Content-Type", "application/json")
.header("X-Browser-Use-API-Key", &api_key)
.json(&json!({}))
.send()
.await
.map_err(|e| format!("Browser Use request failed: {}", e))?;
let status = response.status();
let body = response
.text()
.await
.map_err(|e| format!("Failed to read Browser Use response: {}", e))?;
if !status.is_success() {
return Err(format!(
"Browser Use API error ({}): {}",
status.as_u16(),
body
));
}
let json: Value =
serde_json::from_str(&body).map_err(|e| format!("Invalid Browser Use response: {}", e))?;
let session_id = json
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let ws_url = json
.get("cdp_url")
.or_else(|| json.get("cdpUrl"))
.and_then(|v| v.as_str())
.map(String::from)
.ok_or_else(|| "Browser Use response missing cdp_url or cdpUrl".to_string())?;
Ok((
ws_url,
Some(ProviderSession {
provider: "browser-use".to_string(),
session_id,
}),
))
}
async fn connect_kernel() -> Result<(String, Option<ProviderSession>), String> {
let api_key =
env::var("KERNEL_API_KEY").map_err(|_| "KERNEL_API_KEY environment variable is not set")?;
let endpoint =
env::var("KERNEL_ENDPOINT").unwrap_or_else(|_| "https://api.onkernel.com".to_string());
let url = format!("{}/browsers", endpoint.trim_end_matches('/'));
let headless = env::var("KERNEL_HEADLESS")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(true);
let stealth = env::var("KERNEL_STEALTH")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let timeout_seconds = env::var("KERNEL_TIMEOUT_SECONDS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(300);
let mut body = json!({
"headless": headless,
"stealth": stealth,
"timeout_seconds": timeout_seconds,
});
if let Ok(profile) = env::var("KERNEL_PROFILE_NAME") {
if !profile.is_empty() {
body.as_object_mut()
.unwrap()
.insert("profile".to_string(), json!(profile));
}
}
let client = reqwest::Client::new();
let response = client
.post(&url)
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key))
.json(&body)
.send()
.await
.map_err(|e| format!("Kernel request failed: {}", e))?;
let status = response.status();
let resp_body = response
.text()
.await
.map_err(|e| format!("Failed to read Kernel response: {}", e))?;
if !status.is_success() {
return Err(format!(
"Kernel API error ({}): {}",
status.as_u16(),
resp_body
));
}
let json: Value =
serde_json::from_str(&resp_body).map_err(|e| format!("Invalid Kernel response: {}", e))?;
let session_id = json
.get("session_id")
.or_else(|| json.get("id"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let ws_url = json
.get("cdp_ws_url")
.or_else(|| json.get("connectUrl"))
.or_else(|| json.get("connect_url"))
.or_else(|| json.get("cdpUrl"))
.or_else(|| json.get("cdp_url"))
.and_then(|v| v.as_str())
.map(String::from)
.ok_or_else(|| {
"Kernel response missing cdp_ws_url, connectUrl, connect_url, cdpUrl, or cdp_url"
.to_string()
})?;
Ok((
ws_url,
Some(ProviderSession {
provider: "kernel".to_string(),
session_id,
}),
))
}