Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e005c7251b |
@@ -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: <https://github.com/vercel-labs/agent-browser>
|
||||
- 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: `<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
|
||||
```
|
||||
This fork intentionally keeps command workflows close to upstream while concentrating custom behavior in stealth, policy, and anti-detection handling.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -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",
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -182,6 +182,19 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
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)
|
||||
}
|
||||
"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();
|
||||
|
||||
+32
-1
@@ -34,6 +34,7 @@ pub struct Config {
|
||||
pub annotate: Option<bool>,
|
||||
pub color_scheme: Option<String>,
|
||||
pub download_path: Option<String>,
|
||||
pub risk_mode: Option<String>,
|
||||
}
|
||||
|
||||
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<Option<String>> {
|
||||
"--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<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
|
||||
// (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<String> {
|
||||
"--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]
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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 <name> Use specific session
|
||||
--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
|
||||
|
||||
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 <scheme> Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME)
|
||||
--download-path <path> Default download directory (or AGENT_BROWSER_DOWNLOAD_PATH)
|
||||
--risk-mode <mode> Verify/captcha handling: off, warn, block (or AGENT_BROWSER_RISK_MODE)
|
||||
--session-name <name> Auto-save/restore session state (cookies, localStorage)
|
||||
--config <path> Use a custom config file (or AGENT_BROWSER_CONFIG env)
|
||||
--debug Debug output
|
||||
@@ -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:
|
||||
|
||||
@@ -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 <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 dblclick <sel> # Double-click
|
||||
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
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Priority</th><th>Location</th><th>Scope</th></tr>
|
||||
<tr>
|
||||
<th>Priority</th>
|
||||
<th>Location</th>
|
||||
<th>Scope</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>1 (lowest)</td><td><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>
|
||||
<tr>
|
||||
<td>1 (lowest)</td>
|
||||
<td>
|
||||
<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>
|
||||
</table>
|
||||
|
||||
@@ -50,34 +76,229 @@ Every CLI flag can be set in the config file using its camelCase equivalent:
|
||||
|
||||
<table>
|
||||
<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>
|
||||
<tbody>
|
||||
<tr><td><code>headed</code></td><td><code>--headed</code></td><td>boolean</td></tr>
|
||||
<tr><td><code>json</code></td><td><code>--json</code></td><td>boolean</td></tr>
|
||||
<tr><td><code>full</code></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>headers</code></td><td><code>--headers</code></td><td>string (JSON)</td></tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>headed</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>--headed</code>
|
||||
</td>
|
||||
<td>boolean</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>json</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>--json</code>
|
||||
</td>
|
||||
<td>boolean</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>full</code>
|
||||
</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>
|
||||
</table>
|
||||
|
||||
`riskMode` defaults to `warn` when unset.
|
||||
|
||||
## Common Configurations
|
||||
|
||||
### Local Development
|
||||
@@ -146,21 +367,125 @@ These environment variables configure additional daemon and runtime behavior:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Description</th><th>Default</th></tr>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Description</th>
|
||||
<th>Default</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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><td><code>AGENT_BROWSER_ALLOW_FILE_ACCESS</code></td><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_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>
|
||||
<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>
|
||||
<td>
|
||||
<code>AGENT_BROWSER_ALLOW_FILE_ACCESS</code>
|
||||
</td>
|
||||
<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>
|
||||
</table>
|
||||
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -51,6 +51,7 @@ agent-browser open https://example.com && agent-browser wait --load networkidle
|
||||
```bash
|
||||
# Navigation
|
||||
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 --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
|
||||
|
||||
+20
-1
@@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
+109
-54
@@ -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<Response<NavigateData>> {
|
||||
): Promise<Response> {
|
||||
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<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 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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<string, string>;
|
||||
// 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 {
|
||||
|
||||
Reference in New Issue
Block a user