Skip to main content

codex/builder/
mod.rs

1use std::{path::PathBuf, time::Duration};
2
3#[cfg(test)]
4use std::ffi::OsString;
5
6use crate::home::CommandEnvironment;
7use tokio::process::Command;
8
9mod cli_overrides;
10mod types;
11
12pub use types::{
13    ApprovalPolicy, CliOverrides, CliOverridesPatch, ColorMode, ConfigOverride, FeatureToggles,
14    FlagState, LocalProvider, ModelVerbosity, ReasoningEffort, ReasoningOverrides,
15    ReasoningSummary, ReasoningSummaryFormat, SafetyOverride, SandboxMode,
16};
17
18pub(super) type ResolvedCliOverrides = cli_overrides::ResolvedCliOverrides;
19
20#[cfg(test)]
21pub(super) const DEFAULT_REASONING_CONFIG_GPT5: &[(&str, &str)] =
22    cli_overrides::DEFAULT_REASONING_CONFIG_GPT5;
23#[cfg(test)]
24pub(super) const DEFAULT_REASONING_CONFIG_GPT5_CODEX: &[(&str, &str)] =
25    cli_overrides::DEFAULT_REASONING_CONFIG_GPT5_CODEX;
26#[cfg(test)]
27pub(super) const DEFAULT_REASONING_CONFIG_GPT5_1: &[(&str, &str)] =
28    cli_overrides::DEFAULT_REASONING_CONFIG_GPT5_1;
29
30#[cfg(test)]
31pub(super) fn reasoning_config_for(
32    model: Option<&str>,
33) -> Option<&'static [(&'static str, &'static str)]> {
34    cli_overrides::reasoning_config_for(model)
35}
36
37pub(super) fn resolve_cli_overrides(
38    builder: &CliOverrides,
39    patch: &CliOverridesPatch,
40    model: Option<&str>,
41) -> ResolvedCliOverrides {
42    cli_overrides::resolve_cli_overrides(builder, patch, model)
43}
44
45#[cfg(test)]
46pub(super) fn cli_override_args(
47    resolved: &ResolvedCliOverrides,
48    include_search: bool,
49) -> Vec<OsString> {
50    cli_overrides::cli_override_args(resolved, include_search)
51}
52
53pub(super) fn apply_cli_overrides(
54    command: &mut Command,
55    resolved: &ResolvedCliOverrides,
56    include_search: bool,
57) {
58    cli_overrides::apply_cli_overrides(command, resolved, include_search);
59}
60
61/// Builder for [`crate::CodexClient`].
62///
63/// CLI parity planning and implementation history lives under `.archived/project_management/next/`
64/// (see `.archived/project_management/next/codex-cli-parity/`) and the parity ADRs in `docs/adr/`.
65#[derive(Clone, Debug)]
66pub struct CodexClientBuilder {
67    pub(super) binary: PathBuf,
68    pub(super) codex_home: Option<PathBuf>,
69    pub(super) create_home_dirs: bool,
70    pub(super) model: Option<String>,
71    pub(super) timeout: Duration,
72    pub(super) color_mode: ColorMode,
73    pub(super) working_dir: Option<PathBuf>,
74    pub(super) add_dirs: Vec<PathBuf>,
75    pub(super) images: Vec<PathBuf>,
76    pub(super) json_output: bool,
77    pub(super) output_schema: bool,
78    pub(super) quiet: bool,
79    pub(super) mirror_stdout: bool,
80    pub(super) json_event_log: Option<PathBuf>,
81    pub(super) cli_overrides: CliOverrides,
82    pub(super) capability_overrides: crate::CapabilityOverrides,
83    pub(super) capability_cache_policy: crate::CapabilityCachePolicy,
84}
85
86impl CodexClientBuilder {
87    /// Starts a new builder with default values.
88    pub fn new() -> Self {
89        Self::default()
90    }
91
92    /// Sets the path to the Codex binary.
93    ///
94    /// Defaults to `CODEX_BINARY` when present or `codex` on `PATH`. Use this to pin a packaged
95    /// binary, e.g. the path returned from [`crate::resolve_bundled_binary`] when your app ships Codex
96    /// inside an isolated bundle.
97    pub fn binary(mut self, binary: impl Into<PathBuf>) -> Self {
98        self.binary = binary.into();
99        self
100    }
101
102    /// Sets a custom `CODEX_HOME` path that will be applied per command.
103    /// Directories are created by default; disable via [`Self::create_home_dirs`].
104    pub fn codex_home(mut self, home: impl Into<PathBuf>) -> Self {
105        self.codex_home = Some(home.into());
106        self
107    }
108
109    /// Controls whether the CODEX_HOME directory tree should be created if missing.
110    /// Defaults to `true` when [`Self::codex_home`] is set.
111    pub fn create_home_dirs(mut self, enable: bool) -> Self {
112        self.create_home_dirs = enable;
113        self
114    }
115
116    /// Sets the model that should be used for every `codex exec` call.
117    pub fn model(mut self, model: impl Into<String>) -> Self {
118        let model = model.into();
119        self.model = (!model.trim().is_empty()).then_some(model);
120        self
121    }
122
123    /// Overrides the maximum amount of time to wait for Codex to respond.
124    pub fn timeout(mut self, timeout: Duration) -> Self {
125        self.timeout = timeout;
126        self
127    }
128
129    /// Controls whether Codex may emit ANSI colors (`--color`). Defaults to [`ColorMode::Never`].
130    pub fn color_mode(mut self, color_mode: ColorMode) -> Self {
131        self.color_mode = color_mode;
132        self
133    }
134
135    /// Forces Codex to run with the provided working directory instead of a fresh temp dir.
136    pub fn working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
137        self.working_dir = Some(dir.into());
138        self
139    }
140
141    /// Requests that `codex exec` include one or more `--add-dir` flags when the
142    /// probed binary supports them. Unsupported or unknown capability results
143    /// skip the flag to avoid CLI errors.
144    pub fn add_dir(mut self, path: impl Into<PathBuf>) -> Self {
145        self.add_dirs.push(path.into());
146        self
147    }
148
149    /// Replaces the current add-dir list with the provided collection.
150    pub fn add_dirs<I, P>(mut self, dirs: I) -> Self
151    where
152        I: IntoIterator<Item = P>,
153        P: Into<PathBuf>,
154    {
155        self.add_dirs = dirs.into_iter().map(Into::into).collect();
156        self
157    }
158
159    /// Adds an image to the prompt by passing `--image <path>` to `codex exec`.
160    pub fn image(mut self, path: impl Into<PathBuf>) -> Self {
161        self.images.push(path.into());
162        self
163    }
164
165    /// Replaces the current image list with the provided collection.
166    pub fn images<I, P>(mut self, images: I) -> Self
167    where
168        I: IntoIterator<Item = P>,
169        P: Into<PathBuf>,
170    {
171        self.images = images.into_iter().map(Into::into).collect();
172        self
173    }
174
175    /// Enables Codex's JSONL output mode (`--json`).
176    ///
177    /// Prompts are piped via stdin when enabled. Events include `thread.started`
178    /// (or `thread.resumed` when continuing), `turn.started`/`turn.completed`/`turn.failed`,
179    /// and `item.created`/`item.updated` with `item.type` such as `agent_message` or `reasoning`.
180    /// Pair with `.mirror_stdout(false)` if you plan to parse the stream instead of just mirroring it.
181    pub fn json(mut self, enable: bool) -> Self {
182        self.json_output = enable;
183        self
184    }
185
186    /// Requests the `--output-schema` flag when the probed binary reports
187    /// support. When capability detection is inconclusive, the flag is skipped
188    /// to maintain compatibility with older releases.
189    pub fn output_schema(mut self, enable: bool) -> Self {
190        self.output_schema = enable;
191        self
192    }
193
194    /// Suppresses mirroring Codex stderr to the console.
195    pub fn quiet(mut self, enable: bool) -> Self {
196        self.quiet = enable;
197        self
198    }
199
200    /// Controls whether Codex stdout should be mirrored to the console while
201    /// also being captured. Disable this when you plan to parse JSONL output or
202    /// tee the stream to a log file (see `crates/codex/examples/stream_with_log.rs`).
203    pub fn mirror_stdout(mut self, enable: bool) -> Self {
204        self.mirror_stdout = enable;
205        self
206    }
207
208    /// Tees each JSONL event line from [`crate::CodexClient::stream_exec`] into a log file.
209    /// Logs append to existing files, flush after each line, and create parent directories as
210    /// needed. [`crate::ExecStreamRequest::json_event_log`] overrides this default per request.
211    pub fn json_event_log(mut self, path: impl Into<PathBuf>) -> Self {
212        self.json_event_log = Some(path.into());
213        self
214    }
215
216    /// Adds a `--config key=value` override that will be applied to every Codex invocation.
217    pub fn config_override(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
218        self.cli_overrides
219            .config_overrides
220            .push(ConfigOverride::new(key, value));
221        self
222    }
223
224    /// Adds a preformatted `--config key=value` override without parsing the input.
225    pub fn config_override_raw(mut self, raw: impl Into<String>) -> Self {
226        self.cli_overrides
227            .config_overrides
228            .push(ConfigOverride::from_raw(raw));
229        self
230    }
231
232    /// Replaces the config overrides with the provided collection.
233    pub fn config_overrides<I, K, V>(mut self, overrides: I) -> Self
234    where
235        I: IntoIterator<Item = (K, V)>,
236        K: Into<String>,
237        V: Into<String>,
238    {
239        self.cli_overrides.config_overrides = overrides
240            .into_iter()
241            .map(|(key, value)| ConfigOverride::new(key, value))
242            .collect();
243        self
244    }
245
246    /// Selects a Codex config profile (`--profile`).
247    pub fn profile(mut self, profile: impl Into<String>) -> Self {
248        let profile = profile.into();
249        self.cli_overrides.profile = (!profile.trim().is_empty()).then_some(profile);
250        self
251    }
252
253    /// Sets `model_reasoning_effort` via `--config`.
254    pub fn reasoning_effort(mut self, effort: ReasoningEffort) -> Self {
255        self.cli_overrides.reasoning.effort = Some(effort);
256        self
257    }
258
259    /// Sets `model_reasoning_summary` via `--config`.
260    pub fn reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
261        self.cli_overrides.reasoning.summary = Some(summary);
262        self
263    }
264
265    /// Sets `model_verbosity` via `--config`.
266    pub fn reasoning_verbosity(mut self, verbosity: ModelVerbosity) -> Self {
267        self.cli_overrides.reasoning.verbosity = Some(verbosity);
268        self
269    }
270
271    /// Sets `model_reasoning_summary_format` via `--config`.
272    pub fn reasoning_summary_format(mut self, format: ReasoningSummaryFormat) -> Self {
273        self.cli_overrides.reasoning.summary_format = Some(format);
274        self
275    }
276
277    /// Sets `model_supports_reasoning_summaries` via `--config`.
278    pub fn supports_reasoning_summaries(mut self, enable: bool) -> Self {
279        self.cli_overrides.reasoning.supports_summaries = Some(enable);
280        self
281    }
282
283    /// Controls whether GPT-5* reasoning defaults should be injected automatically.
284    pub fn auto_reasoning_defaults(mut self, enable: bool) -> Self {
285        self.cli_overrides.auto_reasoning_defaults = enable;
286        self
287    }
288
289    /// Sets the approval policy for Codex subprocesses.
290    pub fn approval_policy(mut self, policy: ApprovalPolicy) -> Self {
291        self.cli_overrides.approval_policy = Some(policy);
292        self
293    }
294
295    /// Sets the sandbox mode for Codex subprocesses.
296    pub fn sandbox_mode(mut self, mode: SandboxMode) -> Self {
297        self.cli_overrides.sandbox_mode = Some(mode);
298        self
299    }
300
301    /// Applies the `--full-auto` safety override unless explicit sandbox/approval options are set.
302    pub fn full_auto(mut self, enable: bool) -> Self {
303        self.cli_overrides.safety_override = if enable {
304            SafetyOverride::FullAuto
305        } else {
306            SafetyOverride::Inherit
307        };
308        self
309    }
310
311    /// Applies the `--dangerously-bypass-approvals-and-sandbox` override.
312    pub fn dangerously_bypass_approvals_and_sandbox(mut self, enable: bool) -> Self {
313        self.cli_overrides.safety_override = if enable {
314            SafetyOverride::DangerouslyBypass
315        } else {
316            SafetyOverride::Inherit
317        };
318        self
319    }
320
321    /// Applies `--cd <dir>` to Codex invocations while keeping the process cwd set to `working_dir`.
322    pub fn cd(mut self, dir: impl Into<PathBuf>) -> Self {
323        self.cli_overrides.cd = Some(dir.into());
324        self
325    }
326
327    /// Selects a remote Codex target (`--remote`).
328    pub fn remote(mut self, remote: impl Into<String>) -> Self {
329        let remote = remote.into();
330        self.cli_overrides.remote = (!remote.trim().is_empty()).then_some(remote);
331        self
332    }
333
334    /// Selects the env var that provides the remote auth token (`--remote-auth-token-env`).
335    pub fn remote_auth_token_env(mut self, env_var: impl Into<String>) -> Self {
336        let env_var = env_var.into();
337        self.cli_overrides.remote_auth_token_env = (!env_var.trim().is_empty()).then_some(env_var);
338        self
339    }
340
341    /// Selects a local provider backend (`--local-provider`).
342    pub fn local_provider(mut self, provider: LocalProvider) -> Self {
343        self.cli_overrides.local_provider = Some(provider);
344        self
345    }
346
347    /// Requests the CLI `--oss` flag to favor OSS/local backends when available.
348    pub fn oss(mut self, enable: bool) -> Self {
349        self.cli_overrides.oss = if enable {
350            FlagState::Enable
351        } else {
352            FlagState::Disable
353        };
354        self
355    }
356
357    /// Adds a `--enable <feature>` toggle to Codex invocations.
358    pub fn enable_feature(mut self, name: impl Into<String>) -> Self {
359        self.cli_overrides.feature_toggles.enable.push(name.into());
360        self
361    }
362
363    /// Adds a `--disable <feature>` toggle to Codex invocations.
364    pub fn disable_feature(mut self, name: impl Into<String>) -> Self {
365        self.cli_overrides.feature_toggles.disable.push(name.into());
366        self
367    }
368
369    /// Controls whether `--search` is passed through to Codex.
370    pub fn search(mut self, enable: bool) -> Self {
371        self.cli_overrides.search = if enable {
372            FlagState::Enable
373        } else {
374            FlagState::Disable
375        };
376        self
377    }
378
379    /// Supplies manual capability data to skip probes or adjust feature flags.
380    pub fn capability_overrides(mut self, overrides: crate::CapabilityOverrides) -> Self {
381        self.capability_overrides = overrides;
382        self
383    }
384
385    /// Convenience to apply feature overrides or vendor hints without touching versions.
386    pub fn capability_feature_overrides(
387        mut self,
388        overrides: crate::CapabilityFeatureOverrides,
389    ) -> Self {
390        self.capability_overrides.features = overrides;
391        self
392    }
393
394    /// Convenience to opt into specific feature flags while leaving other probes intact.
395    pub fn capability_feature_hints(mut self, features: crate::CodexFeatureFlags) -> Self {
396        self.capability_overrides.features = crate::CapabilityFeatureOverrides::enabling(features);
397        self
398    }
399
400    /// Supplies a precomputed capability snapshot for pinned or bundled Codex builds.
401    /// Combine with `write_capabilities_snapshot` / `read_capabilities_snapshot`
402    /// to persist probe results between processes.
403    pub fn capability_snapshot(mut self, snapshot: crate::CodexCapabilities) -> Self {
404        self.capability_overrides.snapshot = Some(snapshot);
405        self
406    }
407
408    /// Overrides the probed version data with caller-provided metadata.
409    pub fn capability_version_override(mut self, version: crate::CodexVersionInfo) -> Self {
410        self.capability_overrides.version = Some(version);
411        self
412    }
413
414    /// Controls how capability probes interact with the in-process cache.
415    /// Use [`crate::CapabilityCachePolicy::Refresh`] to enforce a TTL/backoff when
416    /// binaries are hot-swapped without changing fingerprints.
417    pub fn capability_cache_policy(mut self, policy: crate::CapabilityCachePolicy) -> Self {
418        self.capability_cache_policy = policy;
419        self
420    }
421
422    /// Convenience to bypass the capability cache when a fresh snapshot is required.
423    /// Bypass skips cache reads and writes for the probe.
424    pub fn bypass_capability_cache(mut self, bypass: bool) -> Self {
425        self.capability_cache_policy = if bypass {
426            crate::CapabilityCachePolicy::Bypass
427        } else {
428            crate::CapabilityCachePolicy::PreferCache
429        };
430        self
431    }
432
433    /// Builds the [`crate::CodexClient`].
434    pub fn build(self) -> crate::CodexClient {
435        let command_env =
436            CommandEnvironment::new(self.binary, self.codex_home, self.create_home_dirs);
437        crate::CodexClient {
438            command_env,
439            model: self.model,
440            timeout: self.timeout,
441            color_mode: self.color_mode,
442            working_dir: self.working_dir,
443            add_dirs: self.add_dirs,
444            images: self.images,
445            json_output: self.json_output,
446            output_schema: self.output_schema,
447            quiet: self.quiet,
448            mirror_stdout: self.mirror_stdout,
449            json_event_log: self.json_event_log,
450            cli_overrides: self.cli_overrides,
451            capability_overrides: self.capability_overrides,
452            capability_cache_policy: self.capability_cache_policy,
453        }
454    }
455}
456
457impl Default for CodexClientBuilder {
458    fn default() -> Self {
459        Self {
460            binary: crate::defaults::default_binary_path(),
461            codex_home: None,
462            create_home_dirs: true,
463            model: None,
464            timeout: crate::defaults::DEFAULT_TIMEOUT,
465            color_mode: ColorMode::Never,
466            working_dir: None,
467            add_dirs: Vec::new(),
468            images: Vec::new(),
469            json_output: false,
470            output_schema: false,
471            quiet: false,
472            mirror_stdout: true,
473            json_event_log: None,
474            cli_overrides: CliOverrides::default(),
475            capability_overrides: crate::CapabilityOverrides::default(),
476            capability_cache_policy: crate::CapabilityCachePolicy::default(),
477        }
478    }
479}