Skip to main content

devflow_core/agents/
codex.rs

1//! OpenAI Codex agent adapter.
2//!
3//! Launches `codex exec "<prompt>"` in non-interactive mode with JSON output.
4
5use super::AgentAdapter;
6use crate::phase_id::PhaseId;
7use std::path::PathBuf;
8
9pub struct CodexAgent;
10
11impl AgentAdapter for CodexAgent {
12    fn name(&self) -> &'static str {
13        "OpenAI Codex"
14    }
15
16    fn exec_command(
17        &self,
18        _phase: PhaseId,
19        prompt: &str,
20        extra_writable_roots: &[PathBuf],
21    ) -> (&'static str, Vec<String>) {
22        let mut args: Vec<String> = vec![
23            "exec".into(),
24            "--sandbox".into(),
25            "workspace-write".into(),
26            "--json".into(),
27        ];
28        // Linked-worktree commits write git metadata outside the
29        // workspace-write sandbox (13-06 dogfood finding: Code stage
30        // implemented and tested, then could not commit). Grant every extra
31        // root in one TOML list value; escape backslashes and quotes in
32        // paths.
33        if !extra_writable_roots.is_empty() {
34            let list = extra_writable_roots
35                .iter()
36                .map(|root| {
37                    let escaped = root
38                        .display()
39                        .to_string()
40                        .replace('\\', "\\\\")
41                        .replace('"', "\\\"");
42                    format!("\"{escaped}\"")
43                })
44                .collect::<Vec<_>>()
45                .join(",");
46            args.push("-c".into());
47            args.push(format!("sandbox_workspace_write.writable_roots=[{list}]"));
48        }
49        args.push(prompt.to_string());
50        ("codex", args)
51    }
52
53    /// The sandbox has no route to the operator's signing agent, so signed
54    /// commits/tags fail headless (`ssh-keygen -Y sign` → passphrase error).
55    /// Disable signing via env, scoped to this agent's process tree only.
56    fn extra_env(&self) -> Vec<(String, String)> {
57        vec![
58            ("GIT_CONFIG_COUNT".into(), "2".into()),
59            ("GIT_CONFIG_KEY_0".into(), "commit.gpgsign".into()),
60            ("GIT_CONFIG_VALUE_0".into(), "false".into()),
61            ("GIT_CONFIG_KEY_1".into(), "tag.gpgsign".into()),
62            ("GIT_CONFIG_VALUE_1".into(), "false".into()),
63        ]
64    }
65
66    fn completion_signal_detected(&self, _output: &str) -> bool {
67        false
68    }
69}