diff --git a/desktop/src-tauri/src/managed_agents/agent_env.rs b/desktop/src-tauri/src/managed_agents/agent_env.rs index bf6bcb2298..7b87d3bd95 100644 --- a/desktop/src-tauri/src/managed_agents/agent_env.rs +++ b/desktop/src-tauri/src/managed_agents/agent_env.rs @@ -112,11 +112,25 @@ mod tests { discovery_env_with_baked_floor, parse_agent_env_lines, }; + fn environment_dump_command() -> std::process::Command { + #[cfg(windows)] + { + let mut command = std::process::Command::new("cmd"); + command.args(["/C", "set"]); + command + } + + #[cfg(not(windows))] + { + std::process::Command::new("env") + } + } + #[test] fn buzz_agent_provider_defaults_empty_in_oss_build() { // OSS (and normal test) builds set neither BUZZ_BUILD_BUZZ_AGENT_*, // so nothing is baked in and no BUZZ_AGENT_* is injected on spawn. - let mut cmd = std::process::Command::new("env"); + let mut cmd = environment_dump_command(); cmd.env_clear(); build_buzz_agent_provider_defaults(&mut cmd); let output = cmd.output().expect("env should run"); @@ -223,7 +237,7 @@ mod tests { // writing the baked default first, then overwriting with the record value. #[test] fn baked_defaults_do_not_override_record_provider_written_after() { - let mut cmd = std::process::Command::new("env"); + let mut cmd = environment_dump_command(); cmd.env_clear(); // Simulate what an internal build's baked defaults would inject. cmd.env("BUZZ_AGENT_PROVIDER", "databricks"); diff --git a/desktop/src-tauri/src/managed_agents/readiness/cli_probe.rs b/desktop/src-tauri/src/managed_agents/readiness/cli_probe.rs index 8e2f866b7e..0b647e4ec8 100644 --- a/desktop/src-tauri/src/managed_agents/readiness/cli_probe.rs +++ b/desktop/src-tauri/src/managed_agents/readiness/cli_probe.rs @@ -16,6 +16,7 @@ pub(crate) fn augmented_path() -> Option { .and_then(|exe| exe.parent().map(std::path::Path::to_path_buf)), crate::managed_agents::login_shell_path(), nvm_bin, + std::env::var_os("PATH"), ) } diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index 77b3a241e0..3a7ed9b719 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -1573,6 +1573,7 @@ pub fn spawn_agent_child( .and_then(|exe| exe.parent().map(std::path::Path::to_path_buf)), login_shell_path(), nvm_bin, + std::env::var_os("PATH"), ); let mut command = std::process::Command::new(&resolved_acp_command); diff --git a/desktop/src-tauri/src/managed_agents/runtime/path.rs b/desktop/src-tauri/src/managed_agents/runtime/path.rs index c1baadda7f..8fd4753e9d 100644 --- a/desktop/src-tauri/src/managed_agents/runtime/path.rs +++ b/desktop/src-tauri/src/managed_agents/runtime/path.rs @@ -11,6 +11,8 @@ use std::path::PathBuf; /// 4. `nvm_bin` — nvm's default Node.js bin dir (if the user uses nvm) /// 5. exe parent dir — DMG sidecars under `Contents/MacOS/` /// 6. user's login-shell `PATH` — runtimes like node/python from other managers +/// 7. on Windows, the app's inherited `PATH` — standard installations such as +/// `C:\Program Files\nodejs` are not available from a POSIX login shell /// /// `shell_path` is the raw colon-delimited string from a login shell, so it is /// split into individual entries before joining. Pushing it as a single segment @@ -23,6 +25,7 @@ pub(in crate::managed_agents) fn build_augmented_path( exe_parent: Option, shell_path: Option, nvm_bin: Option, + inherited_path: Option, ) -> Option { let mut parts: Vec = Vec::new(); let home_added = home.is_some(); @@ -49,6 +52,14 @@ pub(in crate::managed_agents) fn build_augmented_path( if let Some(shell_path) = shell_path { parts.extend(std::env::split_paths(&shell_path)); } + // Windows does not use `login_shell_path()` because Git Bash returns a + // POSIX, colon-delimited PATH. Do retain the native process PATH though: + // npm shims such as `codex-acp.cmd` need it to locate a standard Node.js + // installation in `C:\Program Files\nodejs`. + #[cfg(windows)] + parts.extend(windows_path_entries(inherited_path)); + #[cfg(not(windows))] + let _ = inherited_path; if parts.is_empty() { return None; } @@ -58,9 +69,19 @@ pub(in crate::managed_agents) fn build_augmented_path( .map(|s| s.to_string_lossy().into_owned()) } +#[cfg(windows)] +fn windows_path_entries(path: Option) -> Vec { + path.as_deref() + .map(std::env::split_paths) + .into_iter() + .flatten() + .collect() +} + #[cfg(test)] mod tests { use super::build_augmented_path; + #[cfg(not(windows))] use std::path::PathBuf; #[cfg(unix)] @@ -75,6 +96,7 @@ mod tests { Some(PathBuf::from("/Applications/Buzz.app/Contents/MacOS")), Some("/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin".to_string()), None, + None, ); let result = result.expect("path"); assert!(result.starts_with("/home/agent/.local/bin:"), "{result}"); @@ -88,15 +110,17 @@ mod tests { ); } + #[cfg(not(windows))] #[test] fn none_when_no_inputs() { - assert_eq!(build_augmented_path(None, None, None, None), None); + assert_eq!(build_augmented_path(None, None, None, None, None), None); } #[cfg(unix)] #[test] fn shell_path_only() { - let result = build_augmented_path(None, None, Some("/usr/bin:/bin".to_string()), None); + let result = + build_augmented_path(None, None, Some("/usr/bin:/bin".to_string()), None, None); assert_eq!(result.as_deref(), Some("/usr/bin:/bin")); } @@ -108,6 +132,7 @@ mod tests { Some(PathBuf::from("/Applications/Buzz.app/Contents/MacOS")), Some("/usr/bin:/bin".to_string()), Some(PathBuf::from("/home/user/.nvm/versions/node/v20.0.0/bin")), + None, ); let result = result.expect("path"); let local = result.find("/home/user/.local/bin").unwrap(); @@ -129,9 +154,23 @@ mod tests { Some(PathBuf::from("/usr/local/bin")), None, None, + None, ); let result = result.expect("path"); assert!(result.starts_with("/home/user/.local/bin:"), "{result}"); assert!(result.ends_with(":/usr/local/bin"), "{result}"); } + + #[cfg(windows)] + #[test] + fn windows_inherited_path_is_available_to_managed_processes() { + let result = build_augmented_path( + None, + None, + None, + None, + Some(std::ffi::OsString::from(r"C:\Program Files\nodejs")), + ); + assert_eq!(result.as_deref(), Some(r"C:\Program Files\nodejs")); + } }