Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1fbdaadeb | ||
|
|
839aaa5586 | ||
|
|
a7f9c24fdb | ||
|
|
42ade7b4e8 |
@@ -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.27"
|
version = "0.27.0-fork.30"
|
||||||
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.27"
|
version = "0.27.0-fork.30"
|
||||||
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"
|
||||||
|
|||||||
@@ -773,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 {
|
||||||
@@ -850,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> {
|
||||||
@@ -2200,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,31 @@
|
|||||||
const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLContextFallback: false, hideCanvas: false, canvasSeed: 0 };
|
const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLContextFallback: false, hideCanvas: false, canvasSeed: 0 };
|
||||||
|
// Redefine a navigator property on its PROTOTYPE (Navigator / WorkerNavigator),
|
||||||
|
// the way real Chrome exposes these — as prototype getters, NOT instance own
|
||||||
|
// properties. Adding an own property to the `navigator` instance is itself a
|
||||||
|
// detectable automation tell: real Chrome's `Object.getOwnPropertyNames(navigator)`
|
||||||
|
// is empty, so any name we leave on the instance is caught by rebrowser's
|
||||||
|
// `navigatorWebdriver` probe and similar checks. We mirror the proven `vendor`
|
||||||
|
// patch below: define on the prototype, native-mask the getter's toString, then
|
||||||
|
// delete any instance shadow. Falls back to an instance define only if the
|
||||||
|
// prototype is locked. (A top-level `const` like this is script-scoped, not a
|
||||||
|
// `window` property, so it does not leak — same as `__abStealth` above.)
|
||||||
|
const __abRedefineNavProto = (name, getterImpl) => {
|
||||||
|
try {
|
||||||
|
const proto = Object.getPrototypeOf(navigator);
|
||||||
|
const nativeGet = Object.getOwnPropertyDescriptor(proto, name) && Object.getOwnPropertyDescriptor(proto, name).get;
|
||||||
|
const getter = function () { return getterImpl(); };
|
||||||
|
if (nativeGet) {
|
||||||
|
Object.defineProperty(getter, 'name', { value: 'get ' + name, configurable: true });
|
||||||
|
Object.defineProperty(getter, 'toString', { value: () => nativeGet.toString(), configurable: true, writable: true });
|
||||||
|
}
|
||||||
|
Object.defineProperty(proto, name, { get: getter, configurable: true, enumerable: true });
|
||||||
|
try { delete navigator[name]; } catch (e) {}
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
try { Object.defineProperty(navigator, name, { get: () => getterImpl(), configurable: true }); } catch (e2) {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
(function(){
|
(function(){
|
||||||
// Prefer the CDP-level automation override (Emulation.setAutomationOverride),
|
// Prefer the CDP-level automation override (Emulation.setAutomationOverride),
|
||||||
// which makes navigator.webdriver report `false` NATIVELY — undetectable by
|
// which makes navigator.webdriver report `false` NATIVELY — undetectable by
|
||||||
@@ -354,18 +381,8 @@ const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLCon
|
|||||||
const config = (typeof __abStealth === 'object' && __abStealth) ? __abStealth : null;
|
const config = (typeof __abStealth === 'object' && __abStealth) ? __abStealth : null;
|
||||||
if (!config || !Array.isArray(config.languages) || config.languages.length === 0) return;
|
if (!config || !Array.isArray(config.languages) || config.languages.length === 0) return;
|
||||||
const locale = typeof config.locale === 'string' ? config.locale : config.languages[0];
|
const locale = typeof config.locale === 'string' ? config.locale : config.languages[0];
|
||||||
try {
|
__abRedefineNavProto('language', () => locale);
|
||||||
Object.defineProperty(navigator, 'language', {
|
__abRedefineNavProto('languages', () => config.languages.slice());
|
||||||
get: () => locale,
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
} catch {}
|
|
||||||
try {
|
|
||||||
Object.defineProperty(navigator, 'languages', {
|
|
||||||
get: () => config.languages.slice(),
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
} catch {}
|
|
||||||
})();
|
})();
|
||||||
(function(){
|
(function(){
|
||||||
const ua = String(navigator.userAgent || '');
|
const ua = String(navigator.userAgent || '');
|
||||||
@@ -394,6 +411,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 +462,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', {
|
||||||
@@ -1023,10 +1072,15 @@ const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLCon
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (defineContacts(navigator)) return;
|
// Prototype-first (like the vendor patch): real Chrome exposes navigator
|
||||||
try {
|
// members on the prototype, not as instance own properties. Define on the
|
||||||
defineContacts(Object.getPrototypeOf(navigator));
|
// prototype and remove any instance shadow so Object.getOwnPropertyNames(navigator)
|
||||||
} catch {}
|
// stays empty; fall back to the instance only if the prototype is locked.
|
||||||
|
if (defineContacts(Object.getPrototypeOf(navigator))) {
|
||||||
|
try { delete navigator.contacts; } catch {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
defineContacts(navigator);
|
||||||
})();
|
})();
|
||||||
(function(){
|
(function(){
|
||||||
const ContentIndexCtor = typeof ContentIndex === 'function'
|
const ContentIndexCtor = typeof ContentIndex === 'function'
|
||||||
@@ -1233,12 +1287,7 @@ const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLCon
|
|||||||
}
|
}
|
||||||
return values;
|
return values;
|
||||||
};
|
};
|
||||||
try {
|
__abRedefineNavProto('userAgentData', () => patched);
|
||||||
Object.defineProperty(navigator, 'userAgentData', {
|
|
||||||
get: () => patched,
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
} catch {}
|
|
||||||
})();
|
})();
|
||||||
(function(){
|
(function(){
|
||||||
const ua = navigator.userAgent;
|
const ua = navigator.userAgent;
|
||||||
|
|||||||
|
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.27",
|
"version": "0.27.0-fork.30",
|
||||||
"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)
|
||||||
|
|||||||