#![forbid(unsafe_code)]
mod apply_diff;
mod auth;
mod builder;
mod bundled_binary;
mod cli;
mod client_core;
mod commands;
mod defaults;
mod error;
mod events;
mod exec;
mod execpolicy;
mod home;
pub mod jsonl;
pub mod mcp;
mod process;
pub mod rollout_jsonl;
pub mod wrapper_coverage_manifest;
pub use crate::error::CodexError;
pub use apply_diff::{ApplyDiffArtifacts, CloudApplyRequest, CloudDiffRequest};
pub use auth::{AuthSessionHelper, CodexAuthMethod, CodexAuthStatus, CodexLogoutStatus};
pub use builder::{
ApprovalPolicy, CliOverrides, CliOverridesPatch, CodexClientBuilder, ColorMode, ConfigOverride,
FeatureToggles, FlagState, LocalProvider, ModelVerbosity, ReasoningEffort, ReasoningOverrides,
ReasoningSummary, ReasoningSummaryFormat, SafetyOverride, SandboxMode,
};
pub use bundled_binary::{
default_bundled_platform_label, resolve_bundled_binary, BundledBinary, BundledBinaryError,
BundledBinarySpec,
};
pub use cli::{
AppServerCodegenOutput, AppServerCodegenRequest, AppServerCodegenTarget, AppServerProxyRequest,
AppServerRequest, CloudExecRequest, CloudListOutput, CloudListRequest, CloudOverviewRequest,
CloudStatusRequest, CodexFeature, CodexFeatureStage, DebugAppServerHelpRequest,
DebugAppServerRequest, DebugAppServerSendMessageV2Request, DebugCommandRequest,
DebugHelpRequest, DebugModelsRequest, DebugPromptInputRequest, ExecRequest,
ExecReviewCommandRequest, ExecServerRequest, FeaturesCommandRequest, FeaturesDisableRequest,
FeaturesEnableRequest, FeaturesListFormat, FeaturesListOutput, FeaturesListRequest,
ForkSessionRequest, HelpCommandRequest, HelpScope, McpAddRequest, McpAddTransport,
McpGetRequest, McpListOutput, McpListRequest, McpLogoutRequest, McpOauthLoginRequest,
McpOverviewRequest, McpRemoveRequest, PluginCommandRequest, PluginHelpRequest,
PluginMarketplaceAddRequest, PluginMarketplaceCommandRequest, PluginMarketplaceHelpRequest,
PluginMarketplaceRemoveRequest, PluginMarketplaceUpgradeRequest, ResponsesApiProxyHandle,
ResponsesApiProxyInfo, ResponsesApiProxyRequest, ResumeSessionRequest, ReviewCommandRequest,
SandboxCommandRequest, SandboxPlatform, SandboxRun, StdioToUdsRequest,
};
pub use events::{
CommandExecutionDelta, CommandExecutionState, EventError, FileChangeDelta, FileChangeKind,
FileChangeState, ItemDelta, ItemDeltaPayload, ItemEnvelope, ItemFailure, ItemPayload,
ItemSnapshot, ItemStatus, McpToolCallDelta, McpToolCallState, TextContent, TextDelta,
ThreadEvent, ThreadStarted, TodoItem, TodoListDelta, TodoListState, ToolCallStatus,
TurnCompleted, TurnFailed, TurnStarted, WebSearchDelta, WebSearchState, WebSearchStatus,
};
pub use exec::{
DynExecCompletion, DynThreadEventStream, ExecCompletion, ExecStream, ExecStreamControl,
ExecStreamError, ExecStreamRequest, ExecTerminationHandle, ResumeRequest, ResumeSelector,
};
pub use execpolicy::{
ExecPolicyCheckRequest, ExecPolicyCheckResult, ExecPolicyDecision, ExecPolicyEvaluation,
ExecPolicyMatch, ExecPolicyNoMatch, ExecPolicyRuleMatch,
};
pub use home::{AuthSeedError, AuthSeedOptions, AuthSeedOutcome, CodexHomeLayout};
pub use jsonl::{
thread_event_jsonl_file, thread_event_jsonl_reader, JsonlThreadEventParser,
ThreadEventJsonlFileReader, ThreadEventJsonlReader, ThreadEventJsonlRecord,
};
pub use rollout_jsonl::{
find_rollout_file_by_id, find_rollout_files, rollout_jsonl_file, rollout_jsonl_reader,
RolloutBaseInstructions, RolloutContentPart, RolloutEvent, RolloutEventMsg,
RolloutEventMsgPayload, RolloutJsonlError, RolloutJsonlFileReader, RolloutJsonlParser,
RolloutJsonlReader, RolloutJsonlRecord, RolloutResponseItem, RolloutResponseItemPayload,
RolloutSessionMeta, RolloutSessionMetaPayload, RolloutUnknown,
};
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
time::{Duration, SystemTime},
};
use home::CommandEnvironment;
use process::command_output_text;
use tracing::warn;
#[cfg(test)]
use tokio::time;
#[cfg(test)]
use tokio::sync::mpsc;
#[cfg(test)]
use builder::{
cli_override_args, reasoning_config_for, DEFAULT_REASONING_CONFIG_GPT5,
DEFAULT_REASONING_CONFIG_GPT5_1, DEFAULT_REASONING_CONFIG_GPT5_CODEX,
};
fn normalize_non_empty(value: &str) -> Option<String> {
let trimmed = value.trim();
(!trimmed.is_empty()).then_some(trimmed.to_string())
}
type Command = tokio::process::Command;
type ConsoleTarget = crate::process::ConsoleTarget;
#[cfg(test)]
type OsString = std::ffi::OsString;
async fn tee_stream<R>(
reader: R,
target: ConsoleTarget,
mirror_console: bool,
) -> Result<Vec<u8>, std::io::Error>
where
R: tokio::io::AsyncRead + Unpin,
{
crate::process::tee_stream(reader, target, mirror_console).await
}
fn spawn_with_retry(
command: &mut Command,
binary: &std::path::Path,
) -> Result<tokio::process::Child, CodexError> {
crate::process::spawn_with_retry(command, binary)
}
fn resolve_cli_overrides(
builder: &CliOverrides,
patch: &CliOverridesPatch,
model: Option<&str>,
) -> builder::ResolvedCliOverrides {
builder::resolve_cli_overrides(builder, patch, model)
}
fn apply_cli_overrides(
command: &mut Command,
resolved: &builder::ResolvedCliOverrides,
include_search: bool,
) {
builder::apply_cli_overrides(command, resolved, include_search);
}
#[cfg(test)]
fn bundled_binary_filename(platform: &str) -> &'static str {
bundled_binary::bundled_binary_filename(platform)
}
mod capabilities;
mod version;
pub use capabilities::*;
pub use version::update_advisory_from_capabilities;
#[derive(Clone, Debug)]
pub struct CodexClient {
command_env: CommandEnvironment,
model: Option<String>,
timeout: Duration,
color_mode: ColorMode,
working_dir: Option<PathBuf>,
add_dirs: Vec<PathBuf>,
images: Vec<PathBuf>,
json_output: bool,
output_schema: bool,
quiet: bool,
mirror_stdout: bool,
json_event_log: Option<PathBuf>,
cli_overrides: CliOverrides,
capability_overrides: CapabilityOverrides,
capability_cache_policy: CapabilityCachePolicy,
}
impl CodexClient {
pub fn builder() -> CodexClientBuilder {
CodexClientBuilder::default()
}
pub fn codex_home_layout(&self) -> Option<CodexHomeLayout> {
self.command_env.codex_home_layout()
}
pub async fn probe_capabilities(&self) -> CodexCapabilities {
self.probe_capabilities_internal(self.capability_cache_policy, &[], None)
.await
}
pub async fn probe_capabilities_with_env_overrides(
&self,
env_overrides: &BTreeMap<String, String>,
) -> CodexCapabilities {
if env_overrides.is_empty() {
return self.probe_capabilities().await;
}
let env_overrides: Vec<(String, String)> = env_overrides
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
self.probe_capabilities_internal(CapabilityCachePolicy::Bypass, &env_overrides, None)
.await
}
pub async fn probe_capabilities_with_policy(
&self,
cache_policy: CapabilityCachePolicy,
) -> CodexCapabilities {
self.probe_capabilities_internal(cache_policy, &[], None)
.await
}
pub(crate) async fn probe_capabilities_for_current_dir(
&self,
current_dir: &Path,
) -> CodexCapabilities {
self.probe_capabilities_internal(self.capability_cache_policy, &[], Some(current_dir))
.await
}
pub(crate) async fn probe_capabilities_with_env_overrides_for_current_dir(
&self,
env_overrides: &BTreeMap<String, String>,
current_dir: &Path,
) -> CodexCapabilities {
if env_overrides.is_empty() {
return self.probe_capabilities_for_current_dir(current_dir).await;
}
let env_overrides: Vec<(String, String)> = env_overrides
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
self.probe_capabilities_internal(
CapabilityCachePolicy::Bypass,
&env_overrides,
Some(current_dir),
)
.await
}
async fn probe_capabilities_internal(
&self,
cache_policy: CapabilityCachePolicy,
env_overrides: &[(String, String)],
current_dir: Option<&Path>,
) -> CodexCapabilities {
let cache_key = capability_cache_key_for_current_dir_with_env(
self.command_env.binary_path(),
current_dir,
env_overrides,
);
let fingerprint = current_fingerprint(&cache_key);
let overrides = &self.capability_overrides;
let cache_reads_enabled = matches!(cache_policy, CapabilityCachePolicy::PreferCache)
&& has_fingerprint_metadata(&fingerprint);
let cache_writes_enabled = !matches!(cache_policy, CapabilityCachePolicy::Bypass)
&& has_fingerprint_metadata(&fingerprint);
if let Some(snapshot) = overrides.snapshot.clone() {
let capabilities = finalize_capabilities_with_overrides(
snapshot,
overrides,
cache_key.clone(),
fingerprint.clone(),
true,
);
if cache_writes_enabled {
update_capability_cache(capabilities.clone());
}
return capabilities;
}
if cache_reads_enabled {
if let Some(cached) = cached_capabilities(&cache_key, &fingerprint) {
if overrides.is_empty() {
return cached;
}
let merged = finalize_capabilities_with_overrides(
cached,
overrides,
cache_key.clone(),
fingerprint.clone(),
false,
);
if cache_writes_enabled {
update_capability_cache(merged.clone());
}
return merged;
}
}
let probed = self
.probe_capabilities_uncached(
&cache_key,
fingerprint.clone(),
env_overrides,
current_dir,
)
.await;
let capabilities =
finalize_capabilities_with_overrides(probed, overrides, cache_key, fingerprint, false);
if cache_writes_enabled {
update_capability_cache(capabilities.clone());
}
capabilities
}
async fn probe_capabilities_uncached(
&self,
cache_key: &CapabilityCacheKey,
fingerprint: Option<BinaryFingerprint>,
env_overrides: &[(String, String)],
current_dir: Option<&Path>,
) -> CodexCapabilities {
let mut plan = CapabilityProbePlan::default();
let mut features = CodexFeatureFlags::default();
let mut version = None;
plan.steps.push(CapabilityProbeStep::VersionFlag);
match self
.run_basic_command_with_env_overrides_and_current_dir(
["--version"],
env_overrides,
current_dir,
)
.await
{
Ok(output) => {
if !output.status.success() {
warn!(
status = ?output.status,
binary = ?cache_key.binary_path,
"codex --version exited non-zero"
);
}
let text = command_output_text(&output);
if !text.trim().is_empty() {
version = Some(version::parse_version_output(&text));
}
}
Err(error) => warn!(
?error,
binary = ?cache_key.binary_path,
"codex --version probe failed"
),
}
let mut parsed_features = false;
plan.steps.push(CapabilityProbeStep::FeaturesListJson);
match self
.run_basic_command_with_env_overrides_and_current_dir(
["features", "list", "--json"],
env_overrides,
current_dir,
)
.await
{
Ok(output) => {
if !output.status.success() {
warn!(
status = ?output.status,
binary = ?cache_key.binary_path,
"codex features list --json exited non-zero"
);
}
if output.status.success() {
features.supports_features_list = true;
}
let text = command_output_text(&output);
if let Some(parsed) = version::parse_features_from_json(&text) {
version::merge_feature_flags(&mut features, parsed);
parsed_features = version::detected_feature_flags(&features);
} else if !text.is_empty() {
let parsed = version::parse_features_from_text(&text);
version::merge_feature_flags(&mut features, parsed);
parsed_features = version::detected_feature_flags(&features);
}
}
Err(error) => warn!(
?error,
binary = ?cache_key.binary_path,
"codex features list --json probe failed"
),
}
if !parsed_features {
plan.steps.push(CapabilityProbeStep::FeaturesListText);
match self
.run_basic_command_with_env_overrides_and_current_dir(
["features", "list"],
env_overrides,
current_dir,
)
.await
{
Ok(output) => {
if !output.status.success() {
warn!(
status = ?output.status,
binary = ?cache_key.binary_path,
"codex features list exited non-zero"
);
}
if output.status.success() {
features.supports_features_list = true;
}
let text = command_output_text(&output);
let parsed = version::parse_features_from_text(&text);
version::merge_feature_flags(&mut features, parsed);
}
Err(error) => warn!(
?error,
binary = ?cache_key.binary_path,
"codex features list probe failed"
),
}
}
if version::should_run_help_fallback(&features) {
plan.steps.push(CapabilityProbeStep::HelpFallback);
match self
.run_basic_command_with_env_overrides_and_current_dir(
["--help"],
env_overrides,
current_dir,
)
.await
{
Ok(output) => {
if !output.status.success() {
warn!(
status = ?output.status,
binary = ?cache_key.binary_path,
"codex --help exited non-zero"
);
}
let text = command_output_text(&output);
let parsed = version::parse_help_output(&text);
version::merge_feature_flags(&mut features, parsed);
}
Err(error) => warn!(
?error,
binary = ?cache_key.binary_path,
"codex --help probe failed"
),
}
}
CodexCapabilities {
cache_key: cache_key.clone(),
fingerprint,
version,
features,
probe_plan: plan,
collected_at: SystemTime::now(),
}
}
pub async fn update_advisory(
&self,
latest_releases: &CodexLatestReleases,
) -> CodexUpdateAdvisory {
let capabilities = self.probe_capabilities().await;
update_advisory_from_capabilities(&capabilities, latest_releases)
}
}
impl Default for CodexClient {
fn default() -> Self {
CodexClient::builder().build()
}
}
#[cfg(all(test, unix))]
mod tests;