feat(cli): 强制使用用户现有浏览器并移除 profile/channel

- 禁用 --profile/AGENT_BROWSER_PROFILE 与 --channel/AGENT_BROWSER_CHANNEL,并给出项目策略提示

- 默认模式强制连接 localhost:9333,连接失败直接报错,不再自动回退新开浏览器

- 同步更新 README、技能文档、docs 与 --help 输出

- 版本升级到 0.14.0-fork.2 并同步 cli/Cargo.toml 与 Cargo.lock
This commit is contained in:
leeguooooo
2026-02-24 17:05:11 +09:00
parent 893ddfd259
commit 699ccbd3cb
19 changed files with 317 additions and 172 deletions
+22 -29
View File
@@ -383,33 +383,9 @@ Each session has its own:
- Navigation history
- Authentication state
## Persistent Profiles
By default, browser state (cookies, localStorage, login sessions) is ephemeral and lost when the browser closes. Use `--profile` to persist state across browser restarts:
```bash
# Use a persistent profile directory
agent-browser --profile ~/.myapp-profile open myapp.com
# Login once, then reuse the authenticated session
agent-browser --profile ~/.myapp-profile open myapp.com/dashboard
# Or via environment variable
AGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com
```
The profile directory stores:
- Cookies and localStorage
- IndexedDB data
- Service workers
- Browser cache
- Login sessions
**Tip**: Use different profile paths for different projects to keep their browser state isolated.
## Session Persistence
Alternatively, use `--session-name` to automatically save and restore cookies and localStorage across browser restarts:
Use `--session-name` to automatically save and restore cookies and localStorage across browser restarts:
```bash
# Auto-save/load state for "twitter" session
@@ -492,7 +468,6 @@ This is useful for multimodal AI models that can reason about visual layout, unl
|--------|-------------|
| `--session <name>` | Use isolated session (or `AGENT_BROWSER_SESSION` env) |
| `--session-name <name>` | Auto-save/restore session state (or `AGENT_BROWSER_SESSION_NAME` env) |
| `--profile <path>` | Persistent browser profile directory (or `AGENT_BROWSER_PROFILE` env) |
| `--state <path>` | Load storage state from JSON file (or `AGENT_BROWSER_STATE` env) |
| `--headers <json>` | Set HTTP headers scoped to the URL's origin |
| `--executable-path <path>` | Custom browser executable (or `AGENT_BROWSER_EXECUTABLE_PATH` env) |
@@ -503,7 +478,6 @@ This is useful for multimodal AI models that can reason about visual layout, unl
| `--proxy-bypass <hosts>` | Hosts to bypass proxy (or `AGENT_BROWSER_PROXY_BYPASS` env) |
| `--ignore-https-errors` | Ignore HTTPS certificate errors (useful for self-signed certs) |
| `--allow-file-access` | Allow file:// URLs to access local files (Chromium only) |
| `--stealth` | Stealth mode (default: on): local launch uses Chromium args + init scripts; CDP/provider uses init scripts |
| `-p, --provider <name>` | Cloud browser provider (or `AGENT_BROWSER_PROVIDER` env) |
| `--device <name>` | iOS device name, e.g. "iPhone 15 Pro" (or `AGENT_BROWSER_IOS_DEVICE` env) |
| `--json` | JSON output (for agents) |
@@ -516,6 +490,11 @@ This is useful for multimodal AI models that can reason about visual layout, unl
| `--config <path>` | Use a custom config file (or `AGENT_BROWSER_CONFIG` env) |
| `--debug` | Debug output |
Project policy:
- `--profile` / `AGENT_BROWSER_PROFILE` are forbidden
- `--channel` / `AGENT_BROWSER_CHANNEL` are forbidden
- Default mode must connect to an existing browser at `localhost:9333` (no automatic local-launch fallback)
## Configuration
Create an `agent-browser.json` file to set persistent defaults instead of repeating flags on every command.
@@ -533,7 +512,6 @@ Create an `agent-browser.json` file to set persistent defaults instead of repeat
{
"headed": true,
"proxy": "http://localhost:8080",
"profile": "./browser-data",
"userAgent": "my-agent/1.0",
"ignoreHttpsErrors": true
}
@@ -769,6 +747,8 @@ The `--allow-file-access` flag adds Chromium flags (`--allow-file-access-from-fi
`agent-browser-stealth` is built around stealth as a primary design goal, not an add-on.
Stealth is **always on** with no flag needed. Every browser session automatically applies anti-detection countermeasures:
- **Uses Chrome channel for Chromium launches** -- local Chromium sessions are launched through Playwright's `chrome` channel for a genuine Chrome fingerprint
- Removes `navigator.webdriver` automation indicator
- Disables Chromium's `AutomationControlled` blink feature
- Replaces "HeadlessChrome" in User-Agent and userAgentData (including CDP-level override)
@@ -806,14 +786,27 @@ All interactions are automatically humanized to avoid behavioral detection:
- **Randomized typing** -- When using `type --delay`, each keystroke delay varies by +-40% so timing appears natural rather than mechanical
- **Random wait ranges** -- `wait 2000-5000` pauses for a random duration between 2 and 5 seconds
- **Bezier curve mouse movement** -- Before every `click`, the mouse moves to the target element along a randomized cubic Bezier curve with natural-looking control points
- **Navigation pacing** -- Each page navigation includes a short random delay (300-1000ms) to avoid burst patterns
These behaviors are always active and require no additional flags.
### Auto Region Detection
When navigating to a site, the URL's TLD is used to automatically match locale, timezone, and Accept-Language headers to the target region. For example, opening `shopee.tw` automatically sets locale to `zh-TW` and timezone to `Asia/Taipei`, eliminating region-signal mismatches that server-side risk systems commonly flag.
Supported TLDs include: `.tw`, `.cn`, `.hk`, `.jp`, `.kr`, `.th`, `.vn`, `.sg`, `.my`, `.id`, `.ph`, `.br`, `.mx`, `.de`, `.fr`, `.uk`, `.ru`, `.in`, `.au`, and more.
Override with environment variables: `AGENT_BROWSER_LOCALE`, `AGENT_BROWSER_TIMEZONE`.
### Captcha / Verification Detection
If a navigation lands on a known captcha or verification page (detected by URL patterns like `/verify/captcha` or titles like "Checking your browser"), the browser automatically retries up to 2 times with randomized backoff (3-7 seconds). If all retries are exhausted, a warning suggests `--headed` mode or `--session-name` persistence.
## CDP Mode
Connect to an existing browser via Chrome DevTools Protocol:
By default in this fork, when you run commands without `--cdp`, agent-browser first tries `localhost:9333` (resident Chrome) and falls back to launching a local Playwright browser if CDP is unavailable.
By default in this fork, when you run commands without `--cdp`, agent-browser requires an existing browser at `localhost:9333` (resident browser via CDP). If CDP is unavailable, the command fails fast instead of launching a new managed browser.
```bash
# Start Chrome with: google-chrome --remote-debugging-port=9222
+1 -1
View File
@@ -4,7 +4,7 @@ version = 4
[[package]]
name = "agent-browser-stealth"
version = "0.14.0-fork.1"
version = "0.14.0-fork.2"
dependencies = [
"base64",
"dirs",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "agent-browser-stealth"
version = "0.14.0-fork.1"
version = "0.14.0-fork.2"
edition = "2021"
description = "Stealth browser automation CLI for AI agents with anti-bot evasions"
license = "Apache-2.0"
-2
View File
@@ -1901,7 +1901,6 @@ mod tests {
executable_path: None,
extensions: Vec::new(),
cdp: None,
profile: None,
state: None,
proxy: None,
proxy_bypass: None,
@@ -1915,7 +1914,6 @@ mod tests {
session_name: None,
cli_executable_path: false,
cli_extensions: false,
cli_profile: false,
cli_state: false,
cli_args: false,
cli_user_agent: false,
-9
View File
@@ -215,7 +215,6 @@ pub fn ensure_daemon(
proxy_bypass: Option<&str>,
ignore_https_errors: bool,
allow_file_access: bool,
profile: Option<&str>,
state: Option<&str>,
provider: Option<&str>,
device: Option<&str>,
@@ -345,10 +344,6 @@ pub fn ensure_daemon(
cmd.env("AGENT_BROWSER_ALLOW_FILE_ACCESS", "1");
}
if let Some(prof) = profile {
cmd.env("AGENT_BROWSER_PROFILE", prof);
}
if let Some(st) = state {
cmd.env("AGENT_BROWSER_STATE", st);
}
@@ -433,10 +428,6 @@ pub fn ensure_daemon(
cmd.env("AGENT_BROWSER_ALLOW_FILE_ACCESS", "1");
}
if let Some(prof) = profile {
cmd.env("AGENT_BROWSER_PROFILE", prof);
}
if let Some(st) = state {
cmd.env("AGENT_BROWSER_STATE", st);
}
+2 -26
View File
@@ -19,7 +19,6 @@ pub struct Config {
pub session_name: Option<String>,
pub executable_path: Option<String>,
pub extensions: Option<Vec<String>>,
pub profile: Option<String>,
pub state: Option<String>,
pub proxy: Option<String>,
pub proxy_bypass: Option<String>,
@@ -53,7 +52,6 @@ impl Config {
}
(a, b) => b.or(a),
},
profile: other.profile.or(self.profile),
state: other.state.or(self.state),
proxy: other.proxy.or(self.proxy),
proxy_bypass: other.proxy_bypass.or(self.proxy_bypass),
@@ -132,6 +130,7 @@ fn extract_config_path(args: &[String]) -> Option<Option<String>> {
"--device",
"--session-name",
"--color-scheme",
"--channel",
];
let mut i = 0;
while i < args.len() {
@@ -188,7 +187,6 @@ pub struct Flags {
pub executable_path: Option<String>,
pub cdp: Option<String>,
pub extensions: Vec<String>,
pub profile: Option<String>,
pub state: Option<String>,
pub proxy: Option<String>,
pub proxy_bypass: Option<String>,
@@ -207,7 +205,6 @@ pub struct Flags {
// (as opposed to being set only via environment variables)
pub cli_executable_path: bool,
pub cli_extensions: bool,
pub cli_profile: bool,
pub cli_state: bool,
pub cli_args: bool,
pub cli_user_agent: bool,
@@ -257,7 +254,6 @@ pub fn parse_flags(args: &[String]) -> Flags {
.or(config.executable_path),
cdp: config.cdp,
extensions,
profile: env::var("AGENT_BROWSER_PROFILE").ok().or(config.profile),
state: env::var("AGENT_BROWSER_STATE").ok().or(config.state),
proxy: env::var("AGENT_BROWSER_PROXY").ok().or(config.proxy),
proxy_bypass: env::var("AGENT_BROWSER_PROXY_BYPASS")
@@ -284,7 +280,6 @@ pub fn parse_flags(args: &[String]) -> Flags {
.or(config.color_scheme),
cli_executable_path: false,
cli_extensions: false,
cli_profile: false,
cli_state: false,
cli_args: false,
cli_user_agent: false,
@@ -357,13 +352,6 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1;
}
}
"--profile" => {
if let Some(s) = args.get(i + 1) {
flags.profile = Some(s.clone());
flags.cli_profile = true;
i += 1;
}
}
"--state" => {
if let Some(s) = args.get(i + 1) {
flags.state = Some(s.clone());
@@ -486,7 +474,6 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
"--executable-path",
"--cdp",
"--extension",
"--profile",
"--state",
"--proxy",
"--proxy-bypass",
@@ -668,12 +655,6 @@ mod tests {
assert!(flags.cli_extensions);
}
#[test]
fn test_cli_profile_tracking() {
let flags = parse_flags(&args("--profile /path/to/profile snapshot"));
assert!(flags.cli_profile);
}
#[test]
fn test_cli_annotate_tracking() {
let flags = parse_flags(&args("--annotate screenshot"));
@@ -690,10 +671,9 @@ mod tests {
#[test]
fn test_cli_multiple_flags_tracking() {
let flags = parse_flags(&args(
"--executable-path /chrome --profile /profile --proxy http://proxy snapshot",
"--executable-path /chrome --proxy http://proxy snapshot",
));
assert!(flags.cli_executable_path);
assert!(flags.cli_profile);
assert!(flags.cli_proxy);
assert!(!flags.cli_extensions);
assert!(!flags.cli_state);
@@ -712,7 +692,6 @@ mod tests {
"sessionName": "my-app",
"executablePath": "/usr/bin/chromium",
"extensions": ["/ext1", "/ext2"],
"profile": "/tmp/profile",
"state": "/tmp/state.json",
"proxy": "http://proxy:8080",
"proxyBypass": "localhost",
@@ -738,7 +717,6 @@ mod tests {
config.extensions,
Some(vec!["/ext1".to_string(), "/ext2".to_string()])
);
assert_eq!(config.profile.as_deref(), Some("/tmp/profile"));
assert_eq!(config.state.as_deref(), Some("/tmp/state.json"));
assert_eq!(config.proxy.as_deref(), Some("http://proxy:8080"));
assert_eq!(config.proxy_bypass.as_deref(), Some("localhost"));
@@ -784,7 +762,6 @@ mod tests {
let user = Config {
headed: Some(true),
proxy: Some("http://user-proxy:8080".to_string()),
profile: Some("/user/profile".to_string()),
..Config::default()
};
let project = Config {
@@ -795,7 +772,6 @@ mod tests {
let merged = user.merge(project);
assert_eq!(merged.headed, Some(true)); // kept from user
assert_eq!(merged.proxy.as_deref(), Some("http://project-proxy:9090")); // overridden by project
assert_eq!(merged.profile.as_deref(), Some("/user/profile")); // kept from user
assert_eq!(merged.debug, Some(true)); // added by project
}
+54 -17
View File
@@ -153,6 +153,47 @@ fn main() {
return;
}
if args.iter().any(|a| a == "--profile") {
let msg =
"Project policy: --profile is forbidden. Use your existing browser and --session-name for state persistence.";
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
} else {
eprintln!("{} {}", color::error_indicator(), msg);
}
exit(1);
}
if env::var("AGENT_BROWSER_PROFILE").is_ok() {
let msg =
"Project policy: AGENT_BROWSER_PROFILE is forbidden. Remove it and use --session-name.";
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
} else {
eprintln!("{} {}", color::error_indicator(), msg);
}
exit(1);
}
if args.iter().any(|a| a == "--channel") {
let msg = "Project policy: --channel is forbidden. Browser selection follows your existing browser session.";
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
} else {
eprintln!("{} {}", color::error_indicator(), msg);
}
exit(1);
}
if env::var("AGENT_BROWSER_CHANNEL").is_ok() {
let msg =
"Project policy: AGENT_BROWSER_CHANNEL is forbidden. Remove it and use your existing browser session.";
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
} else {
eprintln!("{} {}", color::error_indicator(), msg);
}
exit(1);
}
if clean.is_empty() {
print_help();
return;
@@ -221,7 +262,6 @@ fn main() {
flags.proxy_bypass.as_deref(),
flags.ignore_https_errors,
flags.allow_file_access,
flags.profile.as_deref(),
flags.state.as_deref(),
flags.provider.as_deref(),
flags.device.as_deref(),
@@ -254,11 +294,6 @@ fn main() {
} else {
None
},
if flags.cli_profile {
Some("--profile")
} else {
None
},
if flags.cli_state {
Some("--state")
} else {
@@ -513,14 +548,13 @@ fn main() {
}
}
// Default fork behavior: when no explicit connection mode is provided,
// try attaching to resident Chrome on CDP :9333 first. If unavailable,
// silently fall back to local launch behavior below.
// 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.
let can_try_default_cdp = flags.cdp.is_none()
&& !flags.auto_connect
&& flags.provider.is_none()
&& flags.executable_path.is_none()
&& flags.profile.is_none()
&& flags.state.is_none()
&& flags.proxy.is_none()
&& flags.args.is_none()
@@ -545,11 +579,19 @@ fn main() {
launched_via_default_cdp = 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 flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
} else {
eprintln!("{} {}", color::error_indicator(), msg);
}
exit(1);
}
// Launch headed browser or configure browser options (without CDP or provider)
if (flags.headed
|| flags.executable_path.is_some()
|| flags.profile.is_some()
|| flags.state.is_some()
|| flags.proxy.is_some()
|| flags.args.is_some()
@@ -577,11 +619,6 @@ fn main() {
cmd_obj.insert("executablePath".to_string(), json!(exec_path));
}
// Add profile path if specified
if let Some(ref profile_path) = flags.profile {
cmd_obj.insert("profile".to_string(), json!(profile_path));
}
// Add state path if specified
if let Some(ref state_path) = flags.state {
cmd_obj.insert("storageState".to_string(), json!(state_path));
@@ -627,7 +664,7 @@ fn main() {
match send_command(launch_cmd, &flags.session) {
Ok(resp) => {
if !resp.success {
// Launch command failed (e.g., invalid state file, profile error)
// Launch command failed (e.g., invalid state file)
let error_msg = resp
.error
.unwrap_or_else(|| "Browser launch failed".to_string());
+12 -4
View File
@@ -22,6 +22,9 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
if let Some(title) = data.get("title").and_then(|v| v.as_str()) {
println!("{} {}", color::success_indicator(), color::bold(title));
println!(" {}", color::dim(url));
if let Some(warning) = data.get("warning").and_then(|v| v.as_str()) {
println!("{} {}", color::warning_indicator(), warning);
}
return;
}
println!("{}", url);
@@ -2100,7 +2103,6 @@ Snapshot Options:
Options:
--session <name> Isolated session (or AGENT_BROWSER_SESSION env)
--profile <path> Persistent browser profile (or AGENT_BROWSER_PROFILE env)
--state <path> Load storage state from JSON file (or AGENT_BROWSER_STATE env)
--headers <json> HTTP headers scoped to URL's origin (for auth)
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
@@ -2122,13 +2124,18 @@ Options:
--headed Show browser window (not headless)
--cdp <port> Connect via CDP (Chrome DevTools Protocol)
--auto-connect Auto-discover and connect to running Chrome
Default launch tries CDP at localhost:9333 first, then falls back to local browser launch
Project default: require existing browser at localhost:9333 (no auto local fallback)
--color-scheme <scheme> Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME)
--session-name <name> Auto-save/restore session state (cookies, localStorage)
--config <path> Use a custom config file (or AGENT_BROWSER_CONFIG env)
--debug Debug output
--version, -V Show version (fork builds include upstream/fork info)
Policy:
--profile / AGENT_BROWSER_PROFILE are forbidden
--channel / AGENT_BROWSER_CHANNEL are forbidden
Use existing browser session (CDP localhost:9333) or pass --cdp explicitly
Configuration:
agent-browser looks for agent-browser.json in these locations (lowest to highest priority):
1. ~/.agent-browser/config.json User-level defaults
@@ -2146,7 +2153,7 @@ Configuration:
Extensions from user and project configs are merged (not replaced).
Example agent-browser.json:
{{"headed": true, "proxy": "http://localhost:8080", "profile": "./browser-data"}}
{{"headed": true, "proxy": "http://localhost:8080", "userAgent": "my-agent/1.0"}}
Environment:
AGENT_BROWSER_CONFIG Path to config file (or use --config)
@@ -2166,6 +2173,8 @@ Environment:
AGENT_BROWSER_AUTO_CONNECT Auto-discover and connect to running Chrome
AGENT_BROWSER_ALLOW_FILE_ACCESS Allow file:// URLs to access local files
AGENT_BROWSER_LOCALE Override auto-detected locale (e.g., zh-TW, ja-JP)
AGENT_BROWSER_TIMEZONE Override auto-detected timezone (e.g., Asia/Taipei)
AGENT_BROWSER_COLOR_SCHEME Color scheme preference (dark, light, no-preference)
AGENT_BROWSER_DEFAULT_TIMEOUT Default Playwright timeout in ms (default: 25000)
AGENT_BROWSER_SESSION_NAME Auto-save/load state persistence name
@@ -2195,7 +2204,6 @@ Examples:
agent-browser --cdp 9222 snapshot # Connect via CDP port
agent-browser --auto-connect snapshot # Auto-discover running Chrome
agent-browser --color-scheme dark open example.com # Dark mode
agent-browser --profile ~/.myapp open example.com # Persistent profile
agent-browser --session-name myapp open example.com # Auto-save/restore state
Command Chaining:
+6 -2
View File
@@ -6,7 +6,12 @@ export const metadata = pageMetadata("cdp-mode")
Connect to an existing browser via Chrome DevTools Protocol:
Default behavior in this fork: when `--cdp` is omitted, agent-browser first tries `localhost:9333` and falls back to local browser launch if CDP is unavailable.
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).
Project policy:
- `--profile` / `AGENT_BROWSER_PROFILE` are forbidden
- `--channel` / `AGENT_BROWSER_CHANNEL` are forbidden
```bash
# Start Chrome with: google-chrome --remote-debugging-port=9222
@@ -112,7 +117,6 @@ This enables control of:
</thead>
<tbody>
<tr><td><code>--session &lt;name&gt;</code></td><td>Use isolated session</td></tr>
<tr><td><code>--profile &lt;path&gt;</code></td><td>Persistent browser profile directory</td></tr>
<tr><td><code>-p &lt;provider&gt;</code></td><td>Cloud browser provider (<code>browserbase</code>, <code>browseruse</code>, <code>kernel</code>)</td></tr>
<tr><td><code>--headers &lt;json&gt;</code></td><td>HTTP headers scoped to origin</td></tr>
<tr><td><code>--executable-path</code></td><td>Custom browser executable</td></tr>
+1 -2
View File
@@ -240,7 +240,6 @@ agent-browser reload # Reload page
```bash
--session <name> # Isolated browser session
--session-name <name> # Auto-save/restore session state (cookies, localStorage)
--profile <path> # Persistent browser profile directory
--state <path> # Load storage state from JSON file
--headers <json> # HTTP headers scoped to URL's origin
--executable-path <path> # Custom browser executable
@@ -251,7 +250,7 @@ agent-browser reload # Reload page
--proxy-bypass <hosts> # Hosts to bypass proxy
--ignore-https-errors # Ignore HTTPS certificate errors
--allow-file-access # Allow file:// URLs to access local files (Chromium only)
--stealth # Stealth mode: local uses launch args+init scripts; CDP/provider uses init scripts
--stealth # Stealth mode (always on by default)
-p, --provider <name> # Browser provider (ios, browserbase, kernel, browseruse)
--device <name> # iOS device name (e.g., "iPhone 15 Pro")
--json # JSON output (for scripts)
+2 -4
View File
@@ -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 already prefers a resident Chrome at `localhost:9333` (CDP) and falls back to local Playwright launch when unavailable.
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.
## Config File Locations
@@ -39,7 +39,6 @@ AGENT_BROWSER_CONFIG=./ci-config.json agent-browser open example.com
{
"headed": true,
"proxy": "http://localhost:8080",
"profile": "./browser-data",
"userAgent": "my-agent/1.0",
"ignoreHttpsErrors": true
}
@@ -62,7 +61,6 @@ Every CLI flag can be set in the config file using its camelCase equivalent:
<tr><td><code>sessionName</code></td><td><code>--session-name</code></td><td>string</td></tr>
<tr><td><code>executablePath</code></td><td><code>--executable-path</code></td><td>string</td></tr>
<tr><td><code>extensions</code></td><td><code>--extension</code></td><td>string[]</td></tr>
<tr><td><code>profile</code></td><td><code>--profile</code></td><td>string</td></tr>
<tr><td><code>state</code></td><td><code>--state</code></td><td>string</td></tr>
<tr><td><code>proxy</code></td><td><code>--proxy</code></td><td>string</td></tr>
<tr><td><code>proxyBypass</code></td><td><code>--proxy-bypass</code></td><td>string</td></tr>
@@ -86,7 +84,7 @@ Every CLI flag can be set in the config file using its camelCase equivalent:
```json
{
"headed": true,
"profile": "./browser-data"
"sessionName": "local-dev"
}
```
-23
View File
@@ -34,29 +34,6 @@ Each session has its own:
- Navigation history
- Authentication state
## Persistent profiles
By default, browser state is lost when the browser closes. Use `--profile` to persist state across restarts:
```bash
# Use a persistent profile directory
agent-browser --profile ~/.myapp-profile open myapp.com
# Login once, then reuse the authenticated session
agent-browser --profile ~/.myapp-profile open myapp.com/dashboard
# Or via environment variable
AGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com
```
The profile directory stores:
- Cookies and localStorage
- IndexedDB data
- Service workers
- Browser cache
- Login sessions
## Session persistence
Use `--session-name` to automatically save and restore cookies and localStorage across browser restarts:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "agent-browser-stealth",
"version": "0.14.0-fork.1",
"version": "0.14.0-fork.2",
"description": "Stealth browser automation CLI for AI agents with anti-bot evasions",
"type": "module",
"main": "dist/daemon.js",
+22 -5
View File
@@ -179,7 +179,7 @@ agent-browser session list
### Connect to Existing Chrome
By default in this fork, commands without `--cdp` try `localhost:9333` first and automatically fall back to a local browser launch if CDP is unavailable.
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).
```bash
# Auto-discover running Chrome with remote debugging enabled
@@ -222,11 +222,29 @@ agent-browser --allow-file-access open file:///path/to/page.html
agent-browser screenshot output.png
```
### Project Policy
- `--profile` / `AGENT_BROWSER_PROFILE` are forbidden
- `--channel` / `AGENT_BROWSER_CHANNEL` are forbidden
- Use existing browser sessions (default CDP `localhost:9333`) or pass `--cdp` explicitly
### Stealth Mode (Always On)
Stealth is always active -- no flags needed. All sessions automatically apply anti-detection patches (navigator.webdriver removal, UA override, plugin injection, WebGL masking, humanized interactions, etc.).
For best results against strong bot detection, use `--headed` and `--profile`.
Chromium launches in managed mode use Chrome channel by default for a genuine browser binary fingerprint.
For best results against strong bot detection, use `--headed` and `--session-name`.
### Auto Region Detection
The browser automatically detects the target site's region from the URL TLD and sets matching locale, timezone, and Accept-Language headers. For example, navigating to `shopee.tw` sets locale `zh-TW` and timezone `Asia/Taipei`. This reduces server-side risk scoring from region-signal mismatches.
Override: `AGENT_BROWSER_LOCALE`, `AGENT_BROWSER_TIMEZONE` env vars.
### Captcha Detection & Auto-Retry
When a navigation lands on a captcha/verification page, the browser automatically retries up to 2 times with randomized backoff (3-7s). If detection persists, a warning is shown suggesting `--headed` mode or `--session-name` persistence.
### iOS Simulator (Mobile Safari)
@@ -314,7 +332,7 @@ agent-browser automatically humanizes interactions to avoid behavioral detection
- **Random wait ranges**: `wait 2000-5000` pauses for a random duration in that range
- **Bezier curve mouse**: Before every `click`, the mouse moves along a natural-looking curve
These behaviors are always active. For sensitive sites, combine with `--headed` and `--profile` for best results.
These behaviors are always active. For sensitive sites, combine with `--headed` and `--session-name` for best results.
## Session Management and Cleanup
@@ -419,8 +437,7 @@ Create `agent-browser.json` in the project root for persistent settings:
```json
{
"headed": true,
"proxy": "http://localhost:8080",
"profile": "./browser-data"
"proxy": "http://localhost:8080"
}
```
+70 -2
View File
@@ -529,21 +529,89 @@ async function handleNavigate(
): Promise<Response<NavigateData>> {
const page = browser.getPage();
// Set target URL for region auto-detection (locale/timezone)
await browser.setTargetUrl(command.url);
// If headers are provided, set up scoped headers for this origin
if (command.headers && Object.keys(command.headers).length > 0) {
await browser.setScopedHeaders(command.url, command.headers);
}
// Humanized navigation pacing: random short delay before navigating
const pace = 300 + Math.random() * 700;
await page.waitForTimeout(Math.round(pace));
await page.goto(command.url, {
waitUntil: command.waitUntil ?? 'load',
});
// Detect captcha/verification pages and retry with backoff
const finalUrl = page.url();
const title = await page.title();
const captchaDetected = isCaptchaPage(finalUrl, title);
if (captchaDetected) {
const maxRetries = 2;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const backoff = 3000 + Math.random() * 4000;
await page.waitForTimeout(Math.round(backoff));
await page.goto(command.url, {
waitUntil: command.waitUntil ?? 'load',
});
const retryUrl = page.url();
const retryTitle = await page.title();
if (!isCaptchaPage(retryUrl, retryTitle)) {
return successResponse(command.id, {
url: retryUrl,
title: retryTitle,
});
}
}
// All retries exhausted -- return the page as-is with a warning
return successResponse(command.id, {
url: page.url(),
title: await page.title(),
warning:
'Captcha/verification page detected. Try --headed mode or use --session-name for state persistence.',
} as NavigateData);
}
return successResponse(command.id, {
url: page.url(),
title: await page.title(),
url: finalUrl,
title,
});
}
function isCaptchaPage(url: string, title: string): boolean {
const lowerUrl = url.toLowerCase();
const lowerTitle = title.toLowerCase();
const captchaPatterns = [
'/verify/captcha',
'/captcha',
'/challenge',
'scene=crawler',
'scene=anti_bot',
'recaptcha',
'hcaptcha',
];
const titlePatterns = [
'verify',
'captcha',
'challenge',
'attention required',
'just a moment',
'checking your browser',
'access denied',
'驗證',
'验证',
'人机验证',
];
return (
captchaPatterns.some((p) => lowerUrl.includes(p)) ||
titlePatterns.some((p) => lowerTitle.includes(p))
);
}
function bezierPoint(t: number, p0: number, p1: number, p2: number, p3: number): number {
const u = 1 - t;
return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3;
+121 -40
View File
@@ -249,13 +249,117 @@ export class BrowserManager {
return undefined;
}
// TLD -> {locale, timezone} mapping for automatic region consistency
private static readonly TLD_REGION_MAP: Record<string, { locale: string; timezone: string }> = {
tw: { locale: 'zh-TW', timezone: 'Asia/Taipei' },
cn: { locale: 'zh-CN', timezone: 'Asia/Shanghai' },
hk: { locale: 'zh-HK', timezone: 'Asia/Hong_Kong' },
jp: { locale: 'ja-JP', timezone: 'Asia/Tokyo' },
kr: { locale: 'ko-KR', timezone: 'Asia/Seoul' },
th: { locale: 'th-TH', timezone: 'Asia/Bangkok' },
vn: { locale: 'vi-VN', timezone: 'Asia/Ho_Chi_Minh' },
sg: { locale: 'en-SG', timezone: 'Asia/Singapore' },
my: { locale: 'ms-MY', timezone: 'Asia/Kuala_Lumpur' },
id: { locale: 'id-ID', timezone: 'Asia/Jakarta' },
ph: { locale: 'en-PH', timezone: 'Asia/Manila' },
br: { locale: 'pt-BR', timezone: 'America/Sao_Paulo' },
mx: { locale: 'es-MX', timezone: 'America/Mexico_City' },
ar: { locale: 'es-AR', timezone: 'America/Argentina/Buenos_Aires' },
de: { locale: 'de-DE', timezone: 'Europe/Berlin' },
fr: { locale: 'fr-FR', timezone: 'Europe/Paris' },
uk: { locale: 'en-GB', timezone: 'Europe/London' },
ru: { locale: 'ru-RU', timezone: 'Europe/Moscow' },
in: { locale: 'hi-IN', timezone: 'Asia/Kolkata' },
au: { locale: 'en-AU', timezone: 'Australia/Sydney' },
};
// Target URL set during navigation, used for region auto-detection
private targetUrl: string | undefined = undefined;
/**
* Set the target URL for region auto-detection.
* Called from navigate/open commands so locale/timezone can adapt.
* Applies CDP overrides to align locale/timezone with the target site's region.
*/
async setTargetUrl(url: string): Promise<void> {
this.targetUrl = url;
const region = this.getRegionFromUrl(url);
if (!region) return;
// Skip if user has explicitly set locale/timezone via env
const envLocale = process.env.AGENT_BROWSER_LOCALE;
const envTimezone = process.env.AGENT_BROWSER_TIMEZONE || process.env.TZ;
try {
const page = this.getPage();
const cdp = await page.context().newCDPSession(page);
if (!envTimezone) {
await cdp
.send('Emulation.setTimezoneOverride', { timezoneId: region.timezone })
.catch(() => {});
}
if (!envLocale) {
await cdp.send('Emulation.setLocaleOverride', { locale: region.locale }).catch(() => {});
// Update Accept-Language header to match
const langHeader = this.buildAcceptLanguageHeader(region.locale);
const context = page.context();
const currentHeaders = this.contextHeaders ?? {};
await context.setExtraHTTPHeaders({ ...currentHeaders, 'Accept-Language': langHeader });
}
await cdp.detach().catch(() => {});
} catch {
// CDP not available (non-Chromium), skip dynamic override
}
}
private getRegionFromUrl(url?: string): { locale: string; timezone: string } | undefined {
if (!url) return undefined;
try {
const hostname = new URL(url).hostname;
const parts = hostname.split('.');
const tld = parts[parts.length - 1];
// Check compound TLDs like co.th, com.tw, co.id
const secondLevel = parts.length >= 2 ? parts[parts.length - 2] : '';
const compoundTld = `${secondLevel}.${tld}`;
// Try compound first (e.g., "co.th" -> "th", "com.tw" -> "tw")
const compoundMatch = BrowserManager.TLD_REGION_MAP[tld];
if (
compoundMatch &&
(secondLevel === 'co' ||
secondLevel === 'com' ||
secondLevel === 'or' ||
secondLevel === 'org')
) {
return compoundMatch;
}
// Then direct TLD
if (BrowserManager.TLD_REGION_MAP[tld]) {
return BrowserManager.TLD_REGION_MAP[tld];
}
return undefined;
} catch {
return undefined;
}
}
private resolveStealthLocale(headers?: Record<string, string>): string {
const headerLocale = this.getHeaderValue(headers, 'accept-language');
const normalizedHeaderLocale = this.normalizeLocaleTag(headerLocale);
if (normalizedHeaderLocale) return normalizedHeaderLocale;
// Explicit env var takes priority
const envLocale = this.normalizeLocaleTag(process.env.AGENT_BROWSER_LOCALE);
if (envLocale) return envLocale;
// Auto-detect from target URL TLD
const urlRegion = this.getRegionFromUrl(this.targetUrl);
if (urlRegion) return urlRegion.locale;
const candidates = [
process.env.AGENT_BROWSER_LOCALE,
process.env.LC_ALL,
process.env.LC_MESSAGES,
process.env.LANG,
@@ -269,16 +373,16 @@ export class BrowserManager {
}
private resolveStealthTimezoneId(): string | undefined {
const candidates = [
process.env.AGENT_BROWSER_TIMEZONE,
process.env.TZ,
Intl.DateTimeFormat().resolvedOptions().timeZone,
];
for (const value of candidates) {
const timezone = value?.trim();
if (!timezone) continue;
if (timezone === 'UTC' || timezone.includes('/')) return timezone;
}
// Explicit env var takes priority
const envTz = process.env.AGENT_BROWSER_TIMEZONE?.trim() || process.env.TZ?.trim();
if (envTz && (envTz === 'UTC' || envTz.includes('/'))) return envTz;
// Auto-detect from target URL TLD
const urlRegion = this.getRegionFromUrl(this.targetUrl);
if (urlRegion) return urlRegion.timezone;
const systemTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (systemTz) return systemTz;
return undefined;
}
@@ -1409,23 +1513,12 @@ export class BrowserManager {
// Determine CDP endpoint: prefer cdpUrl over cdpPort for flexibility
const cdpEndpoint = options.cdpUrl ?? (options.cdpPort ? String(options.cdpPort) : undefined);
const hasExtensions = !!options.extensions?.length;
const hasProfile = !!options.profile;
const hasStorageState = !!options.storageState;
if (hasExtensions && cdpEndpoint) {
throw new Error('Extensions cannot be used with CDP connection');
}
if (hasProfile && cdpEndpoint) {
throw new Error('Profile cannot be used with CDP connection');
}
if (hasStorageState && hasProfile) {
throw new Error(
'Storage state cannot be used with profile (profile is already persistent storage)'
);
}
if (hasStorageState && hasExtensions) {
throw new Error(
'Storage state cannot be used with extensions (extensions require persistent context)'
@@ -1510,6 +1603,10 @@ export class BrowserManager {
const launcher =
browserType === 'firefox' ? firefox : browserType === 'webkit' ? webkit : chromium;
// Chromium launches always use the Chrome channel unless a custom executable is provided.
const chromeChannel =
browserType === 'chromium' && !options.executablePath ? 'chrome' : undefined;
const stealthPolicy = this.getStealthPolicy(browserType);
const contextDefaults = this.buildStealthContextDefaults(stealthPolicy, options.headers);
const extraHTTPHeaders = contextDefaults.extraHTTPHeaders;
@@ -1572,6 +1669,7 @@ export class BrowserManager {
{
headless: false,
executablePath: options.executablePath,
...(chromeChannel && { channel: chromeChannel }),
args: allArgs,
viewport,
extraHTTPHeaders,
@@ -1584,29 +1682,12 @@ export class BrowserManager {
}
);
this.isPersistentContext = true;
} else if (hasProfile) {
// Profile uses persistent context for durable cookies/storage
// Expand ~ to home directory since it won't be shell-expanded
const profilePath = options.profile!.replace(/^~\//, os.homedir() + '/');
context = await launcher.launchPersistentContext(profilePath, {
headless: options.headless ?? false,
executablePath: options.executablePath,
args: baseArgs,
viewport,
extraHTTPHeaders,
userAgent: contextUserAgent,
...(this.contextLocale && { locale: this.contextLocale }),
...(this.contextTimezoneId && { timezoneId: this.contextTimezoneId }),
...(options.proxy && { proxy: options.proxy }),
ignoreHTTPSErrors: options.ignoreHTTPSErrors ?? false,
...(this.colorScheme && { colorScheme: this.colorScheme }),
});
this.isPersistentContext = true;
} else {
// Regular ephemeral browser
this.browser = await launcher.launch({
headless: options.headless ?? false,
executablePath: options.executablePath,
...(chromeChannel && { channel: chromeChannel }),
args: baseArgs,
});
this.cdpEndpoint = null;
+1 -2
View File
@@ -466,7 +466,6 @@ export async function startDaemon(options?: {
headless: process.env.AGENT_BROWSER_HEADED !== '1',
executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH,
extensions: extensions,
profile: process.env.AGENT_BROWSER_PROFILE,
storageState: process.env.AGENT_BROWSER_STATE,
args,
userAgent: process.env.AGENT_BROWSER_USER_AGENT,
@@ -480,7 +479,7 @@ export async function startDaemon(options?: {
let launchedViaDefaultCdp = false;
try {
// Keep default CDP attempt minimal. Launch-only options like profile/extensions
// Keep default CDP attempt minimal. Launch-only options like extensions
// are incompatible with CDP and can cause a false-negative fallback.
const cdpLaunchOptions = {
id: launchOptions.id,
-1
View File
@@ -50,7 +50,6 @@ const launchSchema = baseCommandSchema.extend({
ignoreHTTPSErrors: z.boolean().optional(),
allowFileAccess: z.boolean().optional(),
colorScheme: z.enum(['light', 'dark', 'no-preference']).optional(),
profile: z.string().optional(),
storageState: z.string().optional(),
});
+1 -1
View File
@@ -18,7 +18,6 @@ export interface LaunchCommand extends BaseCommand {
cdpUrl?: string;
autoConnect?: boolean; // Auto-discover and connect to running Chrome via DevToolsActivePort
extensions?: string[];
profile?: string; // Path to persistent browser profile directory
storageState?: string; // Path to storage state JSON file
proxy?: {
server: string;
@@ -1073,6 +1072,7 @@ export type Response<T = unknown> = SuccessResponse<T> | ErrorResponse;
export interface NavigateData {
url: string;
title: string;
warning?: string;
}
export interface Annotation {