Update readme with stealth FAQ
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
# agent-browser
|
||||
|
||||
## 0.15.1-fork.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Auto-attach existing browser more reliably by trying CDP localhost:9333 first, then falling back to auto-discovery before failing.
|
||||
|
||||
Align daemon behavior and user-facing docs/skill guidance with the same attachment policy.
|
||||
|
||||
## 0.15.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -15,6 +15,24 @@ This README focuses on stealth architecture and principles. For full command cov
|
||||
- Region signals are auto-aligned (locale/timezone/Accept-Language) to reduce mismatch risk.
|
||||
- Verification/captcha handling is policy-driven (`--risk-mode off|warn|block`).
|
||||
|
||||
## FAQ: `agent-browser` vs `agent-browser-stealth`
|
||||
|
||||
People often ask this: "What's the anti-detection approach compared to `agent-browser-stealth` on npm?"
|
||||
|
||||
- `agent-browser-stealth` on npm is the package name for this fork.
|
||||
- The CLI keeps upstream-compatible command names (`agent-browser` is still the main executable, with `agent-browser-stealth` as an alias).
|
||||
- The practical difference vs upstream `agent-browser` is not one single "stealth switch"; it is a defense-in-depth stack designed for anti-bot pressure.
|
||||
|
||||
The core idea is layered hardening across the full automation lifecycle:
|
||||
|
||||
1. Connection-aware policy: choose the best available stealth capability by mode (local launch/CDP/cloud provider).
|
||||
2. Fingerprint hardening: patch launch args, CDP metadata, and init-script surfaces before page code runs.
|
||||
3. Behavioral humanization: non-uniform typing/mouse/wait patterns instead of perfectly mechanical actions.
|
||||
4. Region coherence: auto-align locale/timezone/language signals to target geography.
|
||||
5. Risk-aware control loop: detect verification/captcha signals and handle them with explicit `risk-mode` policy.
|
||||
|
||||
Goal: reduce detection probability and improve stability in production automation. Non-goal: "guaranteed bypass" on every target.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Install
|
||||
@@ -50,12 +68,12 @@ flowchart TD
|
||||
|
||||
### Policy by Connection Mode
|
||||
|
||||
| Mode | Stealth Capabilities | Notes |
|
||||
|---|---|---|
|
||||
| Local Chromium launch | Chromium launch args + CDP UA override + context init scripts | Most complete stack |
|
||||
| Existing browser via CDP | CDP UA override + context init scripts | No local Chromium arg injection |
|
||||
| Cloud provider (browserbase/browseruse) | Context init scripts | Remote browser runtime controls launch layer |
|
||||
| Kernel provider | Context init scripts + provider-managed stealth | Provider-side stealth may also apply |
|
||||
| Mode | Stealth Capabilities | Notes |
|
||||
| --------------------------------------- | ------------------------------------------------------------- | -------------------------------------------- |
|
||||
| Local Chromium launch | Chromium launch args + CDP UA override + context init scripts | Most complete stack |
|
||||
| Existing browser via CDP | CDP UA override + context init scripts | No local Chromium arg injection |
|
||||
| Cloud provider (browserbase/browseruse) | Context init scripts | Remote browser runtime controls launch layer |
|
||||
| Kernel provider | Context init scripts + provider-managed stealth | Provider-side stealth may also apply |
|
||||
|
||||
## Principle 1: Always-On Stealth with Explicit Boundaries
|
||||
|
||||
@@ -63,7 +81,7 @@ flowchart TD
|
||||
- Project policy forbids:
|
||||
- `--profile` / `AGENT_BROWSER_PROFILE`
|
||||
- `--channel` / `AGENT_BROWSER_CHANNEL`
|
||||
- Default CLI policy expects an existing browser on CDP `localhost:9333` unless explicit connection options are provided.
|
||||
- Default CLI policy auto-attaches an existing browser: try CDP `localhost:9333` first, then auto-discovery unless explicit connection options are provided.
|
||||
|
||||
## Principle 2: Multi-Layer Fingerprint Hardening
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -4,7 +4,7 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "agent-browser-stealth"
|
||||
version = "0.15.1-fork.7"
|
||||
version = "0.15.1-fork.11"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"dirs",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "agent-browser-stealth"
|
||||
version = "0.15.1-fork.7"
|
||||
version = "0.15.1-fork.11"
|
||||
edition = "2021"
|
||||
description = "Stealth browser automation CLI for AI agents with anti-bot evasions"
|
||||
license = "Apache-2.0"
|
||||
|
||||
+28
-7
@@ -399,6 +399,8 @@ fn main() {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let mut attached_to_existing_browser = false;
|
||||
|
||||
// Auto-connect to existing browser
|
||||
if flags.auto_connect {
|
||||
let mut launch_cmd = json!({
|
||||
@@ -436,6 +438,8 @@ fn main() {
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
attached_to_existing_browser = true;
|
||||
}
|
||||
|
||||
// Connect via CDP if --cdp flag is set
|
||||
@@ -526,6 +530,8 @@ fn main() {
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
attached_to_existing_browser = true;
|
||||
}
|
||||
|
||||
// Launch with cloud provider if -p flag is set
|
||||
@@ -567,8 +573,8 @@ fn main() {
|
||||
}
|
||||
|
||||
// Project policy: when no explicit connection mode is provided,
|
||||
// commands must attach to an existing browser on CDP :9333.
|
||||
// If unavailable, fail fast instead of launching a managed browser.
|
||||
// commands should attach to an existing browser.
|
||||
// Try CDP :9333 first, then fall back to auto-connect discovery.
|
||||
let can_try_default_cdp = flags.cdp.is_none()
|
||||
&& !flags.auto_connect
|
||||
&& flags.provider.is_none()
|
||||
@@ -581,7 +587,6 @@ fn main() {
|
||||
&& !flags.allow_file_access
|
||||
&& flags.extensions.is_empty();
|
||||
|
||||
let mut launched_via_default_cdp = false;
|
||||
if can_try_default_cdp {
|
||||
let mut launch_cmd = json!({
|
||||
"id": gen_id(),
|
||||
@@ -594,11 +599,27 @@ fn main() {
|
||||
}
|
||||
|
||||
if let Ok(resp) = send_command(launch_cmd, &flags.session) {
|
||||
launched_via_default_cdp = resp.success;
|
||||
attached_to_existing_browser = resp.success;
|
||||
}
|
||||
|
||||
if !attached_to_existing_browser {
|
||||
let mut auto_connect_cmd = json!({
|
||||
"id": gen_id(),
|
||||
"action": "launch",
|
||||
"autoConnect": true
|
||||
});
|
||||
|
||||
if let Some(ref cs) = flags.color_scheme {
|
||||
auto_connect_cmd["colorScheme"] = json!(cs);
|
||||
}
|
||||
|
||||
if let Ok(resp) = send_command(auto_connect_cmd, &flags.session) {
|
||||
attached_to_existing_browser = resp.success;
|
||||
}
|
||||
}
|
||||
}
|
||||
if can_try_default_cdp && !launched_via_default_cdp {
|
||||
let msg = "Project policy requires using your existing browser. Could not connect to CDP at localhost:9333. Start your browser with remote debugging on port 9333, or pass --cdp <port|url>.";
|
||||
if can_try_default_cdp && !attached_to_existing_browser {
|
||||
let msg = "Project policy requires using your existing browser. Could not connect to CDP at localhost:9333 and auto-discovery also failed. Start Chrome with remote debugging (for example, --remote-debugging-port=9333), or pass --cdp <port|url>.";
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
@@ -621,7 +642,7 @@ fn main() {
|
||||
|| flags.download_path.is_some())
|
||||
&& flags.cdp.is_none()
|
||||
&& flags.provider.is_none()
|
||||
&& !launched_via_default_cdp
|
||||
&& !attached_to_existing_browser
|
||||
{
|
||||
let mut launch_cmd = json!({
|
||||
"id": gen_id(),
|
||||
|
||||
+2
-2
@@ -2398,7 +2398,7 @@ Options:
|
||||
--headed Show browser window (not headless)
|
||||
--cdp <port> Connect via CDP (Chrome DevTools Protocol)
|
||||
--auto-connect Auto-discover and connect to running Chrome
|
||||
Project default: require existing browser at localhost:9333 (no auto local fallback)
|
||||
Project default: try localhost:9333 first, then auto-discovery (no managed local-launch fallback)
|
||||
--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)
|
||||
--risk-mode <mode> Verify/captcha handling: off, warn, block (or AGENT_BROWSER_RISK_MODE)
|
||||
@@ -2416,7 +2416,7 @@ Options:
|
||||
Policy:
|
||||
--profile / AGENT_BROWSER_PROFILE are forbidden
|
||||
--channel / AGENT_BROWSER_CHANNEL are forbidden
|
||||
Use existing browser session (CDP localhost:9333) or pass --cdp explicitly
|
||||
Auto-attach existing browser (prefer CDP localhost:9333, then auto-discovery), or pass --cdp explicitly
|
||||
|
||||
Configuration:
|
||||
agent-browser looks for agent-browser.json in these locations (lowest to highest priority):
|
||||
|
||||
+130
-25
@@ -1,12 +1,12 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
import { pageMetadata } from '@/lib/page-metadata';
|
||||
|
||||
export const metadata = pageMetadata("cdp-mode")
|
||||
export const metadata = pageMetadata('cdp-mode');
|
||||
|
||||
# CDP Mode
|
||||
|
||||
Connect to an existing browser via Chrome DevTools Protocol:
|
||||
|
||||
Default behavior in this fork: when `--cdp` is omitted, agent-browser requires an existing browser at `localhost:9333`. If CDP is unavailable, the command fails fast (no local-launch fallback).
|
||||
Default behavior in this fork: when `--cdp` is omitted, agent-browser auto-attaches to an existing browser by trying `localhost:9333` first, then auto-discovery. If both fail, the command exits (no managed local-launch fallback).
|
||||
|
||||
Project policy:
|
||||
|
||||
@@ -88,12 +88,24 @@ AGENT_BROWSER_COLOR_SCHEME=dark agent-browser --cdp 9222 open https://example.co
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Connection type</th><th>Stealth capabilities</th></tr>
|
||||
<tr>
|
||||
<th>Connection type</th>
|
||||
<th>Stealth capabilities</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Local launch</td><td>Chromium launch args + context init scripts</td></tr>
|
||||
<tr><td>CDP / auto-connect</td><td>Context init scripts</td></tr>
|
||||
<tr><td>Cloud providers</td><td>Context init scripts (Kernel may also apply provider-managed stealth)</td></tr>
|
||||
<tr>
|
||||
<td>Local launch</td>
|
||||
<td>Chromium launch args + context init scripts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CDP / auto-connect</td>
|
||||
<td>Context init scripts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cloud providers</td>
|
||||
<td>Context init scripts (Kernel may also apply provider-managed stealth)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -113,26 +125,119 @@ This enables control of:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Option</th><th>Description</th></tr>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>--session <name></code></td><td>Use isolated session</td></tr>
|
||||
<tr><td><code>-p <provider></code></td><td>Cloud browser provider (<code>browserbase</code>, <code>browseruse</code>, <code>kernel</code>)</td></tr>
|
||||
<tr><td><code>--headers <json></code></td><td>HTTP headers scoped to origin</td></tr>
|
||||
<tr><td><code>--executable-path</code></td><td>Custom browser executable</td></tr>
|
||||
<tr><td><code>--args <args></code></td><td>Browser launch args (comma-separated)</td></tr>
|
||||
<tr><td><code>--user-agent <ua></code></td><td>Custom User-Agent string</td></tr>
|
||||
<tr><td><code>--proxy <url></code></td><td>Proxy server URL</td></tr>
|
||||
<tr><td><code>--proxy-bypass <hosts></code></td><td>Hosts to bypass proxy</td></tr>
|
||||
<tr><td><code>--json</code></td><td>JSON output for scripts</td></tr>
|
||||
<tr><td><code>--full, -f</code></td><td>Full page screenshot</td></tr>
|
||||
<tr><td><code>--name, -n</code></td><td>Locator name filter</td></tr>
|
||||
<tr><td><code>--exact</code></td><td>Exact text match</td></tr>
|
||||
<tr><td><code>--headed</code></td><td>Show browser window</td></tr>
|
||||
<tr><td><code>{"--cdp <port|url>"}</code></td><td>CDP connection (port or WebSocket URL)</td></tr>
|
||||
<tr><td><code>--auto-connect</code></td><td>Auto-discover and connect to running Chrome</td></tr>
|
||||
<tr><td><code>--color-scheme <scheme></code></td><td>Persistent color scheme (<code>dark</code>, <code>light</code>, <code>no-preference</code>)</td></tr>
|
||||
<tr><td><code>--debug</code></td><td>Debug output</td></tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--session <name></code>
|
||||
</td>
|
||||
<td>Use isolated session</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>-p <provider></code>
|
||||
</td>
|
||||
<td>
|
||||
Cloud browser provider (<code>browserbase</code>, <code>browseruse</code>,{' '}
|
||||
<code>kernel</code>)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--headers <json></code>
|
||||
</td>
|
||||
<td>HTTP headers scoped to origin</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--executable-path</code>
|
||||
</td>
|
||||
<td>Custom browser executable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--args <args></code>
|
||||
</td>
|
||||
<td>Browser launch args (comma-separated)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--user-agent <ua></code>
|
||||
</td>
|
||||
<td>Custom User-Agent string</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--proxy <url></code>
|
||||
</td>
|
||||
<td>Proxy server URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--proxy-bypass <hosts></code>
|
||||
</td>
|
||||
<td>Hosts to bypass proxy</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--json</code>
|
||||
</td>
|
||||
<td>JSON output for scripts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--full, -f</code>
|
||||
</td>
|
||||
<td>Full page screenshot</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--name, -n</code>
|
||||
</td>
|
||||
<td>Locator name filter</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--exact</code>
|
||||
</td>
|
||||
<td>Exact text match</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--headed</code>
|
||||
</td>
|
||||
<td>Show browser window</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>{'--cdp <port|url>'}</code>
|
||||
</td>
|
||||
<td>CDP connection (port or WebSocket URL)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--auto-connect</code>
|
||||
</td>
|
||||
<td>Auto-discover and connect to running Chrome</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--color-scheme <scheme></code>
|
||||
</td>
|
||||
<td>
|
||||
Persistent color scheme (<code>dark</code>, <code>light</code>, <code>no-preference</code>)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>--debug</code>
|
||||
</td>
|
||||
<td>Debug output</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export const metadata = pageMetadata('configuration');
|
||||
|
||||
Create an `agent-browser.json` file to set persistent defaults instead of repeating flags on every command.
|
||||
|
||||
In this fork, default launch behavior requires a resident browser at `localhost:9333` (CDP). If unavailable, commands fail fast instead of launching a managed browser.
|
||||
In this fork, default launch behavior auto-attaches to an existing browser by trying `localhost:9333` (CDP) first, then auto-discovery. If both fail, commands exit instead of launching a managed browser.
|
||||
|
||||
## Config File Locations
|
||||
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agent-browser-stealth",
|
||||
"version": "0.15.1-fork.7",
|
||||
"version": "0.15.1-fork.11",
|
||||
"description": "Stealth browser automation CLI for AI agents with anti-bot evasions",
|
||||
"type": "module",
|
||||
"main": "dist/daemon.js",
|
||||
@@ -35,12 +35,13 @@
|
||||
"test:watch": "vitest",
|
||||
"test:e2e:dogfood": "vitest run test/e2e/dogfood.eval.ts",
|
||||
"postinstall": "node scripts/postinstall.js",
|
||||
"verify:native-version": "node scripts/verify-native-version.js",
|
||||
"clawhub:sync": "bash scripts/clawhub-sync.sh",
|
||||
"sync:upstream": "bash scripts/sync-upstream.sh",
|
||||
"sync:upstream:push": "bash scripts/sync-upstream.sh --push",
|
||||
"changeset": "changeset",
|
||||
"ci:version": "changeset version && pnpm run version:sync && pnpm install --no-frozen-lockfile",
|
||||
"ci:publish": "pnpm run version:sync && pnpm run build && changeset publish"
|
||||
"ci:publish": "pnpm run version:sync && pnpm run build && pnpm run build:native && pnpm run verify:native-version && changeset publish"
|
||||
},
|
||||
"keywords": [
|
||||
"browser",
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Verifies that the bundled native binary version matches package.json version.
|
||||
* This prevents publishing npm tarballs where package version and native binary
|
||||
* version drift (e.g. package is fork.8 but binary still reports fork.7).
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { arch, platform } from 'os';
|
||||
import { execFileSync } from 'child_process';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = join(__dirname, '..');
|
||||
|
||||
const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
|
||||
const expectedVersion = pkg.version;
|
||||
|
||||
const ext = platform() === 'win32' ? '.exe' : '';
|
||||
const platformBinary = join(projectRoot, 'bin', `agent-browser-${platform()}-${arch()}${ext}`);
|
||||
|
||||
if (!existsSync(platformBinary)) {
|
||||
console.error(`Error: native binary not found for current platform: ${platformBinary}`);
|
||||
console.error('Run `pnpm run build:native` before publishing.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let versionOutput = '';
|
||||
try {
|
||||
versionOutput = execFileSync(platformBinary, ['--version'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
}).trim();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Error: failed to execute native binary --version: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!versionOutput.includes(expectedVersion)) {
|
||||
console.error(`Version mismatch: package.json=${expectedVersion}, native='${versionOutput}'.`);
|
||||
console.error('Run `pnpm run build:native` and retry publishing.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`✓ Native binary version matches package.json (${expectedVersion})`);
|
||||
@@ -23,7 +23,7 @@ agent-browser install
|
||||
agent-browser --version
|
||||
```
|
||||
|
||||
If default CDP mode is used in your environment, ensure a browser is available at `localhost:9333`, or pass `--cdp` / `--auto-connect` explicitly.
|
||||
If default CDP mode is used in your environment, the CLI first tries `localhost:9333` and then auto-discovery. You can still pass `--cdp` / `--auto-connect` explicitly when needed.
|
||||
|
||||
## Standard execution workflow
|
||||
|
||||
|
||||
@@ -216,7 +216,11 @@ agent-browser session list
|
||||
|
||||
### Connect to Existing Chrome
|
||||
|
||||
By default in this fork, commands without `--cdp` require an existing browser at `localhost:9333`. If CDP is unavailable, the command fails fast (no automatic local browser launch).
|
||||
By default in this fork, commands without `--cdp` auto-attach to your existing browser with this order:
|
||||
|
||||
1. Try CDP at `localhost:9333`
|
||||
2. If unavailable, fall back to `--auto-connect`-style discovery
|
||||
3. If both fail, exit with guidance (no automatic managed local browser launch on this path)
|
||||
|
||||
```bash
|
||||
# Auto-discover running Chrome with remote debugging enabled
|
||||
@@ -225,6 +229,9 @@ agent-browser --auto-connect snapshot
|
||||
|
||||
# Or with explicit CDP port
|
||||
agent-browser --cdp 9222 snapshot
|
||||
|
||||
# Debug auto-attach behavior
|
||||
agent-browser --debug snapshot
|
||||
```
|
||||
|
||||
### Color Scheme (Dark Mode)
|
||||
@@ -263,7 +270,7 @@ agent-browser screenshot output.png
|
||||
|
||||
- `--profile` / `AGENT_BROWSER_PROFILE` are forbidden
|
||||
- `--channel` / `AGENT_BROWSER_CHANNEL` are forbidden
|
||||
- Use existing browser sessions (default CDP `localhost:9333`) or pass `--cdp` explicitly
|
||||
- Use existing browser sessions (default attach path: CDP `localhost:9333` then auto-discovery) or pass `--cdp` explicitly
|
||||
|
||||
### Stealth Mode (Always On)
|
||||
|
||||
@@ -357,8 +364,9 @@ export AGENT_BROWSER_ACTION_POLICY=./policy.json
|
||||
```
|
||||
|
||||
Example `policy.json`:
|
||||
|
||||
```json
|
||||
{"default": "deny", "allow": ["navigate", "snapshot", "click", "scroll", "wait", "get"]}
|
||||
{ "default": "deny", "allow": ["navigate", "snapshot", "click", "scroll", "wait", "get"] }
|
||||
```
|
||||
|
||||
Auth vault operations (`auth login`, etc.) bypass action policy but domain allowlist still applies.
|
||||
|
||||
@@ -54,6 +54,56 @@ describe('BrowserManager', () => {
|
||||
await newBrowser.close();
|
||||
});
|
||||
|
||||
it('should switch from local session when auto-connect is explicitly requested', async () => {
|
||||
const testBrowser = new BrowserManager();
|
||||
await testBrowser.launch({ id: 'test', action: 'launch', headless: true });
|
||||
|
||||
const closeSpy = vi.spyOn(testBrowser, 'close');
|
||||
const autoConnectSpy = vi
|
||||
.spyOn(testBrowser as any, 'autoConnectViaCDP')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
await testBrowser.launch({ id: 'test', action: 'launch', autoConnect: true });
|
||||
|
||||
expect(closeSpy).toHaveBeenCalledTimes(1);
|
||||
expect(autoConnectSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
autoConnectSpy.mockRestore();
|
||||
closeSpy.mockRestore();
|
||||
await testBrowser.close();
|
||||
});
|
||||
|
||||
it('should not relaunch when already connected via healthy CDP and auto-connect is requested', async () => {
|
||||
const addInitScript = vi.fn().mockResolvedValue(undefined);
|
||||
const mockPage = { url: () => 'http://example.com', on: vi.fn(), isClosed: () => false };
|
||||
const mockContext = {
|
||||
pages: () => [mockPage],
|
||||
on: vi.fn(),
|
||||
setDefaultTimeout: vi.fn(),
|
||||
addInitScript,
|
||||
};
|
||||
const mockBrowser = {
|
||||
contexts: () => [mockContext],
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
isConnected: vi.fn(() => true),
|
||||
};
|
||||
const connectSpy = vi.spyOn(chromium, 'connectOverCDP').mockResolvedValue(mockBrowser as any);
|
||||
|
||||
const cdpBrowser = new BrowserManager();
|
||||
await cdpBrowser.launch({ id: 'test', action: 'launch', cdpPort: 9222 });
|
||||
expect(connectSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
const closeSpy = vi.spyOn(cdpBrowser, 'close');
|
||||
await cdpBrowser.launch({ id: 'test', action: 'launch', autoConnect: true });
|
||||
|
||||
expect(closeSpy).not.toHaveBeenCalled();
|
||||
expect(connectSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
closeSpy.mockRestore();
|
||||
await cdpBrowser.close();
|
||||
connectSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should report local stealth policy capabilities', async () => {
|
||||
const testBrowser = new BrowserManager();
|
||||
await testBrowser.launch({ headless: true });
|
||||
@@ -97,6 +147,90 @@ describe('BrowserManager', () => {
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('should reject CDP endpoints with only blank pages when meaningful tabs are required', async () => {
|
||||
const mockPage = { url: () => 'about:blank', on: vi.fn(), isClosed: () => false };
|
||||
const mockContext = {
|
||||
pages: () => [mockPage],
|
||||
on: vi.fn(),
|
||||
setDefaultTimeout: vi.fn(),
|
||||
addInitScript: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const mockBrowser = {
|
||||
contexts: () => [mockContext],
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
isConnected: vi.fn(() => true),
|
||||
};
|
||||
const connectSpy = vi.spyOn(chromium, 'connectOverCDP').mockResolvedValue(mockBrowser as any);
|
||||
|
||||
const cdpBrowser = new BrowserManager();
|
||||
await expect(
|
||||
(cdpBrowser as any).connectViaCDP('9222', {
|
||||
allowCreatePageFallback: false,
|
||||
requireMeaningfulPage: true,
|
||||
})
|
||||
).rejects.toThrow('No existing user tabs found on this CDP endpoint.');
|
||||
|
||||
expect(mockBrowser.close).toHaveBeenCalledTimes(1);
|
||||
connectSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should skip auto-connect candidates without user tabs and continue discovery', async () => {
|
||||
const cdpBrowser = new BrowserManager();
|
||||
const dirsSpy = vi
|
||||
.spyOn(cdpBrowser as any, 'getChromeUserDataDirs')
|
||||
.mockReturnValue(['/tmp/chrome-a', '/tmp/chrome-b']);
|
||||
const activePortSpy = vi.spyOn(cdpBrowser as any, 'readDevToolsActivePort');
|
||||
activePortSpy
|
||||
.mockReturnValueOnce({ port: 9222, wsPath: '/devtools/browser/a' })
|
||||
.mockReturnValueOnce({ port: 9333, wsPath: '/devtools/browser/b' });
|
||||
const probeSpy = vi.spyOn(cdpBrowser as any, 'probeDebugPort');
|
||||
probeSpy
|
||||
.mockResolvedValueOnce('ws://127.0.0.1:9222/devtools/browser/a')
|
||||
.mockResolvedValueOnce('ws://127.0.0.1:9333/devtools/browser/b');
|
||||
const connectViaCDPSpy = vi.spyOn(cdpBrowser as any, 'connectViaCDP');
|
||||
connectViaCDPSpy
|
||||
.mockRejectedValueOnce(new Error('No existing user tabs found on this CDP endpoint.'))
|
||||
.mockResolvedValueOnce(undefined);
|
||||
|
||||
await (cdpBrowser as any).autoConnectViaCDP();
|
||||
|
||||
expect(connectViaCDPSpy).toHaveBeenCalledTimes(2);
|
||||
expect(connectViaCDPSpy.mock.calls[0][1]).toMatchObject({
|
||||
allowCreatePageFallback: false,
|
||||
requireMeaningfulPage: true,
|
||||
});
|
||||
expect(connectViaCDPSpy.mock.calls[1][1]).toMatchObject({
|
||||
allowCreatePageFallback: false,
|
||||
requireMeaningfulPage: true,
|
||||
});
|
||||
|
||||
dirsSpy.mockRestore();
|
||||
activePortSpy.mockRestore();
|
||||
probeSpy.mockRestore();
|
||||
connectViaCDPSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should prefer port 9333 before DevToolsActivePort discovery in auto-connect', async () => {
|
||||
const cdpBrowser = new BrowserManager();
|
||||
const probeSpy = vi.spyOn(cdpBrowser as any, 'probeDebugPort');
|
||||
probeSpy.mockResolvedValueOnce('ws://127.0.0.1:9333/devtools/browser/preferred');
|
||||
const connectViaCDPSpy = vi
|
||||
.spyOn(cdpBrowser as any, 'connectViaCDP')
|
||||
.mockResolvedValue(undefined);
|
||||
const dirsSpy = vi.spyOn(cdpBrowser as any, 'getChromeUserDataDirs');
|
||||
|
||||
await (cdpBrowser as any).autoConnectViaCDP();
|
||||
|
||||
expect(probeSpy).toHaveBeenCalledWith(9333);
|
||||
expect(connectViaCDPSpy).toHaveBeenCalledTimes(1);
|
||||
expect(connectViaCDPSpy.mock.calls[0][0]).toContain('9333');
|
||||
expect(dirsSpy).not.toHaveBeenCalled();
|
||||
|
||||
probeSpy.mockRestore();
|
||||
connectViaCDPSpy.mockRestore();
|
||||
dirsSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should ignore legacy stealth=false and keep CDP stealth capabilities enabled', async () => {
|
||||
const addInitScript = vi.fn().mockResolvedValue(undefined);
|
||||
const mockPage = { url: () => 'http://example.com', on: vi.fn(), isClosed: () => false };
|
||||
|
||||
+124
-8
@@ -671,6 +671,16 @@ export class BrowserManager {
|
||||
return !this.isIgnoredCDPPageUrl(url);
|
||||
}
|
||||
|
||||
private isMeaningfulCDPPage(page: Page): boolean {
|
||||
if (page.isClosed()) return false;
|
||||
const url = this.getSafePageUrl(page).trim().toLowerCase();
|
||||
if (!url) return false;
|
||||
if (url === 'about:blank' || url.startsWith('about:blank#')) return false;
|
||||
if (url === 'chrome://newtab/' || url.startsWith('chrome://newtab')) return false;
|
||||
if (url === 'chrome://new-tab-page/' || url.startsWith('chrome://new-tab-page')) return false;
|
||||
return !this.isIgnoredCDPPageUrl(url);
|
||||
}
|
||||
|
||||
private collectUsableCDPPages(contexts: BrowserContext[]): Page[] {
|
||||
return contexts
|
||||
.flatMap((context) => context.pages())
|
||||
@@ -1594,7 +1604,13 @@ export class BrowserManager {
|
||||
}
|
||||
|
||||
if (this.isLaunched()) {
|
||||
// Explicit --auto-connect should switch away from managed/local/provider sessions
|
||||
// so commands always target a discovered user browser.
|
||||
const shouldSwitchToAutoConnect =
|
||||
!!options.autoConnect &&
|
||||
(this.cdpEndpoint === null || this.stealthConnectionKind !== 'cdp');
|
||||
const needsRelaunch =
|
||||
shouldSwitchToAutoConnect ||
|
||||
(!cdpEndpoint && !options.autoConnect && this.cdpEndpoint !== null) ||
|
||||
(!!cdpEndpoint && this.needsCdpReconnect(cdpEndpoint)) ||
|
||||
(!!options.autoConnect && !this.isCdpConnectionAlive());
|
||||
@@ -1923,7 +1939,11 @@ export class BrowserManager {
|
||||
*/
|
||||
private async connectViaCDP(
|
||||
cdpEndpoint: string | undefined,
|
||||
options?: { timeout?: number }
|
||||
options?: {
|
||||
timeout?: number;
|
||||
allowCreatePageFallback?: boolean;
|
||||
requireMeaningfulPage?: boolean;
|
||||
}
|
||||
): Promise<void> {
|
||||
this.stealthConnectionKind = 'cdp';
|
||||
if (!cdpEndpoint) {
|
||||
@@ -1969,8 +1989,12 @@ export class BrowserManager {
|
||||
}
|
||||
|
||||
let allPages = this.collectUsableCDPPages(contexts);
|
||||
const allowCreatePageFallback = options?.allowCreatePageFallback ?? true;
|
||||
|
||||
if (allPages.length === 0) {
|
||||
if (!allowCreatePageFallback) {
|
||||
throw new Error('No existing user tabs found on this CDP endpoint.');
|
||||
}
|
||||
// Some Chrome instances (especially with custom UI pages) expose only internal/transient
|
||||
// pages over CDP. Create a fresh page so commands always have a stable target.
|
||||
let fallbackPage: Page | null = null;
|
||||
@@ -1996,6 +2020,14 @@ export class BrowserManager {
|
||||
allPages = [fallbackPage];
|
||||
}
|
||||
|
||||
if (options?.requireMeaningfulPage) {
|
||||
const meaningfulPages = allPages.filter((page) => this.isMeaningfulCDPPage(page));
|
||||
if (meaningfulPages.length === 0) {
|
||||
throw new Error('No existing user tabs found on this CDP endpoint.');
|
||||
}
|
||||
allPages = meaningfulPages;
|
||||
}
|
||||
|
||||
// All validation passed - commit state
|
||||
this.browser = browser;
|
||||
this.cdpEndpoint = cdpEndpoint;
|
||||
@@ -2105,6 +2137,35 @@ export class BrowserManager {
|
||||
* 4. If a port responds, connect via CDP
|
||||
*/
|
||||
private async autoConnectViaCDP(): Promise<void> {
|
||||
let sawEndpointWithoutUserTabs = false;
|
||||
|
||||
// Strategy 0: Prefer project-default resident CDP port first.
|
||||
// This keeps user + agent on the same browser session when 9333 is available.
|
||||
{
|
||||
const wsUrl = await this.probeDebugPort(9333);
|
||||
if (wsUrl) {
|
||||
try {
|
||||
await this.connectViaCDP(wsUrl, {
|
||||
allowCreatePageFallback: false,
|
||||
requireMeaningfulPage: true,
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (message.includes('No existing user tabs found on this CDP endpoint')) {
|
||||
sawEndpointWithoutUserTabs = true;
|
||||
if (process.env.AGENT_BROWSER_DEBUG === '1') {
|
||||
console.error(
|
||||
`[DEBUG] Skipping preferred CDP endpoint without user tabs (${wsUrl}): ${message}`
|
||||
);
|
||||
}
|
||||
} else if (process.env.AGENT_BROWSER_DEBUG === '1') {
|
||||
console.error(`[DEBUG] Failed preferred CDP candidate (${wsUrl}): ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 1: Check DevToolsActivePort files
|
||||
const userDataDirs = this.getChromeUserDataDirs();
|
||||
for (const dir of userDataDirs) {
|
||||
@@ -2113,8 +2174,25 @@ export class BrowserManager {
|
||||
// Try HTTP discovery first (works with --remote-debugging-port mode)
|
||||
const wsUrl = await this.probeDebugPort(activePort.port);
|
||||
if (wsUrl) {
|
||||
await this.connectViaCDP(wsUrl);
|
||||
return;
|
||||
try {
|
||||
await this.connectViaCDP(wsUrl, {
|
||||
allowCreatePageFallback: false,
|
||||
requireMeaningfulPage: true,
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (message.includes('No existing user tabs found on this CDP endpoint')) {
|
||||
sawEndpointWithoutUserTabs = true;
|
||||
if (process.env.AGENT_BROWSER_DEBUG === '1') {
|
||||
console.error(
|
||||
`[DEBUG] Skipping CDP endpoint without user tabs (${wsUrl}): ${message}`
|
||||
);
|
||||
}
|
||||
} else if (process.env.AGENT_BROWSER_DEBUG === '1') {
|
||||
console.error(`[DEBUG] Failed CDP candidate (${wsUrl}): ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// HTTP probe failed -- Chrome M144+ chrome://inspect remote debugging uses a
|
||||
// WebSocket-only server with no HTTP endpoints. Connect using the WebSocket
|
||||
@@ -2127,24 +2205,62 @@ export class BrowserManager {
|
||||
`attempting direct WebSocket connection to ${directWsUrl}`
|
||||
);
|
||||
}
|
||||
await this.connectViaCDP(directWsUrl, { timeout: 60_000 });
|
||||
await this.connectViaCDP(directWsUrl, {
|
||||
timeout: 60_000,
|
||||
allowCreatePageFallback: false,
|
||||
requireMeaningfulPage: true,
|
||||
});
|
||||
return;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (message.includes('No existing user tabs found on this CDP endpoint')) {
|
||||
sawEndpointWithoutUserTabs = true;
|
||||
if (process.env.AGENT_BROWSER_DEBUG === '1') {
|
||||
console.error(
|
||||
`[DEBUG] Skipping CDP endpoint without user tabs (${directWsUrl}): ${message}`
|
||||
);
|
||||
}
|
||||
} else if (process.env.AGENT_BROWSER_DEBUG === '1') {
|
||||
console.error(`[DEBUG] Failed CDP candidate (${directWsUrl}): ${message}`);
|
||||
}
|
||||
// Direct WebSocket also failed, try next directory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Probe common debugging ports
|
||||
const commonPorts = [9222, 9229, 9333];
|
||||
const commonPorts = [9222, 9229];
|
||||
for (const port of commonPorts) {
|
||||
const wsUrl = await this.probeDebugPort(port);
|
||||
if (wsUrl) {
|
||||
await this.connectViaCDP(wsUrl);
|
||||
return;
|
||||
try {
|
||||
await this.connectViaCDP(wsUrl, {
|
||||
allowCreatePageFallback: false,
|
||||
requireMeaningfulPage: true,
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (message.includes('No existing user tabs found on this CDP endpoint')) {
|
||||
sawEndpointWithoutUserTabs = true;
|
||||
if (process.env.AGENT_BROWSER_DEBUG === '1') {
|
||||
console.error(
|
||||
`[DEBUG] Skipping CDP endpoint without user tabs (${wsUrl}): ${message}`
|
||||
);
|
||||
}
|
||||
} else if (process.env.AGENT_BROWSER_DEBUG === '1') {
|
||||
console.error(`[DEBUG] Failed CDP candidate (${wsUrl}): ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sawEndpointWithoutUserTabs) {
|
||||
throw new Error(
|
||||
'Found CDP endpoints, but none exposed existing user tabs. Ensure you are attaching to the same Chrome instance/profile you are using manually.'
|
||||
);
|
||||
}
|
||||
|
||||
// Nothing found
|
||||
const platform = os.platform();
|
||||
let hint: string;
|
||||
|
||||
+31
-8
@@ -406,8 +406,7 @@ export async function startDaemon(options?: {
|
||||
}
|
||||
|
||||
// Auto-launch if not already launched and this isn't a launch/close/state_load command.
|
||||
// Default behavior for this fork: first try attaching to a resident Chrome on CDP :9333,
|
||||
// then fall back to launching a local Playwright browser if CDP is unavailable.
|
||||
// Default behavior for this fork: attach to an existing browser only.
|
||||
if (
|
||||
!manager.isLaunched() &&
|
||||
parseResult.command.action !== 'launch' &&
|
||||
@@ -477,10 +476,10 @@ export async function startDaemon(options?: {
|
||||
autoStateFilePath: getSessionAutoStatePath(),
|
||||
};
|
||||
|
||||
let launchedViaDefaultCdp = false;
|
||||
let attachedToExistingBrowser = false;
|
||||
try {
|
||||
// Keep default CDP attempt minimal. Launch-only options like extensions
|
||||
// are incompatible with CDP and can cause a false-negative fallback.
|
||||
// are incompatible with CDP and can cause false-negative attach failures.
|
||||
const cdpLaunchOptions = {
|
||||
id: launchOptions.id,
|
||||
action: launchOptions.action,
|
||||
@@ -492,7 +491,7 @@ export async function startDaemon(options?: {
|
||||
await manager.launch({
|
||||
...cdpLaunchOptions,
|
||||
});
|
||||
launchedViaDefaultCdp = true;
|
||||
attachedToExistingBrowser = true;
|
||||
if (process.env.AGENT_BROWSER_DEBUG === '1') {
|
||||
console.error('[DEBUG] Auto-launch connected via default CDP port 9333');
|
||||
}
|
||||
@@ -500,13 +499,37 @@ export async function startDaemon(options?: {
|
||||
if (process.env.AGENT_BROWSER_DEBUG === '1') {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(
|
||||
`[DEBUG] Default CDP port 9333 unavailable, falling back to local launch: ${message}`
|
||||
`[DEBUG] Default CDP port 9333 unavailable, trying auto-connect discovery: ${message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!launchedViaDefaultCdp) {
|
||||
await manager.launch(launchOptions);
|
||||
if (!attachedToExistingBrowser) {
|
||||
try {
|
||||
await manager.launch({
|
||||
id: launchOptions.id,
|
||||
action: launchOptions.action,
|
||||
autoConnect: true,
|
||||
ignoreHTTPSErrors: launchOptions.ignoreHTTPSErrors,
|
||||
colorScheme: launchOptions.colorScheme,
|
||||
userAgent: launchOptions.userAgent,
|
||||
});
|
||||
attachedToExistingBrowser = true;
|
||||
if (process.env.AGENT_BROWSER_DEBUG === '1') {
|
||||
console.error('[DEBUG] Auto-launch connected via auto-connect discovery');
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.env.AGENT_BROWSER_DEBUG === '1') {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[DEBUG] Auto-connect discovery failed: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!attachedToExistingBrowser) {
|
||||
throw new Error(
|
||||
'Project policy requires using your existing browser. Could not connect to CDP at localhost:9333 and auto-discovery also failed.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user