diff --git a/desktop/src-tauri/src/managed_agents/readiness/cli_login.rs b/desktop/src-tauri/src/managed_agents/readiness/cli_login.rs index 4036d9f239..6d53025f4b 100644 --- a/desktop/src-tauri/src/managed_agents/readiness/cli_login.rs +++ b/desktop/src-tauri/src/managed_agents/readiness/cli_login.rs @@ -1,10 +1,7 @@ use std::path::Path; use crate::managed_agents::{ - discovery::{ - classify_runtime, codex_adapter_availability, find_command, resolve_command, - KnownAcpRuntime, - }, + discovery::{classify_runtime, codex_adapter_availability, find_command, KnownAcpRuntime}, AcpAvailabilityStatus, }; @@ -39,14 +36,16 @@ pub(super) fn requirements( match availability { AcpAvailabilityStatus::Available => { - let Some(binary_path) = resolve_command(probe_args[0]) else { + let augmented_path = cli_probe::augmented_path(); + let Some(binary_path) = + cli_probe::resolve_probe_command(probe_args[0], augmented_path.as_deref()) + else { return vec![missing_requirement( probe_args, setup_copy, AcpAvailabilityStatus::Available, )]; }; - let augmented_path = cli_probe::augmented_path(); match cli_probe::login_probe(&binary_path, probe_args, augmented_path.as_deref()) { cli_probe::ProbeOutcome::LoggedIn => vec![], cli_probe::ProbeOutcome::LoggedOut => vec![missing_requirement( diff --git a/desktop/src-tauri/src/managed_agents/readiness/cli_probe.rs b/desktop/src-tauri/src/managed_agents/readiness/cli_probe.rs index 513da4e2a8..8fb1344915 100644 --- a/desktop/src-tauri/src/managed_agents/readiness/cli_probe.rs +++ b/desktop/src-tauri/src/managed_agents/readiness/cli_probe.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use crate::managed_agents::runtime::build_augmented_path; @@ -21,6 +21,42 @@ pub(crate) fn augmented_path() -> Option { ) } +/// Resolve a login-probe command with the same PATH precedence used by the +/// managed-agent child process. +/// +/// The desktop process may have a stale shim earlier on its ambient PATH than +/// the actual CLI selected by [`augmented_path`]. Probing that shim can report +/// a false logged-out state even though the spawned harness would use a healthy +/// authenticated CLI. Prefer the child PATH on Unix, then fall back to the +/// normal command resolver for explicit paths and platform-specific shims. +pub(crate) fn resolve_probe_command( + command: &str, + augmented_path: Option<&str>, +) -> Option { + #[cfg(not(windows))] + if !command.contains(std::path::MAIN_SEPARATOR) { + if let Some(path) = augmented_path { + for directory in std::env::split_paths(path) { + let candidate = directory.join(command); + if is_executable_file(&candidate) { + return Some(candidate); + } + } + } + } + + crate::managed_agents::resolve_command(command) +} + +#[cfg(not(windows))] +fn is_executable_file(path: &Path) -> bool { + use std::os::unix::fs::PermissionsExt; + + path.metadata() + .map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0) + .unwrap_or(false) +} + /// Outcome of a CLI login-status probe. #[derive(Debug, PartialEq, Eq)] pub(crate) enum ProbeOutcome { @@ -164,6 +200,31 @@ mod tests { ); } + #[cfg(unix)] + #[test] + fn resolve_probe_command_prefers_augmented_child_path() { + use std::fs; + use std::os::unix::fs::PermissionsExt; + + let temp = tempfile::tempdir().expect("temp dir"); + let preferred_dir = temp.path().join("preferred-bin"); + fs::create_dir_all(&preferred_dir).expect("preferred dir"); + let preferred = preferred_dir.join("fake-codex"); + fs::write(&preferred, "#!/bin/sh\nexit 0\n").expect("write preferred command"); + fs::set_permissions(&preferred, fs::Permissions::from_mode(0o755)) + .expect("chmod preferred command"); + let augmented = std::env::join_paths([preferred_dir.as_path()]) + .expect("join augmented PATH") + .to_string_lossy() + .into_owned(); + + assert_eq!( + super::resolve_probe_command("fake-codex", Some(&augmented)), + Some(preferred), + "readiness must resolve the same command selected by the child PATH" + ); + } + #[cfg(unix)] #[test] fn login_probe_config_invalid_on_stderr_signal() { diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md index b578326eba..eaf70d85d2 100644 --- a/desktop/src/features/agents/AGENTS.md +++ b/desktop/src/features/agents/AGENTS.md @@ -171,6 +171,11 @@ with a TypeScript lookup table or an id comparison in a component. `getAgentAccessOwnerOnly()` is true, every managed agent's access control is locked to owner-only, including provider-backed agents. A provider backend does not prove remote execution and must never create a policy carve-out. +12. **CLI readiness uses the child PATH.** Login probes must resolve the CLI + from the same augmented PATH used by the managed-agent child. The desktop's + ambient PATH may contain an older wrapper that resolves a different package + manager prefix and falsely reports that an otherwise healthy CLI is logged + out. ## The tests that enforce this