chore: expose codex_home via Config (#941)

This commit is contained in:
Michael Bolin
2025-05-15 00:30:13 -07:00
committed by GitHub
parent 0b9ef93da5
commit 5fc9fc3e3e
10 changed files with 212 additions and 98 deletions

View File

@@ -610,7 +610,7 @@ async fn submission_loop(
// `instructions` value into the Session struct. // `instructions` value into the Session struct.
let session_id = Uuid::new_v4(); let session_id = Uuid::new_v4();
let rollout_recorder = let rollout_recorder =
match RolloutRecorder::new(session_id, instructions.clone()).await { match RolloutRecorder::new(&config, session_id, instructions.clone()).await {
Ok(r) => Some(r), Ok(r) => Some(r),
Err(e) => { Err(e) => {
tracing::warn!("failed to initialise rollout recorder: {e}"); tracing::warn!("failed to initialise rollout recorder: {e}");

View File

@@ -77,6 +77,10 @@ pub struct Config {
/// Maximum number of bytes to include from an AGENTS.md project doc file. /// Maximum number of bytes to include from an AGENTS.md project doc file.
pub project_doc_max_bytes: usize, pub project_doc_max_bytes: usize,
/// Directory containing all Codex state (defaults to `~/.codex` but can be
/// overridden by the `CODEX_HOME` environment variable).
pub codex_home: PathBuf,
} }
/// Base config deserialized from ~/.codex/config.toml. /// Base config deserialized from ~/.codex/config.toml.
@@ -132,8 +136,8 @@ impl ConfigToml {
/// Attempt to parse the file at `~/.codex/config.toml`. If it does not /// Attempt to parse the file at `~/.codex/config.toml`. If it does not
/// exist, return a default config. Though if it exists and cannot be /// exist, return a default config. Though if it exists and cannot be
/// parsed, report that to the user and force them to fix it. /// parsed, report that to the user and force them to fix it.
fn load_from_toml() -> std::io::Result<Self> { fn load_from_toml(codex_home: &Path) -> std::io::Result<Self> {
let config_toml_path = codex_dir()?.join("config.toml"); let config_toml_path = codex_home.join("config.toml");
match std::fs::read_to_string(&config_toml_path) { match std::fs::read_to_string(&config_toml_path) {
Ok(contents) => toml::from_str::<Self>(&contents).map_err(|e| { Ok(contents) => toml::from_str::<Self>(&contents).map_err(|e| {
tracing::error!("Failed to parse config.toml: {e}"); tracing::error!("Failed to parse config.toml: {e}");
@@ -161,7 +165,7 @@ where
match permissions { match permissions {
Some(raw_permissions) => { Some(raw_permissions) => {
let base_path = codex_dir().map_err(serde::de::Error::custom)?; let base_path = find_codex_home().map_err(serde::de::Error::custom)?;
let converted = raw_permissions let converted = raw_permissions
.into_iter() .into_iter()
@@ -194,18 +198,25 @@ impl Config {
/// ~/.codex/config.toml, ~/.codex/instructions.md, embedded defaults, and /// ~/.codex/config.toml, ~/.codex/instructions.md, embedded defaults, and
/// any values provided in `overrides` (highest precedence). /// any values provided in `overrides` (highest precedence).
pub fn load_with_overrides(overrides: ConfigOverrides) -> std::io::Result<Self> { pub fn load_with_overrides(overrides: ConfigOverrides) -> std::io::Result<Self> {
let cfg: ConfigToml = ConfigToml::load_from_toml()?; // Resolve the directory that stores Codex state (e.g. ~/.codex or the
// value of $CODEX_HOME) so we can embed it into the resulting
// `Config` instance.
let codex_home = find_codex_home()?;
let cfg: ConfigToml = ConfigToml::load_from_toml(&codex_home)?;
tracing::warn!("Config parsed from config.toml: {cfg:?}"); tracing::warn!("Config parsed from config.toml: {cfg:?}");
let codex_dir = codex_dir().ok();
Self::load_from_base_config_with_overrides(cfg, overrides, codex_dir.as_deref()) Self::load_from_base_config_with_overrides(cfg, overrides, codex_home)
} }
fn load_from_base_config_with_overrides( /// Meant to be used exclusively for tests: `load_with_overrides()` should
/// be used in all other cases.
pub fn load_from_base_config_with_overrides(
cfg: ConfigToml, cfg: ConfigToml,
overrides: ConfigOverrides, overrides: ConfigOverrides,
codex_dir: Option<&Path>, codex_home: PathBuf,
) -> std::io::Result<Self> { ) -> std::io::Result<Self> {
let instructions = Self::load_instructions(codex_dir); let instructions = Self::load_instructions(Some(&codex_home));
// Destructure ConfigOverrides fully to ensure all overrides are applied. // Destructure ConfigOverrides fully to ensure all overrides are applied.
let ConfigOverrides { let ConfigOverrides {
@@ -308,6 +319,7 @@ impl Config {
mcp_servers: cfg.mcp_servers, mcp_servers: cfg.mcp_servers,
model_providers, model_providers,
project_doc_max_bytes: cfg.project_doc_max_bytes.unwrap_or(PROJECT_DOC_MAX_BYTES), project_doc_max_bytes: cfg.project_doc_max_bytes.unwrap_or(PROJECT_DOC_MAX_BYTES),
codex_home,
}; };
Ok(config) Ok(config)
} }
@@ -328,27 +340,29 @@ impl Config {
} }
}) })
} }
/// Meant to be used exclusively for tests: `load_with_overrides()` should
/// be used in all other cases.
pub fn load_default_config_for_test() -> Self {
#[expect(clippy::expect_used)]
Self::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides::default(),
None,
)
.expect("defaults for test should always succeed")
}
} }
fn default_model() -> String { fn default_model() -> String {
OPENAI_DEFAULT_MODEL.to_string() OPENAI_DEFAULT_MODEL.to_string()
} }
/// Returns the path to the Codex configuration directory, which is `~/.codex`. /// Returns the path to the Codex configuration directory, which can be
/// Does not verify that the directory exists. /// specified by the `CODEX_HOME` environment variable. If not set, defaults to
pub fn codex_dir() -> std::io::Result<PathBuf> { /// `~/.codex`.
///
/// - If `CODEX_HOME` is set, the value will be canonicalized and this
/// function will Err if the path does not exist.
/// - If `CODEX_HOME` is not set, this function does not verify that the
/// directory exists.
fn find_codex_home() -> std::io::Result<PathBuf> {
// Honor the `CODEX_HOME` environment variable when it is set to allow users
// (and tests) to override the default location.
if let Ok(val) = std::env::var("CODEX_HOME") {
if !val.is_empty() {
return PathBuf::from(val).canonicalize();
}
}
let mut p = home_dir().ok_or_else(|| { let mut p = home_dir().ok_or_else(|| {
std::io::Error::new( std::io::Error::new(
std::io::ErrorKind::NotFound, std::io::ErrorKind::NotFound,
@@ -361,8 +375,8 @@ pub fn codex_dir() -> std::io::Result<PathBuf> {
/// Returns the path to the folder where Codex logs are stored. Does not verify /// Returns the path to the folder where Codex logs are stored. Does not verify
/// that the directory exists. /// that the directory exists.
pub fn log_dir() -> std::io::Result<PathBuf> { pub fn log_dir(cfg: &Config) -> std::io::Result<PathBuf> {
let mut p = codex_dir()?; let mut p = cfg.codex_home.clone();
p.push("log"); p.push("log");
Ok(p) Ok(p)
} }
@@ -470,20 +484,26 @@ mod tests {
assert!(msg.contains("not-a-real-permission")); assert!(msg.contains("not-a-real-permission"));
} }
/// Users can specify config values at multiple levels that have the struct PrecedenceTestFixture {
/// following precedence: cwd: TempDir,
/// codex_home: TempDir,
/// 1. custom command-line argument, e.g. `--model o3` cfg: ConfigToml,
/// 2. as part of a profile, where the `--profile` is specified via a CLI model_provider_map: HashMap<String, ModelProviderInfo>,
/// (or in the config file itelf) openai_provider: ModelProviderInfo,
/// 3. as an entry in `config.toml`, e.g. `model = "o3"` openai_chat_completions_provider: ModelProviderInfo,
/// 4. the default value for a required field defined in code, e.g., }
/// `crate::flags::OPENAI_DEFAULT_MODEL`
/// impl PrecedenceTestFixture {
/// Note that profiles are the recommended way to specify a group of fn cwd(&self) -> PathBuf {
/// configuration options together. self.cwd.path().to_path_buf()
#[test] }
fn test_precedence_overrides_then_profile_then_config_toml() -> std::io::Result<()> {
fn codex_home(&self) -> PathBuf {
self.codex_home.path().to_path_buf()
}
}
fn create_test_fixture() -> std::io::Result<PrecedenceTestFixture> {
let toml = r#" let toml = r#"
model = "o3" model = "o3"
approval_policy = "unless-allow-listed" approval_policy = "unless-allow-listed"
@@ -526,6 +546,8 @@ disable_response_storage = true
// a parent folder, either. // a parent folder, either.
std::fs::write(cwd.join(".git"), "gitdir: nowhere")?; std::fs::write(cwd.join(".git"), "gitdir: nowhere")?;
let codex_home_temp_dir = TempDir::new().unwrap();
let openai_chat_completions_provider = ModelProviderInfo { let openai_chat_completions_provider = ModelProviderInfo {
name: "OpenAI using Chat Completions".to_string(), name: "OpenAI using Chat Completions".to_string(),
base_url: "https://api.openai.com/v1".to_string(), base_url: "https://api.openai.com/v1".to_string(),
@@ -547,94 +569,143 @@ disable_response_storage = true
.expect("openai provider should exist") .expect("openai provider should exist")
.clone(); .clone();
Ok(PrecedenceTestFixture {
cwd: cwd_temp_dir,
codex_home: codex_home_temp_dir,
cfg,
model_provider_map,
openai_provider,
openai_chat_completions_provider,
})
}
/// Users can specify config values at multiple levels that have the
/// following precedence:
///
/// 1. custom command-line argument, e.g. `--model o3`
/// 2. as part of a profile, where the `--profile` is specified via a CLI
/// (or in the config file itelf)
/// 3. as an entry in `config.toml`, e.g. `model = "o3"`
/// 4. the default value for a required field defined in code, e.g.,
/// `crate::flags::OPENAI_DEFAULT_MODEL`
///
/// Note that profiles are the recommended way to specify a group of
/// configuration options together.
#[test]
fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
let fixture = create_test_fixture()?;
let o3_profile_overrides = ConfigOverrides { let o3_profile_overrides = ConfigOverrides {
config_profile: Some("o3".to_string()), config_profile: Some("o3".to_string()),
cwd: Some(cwd.clone()), cwd: Some(fixture.cwd()),
..Default::default() ..Default::default()
}; };
let o3_profile_config = let o3_profile_config: Config = Config::load_from_base_config_with_overrides(
Config::load_from_base_config_with_overrides(cfg.clone(), o3_profile_overrides, None)?; fixture.cfg.clone(),
o3_profile_overrides,
fixture.codex_home(),
)?;
assert_eq!( assert_eq!(
Config { Config {
model: "o3".to_string(), model: "o3".to_string(),
model_provider_id: "openai".to_string(), model_provider_id: "openai".to_string(),
model_provider: openai_provider.clone(), model_provider: fixture.openai_provider.clone(),
approval_policy: AskForApproval::Never, approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::new_read_only_policy(), sandbox_policy: SandboxPolicy::new_read_only_policy(),
disable_response_storage: false, disable_response_storage: false,
instructions: None, instructions: None,
notify: None, notify: None,
cwd: cwd.clone(), cwd: fixture.cwd(),
mcp_servers: HashMap::new(), mcp_servers: HashMap::new(),
model_providers: model_provider_map.clone(), model_providers: fixture.model_provider_map.clone(),
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES, project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
codex_home: fixture.codex_home(),
}, },
o3_profile_config o3_profile_config
); );
Ok(())
}
#[test]
fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
let fixture = create_test_fixture()?;
let gpt3_profile_overrides = ConfigOverrides { let gpt3_profile_overrides = ConfigOverrides {
config_profile: Some("gpt3".to_string()), config_profile: Some("gpt3".to_string()),
cwd: Some(cwd.clone()), cwd: Some(fixture.cwd()),
..Default::default() ..Default::default()
}; };
let gpt3_profile_config = Config::load_from_base_config_with_overrides( let gpt3_profile_config = Config::load_from_base_config_with_overrides(
cfg.clone(), fixture.cfg.clone(),
gpt3_profile_overrides, gpt3_profile_overrides,
None, fixture.codex_home(),
)?; )?;
let expected_gpt3_profile_config = Config { let expected_gpt3_profile_config = Config {
model: "gpt-3.5-turbo".to_string(), model: "gpt-3.5-turbo".to_string(),
model_provider_id: "openai-chat-completions".to_string(), model_provider_id: "openai-chat-completions".to_string(),
model_provider: openai_chat_completions_provider, model_provider: fixture.openai_chat_completions_provider.clone(),
approval_policy: AskForApproval::UnlessAllowListed, approval_policy: AskForApproval::UnlessAllowListed,
sandbox_policy: SandboxPolicy::new_read_only_policy(), sandbox_policy: SandboxPolicy::new_read_only_policy(),
disable_response_storage: false, disable_response_storage: false,
instructions: None, instructions: None,
notify: None, notify: None,
cwd: cwd.clone(), cwd: fixture.cwd(),
mcp_servers: HashMap::new(), mcp_servers: HashMap::new(),
model_providers: model_provider_map.clone(), model_providers: fixture.model_provider_map.clone(),
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES, project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
codex_home: fixture.codex_home(),
}; };
assert_eq!(expected_gpt3_profile_config.clone(), gpt3_profile_config);
assert_eq!(expected_gpt3_profile_config, gpt3_profile_config);
// Verify that loading without specifying a profile in ConfigOverrides // Verify that loading without specifying a profile in ConfigOverrides
// uses the default profile from the config file. // uses the default profile from the config file (which is "gpt3").
let default_profile_overrides = ConfigOverrides { let default_profile_overrides = ConfigOverrides {
cwd: Some(cwd.clone()), cwd: Some(fixture.cwd()),
..Default::default() ..Default::default()
}; };
let default_profile_config = Config::load_from_base_config_with_overrides( let default_profile_config = Config::load_from_base_config_with_overrides(
cfg.clone(), fixture.cfg.clone(),
default_profile_overrides, default_profile_overrides,
None, fixture.codex_home(),
)?; )?;
assert_eq!(expected_gpt3_profile_config, default_profile_config); assert_eq!(expected_gpt3_profile_config, default_profile_config);
Ok(())
}
#[test]
fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
let fixture = create_test_fixture()?;
let zdr_profile_overrides = ConfigOverrides { let zdr_profile_overrides = ConfigOverrides {
config_profile: Some("zdr".to_string()), config_profile: Some("zdr".to_string()),
cwd: Some(cwd.clone()), cwd: Some(fixture.cwd()),
..Default::default() ..Default::default()
}; };
let zdr_profile_config = let zdr_profile_config = Config::load_from_base_config_with_overrides(
Config::load_from_base_config_with_overrides(cfg.clone(), zdr_profile_overrides, None)?; fixture.cfg.clone(),
assert_eq!( zdr_profile_overrides,
Config { fixture.codex_home(),
model: "o3".to_string(), )?;
model_provider_id: "openai".to_string(), let expected_zdr_profile_config = Config {
model_provider: openai_provider.clone(), model: "o3".to_string(),
approval_policy: AskForApproval::OnFailure, model_provider_id: "openai".to_string(),
sandbox_policy: SandboxPolicy::new_read_only_policy(), model_provider: fixture.openai_provider.clone(),
disable_response_storage: true, approval_policy: AskForApproval::OnFailure,
instructions: None, sandbox_policy: SandboxPolicy::new_read_only_policy(),
notify: None, disable_response_storage: true,
cwd: cwd.clone(), instructions: None,
mcp_servers: HashMap::new(), notify: None,
model_providers: model_provider_map.clone(), cwd: fixture.cwd(),
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES, mcp_servers: HashMap::new(),
}, model_providers: fixture.model_provider_map.clone(),
zdr_profile_config project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
); codex_home: fixture.codex_home(),
};
assert_eq!(expected_zdr_profile_config, zdr_profile_config);
Ok(()) Ok(())
} }

View File

@@ -1,12 +1,11 @@
//! Root of the `codex-core` library. //! Root of the `codex-core` library.
// Prevent accidental direct writes to stdout/stderr in library code. All // Prevent accidental direct writes to stdout/stderr in library code. All
// uservisible output must go through the appropriate abstraction (e.g., // user-visible output must go through the appropriate abstraction (e.g.,
// the TUI or the tracing stack). // the TUI or the tracing stack).
#![deny(clippy::print_stdout, clippy::print_stderr)] #![deny(clippy::print_stdout, clippy::print_stderr)]
mod chat_completions; mod chat_completions;
mod client; mod client;
mod client_common; mod client_common;
pub mod codex; pub mod codex;

View File

@@ -137,7 +137,8 @@ mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)] #![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*; use super::*;
use crate::config::Config; use crate::config::ConfigOverrides;
use crate::config::ConfigToml;
use std::fs; use std::fs;
use tempfile::TempDir; use tempfile::TempDir;
@@ -147,12 +148,19 @@ mod tests {
/// value is cleared to mimic a scenario where no system instructions have /// value is cleared to mimic a scenario where no system instructions have
/// been configured. /// been configured.
fn make_config(root: &TempDir, limit: usize, instructions: Option<&str>) -> Config { fn make_config(root: &TempDir, limit: usize, instructions: Option<&str>) -> Config {
let mut cfg = Config::load_default_config_for_test(); let codex_home = TempDir::new().unwrap();
cfg.cwd = root.path().to_path_buf(); let mut config = Config::load_from_base_config_with_overrides(
cfg.project_doc_max_bytes = limit; ConfigToml::default(),
ConfigOverrides::default(),
codex_home.path().to_path_buf(),
)
.expect("defaults for test should always succeed");
cfg.instructions = instructions.map(ToOwned::to_owned); config.cwd = root.path().to_path_buf();
cfg config.project_doc_max_bytes = limit;
config.instructions = instructions.map(ToOwned::to_owned);
config
} }
/// AGENTS.md missing should yield `None`. /// AGENTS.md missing should yield `None`.

View File

@@ -17,7 +17,7 @@ use tokio::sync::mpsc::Sender;
use tokio::sync::mpsc::{self}; use tokio::sync::mpsc::{self};
use uuid::Uuid; use uuid::Uuid;
use crate::config::codex_dir; use crate::config::Config;
use crate::models::ResponseItem; use crate::models::ResponseItem;
/// Folder inside `~/.codex` that holds saved rollouts. /// Folder inside `~/.codex` that holds saved rollouts.
@@ -49,12 +49,16 @@ impl RolloutRecorder {
/// Attempt to create a new [`RolloutRecorder`]. If the sessions directory /// Attempt to create a new [`RolloutRecorder`]. If the sessions directory
/// cannot be created or the rollout file cannot be opened we return the /// cannot be created or the rollout file cannot be opened we return the
/// error so the caller can decide whether to disable persistence. /// error so the caller can decide whether to disable persistence.
pub async fn new(uuid: Uuid, instructions: Option<String>) -> std::io::Result<Self> { pub async fn new(
config: &Config,
uuid: Uuid,
instructions: Option<String>,
) -> std::io::Result<Self> {
let LogFileInfo { let LogFileInfo {
file, file,
session_id, session_id,
timestamp, timestamp,
} = create_log_file(uuid)?; } = create_log_file(config, uuid)?;
// Build the static session metadata JSON first. // Build the static session metadata JSON first.
let timestamp_format: &[FormatItem] = format_description!( let timestamp_format: &[FormatItem] = format_description!(
@@ -154,9 +158,9 @@ struct LogFileInfo {
timestamp: OffsetDateTime, timestamp: OffsetDateTime,
} }
fn create_log_file(session_id: Uuid) -> std::io::Result<LogFileInfo> { fn create_log_file(config: &Config, session_id: Uuid) -> std::io::Result<LogFileInfo> {
// Resolve ~/.codex/sessions and create it if missing. // Resolve ~/.codex/sessions and create it if missing.
let mut dir = codex_dir()?; let mut dir = config.codex_home.clone();
dir.push(SESSIONS_SUBDIR); dir.push(SESSIONS_SUBDIR);
fs::create_dir_all(&dir)?; fs::create_dir_all(&dir)?;

View File

@@ -20,13 +20,15 @@
use std::time::Duration; use std::time::Duration;
use codex_core::Codex; use codex_core::Codex;
use codex_core::config::Config;
use codex_core::error::CodexErr; use codex_core::error::CodexErr;
use codex_core::protocol::AgentMessageEvent; use codex_core::protocol::AgentMessageEvent;
use codex_core::protocol::ErrorEvent; use codex_core::protocol::ErrorEvent;
use codex_core::protocol::EventMsg; use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem; use codex_core::protocol::InputItem;
use codex_core::protocol::Op; use codex_core::protocol::Op;
mod test_support;
use tempfile::TempDir;
use test_support::load_default_config_for_test;
use tokio::sync::Notify; use tokio::sync::Notify;
use tokio::time::timeout; use tokio::time::timeout;
@@ -57,7 +59,8 @@ async fn spawn_codex() -> Result<Codex, CodexErr> {
std::env::set_var("OPENAI_STREAM_MAX_RETRIES", "2"); std::env::set_var("OPENAI_STREAM_MAX_RETRIES", "2");
} }
let config = Config::load_default_config_for_test(); let codex_home = TempDir::new().unwrap();
let config = load_default_config_for_test(&codex_home);
let (agent, _init_id) = Codex::spawn(config, std::sync::Arc::new(Notify::new())).await?; let (agent, _init_id) = Codex::spawn(config, std::sync::Arc::new(Notify::new())).await?;
Ok(agent) Ok(agent)

View File

@@ -2,13 +2,15 @@ use std::time::Duration;
use codex_core::Codex; use codex_core::Codex;
use codex_core::ModelProviderInfo; use codex_core::ModelProviderInfo;
use codex_core::config::Config;
use codex_core::exec::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use codex_core::exec::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use codex_core::protocol::ErrorEvent; use codex_core::protocol::ErrorEvent;
use codex_core::protocol::EventMsg; use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem; use codex_core::protocol::InputItem;
use codex_core::protocol::Op; use codex_core::protocol::Op;
mod test_support;
use serde_json::Value; use serde_json::Value;
use tempfile::TempDir;
use test_support::load_default_config_for_test;
use tokio::time::timeout; use tokio::time::timeout;
use wiremock::Match; use wiremock::Match;
use wiremock::Mock; use wiremock::Mock;
@@ -108,7 +110,8 @@ async fn keeps_previous_response_id_between_tasks() {
}; };
// Init session // Init session
let mut config = Config::load_default_config_for_test(); let codex_home = TempDir::new().unwrap();
let mut config = load_default_config_for_test(&codex_home);
config.model_provider = model_provider; config.model_provider = model_provider;
let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new()); let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new());
let (codex, _init_id) = Codex::spawn(config, ctrl_c.clone()).await.unwrap(); let (codex, _init_id) = Codex::spawn(config, ctrl_c.clone()).await.unwrap();

View File

@@ -5,10 +5,12 @@ use std::time::Duration;
use codex_core::Codex; use codex_core::Codex;
use codex_core::ModelProviderInfo; use codex_core::ModelProviderInfo;
use codex_core::config::Config;
use codex_core::exec::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use codex_core::exec::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use codex_core::protocol::InputItem; use codex_core::protocol::InputItem;
use codex_core::protocol::Op; use codex_core::protocol::Op;
mod test_support;
use tempfile::TempDir;
use test_support::load_default_config_for_test;
use tokio::time::timeout; use tokio::time::timeout;
use wiremock::Mock; use wiremock::Mock;
use wiremock::MockServer; use wiremock::MockServer;
@@ -96,7 +98,8 @@ async fn retries_on_early_close() {
}; };
let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new()); let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new());
let mut config = Config::load_default_config_for_test(); let codex_home = TempDir::new().unwrap();
let mut config = load_default_config_for_test(&codex_home);
config.model_provider = model_provider; config.model_provider = model_provider;
let (codex, _init_id) = Codex::spawn(config, ctrl_c).await.unwrap(); let (codex, _init_id) = Codex::spawn(config, ctrl_c).await.unwrap();

View File

@@ -0,0 +1,23 @@
#![allow(clippy::expect_used)]
// Helpers shared by the integration tests. These are located inside the
// `tests/` tree on purpose so they never become part of the public API surface
// of the `codex-core` crate.
use tempfile::TempDir;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::ConfigToml;
/// Returns a default `Config` whose on-disk state is confined to the provided
/// temporary directory. Using a per-test directory keeps tests hermetic and
/// avoids clobbering a developers real `~/.codex`.
pub fn load_default_config_for_test(codex_home: &TempDir) -> Config {
Config::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides::default(),
codex_home.path().to_path_buf(),
)
.expect("defaults for test should always succeed")
}

View File

@@ -69,7 +69,7 @@ pub fn run_main(cli: Cli) -> std::io::Result<()> {
} }
}; };
let log_dir = codex_core::config::log_dir()?; let log_dir = codex_core::config::log_dir(&config)?;
std::fs::create_dir_all(&log_dir)?; std::fs::create_dir_all(&log_dir)?;
// Open (or create) your log file, appending to it. // Open (or create) your log file, appending to it.
let mut log_file_opts = OpenOptions::new(); let mut log_file_opts = OpenOptions::new();