Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions desktop/src-tauri/src/managed_agents/agent_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub(crate) fn augmented_path() -> Option<String> {
.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"),
)
}

Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/managed_agents/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
43 changes: 41 additions & 2 deletions desktop/src-tauri/src/managed_agents/runtime/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,6 +25,7 @@ pub(in crate::managed_agents) fn build_augmented_path(
exe_parent: Option<PathBuf>,
shell_path: Option<String>,
nvm_bin: Option<PathBuf>,
inherited_path: Option<std::ffi::OsString>,
) -> Option<String> {
let mut parts: Vec<PathBuf> = Vec::new();
let home_added = home.is_some();
Expand All @@ -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;
}
Expand All @@ -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<std::ffi::OsString>) -> Vec<PathBuf> {
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)]
Expand All @@ -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}");
Expand All @@ -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"));
}

Expand All @@ -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();
Expand All @@ -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"));
}
}