Skip to content
Merged
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
4 changes: 4 additions & 0 deletions desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ const overrides = new Map([
// team-instructions-first-class: ManagedAgentRecord fixture gains the new
// team_id field (+1 line).
["src-tauri/src/managed_agents/readiness.rs", 1765],
// Windows PATH-correctness fix: 3 #[cfg(windows)] test functions covering
// .cmd shim rejection, .bat shim rejection, and .exe acceptance for
// configure_runtime_cli (fix #2397). Test-only growth; queued to split.
["src-tauri/src/managed_agents/runtime/tests.rs", 1041],
// applyWorkspace reposDir parameter plus the validateReposDir binding,
// threaded through Tauri invokes for configurable repos_dir, plus the
// harness-persona-sync `harnessOverride` create-input bit — load-bearing
Expand Down
75 changes: 65 additions & 10 deletions desktop/src-tauri/src/commands/agent_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -568,16 +568,32 @@ fn install_shell_command(command: &str) -> Result<std::process::Command, String>
cmd.env("npm_config_cache", prefix.join("cache"));
}

let mut path_parts = Vec::new();
if let Some(managed_node_bin) = crate::managed_agents::buzz_managed_node_bin_dir() {
path_parts.push(managed_node_bin);
}
if let Some(managed_bin) = crate::managed_agents::buzz_managed_npm_bin_dir() {
path_parts.push(managed_bin);
}
if let Some(ref path) = crate::managed_agents::login_shell_path() {
path_parts.extend(std::env::split_paths(path));
}
// Compose the PATH for the install shell using the same kernel as the
// runtime/probe path so the two can never drift. managed entries first
// (Node/npm bins keep precedence); login-shell entries next; inherited
// process PATH appended last on Windows when no login-shell PATH exists
// (login_shell_path() always returns None on Windows — Git Bash paths are
// POSIX-shaped and poison native children; cmd.env("PATH", …) replaces
// rather than extends, so without inherited the install shell loses npm).
let login_path = crate::managed_agents::login_shell_path();
let had_login = login_path.is_some();
let managed: Vec<std::path::PathBuf> = [
crate::managed_agents::buzz_managed_node_bin_dir(),
crate::managed_agents::buzz_managed_npm_bin_dir(),
]
.into_iter()
.flatten()
.collect();
let login: Vec<std::path::PathBuf> = login_path
.as_deref()
.map(|p| std::env::split_paths(p).collect())
.unwrap_or_default();
let inherited: Vec<std::path::PathBuf> = std::env::var_os("PATH")
.map(|p| std::env::split_paths(&p).collect())
.unwrap_or_default();
let use_inherited = crate::managed_agents::should_use_inherited(had_login, true, cfg!(windows));
let path_parts =
crate::managed_agents::compose_path_entries(managed, login, inherited, use_inherited);
if !path_parts.is_empty() {
if let Ok(path) = std::env::join_paths(path_parts) {
cmd.env("PATH", path);
Expand Down Expand Up @@ -1296,6 +1312,45 @@ mod tests {
);
}

/// On Windows, `install_shell_command` must set PATH to a value that
/// includes the inherited process PATH, so node/npm are visible inside
/// the install shell even when no managed Node runtime is present.
#[cfg(windows)]
#[test]
fn test_install_shell_command_includes_process_path_on_windows() {
let _guard = crate::managed_agents::lock_path_mutex();
let previous = std::env::var_os("PATH");
// Plant a sentinel in the process PATH that the test can detect.
let sentinel = r"C:\TestSentinel\bin";
std::env::set_var("PATH", sentinel);

let result = super::install_shell_command("echo test");

match previous {
Some(p) => std::env::set_var("PATH", p),
None => std::env::remove_var("PATH"),
}

let cmd = result.expect("install_shell_command must succeed on Windows with Git");
let path_value = cmd
.get_envs()
.find(|(key, _)| *key == "PATH")
.and_then(|(_, val)| val)
.map(|v| v.to_string_lossy().into_owned())
.expect("install_shell_command must always set a PATH env var on Windows");

// The sentinel (inherited process PATH) must appear in the composed PATH.
assert!(
path_value.contains(sentinel),
"install_shell_command PATH must include the inherited process PATH; got: {path_value}"
);
// The sentinel must appear LAST — managed Buzz dirs must have precedence.
assert!(
path_value.ends_with(sentinel),
"inherited process PATH must be appended LAST so managed dirs keep precedence; got: {path_value}"
);
}

// ── Phase B: per-OS install commands ──────────────────────────────────────

/// On non-Windows, cli_install_commands_for_os returns the default commands.
Expand Down
12 changes: 12 additions & 0 deletions desktop/src-tauri/src/managed_agents/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ use crate::{

mod path;
pub(in crate::managed_agents) use path::build_augmented_path;
pub(crate) use path::compose_path_entries;
pub(crate) use path::should_skip_claude_executable;
pub(crate) use path::should_use_inherited;

mod stop;
pub(crate) use stop::managed_agent_runtime_keys;
Expand Down Expand Up @@ -1602,6 +1605,15 @@ pub(crate) fn configure_runtime_cli(
return;
}
if let Some(cli_path) = runtime.underlying_cli.and_then(resolve_command) {
// On Windows, `.cmd` and `.bat` files are batch shims — they cannot be
// passed directly to `CreateProcess` and cause EINVAL when the Claude
// adapter tries to spawn them (issue #2397). Skip setting
// `CLAUDE_CODE_EXECUTABLE` for shim paths so the adapter falls back to
// its own PATH lookup and finds the real binary instead.
// Non-Windows: `.cmd`/`.bat` are valid executables and must be assigned.
if should_skip_claude_executable(&cli_path, cfg!(windows)) {
return;
}
command.env("CLAUDE_CODE_EXECUTABLE", cli_path);
}
}
Expand Down
Loading
Loading