diff --git a/src-tauri/src/agents/discovery.rs b/src-tauri/src/agents/discovery.rs index 47f2340..a6ac899 100644 --- a/src-tauri/src/agents/discovery.rs +++ b/src-tauri/src/agents/discovery.rs @@ -3,6 +3,18 @@ use super::*; const AGENT_VERSION_PROBE_TIMEOUT: Duration = Duration::from_secs(5); const AGENT_VERSION_PROBE_POLL_INTERVAL: Duration = Duration::from_millis(20); +#[cfg(not(test))] +fn agent_configuration_environment(name: &str) -> Option { + env::var_os(name) + .map(PathBuf::from) + .filter(|path| !path.as_os_str().is_empty()) +} + +#[cfg(test)] +fn agent_configuration_environment(_: &str) -> Option { + None +} + pub(crate) fn agent_config_paths(client: AgentClient, home: &Path) -> Vec { match client { AgentClient::ClaudeCode => { @@ -89,23 +101,17 @@ pub(crate) fn opencode_config_path_from_environment( } pub(crate) fn kimi_code_home(home: &Path) -> PathBuf { - env::var_os("KIMI_CODE_HOME") - .map(PathBuf::from) - .filter(|path| !path.as_os_str().is_empty()) + agent_configuration_environment("KIMI_CODE_HOME") .unwrap_or_else(|| home.join(".kimi-code")) } pub(crate) fn grok_build_home(home: &Path) -> PathBuf { - env::var_os("GROK_HOME") - .map(PathBuf::from) - .filter(|path| !path.as_os_str().is_empty()) + agent_configuration_environment("GROK_HOME") .unwrap_or_else(|| home.join(".grok")) } pub(crate) fn deepseek_harness_home(home: &Path) -> PathBuf { - env::var_os("DSH_HOME") - .map(PathBuf::from) - .filter(|path| !path.as_os_str().is_empty()) + agent_configuration_environment("DSH_HOME") .unwrap_or_else(|| home.join(".dsh")) } @@ -131,9 +137,7 @@ pub(crate) fn read_package_json_version(path: &Path) -> Option { } pub(crate) fn pi_agent_directory(home: &Path) -> PathBuf { - env::var_os("PI_CODING_AGENT_DIR") - .map(PathBuf::from) - .filter(|path| !path.as_os_str().is_empty()) + agent_configuration_environment("PI_CODING_AGENT_DIR") .unwrap_or_else(|| home.join(".pi/agent")) } @@ -683,9 +687,7 @@ pub(crate) fn remove_pi_package(executable: &Path, home: &Path) -> Result<(), St } pub(crate) fn codex_configuration_directory(home: &Path) -> PathBuf { - env::var_os("CODEX_HOME") - .map(PathBuf::from) - .filter(|path| !path.as_os_str().is_empty()) + agent_configuration_environment("CODEX_HOME") .unwrap_or_else(|| home.join(".codex")) } @@ -786,15 +788,13 @@ pub(crate) fn claude_desktop_config_paths(_home: &Path) -> Vec { }; #[cfg(target_os = "windows")] { - let local = env::var_os("LOCALAPPDATA") - .map(PathBuf::from) + let local = agent_configuration_environment("LOCALAPPDATA") .unwrap_or_else(|| _home.join("AppData/Local")); claude_desktop_config_paths_from_local_app_data(&local) } #[cfg(target_os = "linux")] let (normal, threep) = { - let config_home = env::var_os("XDG_CONFIG_HOME") - .map(PathBuf::from) + let config_home = agent_configuration_environment("XDG_CONFIG_HOME") .filter(|path| path.is_absolute()) .unwrap_or_else(|| _home.join(".config")); (config_home.join("Claude"), config_home.join("Claude-3p")) @@ -865,16 +865,12 @@ pub(crate) fn find_windows_claude_data_directory( } pub(crate) fn hermes_agent_config_path(home: &Path) -> PathBuf { - if let Some(directory) = env::var_os("HERMES_HOME") - .map(PathBuf::from) - .filter(|path| !path.as_os_str().is_empty()) - { + if let Some(directory) = agent_configuration_environment("HERMES_HOME") { return directory.join("config.yaml"); } #[cfg(target_os = "windows")] { - env::var_os("LOCALAPPDATA") - .map(PathBuf::from) + agent_configuration_environment("LOCALAPPDATA") .unwrap_or_else(|| home.join("AppData/Local")) .join("hermes/config.yaml") } diff --git a/src-tauri/src/agents/history.rs b/src-tauri/src/agents/history.rs index 90873b2..578667a 100644 --- a/src-tauri/src/agents/history.rs +++ b/src-tauri/src/agents/history.rs @@ -65,6 +65,13 @@ pub(crate) struct HistoryPreview { } pub(crate) fn history_paths(client: &str, home: &Path) -> Result, String> { + let paths = history_paths_inner(client, home)?; + #[cfg(test)] + assert!(paths.iter().all(|path| path.starts_with(home))); + Ok(paths) +} + +fn history_paths_inner(client: &str, home: &Path) -> Result, String> { if client == PI_AGENT_ID { return Ok(vec![ pi_provider_config_path(home), diff --git a/src-tauri/src/tests.rs b/src-tauri/src/tests.rs index d062da5..8f14da8 100644 --- a/src-tauri/src/tests.rs +++ b/src-tauri/src/tests.rs @@ -1,6 +1,7 @@ use super::*; mod agent_configuration; +mod agent_paths; mod agent_state; mod agent_transactions; mod alias_edit; diff --git a/src-tauri/src/tests/agent_paths.rs b/src-tauri/src/tests/agent_paths.rs new file mode 100644 index 0000000..747da6d --- /dev/null +++ b/src-tauri/src/tests/agent_paths.rs @@ -0,0 +1,60 @@ +use super::support::*; +use super::*; + +#[test] +fn configuration_paths_ignore_inherited_environment() { + let home = agent_test_home("isolated-paths"); + for client in [ + "claude-code", + "claude-desktop", + "codex", + "opencode", + "openclaw", + "hermes", + "deepseek-harness", + "zcode", + "kimi-code", + "grok-build", + "pi", + ] { + let paths = history_paths(client, &home).unwrap(); + assert!(!paths.is_empty(), "{client}"); + assert!( + paths.iter().all(|path| path.starts_with(&home)), + "{client}: {paths:?}" + ); + } + if env::var_os("CPA_PATH_ISOLATION_CHILD").is_none() { + let outside = home.join("inherited-environment"); + let mut command = Command::new(env::current_exe().unwrap()); + command.args([ + "--exact", + "tests::agent_paths::configuration_paths_ignore_inherited_environment", + "--nocapture", + ]); + command.env("CPA_PATH_ISOLATION_CHILD", "1"); + for variable in [ + "LOCALAPPDATA", + "XDG_CONFIG_HOME", + "CODEX_HOME", + "HERMES_HOME", + "OPENCODE_CONFIG", + "KIMI_CODE_HOME", + "GROK_HOME", + "DSH_HOME", + "PI_CODING_AGENT_DIR", + ] { + command.env(variable, &outside); + } + configure_background_command(&mut command); + let output = command.output().unwrap(); + assert!( + output.status.success(), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(!outside.exists()); + } + fs::remove_dir_all(home).unwrap(); +}