feat(stealth): add risk-mode signals and document stealth architecture

This commit is contained in:
leeguooooo
2026-02-25 10:27:45 +09:00
parent 11eab471f1
commit e005c7251b
16 changed files with 844 additions and 242 deletions
+1 -1
View File
@@ -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
View File
@@ -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"
+22
View File
@@ -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
View File
@@ -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]
+15
View File
@@ -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.";
+32
View File
@@ -25,6 +25,33 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
if let Some(warning) = data.get("warning").and_then(|v| v.as_str()) {
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: