fix(agents): enable external copilot command

This commit is contained in:
James Peter
2025-11-13 13:43:42 +10:00
parent 4dbae974d6
commit 26bb270b9c
8 changed files with 163 additions and 10 deletions
+38 -7
View File
@@ -19,6 +19,7 @@ use tokio::time::Duration as TokioDuration;
use std::thread;
use crate::agent_defaults::{agent_model_spec, default_params_for};
use shlex::split as shlex_split;
use crate::config_types::AgentConfig;
use crate::openai_tools::JsonSchema;
use crate::openai_tools::OpenAiTool;
@@ -702,12 +703,19 @@ async fn execute_model_with_permissions(
model.to_lowercase()
};
let (command_base, command_extra_args) = split_command_and_args(&command);
let command_for_spawn = if command_base.is_empty() {
command.clone()
} else {
command_base.clone()
};
// Special case: for the built‑in Codex agent, prefer invoking the currently
// running executable with the `exec` subcommand rather than relying on a
// `codex` binary to be present on PATH. This improves portability,
// especially on Windows where global shims may be missing.
let model_lower = model.to_lowercase();
let command_lower = command.to_ascii_lowercase();
let command_lower = command_for_spawn.to_ascii_lowercase();
fn is_known_family(s: &str) -> bool {
matches!(s, "claude" | "gemini" | "qwen" | "codex" | "code" | "cloud")
}
@@ -728,7 +736,7 @@ async fn execute_model_with_permissions(
if matches!(family, "code" | "codex" | "cloud") {
if config.is_none() {
if !command_exists(&command) {
if !command_exists(&command_for_spawn) {
use_current_exe = true;
}
} else if let Some(ref cfg) = config {
@@ -744,7 +752,7 @@ async fn execute_model_with_permissions(
Err(e) => return Err(format!("Failed to resolve current executable: {}", e)),
}
} else {
Command::new(command.clone())
Command::new(command_for_spawn.clone())
};
// Set working directory if provided
@@ -761,7 +769,7 @@ async fn execute_model_with_permissions(
}
}
let mut final_args: Vec<String> = Vec::new();
let mut final_args: Vec<String> = command_extra_args;
if let Some(ref cfg) = config {
if read_only {
@@ -828,14 +836,17 @@ async fn execute_model_with_permissions(
final_args.extend(spec_model_args.iter().cloned());
final_args.push(prompt.to_string());
}
_ => { return Err(format!("Unknown model: {}", model)); }
_ => {
final_args.extend(spec_model_args.iter().cloned());
final_args.push(prompt.to_string());
}
}
// Proactively check for presence of external command before spawn when not
// using the current executable fallback. This avoids confusing OS errors
// like "program not found" and lets us surface a cleaner message.
if !(family == "codex" || family == "code" || (family == "cloud" && config.is_none()))
&& !command_exists(&command)
&& !command_exists(&command_for_spawn)
{
return Err(format!("Required agent '{}' is not installed or not in PATH", command));
}
@@ -913,7 +924,7 @@ async fn execute_model_with_permissions(
std::env::current_exe().map_err(|e| format!("Failed to resolve current executable: {}", e))?
} else {
// Use program name; PATH resolution will be handled by spawn helper with provided env.
std::path::PathBuf::from(&command)
std::path::PathBuf::from(&command_for_spawn)
};
let args = final_args.clone();
@@ -1014,6 +1025,26 @@ fn strip_model_flags(args: &mut Vec<String>) {
}
}
pub fn split_command_and_args(command: &str) -> (String, Vec<String>) {
let trimmed = command.trim();
if trimmed.is_empty() {
return (String::new(), Vec::new());
}
if let Some(tokens) = shlex_split(trimmed) {
if let Some((first, rest)) = tokens.split_first() {
return (first.clone(), rest.to_vec());
}
}
let tokens: Vec<String> = trimmed.split_whitespace().map(|s| s.to_string()).collect();
if tokens.is_empty() {
(String::new(), Vec::new())
} else {
let head = tokens[0].clone();
(head, tokens[1..].to_vec())
}
}
const AGENT_SMOKE_TEST_PROMPT: &str = "Reply only with the string \"ok\". Do not include any other words.";
const AGENT_SMOKE_TEST_EXPECTED: &str = "ok";
const AGENT_SMOKE_TEST_TIMEOUT: TokioDuration = TokioDuration::from_secs(20);
+9 -1
View File
@@ -63,6 +63,7 @@ use crate::EnvironmentContextEmission;
use crate::AuthManager;
use crate::CodexAuth;
use crate::agent_tool::AgentStatusUpdatePayload;
use crate::split_command_and_args;
use crate::git_worktree;
use crate::protocol::ApprovedCommandMatchKind;
use crate::protocol::WebSearchBeginEvent;
@@ -8309,7 +8310,14 @@ pub(crate) async fn handle_run_agent(
// Helper: derive the command to check for a given model/config pair.
fn resolve_command_for_check(model: &str, cfg: Option<&crate::config_types::AgentConfig>) -> (String, bool) {
if let Some(c) = cfg { return (c.command.clone(), false); }
if let Some(c) = cfg {
let (base, _) = split_command_and_args(&c.command);
let trimmed = base.trim();
if !trimmed.is_empty() {
return (trimmed.to_string(), false);
}
return (c.command.clone(), false);
}
let m = model.to_lowercase();
match m.as_str() {
// Built-in: always available via current_exe fallback.
+1
View File
@@ -116,6 +116,7 @@ pub mod util;
pub use apply_patch::CODEX_APPLY_PATCH_ARG1;
pub use command_safety::is_safe_command;
pub use agent_tool::smoke_test_agent_blocking;
pub use agent_tool::split_command_and_args;
pub use safety::get_platform_sandbox;
pub use housekeeping::run_housekeeping_if_due;
pub use housekeeping::CleanupOutcome;