fix(desktop): resolve agent command path for DMG builds (#372)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wes
2026-04-20 14:16:20 -06:00
committed by GitHub
parent 28bf8bfeee
commit c6c27481a3
4 changed files with 51 additions and 26 deletions

View File

@@ -45,7 +45,7 @@ const overrides = new Map([
["src-tauri/src/lib.rs", 710], // sprout-media:// proxy + Range headers + Sprout nest init (ensure_nest) in setup() + huddle command registration + PTT global shortcut handler + persona pack commands + app_handle storage for event emission
["src-tauri/src/commands/media.rs", 720], // ffmpeg video transcode + poster frame extraction + run_ffmpeg_with_timeout (find_ffmpeg, is_video_file, transcode_to_mp4, extract_poster_frame, transcode_and_extract_poster) + spawn_blocking wrappers + tests
["src-tauri/src/commands/agents.rs", 880], // remote agent lifecycle routing (local + provider branches) + scope enforcement + persona pack metadata wiring + mcp_toolsets field
["src-tauri/src/managed_agents/runtime.rs", 690], // KNOWN_AGENT_BINARIES const + process_belongs_to_us FFI (macOS proc_name + Linux /proc/comm) + terminate_process + start/stop/sync lifecycle + pack persona live-read
["src-tauri/src/managed_agents/runtime.rs", 700], // KNOWN_AGENT_BINARIES const + process_belongs_to_us FFI (macOS proc_name + Linux /proc/comm) + terminate_process + start/stop/sync lifecycle + pack persona live-read + login shell PATH augmentation
["src-tauri/src/managed_agents/backend.rs", 530], // provider IPC, validation, discovery, binary resolution + tests
["src/features/huddle/HuddleContext.tsx", 650], // huddle lifecycle context + joinHuddle + connectAndSetupMedia shared helper + activeSpeakers/isReconnecting state + PTT (reusable AudioContext) + TTS subscription + mic level analyser (10fps throttle) + agent pubkey refresh
["src/features/agents/hooks.ts", 540], // agent query/mutation surface now includes built-in persona library activation + useUpdateManagedAgentMutation

View File

@@ -49,12 +49,11 @@ pub async fn get_agent_models(
let args = normalize_agent_args(&record.agent_command, record.agent_args.clone());
(
resolved,
record.agent_command.clone(),
args,
record.model.clone(),
)
let resolved_agent = resolve_command(&record.agent_command, Some(&app))
.map(|p| p.display().to_string())
.unwrap_or_else(|| record.agent_command.clone());
(resolved, resolved_agent, args, record.model.clone())
}; // store lock released — subprocess runs without holding the lock
// Use spawn_blocking because the desktop Tauri crate doesn't enable
@@ -65,6 +64,9 @@ pub async fn get_agent_models(
if let Some(home) = default_agent_workdir() {
cmd.current_dir(home);
}
if let Some(ref path) = crate::managed_agents::login_shell_path() {
cmd.env("PATH", path);
}
cmd.arg("models")
.arg("--json")
.env("SPROUT_ACP_AGENT_COMMAND", &agent_command)

View File

@@ -232,32 +232,45 @@ fn path_candidates_from_env(command: &str) -> Vec<PathBuf> {
.unwrap_or_default()
}
fn find_via_login_shell(command: &str) -> Option<PathBuf> {
/// Run a command in a login shell (tries zsh then bash).
/// Returns trimmed stdout if the command succeeds with non-empty output.
fn run_in_login_shell(args: &[&str]) -> Option<String> {
for shell in ["/bin/zsh", "/bin/bash"] {
let Ok(output) = Command::new(shell)
.args(["-l", "-c", r#"command -v -- "$1""#, "_", command])
.output()
else {
let Ok(output) = Command::new(shell).args(args).output() else {
continue;
};
if !output.status.success() {
continue;
}
let stdout = String::from_utf8_lossy(&output.stdout);
let Some(resolved) = stdout.lines().rfind(|line| !line.trim().is_empty()) else {
continue;
};
let path = PathBuf::from(resolved.trim());
if path.is_absolute() && path.exists() {
return Some(path);
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !stdout.is_empty() {
return Some(stdout);
}
}
None
}
fn find_via_login_shell(command: &str) -> Option<PathBuf> {
let stdout = run_in_login_shell(&["-l", "-c", r#"command -v -- "$1""#, "_", command])?;
let resolved = stdout.lines().rfind(|line| !line.trim().is_empty())?;
let path = PathBuf::from(resolved.trim());
(path.is_absolute() && path.exists()).then_some(path)
}
/// Return the user's full PATH from a login shell.
/// Cached via OnceLock so we only spawn one shell per app lifetime.
pub fn login_shell_path() -> Option<String> {
use std::sync::OnceLock;
static CACHED: OnceLock<Option<String>> = OnceLock::new();
CACHED
.get_or_init(|| {
let stdout = run_in_login_shell(&["-l", "-c", "echo $PATH"])?;
let last_line = stdout.lines().rfind(|l| !l.trim().is_empty())?;
Some(last_line.trim().to_string())
})
.clone()
}
fn find_command(command: &str) -> Option<PathBuf> {
resolve_command(command, None)
}

View File

@@ -4,9 +4,9 @@ use tauri::AppHandle;
use crate::{
managed_agents::{
append_log_marker, managed_agent_log_path, missing_command_message, normalize_agent_args,
open_log_file, resolve_command, ManagedAgentProcess, ManagedAgentRecord,
ManagedAgentSummary,
append_log_marker, login_shell_path, managed_agent_log_path, missing_command_message,
normalize_agent_args, open_log_file, resolve_command, ManagedAgentProcess,
ManagedAgentRecord, ManagedAgentSummary,
},
util::now_iso,
};
@@ -482,6 +482,13 @@ pub fn start_managed_agent_process(
.ok_or_else(|| missing_command_message(&record.acp_command, "ACP harness command"))?;
let resolved_mcp_command = resolve_command(&record.mcp_command, Some(app))
.ok_or_else(|| missing_command_message(&record.mcp_command, "MCP server command"))?;
// Resolve agent command to a full path (DMG launches have minimal PATH).
let resolved_agent_command = resolve_command(&record.agent_command, Some(app))
.map(|p| p.display().to_string())
.unwrap_or_else(|| record.agent_command.clone());
// Augment PATH for DMG launches so child processes (e.g. #!/usr/bin/env node) can find their runtimes.
let augmented_path = login_shell_path();
let mut command = std::process::Command::new(&resolved_acp_command);
if let Some(home) = super::default_agent_workdir() {
@@ -490,9 +497,12 @@ pub fn start_managed_agent_process(
command.stdin(std::process::Stdio::null());
command.stdout(std::process::Stdio::from(stdout));
command.stderr(std::process::Stdio::from(stderr));
if let Some(ref path) = augmented_path {
command.env("PATH", path);
}
command.env("SPROUT_PRIVATE_KEY", &record.private_key_nsec);
command.env("SPROUT_RELAY_URL", &record.relay_url);
command.env("SPROUT_ACP_AGENT_COMMAND", &record.agent_command);
command.env("SPROUT_ACP_AGENT_COMMAND", &resolved_agent_command);
command.env("SPROUT_ACP_AGENT_ARGS", agent_args.join(","));
command.env("SPROUT_ACP_MCP_COMMAND", &resolved_mcp_command);
// Desktop-managed agents should favor the latest owner mention in a