fix(ai): avoid Codex Windows batch shims

This commit is contained in:
lucaronin
2026-05-05 11:45:56 +02:00
parent b8e3d8193c
commit 6ce04eead6

View File

@@ -178,7 +178,7 @@ where
let last_message_path = last_message_dir.path().join("last-message.txt");
let args = build_codex_args(&request, Some(&last_message_path))?;
let prompt = build_codex_prompt(&request);
let command = build_codex_command(binary, args, prompt, &request.vault_path);
let command = build_codex_command(binary, args, prompt, &request.vault_path)?;
let emit = with_codex_last_message_fallback(emit, last_message_path);
crate::cli_agent_runtime::run_ai_agent_json_stream(
@@ -223,9 +223,13 @@ fn build_codex_command(
args: Vec<String>,
prompt: String,
vault_path: &str,
) -> std::process::Command {
let mut command = crate::hidden_command(binary);
) -> Result<std::process::Command, String> {
let target = codex_command_target(binary)?;
let mut command = crate::hidden_command(&target.program);
crate::cli_agent_runtime::configure_agent_command_environment(&mut command, binary);
if let Some(first_arg) = target.first_arg {
command.arg(first_arg);
}
command
.args(args)
.arg(prompt)
@@ -233,7 +237,65 @@ fn build_codex_command(
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
command
Ok(command)
}
struct CodexCommandTarget {
program: PathBuf,
first_arg: Option<PathBuf>,
}
fn codex_command_target(binary: &Path) -> Result<CodexCommandTarget, String> {
if is_windows_batch_shim(binary) {
if let Some(script) = node_script_from_windows_cmd_shim(binary) {
return Ok(CodexCommandTarget {
program: crate::mcp::find_node()?,
first_arg: Some(script),
});
}
}
Ok(CodexCommandTarget {
program: binary.to_path_buf(),
first_arg: None,
})
}
fn is_windows_batch_shim(binary: &Path) -> bool {
binary
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| {
extension.eq_ignore_ascii_case("cmd") || extension.eq_ignore_ascii_case("bat")
})
}
fn node_script_from_windows_cmd_shim(binary: &Path) -> Option<PathBuf> {
let contents = std::fs::read_to_string(binary).ok()?;
contents
.split('"')
.skip(1)
.step_by(2)
.filter(|token| is_node_script_token(token))
.find_map(|token| resolve_cmd_shim_script_path(binary, token))
}
fn is_node_script_token(token: &str) -> bool {
let lower = token.to_ascii_lowercase();
(lower.ends_with(".js") || lower.ends_with(".mjs") || lower.ends_with(".cjs"))
&& (lower.starts_with("%dp0%") || lower.starts_with("%~dp0"))
}
fn resolve_cmd_shim_script_path(binary: &Path, token: &str) -> Option<PathBuf> {
let relative = token
.strip_prefix("%dp0%")
.or_else(|| token.strip_prefix("%~dp0"))?
.trim_start_matches(['\\', '/']);
let mut script = binary.parent()?.to_path_buf();
for part in relative.split(['\\', '/']).filter(|part| !part.is_empty()) {
script.push(part);
}
script.is_file().then_some(script)
}
fn build_codex_args(
@@ -621,7 +683,7 @@ mod tests {
fn build_codex_command_keeps_agent_process_contract() {
let binary = PathBuf::from("codex");
let args = vec!["exec".to_string(), "--json".to_string()];
let command = build_codex_command(&binary, args, "Summarize".into(), "/tmp/vault");
let command = build_codex_command(&binary, args, "Summarize".into(), "/tmp/vault").unwrap();
let actual_args: Vec<&OsStr> = command.get_args().collect();
assert_eq!(command.get_program(), OsStr::new("codex"));
@@ -644,7 +706,8 @@ mod tests {
vec!["exec".to_string(), "--json".to_string()],
"Summarize".into(),
"/tmp/vault",
);
)
.unwrap();
let path_value = command
.get_envs()
.find(|(key, _)| *key == OsStr::new("PATH"))
@@ -658,6 +721,51 @@ mod tests {
);
}
#[test]
fn build_codex_command_avoids_windows_cmd_shim_for_complex_args() {
let dir = tempfile::tempdir().unwrap();
let shim = dir.path().join("codex.cmd");
let script = dir
.path()
.join("node_modules")
.join("@openai")
.join("codex")
.join("bin")
.join("codex.js");
std::fs::create_dir_all(script.parent().unwrap()).unwrap();
std::fs::write(&script, "console.log('codex')\n").unwrap();
std::fs::write(
&shim,
r#"@ECHO off
"%_prog%" "%dp0%\node_modules\@openai\codex\bin\codex.js" %*
"#,
)
.unwrap();
let command = build_codex_command(
&shim,
vec![
"exec".to_string(),
"-c".to_string(),
r#"mcp_servers.tolaria.command="C:\\Program Files\\node.exe""#.to_string(),
],
"Summarize".into(),
"/tmp/vault",
)
.unwrap();
assert_ne!(
command.get_program(),
shim.as_os_str(),
"Codex npm .cmd shims cannot safely receive quoted -c args directly"
);
let actual_args = command.get_args().collect::<Vec<_>>();
assert_eq!(actual_args.first().copied(), Some(script.as_os_str()));
assert!(actual_args
.iter()
.any(|arg| *arg == OsStr::new("Summarize")));
}
#[cfg(unix)]
#[test]
fn run_codex_agent_stream_reads_ndjson_and_returns_thread_id() {