fix(agent): propagate discovered PATH to agent subprocesses

Prioritize the agent executable directory while preserving discovered
search paths for Pi package operations and CLI version probes.

Add coverage for PATH ordering and downstream command resolution.
This commit is contained in:
lzt404 committed 2026-08-26 22:46:23 +08:00
1 parent 5ebfe43778
commit 50df18067b
2 files changed
+107 -9

No files matched your search

+46 -9
View File
@@ -396,7 +396,9 @@ pub(crate) fn inspect_pi_provider_status(
"unconfigured"
};
let error = errors.into_iter().next();
let cli_version = executable.as_deref().and_then(read_agent_version);
let cli_version = executable
.as_deref()
.and_then(|path| read_agent_version(path, home));
let plugin_version = read_pi_provider_version(home).ok().flatten();
AgentConfigStatus {
id: PI_AGENT_ID.to_string(),
@@ -559,6 +561,31 @@ pub(crate) fn uninstall_pi_provider_inner(
))
}
pub(crate) fn agent_command_path(
home: &Path,
executable: &Path,
) -> Result<std::ffi::OsString, String> {
let mut directories = agent_executable_directories(home);
if let Some(executable_directory) = executable
.parent()
.filter(|path| !path.as_os_str().is_empty())
{
directories.retain(|directory| directory != executable_directory);
directories.insert(0, executable_directory.to_path_buf());
}
env::join_paths(directories).map_err(|error| format!("构造智能体命令 PATH 失败: {error}"))
}
pub(crate) fn configure_agent_command_environment(
command: &mut Command,
home: &Path,
executable: &Path,
) -> Result<(), String> {
command.env("PATH", agent_command_path(home, executable)?);
Ok(())
}
pub(crate) fn install_pi_package(
executable: &Path,
home: &Path,
@@ -576,6 +603,7 @@ pub(crate) fn install_pi_package(
.stdin(Stdio::null());
configure_background_command(&mut command);
configure_networked_command(&mut command, proxy_url);
configure_agent_command_environment(&mut command, home, executable)?;
let output = command
.output()
.map_err(|error| format!("执行 Pi 插件安装失败: {error}"))?;
@@ -613,6 +641,7 @@ pub(crate) fn update_pi_package(
.stdin(Stdio::null());
configure_background_command(&mut command);
configure_networked_command(&mut command, proxy_url);
configure_agent_command_environment(&mut command, home, executable)?;
let output = command
.output()
.map_err(|error| format!("执行 Pi 插件更新失败: {error}"))?;
@@ -644,6 +673,7 @@ pub(crate) fn remove_pi_package(executable: &Path, home: &Path) -> Result<(), St
.current_dir(home)
.stdin(Stdio::null());
configure_background_command(&mut command);
configure_agent_command_environment(&mut command, home, executable)?;
let output = command
.output()
.map_err(|error| format!("鎵ц Pi 鎻掍欢鍗歌浇澶辫触: {error}"))?;
@@ -899,9 +929,11 @@ pub(crate) fn inspect_agent_config(
find_named_agent_executable(home, &["zcode"])
.filter(|path| executable.as_ref() != Some(path))
.as_deref()
.and_then(read_agent_version)
.and_then(|path| read_agent_version(path, home))
} else if should_probe_primary_agent_executable_version(client) {
executable.as_deref().and_then(read_agent_version)
executable
.as_deref()
.and_then(|path| read_agent_version(path, home))
} else {
None
};
@@ -915,7 +947,7 @@ pub(crate) fn inspect_agent_config(
AgentClient::ClaudeDesktop => read_claude_desktop_version(home),
AgentClient::Codex => codex_app_installation
.as_ref()
.and_then(read_codex_app_installation_version),
.and_then(|installation| read_codex_app_installation_version(installation, home)),
AgentClient::OpenCode => opencode_desktop_application
.as_deref()
.and_then(read_opencode_desktop_version),
@@ -1950,7 +1982,10 @@ pub(crate) fn read_claude_desktop_version(home: &Path) -> Option<String> {
}
}
pub(crate) fn read_codex_app_installation_version(installation: &CodexAppTarget) -> Option<String> {
pub(crate) fn read_codex_app_installation_version(
installation: &CodexAppTarget,
_home: &Path,
) -> Option<String> {
#[cfg(target_os = "windows")]
{
match installation {
@@ -1959,7 +1994,7 @@ pub(crate) fn read_codex_app_installation_version(installation: &CodexAppTarget)
read_windows_codex_store_version()
}
CodexAppTarget::Application(path) => {
read_windows_executable_version(path).or_else(|| read_agent_version(path))
read_windows_executable_version(path).or_else(|| read_agent_version(path, _home))
}
}
}
@@ -1979,7 +2014,7 @@ pub(crate) fn read_zcode_app_version(home: &Path) -> Option<String> {
#[cfg(target_os = "windows")]
{
find_zcode_desktop_executable(home).and_then(|path| {
read_windows_executable_version(&path).or_else(|| read_agent_version(&path))
read_windows_executable_version(&path).or_else(|| read_agent_version(&path, home))
})
}
#[cfg(target_os = "macos")]
@@ -1992,7 +2027,8 @@ pub(crate) fn read_zcode_app_version(home: &Path) -> Option<String> {
.find(|path| path.is_dir())
.and_then(|path| read_macos_app_version(&path))
.or_else(|| {
find_named_agent_executable(home, &["zcode"]).and_then(|path| read_agent_version(&path))
find_named_agent_executable(home, &["zcode"])
.and_then(|path| read_agent_version(&path, home))
})
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
@@ -2748,7 +2784,7 @@ fn terminate_agent_probe_process(child: &mut Child) {
let _ = child.wait();
}
pub(crate) fn read_agent_version(path: &Path) -> Option<String> {
pub(crate) fn read_agent_version(path: &Path, home: &Path) -> Option<String> {
#[cfg(target_os = "windows")]
let mut command = windows_command_for_executable(path, false);
@@ -2757,6 +2793,7 @@ pub(crate) fn read_agent_version(path: &Path) -> Option<String> {
command.arg("--version");
configure_background_command(&mut command);
configure_agent_command_environment(&mut command, home, path).ok()?;
let output = command_output_with_timeout(&mut command, AGENT_VERSION_PROBE_TIMEOUT).ok()??;
if !output.status.success() {
return None;
@@ -1919,6 +1919,67 @@ fn detected_agent_version_requires_a_real_version_value() {
assert!(normalize_detected_agent_version(&"1".repeat(257)).is_none());
}
#[test]
fn agent_command_path_prioritizes_the_executable_and_preserves_discovered_paths() {
let home = agent_test_home("command-path");
let executable_directory = home.join("node-runtime/bin");
let executable = executable_directory.join("pi");
fs::create_dir_all(&executable_directory).unwrap();
let path = agent_command_path(&home, &executable).unwrap();
let directories = env::split_paths(&path).collect::<Vec<_>>();
assert_eq!(directories.first(), Some(&executable_directory));
assert!(directories.contains(&home.join(".local/bin")));
if let Some(inherited_path) = env::var_os("PATH") {
for inherited_directory in env::split_paths(&inherited_path) {
assert!(directories.contains(&inherited_directory));
}
}
fs::remove_dir_all(home).unwrap();
}
#[test]
fn agent_version_probe_exposes_the_executable_directory_to_child_commands() {
let home = agent_test_home("version-path");
let executable_directory = home.join("node-runtime/bin");
fs::create_dir_all(&executable_directory).unwrap();
#[cfg(target_os = "windows")]
let (executable, helper) = (
executable_directory.join("pi.cmd"),
executable_directory.join("agent-version-helper.cmd"),
);
#[cfg(not(target_os = "windows"))]
let (executable, helper) = (
executable_directory.join("pi"),
executable_directory.join("agent-version-helper"),
);
#[cfg(target_os = "windows")]
{
fs::write(&executable, "@echo off\r\nagent-version-helper.cmd\r\n").unwrap();
fs::write(&helper, "@echo off\r\necho pi 1.2.3\r\n").unwrap();
}
#[cfg(not(target_os = "windows"))]
{
use std::os::unix::fs::PermissionsExt;
fs::write(&executable, "#!/usr/bin/env sh\nagent-version-helper\n").unwrap();
fs::write(&helper, "#!/usr/bin/env sh\nprintf 'pi 1.2.3\\n'\n").unwrap();
fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)).unwrap();
fs::set_permissions(&helper, fs::Permissions::from_mode(0o755)).unwrap();
}
assert_eq!(
read_agent_version(&executable, &home).as_deref(),
Some("pi 1.2.3")
);
fs::remove_dir_all(home).unwrap();
}
#[test]
fn agent_probe_command_captures_output_before_timeout() {
#[cfg(target_os = "windows")]