Skip to main content

monoloop_connector_codex/
config.rs

1//! OpenAI Codex ACP connector configuration.
2//!
3//! Native Codex exposes `app-server` / MCP / exec surfaces, but not ACP directly.
4//! The practical ACP path for Monoloop is the official adapter
5//! `@agentclientprotocol/codex-acp` (stdio NDJSON), which starts Codex App Server
6//! and speaks Agent Client Protocol. Pin via `CODEX_ACP_BIN` or rely on discovery.
7
8use std::path::PathBuf;
9use std::time::Duration;
10
11/// How to launch the ACP server process for Codex.
12#[derive(Clone, Debug)]
13pub struct CodexAgentConfig {
14    /// Command to run (default: discover `codex-acp`, else `npx`).
15    pub command: PathBuf,
16    /// Args after command (default: empty for `codex-acp`, or
17    /// `["--yes","@agentclientprotocol/codex-acp"]` for npx).
18    pub args: Vec<String>,
19    /// Working directory for the process and default session `cwd`.
20    pub cwd: PathBuf,
21    /// Auth method id for ACP `authenticate` when `authenticate` is true.
22    ///
23    /// codex-acp advertises ChatGPT login and API-key methods; default leaves
24    /// auth to existing `codex login` / `OPENAI_API_KEY` / `CODEX_API_KEY`.
25    pub auth_method_id: String,
26    /// When true, send `authenticate` after initialize (may open a login flow).
27    pub authenticate: bool,
28    /// Client name reported in `initialize`.
29    pub client_name: String,
30    /// Client version reported in `initialize`.
31    pub client_version: String,
32    /// Handshake / RPC deadline.
33    pub rpc_deadline: Duration,
34    /// Max bytes per NDJSON line (fail-closed).
35    pub max_line_bytes: usize,
36    /// Bounded output queue for dialect bytes (session/update lines).
37    pub max_output_queue: usize,
38    /// Auto-answer `session/request_permission` with allow-once.
39    pub auto_allow_permissions: bool,
40    /// When true, advertise client fs read/write capabilities.
41    pub advertise_fs: bool,
42    /// Optional path to append raw NDJSON lines (test diagnostics only).
43    pub raw_dump_path: Option<PathBuf>,
44}
45
46impl Default for CodexAgentConfig {
47    fn default() -> Self {
48        let (command, args) = discover_acp_command();
49        Self {
50            command,
51            args,
52            cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
53            // Prefer env/login already on the host; explicit authenticate can hang.
54            auth_method_id: "openai-api-key".into(),
55            authenticate: false,
56            client_name: "monoloop-codex".into(),
57            client_version: env!("CARGO_PKG_VERSION").into(),
58            rpc_deadline: Duration::from_secs(90),
59            max_line_bytes: 8 * 1024 * 1024,
60            max_output_queue: 256,
61            // Fail closed: hosts must opt in to auto-approve tool permissions.
62            auto_allow_permissions: false,
63            advertise_fs: false,
64            raw_dump_path: None,
65        }
66    }
67}
68
69impl CodexAgentConfig {
70    /// Config for a project directory.
71    pub fn for_project(cwd: impl Into<PathBuf>) -> Self {
72        Self {
73            cwd: cwd.into(),
74            ..Default::default()
75        }
76    }
77
78    /// Attach a raw NDJSON dump path.
79    pub fn with_raw_dump(mut self, path: impl Into<PathBuf>) -> Self {
80        self.raw_dump_path = Some(path.into());
81        self
82    }
83
84    /// Point at a specific Codex binary via `CODEX_PATH` for the adapter.
85    ///
86    /// The ACP adapter reads `CODEX_PATH` from the process environment; this
87    /// only records intent for callers (env is set by the host before spawn).
88    pub fn with_codex_path_env_hint(self) -> Self {
89        self
90    }
91
92    /// Prefer a globally installed `codex-acp` binary when present.
93    pub fn with_global_codex_acp(mut self) -> Self {
94        self.command = PathBuf::from("codex-acp");
95        self.args.clear();
96        self
97    }
98
99    /// Force unattended permission auto-allow on the ACP client side.
100    ///
101    /// Only for trusted test sandboxes / unattended qualification.
102    pub fn with_auto_allow_permissions(mut self) -> Self {
103        self.auto_allow_permissions = true;
104        self
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn default_denies_auto_permissions() {
114        assert!(!CodexAgentConfig::default().auto_allow_permissions);
115    }
116
117    #[test]
118    fn opt_in_enables_auto_permissions() {
119        assert!(
120            CodexAgentConfig::default()
121                .with_auto_allow_permissions()
122                .auto_allow_permissions
123        );
124    }
125}
126
127/// Resolve ACP server argv:
128/// `CODEX_ACP_BIN` → `codex-acp` on PATH → `npx --yes @agentclientprotocol/codex-acp`.
129fn discover_acp_command() -> (PathBuf, Vec<String>) {
130    if let Some(bin) = std::env::var_os("CODEX_ACP_BIN") {
131        return (PathBuf::from(bin), Vec::new());
132    }
133    if which("codex-acp").is_some() {
134        return (PathBuf::from("codex-acp"), Vec::new());
135    }
136    (
137        PathBuf::from(std::env::var_os("NPX_BIN").unwrap_or_else(|| "npx".into())),
138        vec!["--yes".into(), "@agentclientprotocol/codex-acp".into()],
139    )
140}
141
142fn which(name: &str) -> Option<PathBuf> {
143    let path = std::env::var_os("PATH")?;
144    for dir in std::env::split_paths(&path) {
145        let candidate = dir.join(name);
146        if candidate.is_file() {
147            return Some(candidate);
148        }
149    }
150    None
151}
152
153/// Session create parameters (`session/new` + optional mode).
154///
155/// codex-acp modes (see adapter docs): `read-only` | `agent` | `agent-full-access`.
156#[derive(Clone, Debug)]
157pub struct CodexSessionConfig {
158    /// Session working directory.
159    pub cwd: PathBuf,
160    /// MCP servers (opaque JSON; usually empty).
161    pub mcp_servers: serde_json::Value,
162    /// Mode after create via `session/set_mode`.
163    pub mode_id: Option<String>,
164}
165
166impl CodexSessionConfig {
167    /// Session in the given cwd.
168    pub fn new(cwd: impl Into<PathBuf>) -> Self {
169        Self {
170            cwd: cwd.into(),
171            mcp_servers: serde_json::json!([]),
172            mode_id: None,
173        }
174    }
175
176    /// Read-only mode (plan / review style).
177    pub fn with_read_only_mode(mut self) -> Self {
178        self.mode_id = Some("read-only".into());
179        self
180    }
181
182    /// Default agent mode (workspace write sandbox typical).
183    pub fn with_agent_mode(mut self) -> Self {
184        self.mode_id = Some("agent".into());
185        self
186    }
187
188    /// Full-access agent mode (elevated sandbox; test sandboxes only).
189    pub fn with_agent_full_access_mode(mut self) -> Self {
190        self.mode_id = Some("agent-full-access".into());
191        self
192    }
193}