From ac9b17d5cb7d85b30f4624eee42ae7c3ed7d6c10 Mon Sep 17 00:00:00 2001 From: Ian Oberst Date: Tue, 7 Jul 2026 15:28:40 -0700 Subject: [PATCH] fix(desktop): use augmented PATH for readiness probes Co-authored-by: Ian Oberst Signed-off-by: Ian Oberst Co-authored-by: Codex Ai-assisted: true --- .../src-tauri/src/managed_agents/readiness.rs | 13 ++- .../src/managed_agents/readiness/cli_probe.rs | 99 +++++++++++++++++++ .../src-tauri/src/managed_agents/runtime.rs | 2 +- .../src/managed_agents/runtime/path.rs | 2 +- 4 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 desktop/src-tauri/src/managed_agents/readiness/cli_probe.rs diff --git a/desktop/src-tauri/src/managed_agents/readiness.rs b/desktop/src-tauri/src/managed_agents/readiness.rs index aff4bf17ae..b560b151a4 100644 --- a/desktop/src-tauri/src/managed_agents/readiness.rs +++ b/desktop/src-tauri/src/managed_agents/readiness.rs @@ -52,6 +52,8 @@ use crate::managed_agents::{ types::{AcpAvailabilityStatus, ManagedAgentRecord, PersonaRecord}, }; +mod cli_probe; + // ── EffectiveAgentEnv ───────────────────────────────────────────────────────── /// The resolved environment that a spawn of `record` would actually receive. @@ -469,11 +471,12 @@ fn cli_login_requirements( }]; }; - let logged_in = std::process::Command::new(&binary_path) - .args(&probe_args[1..]) - .output() - .map(|o| o.status.success()) - .unwrap_or(false); + let augmented_path = cli_probe::augmented_path(); + let logged_in = cli_probe::login_probe_succeeds( + &binary_path, + probe_args, + augmented_path.as_deref(), + ); if logged_in { vec![] diff --git a/desktop/src-tauri/src/managed_agents/readiness/cli_probe.rs b/desktop/src-tauri/src/managed_agents/readiness/cli_probe.rs new file mode 100644 index 0000000000..16b3389406 --- /dev/null +++ b/desktop/src-tauri/src/managed_agents/readiness/cli_probe.rs @@ -0,0 +1,99 @@ +use std::path::Path; + +use crate::managed_agents::runtime::build_augmented_path; + +pub(super) fn augmented_path() -> Option { + build_augmented_path( + dirs::home_dir(), + std::env::current_exe() + .ok() + .and_then(|exe| exe.parent().map(std::path::Path::to_path_buf)), + crate::managed_agents::login_shell_path(), + ) +} + +pub(super) fn login_probe_succeeds( + binary_path: &Path, + probe_args: &[&str], + augmented_path: Option<&str>, +) -> bool { + // Run the probe at the resolved absolute path so the GUI-PATH gap is + // bypassed. Inject the same augmented PATH used for launched agents so + // script shims with `/usr/bin/env ` shebangs can find runtimes + // such as node/python when the app was launched with a bare GUI PATH. + let mut command = std::process::Command::new(binary_path); + command.args(&probe_args[1..]); + if let Some(path) = augmented_path { + command.env("PATH", path); + } + command + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} + +#[cfg(test)] +mod tests { + #[cfg(unix)] + #[test] + fn login_probe_uses_augmented_path_for_env_shebang_interpreter() { + use std::fs; + use std::os::unix::fs::PermissionsExt; + use std::process::Command; + + let temp = tempfile::tempdir().expect("temp dir"); + let script_dir = temp.path().join("script-bin"); + let interpreter_dir = temp.path().join("interpreter-bin"); + let empty_path_dir = temp.path().join("empty-bin"); + fs::create_dir_all(&script_dir).expect("script dir"); + fs::create_dir_all(&interpreter_dir).expect("interpreter dir"); + fs::create_dir_all(&empty_path_dir).expect("empty path dir"); + + let interpreter_path = interpreter_dir.join("node"); + let marker_path = temp.path().join("fake-node-ran"); + fs::write( + &interpreter_path, + format!( + "#!/bin/sh\nprintf 'fake node ran\\n' > '{}' || exit 1\nexit 0\n", + marker_path.display() + ), + ) + .expect("write interpreter"); + fs::set_permissions(&interpreter_path, fs::Permissions::from_mode(0o755)) + .expect("chmod interpreter"); + + let script_path = script_dir.join("fake-codex"); + fs::write(&script_path, "#!/usr/bin/env node\n").expect("write script"); + fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755)).expect("chmod script"); + + let scrubbed_path = std::env::join_paths([empty_path_dir.as_path()]) + .expect("join scrubbed PATH") + .to_string_lossy() + .into_owned(); + let without_augmented_path = Command::new(&script_path) + .args(["login", "status"]) + .env("PATH", &scrubbed_path) + .output() + .expect("run script with scrubbed PATH"); + assert!( + !without_augmented_path.status.success(), + "with a scrubbed PATH, /usr/bin/env should not find node" + ); + + let augmented_path = + std::env::join_paths([interpreter_dir.as_path()]).expect("join augmented PATH"); + let augmented_path = augmented_path.to_string_lossy().into_owned(); + assert!( + super::login_probe_succeeds( + &script_path, + &["fake-codex", "login", "status"], + Some(&augmented_path), + ), + "the injected augmented PATH should allow /usr/bin/env to find the interpreter" + ); + assert!( + marker_path.exists(), + "the fake node from the injected PATH should have run" + ); + } +} diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index 354e343070..5a62a53e52 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -14,7 +14,7 @@ use crate::{ }; mod path; -use path::build_augmented_path; +pub(in crate::managed_agents) use path::build_augmented_path; mod sweep; pub(crate) use sweep::sweep_untracked_bundle_harnesses; diff --git a/desktop/src-tauri/src/managed_agents/runtime/path.rs b/desktop/src-tauri/src/managed_agents/runtime/path.rs index 0185e61b82..8dea769421 100644 --- a/desktop/src-tauri/src/managed_agents/runtime/path.rs +++ b/desktop/src-tauri/src/managed_agents/runtime/path.rs @@ -15,7 +15,7 @@ use std::path::PathBuf; /// error), collapsing the entire augmented `PATH` to `None` — the bug this /// guards against, which left managed agents unable to find `buzz`. Returns /// `None` only when no entries exist. -pub(super) fn build_augmented_path( +pub(in crate::managed_agents) fn build_augmented_path( home: Option, exe_parent: Option, shell_path: Option,