use super::{AgentDriver, InteractivityMode};
use crate::phase_id::PhaseId;
use std::path::PathBuf;
pub struct CodexDriver;
impl AgentDriver for CodexDriver {
fn name(&self) -> &'static str {
"OpenAI Codex"
}
fn render_prompt(&self, intent: &crate::prompt::StageIntent) -> String {
crate::prompt::render_workflow_style(intent, &self.workflow_root())
}
fn build_command(
&self,
_phase: PhaseId,
prompt: &str,
extra_writable_roots: &[PathBuf],
) -> (&'static str, Vec<String>) {
let mut args: Vec<String> = vec![
"-a".into(),
"never".into(),
"exec".into(),
"--sandbox".into(),
"workspace-write".into(),
"--json".into(),
];
if !extra_writable_roots.is_empty() {
let list = extra_writable_roots
.iter()
.map(|root| {
let path = root
.to_str()
.expect("non-UTF-8 writable root path — refusing Codex launch");
format!("\"{}\"", escape_toml_basic_string(path))
})
.collect::<Vec<_>>()
.join(",");
args.push("-c".into());
args.push(format!("sandbox_workspace_write.writable_roots=[{list}]"));
}
args.push(prompt.to_string());
("codex", args)
}
fn parse_completion(&self, output: &str) -> Option<crate::agent_result::AgentResult> {
crate::agent_result::parse_codex_event_result(output)
}
fn environment(&self) -> Vec<(String, String)> {
vec![
("GIT_CONFIG_COUNT".into(), "2".into()),
("GIT_CONFIG_KEY_0".into(), "commit.gpgsign".into()),
("GIT_CONFIG_VALUE_0".into(), "false".into()),
("GIT_CONFIG_KEY_1".into(), "tag.gpgsign".into()),
("GIT_CONFIG_VALUE_1".into(), "false".into()),
]
}
fn interactivity_mode(&self, stage: crate::stage::Stage) -> InteractivityMode {
use crate::stage::Stage;
match stage {
Stage::Define | Stage::Plan => InteractivityMode::RequiresExistingArtifact,
_ => InteractivityMode::HeadlessSafe,
}
}
}
fn escape_toml_basic_string(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
c if (c as u32) < 0x20 || c == '\u{7F}' => {
out.push_str(&format!("\\u{:04X}", c as u32))
}
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::phase_id::PhaseId;
fn writable_roots_flag(args: &[String]) -> &str {
let idx = args
.iter()
.position(|a| a == "-c")
.expect("-c flag present");
&args[idx + 1]
}
#[test]
fn codex_writable_roots_escape_hostile_paths() {
let roots = vec![PathBuf::from("/repo/a\"b\\c\nd")];
let (_, args) = CodexDriver.build_command(PhaseId::new(7), "prompt", &roots);
let flag = writable_roots_flag(&args);
assert!(
!flag.contains('\n'),
"raw newline must be escaped: {flag:?}"
);
assert!(flag.contains(r#"\n"#), "newline must be `\\n`: {flag:?}");
assert!(flag.contains(r#"\""#), "quote must be escaped: {flag:?}");
assert!(
flag.contains(r#"\\"#),
"backslash must be escaped: {flag:?}"
);
}
#[test]
fn codex_writable_roots_escape_del() {
let roots = vec![PathBuf::from("/repo/a\u{7F}b")];
let (_, args) = CodexDriver.build_command(PhaseId::new(7), "prompt", &roots);
let flag = writable_roots_flag(&args);
assert!(
!flag.contains('\u{7F}'),
"raw DEL must be escaped: {flag:?}"
);
assert!(
flag.contains(r#"\u007F"#),
"DEL must be `\\u007F`: {flag:?}"
);
}
#[cfg(unix)]
#[test]
#[should_panic(expected = "non-UTF-8 writable root")]
fn codex_writable_roots_refuses_non_utf8_paths() {
use std::os::unix::ffi::OsStringExt;
let raw = std::ffi::OsString::from_vec(vec![b'/', b'r', b'e', b'p', b'o', 0xFF, b'x']);
let roots = vec![PathBuf::from(raw)];
let _ = CodexDriver.build_command(PhaseId::new(7), "prompt", &roots);
}
}