From f9124b1ed3fb2466d321a8d0d4189bfc29f5921f Mon Sep 17 00:00:00 2001 From: davide Date: Sat, 18 Apr 2026 23:55:59 -0400 Subject: [PATCH] feat: stealth v2 -- proper PluginArray, deviceMemory, hardwareConcurrency Reshape navigator.plugins / navigator.mimeTypes so their constructors are PluginArray / MimeTypeArray rather than plain Array, and populate each Plugin entry with Plugin.prototype-based MimeType children. Also define navigator.deviceMemory (8) and navigator.hardwareConcurrency (8). These close the three remaining bot.sannysoft.com detection signals that the celeria sandbox hits on top of the stealth.1 baseline (Plugins is of type PluginArray, Plugins length, CHR_MEMORY). Bumps the fork tag to 0.26.0-celeria-stealth.2. Co-Authored-By: Claude Opus 4.7 (1M context) --- cli/Cargo.lock | 2 +- cli/Cargo.toml | 2 +- cli/src/native/stealth.rs | 92 +++++++++++++++++++++++++++++---- package.json | 2 +- packages/dashboard/package.json | 2 +- 5 files changed, 85 insertions(+), 15 deletions(-) diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 74a70a10a..461663f2c 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -45,7 +45,7 @@ dependencies = [ [[package]] name = "agent-browser" -version = "0.26.0-celeria-stealth.1" +version = "0.26.0-celeria-stealth.2" dependencies = [ "aes-gcm", "async-trait", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index b1c7ab7a2..19262fd7f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "agent-browser" -version = "0.26.0-celeria-stealth.1" +version = "0.26.0-celeria-stealth.2" edition = "2021" description = "Fast browser automation CLI for AI agents" license = "Apache-2.0" diff --git a/cli/src/native/stealth.rs b/cli/src/native/stealth.rs index e74737cff..4f51556ab 100644 --- a/cli/src/native/stealth.rs +++ b/cli/src/native/stealth.rs @@ -3,9 +3,12 @@ //! `AGENT_BROWSER_STEALTH=1` is enabled. //! //! The script masks the most common bot-detection signals shipped by stock -//! Chromium: `navigator.webdriver`, missing `chrome.runtime`, empty plugins, -//! identical `navigator.languages`, the WebGL vendor/renderer tuple, and the -//! permissions `Notification` mismatch. It is paired with the +//! Chromium: `navigator.webdriver`, missing `chrome.runtime`, empty / +//! plain-array `navigator.plugins` (bot.sannysoft checks +//! `navigator.plugins.constructor.name === 'PluginArray'`), missing +//! `deviceMemory`/`hardwareConcurrency`, identical `navigator.languages`, +//! the WebGL vendor/renderer tuple, and the permissions `Notification` +//! mismatch. It is paired with the //! `--disable-blink-features=AutomationControlled` launch arg in `chrome.rs` //! to also drop the corresponding header/feature signals. //! @@ -51,22 +54,73 @@ pub const STEALTH_INIT_SCRIPT: &str = r#" } catch (_) {} try { - const fakePlugins = [ - { name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' }, - { name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' }, - { name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' }, - { name: 'Microsoft Edge PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' }, - { name: 'WebKit built-in PDF', filename: 'internal-pdf-viewer', description: 'Portable Document Format' }, + const makeMimeType = (type, description, suffixes, enabledPlugin) => { + const mt = Object.create(MimeType.prototype); + Object.defineProperties(mt, { + type: { value: type, enumerable: true }, + description: { value: description, enumerable: true }, + suffixes: { value: suffixes, enumerable: true }, + enabledPlugin: { value: enabledPlugin, enumerable: true }, + }); + return mt; + }; + + const makePlugin = (name, filename, description, mimeSpecs) => { + const plugin = Object.create(Plugin.prototype); + Object.defineProperties(plugin, { + name: { value: name, enumerable: true }, + filename: { value: filename, enumerable: true }, + description: { value: description, enumerable: true }, + }); + const mimes = mimeSpecs.map((s) => makeMimeType(s.type, s.description, s.suffixes, plugin)); + mimes.forEach((m, i) => Object.defineProperty(plugin, i, { value: m, enumerable: true })); + mimes.forEach((m) => { if (!(m.type in plugin)) Object.defineProperty(plugin, m.type, { value: m }); }); + Object.defineProperty(plugin, 'length', { value: mimes.length }); + Object.defineProperty(plugin, 'item', { value: function (i) { return this[i] != null ? this[i] : null; } }); + Object.defineProperty(plugin, 'namedItem', { value: function (n) { return this[n] != null ? this[n] : null; } }); + return { plugin, mimes }; + }; + + const pdfSpecs = [ + { type: 'application/pdf', description: 'Portable Document Format', suffixes: 'pdf' }, + { type: 'text/pdf', description: 'Portable Document Format', suffixes: 'pdf' }, + ]; + + const built = [ + makePlugin('PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format', pdfSpecs), + makePlugin('Chrome PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format', pdfSpecs), + makePlugin('Chromium PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format', pdfSpecs), + makePlugin('Microsoft Edge PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format', pdfSpecs), + makePlugin('WebKit built-in PDF', 'internal-pdf-viewer', 'Portable Document Format', pdfSpecs), ]; + const plugins = built.map((b) => b.plugin); + const allMimes = []; + built.forEach((b) => b.mimes.forEach((m) => { if (!allMimes.some((x) => x.type === m.type)) allMimes.push(m); })); + + const pluginArray = Object.create(PluginArray.prototype); + plugins.forEach((p, i) => Object.defineProperty(pluginArray, i, { value: p, enumerable: true })); + plugins.forEach((p) => { if (!(p.name in pluginArray)) Object.defineProperty(pluginArray, p.name, { value: p }); }); + Object.defineProperty(pluginArray, 'length', { value: plugins.length, enumerable: true }); + Object.defineProperty(pluginArray, 'item', { value: function (i) { return this[i] != null ? this[i] : null; } }); + Object.defineProperty(pluginArray, 'namedItem', { value: function (n) { return this[n] != null ? this[n] : null; } }); + Object.defineProperty(pluginArray, 'refresh', { value: function () {} }); + + const mimeTypeArray = Object.create(MimeTypeArray.prototype); + allMimes.forEach((m, i) => Object.defineProperty(mimeTypeArray, i, { value: m, enumerable: true })); + allMimes.forEach((m) => { if (!(m.type in mimeTypeArray)) Object.defineProperty(mimeTypeArray, m.type, { value: m }); }); + Object.defineProperty(mimeTypeArray, 'length', { value: allMimes.length, enumerable: true }); + Object.defineProperty(mimeTypeArray, 'item', { value: function (i) { return this[i] != null ? this[i] : null; } }); + Object.defineProperty(mimeTypeArray, 'namedItem', { value: function (n) { return this[n] != null ? this[n] : null; } }); + Object.defineProperty(Navigator.prototype, 'plugins', { configurable: true, enumerable: true, - get: () => fakePlugins, + get: () => pluginArray, }); Object.defineProperty(Navigator.prototype, 'mimeTypes', { configurable: true, enumerable: true, - get: () => [{ type: 'application/pdf', suffixes: 'pdf', description: '' }], + get: () => mimeTypeArray, }); } catch (_) {} @@ -78,6 +132,22 @@ pub const STEALTH_INIT_SCRIPT: &str = r#" }); } catch (_) {} + try { + Object.defineProperty(Navigator.prototype, 'deviceMemory', { + configurable: true, + enumerable: true, + get: () => 8, + }); + } catch (_) {} + + try { + Object.defineProperty(Navigator.prototype, 'hardwareConcurrency', { + configurable: true, + enumerable: true, + get: () => 8, + }); + } catch (_) {} + try { const patchGetParameter = (proto) => { if (!proto || !proto.getParameter) return; diff --git a/package.json b/package.json index 216dee6d4..1ca55e938 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-browser", - "version": "0.26.0-celeria-stealth.1", + "version": "0.26.0-celeria-stealth.2", "description": "Browser automation CLI for AI agents", "type": "module", "files": [ diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index b316650a2..8dafd0dbd 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "dashboard", - "version": "0.26.0-celeria-stealth.1", + "version": "0.26.0-celeria-stealth.2", "private": true, "scripts": { "dev": "next dev",