Skip to main content

devflow_core/agents/
codex.rs

1//! OpenAI Codex agent driver + legacy adapter.
2//!
3//! Launches `codex -a never exec "<prompt>"` in non-interactive mode with JSON
4//! output. `-a never` is the GLOBAL approval flag and must precede `exec` —
5//! verified against the installed CLI (a `codex exec -a never` placement is
6//! rejected as an unknown argument).
7
8use super::{AgentAdapter, AgentDriver, InteractivityMode};
9use crate::phase_id::PhaseId;
10use std::path::PathBuf;
11
12/// The modular driver for Codex (37-03): owns the launch argv, the JSONL
13/// completion parsing, the signing-disable environment, and the Codex-native
14/// workflow-reference prompt.
15pub struct CodexDriver;
16
17impl AgentDriver for CodexDriver {
18    fn name(&self) -> &'static str {
19        "OpenAI Codex"
20    }
21
22    fn render_prompt(&self, intent: &crate::prompt::StageIntent) -> String {
23        crate::prompt::render_workflow_style(intent, &self.workflow_root())
24    }
25
26    fn build_command(
27        &self,
28        _phase: PhaseId,
29        prompt: &str,
30        extra_writable_roots: &[PathBuf],
31    ) -> (&'static str, Vec<String>) {
32        let mut args: Vec<String> = vec![
33            // `-a never` is the GLOBAL non-interactive approval flag (must
34            // precede `exec`); spawn-tested against the installed CLI.
35            "-a".into(),
36            "never".into(),
37            "exec".into(),
38            "--sandbox".into(),
39            "workspace-write".into(),
40            "--json".into(),
41        ];
42        // Linked-worktree commits write git metadata outside the
43        // workspace-write sandbox (13-06 dogfood finding: Code stage
44        // implemented and tested, then could not commit). Grant every extra
45        // root in one TOML list value; escape backslashes and quotes in paths.
46        if !extra_writable_roots.is_empty() {
47            let list = extra_writable_roots
48                .iter()
49                .map(|root| {
50                    let escaped = root
51                        .display()
52                        .to_string()
53                        .replace('\\', "\\\\")
54                        .replace('"', "\\\"");
55                    format!("\"{escaped}\"")
56                })
57                .collect::<Vec<_>>()
58                .join(",");
59            args.push("-c".into());
60            args.push(format!("sandbox_workspace_write.writable_roots=[{list}]"));
61        }
62        args.push(prompt.to_string());
63        ("codex", args)
64    }
65
66    /// Relocate the Codex JSONL completion parsing under driver ownership: the
67    /// function body lives in `agent_result.rs` (where the result-evaluation
68    /// path and its fixtures live), and this method is the driver's contract
69    /// entry point for it.
70    fn parse_completion(&self, output: &str) -> Option<crate::agent_result::AgentResult> {
71        crate::agent_result::parse_codex_event_result(output)
72    }
73
74    fn environment(&self) -> Vec<(String, String)> {
75        // The sandbox has no route to the operator's signing agent, so signed
76        // commits/tags fail headless (`ssh-keygen -Y sign` → passphrase error).
77        // Disable signing via env, scoped to this agent's process tree only.
78        vec![
79            ("GIT_CONFIG_COUNT".into(), "2".into()),
80            ("GIT_CONFIG_KEY_0".into(), "commit.gpgsign".into()),
81            ("GIT_CONFIG_VALUE_0".into(), "false".into()),
82            ("GIT_CONFIG_KEY_1".into(), "tag.gpgsign".into()),
83            ("GIT_CONFIG_VALUE_1".into(), "false".into()),
84        ]
85    }
86
87    fn interactivity_mode(&self, stage: crate::stage::Stage) -> InteractivityMode {
88        use crate::stage::Stage;
89        match stage {
90            // Codex cannot run the interactive discuss-phase interview or the
91            // interactive plan-phase decision headless — its Define/Plan stages
92            // need the artifact to pre-exist (13-06 dogfood finding).
93            Stage::Define | Stage::Plan => InteractivityMode::RequiresExistingArtifact,
94            _ => InteractivityMode::HeadlessSafe,
95        }
96    }
97}
98
99/// Legacy `AgentAdapter` face for Codex (D-11 removal point). Delegates to
100/// [`CodexDriver`].
101pub struct CodexAgent;
102
103impl AgentAdapter for CodexAgent {
104    fn name(&self) -> &'static str {
105        CodexDriver.name()
106    }
107
108    fn exec_command(
109        &self,
110        phase: PhaseId,
111        prompt: &str,
112        extra_writable_roots: &[PathBuf],
113    ) -> (&'static str, Vec<String>) {
114        CodexDriver.build_command(phase, prompt, extra_writable_roots)
115    }
116
117    fn extra_env(&self) -> Vec<(String, String)> {
118        CodexDriver.environment()
119    }
120
121    fn completion_signal_detected(&self, _output: &str) -> bool {
122        false
123    }
124
125    fn render_prompt(&self, intent: &crate::prompt::StageIntent) -> String {
126        CodexDriver.render_prompt(intent)
127    }
128}