feat: enforce default session daemon isolation and bump 0.16.3-fork.4
This commit is contained in:
@@ -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`.
|
`--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 |
|
| Option | Purpose | Typical Usage |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `--parallel <name>` | Isolate runtime channel for concurrent AI tasks | Stateless/no-login parallel jobs |
|
| `--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.
|
- Prefer `--headed` for high-friction targets.
|
||||||
- Reuse session state with one stable `--session-name` for continuity (when omitted, it defaults to `default`).
|
- 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.
|
- 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.
|
- Use `--resident` only for deliberate long-running workflows, and close when done.
|
||||||
- Keep locale/timezone consistent with target market.
|
- Keep locale/timezone consistent with target market.
|
||||||
- For challenge-heavy pages, prefer `--wait-until domcontentloaded` on `open`/`navigate` to avoid `load` stalls.
|
- For challenge-heavy pages, prefer `--wait-until domcontentloaded` on `open`/`navigate` to avoid `load` stalls.
|
||||||
|
|||||||
Generated
+1
-1
@@ -45,7 +45,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-browser-stealth"
|
name = "agent-browser-stealth"
|
||||||
version = "0.16.3-fork.3"
|
version = "0.16.3-fork.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "agent-browser-stealth"
|
name = "agent-browser-stealth"
|
||||||
version = "0.16.3-fork.3"
|
version = "0.16.3-fork.4"
|
||||||
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"
|
||||||
|
|||||||
+410
-45
@@ -4,7 +4,7 @@ use std::env;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{BufRead, BufReader, Read, Write};
|
use std::io::{BufRead, BufReader, Read, Write};
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -116,6 +116,30 @@ fn get_pid_path(session: &str) -> PathBuf {
|
|||||||
get_socket_dir().join(format!("{}.pid", session))
|
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
|
/// Clean up stale socket and PID files for a session
|
||||||
fn cleanup_stale_files(session: &str) {
|
fn cleanup_stale_files(session: &str) {
|
||||||
// Never delete files for a live daemon. A missing PID file can happen in
|
// 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;
|
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 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)]
|
fn reap_sessions_for_default_start() {
|
||||||
{
|
let socket_dir = get_socket_dir();
|
||||||
let socket_path = get_socket_path(session);
|
let entries = match fs::read_dir(&socket_dir) {
|
||||||
let _ = fs::remove_file(&socket_path);
|
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 expected = expected_daemon_path
|
||||||
{
|
.canonicalize()
|
||||||
let port_path = get_port_path(session);
|
.unwrap_or_else(|_| expected_daemon_path.to_path_buf());
|
||||||
let _ = fs::remove_file(&port_path);
|
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)]
|
#[cfg(windows)]
|
||||||
@@ -202,17 +400,35 @@ pub fn ensure_daemon(
|
|||||||
tab_group: Option<&str>,
|
tab_group: Option<&str>,
|
||||||
tab_group_plugin_id: Option<&str>,
|
tab_group_plugin_id: Option<&str>,
|
||||||
) -> Result<DaemonResult, String> {
|
) -> 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.
|
// Socket readiness is the source of truth for a usable daemon.
|
||||||
// PID files can be missing/stale under concurrent start/stop races.
|
// PID files can be missing/stale under concurrent start/stop races.
|
||||||
if daemon_ready(session) {
|
if daemon_ready(session) {
|
||||||
// Double-check it's actually responsive by waiting and checking again
|
let mut should_reuse = true;
|
||||||
// This handles the race condition where daemon is shutting down
|
if session == "default" {
|
||||||
// (daemon has a 100ms shutdown delay, so we wait longer)
|
should_reuse = validate_default_daemon_identity(&daemon_path);
|
||||||
thread::sleep(Duration::from_millis(150));
|
}
|
||||||
if daemon_ready(session) {
|
|
||||||
return Ok(DaemonResult {
|
if should_reuse {
|
||||||
already_running: true,
|
// 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.
|
// Keep handle to detect early daemon exit and surface startup errors.
|
||||||
#[allow(unused_assignments)]
|
#[allow(unused_assignments)]
|
||||||
let mut daemon_child: Option<std::process::Child> = None;
|
let mut daemon_child: Option<std::process::Child> = None;
|
||||||
@@ -290,14 +483,15 @@ pub fn ensure_daemon(
|
|||||||
use std::os::unix::process::CommandExt;
|
use std::os::unix::process::CommandExt;
|
||||||
|
|
||||||
let mut cmd = Command::new("node");
|
let mut cmd = Command::new("node");
|
||||||
cmd.arg(daemon_path)
|
cmd.arg(&daemon_path)
|
||||||
.arg(if resident {
|
.arg(if resident {
|
||||||
"--resident"
|
"--resident"
|
||||||
} else {
|
} else {
|
||||||
"--idle-auto-shutdown"
|
"--idle-auto-shutdown"
|
||||||
})
|
})
|
||||||
.env("AGENT_BROWSER_DAEMON", "1")
|
.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 {
|
if headed {
|
||||||
cmd.env("AGENT_BROWSER_HEADED", "1");
|
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)
|
// On Windows, call node directly. Command::new handles PATH resolution (node.exe or node.cmd)
|
||||||
// and automatically quotes arguments containing spaces.
|
// and automatically quotes arguments containing spaces.
|
||||||
let mut cmd = Command::new("node");
|
let mut cmd = Command::new("node");
|
||||||
cmd.arg(daemon_path)
|
cmd.arg(&daemon_path)
|
||||||
.arg(if resident {
|
.arg(if resident {
|
||||||
"--resident"
|
"--resident"
|
||||||
} else {
|
} else {
|
||||||
"--idle-auto-shutdown"
|
"--idle-auto-shutdown"
|
||||||
})
|
})
|
||||||
.env("AGENT_BROWSER_DAEMON", "1")
|
.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 {
|
if headed {
|
||||||
cmd.env("AGENT_BROWSER_HEADED", "1");
|
cmd.env("AGENT_BROWSER_HEADED", "1");
|
||||||
@@ -606,6 +801,15 @@ fn send_command_once(cmd: &Value, session: &str) -> Result<Response, String> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::test_utils::EnvGuard;
|
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]
|
#[test]
|
||||||
fn test_get_socket_dir_explicit_override() {
|
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 ===
|
// === Transient Error Detection Tests ===
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+2
-47
@@ -10,16 +10,10 @@ mod validation;
|
|||||||
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
|
||||||
use std::process::exit;
|
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 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 flags::{clean_args, parse_flags};
|
||||||
use install::run_install;
|
use install::run_install;
|
||||||
use output::{print_command_help, print_help, print_response, print_version};
|
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 {
|
match subcommand {
|
||||||
Some("list") => {
|
Some("list") => {
|
||||||
let socket_dir = get_socket_dir();
|
let sessions = list_live_sessions();
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if json_mode {
|
if json_mode {
|
||||||
println!(
|
println!(
|
||||||
|
|||||||
+3
-1
@@ -2425,7 +2425,7 @@ Confirmation:
|
|||||||
|
|
||||||
Sessions:
|
Sessions:
|
||||||
session Show current session name
|
session Show current session name
|
||||||
session list List active sessions
|
session list List active sessions (stale entries are auto-cleaned)
|
||||||
|
|
||||||
Setup:
|
Setup:
|
||||||
install Install browser binaries
|
install Install browser binaries
|
||||||
@@ -2471,6 +2471,7 @@ Options:
|
|||||||
--wait-until <mode> Navigation wait strategy for open/navigate: load, domcontentloaded, networkidle
|
--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>)
|
--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)
|
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
|
--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)
|
--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)
|
--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_CONFIG Path to config file (or use --config)
|
||||||
AGENT_BROWSER_PARALLEL Isolated runtime channel for parallel AI runs (maps to parallel-<name>)
|
AGENT_BROWSER_PARALLEL Isolated runtime channel for parallel AI runs (maps to parallel-<name>)
|
||||||
Best for stateless/no-login tasks where throughput matters
|
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_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_STATE_EXPIRE_DAYS Auto-delete states older than N days (default: 30)
|
||||||
AGENT_BROWSER_EXECUTABLE_PATH Custom browser executable path
|
AGENT_BROWSER_EXECUTABLE_PATH Custom browser executable path
|
||||||
|
|||||||
@@ -279,10 +279,12 @@ agent-browser state clean --older-than <days> # Delete old states
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
agent-browser session # Show current session name
|
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
|
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
|
## Daemon lifetime
|
||||||
|
|
||||||
By default, daemon processes auto-shutdown after 10 minutes of inactivity.
|
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
|
--auto-connect # Auto-discover and connect to running Chrome
|
||||||
--tab-group <name> # Base title for agent tab groups (CDP plugin mode)
|
--tab-group <name> # Base title for agent tab groups (CDP plugin mode)
|
||||||
--tab-group-plugin-id <id> # Expected extension ID for tab-group handshake
|
--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
|
--resident # Keep daemon running; disable 10-minute idle auto-shutdown
|
||||||
--wait-until <mode> # Navigation wait strategy for open/navigate (load, domcontentloaded, networkidle)
|
--wait-until <mode> # Navigation wait strategy for open/navigate (load, domcontentloaded, networkidle)
|
||||||
--debug # Debug output (includes stealth connection type + capabilities)
|
--debug # Debug output (includes stealth connection type + capabilities)
|
||||||
|
|||||||
@@ -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`.
|
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
|
## CLI-only daemon lifecycle flag
|
||||||
|
|
||||||
@@ -504,7 +505,7 @@ These environment variables configure additional daemon and runtime behavior:
|
|||||||
<code>AGENT_BROWSER_PARALLEL</code>
|
<code>AGENT_BROWSER_PARALLEL</code>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
Isolated runtime channel name for parallel AI runs (maps to <code>parallel-<name></code>).
|
Isolated runtime channel name for parallel AI runs (maps to <code>parallel-<name></code>). Non-default daemons are reaped when a default-session command starts.
|
||||||
</td>
|
</td>
|
||||||
<td>(none)</td>
|
<td>(none)</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ agent-browser --parallel worker-a session
|
|||||||
|
|
||||||
# Show active daemon sessions
|
# Show active daemon sessions
|
||||||
agent-browser session list
|
agent-browser session list
|
||||||
|
# stale entries are auto-cleaned during listing
|
||||||
```
|
```
|
||||||
|
|
||||||
## Session isolation
|
## 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`.
|
`--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:
|
For long-running workers, add `--resident` to disable idle auto-shutdown:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -195,7 +201,7 @@ agent-browser set headers '{"X-Custom-Header": "value"}'
|
|||||||
<td>
|
<td>
|
||||||
<code>AGENT_BROWSER_PARALLEL</code>
|
<code>AGENT_BROWSER_PARALLEL</code>
|
||||||
</td>
|
</td>
|
||||||
<td>Isolated runtime channel name for parallel AI runs (maps to <code>parallel-<name></code>)</td>
|
<td>Isolated runtime channel name for parallel AI runs (maps to <code>parallel-<name></code>). Non-default daemons are reaped when a default-session command starts.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "agent-browser-stealth",
|
"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",
|
"description": "Stealth browser automation CLI for AI agents with anti-bot evasions",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/daemon.js",
|
"main": "dist/daemon.js",
|
||||||
|
|||||||
@@ -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.
|
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
|
### Connect to Existing Chrome
|
||||||
|
|
||||||
@@ -504,6 +505,7 @@ These behaviors are always active. For sensitive sites, combine with `--headed`
|
|||||||
## Session Management and Cleanup
|
## 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.
|
`--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:
|
Always close your browser session when done to avoid leaked processes:
|
||||||
|
|
||||||
|
|||||||
@@ -269,6 +269,14 @@ export function getPidFile(session?: string): string {
|
|||||||
return path.join(getSocketDir(), `${sess}.pid`);
|
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
|
* Check if daemon is running for the current session
|
||||||
*/
|
*/
|
||||||
@@ -313,9 +321,11 @@ export function getConnectionInfo(
|
|||||||
export function cleanupSocket(session?: string): void {
|
export function cleanupSocket(session?: string): void {
|
||||||
const pidFile = getPidFile(session);
|
const pidFile = getPidFile(session);
|
||||||
const streamPortFile = getStreamPortFile(session);
|
const streamPortFile = getStreamPortFile(session);
|
||||||
|
const metaFile = getMetaFile(session);
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
|
if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
|
||||||
if (fs.existsSync(streamPortFile)) fs.unlinkSync(streamPortFile);
|
if (fs.existsSync(streamPortFile)) fs.unlinkSync(streamPortFile);
|
||||||
|
if (fs.existsSync(metaFile)) fs.unlinkSync(metaFile);
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
const portFile = getPortFile(session);
|
const portFile = getPortFile(session);
|
||||||
if (fs.existsSync(portFile)) fs.unlinkSync(portFile);
|
if (fs.existsSync(portFile)) fs.unlinkSync(portFile);
|
||||||
@@ -792,6 +802,28 @@ export async function startDaemon(options?: {
|
|||||||
|
|
||||||
// Write PID file before listening
|
// Write PID file before listening
|
||||||
fs.writeFileSync(pidFile, process.pid.toString());
|
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) {
|
if (isWindows) {
|
||||||
// Windows: use TCP socket on localhost
|
// Windows: use TCP socket on localhost
|
||||||
|
|||||||
Reference in New Issue
Block a user