Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e005c7251b |
@@ -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
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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]
|
||||||
|
|||||||
@@ -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.";
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user