fix(core/config): prefer ~/.code writes while reading legacy state

This commit is contained in:
James Peter
2025-09-18 13:30:11 +10:00
parent d97eaaf242
commit 262979020a
8 changed files with 120 additions and 50 deletions
+3 -1
View File
@@ -2,6 +2,7 @@ use std::future::Future;
use std::path::Path;
use std::path::PathBuf;
use codex_core::config::resolve_codex_path_for_read;
use codex_core::CODEX_APPLY_PATCH_ARG1;
#[cfg(unix)]
use std::os::unix::fs::symlink;
@@ -113,7 +114,8 @@ const ILLEGAL_ENV_VAR_PREFIX: &str = "CODEX_";
fn load_dotenv() {
// 1) Load from global ~/.code/.env (or ~/.codex/.env) first.
if let Ok(codex_home) = codex_core::config::find_codex_home() {
if let Ok(iter) = dotenvy::from_path_iter(codex_home.join(".env")) {
let global_env_path = resolve_codex_path_for_read(&codex_home, Path::new(".env"));
if let Ok(iter) = dotenvy::from_path_iter(global_env_path) {
// Global env may legitimately contain provider keys for Code usage.
set_filtered(iter);
}
+3 -1
View File
@@ -19,6 +19,7 @@ use codex_protocol::mcp_protocol::AuthMode;
use crate::token_data::TokenData;
use crate::token_data::parse_id_token;
use crate::config::resolve_codex_path_for_read;
#[derive(Debug, Clone)]
pub struct CodexAuth {
@@ -238,8 +239,9 @@ fn load_auth(
// back to AuthMode::ApiKey using the OPENAI_API_KEY environment variable
// (if it is set).
let auth_file = get_auth_file(codex_home);
let auth_read_path = resolve_codex_path_for_read(codex_home, Path::new("auth.json"));
let client = crate::default_client::create_client(originator);
let auth_dot_json = match try_read_auth_json(&auth_file) {
let auth_dot_json = match try_read_auth_json(&auth_read_path) {
Ok(auth) => auth,
// If auth.json does not exist, try to read the OPENAI_API_KEY from the
// environment variable.
+94 -39
View File
@@ -31,9 +31,10 @@ use dirs::home_dir;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use std::io::ErrorKind;
use std::sync::OnceLock;
use tempfile::NamedTempFile;
use toml::Value as TomlValue;
use toml_edit::Array as TomlArray;
@@ -284,8 +285,8 @@ pub fn load_config_as_toml_with_cli_overrides(
/// Read `CODEX_HOME/config.toml` and return it as a generic TOML value. Returns
/// an empty TOML table when the file does not exist.
pub fn load_config_as_toml(codex_home: &Path) -> std::io::Result<TomlValue> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
match std::fs::read_to_string(&config_path) {
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
match std::fs::read_to_string(&read_path) {
Ok(contents) => match toml::from_str::<TomlValue>(&contents) {
Ok(val) => Ok(val),
Err(e) => {
@@ -323,7 +324,8 @@ pub fn write_global_mcp_servers(
servers: &BTreeMap<String, McpServerConfig>,
) -> std::io::Result<()> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
let mut doc = match std::fs::read_to_string(&config_path) {
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let mut doc = match std::fs::read_to_string(&read_path) {
Ok(contents) => contents
.parse::<DocumentMut>()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?,
@@ -397,7 +399,8 @@ pub async fn persist_model_selection(
use tokio::fs;
let config_path = codex_home.join(CONFIG_TOML_FILE);
let existing = match fs::read_to_string(&config_path).await {
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let existing = match fs::read_to_string(&read_path).await {
Ok(raw) => Some(raw),
Err(err) if err.kind() == ErrorKind::NotFound => None,
Err(err) => return Err(err.into()),
@@ -473,7 +476,8 @@ pub async fn persist_model_selection(
pub fn set_project_trusted(codex_home: &Path, project_path: &Path) -> anyhow::Result<()> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
// Parse existing config if present; otherwise start a new document.
let mut doc = match std::fs::read_to_string(config_path.clone()) {
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let mut doc = match std::fs::read_to_string(&read_path) {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e.into()),
@@ -559,7 +563,8 @@ pub fn set_tui_theme_name(codex_home: &Path, theme: ThemeName) -> anyhow::Result
let config_path = codex_home.join(CONFIG_TOML_FILE);
// Parse existing config if present; otherwise start a new document.
let mut doc = match std::fs::read_to_string(config_path.clone()) {
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let mut doc = match std::fs::read_to_string(&read_path) {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e.into()),
@@ -613,7 +618,8 @@ pub fn set_tui_spinner_name(codex_home: &Path, spinner_name: &str) -> anyhow::Re
let config_path = codex_home.join(CONFIG_TOML_FILE);
// Parse existing config if present; otherwise start a new document.
let mut doc = match std::fs::read_to_string(config_path.clone()) {
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let mut doc = match std::fs::read_to_string(&read_path) {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e.into()),
@@ -645,7 +651,8 @@ pub fn set_custom_spinner(
frames: &[String],
) -> anyhow::Result<()> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
let mut doc = match std::fs::read_to_string(&config_path) {
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let mut doc = match std::fs::read_to_string(&read_path) {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e.into()),
@@ -678,7 +685,8 @@ pub fn set_custom_theme(
is_dark: Option<bool>,
) -> anyhow::Result<()> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
let mut doc = match std::fs::read_to_string(&config_path) {
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let mut doc = match std::fs::read_to_string(&read_path) {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e.into()),
@@ -742,7 +750,8 @@ pub fn set_tui_alternate_screen(codex_home: &Path, enabled: bool) -> anyhow::Res
let config_path = codex_home.join(CONFIG_TOML_FILE);
// Parse existing config if present; otherwise start a new document.
let mut doc = match std::fs::read_to_string(config_path.clone()) {
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let mut doc = match std::fs::read_to_string(&read_path) {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e.into()),
@@ -769,7 +778,8 @@ pub fn set_github_check_on_push(codex_home: &Path, enabled: bool) -> anyhow::Res
let config_path = codex_home.join(CONFIG_TOML_FILE);
// Parse existing config if present; otherwise start a new document.
let mut doc = match std::fs::read_to_string(config_path.clone()) {
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let mut doc = match std::fs::read_to_string(&read_path) {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e.into()),
@@ -802,7 +812,8 @@ pub fn set_project_access_mode(
let config_path = codex_home.join(CONFIG_TOML_FILE);
// Parse existing config if present; otherwise start a new document.
let mut doc = match std::fs::read_to_string(config_path.clone()) {
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let mut doc = match std::fs::read_to_string(&read_path) {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e.into()),
@@ -864,8 +875,8 @@ pub fn list_mcp_servers(codex_home: &Path) -> anyhow::Result<(
Vec<(String, McpServerConfig)>,
Vec<(String, McpServerConfig)>,
)> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
let doc_str = std::fs::read_to_string(&config_path).unwrap_or_default();
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let doc_str = std::fs::read_to_string(&read_path).unwrap_or_default();
let doc = doc_str.parse::<DocumentMut>().unwrap_or_else(|_| DocumentMut::new());
fn table_to_list(tbl: &toml_edit::Table) -> Vec<(String, McpServerConfig)> {
@@ -937,7 +948,8 @@ pub fn add_mcp_server(
}
let config_path = codex_home.join(CONFIG_TOML_FILE);
let mut doc = match std::fs::read_to_string(&config_path) {
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let mut doc = match std::fs::read_to_string(&read_path) {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e.into()),
@@ -994,7 +1006,8 @@ pub fn set_mcp_server_enabled(
enabled: bool,
) -> anyhow::Result<bool> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
let mut doc = match std::fs::read_to_string(&config_path) {
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let mut doc = match std::fs::read_to_string(&read_path) {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e.into()),
@@ -1731,26 +1744,76 @@ fn default_review_model() -> String {
OPENAI_DEFAULT_REVIEW_MODEL.to_string()
}
fn env_path(var: &str) -> std::io::Result<Option<PathBuf>> {
match std::env::var(var) {
Ok(val) if !val.trim().is_empty() => {
let canonical = PathBuf::from(val).canonicalize()?;
Ok(Some(canonical))
}
_ => Ok(None),
}
}
fn env_overrides_present() -> bool {
matches!(std::env::var("CODE_HOME"), Ok(ref v) if !v.trim().is_empty())
|| matches!(std::env::var("CODEX_HOME"), Ok(ref v) if !v.trim().is_empty())
}
fn legacy_codex_home_dir() -> Option<PathBuf> {
static LEGACY: OnceLock<Option<PathBuf>> = OnceLock::new();
LEGACY
.get_or_init(|| {
if env_overrides_present() {
return None;
}
let Some(home) = home_dir() else {
return None;
};
let candidate = home.join(".codex");
if path_exists(&candidate) {
Some(candidate)
} else {
None
}
})
.clone()
}
fn path_exists(path: &Path) -> bool {
std::fs::metadata(path).is_ok()
}
/// Resolve the filesystem path used for *reading* Codex state that may live in
/// a legacy `~/.codex` directory. Writes should continue targeting `codex_home`.
pub fn resolve_codex_path_for_read(codex_home: &Path, relative: &Path) -> PathBuf {
if env_overrides_present() {
return codex_home.join(relative);
}
if let Some(legacy) = legacy_codex_home_dir() {
let candidate = legacy.join(relative);
if path_exists(&candidate) {
return candidate;
}
}
codex_home.join(relative)
}
/// Returns the path to the Code/Codex configuration directory, which can be
/// specified by the `CODE_HOME` or `CODEX_HOME` environment variables. If not set,
/// defaults to `~/.code` (falling back to `~/.codex` if it exists for compatibility).
/// defaults to `~/.code` for the fork.
///
/// - If `CODE_HOME` or `CODEX_HOME` is set, the value will be canonicalized and this
/// function will Err if the path does not exist.
/// - If neither is set, this function does not verify that the directory exists.
pub fn find_codex_home() -> std::io::Result<PathBuf> {
// First check CODE_HOME for the fork
if let Ok(val) = std::env::var("CODE_HOME") {
if !val.is_empty() {
return PathBuf::from(val).canonicalize();
}
if let Some(path) = env_path("CODE_HOME")? {
return Ok(path);
}
// Fall back to CODEX_HOME for compatibility
if let Ok(val) = std::env::var("CODEX_HOME") {
if !val.is_empty() {
return PathBuf::from(val).canonicalize();
}
if let Some(path) = env_path("CODEX_HOME")? {
return Ok(path);
}
let home = home_dir().ok_or_else(|| {
@@ -1760,17 +1823,9 @@ pub fn find_codex_home() -> std::io::Result<PathBuf> {
)
})?;
// Check if ~/.codex exists for backward compatibility
let mut codex_path = home.clone();
codex_path.push(".codex");
if codex_path.exists() {
return Ok(codex_path);
}
// Otherwise use ~/.code for the fork
let mut p = home;
p.push(".code");
Ok(p)
let mut write_path = home;
write_path.push(".code");
Ok(write_path)
}
/// Returns the path to the folder where Codex logs are stored. Does not verify
+9 -4
View File
@@ -1,3 +1,4 @@
use crate::config::resolve_codex_path_for_read;
use crate::config_types::SubagentCommandConfig;
use anyhow::Result;
use std::path::Path;
@@ -99,7 +100,8 @@ fn apply_toml_edit_override_segments(
pub async fn upsert_subagent_command(codex_home: &Path, cmd: &SubagentCommandConfig) -> Result<()> {
const CONFIG_TOML_FILE: &str = "config.toml";
let config_path = codex_home.join(CONFIG_TOML_FILE);
let mut doc = match tokio::fs::read_to_string(&config_path).await {
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let mut doc = match tokio::fs::read_to_string(&read_path).await {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
tokio::fs::create_dir_all(codex_home).await?;
@@ -166,7 +168,8 @@ pub async fn upsert_subagent_command(codex_home: &Path, cmd: &SubagentCommandCon
pub async fn delete_subagent_command(codex_home: &Path, name: &str) -> Result<bool> {
const CONFIG_TOML_FILE: &str = "config.toml";
let config_path = codex_home.join(CONFIG_TOML_FILE);
let mut doc = match tokio::fs::read_to_string(&config_path).await {
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let mut doc = match tokio::fs::read_to_string(&read_path).await {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(e) => return Err(e.into()),
@@ -207,7 +210,8 @@ pub async fn upsert_agent_config(
) -> Result<()> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
let mut doc = match tokio::fs::read_to_string(&config_path).await {
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let mut doc = match tokio::fs::read_to_string(&read_path).await {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
tokio::fs::create_dir_all(codex_home).await?;
@@ -328,7 +332,8 @@ async fn persist_overrides_with_behavior(
}
let config_path = codex_home.join(CONFIG_TOML_FILE);
let read_result = tokio::fs::read_to_string(&config_path).await;
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let read_result = tokio::fs::read_to_string(&read_path).await;
let mut doc = match read_result {
Ok(contents) => contents.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
+4 -1
View File
@@ -5,6 +5,8 @@ use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use crate::config::resolve_codex_path_for_read;
pub(crate) const INTERNAL_STORAGE_FILE: &str = "internal_storage.json";
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
@@ -19,8 +21,9 @@ pub struct InternalStorage {
impl InternalStorage {
pub fn load(codex_home: &Path) -> Self {
let storage_path = codex_home.join(INTERNAL_STORAGE_FILE);
let read_path = resolve_codex_path_for_read(codex_home, Path::new(INTERNAL_STORAGE_FILE));
match std::fs::read_to_string(&storage_path) {
match std::fs::read_to_string(&read_path) {
Ok(serialized) => match serde_json::from_str::<Self>(&serialized) {
Ok(mut storage) => {
storage.storage_path = storage_path;
+2 -2
View File
@@ -17,6 +17,7 @@ use uuid::Uuid;
use super::SESSIONS_SUBDIR;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::RolloutLine;
use crate::config::resolve_codex_path_for_read;
/// Returned page of conversation summaries.
#[derive(Debug, Default, PartialEq)]
@@ -90,8 +91,7 @@ pub(crate) async fn get_conversations(
page_size: usize,
cursor: Option<&Cursor>,
) -> io::Result<ConversationsPage> {
let mut root = codex_home.to_path_buf();
root.push(SESSIONS_SUBDIR);
let root = resolve_codex_path_for_read(codex_home, Path::new(SESSIONS_SUBDIR));
if !root.exists() {
return Ok(ConversationsPage {
+2 -1
View File
@@ -111,6 +111,7 @@ use crate::user_approval_widget::ApprovalRequest;
use codex_ansi_escape::ansi_escape_line;
use codex_browser::BrowserManager;
use codex_core::config::find_codex_home;
use codex_core::config::resolve_codex_path_for_read;
use codex_core::config::set_github_check_on_push;
use codex_file_search::FileMatch;
use codex_protocol::models::ContentItem;
@@ -138,7 +139,7 @@ struct CachedConnection {
async fn read_cached_connection() -> Option<(Option<u16>, Option<String>)> {
let codex_home = find_codex_home().ok()?;
let path = codex_home.join("cache.json");
let path = resolve_codex_path_for_read(&codex_home, std::path::Path::new("cache.json"));
let bytes = tokio::fs::read(path).await.ok()?;
let parsed: CachedConnection = serde_json::from_slice(&bytes).ok()?;
Some((parsed.port, parsed.ws))
+3 -1
View File
@@ -7,12 +7,14 @@ use serde::Serialize;
use std::path::Path;
use std::path::PathBuf;
use codex_core::config::resolve_codex_path_for_read;
use codex_core::config::Config;
use codex_core::default_client::create_client;
pub fn get_upgrade_version(config: &Config) -> Option<String> {
let version_file = version_filepath(config);
let info = read_version_info(&version_file).ok();
let read_path = resolve_codex_path_for_read(&config.codex_home, Path::new(VERSION_FILENAME));
let info = read_version_info(&read_path).ok();
let originator = config.responses_originator_header.clone();
// Always refresh the cached latest version in the background so TUI startup