feat: enforce default session daemon isolation and bump 0.16.3-fork.4

This commit is contained in:
leeguooooo
2026-03-05 18:44:52 +09:00
parent 5970579d7c
commit 5e0582cede
12 changed files with 470 additions and 100 deletions
+5
View File
@@ -63,6 +63,10 @@ agent-browser --parallel worker-b open https://example.org
`--parallel` is designed for stateless throughput tasks (navigation, extraction, checks). For authenticated flows, keep using one stable `--session-name`.
Default session isolation policy:
- Running a default-session command reaps all non-default daemon sessions (`parallel-*` and legacy named channels).
- This avoids stale daemon reuse and keeps stealth behavior consistent on the primary channel.
| Option | Purpose | Typical Usage |
| --- | --- | --- |
| `--parallel <name>` | Isolate runtime channel for concurrent AI tasks | Stateless/no-login parallel jobs |
@@ -261,6 +265,7 @@ flowchart TD
- Prefer `--headed` for high-friction targets.
- Reuse session state with one stable `--session-name` for continuity (when omitted, it defaults to `default`).
- Use `--parallel <name>` only for stateless parallel workloads where higher throughput matters.
- Default-session commands will reap all non-default daemon sessions, so keep parallel workers short-lived.
- Use `--resident` only for deliberate long-running workflows, and close when done.
- Keep locale/timezone consistent with target market.
- For challenge-heavy pages, prefer `--wait-until domcontentloaded` on `open`/`navigate` to avoid `load` stalls.
+1 -1
View File
@@ -45,7 +45,7 @@ dependencies = [
[[package]]
name = "agent-browser-stealth"
version = "0.16.3-fork.3"
version = "0.16.3-fork.4"
dependencies = [
"aes-gcm",
"async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "agent-browser-stealth"
version = "0.16.3-fork.3"
version = "0.16.3-fork.4"
edition = "2021"
description = "Stealth browser automation CLI for AI agents with anti-bot evasions"
license = "Apache-2.0"
+410 -45
View File
@@ -4,7 +4,7 @@ use std::env;
use std::fs;
use std::io::{BufRead, BufReader, Read, Write};
use std::net::TcpStream;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;
@@ -116,6 +116,30 @@ fn get_pid_path(session: &str) -> PathBuf {
get_socket_dir().join(format!("{}.pid", session))
}
fn get_stream_path(session: &str) -> PathBuf {
get_socket_dir().join(format!("{}.stream", session))
}
fn get_meta_path(session: &str) -> PathBuf {
get_socket_dir().join(format!("{}.meta.json", session))
}
fn remove_session_artifacts(session: &str) {
let _ = fs::remove_file(get_pid_path(session));
let _ = fs::remove_file(get_stream_path(session));
let _ = fs::remove_file(get_meta_path(session));
#[cfg(unix)]
{
let _ = fs::remove_file(get_socket_path(session));
}
#[cfg(windows)]
{
let _ = fs::remove_file(get_port_path(session));
}
}
/// Clean up stale socket and PID files for a session
fn cleanup_stale_files(session: &str) {
// Never delete files for a live daemon. A missing PID file can happen in
@@ -124,20 +148,194 @@ fn cleanup_stale_files(session: &str) {
return;
}
remove_session_artifacts(session);
}
fn should_reap_for_default(session: &str) -> bool {
session != "default"
}
#[cfg(unix)]
fn process_exists(pid: u32) -> bool {
if pid == 0 {
return false;
}
let rc = unsafe { libc::kill(pid as i32, 0) };
if rc == 0 {
return true;
}
std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH)
}
#[cfg(windows)]
fn process_exists(pid: u32) -> bool {
if pid == 0 {
return false;
}
let output = Command::new("tasklist")
.args(["/FI", &format!("PID eq {}", pid)])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output();
match output {
Ok(out) => {
let text = String::from_utf8_lossy(&out.stdout);
text.contains(&format!(" {}", pid))
}
Err(_) => false,
}
}
#[cfg(unix)]
fn terminate_pid(pid: u32) {
if !process_exists(pid) {
return;
}
let _ = unsafe { libc::kill(pid as i32, libc::SIGTERM) };
for _ in 0..20 {
if !process_exists(pid) {
return;
}
thread::sleep(Duration::from_millis(50));
}
let _ = unsafe { libc::kill(pid as i32, libc::SIGKILL) };
for _ in 0..10 {
if !process_exists(pid) {
return;
}
thread::sleep(Duration::from_millis(20));
}
}
#[cfg(windows)]
fn terminate_pid(pid: u32) {
if !process_exists(pid) {
return;
}
let _ = Command::new("taskkill")
.args(["/PID", &pid.to_string(), "/T", "/F"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
fn terminate_session_daemon(session: &str) {
let pid_path = get_pid_path(session);
let _ = fs::remove_file(&pid_path);
if let Ok(pid_str) = fs::read_to_string(&pid_path) {
if let Ok(pid) = pid_str.trim().parse::<u32>() {
terminate_pid(pid);
}
}
}
#[cfg(unix)]
{
let socket_path = get_socket_path(session);
let _ = fs::remove_file(&socket_path);
fn reap_sessions_for_default_start() {
let socket_dir = get_socket_dir();
let entries = match fs::read_dir(&socket_dir) {
Ok(v) => v,
Err(_) => return,
};
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if !name.ends_with(".pid") {
continue;
}
let session = name.trim_end_matches(".pid");
if session.is_empty() || !should_reap_for_default(session) {
continue;
}
terminate_session_daemon(session);
remove_session_artifacts(session);
}
}
fn validate_default_daemon_identity(expected_daemon_path: &Path) -> bool {
let meta_path = get_meta_path("default");
let meta_raw = match fs::read_to_string(&meta_path) {
Ok(v) => v,
Err(_) => return false,
};
let meta: Value = match serde_json::from_str(&meta_raw) {
Ok(v) => v,
Err(_) => return false,
};
let cli_version = meta
.get("cliVersion")
.and_then(|v| v.as_str())
.unwrap_or_default();
let daemon_path = meta
.get("daemonPath")
.and_then(|v| v.as_str())
.unwrap_or_default();
if cli_version != env!("CARGO_PKG_VERSION") || daemon_path.is_empty() {
return false;
}
#[cfg(windows)]
{
let port_path = get_port_path(session);
let _ = fs::remove_file(&port_path);
let expected = expected_daemon_path
.canonicalize()
.unwrap_or_else(|_| expected_daemon_path.to_path_buf());
let observed = PathBuf::from(daemon_path);
let observed = observed.canonicalize().unwrap_or(observed);
observed == expected
}
fn resolve_daemon_path() -> Result<PathBuf, String> {
let exe_path = env::current_exe().map_err(|e| e.to_string())?;
// Canonicalize to resolve symlinks (e.g., npm global bin symlink -> actual binary)
let exe_path = exe_path.canonicalize().unwrap_or(exe_path);
let exe_dir = exe_path.parent().unwrap();
let mut daemon_paths = vec![
exe_dir.join("daemon.js"),
exe_dir.join("../dist/daemon.js"),
PathBuf::from("dist/daemon.js"),
];
if let Ok(home) = env::var("AGENT_BROWSER_HOME") {
let home_path = PathBuf::from(&home);
daemon_paths.insert(0, home_path.join("dist/daemon.js"));
daemon_paths.insert(1, home_path.join("daemon.js"));
}
let daemon_path = daemon_paths
.into_iter()
.find(|p| p.exists())
.ok_or("Daemon not found. Set AGENT_BROWSER_HOME environment variable or run from project directory.")?;
Ok(daemon_path.canonicalize().unwrap_or(daemon_path))
}
pub fn list_live_sessions() -> Vec<String> {
let socket_dir = get_socket_dir();
let mut sessions = Vec::new();
if let Ok(entries) = fs::read_dir(&socket_dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if !name.ends_with(".pid") {
continue;
}
let session_name = name.trim_end_matches(".pid");
if session_name.is_empty() {
continue;
}
if daemon_ready(session_name) {
sessions.push(session_name.to_string());
} else {
cleanup_stale_files(session_name);
}
}
}
sessions.sort();
sessions
}
#[cfg(windows)]
@@ -202,17 +400,35 @@ pub fn ensure_daemon(
tab_group: Option<&str>,
tab_group_plugin_id: Option<&str>,
) -> Result<DaemonResult, String> {
let daemon_path = resolve_daemon_path()?;
// Project policy: the default runtime channel is a singleton control plane.
// Before touching it, reap all non-default channels to avoid stale daemon reuse.
if session == "default" {
reap_sessions_for_default_start();
}
// Socket readiness is the source of truth for a usable daemon.
// PID files can be missing/stale under concurrent start/stop races.
if daemon_ready(session) {
// Double-check it's actually responsive by waiting and checking again
// This handles the race condition where daemon is shutting down
// (daemon has a 100ms shutdown delay, so we wait longer)
thread::sleep(Duration::from_millis(150));
if daemon_ready(session) {
return Ok(DaemonResult {
already_running: true,
});
let mut should_reuse = true;
if session == "default" {
should_reuse = validate_default_daemon_identity(&daemon_path);
}
if should_reuse {
// Double-check it's actually responsive by waiting and checking again
// This handles the race condition where daemon is shutting down
// (daemon has a 100ms shutdown delay, so we wait longer)
thread::sleep(Duration::from_millis(150));
if daemon_ready(session) {
return Ok(DaemonResult {
already_running: true,
});
}
} else {
terminate_session_daemon(session);
remove_session_artifacts(session);
}
}
@@ -257,29 +473,6 @@ pub fn ensure_daemon(
}
}
let exe_path = env::current_exe().map_err(|e| e.to_string())?;
// Canonicalize to resolve symlinks (e.g., npm global bin symlink -> actual binary)
let exe_path = exe_path.canonicalize().unwrap_or(exe_path);
let exe_dir = exe_path.parent().unwrap();
let mut daemon_paths = vec![
exe_dir.join("daemon.js"),
exe_dir.join("../dist/daemon.js"),
PathBuf::from("dist/daemon.js"),
];
// Check AGENT_BROWSER_HOME environment variable
if let Ok(home) = env::var("AGENT_BROWSER_HOME") {
let home_path = PathBuf::from(&home);
daemon_paths.insert(0, home_path.join("dist/daemon.js"));
daemon_paths.insert(1, home_path.join("daemon.js"));
}
let daemon_path = daemon_paths
.iter()
.find(|p| p.exists())
.ok_or("Daemon not found. Set AGENT_BROWSER_HOME environment variable or run from project directory.")?;
// Keep handle to detect early daemon exit and surface startup errors.
#[allow(unused_assignments)]
let mut daemon_child: Option<std::process::Child> = None;
@@ -290,14 +483,15 @@ pub fn ensure_daemon(
use std::os::unix::process::CommandExt;
let mut cmd = Command::new("node");
cmd.arg(daemon_path)
cmd.arg(&daemon_path)
.arg(if resident {
"--resident"
} else {
"--idle-auto-shutdown"
})
.env("AGENT_BROWSER_DAEMON", "1")
.env("AGENT_BROWSER_SESSION", session);
.env("AGENT_BROWSER_SESSION", session)
.env("AGENT_BROWSER_CLI_VERSION", env!("CARGO_PKG_VERSION"));
if headed {
cmd.env("AGENT_BROWSER_HEADED", "1");
@@ -390,14 +584,15 @@ pub fn ensure_daemon(
// On Windows, call node directly. Command::new handles PATH resolution (node.exe or node.cmd)
// and automatically quotes arguments containing spaces.
let mut cmd = Command::new("node");
cmd.arg(daemon_path)
cmd.arg(&daemon_path)
.arg(if resident {
"--resident"
} else {
"--idle-auto-shutdown"
})
.env("AGENT_BROWSER_DAEMON", "1")
.env("AGENT_BROWSER_SESSION", session);
.env("AGENT_BROWSER_SESSION", session)
.env("AGENT_BROWSER_CLI_VERSION", env!("CARGO_PKG_VERSION"));
if headed {
cmd.env("AGENT_BROWSER_HEADED", "1");
@@ -606,6 +801,15 @@ fn send_command_once(cmd: &Value, session: &str) -> Result<Response, String> {
mod tests {
use super::*;
use crate::test_utils::EnvGuard;
use std::time::{SystemTime, UNIX_EPOCH};
fn test_temp_dir(prefix: &str) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
env::temp_dir().join(format!("{}-{}-{}", prefix, std::process::id(), nonce))
}
#[test]
fn test_get_socket_dir_explicit_override() {
@@ -668,6 +872,167 @@ mod tests {
);
}
#[test]
fn test_should_reap_for_default_policy() {
assert!(!should_reap_for_default("default"));
assert!(should_reap_for_default("parallel-worker-a"));
assert!(should_reap_for_default("legacy-session"));
}
#[test]
fn test_reap_sessions_for_default_start_removes_non_default_artifacts() {
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR"]);
let dir = test_temp_dir("agent-browser-reap");
fs::create_dir_all(&dir).unwrap();
_guard.set("AGENT_BROWSER_SOCKET_DIR", dir.to_string_lossy().as_ref());
fs::write(dir.join("default.pid"), "999999").unwrap();
fs::write(dir.join("parallel-a.pid"), "999999").unwrap();
fs::write(dir.join("parallel-a.meta.json"), "{}").unwrap();
fs::write(dir.join("legacy-x.pid"), "not-a-pid").unwrap();
fs::write(dir.join("legacy-x.meta.json"), "{}").unwrap();
#[cfg(unix)]
{
fs::write(dir.join("parallel-a.sock"), "").unwrap();
fs::write(dir.join("legacy-x.sock"), "").unwrap();
}
#[cfg(windows)]
{
fs::write(dir.join("parallel-a.port"), "").unwrap();
fs::write(dir.join("legacy-x.port"), "").unwrap();
}
reap_sessions_for_default_start();
assert!(dir.join("default.pid").exists());
assert!(!dir.join("parallel-a.pid").exists());
assert!(!dir.join("parallel-a.meta.json").exists());
assert!(!dir.join("legacy-x.pid").exists());
assert!(!dir.join("legacy-x.meta.json").exists());
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_remove_session_artifacts_cleans_all_known_files() {
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR"]);
let dir = test_temp_dir("agent-browser-clean-artifacts");
fs::create_dir_all(&dir).unwrap();
_guard.set("AGENT_BROWSER_SOCKET_DIR", dir.to_string_lossy().as_ref());
let session = "legacy-x";
fs::write(dir.join(format!("{}.pid", session)), "999999").unwrap();
fs::write(dir.join(format!("{}.stream", session)), "35555").unwrap();
fs::write(dir.join(format!("{}.meta.json", session)), "{}").unwrap();
#[cfg(unix)]
fs::write(dir.join(format!("{}.sock", session)), "").unwrap();
#[cfg(windows)]
fs::write(dir.join(format!("{}.port", session)), "45555").unwrap();
remove_session_artifacts(session);
assert!(!dir.join(format!("{}.pid", session)).exists());
assert!(!dir.join(format!("{}.stream", session)).exists());
assert!(!dir.join(format!("{}.meta.json", session)).exists());
#[cfg(unix)]
assert!(!dir.join(format!("{}.sock", session)).exists());
#[cfg(windows)]
assert!(!dir.join(format!("{}.port", session)).exists());
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_validate_default_daemon_identity_match() {
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR"]);
let dir = test_temp_dir("agent-browser-meta-ok");
fs::create_dir_all(&dir).unwrap();
_guard.set("AGENT_BROWSER_SOCKET_DIR", dir.to_string_lossy().as_ref());
let daemon_path = dir.join("daemon.js");
fs::write(&daemon_path, "// test").unwrap();
let canonical = daemon_path.canonicalize().unwrap();
let meta = serde_json::json!({
"cliVersion": env!("CARGO_PKG_VERSION"),
"daemonPath": canonical.to_string_lossy(),
});
fs::write(dir.join("default.meta.json"), meta.to_string()).unwrap();
assert!(validate_default_daemon_identity(&daemon_path));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_validate_default_daemon_identity_version_mismatch() {
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR"]);
let dir = test_temp_dir("agent-browser-meta-bad");
fs::create_dir_all(&dir).unwrap();
_guard.set("AGENT_BROWSER_SOCKET_DIR", dir.to_string_lossy().as_ref());
let daemon_path = dir.join("daemon.js");
fs::write(&daemon_path, "// test").unwrap();
let canonical = daemon_path.canonicalize().unwrap();
let meta = serde_json::json!({
"cliVersion": "0.0.0-fork.0",
"daemonPath": canonical.to_string_lossy(),
});
fs::write(dir.join("default.meta.json"), meta.to_string()).unwrap();
assert!(!validate_default_daemon_identity(&daemon_path));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_validate_default_daemon_identity_path_mismatch() {
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR"]);
let dir = test_temp_dir("agent-browser-meta-path-mismatch");
fs::create_dir_all(&dir).unwrap();
_guard.set("AGENT_BROWSER_SOCKET_DIR", dir.to_string_lossy().as_ref());
let daemon_path = dir.join("daemon.js");
let other_path = dir.join("daemon-other.js");
fs::write(&daemon_path, "// test").unwrap();
fs::write(&other_path, "// other").unwrap();
let other_canonical = other_path.canonicalize().unwrap();
let meta = serde_json::json!({
"cliVersion": env!("CARGO_PKG_VERSION"),
"daemonPath": other_canonical.to_string_lossy(),
});
fs::write(dir.join("default.meta.json"), meta.to_string()).unwrap();
assert!(!validate_default_daemon_identity(&daemon_path));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_validate_default_daemon_identity_missing_meta() {
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR"]);
let dir = test_temp_dir("agent-browser-meta-missing");
fs::create_dir_all(&dir).unwrap();
_guard.set("AGENT_BROWSER_SOCKET_DIR", dir.to_string_lossy().as_ref());
let daemon_path = dir.join("daemon.js");
fs::write(&daemon_path, "// test").unwrap();
assert!(!validate_default_daemon_identity(&daemon_path));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_validate_default_daemon_identity_bad_json() {
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR"]);
let dir = test_temp_dir("agent-browser-meta-bad-json");
fs::create_dir_all(&dir).unwrap();
_guard.set("AGENT_BROWSER_SOCKET_DIR", dir.to_string_lossy().as_ref());
let daemon_path = dir.join("daemon.js");
fs::write(&daemon_path, "// test").unwrap();
fs::write(dir.join("default.meta.json"), "{invalid-json").unwrap();
assert!(!validate_default_daemon_identity(&daemon_path));
let _ = fs::remove_dir_all(&dir);
}
// === Transient Error Detection Tests ===
#[test]
+2 -47
View File
@@ -10,16 +10,10 @@ mod validation;
use serde_json::json;
use std::env;
use std::fs;
use std::process::exit;
#[cfg(windows)]
use windows_sys::Win32::Foundation::CloseHandle;
#[cfg(windows)]
use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
use commands::{gen_id, parse_command, ParseError};
use connection::{ensure_daemon, get_socket_dir, send_command};
use connection::{ensure_daemon, list_live_sessions, send_command};
use flags::{clean_args, parse_flags};
use install::run_install;
use output::{print_command_help, print_help, print_response, print_version};
@@ -59,46 +53,7 @@ fn run_session(args: &[String], session: &str, json_mode: bool) {
match subcommand {
Some("list") => {
let socket_dir = get_socket_dir();
let mut sessions: Vec<String> = Vec::new();
if let Ok(entries) = fs::read_dir(&socket_dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
// Look for pid files in socket directory
if name.ends_with(".pid") {
let session_name = name.strip_suffix(".pid").unwrap_or("");
if !session_name.is_empty() {
// Check if session is actually running
let pid_path = socket_dir.join(&name);
if let Ok(pid_str) = fs::read_to_string(&pid_path) {
if let Ok(pid) = pid_str.trim().parse::<u32>() {
#[cfg(unix)]
let running = unsafe {
libc::kill(pid as i32, 0) == 0
|| std::io::Error::last_os_error().raw_os_error()
!= Some(libc::ESRCH)
};
#[cfg(windows)]
let running = unsafe {
let handle =
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
if handle != 0 {
CloseHandle(handle);
true
} else {
false
}
};
if running {
sessions.push(session_name.to_string());
}
}
}
}
}
}
}
let sessions = list_live_sessions();
if json_mode {
println!(
+3 -1
View File
@@ -2425,7 +2425,7 @@ Confirmation:
Sessions:
session Show current session name
session list List active sessions
session list List active sessions (stale entries are auto-cleaned)
Setup:
install Install browser binaries
@@ -2471,6 +2471,7 @@ Options:
--wait-until <mode> Navigation wait strategy for open/navigate: load, domcontentloaded, networkidle
--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)
Note: starting default session reaps all non-default daemon sessions
--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)
@@ -2514,6 +2515,7 @@ Environment:
AGENT_BROWSER_CONFIG Path to config file (or use --config)
AGENT_BROWSER_PARALLEL Isolated runtime channel for parallel AI runs (maps to parallel-<name>)
Best for stateless/no-login tasks where throughput matters
Note: any default-session command reaps non-default daemon sessions
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
+4 -2
View File
@@ -279,10 +279,12 @@ agent-browser state clean --older-than <days> # Delete old states
```bash
agent-browser session # Show current session name
agent-browser session list # List active sessions
agent-browser session list # List active sessions (auto-cleans stale entries)
agent-browser --parallel worker-a open https://example.com # Isolated runtime for parallel AI tasks
```
When a default-session command starts, all non-default daemon sessions are reaped to avoid stale daemon reuse.
## Daemon lifetime
By default, daemon processes auto-shutdown after 10 minutes of inactivity.
@@ -327,7 +329,7 @@ agent-browser reload # Reload page
--auto-connect # Auto-discover and connect to running Chrome
--tab-group <name> # Base title for agent tab groups (CDP plugin mode)
--tab-group-plugin-id <id> # Expected extension ID for tab-group handshake
--parallel <name> # Isolated runtime channel for parallel AI runs (maps to parallel-<name>)
--parallel <name> # Isolated runtime channel for parallel AI runs (maps to parallel-<name>; reaped when default session starts)
--resident # Keep daemon running; disable 10-minute idle auto-shutdown
--wait-until <mode> # Navigation wait strategy for open/navigate (load, domcontentloaded, networkidle)
--debug # Debug output (includes stealth connection type + capabilities)
+2 -1
View File
@@ -371,6 +371,7 @@ session window isolation controls, activation guard toggles, empty-group cleanup
```
Use this for stateless throughput tasks. For authenticated flows, prefer a stable `sessionName`.
When a default-session command runs, non-default daemon sessions are reaped.
## CLI-only daemon lifecycle flag
@@ -504,7 +505,7 @@ These environment variables configure additional daemon and runtime behavior:
<code>AGENT_BROWSER_PARALLEL</code>
</td>
<td>
Isolated runtime channel name for parallel AI runs (maps to <code>parallel-&lt;name&gt;</code>).
Isolated runtime channel name for parallel AI runs (maps to <code>parallel-&lt;name&gt;</code>). Non-default daemons are reaped when a default-session command starts.
</td>
<td>(none)</td>
</tr>
+7 -1
View File
@@ -17,6 +17,7 @@ agent-browser --parallel worker-a session
# Show active daemon sessions
agent-browser session list
# stale entries are auto-cleaned during listing
```
## Session isolation
@@ -40,6 +41,11 @@ agent-browser --parallel worker-b open https://example.org
`--parallel` is intended for stateless throughput tasks (navigation/extraction/checks). For authenticated flows, use a stable `--session-name`.
Default session isolation policy:
- Running a default-session command reaps all non-default daemon sessions (`parallel-*` and legacy named channels).
- This keeps the primary `default` runtime channel free from stale daemon reuse.
For long-running workers, add `--resident` to disable idle auto-shutdown:
```bash
@@ -195,7 +201,7 @@ agent-browser set headers '{"X-Custom-Header": "value"}'
<td>
<code>AGENT_BROWSER_PARALLEL</code>
</td>
<td>Isolated runtime channel name for parallel AI runs (maps to <code>parallel-&lt;name&gt;</code>)</td>
<td>Isolated runtime channel name for parallel AI runs (maps to <code>parallel-&lt;name&gt;</code>). Non-default daemons are reaped when a default-session command starts.</td>
</tr>
<tr>
<td>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "agent-browser-stealth",
"version": "0.16.3-fork.3",
"version": "0.16.3-fork.4",
"description": "Stealth browser automation CLI for AI agents with anti-bot evasions",
"type": "module",
"main": "dist/daemon.js",
+2
View File
@@ -216,6 +216,7 @@ agent-browser --parallel site2 snapshot -i
```
Use `--parallel <name>` for stateless throughput tasks (navigation, extraction, checks). For login/auth continuity, use `--session-name` instead.
Default-session commands intentionally reap all non-default daemon sessions (`parallel-*` and legacy named channels) to prevent stale daemon reuse.
### Connect to Existing Chrome
@@ -504,6 +505,7 @@ These behaviors are always active. For sensitive sites, combine with `--headed`
## Session Management and Cleanup
`--session` is ignored in this fork. Runtime defaults to `default`; use `--parallel <name>` for isolated concurrent channels, and `--session-name` for persistence isolation.
When a default-session command runs, non-default daemon sessions are reaped automatically.
Always close your browser session when done to avoid leaked processes:
+32
View File
@@ -269,6 +269,14 @@ export function getPidFile(session?: string): string {
return path.join(getSocketDir(), `${sess}.pid`);
}
/**
* Get the daemon metadata file path for a session.
*/
export function getMetaFile(session?: string): string {
const sess = session ?? currentSession;
return path.join(getSocketDir(), `${sess}.meta.json`);
}
/**
* Check if daemon is running for the current session
*/
@@ -313,9 +321,11 @@ export function getConnectionInfo(
export function cleanupSocket(session?: string): void {
const pidFile = getPidFile(session);
const streamPortFile = getStreamPortFile(session);
const metaFile = getMetaFile(session);
try {
if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
if (fs.existsSync(streamPortFile)) fs.unlinkSync(streamPortFile);
if (fs.existsSync(metaFile)) fs.unlinkSync(metaFile);
if (isWindows) {
const portFile = getPortFile(session);
if (fs.existsSync(portFile)) fs.unlinkSync(portFile);
@@ -792,6 +802,28 @@ export async function startDaemon(options?: {
// Write PID file before listening
fs.writeFileSync(pidFile, process.pid.toString());
try {
fs.chmodSync(pidFile, 0o600);
} catch {
// Best-effort hardening; skip on platforms that don't support POSIX modes.
}
const metaFile = getMetaFile();
// Ownership/version proof consumed by the CLI before reusing default daemon.
const daemonMeta = {
session: currentSession,
pid: process.pid,
startedAt: Date.now(),
daemonPath: process.argv[1] ? path.resolve(process.argv[1]) : '',
cliVersion: process.env.AGENT_BROWSER_CLI_VERSION ?? '',
mode: residentMode ? 'resident' : 'idle',
} as const;
fs.writeFileSync(metaFile, JSON.stringify(daemonMeta, null, 2));
try {
fs.chmodSync(metaFile, 0o600);
} catch {
// Best-effort hardening; skip on platforms that don't support POSIX modes.
}
if (isWindows) {
// Windows: use TCP socket on localhost