fix: use ~/.agent-browser for socket files instead of TMPDIR (#180)

* fix: use ~/.agent-browser for socket files instead of TMPDIR

This fixes issue #163 where different TMPDIR values (common with
tmux/screen/VSCode/IntelliJ) caused the CLI and daemon to use
different socket paths.

Socket directory priority:
1. AGENT_BROWSER_SOCKET_DIR (explicit override)
2. $XDG_RUNTIME_DIR/agent-browser (Linux standard)
3. ~/.agent-browser (fallback, like Docker Desktop)

Both CLI (Rust) and daemon (Node.js) now use the same logic.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: session list now looks in correct socket directory

- Make get_socket_dir() public in connection.rs
- Update session list to use get_socket_dir() instead of temp_dir()
- Update pid file pattern from agent-browser-{session}.pid to {session}.pid
- Add tmpdir fallback to daemon.ts when homedir is unavailable

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add unit tests for socket directory resolution

Add comprehensive tests for get_socket_dir/getSocketDir to verify:
- AGENT_BROWSER_SOCKET_DIR takes priority
- Empty strings are ignored (fixes Rust/TypeScript consistency)
- XDG_RUNTIME_DIR fallback works correctly
- Home directory fallback when env vars unset

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
mmhiyoko
2026-01-22 01:15:25 -06:00
committed by GitHub
co-authored by Claude Opus 4.5
parent cb37630ccf
commit 946d236d9f
6 changed files with 401 additions and 30 deletions
+173 -9
View File
@@ -6,10 +6,55 @@ version = 4
name = "agent-browser"
version = "0.6.0"
dependencies = [
"dirs",
"libc",
"serde",
"serde_json",
"windows-sys",
"windows-sys 0.52.0",
]
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "dirs"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
@@ -24,12 +69,28 @@ version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "libredox"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
dependencies = [
"bitflags",
"libc",
]
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "proc-macro2"
version = "1.0.105"
@@ -48,6 +109,17 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom",
"libredox",
"thiserror",
]
[[package]]
name = "serde"
version = "1.0.228"
@@ -102,19 +174,69 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
@@ -123,28 +245,46 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@@ -157,24 +297,48 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
+1
View File
@@ -8,6 +8,7 @@ license = "Apache-2.0"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dirs = "5.0"
[target.'cfg(unix)'.dependencies]
libc = "0.2"
+124 -6
View File
@@ -81,21 +81,44 @@ impl Connection {
}
}
/// Get the base directory for socket/pid files.
/// Priority: AGENT_BROWSER_SOCKET_DIR > XDG_RUNTIME_DIR > ~/.agent-browser > tmpdir
pub fn get_socket_dir() -> PathBuf {
// 1. Explicit override (ignore empty string)
if let Ok(dir) = env::var("AGENT_BROWSER_SOCKET_DIR") {
if !dir.is_empty() {
return PathBuf::from(dir);
}
}
// 2. XDG_RUNTIME_DIR (Linux standard, ignore empty string)
if let Ok(runtime_dir) = env::var("XDG_RUNTIME_DIR") {
if !runtime_dir.is_empty() {
return PathBuf::from(runtime_dir).join("agent-browser");
}
}
// 3. Home directory fallback (like Docker Desktop's ~/.docker/run/)
if let Some(home) = dirs::home_dir() {
return home.join(".agent-browser");
}
// 4. Last resort: temp dir
env::temp_dir().join("agent-browser")
}
#[cfg(unix)]
fn get_socket_path(session: &str) -> PathBuf {
let tmp = env::temp_dir();
tmp.join(format!("agent-browser-{}.sock", session))
get_socket_dir().join(format!("{}.sock", session))
}
fn get_pid_path(session: &str) -> PathBuf {
let tmp = env::temp_dir();
tmp.join(format!("agent-browser-{}.pid", session))
get_socket_dir().join(format!("{}.pid", session))
}
#[cfg(windows)]
fn get_port_path(session: &str) -> PathBuf {
let tmp = env::temp_dir();
tmp.join(format!("agent-browser-{}.port", session))
get_socket_dir().join(format!("{}.port", session))
}
#[cfg(windows)]
@@ -178,6 +201,12 @@ pub fn ensure_daemon(
});
}
// Ensure socket directory exists
let socket_dir = get_socket_dir();
if !socket_dir.exists() {
fs::create_dir_all(&socket_dir).map_err(|e| format!("Failed to create socket directory: {}", e))?;
}
let exe_path = env::current_exe().map_err(|e| e.to_string())?;
let exe_dir = exe_path.parent().unwrap();
@@ -354,3 +383,92 @@ pub fn send_command(cmd: Value, session: &str) -> Result<Response, String> {
serde_json::from_str(&response_line).map_err(|e| format!("Invalid response: {}", e))
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, MutexGuard};
// Mutex to prevent parallel tests from interfering with env vars
static ENV_MUTEX: Mutex<()> = Mutex::new(());
/// RAII guard that locks env mutex and restores env vars on drop
struct EnvGuard<'a> {
_lock: MutexGuard<'a, ()>,
vars: Vec<(String, Option<String>)>,
}
impl<'a> EnvGuard<'a> {
fn new(var_names: &[&str]) -> Self {
let lock = ENV_MUTEX.lock().unwrap();
let vars = var_names
.iter()
.map(|&name| (name.to_string(), env::var(name).ok()))
.collect();
Self { _lock: lock, vars }
}
}
impl Drop for EnvGuard<'_> {
fn drop(&mut self) {
for (name, value) in &self.vars {
match value {
Some(v) => env::set_var(name, v),
None => env::remove_var(name),
}
}
}
}
#[test]
fn test_get_socket_dir_explicit_override() {
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "XDG_RUNTIME_DIR"]);
env::set_var("AGENT_BROWSER_SOCKET_DIR", "/custom/socket/path");
env::remove_var("XDG_RUNTIME_DIR");
assert_eq!(get_socket_dir(), PathBuf::from("/custom/socket/path"));
}
#[test]
fn test_get_socket_dir_ignores_empty_socket_dir() {
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "XDG_RUNTIME_DIR"]);
env::set_var("AGENT_BROWSER_SOCKET_DIR", "");
env::remove_var("XDG_RUNTIME_DIR");
assert!(get_socket_dir().to_string_lossy().ends_with(".agent-browser"));
}
#[test]
fn test_get_socket_dir_xdg_runtime() {
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "XDG_RUNTIME_DIR"]);
env::remove_var("AGENT_BROWSER_SOCKET_DIR");
env::set_var("XDG_RUNTIME_DIR", "/run/user/1000");
assert_eq!(get_socket_dir(), PathBuf::from("/run/user/1000/agent-browser"));
}
#[test]
fn test_get_socket_dir_ignores_empty_xdg_runtime() {
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "XDG_RUNTIME_DIR"]);
env::set_var("AGENT_BROWSER_SOCKET_DIR", "");
env::set_var("XDG_RUNTIME_DIR", "");
assert!(get_socket_dir().to_string_lossy().ends_with(".agent-browser"));
}
#[test]
fn test_get_socket_dir_home_fallback() {
let _guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "XDG_RUNTIME_DIR"]);
env::remove_var("AGENT_BROWSER_SOCKET_DIR");
env::remove_var("XDG_RUNTIME_DIR");
let result = get_socket_dir();
assert!(result.to_string_lossy().ends_with(".agent-browser"));
assert!(result.to_string_lossy().contains("home") || result.to_string_lossy().contains("Users"));
}
}
+7 -10
View File
@@ -19,7 +19,7 @@ use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
use commands::{gen_id, parse_command, ParseError};
use connection::{ensure_daemon, send_command};
use connection::{ensure_daemon, get_socket_dir, send_command};
use flags::{clean_args, parse_flags};
use install::run_install;
use output::{print_command_help, print_help, print_response, print_version};
@@ -59,21 +59,18 @@ fn run_session(args: &[String], session: &str, json_mode: bool) {
match subcommand {
Some("list") => {
let tmp = env::temp_dir();
let socket_dir = get_socket_dir();
let mut sessions: Vec<String> = Vec::new();
if let Ok(entries) = fs::read_dir(&tmp) {
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 socket files (Unix) or pid files
if name.starts_with("agent-browser-") && name.ends_with(".pid") {
let session_name = name
.strip_prefix("agent-browser-")
.and_then(|s| s.strip_suffix(".pid"))
.unwrap_or("");
// 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 = tmp.join(&name);
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)]
+60
View File
@@ -0,0 +1,60 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as os from 'os';
import * as path from 'path';
import { getSocketDir } from './daemon.js';
describe('getSocketDir', () => {
const originalEnv = { ...process.env };
beforeEach(() => {
// Clear relevant env vars before each test
delete process.env.AGENT_BROWSER_SOCKET_DIR;
delete process.env.XDG_RUNTIME_DIR;
});
afterEach(() => {
// Restore original env
process.env = { ...originalEnv };
});
describe('AGENT_BROWSER_SOCKET_DIR', () => {
it('should use custom path when set', () => {
process.env.AGENT_BROWSER_SOCKET_DIR = '/custom/socket/path';
expect(getSocketDir()).toBe('/custom/socket/path');
});
it('should ignore empty string', () => {
process.env.AGENT_BROWSER_SOCKET_DIR = '';
const result = getSocketDir();
expect(result).toContain('.agent-browser');
});
it('should take priority over XDG_RUNTIME_DIR', () => {
process.env.AGENT_BROWSER_SOCKET_DIR = '/custom/path';
process.env.XDG_RUNTIME_DIR = '/run/user/1000';
expect(getSocketDir()).toBe('/custom/path');
});
});
describe('XDG_RUNTIME_DIR', () => {
it('should use when AGENT_BROWSER_SOCKET_DIR is not set', () => {
process.env.XDG_RUNTIME_DIR = '/run/user/1000';
expect(getSocketDir()).toBe('/run/user/1000/agent-browser');
});
it('should ignore empty string', () => {
process.env.AGENT_BROWSER_SOCKET_DIR = '';
process.env.XDG_RUNTIME_DIR = '';
const result = getSocketDir();
expect(result).toContain('.agent-browser');
});
});
describe('fallback', () => {
it('should use home directory when env vars are not set', () => {
const result = getSocketDir();
const expected = path.join(os.homedir(), '.agent-browser');
expect(result).toBe(expected);
});
});
});
+36 -5
View File
@@ -47,6 +47,31 @@ function getPortForSession(session: string): number {
return 49152 + (Math.abs(hash) % 16383);
}
/**
* Get the base directory for socket/pid files.
* Priority: AGENT_BROWSER_SOCKET_DIR > XDG_RUNTIME_DIR > ~/.agent-browser > tmpdir
*/
export function getSocketDir(): string {
// 1. Explicit override
if (process.env.AGENT_BROWSER_SOCKET_DIR) {
return process.env.AGENT_BROWSER_SOCKET_DIR;
}
// 2. XDG_RUNTIME_DIR (Linux standard)
if (process.env.XDG_RUNTIME_DIR) {
return path.join(process.env.XDG_RUNTIME_DIR, 'agent-browser');
}
// 3. Home directory fallback (like Docker Desktop's ~/.docker/run/)
const homeDir = os.homedir();
if (homeDir) {
return path.join(homeDir, '.agent-browser');
}
// 4. Last resort: temp dir
return path.join(os.tmpdir(), 'agent-browser');
}
/**
* Get the socket path for the current session (Unix) or port (Windows)
*/
@@ -55,7 +80,7 @@ export function getSocketPath(session?: string): string {
if (isWindows) {
return String(getPortForSession(sess));
}
return path.join(os.tmpdir(), `agent-browser-${sess}.sock`);
return path.join(getSocketDir(), `${sess}.sock`);
}
/**
@@ -63,7 +88,7 @@ export function getSocketPath(session?: string): string {
*/
export function getPortFile(session?: string): string {
const sess = session ?? currentSession;
return path.join(os.tmpdir(), `agent-browser-${sess}.port`);
return path.join(getSocketDir(), `${sess}.port`);
}
/**
@@ -71,7 +96,7 @@ export function getPortFile(session?: string): string {
*/
export function getPidFile(session?: string): string {
const sess = session ?? currentSession;
return path.join(os.tmpdir(), `agent-browser-${sess}.pid`);
return path.join(getSocketDir(), `${sess}.pid`);
}
/**
@@ -104,7 +129,7 @@ export function getConnectionInfo(
if (isWindows) {
return { type: 'tcp', port: getPortForSession(sess) };
}
return { type: 'unix', path: path.join(os.tmpdir(), `agent-browser-${sess}.sock`) };
return { type: 'unix', path: path.join(getSocketDir(), `${sess}.sock`) };
}
/**
@@ -133,7 +158,7 @@ export function cleanupSocket(session?: string): void {
*/
export function getStreamPortFile(session?: string): string {
const sess = session ?? currentSession;
return path.join(os.tmpdir(), `agent-browser-${sess}.stream`);
return path.join(getSocketDir(), `${sess}.stream`);
}
/**
@@ -141,6 +166,12 @@ export function getStreamPortFile(session?: string): string {
* @param options.streamPort Port for WebSocket stream server (0 to disable)
*/
export async function startDaemon(options?: { streamPort?: number }): Promise<void> {
// Ensure socket directory exists
const socketDir = getSocketDir();
if (!fs.existsSync(socketDir)) {
fs.mkdirSync(socketDir, { recursive: true });
}
// Clean up any stale socket
cleanupSocket();