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}