diff --git a/README.md b/README.md index c7b8cad..5f553ba 100644 --- a/README.md +++ b/README.md @@ -1,163 +1,199 @@ # agent-browser-stealth -Stealth-focused fork of `agent-browser` for anti-bot evasion in production automation. +Stealth-first fork of `agent-browser` for production browser automation under anti-bot pressure. -This fork keeps core browser automation capabilities in sync with upstream `agent-browser`, and focuses its own changes on stealth and anti-detection behavior. +This README focuses on stealth architecture and principles. For full command coverage inherited from upstream, use: -## Positioning +- upstream docs: +- local help: `agent-browser --help` -- Core commands and workflows: aligned with upstream `agent-browser` -- Fork value: stronger anti-bot defaults and operational policies -- Default mindset: no extra stealth toggle, stealth is always on +## What This Fork Optimizes -## Installation +- Stealth is always on (legacy `launch.stealth` is accepted but ignored). +- Fingerprint surfaces are patched at multiple layers (launch args, CDP overrides, init scripts). +- Behavioral signals are humanized (typing cadence, cursor path, pacing, retry backoff). +- Region signals are auto-aligned (locale/timezone/Accept-Language) to reduce mismatch risk. +- Verification/captcha handling is policy-driven (`--risk-mode off|warn|block`). -### Global (recommended) +## Quick Start + +### Install ```bash npm install -g agent-browser-stealth agent-browser install ``` -### Quick try with npx - -```bash -npx agent-browser-stealth install -npx agent-browser-stealth open example.com -``` - -### From source - -```bash -git clone https://github.com/leeguooooo/agent-browser -cd agent-browser -pnpm install -pnpm build -pnpm build:native -pnpm link --global -agent-browser install -``` - -## Quick Start +### Minimal Usage ```bash agent-browser open https://example.com agent-browser snapshot -i agent-browser click @e2 -agent-browser fill @e3 "test@example.com" -agent-browser screenshot page.png ``` -## Anti-Bot Measures +## Stealth Architecture -Stealth is always enabled. Legacy `launch.stealth` is accepted only for compatibility and ignored. - -### 1) Fingerprint hardening - -- Hides automation indicators such as `navigator.webdriver` -- Adds Chromium launch args to reduce automation fingerprints -- Rewrites headless UA markers (`HeadlessChrome`) -- Patches high-signal surfaces such as: - - `navigator.plugins` / `navigator.mimeTypes` - - `window.chrome.runtime` - - WebGL vendor/renderer exposure - - permissions/language/media/device related probes -- Applies both context init scripts and CDP-level UA overrides -- Preserves explicit custom UA from `--user-agent` or `launch({ userAgent })` - -### 2) Behavioral humanization - -- Randomized typing cadence when `--delay` is used -- Random wait ranges (`wait 2000-5000`) -- Bezier-curve mouse movement before click actions -- Randomized navigation pacing - -### 3) Region signal alignment - -- Auto-aligns locale/timezone/Accept-Language by target TLD -- Reduces locale-timezone mismatch risk on region-sensitive sites - -### 4) Verification-aware retry - -- Detects common captcha/verification interstitial patterns -- Retries navigation with randomized backoff when triggered - -## Typing `--delay` Correctly - -Use `--delay` as an option: - -```bash -agent-browser type @e2 "iphone" --delay 120 -agent-browser keyboard type "iphone" --delay 120 +```mermaid +flowchart TD + A["Command Input"] --> B["Stealth Policy Resolver"] + B --> C["Connection Mode Detection"] + C --> D["Launch Layer: Chromium Args"] + C --> E["CDP Layer: UA + Metadata Override"] + C --> F["Context Layer: Init Script Patches"] + D --> G["Behavior Layer: Humanized Interaction"] + E --> G + F --> G + G --> H["Risk Layer: Verification Detection and Handling"] + H --> I["Response with warnings and riskSignals"] ``` -If literal text includes `--delay`, stop option parsing with `--`: +### 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 | + +## Principle 1: Always-On Stealth with Explicit Boundaries + +- Stealth defaults to enabled and does not depend on a runtime toggle. +- 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. + +## Principle 2: Multi-Layer Fingerprint Hardening + +### 2.1 Launch Layer (Local Chromium) + +Injected Chromium args: + +- `--disable-blink-features=AutomationControlled` +- `--use-gl=angle` +- `--use-angle=default` + +If no custom UA is set, the runtime UA is normalized to remove `HeadlessChrome` tokens. + +### 2.2 CDP Layer (Browser/Page Targets) + +- Uses `Emulation.setUserAgentOverride` to align: + - `userAgent` + - `acceptLanguage` + - `userAgentMetadata` brands and versions +- Applies overrides for existing/new targets, including worker-relevant contexts. +- Forces opaque white background (`Emulation.setDefaultBackgroundColorOverride`) to avoid headless transparency fingerprints. + +### 2.3 Context Init-Script Layer (Patch Inventory) + +The init script patch set is injected before page scripts and currently includes: + +1. `navigator.webdriver` removal (including prototype-level cleanup). +2. CSS webdriver heuristic neutralization (`CSS.supports('border-end-end-radius: initial')` probe). +3. `window.chrome.runtime` bootstrap for missing runtime surfaces. +4. Locale/language normalization (`navigator.language`, `navigator.languages`). +5. Realistic `navigator.plugins` and `navigator.mimeTypes`. +6. `navigator.permissions.query` normalization for notifications. +7. WebGL vendor/renderer masking when SwiftShader indicators are present. +8. `cdc_` property cleanup on document/documentElement. +9. Window/screen dimension normalization (`outerWidth/outerHeight/screenX/screenY`). +10. Screen availability patching (`availWidth/availHeight`). +11. Hardware concurrency stabilization. +12. Notification permission consistency. +13. Active text color heuristic patching. +14. `navigator.connection` normalization. +15. Worker network signal normalization (`downlinkMax`). +16. `prefers-color-scheme` light-mode heuristic neutralization. +17. `navigator.share` exposure. +18. `navigator.contacts` exposure. +19. `contentIndex` exposure. +20. `navigator.pdfViewerEnabled` normalization. +21. Media devices surface normalization. +22. `navigator.userAgent` cleanup (strip `HeadlessChrome`). +23. `navigator.userAgentData` brand cleanup. +24. `performance.memory` stabilization. +25. Default background color patching at script level. + +## Principle 3: Behavioral Humanization + +- Navigation pacing jitter before `goto` (short randomized delay). +- Typing jitter for `type --delay` and `keyboard type --delay`: + - per-character randomized delay around the requested base delay (about ±40%). +- Click path humanization: + - cursor moves on a Bezier-like curve before click. +- Wait supports random ranges (`wait min-max`) for non-uniform timing. + +## Principle 4: Region Signal Alignment + +Before navigation, the runtime derives region hints from target URL TLD and aligns: + +- locale +- timezone +- `Accept-Language` + +Examples of built-in mappings include `tw`, `jp`, `kr`, `sg`, `de`, `fr`, `uk`, `in`, `au`. + +Manual overrides are supported: + +- `AGENT_BROWSER_LOCALE` +- `AGENT_BROWSER_TIMEZONE` (or `TZ`) + +## Principle 5: Verification-Aware Risk Control + +When a navigation lands on verification/captcha pages, structured risk signals are generated from URL/title evidence. + +`riskSignals` include: + +- `code` +- `source` (`url` or `title`) +- `evidence` +- `confidence` + +### Risk Mode + +- `warn` (default): retry with randomized backoff and return warnings + `riskSignals`. +- `block`: fail fast once verification/captcha interstitial is detected. +- `off`: skip detection/retry path. ```bash -agent-browser type @e2 -- "--delay 120" -agent-browser keyboard type -- "--delay 120" +agent-browser --risk-mode warn open https://example.com +agent-browser --risk-mode block open https://example.com +AGENT_BROWSER_RISK_MODE=off agent-browser open https://example.com ``` -## Validation Snapshot +```mermaid +flowchart TD + A["Navigate"] --> B["Collect URL and Title Signals"] + B --> C{"risk-mode"} + C -->|off| D["Return Success"] + C -->|block| E["Return Error with First Signal"] + C -->|warn| F["Retry up to 2 times"] + F --> G{"Signals Cleared"} + G -->|yes| H["Return Success + recovery warning + riskSignals"] + G -->|no| I["Return Success + warning + riskSignals"] +``` -Manual checks were run against common public detection pages in headed mode, including: +## Operational Recommendations -- [bot.sannysoft.com](https://bot.sannysoft.com/) -- [CreepJS](https://abrahamjuliot.github.io/creepjs/) -- [areyouheadless](https://arh.antoinevastel.com/bots/areyouheadless) -- [detect-headless](https://infosimples.github.io/detect-headless) +- Prefer `--headed` for high-friction targets. +- Reuse session state with `--session-name` for continuity. +- Keep locale/timezone consistent with target market. +- Use `--risk-mode block` in strict pipelines that require explicit operator intervention on verification pages. -Reproduce CreepJS check: +## Validation Scripts + +Run public detector checks after stealth changes: ```bash +node scripts/check-sannysoft-webdriver.js --binary ./cli/target/release/agent-browser node scripts/check-creepjs-headless.js --binary ./cli/target/release/agent-browser ``` -## Command Coverage And Docs +## Upstream Compatibility -Core command set is intentionally kept compatible with upstream `agent-browser`. - -- Full command reference: [upstream agent-browser docs](https://github.com/vercel-labs/agent-browser) -- Local help: `agent-browser --help` - -## Fork Policies - -This fork enforces a few operational policies: - -- `--profile` / `AGENT_BROWSER_PROFILE` are forbidden -- `--channel` / `AGENT_BROWSER_CHANNEL` are forbidden -- Default mode expects an existing browser via CDP on `localhost:9333` - -## Maintainer Notes (Fork Release) - -- Keep `upstream-main` for clean upstream sync -- Merge upstream into short-lived sync branches, then PR into `main` -- Recommended release format: `-fork.` (example: `0.14.0-fork.3`) -- Use npm Trusted Publishing (OIDC) - -## OpenClaw Skill Sync - -This repo includes a dedicated OpenClaw skill at: - -- `skills/agent-browser-stealth/SKILL.md` - -Local git `pre-push` hook auto-syncs skills before every push: - -- `.husky/pre-push` -> `pnpm run clawhub:sync` - -Manual sync command (same logic as hook): - -```bash -pnpm run clawhub:sync -``` - -This uses your existing local ClawHub login session (no GitHub secret required). - -Temporarily skip auto-sync for one push: - -```bash -SKIP_CLAWHUB_SYNC=1 git push -``` +This fork intentionally keeps command workflows close to upstream while concentrating custom behavior in stealth, policy, and anti-detection handling. ## License diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 801a23e..cc854b2 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "agent-browser-stealth" -version = "0.14.0-fork.4" +version = "0.14.0-fork.5" dependencies = [ "base64", "dirs", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 78d943a..e950f6a 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "agent-browser-stealth" -version = "0.14.0-fork.4" +version = "0.14.0-fork.5" edition = "2021" description = "Stealth browser automation CLI for AI agents with anti-bot evasions" license = "Apache-2.0" diff --git a/cli/src/commands.rs b/cli/src/commands.rs index b5a1476..57554f4 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -182,6 +182,19 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result", + }); + } + } Ok(nav_cmd) } "back" => Ok(json!({ "id": id, "action": "back" })), @@ -2028,6 +2041,7 @@ mod tests { annotate: false, color_scheme: None, download_path: None, + risk_mode: None, } } @@ -2293,6 +2307,14 @@ mod tests { assert_eq!(cmd["headers"]["Authorization"], "Bearer token"); } + #[test] + fn test_navigate_with_risk_mode() { + let mut flags = default_flags(); + flags.risk_mode = Some("block".to_string()); + let cmd = parse_command(&args("open https://example.com"), &flags).unwrap(); + assert_eq!(cmd["riskMode"], "block"); + } + #[test] fn test_navigate_with_multiple_headers() { let mut flags = default_flags(); diff --git a/cli/src/flags.rs b/cli/src/flags.rs index d98e8f6..53c540e 100644 --- a/cli/src/flags.rs +++ b/cli/src/flags.rs @@ -34,6 +34,7 @@ pub struct Config { pub annotate: Option, pub color_scheme: Option, pub download_path: Option, + pub risk_mode: Option, } impl Config { @@ -68,6 +69,7 @@ impl Config { annotate: other.annotate.or(self.annotate), color_scheme: other.color_scheme.or(self.color_scheme), download_path: other.download_path.or(self.download_path), + risk_mode: other.risk_mode.or(self.risk_mode), } } } @@ -134,6 +136,7 @@ fn extract_config_path(args: &[String]) -> Option> { "--color-scheme", "--channel", "--download-path", + "--risk-mode", ]; let mut i = 0; while i < args.len() { @@ -204,6 +207,9 @@ pub struct Flags { pub annotate: bool, pub color_scheme: Option, pub download_path: Option, + /// How verification/captcha detections are handled on navigation: + /// `off` (disable), `warn` (retry and warn), `block` (fail fast). + pub risk_mode: Option, // Track which launch-time options were explicitly passed via CLI // (as opposed to being set only via environment variables) @@ -285,6 +291,10 @@ pub fn parse_flags(args: &[String]) -> Flags { .or(config.color_scheme), download_path: env::var("AGENT_BROWSER_DOWNLOAD_PATH").ok() .or(config.download_path), + risk_mode: env::var("AGENT_BROWSER_RISK_MODE") + .ok() + .or(config.risk_mode) + .map(|s| s.to_ascii_lowercase()), cli_executable_path: false, cli_extensions: false, cli_state: false, @@ -456,6 +466,12 @@ pub fn parse_flags(args: &[String]) -> Flags { i += 1; } } + "--risk-mode" => { + if let Some(s) = args.get(i + 1) { + flags.risk_mode = Some(s.to_ascii_lowercase()); + i += 1; + } + } "--config" => { // Already handled by load_config(); skip the value i += 1; @@ -500,6 +516,7 @@ pub fn clean_args(args: &[String]) -> Vec { "--session-name", "--color-scheme", "--download-path", + "--risk-mode", "--config", ]; @@ -697,6 +714,18 @@ mod tests { assert!(!flags.cli_download_path); } + #[test] + fn test_parse_risk_mode_flag() { + let flags = parse_flags(&args("--risk-mode block open example.com")); + assert_eq!(flags.risk_mode.as_deref(), Some("block")); + } + + #[test] + fn test_clean_args_removes_risk_mode() { + let cleaned = clean_args(&args("--risk-mode warn open example.com")); + assert_eq!(cleaned, vec!["open", "example.com"]); + } + #[test] fn test_cli_multiple_flags_tracking() { let flags = parse_flags(&args( @@ -732,7 +761,8 @@ mod tests { "allowFileAccess": true, "cdp": "9222", "autoConnect": true, - "headers": "{\"Auth\":\"token\"}" + "headers": "{\"Auth\":\"token\"}", + "riskMode": "block" }"#; let config: Config = serde_json::from_str(json).unwrap(); assert_eq!(config.headed, Some(true)); @@ -758,6 +788,7 @@ mod tests { assert_eq!(config.cdp.as_deref(), Some("9222")); assert_eq!(config.auto_connect, Some(true)); assert_eq!(config.headers.as_deref(), Some("{\"Auth\":\"token\"}")); + assert_eq!(config.risk_mode.as_deref(), Some("block")); } #[test] diff --git a/cli/src/main.rs b/cli/src/main.rs index 989e590..a46f1ab 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -153,6 +153,21 @@ fn main() { return; } + if let Some(ref risk_mode) = flags.risk_mode { + if !matches!(risk_mode.as_str(), "off" | "warn" | "block") { + let msg = format!( + "Invalid --risk-mode value: {} (expected off, warn, or block)", + risk_mode + ); + if flags.json { + println!(r#"{{"success":false,"error":"{}"}}"#, msg); + } else { + eprintln!("{} {}", color::error_indicator(), msg); + } + exit(1); + } + } + if args.iter().any(|a| a == "--profile") { let msg = "Project policy: --profile is forbidden. Use your existing browser and --session-name for state persistence."; diff --git a/cli/src/output.rs b/cli/src/output.rs index 466b9e1..834942b 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -25,6 +25,33 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) { if let Some(warning) = data.get("warning").and_then(|v| v.as_str()) { println!("{} {}", color::warning_indicator(), warning); } + if let Some(risk_signals) = data.get("riskSignals").and_then(|v| v.as_array()) { + for signal in risk_signals { + let code = signal + .get("code") + .and_then(|v| v.as_str()) + .unwrap_or("unknown_risk"); + let source = signal.get("source").and_then(|v| v.as_str()).unwrap_or("unknown"); + let evidence = signal + .get("evidence") + .and_then(|v| v.as_str()) + .unwrap_or("-"); + let confidence = signal.get("confidence").and_then(|v| v.as_f64()).unwrap_or(0.0); + println!( + "{} risk-signal code={} source={} evidence={} confidence={:.2}", + color::warning_indicator(), + code, + source, + evidence, + confidence + ); + } + } + if let Some(warnings) = data.get("warnings").and_then(|v| v.as_array()) { + for warning in warnings.iter().filter_map(|v| v.as_str()) { + println!("{} {}", color::warning_indicator(), warning); + } + } return; } println!("{}", url); @@ -590,10 +617,12 @@ Global Options: --json Output as JSON --session Use specific session --headers Set HTTP headers (scoped to this origin) + --risk-mode Risk handling for verify/captcha pages: off, warn, block --headed Show browser window Examples: agent-browser open example.com + agent-browser --risk-mode block open example.com agent-browser open https://github.com agent-browser open localhost:3000 agent-browser open api.example.com --headers '{"Authorization": "Bearer token"}' @@ -2135,6 +2164,7 @@ Options: Project default: require existing browser at localhost:9333 (no auto local fallback) --color-scheme Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME) --download-path Default download directory (or AGENT_BROWSER_DOWNLOAD_PATH) + --risk-mode Verify/captcha handling: off, warn, block (or AGENT_BROWSER_RISK_MODE) --session-name Auto-save/restore session state (cookies, localStorage) --config Use a custom config file (or AGENT_BROWSER_CONFIG env) --debug Debug output @@ -2186,6 +2216,7 @@ Environment: AGENT_BROWSER_TIMEZONE Override auto-detected timezone (e.g., Asia/Taipei) AGENT_BROWSER_COLOR_SCHEME Color scheme preference (dark, light, no-preference) AGENT_BROWSER_DOWNLOAD_PATH Default download directory for browser downloads + AGENT_BROWSER_RISK_MODE Verify/captcha handling mode (off, warn, block) AGENT_BROWSER_DEFAULT_TIMEOUT Default Playwright timeout in ms (default: 25000) AGENT_BROWSER_SESSION_NAME Auto-save/load state persistence name AGENT_BROWSER_STATE_EXPIRE_DAYS Auto-delete saved states older than N days (default: 30) @@ -2214,6 +2245,7 @@ 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 --risk-mode block open example.com # Block on verification/captcha pages agent-browser --session-name myapp open example.com # Auto-save/restore state Command Chaining: diff --git a/docs/src/app/commands/page.mdx b/docs/src/app/commands/page.mdx index 98fb5cd..6be0158 100644 --- a/docs/src/app/commands/page.mdx +++ b/docs/src/app/commands/page.mdx @@ -1,6 +1,6 @@ -import { pageMetadata } from "@/lib/page-metadata" +import { pageMetadata } from '@/lib/page-metadata'; -export const metadata = pageMetadata("commands") +export const metadata = pageMetadata('commands'); # Commands @@ -8,6 +8,7 @@ export const metadata = pageMetadata("commands") ```bash agent-browser open # Navigate (aliases: goto, navigate) +agent-browser --risk-mode block open # Block when verification/captcha interstitial is detected agent-browser click # Click element (--new-tab to open in new tab) agent-browser dblclick # Double-click agent-browser fill # Clear and fill @@ -110,6 +111,16 @@ agent-browser wait --fn "condition" # Wait for JS condition agent-browser wait --download [path] # Wait for download ``` +## Risk Mode + +Control how `open`/`navigate` handles verification or captcha interstitials: + +```bash +agent-browser --risk-mode warn open https://example.com # default: retry and warn with riskSignals +agent-browser --risk-mode block open https://example.com # fail fast on detection +agent-browser --risk-mode off open https://example.com # disable detection/retry +``` + ## Downloads ```bash diff --git a/docs/src/app/configuration/page.mdx b/docs/src/app/configuration/page.mdx index 5e46782..fdae90d 100644 --- a/docs/src/app/configuration/page.mdx +++ b/docs/src/app/configuration/page.mdx @@ -1,6 +1,6 @@ -import { pageMetadata } from "@/lib/page-metadata" +import { pageMetadata } from '@/lib/page-metadata'; -export const metadata = pageMetadata("configuration") +export const metadata = pageMetadata('configuration'); # Configuration @@ -14,13 +14,39 @@ agent-browser checks two locations, merged in priority order: - + + + + + - - - - + + + + + + + + + + + + + + + + + + + +
PriorityLocationScope
PriorityLocationScope
1 (lowest)~/.agent-browser/config.jsonUser-level defaults
2./agent-browser.jsonProject-level overrides
3AGENT_BROWSER_* env varsOverride config values
4 (highest)CLI flagsOverride everything
1 (lowest) + ~/.agent-browser/config.json + User-level defaults
2 + ./agent-browser.json + Project-level overrides
3 + AGENT_BROWSER_* env vars + Override config values
4 (highest)CLI flagsOverride everything
@@ -50,34 +76,229 @@ Every CLI flag can be set in the config file using its camelCase equivalent: - + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Config KeyCLI FlagType
Config KeyCLI FlagType
headed--headedboolean
json--jsonboolean
full--full, -fboolean
debug--debugboolean
session--sessionstring
sessionName--session-namestring
executablePath--executable-pathstring
extensions--extensionstring[]
state--statestring
proxy--proxystring
proxyBypass--proxy-bypassstring
args--argsstring
userAgent--user-agentstring
provider-p, --providerstring
device--devicestring
ignoreHttpsErrors--ignore-https-errorsboolean
allowFileAccess--allow-file-accessboolean
cdp--cdpstring
autoConnect--auto-connectboolean
colorScheme--color-schemestring (dark, light, no-preference)
downloadPath--download-pathstring
headers--headersstring (JSON)
+ headed + + --headed + boolean
+ json + + --json + boolean
+ full + + --full, -f + boolean
+ debug + + --debug + boolean
+ session + + --session + string
+ sessionName + + --session-name + string
+ executablePath + + --executable-path + string
+ extensions + + --extension + string[]
+ state + + --state + string
+ proxy + + --proxy + string
+ proxyBypass + + --proxy-bypass + string
+ args + + --args + string
+ userAgent + + --user-agent + string
+ provider + + -p, --provider + string
+ device + + --device + string
+ ignoreHttpsErrors + + --ignore-https-errors + boolean
+ allowFileAccess + + --allow-file-access + boolean
+ cdp + + --cdp + string
+ autoConnect + + --auto-connect + boolean
+ colorScheme + + --color-scheme + + string (dark, light, no-preference) +
+ downloadPath + + --download-path + string
+ riskMode + + --risk-mode + + string (off, warn, block) +
+ headers + + --headers + string (JSON)
+`riskMode` defaults to `warn` when unset. + ## Common Configurations ### Local Development @@ -146,21 +367,125 @@ These environment variables configure additional daemon and runtime behavior: - + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableDescriptionDefault
VariableDescriptionDefault
AGENT_BROWSER_AUTO_CONNECTAuto-discover and connect to a running Chrome instance.(disabled)
AGENT_BROWSER_ALLOW_FILE_ACCESSAllow file:// URLs to access local files.(disabled)
AGENT_BROWSER_COLOR_SCHEMEColor scheme preference (dark, light, no-preference).(none)
AGENT_BROWSER_DOWNLOAD_PATHDefault directory for browser downloads.(temp directory)
AGENT_BROWSER_DEFAULT_TIMEOUTDefault Playwright timeout in ms. Keep below 30000 to avoid IPC timeouts.25000
AGENT_BROWSER_SESSION_NAMEAuto-save/load state persistence name.(none)
AGENT_BROWSER_STATE_EXPIRE_DAYSAuto-delete saved session states older than N days.30
AGENT_BROWSER_ENCRYPTION_KEY64-char hex key for AES-256-GCM session encryption.(none)
AGENT_BROWSER_STREAM_PORTEnable WebSocket streaming on the specified port (e.g., 9223).(disabled)
AGENT_BROWSER_IOS_DEVICEDefault iOS device name for the ios provider.(none)
AGENT_BROWSER_IOS_UDIDDefault iOS device UDID for the ios provider.(none)
AGENT_BROWSER_DEBUGEnable debug output (1 to enable).(disabled)
+ AGENT_BROWSER_AUTO_CONNECT + Auto-discover and connect to a running Chrome instance.(disabled)
+ AGENT_BROWSER_ALLOW_FILE_ACCESS + + Allow file:// URLs to access local files. + (disabled)
+ AGENT_BROWSER_COLOR_SCHEME + + Color scheme preference (dark, light, no-preference). + (none)
+ AGENT_BROWSER_DOWNLOAD_PATH + Default directory for browser downloads.(temp directory)
+ AGENT_BROWSER_RISK_MODE + + Verification/captcha handling mode (off, warn, block + ). + + warn +
+ AGENT_BROWSER_DEFAULT_TIMEOUT + Default Playwright timeout in ms. Keep below 30000 to avoid IPC timeouts. + 25000 +
+ AGENT_BROWSER_SESSION_NAME + Auto-save/load state persistence name.(none)
+ AGENT_BROWSER_STATE_EXPIRE_DAYS + Auto-delete saved session states older than N days. + 30 +
+ AGENT_BROWSER_ENCRYPTION_KEY + 64-char hex key for AES-256-GCM session encryption.(none)
+ AGENT_BROWSER_STREAM_PORT + + Enable WebSocket streaming on the specified port (e.g., 9223). + (disabled)
+ AGENT_BROWSER_IOS_DEVICE + + Default iOS device name for the ios provider. + (none)
+ AGENT_BROWSER_IOS_UDID + + Default iOS device UDID for the ios provider. + (none)
+ AGENT_BROWSER_DEBUG + + Enable debug output (1 to enable). + (disabled)
diff --git a/package.json b/package.json index e21b3dd..d67627d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-browser-stealth", - "version": "0.14.0-fork.4", + "version": "0.14.0-fork.5", "description": "Stealth browser automation CLI for AI agents with anti-bot evasions", "type": "module", "main": "dist/daemon.js", diff --git a/skills/agent-browser/SKILL.md b/skills/agent-browser/SKILL.md index 2926822..c2d5dd9 100644 --- a/skills/agent-browser/SKILL.md +++ b/skills/agent-browser/SKILL.md @@ -51,6 +51,7 @@ agent-browser open https://example.com && agent-browser wait --load networkidle ```bash # Navigation agent-browser open # Navigate (aliases: goto, navigate) +agent-browser --risk-mode block open # Block if verification/captcha interstitial is detected agent-browser close # Close browser agent-browser --version # Show CLI version (fork builds include upstream/fork) @@ -250,7 +251,19 @@ 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. +When a navigation lands on a captcha/verification page, behavior is controlled by `--risk-mode` (or `AGENT_BROWSER_RISK_MODE`): + +- `warn` (default): retry up to 2 times with randomized backoff (3-7s), then return warning plus structured `riskSignals` +- `block`: fail fast once a risk interstitial is detected +- `off`: disable this detection/retry path + +Examples: + +```bash +agent-browser --risk-mode warn open https://example.com +agent-browser --risk-mode block open https://example.com +AGENT_BROWSER_RISK_MODE=off agent-browser open https://example.com +``` ### iOS Simulator (Mobile Safari) @@ -390,6 +403,7 @@ agent-browser click @e2 # Click using ref from annotated screenshot ``` Use annotated screenshots when: + - The page has unlabeled icon buttons or visual-only elements - You need to verify visual layout or styling - Canvas or chart elements are present (invisible to text snapshots) @@ -432,6 +446,7 @@ agent-browser eval -b "$(echo -n 'Array.from(document.querySelectorAll("a")).map **Why this matters:** When the shell processes your command, inner double quotes, `!` characters (history expansion), backticks, and `$()` can all corrupt the JavaScript before it reaches agent-browser. The `--stdin` and `-b` flags bypass shell interpretation entirely. **Rules of thumb:** + - Single-line, no nested quotes -> regular `eval 'expression'` with single quotes is fine - Nested quotes, arrow functions, template literals, or multiline -> use `eval --stdin <<'EVALEOF'` - Programmatic/generated scripts -> use `eval -b` with base64 @@ -451,23 +466,23 @@ Priority (lowest to highest): `~/.agent-browser/config.json` < `./agent-browser. ## Deep-Dive Documentation -| Reference | When to Use | -|-----------|-------------| -| [references/commands.md](references/commands.md) | Full command reference with all options | -| [references/snapshot-refs.md](references/snapshot-refs.md) | Ref lifecycle, invalidation rules, troubleshooting | +| Reference | When to Use | +| -------------------------------------------------------------------- | --------------------------------------------------------- | +| [references/commands.md](references/commands.md) | Full command reference with all options | +| [references/snapshot-refs.md](references/snapshot-refs.md) | Ref lifecycle, invalidation rules, troubleshooting | | [references/session-management.md](references/session-management.md) | Parallel sessions, state persistence, concurrent scraping | -| [references/authentication.md](references/authentication.md) | Login flows, OAuth, 2FA handling, state reuse | -| [references/video-recording.md](references/video-recording.md) | Recording workflows for debugging and documentation | -| [references/profiling.md](references/profiling.md) | Chrome DevTools profiling for performance analysis | -| [references/proxy-support.md](references/proxy-support.md) | Proxy configuration, geo-testing, rotating proxies | +| [references/authentication.md](references/authentication.md) | Login flows, OAuth, 2FA handling, state reuse | +| [references/video-recording.md](references/video-recording.md) | Recording workflows for debugging and documentation | +| [references/profiling.md](references/profiling.md) | Chrome DevTools profiling for performance analysis | +| [references/proxy-support.md](references/proxy-support.md) | Proxy configuration, geo-testing, rotating proxies | ## Ready-to-Use Templates -| Template | Description | -|----------|-------------| -| [templates/form-automation.sh](templates/form-automation.sh) | Form filling with validation | -| [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, reuse state | -| [templates/capture-workflow.sh](templates/capture-workflow.sh) | Content extraction with screenshots | +| Template | Description | +| ------------------------------------------------------------------------ | ----------------------------------- | +| [templates/form-automation.sh](templates/form-automation.sh) | Form filling with validation | +| [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, reuse state | +| [templates/capture-workflow.sh](templates/capture-workflow.sh) | Content extraction with screenshots | ```bash ./templates/form-automation.sh https://example.com/form diff --git a/src/actions.test.ts b/src/actions.test.ts index 91d7afa..0087075 100644 --- a/src/actions.test.ts +++ b/src/actions.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { toAIFriendlyError } from './actions.js'; +import { detectRiskSignals, toAIFriendlyError } from './actions.js'; describe('toAIFriendlyError', () => { describe('element blocked by overlay', () => { @@ -37,3 +37,22 @@ describe('toAIFriendlyError', () => { }); }); }); + +describe('detectRiskSignals', () => { + it('should detect verification patterns from URL and title', () => { + const signals = detectRiskSignals( + 'https://example.com/verify/captcha?scene=anti_bot', + 'Just a moment...' + ); + expect(signals.length).toBeGreaterThan(0); + expect(signals.some((s) => s.source === 'url' && s.code === 'captcha_interstitial')).toBe(true); + expect( + signals.some((s) => s.source === 'title' && s.code === 'verification_interstitial') + ).toBe(true); + }); + + it('should return empty array for normal pages', () => { + const signals = detectRiskSignals('https://example.com/dashboard', 'Dashboard'); + expect(signals).toEqual([]); + }); +}); diff --git a/src/actions.ts b/src/actions.ts index fa8a42e..9860ef8 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -127,7 +127,6 @@ import type { DiffScreenshotCommand, DiffUrlCommand, Annotation, - NavigateData, ScreenshotData, EvaluateData, DiffSnapshotData, @@ -145,6 +144,8 @@ import type { RecordingRestartData, InputEventData, StylesData, + RiskMode, + RiskSignal, } from './types.js'; import { successResponse, errorResponse } from './protocol.js'; import { diffSnapshots, diffScreenshots } from './diff.js'; @@ -526,7 +527,7 @@ async function handleLaunch( async function handleNavigate( command: NavigateCommand, browser: BrowserManager -): Promise> { +): Promise { const page = browser.getPage(); // Set target URL for region auto-detection (locale/timezone) @@ -545,71 +546,125 @@ async function handleNavigate( 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 + const riskMode: RiskMode = command.riskMode ?? 'warn'; + if (riskMode === 'off') { 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); + }); } + // Detect risk interstitials (captcha/verification) and handle by risk mode. + const finalUrl = page.url(); + const title = await page.title(); + let encounteredSignals = detectRiskSignals(finalUrl, title); + if (encounteredSignals.length === 0) { + return successResponse(command.id, { + url: finalUrl, + title, + }); + } + + if (riskMode === 'block') { + const first = encounteredSignals[0]; + return errorResponse( + command.id, + `Navigation blocked by risk-mode=block: ${first.code} (${first.source}="${first.evidence}")` + ); + } + + 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(); + const retrySignals = detectRiskSignals(retryUrl, retryTitle); + if (retrySignals.length === 0) { + return successResponse(command.id, { + url: retryUrl, + title: retryTitle, + warning: + 'Risk interstitial detected and recovered after retry. Review riskSignals for evidence.', + riskSignals: encounteredSignals, + }); + } + encounteredSignals = mergeRiskSignals(encounteredSignals, retrySignals); + } + + // All retries exhausted -- return the page as-is with a warning and evidence. return successResponse(command.id, { - url: finalUrl, - title, + url: page.url(), + title: await page.title(), + warning: + 'Captcha/verification page detected. Try --headed mode or use --session-name for state persistence.', + riskSignals: encounteredSignals, }); } -function isCaptchaPage(url: string, title: string): boolean { +function mergeRiskSignals(current: RiskSignal[], next: RiskSignal[]): RiskSignal[] { + const merged = new Map(); + for (const signal of [...current, ...next]) { + const key = `${signal.code}|${signal.source}|${signal.evidence}`; + if (!merged.has(key) || (merged.get(key)?.confidence ?? 0) < signal.confidence) { + merged.set(key, signal); + } + } + return [...merged.values()]; +} + +/** + * Detect verification/captcha interstitials and return structured risk evidence. + */ +export function detectRiskSignals(url: string, title: string): RiskSignal[] { const lowerUrl = url.toLowerCase(); const lowerTitle = title.toLowerCase(); - const captchaPatterns = [ - '/verify/captcha', - '/captcha', - '/challenge', - 'scene=crawler', - 'scene=anti_bot', - 'recaptcha', - 'hcaptcha', + const urlPatterns: Array<{ pattern: string; code: string; confidence: number }> = [ + { pattern: '/verify/captcha', code: 'captcha_interstitial', confidence: 0.98 }, + { pattern: '/captcha', code: 'captcha_interstitial', confidence: 0.95 }, + { pattern: '/challenge', code: 'verification_interstitial', confidence: 0.93 }, + { pattern: 'scene=crawler', code: 'bot_challenge', confidence: 0.99 }, + { pattern: 'scene=anti_bot', code: 'bot_challenge', confidence: 0.99 }, + { pattern: 'recaptcha', code: 'captcha_interstitial', confidence: 0.97 }, + { pattern: 'hcaptcha', code: 'captcha_interstitial', confidence: 0.97 }, ]; - const titlePatterns = [ - 'verify', - 'captcha', - 'challenge', - 'attention required', - 'just a moment', - 'checking your browser', - 'access denied', - '驗證', - '验证', - '人机验证', + const titlePatterns: Array<{ pattern: string; code: string; confidence: number }> = [ + { pattern: 'verify', code: 'verification_interstitial', confidence: 0.78 }, + { pattern: 'captcha', code: 'captcha_interstitial', confidence: 0.9 }, + { pattern: 'challenge', code: 'verification_interstitial', confidence: 0.8 }, + { pattern: 'attention required', code: 'verification_interstitial', confidence: 0.96 }, + { pattern: 'just a moment', code: 'verification_interstitial', confidence: 0.95 }, + { pattern: 'checking your browser', code: 'verification_interstitial', confidence: 0.97 }, + { pattern: 'access denied', code: 'access_gate', confidence: 0.86 }, + { pattern: '驗證', code: 'verification_interstitial', confidence: 0.88 }, + { pattern: '验证', code: 'verification_interstitial', confidence: 0.88 }, + { pattern: '人机验证', code: 'captcha_interstitial', confidence: 0.95 }, ]; - return ( - captchaPatterns.some((p) => lowerUrl.includes(p)) || - titlePatterns.some((p) => lowerTitle.includes(p)) - ); + const signals: RiskSignal[] = []; + for (const item of urlPatterns) { + if (lowerUrl.includes(item.pattern)) { + signals.push({ + code: item.code, + source: 'url', + evidence: item.pattern, + confidence: item.confidence, + }); + } + } + for (const item of titlePatterns) { + if (lowerTitle.includes(item.pattern)) { + signals.push({ + code: item.code, + source: 'title', + evidence: item.pattern, + confidence: item.confidence, + }); + } + } + return mergeRiskSignals([], signals); } function bezierPoint(t: number, p0: number, p1: number, p2: number, p3: number): number { diff --git a/src/protocol.test.ts b/src/protocol.test.ts index 1c77f94..4a34c97 100644 --- a/src/protocol.test.ts +++ b/src/protocol.test.ts @@ -44,11 +44,38 @@ describe('parseCommand', () => { } }); + it('should parse navigate with riskMode', () => { + const result = parseCommand( + cmd({ + id: '1', + action: 'navigate', + url: 'https://example.com', + riskMode: 'block', + }) + ); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.riskMode).toBe('block'); + } + }); + it('should reject navigate without url', () => { const result = parseCommand(cmd({ id: '1', action: 'navigate' })); expect(result.success).toBe(false); }); + it('should reject navigate with invalid riskMode', () => { + const result = parseCommand( + cmd({ + id: '1', + action: 'navigate', + url: 'https://example.com', + riskMode: 'invalid', + }) + ); + expect(result.success).toBe(false); + }); + it('should parse back command', () => { const result = parseCommand(cmd({ id: '1', action: 'back' })); expect(result.success).toBe(true); diff --git a/src/protocol.ts b/src/protocol.ts index f635348..3aa2f35 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -59,6 +59,7 @@ const navigateSchema = baseCommandSchema.extend({ url: z.string().min(1), waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle']).optional(), headers: z.record(z.string()).optional(), + riskMode: z.enum(['off', 'warn', 'block']).optional(), }); const clickSchema = baseCommandSchema.extend({ diff --git a/src/types.ts b/src/types.ts index eb3742b..620ffb4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,6 +6,15 @@ export interface BaseCommand { action: string; } +export type RiskMode = 'off' | 'warn' | 'block'; + +export interface RiskSignal { + code: string; + source: 'url' | 'title'; + evidence: string; + confidence: number; +} + // Action-specific command types export interface LaunchCommand extends BaseCommand { action: 'launch'; @@ -41,6 +50,8 @@ export interface NavigateCommand extends BaseCommand { url: string; waitUntil?: 'load' | 'domcontentloaded' | 'networkidle'; headers?: Record; + // off: skip detection/retry, warn: retry then return warning+riskSignals, block: fail fast + riskMode?: RiskMode; } export interface ClickCommand extends BaseCommand { @@ -1074,6 +1085,8 @@ export interface NavigateData { url: string; title: string; warning?: string; + // Structured evidence emitted when verification/captcha patterns are detected. + riskSignals?: RiskSignal[]; } export interface Annotation {