use anyhow::{Result, ensure};
use crate::agent::env_identity::help_defines_flag;
use crate::agent::CliCommandRunner;
fn configure_permissions(
cmd: &mut std::process::Command,
read_only: bool,
roots: &[std::path::PathBuf],
) -> Result<()> {
let mut filesystem = toml::Table::new();
for path in roots {
let path = std::path::absolute(path)?;
let path = path.to_str().ok_or_else(|| anyhow::anyhow!("Non-UTF-8 sandbox root"))?;
filesystem.insert(path.to_string(), toml::Value::String("write".into()));
}
let (name, base) = if read_only { ("aid-read-only", ":read-only") }
else { ("aid-write", ":workspace") };
let profile = toml::Value::Table(toml::Table::from_iter([
("extends".into(), toml::Value::String(base.into())),
("filesystem".into(), toml::Value::Table(filesystem)),
]));
let permissions = toml::Value::Table(toml::Table::from_iter([(name.into(), profile)]));
cmd.arg("--ignore-user-config");
cmd.args(["-c", &format!("permissions={permissions}")]);
cmd.args(["-c", &format!("default_permissions=\"{name}\"")]);
if read_only {
cmd.args(["-c", "approval_policy=\"never\""]);
} else {
cmd.args(["-c", "approval_policy=\"on-request\"", "-c", "approvals_reviewer=\"auto_review\""]);
}
Ok(())
}
pub(super) fn configure_options(
cmd: &mut std::process::Command,
opts: &crate::agent::RunOpts,
resuming: bool,
roots: &[std::path::PathBuf],
) -> Result<()> {
if let Some(ref model) = opts.model {
if super::has_native_model_flag() {
cmd.args(["-m", model]);
} else {
cmd.args(["-c", &format!("model=\"{model}\"")]);
}
}
if let Some(ref output) = opts.output {
cmd.args(["-o", output]);
}
if let Some(ref dir) = opts.dir {
if !std::path::Path::new(dir).exists() {
anyhow::bail!("codex working directory does not exist: {}", dir);
}
if !resuming {
cmd.args(["-C", dir]);
}
cmd.current_dir(dir);
}
if opts.read_only {
let roots = crate::agent::scratch::validate_read_only_roots(opts.dir.as_deref(), roots)?;
configure_permissions(cmd, true, &roots)
} else {
configure_permissions(cmd, false, roots)
}
}
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 validate_exec_help(version: (u32, u32, u32), help: &str) -> Result<()> {
let required = "--ignore-user-config";
ensure!(
help_defines_flag(help, required),
"Codex CLI {}.{}.{} does not define required flag {required}",
version.0,
version.1,
version.2,
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::{validate_exec_help, validate_installed_codex_with};
use crate::agent::CliCommandOutput;
use std::cell::RefCell;
#[test]
fn selects_flags_for_old_and_new_versions() {
for version in [(0, 146, 0), (0, 162, 1), (1, 0, 0)] {
assert!(validate_exec_help(version, " --ignore-user-config\n").is_ok());
}
}
#[test]
fn validates_the_flag_selected_for_each_version() {
assert!(validate_exec_help((0, 146, 0), " --ignore-user-config\n").is_ok());
assert!(validate_exec_help((0, 147, 0), " --ignore-user-config\n").is_ok());
let error = validate_exec_help((0, 147, 0), " --full-auto\n").unwrap_err();
assert!(error.to_string().contains("--ignore-user-config"));
}
#[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: "--ignore-user-config\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 timed_out_help_probe_is_a_soft_skip_for_a_known_version() {
let runner = |_program: &str, _args: &[&str]| {
crate::agent::env_identity::run_bounded("sh", &["-c", "exec sleep 60"], std::time::Duration::from_millis(50))
};
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("--ignore-user-config"));
}
}