Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
839aaa5586 | ||
|
|
a7f9c24fdb | ||
|
|
42ade7b4e8 | ||
|
|
2dabed973e | ||
|
|
dd2deff06c | ||
|
|
340886293a | ||
|
|
fc1699a526 |
@@ -38,6 +38,10 @@ __pycache__/
|
|||||||
*.webm
|
*.webm
|
||||||
test/e2e/.dogfood-output/
|
test/e2e/.dogfood-output/
|
||||||
|
|
||||||
|
# ...but these are real repo assets, not test artifacts — keep them tracked
|
||||||
|
!assets/*.png
|
||||||
|
!extensions/ab-connect/icons/*.png
|
||||||
|
|
||||||
# Package manager
|
# Package manager
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
# agent-browser-stealth
|
# agent-browser-stealth
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Stealth fork of [agent-browser](https://github.com/vercel-labs/agent-browser) — connects to your real Chrome, shares your login sessions, and is undetectable by anti-bot systems.
|
Stealth fork of [agent-browser](https://github.com/vercel-labs/agent-browser) — connects to your real Chrome, shares your login sessions, and is undetectable by anti-bot systems.
|
||||||
|
|
||||||
For basic usage, commands, and API reference, see the [upstream documentation](https://github.com/vercel-labs/agent-browser).
|
For basic usage, commands, and API reference, see the [upstream documentation](https://github.com/vercel-labs/agent-browser).
|
||||||
|
|
||||||
## Why this fork?
|
## Why this fork?
|
||||||
|
|
||||||
|
<img src="assets/fingerprint.png" alt="real but undetectable fingerprint" width="300" align="right" />
|
||||||
|
|
||||||
**agent-browser** launches a fresh browser with an empty profile. You need to log in again, and websites can detect it's automated.
|
**agent-browser** launches a fresh browser with an empty profile. You need to log in again, and websites can detect it's automated.
|
||||||
|
|
||||||
**agent-browser-stealth** connects to your existing Chrome. Your cookies, sessions, and browser fingerprint are all real — because it IS your real browser.
|
**agent-browser-stealth** connects to your existing Chrome. Your cookies, sessions, and browser fingerprint are all real — because it IS your real browser.
|
||||||
@@ -115,6 +119,8 @@ In CI environments, standalone mode is used automatically.
|
|||||||
|
|
||||||
## Anti-detection
|
## Anti-detection
|
||||||
|
|
||||||
|
<img src="assets/shield.png" alt="stealth shield" width="320" align="right" />
|
||||||
|
|
||||||
When connected to your real Chrome, we inject **zero** JavaScript patches. Your browser's fingerprint is completely genuine. The guiding rule is **native CDP/Chrome overrides over JS lies** — a re-defined getter is itself detectable; a native override isn't.
|
When connected to your real Chrome, we inject **zero** JavaScript patches. Your browser's fingerprint is completely genuine. The guiding rule is **native CDP/Chrome overrides over JS lies** — a re-defined getter is itself detectable; a native override isn't.
|
||||||
|
|
||||||
- `navigator.webdriver = false` via `Emulation.setAutomationOverride` (native, undetectable by CreepJS-style lie tests).
|
- `navigator.webdriver = false` via `Emulation.setAutomationOverride` (native, undetectable by CreepJS-style lie tests).
|
||||||
@@ -124,11 +130,27 @@ When connected to your real Chrome, we inject **zero** JavaScript patches. Your
|
|||||||
|
|
||||||
| Test site | Result |
|
| Test site | Result |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [CreepJS](https://abrahamjuliot.github.io/creepjs/) | 0% stealth, 0% headless |
|
| [CreepJS](https://abrahamjuliot.github.io/creepjs/) | **0% stealth · 0% headless** (no override traces at all) |
|
||||||
| [bot.sannysoft.com](https://bot.sannysoft.com) | All green |
|
| [bot.incolumitas.com](https://bot.incolumitas.com/) | all checks OK — `overflowTest`, `overrideTest`, `puppeteerExtraStealthUsed`, worker consistency |
|
||||||
| [Cloudflare Turnstile](https://nowsecure.nl) | Passed |
|
| [bot.sannysoft.com](https://bot.sannysoft.com) | all green |
|
||||||
|
| [BrowserScan](https://www.browserscan.net/bot-detection) | Webdriver · User-Agent · CDP all clean |
|
||||||
|
| [Cloudflare Turnstile](https://nowsecure.nl) | passed |
|
||||||
|
|
||||||
When using `--launch` mode (standalone browser), a full suite of 32 stealth patches is applied for headless Chrome.
|
`0% stealth` on CreepJS is the key number: because the connect path patches **nothing**, there is no override for a lie-detector to catch. (Dashboards that read `navigator.languages` order or IP geolocation may show a soft "navigator"/"location" flag — that tracks *your real Chrome's* language list and network, not an automation tell.)
|
||||||
|
|
||||||
|
When using `--launch` mode (standalone browser), a full suite of stealth patches is applied instead, and it still passes the suite above.
|
||||||
|
|
||||||
|
### Verify it yourself
|
||||||
|
|
||||||
|
Don't take our word for it — point your connected Chrome at the toughest public detectors and compare:
|
||||||
|
|
||||||
|
- **[CreepJS](https://abrahamjuliot.github.io/creepjs/)** — the most thorough fingerprint / lie detector
|
||||||
|
- **[bot.incolumitas.com](https://bot.incolumitas.com/)** — behavioral + fingerprint scoring with a public methodology
|
||||||
|
- **[BrowserScan](https://www.browserscan.net/bot-detection)** — Webdriver / User-Agent / CDP / Navigator
|
||||||
|
- **[bot.sannysoft.com](https://bot.sannysoft.com)** — the classic automation-marker checklist
|
||||||
|
- **[pixelscan.net](https://pixelscan.net/)** · **[iphey.com](https://iphey.com/)** — consistency & identity
|
||||||
|
|
||||||
|
We deliberately **don't ship our own bot detector** — the strongest, most honest benchmark is the market's best detectors run against your real browser.
|
||||||
|
|
||||||
### Tuning knobs (environment variables)
|
### Tuning knobs (environment variables)
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
@@ -45,7 +45,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-browser-stealth"
|
name = "agent-browser-stealth"
|
||||||
version = "0.27.0-fork.25"
|
version = "0.27.0-fork.29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "agent-browser-stealth"
|
name = "agent-browser-stealth"
|
||||||
version = "0.27.0-fork.25"
|
version = "0.27.0-fork.29"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Fast browser automation CLI for AI agents"
|
description = "Fast browser automation CLI for AI agents"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
@@ -146,6 +146,16 @@ struct ChromeArgs {
|
|||||||
temp_user_data_dir: Option<PathBuf>,
|
temp_user_data_dir: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether to launch Chrome headless. The stealth fork FORBIDS headless (it's a
|
||||||
|
/// bot-detection tell), so this is `false` unless an operator explicitly opts in
|
||||||
|
/// via `AGENT_BROWSER_ALLOW_HEADLESS=1` for a display-less server. The `headless`
|
||||||
|
/// LaunchOption is intentionally ignored — headed is non-negotiable for stealth.
|
||||||
|
fn launch_headless() -> bool {
|
||||||
|
std::env::var("AGENT_BROWSER_ALLOW_HEADLESS")
|
||||||
|
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
/// Decide the `--force-webrtc-ip-handling-policy` value, if any, for a launched
|
/// Decide the `--force-webrtc-ip-handling-policy` value, if any, for a launched
|
||||||
/// Chrome. Returns `None` to leave WebRTC at Chrome's default behavior.
|
/// Chrome. Returns `None` to leave WebRTC at Chrome's default behavior.
|
||||||
fn webrtc_ip_handling_policy(has_proxy: bool) -> Option<&'static str> {
|
fn webrtc_ip_handling_policy(has_proxy: bool) -> Option<&'static str> {
|
||||||
@@ -202,9 +212,13 @@ fn build_chrome_args(options: &LaunchOptions) -> Result<ChromeArgs, String> {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|exts| !exts.is_empty());
|
.is_some_and(|exts| !exts.is_empty());
|
||||||
|
|
||||||
// Extensions require headed mode in native Chrome (content scripts are not
|
// Stealth fork: NEVER launch headless. Headless Chrome is a detectable tell
|
||||||
// injected in headless mode). Skip --headless when extensions are loaded.
|
// (creepjs scores ~33% headless even with new-headless; a real GPU and a
|
||||||
if options.headless && !has_extensions {
|
// headed window score 0%). So we always launch headed and ignore the
|
||||||
|
// `headless` option. The only escape is an explicit AGENT_BROWSER_ALLOW_HEADLESS=1
|
||||||
|
// for genuinely display-less servers (discouraged — it forfeits stealth).
|
||||||
|
// Extensions also require headed mode (content scripts aren't injected headless).
|
||||||
|
if launch_headless() && !has_extensions {
|
||||||
args.push("--headless=new".to_string());
|
args.push("--headless=new".to_string());
|
||||||
// Linux paints native scrollbars into viewport screenshots unless
|
// Linux paints native scrollbars into viewport screenshots unless
|
||||||
// Chrome is launched with this flag. `--hide-scrollbars` is
|
// Chrome is launched with this flag. `--hide-scrollbars` is
|
||||||
@@ -278,7 +292,7 @@ fn build_chrome_args(options: &LaunchOptions) -> Result<ChromeArgs, String> {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|a| a.starts_with("--start-maximized") || a.starts_with("--window-size="));
|
.any(|a| a.starts_with("--start-maximized") || a.starts_with("--window-size="));
|
||||||
|
|
||||||
if !has_window_size && options.headless && !has_extensions {
|
if !has_window_size && launch_headless() && !has_extensions {
|
||||||
let (w, h) = options.viewport_size.unwrap_or((1280, 720));
|
let (w, h) = options.viewport_size.unwrap_or((1280, 720));
|
||||||
args.push(format!("--window-size={},{}", w, h));
|
args.push(format!("--window-size={},{}", w, h));
|
||||||
}
|
}
|
||||||
@@ -759,6 +773,26 @@ fn running_process_cmdlines() -> Option<Vec<String>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn auto_connect_cdp() -> Result<String, String> {
|
pub async fn auto_connect_cdp() -> Result<String, String> {
|
||||||
|
// Prefer the dialog-free `ab-connect` extension relay when it is live.
|
||||||
|
// The relay drives the user's REAL Chrome via the extension's
|
||||||
|
// `chrome.debugger` permission, which — unlike a raw `--remote-debugging-port`
|
||||||
|
// CDP attach — never triggers Chrome 136+'s per-connection
|
||||||
|
// "Allow remote debugging?" consent modal. The native-messaging host writes
|
||||||
|
// ~/.agent-browser/relay-cdp-url while connected and removes it on exit, so a
|
||||||
|
// present URL means the relay is up. This must win over the DevToolsActivePort
|
||||||
|
// / :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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let user_data_dirs = get_chrome_user_data_dirs();
|
let user_data_dirs = get_chrome_user_data_dirs();
|
||||||
|
|
||||||
for dir in &user_data_dirs {
|
for dir in &user_data_dirs {
|
||||||
@@ -836,6 +870,22 @@ async fn tcp_port_alive(port: u16) -> bool {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Consent-free liveness for the `ab-connect` relay ws URL (`ws://127.0.0.1:<port>/…`).
|
||||||
|
/// Parses the port and does a bare TCP connect — a stale relay-cdp-url file
|
||||||
|
/// (host exited without cleanup) must not divert auto-connect away from the
|
||||||
|
/// working port path.
|
||||||
|
async fn relay_is_live(ws_url: &str) -> bool {
|
||||||
|
let port = ws_url
|
||||||
|
.strip_prefix("ws://")
|
||||||
|
.and_then(|rest| rest.split('/').next())
|
||||||
|
.and_then(|hostport| hostport.rsplit(':').next())
|
||||||
|
.and_then(|p| p.parse::<u16>().ok());
|
||||||
|
match port {
|
||||||
|
Some(p) => tcp_port_alive(p).await,
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the default Chrome user-data directory paths for the current platform.
|
/// Returns the default Chrome user-data directory paths for the current platform.
|
||||||
/// Includes Chrome, Chrome Canary, Chromium, and Brave.
|
/// Includes Chrome, Chrome Canary, Chromium, and Brave.
|
||||||
pub fn get_chrome_user_data_dirs() -> Vec<PathBuf> {
|
pub fn get_chrome_user_data_dirs() -> Vec<PathBuf> {
|
||||||
@@ -1520,24 +1570,44 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_args_headless_includes_headless_flag() {
|
fn test_build_args_forbids_headless_by_default() {
|
||||||
|
// Stealth fork: headless is FORBIDDEN. `headless: true` is ignored — the
|
||||||
|
// launch is always headed (no --headless / swiftshader / forced size).
|
||||||
|
let g = EnvGuard::new(&["AGENT_BROWSER_ALLOW_HEADLESS"]);
|
||||||
|
g.remove("AGENT_BROWSER_ALLOW_HEADLESS");
|
||||||
|
let opts = LaunchOptions {
|
||||||
|
headless: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let result = build_chrome_args(&opts).unwrap();
|
||||||
|
assert!(
|
||||||
|
!result.args.iter().any(|a| a.contains("--headless")),
|
||||||
|
"headless must be forbidden even when the headless option is true"
|
||||||
|
);
|
||||||
|
assert!(!result
|
||||||
|
.args
|
||||||
|
.iter()
|
||||||
|
.any(|a| a == "--enable-unsafe-swiftshader"));
|
||||||
|
if let Some(dir) = result.temp_user_data_dir {
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_args_allow_headless_escape() {
|
||||||
|
// The only way back to headless: an explicit opt-in for display-less servers.
|
||||||
|
let g = EnvGuard::new(&["AGENT_BROWSER_ALLOW_HEADLESS"]);
|
||||||
|
g.set("AGENT_BROWSER_ALLOW_HEADLESS", "1");
|
||||||
let opts = LaunchOptions {
|
let opts = LaunchOptions {
|
||||||
headless: true,
|
headless: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let result = build_chrome_args(&opts).unwrap();
|
let result = build_chrome_args(&opts).unwrap();
|
||||||
assert!(result.args.iter().any(|a| a == "--headless=new"));
|
assert!(result.args.iter().any(|a| a == "--headless=new"));
|
||||||
assert!(result.args.iter().any(|a| a == "--hide-scrollbars"));
|
|
||||||
assert!(result
|
|
||||||
.args
|
|
||||||
.iter()
|
|
||||||
.any(|a| a == "--enable-unsafe-swiftshader"));
|
|
||||||
assert!(result.args.iter().any(|a| a == "--window-size=1280,720"));
|
assert!(result.args.iter().any(|a| a == "--window-size=1280,720"));
|
||||||
// Temp dir created when no profile
|
if let Some(dir) = result.temp_user_data_dir {
|
||||||
assert!(result.temp_user_data_dir.is_some());
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
let dir = result.temp_user_data_dir.unwrap();
|
}
|
||||||
assert!(dir.exists());
|
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2166,4 +2236,35 @@ mod tests {
|
|||||||
let result = resolve_cdp_from_active_port(port, "/devtools/browser/dead").await;
|
let result = resolve_cdp_from_active_port(port, "/devtools/browser/dead").await;
|
||||||
assert!(result.is_err(), "should fail when nothing is listening");
|
assert!(result.is_err(), "should fail when nothing is listening");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_relay_is_live_true_when_listening() {
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let port = listener.local_addr().unwrap().port();
|
||||||
|
let url = format!("ws://127.0.0.1:{}/abc-guid", port);
|
||||||
|
assert!(
|
||||||
|
relay_is_live(&url).await,
|
||||||
|
"relay_is_live should be true while the port is accepting"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_relay_is_live_false_when_dead() {
|
||||||
|
// Bind to grab a free port, then drop so nothing is listening.
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let port = listener.local_addr().unwrap().port();
|
||||||
|
drop(listener);
|
||||||
|
let url = format!("ws://127.0.0.1:{}/abc-guid", port);
|
||||||
|
assert!(
|
||||||
|
!relay_is_live(&url).await,
|
||||||
|
"relay_is_live must be false for a stale relay-cdp-url (host exited)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_relay_is_live_false_on_malformed_url() {
|
||||||
|
assert!(!relay_is_live("not-a-ws-url").await);
|
||||||
|
assert!(!relay_is_live("ws://127.0.0.1/no-port").await);
|
||||||
|
assert!(!relay_is_live("ws://127.0.0.1:notaport/x").await);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,13 +270,18 @@ pub fn strip_source_url_labels(input: &str) -> String {
|
|||||||
re_block.replace_all(&output, "").to_string()
|
re_block.replace_all(&output, "").to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The legacy `navigator.platform` value (set via the CDP
|
||||||
|
/// `Emulation.setUserAgentOverride` `platform` field). This is NOT the UA-CH
|
||||||
|
/// platform (see `platform_hint`): real Chrome reports `MacIntel` on macOS and
|
||||||
|
/// `Linux x86_64` on Linux, so emitting the UA-CH form ("macOS"/"Linux") here is
|
||||||
|
/// a detectable mismatch against the UA's "Intel Mac OS X" / Linux strings.
|
||||||
fn platform_string() -> &'static str {
|
fn platform_string() -> &'static str {
|
||||||
if cfg!(target_os = "macos") {
|
if cfg!(target_os = "macos") {
|
||||||
"macOS"
|
"MacIntel"
|
||||||
} else if cfg!(target_os = "windows") {
|
} else if cfg!(target_os = "windows") {
|
||||||
"Win32"
|
"Win32"
|
||||||
} else {
|
} else {
|
||||||
"Linux"
|
"Linux x86_64"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -394,6 +394,24 @@ const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLCon
|
|||||||
defineVendor(navigator);
|
defineVendor(navigator);
|
||||||
})();
|
})();
|
||||||
(function(){
|
(function(){
|
||||||
|
// Native > JS lies: a real headed Chrome already exposes the correct, fully
|
||||||
|
// native navigator.plugins (5 PDF-viewer aliases, a native item() that does
|
||||||
|
// the WebIDL uint32-index wrap, length on the prototype). Overriding that
|
||||||
|
// with a JS fake is strictly worse — it ships a non-native item() whose
|
||||||
|
// .toString() reveals the patch, breaks the uint32 wrap (incolumitas
|
||||||
|
// overflowTest), and pins an anachronistic "Native Client" plugin that modern
|
||||||
|
// Chrome removed. Since this fork forbids headless and always launches headed,
|
||||||
|
// the native plugins are present, so we leave them alone. We only fall back to
|
||||||
|
// a synthetic list when native plugins are genuinely empty (e.g. the
|
||||||
|
// discouraged AGENT_BROWSER_ALLOW_HEADLESS escape on old headless).
|
||||||
|
try {
|
||||||
|
const np = navigator.plugins;
|
||||||
|
const itemNative =
|
||||||
|
np && typeof np.item === 'function' &&
|
||||||
|
/\[native code\]/.test(Function.prototype.toString.call(np.item));
|
||||||
|
if (np && np.length > 0 && itemNative) return;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
const makeMimeType = (type, suffixes, description) => {
|
const makeMimeType = (type, suffixes, description) => {
|
||||||
const mime = Object.create(MimeType.prototype);
|
const mime = Object.create(MimeType.prototype);
|
||||||
Object.defineProperties(mime, {
|
Object.defineProperties(mime, {
|
||||||
@@ -427,40 +445,54 @@ const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLCon
|
|||||||
return plugin;
|
return plugin;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Make a fake method masquerade as native: name + `[native code]` toString.
|
||||||
|
const maskNative = (fn, name) => {
|
||||||
|
Object.defineProperty(fn, 'name', { value: name, configurable: true });
|
||||||
|
Object.defineProperty(fn, 'toString', {
|
||||||
|
value: () => `function ${name}() { [native code] }`,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
return fn;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Modern Chrome (since ~v109) exposes exactly these 5 PDF-viewer aliases and
|
||||||
|
// two mimeTypes (application/pdf, text/pdf). Native Client was removed years
|
||||||
|
// ago, so it must NOT appear. Each plugin carries both mimeTypes.
|
||||||
const pdfMime = makeMimeType('application/pdf', 'pdf', 'Portable Document Format');
|
const pdfMime = makeMimeType('application/pdf', 'pdf', 'Portable Document Format');
|
||||||
const chromePdfMime = makeMimeType(
|
const textPdfMime = makeMimeType('text/pdf', 'pdf', 'Portable Document Format');
|
||||||
'application/x-google-chrome-pdf',
|
const mimes = [pdfMime, textPdfMime];
|
||||||
'pdf',
|
|
||||||
'Portable Document Format'
|
|
||||||
);
|
|
||||||
const naclMime = makeMimeType('application/x-nacl', '', 'Native Client Executable');
|
|
||||||
const pnaclMime = makeMimeType('application/x-pnacl', '', 'Portable Native Client Executable');
|
|
||||||
|
|
||||||
const plugins = [
|
const plugins = [
|
||||||
makePlugin('Chrome PDF Plugin', 'Portable Document Format', 'internal-pdf-viewer', [chromePdfMime]),
|
'PDF Viewer',
|
||||||
makePlugin('Chrome PDF Viewer', '', 'mhjfbmdgcfjbbpaeojofohoefgiehjai', [pdfMime]),
|
'Chrome PDF Viewer',
|
||||||
makePlugin('Native Client', '', 'internal-nacl-plugin', [naclMime, pnaclMime]),
|
'Chromium PDF Viewer',
|
||||||
];
|
'Microsoft Edge PDF Viewer',
|
||||||
|
'WebKit built-in PDF',
|
||||||
|
].map((name) => makePlugin(name, 'Portable Document Format', 'internal-pdf-viewer', mimes));
|
||||||
|
|
||||||
const pluginArray = Object.create(PluginArray.prototype);
|
const pluginArray = Object.create(PluginArray.prototype);
|
||||||
plugins.forEach((p, i) => {
|
plugins.forEach((p, i) => {
|
||||||
pluginArray[i] = p;
|
pluginArray[i] = p;
|
||||||
pluginArray[p.name] = p;
|
pluginArray[p.name] = p;
|
||||||
});
|
});
|
||||||
Object.defineProperty(pluginArray, 'length', { get: () => plugins.length });
|
Object.defineProperty(pluginArray, 'length', { get: () => plugins.length });
|
||||||
pluginArray.item = (i) => plugins[i] || null;
|
// `i >>> 0` replicates the WebIDL unsigned-long index coercion, so
|
||||||
pluginArray.namedItem = (name) => plugins.find(p => p.name === name) || null;
|
// item(2**32) wraps to item(0) like the real native PluginArray.item.
|
||||||
pluginArray.refresh = () => {};
|
pluginArray.item = maskNative((i) => plugins[i >>> 0] || null, 'item');
|
||||||
|
pluginArray.namedItem = maskNative((name) => plugins.find(p => p.name === name) || null, 'namedItem');
|
||||||
|
pluginArray.refresh = maskNative(() => {}, 'refresh');
|
||||||
pluginArray[Symbol.iterator] = function*() { for (const p of plugins) yield p; };
|
pluginArray[Symbol.iterator] = function*() { for (const p of plugins) yield p; };
|
||||||
|
|
||||||
const mimeTypes = [chromePdfMime, pdfMime, naclMime, pnaclMime];
|
const mimeTypes = [pdfMime, textPdfMime];
|
||||||
const mimeTypeArray = Object.create(MimeTypeArray.prototype);
|
const mimeTypeArray = Object.create(MimeTypeArray.prototype);
|
||||||
mimeTypes.forEach((m, i) => {
|
mimeTypes.forEach((m, i) => {
|
||||||
mimeTypeArray[i] = m;
|
mimeTypeArray[i] = m;
|
||||||
mimeTypeArray[m.type] = m;
|
mimeTypeArray[m.type] = m;
|
||||||
});
|
});
|
||||||
Object.defineProperty(mimeTypeArray, 'length', { get: () => mimeTypes.length });
|
Object.defineProperty(mimeTypeArray, 'length', { get: () => mimeTypes.length });
|
||||||
mimeTypeArray.item = (i) => mimeTypes[i] || null;
|
mimeTypeArray.item = maskNative((i) => mimeTypes[i >>> 0] || null, 'item');
|
||||||
mimeTypeArray.namedItem = (name) => mimeTypes.find(m => m.type === name) || null;
|
mimeTypeArray.namedItem = maskNative((name) => mimeTypes.find(m => m.type === name) || null, 'namedItem');
|
||||||
mimeTypeArray[Symbol.iterator] = function*() { for (const m of mimeTypes) yield m; };
|
mimeTypeArray[Symbol.iterator] = function*() { for (const m of mimeTypes) yield m; };
|
||||||
|
|
||||||
Object.defineProperty(navigator, 'plugins', {
|
Object.defineProperty(navigator, 'plugins', {
|
||||||
|
|||||||
@@ -1082,7 +1082,7 @@ Global Options:
|
|||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--session <name> Use specific session
|
--session <name> Use specific session
|
||||||
--headers <json> Set HTTP headers (scoped to this origin)
|
--headers <json> Set HTTP headers (scoped to this origin)
|
||||||
--headed Show browser window
|
--headed Show browser window (default; headless is forbidden — it's a bot tell)
|
||||||
--enable react-devtools Inject the React DevTools hook before any page JS
|
--enable react-devtools Inject the React DevTools hook before any page JS
|
||||||
--init-script <path> Register a page init script (repeatable)
|
--init-script <path> Register a page init script (repeatable)
|
||||||
|
|
||||||
@@ -3114,7 +3114,8 @@ Options:
|
|||||||
--screenshot-dir <path> Default screenshot output directory (or AGENT_BROWSER_SCREENSHOT_DIR)
|
--screenshot-dir <path> Default screenshot output directory (or AGENT_BROWSER_SCREENSHOT_DIR)
|
||||||
--screenshot-quality <n> JPEG quality 0-100; ignored for PNG (or AGENT_BROWSER_SCREENSHOT_QUALITY)
|
--screenshot-quality <n> JPEG quality 0-100; ignored for PNG (or AGENT_BROWSER_SCREENSHOT_QUALITY)
|
||||||
--screenshot-format <fmt> Screenshot format: png, jpeg (or AGENT_BROWSER_SCREENSHOT_FORMAT)
|
--screenshot-format <fmt> Screenshot format: png, jpeg (or AGENT_BROWSER_SCREENSHOT_FORMAT)
|
||||||
--headed Show browser window (not headless) (or AGENT_BROWSER_HEADED env)
|
--headed Always on (default). Headless is forbidden (bot-detection tell);
|
||||||
|
display-less servers can opt back in with AGENT_BROWSER_ALLOW_HEADLESS=1
|
||||||
--cdp <port> Connect via CDP (Chrome DevTools Protocol)
|
--cdp <port> Connect via CDP (Chrome DevTools Protocol)
|
||||||
--color-scheme <scheme> Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME)
|
--color-scheme <scheme> Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME)
|
||||||
--download-path <path> Default download directory (or AGENT_BROWSER_DOWNLOAD_PATH)
|
--download-path <path> Default download directory (or AGENT_BROWSER_DOWNLOAD_PATH)
|
||||||
|
|||||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 644 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "agent-browser connect",
|
"name": "agent-browser-stealth",
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"description": "Let agent-browser drive your logged-in Chrome — install once, no token, no per-use confirmation.",
|
"description": "Let agent-browser drive your logged-in Chrome — install once, no token, no per-use confirmation.",
|
||||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
|
||||||
@@ -24,6 +24,6 @@
|
|||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"default_title": "agent-browser connect"
|
"default_title": "agent-browser-stealth"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "agent-browser-stealth",
|
"name": "agent-browser-stealth",
|
||||||
"version": "0.27.0-fork.25",
|
"version": "0.27.0-fork.29",
|
||||||
"description": "Browser automation CLI for AI agents \u2014 stealth fork with anti-detection",
|
"description": "Browser automation CLI for AI agents — stealth fork with anti-detection",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@11.1.3",
|
"packageManager": "pnpm@11.1.3",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ GUI-automation tool** like the `cua-driver` skill — see
|
|||||||
`agent-browser extension connect`. After that it's zero-confirmation, zero-token
|
`agent-browser extension connect`. After that it's zero-confirmation, zero-token
|
||||||
CLI. Use `--launch` instead when a fresh, isolated browser is fine.
|
CLI. Use `--launch` instead when a fresh, isolated browser is fine.
|
||||||
|
|
||||||
|
Each `--session` that connects gets its **own colored Chrome tab group** (named
|
||||||
|
after the session) and drives only its own tabs — multiple agents share the one
|
||||||
|
real browser without cross-talk, and the user's own tabs are never grouped. CDP
|
||||||
|
drives the page without moving the user's mouse/keyboard, so it doesn't fight
|
||||||
|
them for control. **Anti-detection ranking: this real logged-in Chrome (extension
|
||||||
|
connect) > a headed launched browser > headless (forbidden).** A genuine human
|
||||||
|
browser has no headless/automation tells at all, so prefer it for anything
|
||||||
|
anti-bot-sensitive.
|
||||||
|
|
||||||
## Two ways to drive a page — and when to drop to `eval`
|
## Two ways to drive a page — and when to drop to `eval`
|
||||||
|
|
||||||
You have a **real Chrome with the user's DOM**. Two layers, mix them freely:
|
You have a **real Chrome with the user's DOM**. Two layers, mix them freely:
|
||||||
@@ -506,7 +515,9 @@ and [references/authentication.md](references/authentication.md).
|
|||||||
```bash
|
```bash
|
||||||
--session <name> # isolated browser session
|
--session <name> # isolated browser session
|
||||||
--json # JSON output (for machine parsing)
|
--json # JSON output (for machine parsing)
|
||||||
--headed # show the window (default is headless)
|
--headed # default & always-on for stealth — headless is FORBIDDEN
|
||||||
|
# (a bot tell: creepjs flags ~33% headless vs 0% headed).
|
||||||
|
# Display-less servers only: AGENT_BROWSER_ALLOW_HEADLESS=1
|
||||||
--auto-connect # connect to an already-running Chrome
|
--auto-connect # connect to an already-running Chrome
|
||||||
--cdp <port> # connect to a specific CDP port
|
--cdp <port> # connect to a specific CDP port
|
||||||
--profile <name|path> # use a Chrome profile (login state survives)
|
--profile <name|path> # use a Chrome profile (login state survives)
|
||||||
|
|||||||
@@ -302,7 +302,8 @@ agent-browser state load auth.json # Restore saved state
|
|||||||
```bash
|
```bash
|
||||||
agent-browser --session <name> ... # Isolated browser session
|
agent-browser --session <name> ... # Isolated browser session
|
||||||
agent-browser --json ... # JSON output for parsing
|
agent-browser --json ... # JSON output for parsing
|
||||||
agent-browser --headed ... # Show browser window (not headless)
|
agent-browser --headed ... # Default & always-on (stealth). Headless is FORBIDDEN
|
||||||
|
# (bot tell); display-less servers: AGENT_BROWSER_ALLOW_HEADLESS=1
|
||||||
agent-browser --full ... # Full page screenshot (-f)
|
agent-browser --full ... # Full page screenshot (-f)
|
||||||
agent-browser --cdp <port> ... # Connect via Chrome DevTools Protocol
|
agent-browser --cdp <port> ... # Connect via Chrome DevTools Protocol
|
||||||
agent-browser -p <provider> ... # Cloud browser provider (--provider)
|
agent-browser -p <provider> ... # Cloud browser provider (--provider)
|
||||||
|
|||||||