feat: add parallel mode and idle daemon shutdown
This commit is contained in:
@@ -2060,6 +2060,7 @@ mod tests {
|
||||
full: false,
|
||||
headed: false,
|
||||
debug: false,
|
||||
resident: false,
|
||||
headers: None,
|
||||
executable_path: None,
|
||||
extensions: Vec::new(),
|
||||
@@ -2075,6 +2076,7 @@ mod tests {
|
||||
device: None,
|
||||
auto_connect: false,
|
||||
session_name: None,
|
||||
parallel: None,
|
||||
cli_executable_path: false,
|
||||
cli_extensions: false,
|
||||
cli_state: false,
|
||||
@@ -2094,6 +2096,8 @@ mod tests {
|
||||
wait_until: None,
|
||||
cli_tab_group: false,
|
||||
cli_tab_group_plugin_id: false,
|
||||
cli_session_name: false,
|
||||
cli_resident: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -183,6 +183,8 @@ pub struct DaemonResult {
|
||||
pub fn ensure_daemon(
|
||||
session: &str,
|
||||
headed: bool,
|
||||
// Keep daemon resident and disable idle auto-shutdown.
|
||||
resident: bool,
|
||||
executable_path: Option<&str>,
|
||||
extensions: &[String],
|
||||
args: Option<&str>,
|
||||
@@ -289,6 +291,11 @@ pub fn ensure_daemon(
|
||||
|
||||
let mut cmd = Command::new("node");
|
||||
cmd.arg(daemon_path)
|
||||
.arg(if resident {
|
||||
"--resident"
|
||||
} else {
|
||||
"--idle-auto-shutdown"
|
||||
})
|
||||
.env("AGENT_BROWSER_DAEMON", "1")
|
||||
.env("AGENT_BROWSER_SESSION", session);
|
||||
|
||||
@@ -384,6 +391,11 @@ pub fn ensure_daemon(
|
||||
// and automatically quotes arguments containing spaces.
|
||||
let mut cmd = Command::new("node");
|
||||
cmd.arg(daemon_path)
|
||||
.arg(if resident {
|
||||
"--resident"
|
||||
} else {
|
||||
"--idle-auto-shutdown"
|
||||
})
|
||||
.env("AGENT_BROWSER_DAEMON", "1")
|
||||
.env("AGENT_BROWSER_SESSION", session);
|
||||
|
||||
|
||||
+113
-6
@@ -1,4 +1,5 @@
|
||||
use crate::color;
|
||||
use crate::validation::is_valid_session_name;
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
@@ -40,6 +41,7 @@ pub struct Config {
|
||||
pub tab_group_plugin_id: Option<String>,
|
||||
pub risk_mode: Option<String>,
|
||||
pub wait_until: Option<String>,
|
||||
pub parallel: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -78,6 +80,7 @@ impl Config {
|
||||
tab_group_plugin_id: other.tab_group_plugin_id.or(self.tab_group_plugin_id),
|
||||
risk_mode: other.risk_mode.or(self.risk_mode),
|
||||
wait_until: other.wait_until.or(self.wait_until),
|
||||
parallel: other.parallel.or(self.parallel),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,6 +151,7 @@ fn extract_config_path(args: &[String]) -> Option<Option<String>> {
|
||||
"--tab-group-plugin-id",
|
||||
"--risk-mode",
|
||||
"--wait-until",
|
||||
"--parallel",
|
||||
];
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
@@ -199,6 +203,10 @@ pub struct Flags {
|
||||
pub full: bool,
|
||||
pub headed: bool,
|
||||
pub debug: bool,
|
||||
/// Keep daemon resident and disable idle auto-shutdown.
|
||||
pub resident: bool,
|
||||
/// Runtime daemon session channel.
|
||||
/// Defaults to `default`; when `--parallel <name>` is provided it becomes `parallel-<name>`.
|
||||
pub session: String,
|
||||
pub headers: Option<String>,
|
||||
pub executable_path: Option<String>,
|
||||
@@ -214,7 +222,9 @@ pub struct Flags {
|
||||
pub allow_file_access: bool,
|
||||
pub device: Option<String>,
|
||||
pub auto_connect: bool,
|
||||
pub session_name: Option<String>, // Defaults to "default" when unset
|
||||
// Defaults to "default" when unset in default runtime mode.
|
||||
// In --parallel mode, defaults to None unless explicitly provided on CLI.
|
||||
pub session_name: Option<String>,
|
||||
pub annotate: bool,
|
||||
pub color_scheme: Option<String>,
|
||||
pub download_path: Option<String>,
|
||||
@@ -226,6 +236,8 @@ pub struct Flags {
|
||||
/// Navigation wait strategy passed to navigate/open commands:
|
||||
/// `load`, `domcontentloaded`, or `networkidle`.
|
||||
pub wait_until: Option<String>,
|
||||
/// Parallel execution channel name. When set, commands run in an isolated runtime session.
|
||||
pub parallel: Option<String>,
|
||||
|
||||
// Track which launch-time options were explicitly passed via CLI
|
||||
// (as opposed to being set only via environment variables)
|
||||
@@ -241,6 +253,8 @@ pub struct Flags {
|
||||
pub cli_download_path: bool,
|
||||
pub cli_tab_group: bool,
|
||||
pub cli_tab_group_plugin_id: bool,
|
||||
pub cli_session_name: bool,
|
||||
pub cli_resident: bool,
|
||||
}
|
||||
|
||||
pub fn parse_flags(args: &[String]) -> Flags {
|
||||
@@ -273,7 +287,9 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
Err(_) => config.headed.unwrap_or(true),
|
||||
},
|
||||
debug: env_var_is_truthy("AGENT_BROWSER_DEBUG") || config.debug.unwrap_or(false),
|
||||
// --session is disabled: user-facing CLI always uses one default session.
|
||||
resident: false,
|
||||
// --session is disabled for users.
|
||||
// Runtime session defaults to `default`, and can be isolated with `--parallel`.
|
||||
session: "default".to_string(),
|
||||
headers: config.headers,
|
||||
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH")
|
||||
@@ -321,6 +337,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
.or(config.risk_mode)
|
||||
.map(|s| s.to_ascii_lowercase()),
|
||||
wait_until: config.wait_until.map(|s| s.to_ascii_lowercase()),
|
||||
parallel: env::var("AGENT_BROWSER_PARALLEL").ok().or(config.parallel),
|
||||
cli_executable_path: false,
|
||||
cli_extensions: false,
|
||||
cli_state: false,
|
||||
@@ -333,6 +350,8 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
cli_download_path: false,
|
||||
cli_tab_group: false,
|
||||
cli_tab_group_plugin_id: false,
|
||||
cli_session_name: false,
|
||||
cli_resident: false,
|
||||
};
|
||||
|
||||
let mut i = 0;
|
||||
@@ -366,6 +385,14 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--resident" => {
|
||||
let (val, consumed) = parse_bool_arg(args, i);
|
||||
flags.resident = val;
|
||||
flags.cli_resident = true;
|
||||
if consumed {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--headers" => {
|
||||
if let Some(h) = args.get(i + 1) {
|
||||
flags.headers = Some(h.clone());
|
||||
@@ -464,6 +491,13 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
"--session-name" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
flags.session_name = Some(s.clone());
|
||||
flags.cli_session_name = true;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--parallel" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
flags.parallel = Some(s.clone());
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
@@ -523,9 +557,24 @@ 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 the default session id.
|
||||
if flags.session_name.is_none() {
|
||||
if let Some(parallel_name) = &flags.parallel {
|
||||
// Validate early so session id derivation cannot introduce unsafe paths.
|
||||
if !is_valid_session_name(parallel_name) {
|
||||
// Keep default session and let main.rs surface a user-facing validation error.
|
||||
} else {
|
||||
flags.session = format!("parallel-{}", parallel_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Parallel mode is for isolated/stateless runs.
|
||||
// Unless --session-name is explicitly provided on this invocation, disable
|
||||
// auto save/restore persistence to avoid cross-flow auth leakage.
|
||||
if flags.parallel.is_some() && !flags.cli_session_name {
|
||||
flags.session_name = None;
|
||||
}
|
||||
|
||||
// Keep auth/state continuity stable by default for the default runtime session.
|
||||
if flags.session_name.is_none() && flags.parallel.is_none() {
|
||||
flags.session_name = Some("default".to_string());
|
||||
}
|
||||
|
||||
@@ -542,6 +591,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
||||
"--full",
|
||||
"--headed",
|
||||
"--debug",
|
||||
"--resident",
|
||||
"--ignore-https-errors",
|
||||
"--allow-file-access",
|
||||
"--auto-connect",
|
||||
@@ -569,6 +619,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
||||
"--tab-group-plugin-id",
|
||||
"--risk-mode",
|
||||
"--wait-until",
|
||||
"--parallel",
|
||||
"--config",
|
||||
];
|
||||
|
||||
@@ -770,6 +821,34 @@ mod tests {
|
||||
assert_eq!(flags.session_name.as_deref(), Some("default"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parallel_sets_isolated_runtime_session() {
|
||||
let flags = parse_flags(&args("--parallel worker_a snapshot"));
|
||||
assert_eq!(flags.parallel.as_deref(), Some("worker_a"));
|
||||
assert_eq!(flags.session, "parallel-worker_a");
|
||||
assert_eq!(flags.session_name, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parallel_keeps_explicit_session_name() {
|
||||
let flags = parse_flags(&args(
|
||||
"--parallel worker_b --session-name keep-state snapshot",
|
||||
));
|
||||
assert_eq!(flags.session, "parallel-worker_b");
|
||||
assert_eq!(flags.session_name.as_deref(), Some("keep-state"));
|
||||
assert!(flags.cli_session_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parallel_from_env_sets_runtime_session() {
|
||||
let _guard = EnvGuard::new(&["AGENT_BROWSER_PARALLEL", "AGENT_BROWSER_SESSION_NAME"]);
|
||||
env::set_var("AGENT_BROWSER_PARALLEL", "envworker");
|
||||
env::set_var("AGENT_BROWSER_SESSION_NAME", "persisted");
|
||||
let flags = parse_flags(&args("snapshot"));
|
||||
assert_eq!(flags.session, "parallel-envworker");
|
||||
assert_eq!(flags.session_name, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_executable_path_tracking() {
|
||||
// When --executable-path is passed via CLI, cli_executable_path should be true
|
||||
@@ -805,6 +884,20 @@ mod tests {
|
||||
assert!(!flags.cli_annotate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_resident_flag() {
|
||||
let flags = parse_flags(&args("--resident open example.com"));
|
||||
assert!(flags.resident);
|
||||
assert!(flags.cli_resident);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_resident_false() {
|
||||
let flags = parse_flags(&args("--resident false open example.com"));
|
||||
assert!(!flags.resident);
|
||||
assert!(flags.cli_resident);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_download_path_tracking() {
|
||||
let flags = parse_flags(&args("--download-path /tmp/dl snapshot"));
|
||||
@@ -940,6 +1033,18 @@ mod tests {
|
||||
assert_eq!(cleaned, vec!["open", "example.com"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clean_args_removes_parallel() {
|
||||
let cleaned = clean_args(&args("--parallel worker_x open example.com"));
|
||||
assert_eq!(cleaned, vec!["open", "example.com"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clean_args_removes_resident_flag() {
|
||||
let cleaned = clean_args(&args("--resident open example.com"));
|
||||
assert_eq!(cleaned, vec!["open", "example.com"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_multiple_flags_tracking() {
|
||||
let flags = parse_flags(&args(
|
||||
@@ -978,7 +1083,8 @@ mod tests {
|
||||
"headers": "{\"Auth\":\"token\"}",
|
||||
"tabGroup": "Agent Browser Stealth",
|
||||
"tabGroupPluginId": "tab-group-plugin-id",
|
||||
"riskMode": "block"
|
||||
"riskMode": "block",
|
||||
"parallel": "worker-c"
|
||||
}"#;
|
||||
let config: Config = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(config.headed, Some(true));
|
||||
@@ -1010,6 +1116,7 @@ mod tests {
|
||||
Some("tab-group-plugin-id")
|
||||
);
|
||||
assert_eq!(config.risk_mode.as_deref(), Some("block"));
|
||||
assert_eq!(config.parallel.as_deref(), Some("worker-c"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -174,6 +174,24 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref parallel) = flags.parallel {
|
||||
if !validation::is_valid_session_name(parallel) {
|
||||
let msg = format!(
|
||||
"Invalid --parallel value '{}'. Only alphanumeric characters, hyphens, and underscores are allowed.",
|
||||
parallel
|
||||
);
|
||||
if flags.json {
|
||||
println!(
|
||||
r#"{{"success":false,"error":"{}","type":"invalid_parallel_name"}}"#,
|
||||
msg.replace('"', "\\\"")
|
||||
);
|
||||
} 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.";
|
||||
@@ -275,6 +293,7 @@ fn main() {
|
||||
let daemon_result = match ensure_daemon(
|
||||
&flags.session,
|
||||
flags.headed,
|
||||
flags.resident,
|
||||
flags.executable_path.as_deref(),
|
||||
&flags.extensions,
|
||||
flags.args.as_deref(),
|
||||
@@ -346,6 +365,7 @@ fn main() {
|
||||
flags
|
||||
.cli_tab_group_plugin_id
|
||||
.then_some("--tab-group-plugin-id"),
|
||||
flags.cli_resident.then_some("--resident"),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
|
||||
@@ -3,14 +3,20 @@ use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::signal;
|
||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
|
||||
use tokio::time::{Duration, Instant};
|
||||
|
||||
use super::actions::{execute_command, DaemonState};
|
||||
use super::state;
|
||||
|
||||
const IDLE_SHUTDOWN_SECS: u64 = 600;
|
||||
|
||||
pub async fn run_daemon(session: &str) {
|
||||
let resident_mode = env::args().any(|arg| arg == "--resident");
|
||||
let socket_dir = get_daemon_socket_dir();
|
||||
if !socket_dir.exists() {
|
||||
let _ = fs::create_dir_all(&socket_dir);
|
||||
@@ -33,7 +39,7 @@ pub async fn run_daemon(session: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
let result = run_socket_server(&socket_path, session).await;
|
||||
let result = run_socket_server(&socket_path, session, resident_mode).await;
|
||||
|
||||
let _ = fs::remove_file(&socket_path);
|
||||
let _ = fs::remove_file(&pid_path);
|
||||
@@ -47,7 +53,11 @@ pub async fn run_daemon(session: &str) {
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn run_socket_server(socket_path: &PathBuf, _session: &str) -> Result<(), String> {
|
||||
async fn run_socket_server(
|
||||
socket_path: &PathBuf,
|
||||
_session: &str,
|
||||
resident_mode: bool,
|
||||
) -> Result<(), String> {
|
||||
use tokio::net::UnixListener;
|
||||
|
||||
let listener =
|
||||
@@ -55,6 +65,9 @@ async fn run_socket_server(socket_path: &PathBuf, _session: &str) -> Result<(),
|
||||
|
||||
let state: std::sync::Arc<tokio::sync::Mutex<DaemonState>> =
|
||||
std::sync::Arc::new(tokio::sync::Mutex::new(DaemonState::new()));
|
||||
let active_commands = std::sync::Arc::new(AtomicUsize::new(0));
|
||||
let (activity_tx, mut activity_rx) = unbounded_channel::<()>();
|
||||
let mut idle_deadline = Instant::now() + Duration::from_secs(IDLE_SHUTDOWN_SECS);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -62,8 +75,10 @@ async fn run_socket_server(socket_path: &PathBuf, _session: &str) -> Result<(),
|
||||
match accept_result {
|
||||
Ok((stream, _)) => {
|
||||
let state = state.clone();
|
||||
let activity_tx = activity_tx.clone();
|
||||
let active_commands = active_commands.clone();
|
||||
tokio::spawn(async move {
|
||||
handle_connection(stream, state).await;
|
||||
handle_connection(stream, state, activity_tx, active_commands).await;
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -71,6 +86,19 @@ async fn run_socket_server(socket_path: &PathBuf, _session: &str) -> Result<(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) = activity_rx.recv() => {
|
||||
idle_deadline = Instant::now() + Duration::from_secs(IDLE_SHUTDOWN_SECS);
|
||||
}
|
||||
_ = tokio::time::sleep_until(idle_deadline), if !resident_mode => {
|
||||
if active_commands.load(Ordering::SeqCst) == 0 {
|
||||
let mut s = state.lock().await;
|
||||
if let Some(ref mut mgr) = s.browser {
|
||||
let _ = mgr.close().await;
|
||||
}
|
||||
break;
|
||||
}
|
||||
idle_deadline = Instant::now() + Duration::from_secs(IDLE_SHUTDOWN_SECS);
|
||||
}
|
||||
_ = shutdown_signal() => {
|
||||
let mut s = state.lock().await;
|
||||
if let Some(ref mut mgr) = s.browser {
|
||||
@@ -85,7 +113,11 @@ async fn run_socket_server(socket_path: &PathBuf, _session: &str) -> Result<(),
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn run_socket_server(socket_path: &PathBuf, session: &str) -> Result<(), String> {
|
||||
async fn run_socket_server(
|
||||
socket_path: &PathBuf,
|
||||
session: &str,
|
||||
resident_mode: bool,
|
||||
) -> Result<(), String> {
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
let port = get_port_for_session(session);
|
||||
@@ -99,6 +131,9 @@ async fn run_socket_server(socket_path: &PathBuf, session: &str) -> Result<(), S
|
||||
|
||||
let state: std::sync::Arc<tokio::sync::Mutex<DaemonState>> =
|
||||
std::sync::Arc::new(tokio::sync::Mutex::new(DaemonState::new()));
|
||||
let active_commands = std::sync::Arc::new(AtomicUsize::new(0));
|
||||
let (activity_tx, mut activity_rx) = unbounded_channel::<()>();
|
||||
let mut idle_deadline = Instant::now() + Duration::from_secs(IDLE_SHUTDOWN_SECS);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -106,8 +141,10 @@ async fn run_socket_server(socket_path: &PathBuf, session: &str) -> Result<(), S
|
||||
match accept_result {
|
||||
Ok((stream, _)) => {
|
||||
let state = state.clone();
|
||||
let activity_tx = activity_tx.clone();
|
||||
let active_commands = active_commands.clone();
|
||||
tokio::spawn(async move {
|
||||
handle_connection(stream, state).await;
|
||||
handle_connection(stream, state, activity_tx, active_commands).await;
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -115,6 +152,20 @@ async fn run_socket_server(socket_path: &PathBuf, session: &str) -> Result<(), S
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) = activity_rx.recv() => {
|
||||
idle_deadline = Instant::now() + Duration::from_secs(IDLE_SHUTDOWN_SECS);
|
||||
}
|
||||
_ = tokio::time::sleep_until(idle_deadline), if !resident_mode => {
|
||||
if active_commands.load(Ordering::SeqCst) == 0 {
|
||||
let mut s = state.lock().await;
|
||||
if let Some(ref mut mgr) = s.browser {
|
||||
let _ = mgr.close().await;
|
||||
}
|
||||
let _ = fs::remove_file(&port_path);
|
||||
break;
|
||||
}
|
||||
idle_deadline = Instant::now() + Duration::from_secs(IDLE_SHUTDOWN_SECS);
|
||||
}
|
||||
_ = shutdown_signal() => {
|
||||
let mut s = state.lock().await;
|
||||
if let Some(ref mut mgr) = s.browser {
|
||||
@@ -129,7 +180,12 @@ async fn run_socket_server(socket_path: &PathBuf, session: &str) -> Result<(), S
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_connection<S>(stream: S, state: std::sync::Arc<tokio::sync::Mutex<DaemonState>>)
|
||||
async fn handle_connection<S>(
|
||||
stream: S,
|
||||
state: std::sync::Arc<tokio::sync::Mutex<DaemonState>>,
|
||||
activity_tx: UnboundedSender<()>,
|
||||
active_commands: std::sync::Arc<AtomicUsize>,
|
||||
)
|
||||
where
|
||||
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
||||
{
|
||||
@@ -166,6 +222,8 @@ where
|
||||
};
|
||||
|
||||
let is_close = cmd.get("action").and_then(|v| v.as_str()) == Some("close");
|
||||
let _ = activity_tx.send(());
|
||||
active_commands.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
let response = {
|
||||
let mut s = state.lock().await;
|
||||
@@ -175,8 +233,11 @@ where
|
||||
let mut resp = serde_json::to_string(&response).unwrap_or_default();
|
||||
resp.push('\n');
|
||||
if writer.write_all(resp.as_bytes()).await.is_err() {
|
||||
active_commands.fetch_sub(1, Ordering::SeqCst);
|
||||
break;
|
||||
}
|
||||
active_commands.fetch_sub(1, Ordering::SeqCst);
|
||||
let _ = activity_tx.send(());
|
||||
|
||||
if is_close {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
|
||||
+16
-6
@@ -2076,9 +2076,11 @@ Operations:
|
||||
|
||||
Automatic State Persistence:
|
||||
Use --session-name to auto-save/restore state across restarts.
|
||||
If omitted, it defaults to "default":
|
||||
If omitted in default runtime mode, it defaults to "default":
|
||||
agent-browser --session-name myapp open https://example.com
|
||||
Or set AGENT_BROWSER_SESSION_NAME environment variable.
|
||||
Note: with --parallel <name>, persistence is disabled by default unless
|
||||
--session-name is explicitly passed on the same command.
|
||||
|
||||
State Encryption:
|
||||
Set AGENT_BROWSER_ENCRYPTION_KEY (64-char hex) for AES-256-GCM encryption.
|
||||
@@ -2105,7 +2107,7 @@ agent-browser session - Manage sessions
|
||||
|
||||
Usage: agent-browser session [operation]
|
||||
|
||||
Show the current fixed session and active daemon state.
|
||||
Show the current runtime session and active daemon state.
|
||||
|
||||
Operations:
|
||||
(none) Show current session name
|
||||
@@ -2437,7 +2439,7 @@ Snapshot Options:
|
||||
-s, --selector <sel> Scope to CSS selector
|
||||
|
||||
Options:
|
||||
--session <name> Ignored (single default session only)
|
||||
--session <name> Ignored (runtime uses default session unless --parallel is set)
|
||||
--state <path> Load storage state from JSON file (or AGENT_BROWSER_STATE env)
|
||||
--headers <json> HTTP headers scoped to URL's origin (for auth)
|
||||
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
|
||||
@@ -2467,7 +2469,10 @@ Options:
|
||||
Extension side panel supports browser controls + console/network/DOM + workflow scheduling
|
||||
--risk-mode <mode> Verify/captcha handling: off, warn, block (or AGENT_BROWSER_RISK_MODE)
|
||||
--wait-until <mode> Navigation wait strategy for open/navigate: load, domcontentloaded, networkidle
|
||||
--session-name <name> Auto-save/restore session state (defaults to "default")
|
||||
--parallel <name> Isolated runtime channel for parallel AI runs (maps to parallel-<name>)
|
||||
Default behavior in this mode is stateless (no auto session persistence unless --session-name is explicitly passed)
|
||||
--resident Keep daemon running; disable 10-minute idle auto-shutdown
|
||||
--session-name <name> Auto-save/restore session state (defaults to "default" in non-parallel mode)
|
||||
--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)
|
||||
@@ -2482,6 +2487,7 @@ Options:
|
||||
Policy:
|
||||
--profile / AGENT_BROWSER_PROFILE are forbidden
|
||||
--channel / AGENT_BROWSER_CHANNEL are forbidden
|
||||
Daemon auto-shuts down after 10 minutes of inactivity unless --resident is set
|
||||
Auto-attach existing browser (prefer CDP localhost:9333, then auto-discovery), or pass --cdp explicitly
|
||||
|
||||
Configuration:
|
||||
@@ -2497,6 +2503,7 @@ Configuration:
|
||||
Boolean flags accept an optional true/false value to override config:
|
||||
--headed (same as --headed true)
|
||||
--headed false (disables "headed": true from config)
|
||||
--resident false (disable resident mode for this invocation)
|
||||
|
||||
Extensions from user and project configs are merged (not replaced).
|
||||
|
||||
@@ -2505,7 +2512,8 @@ Configuration:
|
||||
|
||||
Environment:
|
||||
AGENT_BROWSER_CONFIG Path to config file (or use --config)
|
||||
AGENT_BROWSER_SESSION_NAME Auto-save/restore state persistence name (default: "default")
|
||||
AGENT_BROWSER_PARALLEL Isolated runtime channel for parallel AI runs (maps to parallel-<name>)
|
||||
Best for stateless/no-login tasks where throughput matters
|
||||
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
|
||||
@@ -2528,7 +2536,7 @@ Environment:
|
||||
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
|
||||
AGENT_BROWSER_SESSION_NAME Auto-save/load state persistence name (default: "default" when --parallel is not set)
|
||||
AGENT_BROWSER_STATE_EXPIRE_DAYS Auto-delete saved states older than N days (default: 30)
|
||||
AGENT_BROWSER_ENCRYPTION_KEY 64-char hex key for AES-256-GCM session encryption
|
||||
AGENT_BROWSER_STREAM_PORT Enable WebSocket streaming on port (e.g., 9223)
|
||||
@@ -2564,6 +2572,8 @@ Examples:
|
||||
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 --parallel worker-a open example.com # Isolated runtime for parallel AI task
|
||||
agent-browser --resident open example.com # Keep daemon resident until explicit close
|
||||
|
||||
Command Chaining:
|
||||
Chain commands with && in a single shell call (browser persists via daemon):
|
||||
|
||||
Reference in New Issue
Block a user