Compare commits

..
16 changed files with 844 additions and 242 deletions
+160 -124
View File
@@ -1,163 +1,199 @@
# agent-browser-stealth # 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: <https://github.com/vercel-labs/agent-browser>
- local help: `agent-browser --help`
- Core commands and workflows: aligned with upstream `agent-browser` ## What This Fork Optimizes
- Fork value: stronger anti-bot defaults and operational policies
- Default mindset: no extra stealth toggle, stealth is always on
## 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 ```bash
npm install -g agent-browser-stealth npm install -g agent-browser-stealth
agent-browser install agent-browser install
``` ```
### Quick try with npx ### Minimal Usage
```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
```bash ```bash
agent-browser open https://example.com agent-browser open https://example.com
agent-browser snapshot -i agent-browser snapshot -i
agent-browser click @e2 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. ```mermaid
flowchart TD
### 1) Fingerprint hardening A["Command Input"] --> B["Stealth Policy Resolver"]
B --> C["Connection Mode Detection"]
- Hides automation indicators such as `navigator.webdriver` C --> D["Launch Layer: Chromium Args"]
- Adds Chromium launch args to reduce automation fingerprints C --> E["CDP Layer: UA + Metadata Override"]
- Rewrites headless UA markers (`HeadlessChrome`) C --> F["Context Layer: Init Script Patches"]
- Patches high-signal surfaces such as: D --> G["Behavior Layer: Humanized Interaction"]
- `navigator.plugins` / `navigator.mimeTypes` E --> G
- `window.chrome.runtime` F --> G
- WebGL vendor/renderer exposure G --> H["Risk Layer: Verification Detection and Handling"]
- permissions/language/media/device related probes H --> I["Response with warnings and riskSignals"]
- 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
``` ```
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 ```bash
agent-browser type @e2 -- "--delay 120" agent-browser --risk-mode warn open https://example.com
agent-browser keyboard type -- "--delay 120" 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/) - Prefer `--headed` for high-friction targets.
- [CreepJS](https://abrahamjuliot.github.io/creepjs/) - Reuse session state with `--session-name` for continuity.
- [areyouheadless](https://arh.antoinevastel.com/bots/areyouheadless) - Keep locale/timezone consistent with target market.
- [detect-headless](https://infosimples.github.io/detect-headless) - 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 ```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 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`. This fork intentionally keeps command workflows close to upstream while concentrating custom behavior in stealth, policy, and anti-detection handling.
- 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: `<upstream>-fork.<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
```
## License ## License
+1 -1
View File
@@ -4,7 +4,7 @@ version = 4
[[package]] [[package]]
name = "agent-browser-stealth" name = "agent-browser-stealth"
version = "0.14.0-fork.4" version = "0.14.0-fork.5"
dependencies = [ dependencies = [
"base64", "base64",
"dirs", "dirs",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "agent-browser-stealth" name = "agent-browser-stealth"
version = "0.14.0-fork.4" version = "0.14.0-fork.5"
edition = "2021" edition = "2021"
description = "Stealth browser automation CLI for AI agents with anti-bot evasions" description = "Stealth browser automation CLI for AI agents with anti-bot evasions"
license = "Apache-2.0" license = "Apache-2.0"
+22
View File
@@ -182,6 +182,19 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
nav_cmd["iosDevice"] = json!(device); nav_cmd["iosDevice"] = json!(device);
} }
} }
if let Some(ref risk_mode) = flags.risk_mode {
if matches!(risk_mode.as_str(), "off" | "warn" | "block") {
nav_cmd["riskMode"] = json!(risk_mode);
} else {
return Err(ParseError::InvalidValue {
message: format!(
"Invalid --risk-mode value: {} (expected off, warn, or block)",
risk_mode
),
usage: "open <url>",
});
}
}
Ok(nav_cmd) Ok(nav_cmd)
} }
"back" => Ok(json!({ "id": id, "action": "back" })), "back" => Ok(json!({ "id": id, "action": "back" })),
@@ -2028,6 +2041,7 @@ mod tests {
annotate: false, annotate: false,
color_scheme: None, color_scheme: None,
download_path: None, download_path: None,
risk_mode: None,
} }
} }
@@ -2293,6 +2307,14 @@ mod tests {
assert_eq!(cmd["headers"]["Authorization"], "Bearer token"); 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] #[test]
fn test_navigate_with_multiple_headers() { fn test_navigate_with_multiple_headers() {
let mut flags = default_flags(); let mut flags = default_flags();
+32 -1
View File
@@ -34,6 +34,7 @@ pub struct Config {
pub annotate: Option<bool>, pub annotate: Option<bool>,
pub color_scheme: Option<String>, pub color_scheme: Option<String>,
pub download_path: Option<String>, pub download_path: Option<String>,
pub risk_mode: Option<String>,
} }
impl Config { impl Config {
@@ -68,6 +69,7 @@ impl Config {
annotate: other.annotate.or(self.annotate), annotate: other.annotate.or(self.annotate),
color_scheme: other.color_scheme.or(self.color_scheme), color_scheme: other.color_scheme.or(self.color_scheme),
download_path: other.download_path.or(self.download_path), 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<Option<String>> {
"--color-scheme", "--color-scheme",
"--channel", "--channel",
"--download-path", "--download-path",
"--risk-mode",
]; ];
let mut i = 0; let mut i = 0;
while i < args.len() { while i < args.len() {
@@ -204,6 +207,9 @@ pub struct Flags {
pub annotate: bool, pub annotate: bool,
pub color_scheme: Option<String>, pub color_scheme: Option<String>,
pub download_path: Option<String>, pub download_path: Option<String>,
/// How verification/captcha detections are handled on navigation:
/// `off` (disable), `warn` (retry and warn), `block` (fail fast).
pub risk_mode: Option<String>,
// Track which launch-time options were explicitly passed via CLI // Track which launch-time options were explicitly passed via CLI
// (as opposed to being set only via environment variables) // (as opposed to being set only via environment variables)
@@ -285,6 +291,10 @@ pub fn parse_flags(args: &[String]) -> Flags {
.or(config.color_scheme), .or(config.color_scheme),
download_path: env::var("AGENT_BROWSER_DOWNLOAD_PATH").ok() download_path: env::var("AGENT_BROWSER_DOWNLOAD_PATH").ok()
.or(config.download_path), .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_executable_path: false,
cli_extensions: false, cli_extensions: false,
cli_state: false, cli_state: false,
@@ -456,6 +466,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1; i += 1;
} }
} }
"--risk-mode" => {
if let Some(s) = args.get(i + 1) {
flags.risk_mode = Some(s.to_ascii_lowercase());
i += 1;
}
}
"--config" => { "--config" => {
// Already handled by load_config(); skip the value // Already handled by load_config(); skip the value
i += 1; i += 1;
@@ -500,6 +516,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
"--session-name", "--session-name",
"--color-scheme", "--color-scheme",
"--download-path", "--download-path",
"--risk-mode",
"--config", "--config",
]; ];
@@ -697,6 +714,18 @@ mod tests {
assert!(!flags.cli_download_path); 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] #[test]
fn test_cli_multiple_flags_tracking() { fn test_cli_multiple_flags_tracking() {
let flags = parse_flags(&args( let flags = parse_flags(&args(
@@ -732,7 +761,8 @@ mod tests {
"allowFileAccess": true, "allowFileAccess": true,
"cdp": "9222", "cdp": "9222",
"autoConnect": true, "autoConnect": true,
"headers": "{\"Auth\":\"token\"}" "headers": "{\"Auth\":\"token\"}",
"riskMode": "block"
}"#; }"#;
let config: Config = serde_json::from_str(json).unwrap(); let config: Config = serde_json::from_str(json).unwrap();
assert_eq!(config.headed, Some(true)); assert_eq!(config.headed, Some(true));
@@ -758,6 +788,7 @@ mod tests {
assert_eq!(config.cdp.as_deref(), Some("9222")); assert_eq!(config.cdp.as_deref(), Some("9222"));
assert_eq!(config.auto_connect, Some(true)); assert_eq!(config.auto_connect, Some(true));
assert_eq!(config.headers.as_deref(), Some("{\"Auth\":\"token\"}")); assert_eq!(config.headers.as_deref(), Some("{\"Auth\":\"token\"}"));
assert_eq!(config.risk_mode.as_deref(), Some("block"));
} }
#[test] #[test]
+15
View File
@@ -153,6 +153,21 @@ fn main() {
return; 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") { if args.iter().any(|a| a == "--profile") {
let msg = let msg =
"Project policy: --profile is forbidden. Use your existing browser and --session-name for state persistence."; "Project policy: --profile is forbidden. Use your existing browser and --session-name for state persistence.";
+32
View File
@@ -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()) { if let Some(warning) = data.get("warning").and_then(|v| v.as_str()) {
println!("{} {}", color::warning_indicator(), warning); 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; return;
} }
println!("{}", url); println!("{}", url);
@@ -590,10 +617,12 @@ Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--headers <json> Set HTTP headers (scoped to this origin) --headers <json> Set HTTP headers (scoped to this origin)
--risk-mode <mode> Risk handling for verify/captcha pages: off, warn, block
--headed Show browser window --headed Show browser window
Examples: Examples:
agent-browser open example.com agent-browser open example.com
agent-browser --risk-mode block open example.com
agent-browser open https://github.com agent-browser open https://github.com
agent-browser open localhost:3000 agent-browser open localhost:3000
agent-browser open api.example.com --headers '{"Authorization": "Bearer token"}' 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) 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) --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) --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)
--session-name <name> Auto-save/restore session state (cookies, localStorage) --session-name <name> Auto-save/restore session state (cookies, localStorage)
--config <path> Use a custom config file (or AGENT_BROWSER_CONFIG env) --config <path> Use a custom config file (or AGENT_BROWSER_CONFIG env)
--debug Debug output --debug Debug output
@@ -2186,6 +2216,7 @@ Environment:
AGENT_BROWSER_TIMEZONE Override auto-detected timezone (e.g., Asia/Taipei) AGENT_BROWSER_TIMEZONE Override auto-detected timezone (e.g., Asia/Taipei)
AGENT_BROWSER_COLOR_SCHEME Color scheme preference (dark, light, no-preference) AGENT_BROWSER_COLOR_SCHEME Color scheme preference (dark, light, no-preference)
AGENT_BROWSER_DOWNLOAD_PATH Default download directory for browser downloads 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_DEFAULT_TIMEOUT Default Playwright timeout in ms (default: 25000)
AGENT_BROWSER_SESSION_NAME Auto-save/load state persistence name 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) 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 --cdp 9222 snapshot # Connect via CDP port
agent-browser --auto-connect snapshot # Auto-discover running Chrome agent-browser --auto-connect snapshot # Auto-discover running Chrome
agent-browser --color-scheme dark open example.com # Dark mode 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 agent-browser --session-name myapp open example.com # Auto-save/restore state
Command Chaining: Command Chaining:
+13 -2
View File
@@ -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 # Commands
@@ -8,6 +8,7 @@ export const metadata = pageMetadata("commands")
```bash ```bash
agent-browser open <url> # Navigate (aliases: goto, navigate) agent-browser open <url> # Navigate (aliases: goto, navigate)
agent-browser --risk-mode block open <url> # Block when verification/captcha interstitial is detected
agent-browser click <sel> # Click element (--new-tab to open in new tab) agent-browser click <sel> # Click element (--new-tab to open in new tab)
agent-browser dblclick <sel> # Double-click agent-browser dblclick <sel> # Double-click
agent-browser fill <sel> <text> # Clear and fill agent-browser fill <sel> <text> # Clear and fill
@@ -110,6 +111,16 @@ agent-browser wait --fn "condition" # Wait for JS condition
agent-browser wait --download [path] # Wait for download 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 ## Downloads
```bash ```bash
+368 -43
View File
@@ -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 # Configuration
@@ -14,13 +14,39 @@ agent-browser checks two locations, merged in priority order:
<table> <table>
<thead> <thead>
<tr><th>Priority</th><th>Location</th><th>Scope</th></tr> <tr>
<th>Priority</th>
<th>Location</th>
<th>Scope</th>
</tr>
</thead> </thead>
<tbody> <tbody>
<tr><td>1 (lowest)</td><td><code>~/.agent-browser/config.json</code></td><td>User-level defaults</td></tr> <tr>
<tr><td>2</td><td><code>./agent-browser.json</code></td><td>Project-level overrides</td></tr> <td>1 (lowest)</td>
<tr><td>3</td><td><code>AGENT_BROWSER_*</code> env vars</td><td>Override config values</td></tr> <td>
<tr><td>4 (highest)</td><td>CLI flags</td><td>Override everything</td></tr> <code>~/.agent-browser/config.json</code>
</td>
<td>User-level defaults</td>
</tr>
<tr>
<td>2</td>
<td>
<code>./agent-browser.json</code>
</td>
<td>Project-level overrides</td>
</tr>
<tr>
<td>3</td>
<td>
<code>AGENT_BROWSER_*</code> env vars
</td>
<td>Override config values</td>
</tr>
<tr>
<td>4 (highest)</td>
<td>CLI flags</td>
<td>Override everything</td>
</tr>
</tbody> </tbody>
</table> </table>
@@ -50,34 +76,229 @@ Every CLI flag can be set in the config file using its camelCase equivalent:
<table> <table>
<thead> <thead>
<tr><th>Config Key</th><th>CLI Flag</th><th>Type</th></tr> <tr>
<th>Config Key</th>
<th>CLI Flag</th>
<th>Type</th>
</tr>
</thead> </thead>
<tbody> <tbody>
<tr><td><code>headed</code></td><td><code>--headed</code></td><td>boolean</td></tr> <tr>
<tr><td><code>json</code></td><td><code>--json</code></td><td>boolean</td></tr> <td>
<tr><td><code>full</code></td><td><code>--full, -f</code></td><td>boolean</td></tr> <code>headed</code>
<tr><td><code>debug</code></td><td><code>--debug</code></td><td>boolean</td></tr> </td>
<tr><td><code>session</code></td><td><code>--session</code></td><td>string</td></tr> <td>
<tr><td><code>sessionName</code></td><td><code>--session-name</code></td><td>string</td></tr> <code>--headed</code>
<tr><td><code>executablePath</code></td><td><code>--executable-path</code></td><td>string</td></tr> </td>
<tr><td><code>extensions</code></td><td><code>--extension</code></td><td>string[]</td></tr> <td>boolean</td>
<tr><td><code>state</code></td><td><code>--state</code></td><td>string</td></tr> </tr>
<tr><td><code>proxy</code></td><td><code>--proxy</code></td><td>string</td></tr> <tr>
<tr><td><code>proxyBypass</code></td><td><code>--proxy-bypass</code></td><td>string</td></tr> <td>
<tr><td><code>args</code></td><td><code>--args</code></td><td>string</td></tr> <code>json</code>
<tr><td><code>userAgent</code></td><td><code>--user-agent</code></td><td>string</td></tr> </td>
<tr><td><code>provider</code></td><td><code>-p, --provider</code></td><td>string</td></tr> <td>
<tr><td><code>device</code></td><td><code>--device</code></td><td>string</td></tr> <code>--json</code>
<tr><td><code>ignoreHttpsErrors</code></td><td><code>--ignore-https-errors</code></td><td>boolean</td></tr> </td>
<tr><td><code>allowFileAccess</code></td><td><code>--allow-file-access</code></td><td>boolean</td></tr> <td>boolean</td>
<tr><td><code>cdp</code></td><td><code>--cdp</code></td><td>string</td></tr> </tr>
<tr><td><code>autoConnect</code></td><td><code>--auto-connect</code></td><td>boolean</td></tr> <tr>
<tr><td><code>colorScheme</code></td><td><code>--color-scheme</code></td><td>string (<code>dark</code>, <code>light</code>, <code>no-preference</code>)</td></tr> <td>
<tr><td><code>downloadPath</code></td><td><code>--download-path</code></td><td>string</td></tr> <code>full</code>
<tr><td><code>headers</code></td><td><code>--headers</code></td><td>string (JSON)</td></tr> </td>
<td>
<code>--full, -f</code>
</td>
<td>boolean</td>
</tr>
<tr>
<td>
<code>debug</code>
</td>
<td>
<code>--debug</code>
</td>
<td>boolean</td>
</tr>
<tr>
<td>
<code>session</code>
</td>
<td>
<code>--session</code>
</td>
<td>string</td>
</tr>
<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>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>
<tr>
<td>
<code>args</code>
</td>
<td>
<code>--args</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>userAgent</code>
</td>
<td>
<code>--user-agent</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>provider</code>
</td>
<td>
<code>-p, --provider</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>device</code>
</td>
<td>
<code>--device</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>ignoreHttpsErrors</code>
</td>
<td>
<code>--ignore-https-errors</code>
</td>
<td>boolean</td>
</tr>
<tr>
<td>
<code>allowFileAccess</code>
</td>
<td>
<code>--allow-file-access</code>
</td>
<td>boolean</td>
</tr>
<tr>
<td>
<code>cdp</code>
</td>
<td>
<code>--cdp</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>autoConnect</code>
</td>
<td>
<code>--auto-connect</code>
</td>
<td>boolean</td>
</tr>
<tr>
<td>
<code>colorScheme</code>
</td>
<td>
<code>--color-scheme</code>
</td>
<td>
string (<code>dark</code>, <code>light</code>, <code>no-preference</code>)
</td>
</tr>
<tr>
<td>
<code>downloadPath</code>
</td>
<td>
<code>--download-path</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>riskMode</code>
</td>
<td>
<code>--risk-mode</code>
</td>
<td>
string (<code>off</code>, <code>warn</code>, <code>block</code>)
</td>
</tr>
<tr>
<td>
<code>headers</code>
</td>
<td>
<code>--headers</code>
</td>
<td>string (JSON)</td>
</tr>
</tbody> </tbody>
</table> </table>
`riskMode` defaults to `warn` when unset.
## Common Configurations ## Common Configurations
### Local Development ### Local Development
@@ -146,21 +367,125 @@ These environment variables configure additional daemon and runtime behavior:
<table> <table>
<thead> <thead>
<tr><th>Variable</th><th>Description</th><th>Default</th></tr> <tr>
<th>Variable</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead> </thead>
<tbody> <tbody>
<tr><td><code>AGENT_BROWSER_AUTO_CONNECT</code></td><td>Auto-discover and connect to a running Chrome instance.</td><td>(disabled)</td></tr> <tr>
<tr><td><code>AGENT_BROWSER_ALLOW_FILE_ACCESS</code></td><td>Allow <code>file://</code> URLs to access local files.</td><td>(disabled)</td></tr> <td>
<tr><td><code>AGENT_BROWSER_COLOR_SCHEME</code></td><td>Color scheme preference (<code>dark</code>, <code>light</code>, <code>no-preference</code>).</td><td>(none)</td></tr> <code>AGENT_BROWSER_AUTO_CONNECT</code>
<tr><td><code>AGENT_BROWSER_DOWNLOAD_PATH</code></td><td>Default directory for browser downloads.</td><td>(temp directory)</td></tr> </td>
<tr><td><code>AGENT_BROWSER_DEFAULT_TIMEOUT</code></td><td>Default Playwright timeout in ms. Keep below 30000 to avoid IPC timeouts.</td><td><code>25000</code></td></tr> <td>Auto-discover and connect to a running Chrome instance.</td>
<tr><td><code>AGENT_BROWSER_SESSION_NAME</code></td><td>Auto-save/load state persistence name.</td><td>(none)</td></tr> <td>(disabled)</td>
<tr><td><code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code></td><td>Auto-delete saved session states older than N days.</td><td><code>30</code></td></tr> </tr>
<tr><td><code>AGENT_BROWSER_ENCRYPTION_KEY</code></td><td>64-char hex key for AES-256-GCM session encryption.</td><td>(none)</td></tr> <tr>
<tr><td><code>AGENT_BROWSER_STREAM_PORT</code></td><td>Enable WebSocket streaming on the specified port (e.g., <code>9223</code>).</td><td>(disabled)</td></tr> <td>
<tr><td><code>AGENT_BROWSER_IOS_DEVICE</code></td><td>Default iOS device name for the <code>ios</code> provider.</td><td>(none)</td></tr> <code>AGENT_BROWSER_ALLOW_FILE_ACCESS</code>
<tr><td><code>AGENT_BROWSER_IOS_UDID</code></td><td>Default iOS device UDID for the <code>ios</code> provider.</td><td>(none)</td></tr> </td>
<tr><td><code>AGENT_BROWSER_DEBUG</code></td><td>Enable debug output (<code>1</code> to enable).</td><td>(disabled)</td></tr> <td>
Allow <code>file://</code> URLs to access local files.
</td>
<td>(disabled)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_COLOR_SCHEME</code>
</td>
<td>
Color scheme preference (<code>dark</code>, <code>light</code>, <code>no-preference</code>).
</td>
<td>(none)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_DOWNLOAD_PATH</code>
</td>
<td>Default directory for browser downloads.</td>
<td>(temp directory)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_RISK_MODE</code>
</td>
<td>
Verification/captcha handling mode (<code>off</code>, <code>warn</code>, <code>block</code>
).
</td>
<td>
<code>warn</code>
</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_DEFAULT_TIMEOUT</code>
</td>
<td>Default Playwright timeout in ms. Keep below 30000 to avoid IPC timeouts.</td>
<td>
<code>25000</code>
</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_SESSION_NAME</code>
</td>
<td>Auto-save/load state persistence name.</td>
<td>(none)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code>
</td>
<td>Auto-delete saved session states older than N days.</td>
<td>
<code>30</code>
</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_ENCRYPTION_KEY</code>
</td>
<td>64-char hex key for AES-256-GCM session encryption.</td>
<td>(none)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_STREAM_PORT</code>
</td>
<td>
Enable WebSocket streaming on the specified port (e.g., <code>9223</code>).
</td>
<td>(disabled)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_IOS_DEVICE</code>
</td>
<td>
Default iOS device name for the <code>ios</code> provider.
</td>
<td>(none)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_IOS_UDID</code>
</td>
<td>
Default iOS device UDID for the <code>ios</code> provider.
</td>
<td>(none)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_DEBUG</code>
</td>
<td>
Enable debug output (<code>1</code> to enable).
</td>
<td>(disabled)</td>
</tr>
</tbody> </tbody>
</table> </table>
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "agent-browser-stealth", "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", "description": "Stealth browser automation CLI for AI agents with anti-bot evasions",
"type": "module", "type": "module",
"main": "dist/daemon.js", "main": "dist/daemon.js",
+29 -14
View File
@@ -51,6 +51,7 @@ agent-browser open https://example.com && agent-browser wait --load networkidle
```bash ```bash
# Navigation # Navigation
agent-browser open <url> # Navigate (aliases: goto, navigate) agent-browser open <url> # Navigate (aliases: goto, navigate)
agent-browser --risk-mode block open <url> # Block if verification/captcha interstitial is detected
agent-browser close # Close browser agent-browser close # Close browser
agent-browser --version # Show CLI version (fork builds include upstream/fork) 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 ### 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) ### iOS Simulator (Mobile Safari)
@@ -390,6 +403,7 @@ agent-browser click @e2 # Click using ref from annotated screenshot
``` ```
Use annotated screenshots when: Use annotated screenshots when:
- The page has unlabeled icon buttons or visual-only elements - The page has unlabeled icon buttons or visual-only elements
- You need to verify visual layout or styling - You need to verify visual layout or styling
- Canvas or chart elements are present (invisible to text snapshots) - 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. **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:** **Rules of thumb:**
- Single-line, no nested quotes -> regular `eval 'expression'` with single quotes is fine - 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'` - Nested quotes, arrow functions, template literals, or multiline -> use `eval --stdin <<'EVALEOF'`
- Programmatic/generated scripts -> use `eval -b` with base64 - 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 ## Deep-Dive Documentation
| Reference | When to Use | | Reference | When to Use |
|-----------|-------------| | -------------------------------------------------------------------- | --------------------------------------------------------- |
| [references/commands.md](references/commands.md) | Full command reference with all options | | [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/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/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/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/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/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/proxy-support.md](references/proxy-support.md) | Proxy configuration, geo-testing, rotating proxies |
## Ready-to-Use Templates ## Ready-to-Use Templates
| Template | Description | | Template | Description |
|----------|-------------| | ------------------------------------------------------------------------ | ----------------------------------- |
| [templates/form-automation.sh](templates/form-automation.sh) | Form filling with validation | | [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/authenticated-session.sh](templates/authenticated-session.sh) | Login once, reuse state |
| [templates/capture-workflow.sh](templates/capture-workflow.sh) | Content extraction with screenshots | | [templates/capture-workflow.sh](templates/capture-workflow.sh) | Content extraction with screenshots |
```bash ```bash
./templates/form-automation.sh https://example.com/form ./templates/form-automation.sh https://example.com/form
+20 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { toAIFriendlyError } from './actions.js'; import { detectRiskSignals, toAIFriendlyError } from './actions.js';
describe('toAIFriendlyError', () => { describe('toAIFriendlyError', () => {
describe('element blocked by overlay', () => { 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([]);
});
});
+109 -54
View File
@@ -127,7 +127,6 @@ import type {
DiffScreenshotCommand, DiffScreenshotCommand,
DiffUrlCommand, DiffUrlCommand,
Annotation, Annotation,
NavigateData,
ScreenshotData, ScreenshotData,
EvaluateData, EvaluateData,
DiffSnapshotData, DiffSnapshotData,
@@ -145,6 +144,8 @@ import type {
RecordingRestartData, RecordingRestartData,
InputEventData, InputEventData,
StylesData, StylesData,
RiskMode,
RiskSignal,
} from './types.js'; } from './types.js';
import { successResponse, errorResponse } from './protocol.js'; import { successResponse, errorResponse } from './protocol.js';
import { diffSnapshots, diffScreenshots } from './diff.js'; import { diffSnapshots, diffScreenshots } from './diff.js';
@@ -526,7 +527,7 @@ async function handleLaunch(
async function handleNavigate( async function handleNavigate(
command: NavigateCommand, command: NavigateCommand,
browser: BrowserManager browser: BrowserManager
): Promise<Response<NavigateData>> { ): Promise<Response> {
const page = browser.getPage(); const page = browser.getPage();
// Set target URL for region auto-detection (locale/timezone) // Set target URL for region auto-detection (locale/timezone)
@@ -545,71 +546,125 @@ async function handleNavigate(
waitUntil: command.waitUntil ?? 'load', waitUntil: command.waitUntil ?? 'load',
}); });
// Detect captcha/verification pages and retry with backoff const riskMode: RiskMode = command.riskMode ?? 'warn';
const finalUrl = page.url(); if (riskMode === 'off') {
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, { return successResponse(command.id, {
url: page.url(), url: page.url(),
title: await page.title(), 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, { return successResponse(command.id, {
url: finalUrl, url: page.url(),
title, 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<string, RiskSignal>();
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 lowerUrl = url.toLowerCase();
const lowerTitle = title.toLowerCase(); const lowerTitle = title.toLowerCase();
const captchaPatterns = [ const urlPatterns: Array<{ pattern: string; code: string; confidence: number }> = [
'/verify/captcha', { pattern: '/verify/captcha', code: 'captcha_interstitial', confidence: 0.98 },
'/captcha', { pattern: '/captcha', code: 'captcha_interstitial', confidence: 0.95 },
'/challenge', { pattern: '/challenge', code: 'verification_interstitial', confidence: 0.93 },
'scene=crawler', { pattern: 'scene=crawler', code: 'bot_challenge', confidence: 0.99 },
'scene=anti_bot', { pattern: 'scene=anti_bot', code: 'bot_challenge', confidence: 0.99 },
'recaptcha', { pattern: 'recaptcha', code: 'captcha_interstitial', confidence: 0.97 },
'hcaptcha', { pattern: 'hcaptcha', code: 'captcha_interstitial', confidence: 0.97 },
]; ];
const titlePatterns = [ const titlePatterns: Array<{ pattern: string; code: string; confidence: number }> = [
'verify', { pattern: 'verify', code: 'verification_interstitial', confidence: 0.78 },
'captcha', { pattern: 'captcha', code: 'captcha_interstitial', confidence: 0.9 },
'challenge', { pattern: 'challenge', code: 'verification_interstitial', confidence: 0.8 },
'attention required', { pattern: 'attention required', code: 'verification_interstitial', confidence: 0.96 },
'just a moment', { pattern: 'just a moment', code: 'verification_interstitial', confidence: 0.95 },
'checking your browser', { pattern: 'checking your browser', code: 'verification_interstitial', confidence: 0.97 },
'access denied', { 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 ( const signals: RiskSignal[] = [];
captchaPatterns.some((p) => lowerUrl.includes(p)) || for (const item of urlPatterns) {
titlePatterns.some((p) => lowerTitle.includes(p)) 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 { function bezierPoint(t: number, p0: number, p1: number, p2: number, p3: number): number {
+27
View File
@@ -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', () => { it('should reject navigate without url', () => {
const result = parseCommand(cmd({ id: '1', action: 'navigate' })); const result = parseCommand(cmd({ id: '1', action: 'navigate' }));
expect(result.success).toBe(false); 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', () => { it('should parse back command', () => {
const result = parseCommand(cmd({ id: '1', action: 'back' })); const result = parseCommand(cmd({ id: '1', action: 'back' }));
expect(result.success).toBe(true); expect(result.success).toBe(true);
+1
View File
@@ -59,6 +59,7 @@ const navigateSchema = baseCommandSchema.extend({
url: z.string().min(1), url: z.string().min(1),
waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle']).optional(), waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle']).optional(),
headers: z.record(z.string()).optional(), headers: z.record(z.string()).optional(),
riskMode: z.enum(['off', 'warn', 'block']).optional(),
}); });
const clickSchema = baseCommandSchema.extend({ const clickSchema = baseCommandSchema.extend({
+13
View File
@@ -6,6 +6,15 @@ export interface BaseCommand {
action: string; action: string;
} }
export type RiskMode = 'off' | 'warn' | 'block';
export interface RiskSignal {
code: string;
source: 'url' | 'title';
evidence: string;
confidence: number;
}
// Action-specific command types // Action-specific command types
export interface LaunchCommand extends BaseCommand { export interface LaunchCommand extends BaseCommand {
action: 'launch'; action: 'launch';
@@ -41,6 +50,8 @@ export interface NavigateCommand extends BaseCommand {
url: string; url: string;
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle'; waitUntil?: 'load' | 'domcontentloaded' | 'networkidle';
headers?: Record<string, string>; headers?: Record<string, string>;
// off: skip detection/retry, warn: retry then return warning+riskSignals, block: fail fast
riskMode?: RiskMode;
} }
export interface ClickCommand extends BaseCommand { export interface ClickCommand extends BaseCommand {
@@ -1074,6 +1085,8 @@ export interface NavigateData {
url: string; url: string;
title: string; title: string;
warning?: string; warning?: string;
// Structured evidence emitted when verification/captcha patterns are detected.
riskSignals?: RiskSignal[];
} }
export interface Annotation { export interface Annotation {