feat: add CDP tab-group plugin handshake with silent fallback
This commit is contained in:
@@ -2055,8 +2055,10 @@ mod tests {
|
||||
color_scheme: None,
|
||||
download_path: None,
|
||||
tab_group: None,
|
||||
tab_group_plugin_id: None,
|
||||
risk_mode: None,
|
||||
cli_tab_group: false,
|
||||
cli_tab_group_plugin_id: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -228,6 +228,7 @@ pub fn ensure_daemon(
|
||||
debug: bool,
|
||||
download_path: Option<&str>,
|
||||
tab_group: Option<&str>,
|
||||
tab_group_plugin_id: Option<&str>,
|
||||
) -> Result<DaemonResult, String> {
|
||||
// Check if daemon is running AND responsive
|
||||
if is_daemon_running(session) && daemon_ready(session) {
|
||||
@@ -378,6 +379,9 @@ pub fn ensure_daemon(
|
||||
if let Some(tg) = tab_group {
|
||||
cmd.env("AGENT_BROWSER_TAB_GROUP", tg);
|
||||
}
|
||||
if let Some(plugin_id) = tab_group_plugin_id {
|
||||
cmd.env("AGENT_BROWSER_TAB_GROUP_PLUGIN_ID", plugin_id);
|
||||
}
|
||||
|
||||
// Create new process group and session to fully detach
|
||||
unsafe {
|
||||
@@ -468,6 +472,9 @@ pub fn ensure_daemon(
|
||||
if let Some(tg) = tab_group {
|
||||
cmd.env("AGENT_BROWSER_TAB_GROUP", tg);
|
||||
}
|
||||
if let Some(plugin_id) = tab_group_plugin_id {
|
||||
cmd.env("AGENT_BROWSER_TAB_GROUP_PLUGIN_ID", plugin_id);
|
||||
}
|
||||
|
||||
// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
|
||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
||||
|
||||
+166
-3
@@ -1,4 +1,4 @@
|
||||
use crate::color;
|
||||
use crate::{color, validation};
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
@@ -7,6 +7,8 @@ use std::path::{Path, PathBuf};
|
||||
const CONFIG_DIR: &str = ".agent-browser";
|
||||
const CONFIG_FILENAME: &str = "config.json";
|
||||
const PROJECT_CONFIG_FILENAME: &str = "agent-browser.json";
|
||||
const DEFAULT_TAB_GROUP: &str = "Agent Browser Stealth";
|
||||
const DEFAULT_TAB_GROUP_PLUGIN_ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
@@ -35,6 +37,7 @@ pub struct Config {
|
||||
pub color_scheme: Option<String>,
|
||||
pub download_path: Option<String>,
|
||||
pub tab_group: Option<String>,
|
||||
pub tab_group_plugin_id: Option<String>,
|
||||
pub risk_mode: Option<String>,
|
||||
}
|
||||
|
||||
@@ -71,6 +74,7 @@ impl Config {
|
||||
color_scheme: other.color_scheme.or(self.color_scheme),
|
||||
download_path: other.download_path.or(self.download_path),
|
||||
tab_group: other.tab_group.or(self.tab_group),
|
||||
tab_group_plugin_id: other.tab_group_plugin_id.or(self.tab_group_plugin_id),
|
||||
risk_mode: other.risk_mode.or(self.risk_mode),
|
||||
}
|
||||
}
|
||||
@@ -139,6 +143,7 @@ fn extract_config_path(args: &[String]) -> Option<Option<String>> {
|
||||
"--channel",
|
||||
"--download-path",
|
||||
"--tab-group",
|
||||
"--tab-group-plugin-id",
|
||||
"--risk-mode",
|
||||
];
|
||||
let mut i = 0;
|
||||
@@ -206,11 +211,12 @@ pub struct Flags {
|
||||
pub allow_file_access: bool,
|
||||
pub device: Option<String>,
|
||||
pub auto_connect: bool,
|
||||
pub session_name: Option<String>,
|
||||
pub session_name: Option<String>, // Defaults to --session when unset
|
||||
pub annotate: bool,
|
||||
pub color_scheme: Option<String>,
|
||||
pub download_path: Option<String>,
|
||||
pub tab_group: Option<String>,
|
||||
pub tab_group_plugin_id: Option<String>,
|
||||
/// How verification/captcha detections are handled on navigation:
|
||||
/// `off` (disable), `warn` (retry and warn), `block` (fail fast).
|
||||
pub risk_mode: Option<String>,
|
||||
@@ -228,6 +234,7 @@ pub struct Flags {
|
||||
pub cli_annotate: bool,
|
||||
pub cli_download_path: bool,
|
||||
pub cli_tab_group: bool,
|
||||
pub cli_tab_group_plugin_id: bool,
|
||||
}
|
||||
|
||||
pub fn parse_flags(args: &[String]) -> Flags {
|
||||
@@ -296,7 +303,14 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
.or(config.color_scheme),
|
||||
download_path: env::var("AGENT_BROWSER_DOWNLOAD_PATH").ok()
|
||||
.or(config.download_path),
|
||||
tab_group: env::var("AGENT_BROWSER_TAB_GROUP").ok().or(config.tab_group),
|
||||
tab_group: env::var("AGENT_BROWSER_TAB_GROUP")
|
||||
.ok()
|
||||
.or(config.tab_group)
|
||||
.or_else(|| Some(DEFAULT_TAB_GROUP.to_string())),
|
||||
tab_group_plugin_id: env::var("AGENT_BROWSER_TAB_GROUP_PLUGIN_ID")
|
||||
.ok()
|
||||
.or(config.tab_group_plugin_id)
|
||||
.or_else(|| Some(DEFAULT_TAB_GROUP_PLUGIN_ID.to_string())),
|
||||
risk_mode: env::var("AGENT_BROWSER_RISK_MODE")
|
||||
.ok()
|
||||
.or(config.risk_mode)
|
||||
@@ -312,6 +326,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
cli_annotate: false,
|
||||
cli_download_path: false,
|
||||
cli_tab_group: false,
|
||||
cli_tab_group_plugin_id: false,
|
||||
};
|
||||
|
||||
let mut i = 0;
|
||||
@@ -480,6 +495,13 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--tab-group-plugin-id" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
flags.tab_group_plugin_id = Some(s.clone());
|
||||
flags.cli_tab_group_plugin_id = true;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--risk-mode" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
flags.risk_mode = Some(s.to_ascii_lowercase());
|
||||
@@ -494,6 +516,18 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Keep auth/state continuity stable by default: if no explicit --session-name
|
||||
// is provided, derive it from --session (or fall back to "default" when invalid).
|
||||
if flags.session_name.is_none() {
|
||||
let derived = if validation::is_valid_session_name(&flags.session) {
|
||||
flags.session.clone()
|
||||
} else {
|
||||
"default".to_string()
|
||||
};
|
||||
flags.session_name = Some(derived);
|
||||
}
|
||||
|
||||
flags
|
||||
}
|
||||
|
||||
@@ -531,6 +565,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
||||
"--color-scheme",
|
||||
"--download-path",
|
||||
"--tab-group",
|
||||
"--tab-group-plugin-id",
|
||||
"--risk-mode",
|
||||
"--config",
|
||||
];
|
||||
@@ -566,6 +601,36 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
static ENV_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
struct EnvGuard<'a> {
|
||||
_lock: MutexGuard<'a, ()>,
|
||||
vars: Vec<(String, Option<String>)>,
|
||||
}
|
||||
|
||||
impl<'a> EnvGuard<'a> {
|
||||
fn new(var_names: &[&str]) -> Self {
|
||||
let lock = ENV_MUTEX.lock().unwrap();
|
||||
let vars = var_names
|
||||
.iter()
|
||||
.map(|&name| (name.to_string(), env::var(name).ok()))
|
||||
.collect();
|
||||
Self { _lock: lock, vars }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
for (name, value) in &self.vars {
|
||||
match value {
|
||||
Some(v) => env::set_var(name, v),
|
||||
None => env::remove_var(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn args(s: &str) -> Vec<String> {
|
||||
s.split_whitespace().map(String::from).collect()
|
||||
@@ -679,6 +744,19 @@ mod tests {
|
||||
));
|
||||
assert_eq!(flags.session, "test");
|
||||
assert_eq!(flags.executable_path, Some("/custom/chrome".to_string()));
|
||||
assert_eq!(flags.session_name.as_deref(), Some("test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_name_defaults_to_session_when_not_provided() {
|
||||
let flags = parse_flags(&args("--session my-session snapshot"));
|
||||
assert_eq!(flags.session_name.as_deref(), Some("my-session"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_session_falls_back_to_default_session_name() {
|
||||
let flags = parse_flags(&args("--session bad/session snapshot"));
|
||||
assert_eq!(flags.session_name.as_deref(), Some("default"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -729,6 +807,23 @@ mod tests {
|
||||
assert!(!flags.cli_download_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_tab_group_is_enabled() {
|
||||
let flags = parse_flags(&args("snapshot"));
|
||||
assert_eq!(flags.tab_group.as_deref(), Some(DEFAULT_TAB_GROUP));
|
||||
assert!(!flags.cli_tab_group);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_tab_group_plugin_id_is_enabled() {
|
||||
let flags = parse_flags(&args("snapshot"));
|
||||
assert_eq!(
|
||||
flags.tab_group_plugin_id.as_deref(),
|
||||
Some(DEFAULT_TAB_GROUP_PLUGIN_ID)
|
||||
);
|
||||
assert!(!flags.cli_tab_group_plugin_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_tab_group_flag() {
|
||||
let input = vec![
|
||||
@@ -747,6 +842,69 @@ mod tests {
|
||||
assert_eq!(cleaned, vec!["open", "example.com"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_tab_group_plugin_id_flag() {
|
||||
let input = vec![
|
||||
"--tab-group-plugin-id".to_string(),
|
||||
"cli-plugin-id".to_string(),
|
||||
"snapshot".to_string(),
|
||||
];
|
||||
let flags = parse_flags(&input);
|
||||
assert_eq!(flags.tab_group_plugin_id.as_deref(), Some("cli-plugin-id"));
|
||||
assert!(flags.cli_tab_group_plugin_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clean_args_removes_tab_group_plugin_id() {
|
||||
let cleaned = clean_args(&args(
|
||||
"--tab-group-plugin-id cli-plugin-id open example.com",
|
||||
));
|
||||
assert_eq!(cleaned, vec!["open", "example.com"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tab_group_plugin_id_precedence_config_env_cli() {
|
||||
use std::io::Write;
|
||||
|
||||
let _guard = EnvGuard::new(&["AGENT_BROWSER_TAB_GROUP_PLUGIN_ID"]);
|
||||
|
||||
let dir = std::env::temp_dir().join("ab-test-plugin-id-precedence");
|
||||
let _ = fs::create_dir_all(&dir);
|
||||
let config_path = dir.join("config.json");
|
||||
let mut f = fs::File::create(&config_path).unwrap();
|
||||
writeln!(f, r#"{{"tabGroupPluginId":"config-plugin-id"}}"#).unwrap();
|
||||
|
||||
env::set_var("AGENT_BROWSER_TAB_GROUP_PLUGIN_ID", "env-plugin-id");
|
||||
|
||||
let env_args = vec![
|
||||
"--config".to_string(),
|
||||
config_path.to_string_lossy().to_string(),
|
||||
"snapshot".to_string(),
|
||||
];
|
||||
let flags_from_env = parse_flags(&env_args);
|
||||
assert_eq!(
|
||||
flags_from_env.tab_group_plugin_id.as_deref(),
|
||||
Some("env-plugin-id")
|
||||
);
|
||||
|
||||
let cli_args = vec![
|
||||
"--config".to_string(),
|
||||
config_path.to_string_lossy().to_string(),
|
||||
"--tab-group-plugin-id".to_string(),
|
||||
"cli-plugin-id".to_string(),
|
||||
"snapshot".to_string(),
|
||||
];
|
||||
let flags_from_cli = parse_flags(&cli_args);
|
||||
assert_eq!(
|
||||
flags_from_cli.tab_group_plugin_id.as_deref(),
|
||||
Some("cli-plugin-id")
|
||||
);
|
||||
assert!(flags_from_cli.cli_tab_group_plugin_id);
|
||||
|
||||
let _ = fs::remove_file(&config_path);
|
||||
let _ = fs::remove_dir(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_risk_mode_flag() {
|
||||
let flags = parse_flags(&args("--risk-mode block open example.com"));
|
||||
@@ -796,6 +954,7 @@ mod tests {
|
||||
"autoConnect": true,
|
||||
"headers": "{\"Auth\":\"token\"}",
|
||||
"tabGroup": "Agent Browser Stealth",
|
||||
"tabGroupPluginId": "tab-group-plugin-id",
|
||||
"riskMode": "block"
|
||||
}"#;
|
||||
let config: Config = serde_json::from_str(json).unwrap();
|
||||
@@ -823,6 +982,10 @@ mod tests {
|
||||
assert_eq!(config.auto_connect, Some(true));
|
||||
assert_eq!(config.headers.as_deref(), Some("{\"Auth\":\"token\"}"));
|
||||
assert_eq!(config.tab_group.as_deref(), Some("Agent Browser Stealth"));
|
||||
assert_eq!(
|
||||
config.tab_group_plugin_id.as_deref(),
|
||||
Some("tab-group-plugin-id")
|
||||
);
|
||||
assert_eq!(config.risk_mode.as_deref(), Some("block"));
|
||||
}
|
||||
|
||||
|
||||
+30
-3
@@ -288,6 +288,7 @@ fn main() {
|
||||
flags.debug,
|
||||
flags.download_path.as_deref(),
|
||||
flags.tab_group.as_deref(),
|
||||
flags.tab_group_plugin_id.as_deref(),
|
||||
) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
@@ -340,6 +341,9 @@ fn main() {
|
||||
flags.cli_allow_file_access.then_some("--allow-file-access"),
|
||||
flags.cli_download_path.then_some("--download-path"),
|
||||
flags.cli_tab_group.then_some("--tab-group"),
|
||||
flags
|
||||
.cli_tab_group_plugin_id
|
||||
.then_some("--tab-group-plugin-id"),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -429,6 +433,9 @@ fn main() {
|
||||
if let Some(ref tg) = flags.tab_group {
|
||||
launch_cmd["tabGroup"] = json!(tg);
|
||||
}
|
||||
if let Some(ref plugin_id) = flags.tab_group_plugin_id {
|
||||
launch_cmd["tabGroupPluginId"] = json!(plugin_id);
|
||||
}
|
||||
|
||||
let err = match send_command(launch_cmd, &flags.session) {
|
||||
Ok(resp) if resp.success => None,
|
||||
@@ -524,6 +531,9 @@ fn main() {
|
||||
if let Some(ref tg) = flags.tab_group {
|
||||
launch_cmd["tabGroup"] = json!(tg);
|
||||
}
|
||||
if let Some(ref plugin_id) = flags.tab_group_plugin_id {
|
||||
launch_cmd["tabGroupPluginId"] = json!(plugin_id);
|
||||
}
|
||||
|
||||
let err = match send_command(launch_cmd, &flags.session) {
|
||||
Ok(resp) if resp.success => None,
|
||||
@@ -560,6 +570,9 @@ fn main() {
|
||||
if let Some(ref tg) = flags.tab_group {
|
||||
launch_cmd["tabGroup"] = json!(tg);
|
||||
}
|
||||
if let Some(ref plugin_id) = flags.tab_group_plugin_id {
|
||||
launch_cmd["tabGroupPluginId"] = json!(plugin_id);
|
||||
}
|
||||
|
||||
match send_command(launch_cmd, &flags.session) {
|
||||
Ok(resp) => {
|
||||
@@ -600,8 +613,7 @@ fn main() {
|
||||
&& flags.user_agent.is_none()
|
||||
&& !flags.ignore_https_errors
|
||||
&& !flags.allow_file_access
|
||||
&& flags.extensions.is_empty()
|
||||
&& flags.tab_group.is_none();
|
||||
&& flags.extensions.is_empty();
|
||||
|
||||
if can_try_default_cdp {
|
||||
let mut launch_cmd = json!({
|
||||
@@ -613,6 +625,12 @@ fn main() {
|
||||
if let Some(ref cs) = flags.color_scheme {
|
||||
launch_cmd["colorScheme"] = json!(cs);
|
||||
}
|
||||
if let Some(ref tg) = flags.tab_group {
|
||||
launch_cmd["tabGroup"] = json!(tg);
|
||||
}
|
||||
if let Some(ref plugin_id) = flags.tab_group_plugin_id {
|
||||
launch_cmd["tabGroupPluginId"] = json!(plugin_id);
|
||||
}
|
||||
|
||||
if let Ok(resp) = send_command(launch_cmd, &flags.session) {
|
||||
attached_to_existing_browser = resp.success;
|
||||
@@ -628,6 +646,12 @@ fn main() {
|
||||
if let Some(ref cs) = flags.color_scheme {
|
||||
auto_connect_cmd["colorScheme"] = json!(cs);
|
||||
}
|
||||
if let Some(ref tg) = flags.tab_group {
|
||||
auto_connect_cmd["tabGroup"] = json!(tg);
|
||||
}
|
||||
if let Some(ref plugin_id) = flags.tab_group_plugin_id {
|
||||
auto_connect_cmd["tabGroupPluginId"] = json!(plugin_id);
|
||||
}
|
||||
|
||||
if let Ok(resp) = send_command(auto_connect_cmd, &flags.session) {
|
||||
attached_to_existing_browser = resp.success;
|
||||
@@ -656,7 +680,7 @@ fn main() {
|
||||
|| flags.debug
|
||||
|| flags.color_scheme.is_some()
|
||||
|| flags.download_path.is_some()
|
||||
|| flags.tab_group.is_some())
|
||||
)
|
||||
&& flags.cdp.is_none()
|
||||
&& flags.provider.is_none()
|
||||
&& !attached_to_existing_browser
|
||||
@@ -724,6 +748,9 @@ fn main() {
|
||||
if let Some(ref tg) = flags.tab_group {
|
||||
launch_cmd["tabGroup"] = json!(tg);
|
||||
}
|
||||
if let Some(ref plugin_id) = flags.tab_group_plugin_id {
|
||||
launch_cmd["tabGroupPluginId"] = json!(plugin_id);
|
||||
}
|
||||
|
||||
match send_command(launch_cmd, &flags.session) {
|
||||
Ok(resp) => {
|
||||
|
||||
+8
-5
@@ -2040,7 +2040,8 @@ Operations:
|
||||
clean --older-than <days> Delete expired state files
|
||||
|
||||
Automatic State Persistence:
|
||||
Use --session-name to auto-save/restore state across restarts:
|
||||
Use --session-name to auto-save/restore state across restarts.
|
||||
If omitted, it defaults to --session (or "default"):
|
||||
agent-browser --session-name myapp open https://example.com
|
||||
Or set AGENT_BROWSER_SESSION_NAME environment variable.
|
||||
|
||||
@@ -2409,9 +2410,10 @@ Options:
|
||||
Project default: try localhost:9333 first, then auto-discovery (no managed local-launch 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)
|
||||
--tab-group <name> Override default tab group title for agent tabs in Chromium local launch (or AGENT_BROWSER_TAB_GROUP)
|
||||
--tab-group <name> Base title for agent tab groups (CDP plugin mode; silent no-op if plugin unavailable)
|
||||
--tab-group-plugin-id <id> Expected Chrome extension ID for tab-group handshake (or AGENT_BROWSER_TAB_GROUP_PLUGIN_ID)
|
||||
--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 (defaults to --session)
|
||||
--content-boundaries Wrap page output in boundary markers (or AGENT_BROWSER_CONTENT_BOUNDARIES)
|
||||
--max-output <chars> Truncate page output to N chars (or AGENT_BROWSER_MAX_OUTPUT)
|
||||
--allowed-domains <list> Restrict navigation domains (or AGENT_BROWSER_ALLOWED_DOMAINS)
|
||||
@@ -2449,7 +2451,7 @@ Configuration:
|
||||
Environment:
|
||||
AGENT_BROWSER_CONFIG Path to config file (or use --config)
|
||||
AGENT_BROWSER_SESSION Session name (default: "default")
|
||||
AGENT_BROWSER_SESSION_NAME Auto-save/restore state persistence name
|
||||
AGENT_BROWSER_SESSION_NAME Auto-save/restore state persistence name (default: AGENT_BROWSER_SESSION)
|
||||
AGENT_BROWSER_ENCRYPTION_KEY 64-char hex key for AES-256-GCM state encryption
|
||||
AGENT_BROWSER_STATE_EXPIRE_DAYS Auto-delete states older than N days (default: 30)
|
||||
AGENT_BROWSER_EXECUTABLE_PATH Custom browser executable path
|
||||
@@ -2468,7 +2470,8 @@ 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_TAB_GROUP Override default tab group title (Chromium local launch only)
|
||||
AGENT_BROWSER_TAB_GROUP Base title for tab groups (default: "Agent Browser Stealth"; session suffix auto-appended)
|
||||
AGENT_BROWSER_TAB_GROUP_PLUGIN_ID Expected Chrome extension ID for tab-group handshake (default: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user