#[cfg(feature = "json")]
pub mod auth;
#[cfg(feature = "json")]
pub mod budget;
#[cfg(any(feature = "json", feature = "config"))]
mod codex_home;
pub mod command;
#[cfg(feature = "config")]
pub mod config;
pub mod dangerous;
pub mod error;
pub mod exec;
#[cfg(feature = "json")]
pub mod history;
pub mod mcp_config;
pub mod retry;
#[cfg(feature = "json")]
pub mod session;
#[cfg(feature = "json")]
pub mod streaming;
#[cfg(all(test, unix))]
mod test_support;
pub mod types;
pub mod version;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::Duration;
#[cfg(feature = "json")]
pub use auth::{AuthStatus, AuthStrategy};
#[cfg(feature = "json")]
pub use budget::{TokenBudget, TokenBudgetBuilder};
pub use command::CodexCommand;
pub use command::apply::ApplyCommand;
pub use command::completion::{CompletionCommand, Shell};
pub use command::doctor::DoctorCommand;
pub use command::exec::{ExecCommand, ExecResumeCommand};
pub use command::features::{FeaturesDisableCommand, FeaturesEnableCommand, FeaturesListCommand};
pub use command::fork::ForkCommand;
pub use command::login::{LoginCommand, LoginStatusCommand, LogoutCommand};
pub use command::mcp::{
McpAddCommand, McpGetCommand, McpListCommand, McpLoginCommand, McpLogoutCommand,
McpRemoveCommand,
};
pub use command::mcp_server::McpServerCommand;
pub use command::plugin::{
PluginAddCommand, PluginListCommand, PluginMarketplaceAddCommand, PluginMarketplaceListCommand,
PluginMarketplaceRemoveCommand, PluginMarketplaceUpgradeCommand, PluginRemoveCommand,
};
pub use command::raw::RawCommand;
pub use command::resume::ResumeCommand;
pub use command::review::ReviewCommand;
pub use command::sandbox::SandboxCommand;
pub use command::session_mgmt::{ArchiveCommand, DeleteCommand, UnarchiveCommand};
pub use command::update::UpdateCommand;
pub use command::version::VersionCommand;
#[cfg(feature = "config")]
pub use config::CodexConfig;
pub use error::{Error, FailureKind, Result};
pub use exec::CommandOutput;
#[cfg(feature = "json")]
pub use history::{SessionFile, SessionLog, SessionMeta, SessionQuery};
pub use mcp_config::{McpConfigBuilder, McpServerConfig};
pub use retry::{BackoffStrategy, RetryPolicy};
#[cfg(feature = "json")]
pub use session::{Session, TurnRecord};
pub use types::*;
pub use version::{
CliVersion, CliVersionStatus, TESTED_CLI_VERSION_MAX, TESTED_CLI_VERSION_MIN, VersionParseError,
};
#[derive(Debug, Clone)]
pub struct Codex {
pub(crate) binary: PathBuf,
pub(crate) working_dir: Option<PathBuf>,
pub(crate) env: HashMap<String, String>,
pub(crate) global_args: Vec<String>,
pub(crate) timeout: Option<Duration>,
pub(crate) termination_grace: Duration,
pub(crate) process_group: bool,
pub(crate) retry_policy: Option<RetryPolicy>,
pub(crate) tested_cli_version_range: (CliVersion, CliVersion),
}
impl Codex {
#[must_use]
pub fn builder() -> CodexBuilder {
CodexBuilder::default()
}
#[must_use]
pub fn binary(&self) -> &Path {
&self.binary
}
#[must_use]
pub fn working_dir(&self) -> Option<&Path> {
self.working_dir.as_deref()
}
#[must_use]
pub fn with_working_dir(&self, dir: impl Into<PathBuf>) -> Self {
let mut clone = self.clone();
clone.working_dir = Some(dir.into());
clone
}
#[cfg(feature = "config")]
pub fn config(&self) -> Result<Option<crate::config::CodexConfig>> {
let home = crate::codex_home::resolve(&|key| {
self.env
.get(key)
.cloned()
.or_else(|| std::env::var(key).ok())
});
crate::config::load_from_home(home)
}
#[cfg(feature = "json")]
#[must_use]
pub fn auth_status(&self) -> crate::auth::AuthStatus {
crate::auth::detect_with(|key| {
self.env
.get(key)
.cloned()
.or_else(|| std::env::var(key).ok())
})
}
pub async fn cli_version(&self) -> Result<CliVersion> {
let output = VersionCommand::new().execute(self).await?;
CliVersion::parse_version_output(&output.stdout).map_err(|e| Error::Io {
message: format!("failed to parse CLI version: {e}"),
source: std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()),
working_dir: None,
})
}
pub async fn check_version(&self, minimum: &CliVersion) -> Result<CliVersion> {
let version = self.cli_version().await?;
if version.satisfies_minimum(minimum) {
Ok(version)
} else {
Err(Error::VersionMismatch {
found: version,
minimum: *minimum,
})
}
}
#[must_use]
pub fn tested_cli_version_range(&self) -> (CliVersion, CliVersion) {
self.tested_cli_version_range
}
pub async fn cli_version_status(&self) -> Result<CliVersionStatus> {
let (min, max) = self.tested_cli_version_range;
let status = self.cli_version().await?.status_within(&min, &max);
warn_on_drift(&status);
Ok(status)
}
pub async fn ensure_tested_cli_version(&self) -> Result<CliVersion> {
let (min, max) = self.tested_cli_version_range;
let found = self.cli_version().await?;
match found.status_within(&min, &max) {
CliVersionStatus::Tested => Ok(found),
status => {
warn_on_drift(&status);
Err(Error::UntestedCliVersion {
found,
tested_min: min,
tested_max: max,
})
}
}
}
}
fn warn_on_drift(status: &CliVersionStatus) {
match status {
CliVersionStatus::Tested => {}
CliVersionStatus::NewerUntested { found, tested_max } => {
tracing::warn!(
found = %found,
tested_max = %tested_max,
"codex CLI is newer than this wrapper's tested-against range; \
semantics may have drifted"
);
}
CliVersionStatus::OlderThanMinimum { found, minimum } => {
tracing::warn!(
found = %found,
minimum = %minimum,
"codex CLI is older than this wrapper's tested-against range; \
some emitted arguments are likely to be rejected"
);
}
}
}
#[derive(Debug, Default)]
pub struct CodexBuilder {
binary: Option<PathBuf>,
working_dir: Option<PathBuf>,
env: HashMap<String, String>,
global_args: Vec<String>,
timeout: Option<Duration>,
termination_grace: Option<Duration>,
process_group: Option<bool>,
retry_policy: Option<RetryPolicy>,
tested_cli_version_range: Option<(CliVersion, CliVersion)>,
}
impl CodexBuilder {
#[must_use]
pub fn tested_cli_version_range(mut self, min: CliVersion, max: CliVersion) -> Self {
self.tested_cli_version_range = Some((min, max));
self
}
#[must_use]
pub fn binary(mut self, path: impl Into<PathBuf>) -> Self {
self.binary = Some(path.into());
self
}
#[must_use]
pub fn working_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.working_dir = Some(path.into());
self
}
#[must_use]
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.insert(key.into(), value.into());
self
}
#[must_use]
pub fn envs(
mut self,
vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
) -> Self {
for (key, value) in vars {
self.env.insert(key.into(), value.into());
}
self
}
#[must_use]
pub fn timeout_secs(mut self, seconds: u64) -> Self {
self.timeout = Some(Duration::from_secs(seconds));
self
}
#[must_use]
pub fn timeout(mut self, duration: Duration) -> Self {
self.timeout = Some(duration);
self
}
#[must_use]
pub fn termination_grace(mut self, duration: Duration) -> Self {
self.termination_grace = Some(duration);
self
}
#[must_use]
pub fn process_group(mut self, enabled: bool) -> Self {
self.process_group = Some(enabled);
self
}
#[must_use]
pub fn arg(mut self, arg: impl Into<String>) -> Self {
self.global_args.push(arg.into());
self
}
#[must_use]
pub fn config(mut self, key_value: impl Into<String>) -> Self {
self.global_args.push("-c".into());
self.global_args.push(key_value.into());
self
}
#[must_use]
pub fn enable(mut self, feature: impl Into<String>) -> Self {
self.global_args.push("--enable".into());
self.global_args.push(feature.into());
self
}
#[must_use]
pub fn disable(mut self, feature: impl Into<String>) -> Self {
self.global_args.push("--disable".into());
self.global_args.push(feature.into());
self
}
#[must_use]
pub fn retry(mut self, policy: RetryPolicy) -> Self {
self.retry_policy = Some(policy);
self
}
pub fn build(self) -> Result<Codex> {
let binary = match self.binary {
Some(path) => path,
None => which::which("codex").map_err(|_| Error::NotFound)?,
};
Ok(Codex {
binary,
working_dir: self.working_dir,
env: self.env,
global_args: self.global_args,
termination_grace: self
.termination_grace
.unwrap_or_else(|| Duration::from_secs(5)),
process_group: self.process_group.unwrap_or(true),
timeout: self.timeout,
retry_policy: self.retry_policy,
tested_cli_version_range: self.tested_cli_version_range.unwrap_or((
version::TESTED_CLI_VERSION_MIN,
version::TESTED_CLI_VERSION_MAX,
)),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_with_binary() {
let codex = Codex::builder()
.binary("/usr/local/bin/codex")
.env("FOO", "bar")
.timeout_secs(60)
.build()
.unwrap();
assert_eq!(codex.binary, PathBuf::from("/usr/local/bin/codex"));
assert_eq!(codex.env.get("FOO").unwrap(), "bar");
assert_eq!(codex.timeout, Some(Duration::from_secs(60)));
}
#[test]
fn builder_global_args() {
let codex = Codex::builder()
.binary("/usr/local/bin/codex")
.config("model=\"gpt-5\"")
.enable("foo")
.disable("bar")
.build()
.unwrap();
assert_eq!(
codex.global_args,
vec![
"-c",
"model=\"gpt-5\"",
"--enable",
"foo",
"--disable",
"bar"
]
);
}
#[test]
fn client_defaults_to_the_crate_tested_range() {
let codex = Codex::builder().binary("/bin/echo").build().unwrap();
assert_eq!(
codex.tested_cli_version_range(),
(
version::TESTED_CLI_VERSION_MIN,
version::TESTED_CLI_VERSION_MAX
)
);
}
#[test]
fn builder_can_override_the_tested_range() {
let min = CliVersion::new(1, 0, 0);
let max = CliVersion::new(2, 0, 0);
let codex = Codex::builder()
.binary("/bin/echo")
.tested_cli_version_range(min, max)
.build()
.unwrap();
assert_eq!(codex.tested_cli_version_range(), (min, max));
}
#[test]
fn untested_version_error_names_both_bounds() {
let err = Error::UntestedCliVersion {
found: CliVersion::new(0, 200, 0),
tested_min: CliVersion::new(0, 145, 0),
tested_max: CliVersion::new(0, 146, 0),
};
assert_eq!(
err.to_string(),
"CLI version 0.200.0 is outside the tested range 0.145.0..=0.146.0"
);
}
#[cfg(feature = "json")]
#[test]
fn auth_status_honors_a_client_codex_home() {
let dir =
std::env::temp_dir().join(format!("codex-wrapper-client-auth-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("auth.json"),
r#"{"auth_mode":"apikey","OPENAI_API_KEY":"sk-secret"}"#,
)
.unwrap();
let codex = Codex::builder()
.binary("/bin/echo")
.env("CODEX_HOME", dir.to_str().unwrap())
.build()
.unwrap();
let status = codex.auth_status();
assert_eq!(status.codex_home, dir);
assert!(status.is_configured());
assert!(
!format!("{status:?}").contains("sk-secret"),
"the credential leaked into the status"
);
let _ = std::fs::remove_dir_all(&dir);
}
}