Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abb65c632b | ||
|
|
e803bffbbb | ||
|
|
96ee2f9758 | ||
|
|
0a3d2a91a6 | ||
|
|
1e5dfd35cb | ||
|
|
b6b2ca56ca |
Generated
+1
-1
@@ -45,7 +45,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-browser-stealth"
|
||||
version = "0.27.0-fork.39"
|
||||
version = "0.27.0-fork.42"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"async-trait",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "agent-browser-stealth"
|
||||
version = "0.27.0-fork.39"
|
||||
version = "0.27.0-fork.42"
|
||||
edition = "2021"
|
||||
description = "Fast browser automation CLI for AI agents"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -344,6 +344,19 @@ fn host_manifest_path_for_chrome() -> Option<PathBuf> {
|
||||
})
|
||||
}
|
||||
|
||||
/// True if the ab-connect native-messaging host manifest is present — i.e. the
|
||||
/// user has set up the extension path. When installed, auto-connect treats the
|
||||
/// dialog-free extension relay as the *intended* transport and refuses to fall
|
||||
/// back to a raw debug port (which would pop Chrome 136+'s "Allow remote
|
||||
/// debugging?" consent modal). The relay-url file comes and goes with the
|
||||
/// service worker; this manifest is the durable signal that the extension is
|
||||
/// the chosen path.
|
||||
pub fn host_installed() -> bool {
|
||||
native_messaging_dirs()
|
||||
.into_iter()
|
||||
.any(|d| d.join(format!("{HOST_NAME}.json")).exists())
|
||||
}
|
||||
|
||||
fn report(json: bool, ok: bool, msg: &str) {
|
||||
if json {
|
||||
println!(
|
||||
|
||||
@@ -309,6 +309,12 @@ pub struct BrowserManager {
|
||||
pub ignore_https_errors: bool,
|
||||
/// Origins visited during this session, used by save_state to collect cross-origin localStorage.
|
||||
visited_origins: HashSet<String>,
|
||||
/// Target IDs of tabs THIS session created via `Target.createTarget`. When
|
||||
/// connected to the user's real Chrome (not a launched browser), these are
|
||||
/// closed on `close()` so the session's tabs don't pile up in the user's
|
||||
/// browser after it ends. Only ever holds tabs we created — never the user's
|
||||
/// existing tabs or other sessions' tabs — so closing them is always safe.
|
||||
created_targets: HashSet<String>,
|
||||
next_tab_id: u32,
|
||||
/// Whether to enable the CDP `Runtime` domain (console / error / exception capture).
|
||||
/// OFF by default for stealth: a live `Runtime.enable` is a detectable CDP signal
|
||||
@@ -433,6 +439,7 @@ impl BrowserManager {
|
||||
download_path: download_path.clone(),
|
||||
ignore_https_errors,
|
||||
visited_origins: HashSet::new(),
|
||||
created_targets: HashSet::new(),
|
||||
next_tab_id: 1,
|
||||
capture_console: console_capture_enabled(),
|
||||
};
|
||||
@@ -523,6 +530,7 @@ impl BrowserManager {
|
||||
download_path: None,
|
||||
ignore_https_errors: false,
|
||||
visited_origins: HashSet::new(),
|
||||
created_targets: HashSet::new(),
|
||||
next_tab_id: 1,
|
||||
capture_console: console_capture_enabled(),
|
||||
};
|
||||
@@ -586,6 +594,8 @@ impl BrowserManager {
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
// We created this tab — own it so close() can clean it up.
|
||||
self.created_targets.insert(result.target_id.clone());
|
||||
|
||||
let attach_result: AttachToTargetResult = self
|
||||
.client
|
||||
@@ -892,6 +902,24 @@ impl BrowserManager {
|
||||
.client
|
||||
.send_command_no_params("Browser.close", None)
|
||||
.await;
|
||||
} else {
|
||||
// Connected to the user's real Chrome: we must NOT close their
|
||||
// browser, but we DO own the tabs this session created. Close them so
|
||||
// they don't pile up in the user's window (in their per-session tab
|
||||
// group) every time a session ends, idles out, or the daemon shuts
|
||||
// down. `created_targets` only holds tabs we made via
|
||||
// Target.createTarget — never the user's existing tabs or other
|
||||
// sessions' — so this is always safe. Best-effort per tab.
|
||||
for target_id in self.created_targets.drain() {
|
||||
let _ = self
|
||||
.client
|
||||
.send_command_typed::<_, Value>(
|
||||
"Target.closeTarget",
|
||||
&CloseTargetParams { target_id },
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut process) = self.browser_process.take() {
|
||||
@@ -993,6 +1021,8 @@ impl BrowserManager {
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
// We created this tab — own it so close() can clean it up.
|
||||
self.created_targets.insert(result.target_id.clone());
|
||||
|
||||
let attach_result: AttachToTargetResult = self
|
||||
.client
|
||||
@@ -1158,6 +1188,8 @@ impl BrowserManager {
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
// We created this tab — own it so close() can clean it up.
|
||||
self.created_targets.insert(result.target_id.clone());
|
||||
|
||||
let attach: AttachToTargetResult = self
|
||||
.client
|
||||
@@ -1750,6 +1782,7 @@ async fn initialize_lightpanda_manager(
|
||||
download_path: None,
|
||||
ignore_https_errors: false,
|
||||
visited_origins: HashSet::new(),
|
||||
created_targets: HashSet::new(),
|
||||
next_tab_id: 1,
|
||||
capture_console: console_capture_enabled(),
|
||||
};
|
||||
|
||||
@@ -783,14 +783,46 @@ pub async fn auto_connect_cdp() -> Result<String, String> {
|
||||
// / :9222 probes below: if the user's Chrome happens to also be listening on a
|
||||
// debug port, attaching there would pop the consent dialog and defeat the
|
||||
// whole zero-interaction extension path.
|
||||
if let Some(relay) = crate::connect::relay_url() {
|
||||
// The relay is a local CDP-over-WS endpoint we connect to like Chrome.
|
||||
// A bare TCP liveness check (no WS upgrade) confirms it is actually
|
||||
// accepting before we commit, mirroring the consent-free probe used for
|
||||
// DevToolsActivePort.
|
||||
if relay_is_live(&relay).await {
|
||||
return Ok(relay);
|
||||
// If the extension is installed, it is the *intended* transport. The relay
|
||||
// URL file comes and goes with the MV3 service worker (a Chrome restart or an
|
||||
// idle SW briefly drops it), so a single failed probe doesn't mean "no
|
||||
// extension" — retry for a few seconds while it reconnects. Crucially, when
|
||||
// the extension is set up we must NEVER fall through to the raw :9222 path
|
||||
// below: that pops Chrome 136+'s "Allow remote debugging?" dialog, the exact
|
||||
// thing the extension exists to avoid.
|
||||
// ~15s of retries (500ms apart) when the extension is installed: long enough
|
||||
// for the MV3 service worker to wake and reconnect on its own (onStartup
|
||||
// after a Chrome restart, or the keepalive alarm) so the relay self-heals
|
||||
// with NO user action. The loop re-checks the relay file every iteration, so
|
||||
// a recovery mid-wait is picked up immediately — the full window is only ever
|
||||
// spent when the extension is genuinely down.
|
||||
let host_installed = crate::connect::host_installed();
|
||||
let relay_attempts = if host_installed { 30 } else { 1 };
|
||||
for attempt in 0..relay_attempts {
|
||||
if let Some(relay) = crate::connect::relay_url() {
|
||||
// The relay is a local CDP-over-WS endpoint we connect to like Chrome.
|
||||
// A bare TCP liveness check (no WS upgrade) confirms it is actually
|
||||
// accepting before we commit, mirroring the consent-free probe used
|
||||
// for DevToolsActivePort.
|
||||
if relay_is_live(&relay).await {
|
||||
return Ok(relay);
|
||||
}
|
||||
}
|
||||
if host_installed && attempt + 1 < relay_attempts {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
|
||||
if host_installed {
|
||||
return Err(
|
||||
"The agent-browser-stealth extension is installed, but its relay \
|
||||
isn't connected right now. Wake it up — click the extension's \
|
||||
toolbar icon, or reload it at chrome://extensions — then retry. \
|
||||
(agent-browser will not attach to a raw --remote-debugging-port \
|
||||
while the extension is set up, because that pops Chrome's \"Allow \
|
||||
remote debugging?\" dialog. Use --cdp <port> to force the raw path.)"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let user_data_dirs = get_chrome_user_data_dirs();
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agent-browser-stealth",
|
||||
"version": "0.27.0-fork.39",
|
||||
"version": "0.27.0-fork.42",
|
||||
"description": "Browser automation CLI for AI agents — stealth fork with anti-detection",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.1.3",
|
||||
|
||||
Reference in New Issue
Block a user