use anyhow::{Context, Result, ensure};
use std::process::Command;
use crate::agent::{CliCommandOutput, CliCommandRunner};
const APPROVE_FOR_ME_VERSION: (u32, u32, u32) = (0, 147, 0);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ApprovalFlag {
ApproveForMe,
FullAuto,
}
impl ApprovalFlag {
pub(super) const fn as_str(self) -> &'static str {
match self {
Self::ApproveForMe => "--approve-for-me",
Self::FullAuto => "--full-auto",
}
}
}
pub(super) fn approval_flag_for_version(version: (u32, u32, u32)) -> ApprovalFlag {
if version >= APPROVE_FOR_ME_VERSION {
ApprovalFlag::ApproveForMe
} else {
ApprovalFlag::FullAuto
}
}
pub(super) fn validate_installed_codex(version: Option<(u32, u32, u32)>) -> Result<()> {
validate_installed_codex_with(version, &run_codex_command)
}
pub(super) fn validate_installed_codex_with(
version: Option<(u32, u32, u32)>,
run: &CliCommandRunner<'_>,
) -> Result<()> {
let Some(version) = version else {
eprintln!("warning: could not read Codex CLI version; skipping flag validation");
return Ok(());
};
let output = match run("codex", &["exec", "--help"]) {
Ok(output) if output.success => output,
Ok(_) => {
eprintln!("warning: `codex exec --help` failed; skipping flag validation");
return Ok(());
}
Err(error) => {
eprintln!("warning: could not inspect Codex CLI flags ({error:#}); skipping validation");
return Ok(());
}
};
validate_exec_help(version, &format!("{}{}", output.stdout, output.stderr))
}
fn run_codex_command(program: &str, args: &[&str]) -> Result<CliCommandOutput> {
let output = Command::new(program)
.args(args)
.output()
.context("failed to inspect Codex CLI capabilities")?;
Ok(CliCommandOutput {
success: output.status.success(),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
})
}
fn validate_exec_help(version: (u32, u32, u32), help: &str) -> Result<()> {
let required = approval_flag_for_version(version).as_str();
ensure!(
help_defines_flag(help, required),
"Codex CLI {}.{}.{} does not define required flag {required}",
version.0,
version.1,
version.2,
);
Ok(())
}
fn help_defines_flag(help: &str, flag: &str) -> bool {
help.lines().any(|line| {
line.trim_start().strip_prefix(flag).is_some_and(|rest| {
rest.is_empty() || rest.starts_with([' ', '\t', '=', ','])
})
})
}
#[cfg(test)]
mod tests {
use super::{ApprovalFlag, approval_flag_for_version, validate_exec_help, validate_installed_codex_with};
use crate::agent::CliCommandOutput;
use std::cell::RefCell;
#[test]
fn selects_flags_for_old_and_new_versions() {
assert_eq!(approval_flag_for_version((0, 146, 0)), ApprovalFlag::FullAuto);
assert_eq!(approval_flag_for_version((0, 147, 0)), ApprovalFlag::ApproveForMe);
assert_eq!(approval_flag_for_version((1, 0, 0)), ApprovalFlag::ApproveForMe);
}
#[test]
fn validates_the_flag_selected_for_each_version() {
assert!(validate_exec_help((0, 146, 0), " --full-auto\n").is_ok());
assert!(validate_exec_help((0, 147, 0), " --approve-for-me\n").is_ok());
let error = validate_exec_help((0, 147, 0), " --full-auto\n").unwrap_err();
assert!(error.to_string().contains("--approve-for-me"));
}
#[test]
fn validates_codex_help_through_the_injected_runner() {
let invocation = RefCell::new(None);
let runner = |program: &str, args: &[&str]| {
*invocation.borrow_mut() = Some((
program.to_string(),
args.iter().map(|arg| (*arg).to_string()).collect::<Vec<_>>(),
));
Ok(CliCommandOutput {
success: true,
stdout: "--approve-for-me\n".to_string(),
stderr: String::new(),
})
};
validate_installed_codex_with(Some((0, 147, 0)), &runner).unwrap();
assert_eq!(
*invocation.borrow(),
Some((
"codex".to_string(),
vec!["exec".to_string(), "--help".to_string()]
))
);
}
#[test]
fn unknown_version_skips_flag_validation() {
let runner = |_program: &str, _args: &[&str]| -> anyhow::Result<CliCommandOutput> {
panic!("help must not be required when the version is unknown")
};
validate_installed_codex_with(None, &runner).unwrap();
}
#[test]
fn failed_help_probe_does_not_block_a_known_version() {
let runner = |_program: &str, _args: &[&str]| {
Ok(CliCommandOutput {
success: false,
stdout: String::new(),
stderr: "unsupported".to_string(),
})
};
validate_installed_codex_with(Some((0, 147, 0)), &runner).unwrap();
}
#[test]
fn known_version_still_rejects_a_missing_required_flag() {
let runner = |_program: &str, _args: &[&str]| {
Ok(CliCommandOutput {
success: true,
stdout: "--full-auto\n".to_string(),
stderr: String::new(),
})
};
let error = validate_installed_codex_with(Some((0, 147, 0)), &runner).unwrap_err();
assert!(error.to_string().contains("--approve-for-me"));
}
}