mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Skip loading codex home as project layer (#10207)
Summary: - Fixes issue #9932: https://github.com/openai/codex/issues/9932 - Prevents `$CODEX_HOME` (typically `~/.codex`) from being discovered as a project `.codex` layer by skipping it during project layer traversal. We compare both normalized absolute paths and best-effort canonicalized paths to handle symlinks. - Adds regression tests for home-directory invocation and for the case where `CODEX_HOME` points to a project `.codex` directory (e.g., worktrees/editor integrations). Testing: - `cargo build -p codex-cli --bin codex` - `cargo build -p codex-rmcp-client --bin test_stdio_server` - `cargo test -p codex-core` - `cargo test --all-features` - Manual: ran `target/debug/codex` from `~` and confirmed the disabled-folder warning and trust prompt no longer appear.
This commit is contained in:
@@ -25,6 +25,7 @@ use codex_protocol::config_types::TrustLevel;
|
|||||||
use codex_protocol::protocol::AskForApproval;
|
use codex_protocol::protocol::AskForApproval;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||||
|
use dunce::canonicalize as normalize_path;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -236,6 +237,7 @@ pub async fn load_config_layers_state(
|
|||||||
&cwd,
|
&cwd,
|
||||||
&project_trust_context.project_root,
|
&project_trust_context.project_root,
|
||||||
&project_trust_context,
|
&project_trust_context,
|
||||||
|
codex_home,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
layers.extend(project_layers);
|
layers.extend(project_layers);
|
||||||
@@ -671,7 +673,11 @@ async fn load_project_layers(
|
|||||||
cwd: &AbsolutePathBuf,
|
cwd: &AbsolutePathBuf,
|
||||||
project_root: &AbsolutePathBuf,
|
project_root: &AbsolutePathBuf,
|
||||||
trust_context: &ProjectTrustContext,
|
trust_context: &ProjectTrustContext,
|
||||||
|
codex_home: &Path,
|
||||||
) -> io::Result<Vec<ConfigLayerEntry>> {
|
) -> io::Result<Vec<ConfigLayerEntry>> {
|
||||||
|
let codex_home_abs = AbsolutePathBuf::from_absolute_path(codex_home)?;
|
||||||
|
let codex_home_normalized =
|
||||||
|
normalize_path(codex_home_abs.as_path()).unwrap_or_else(|_| codex_home_abs.to_path_buf());
|
||||||
let mut dirs = cwd
|
let mut dirs = cwd
|
||||||
.as_path()
|
.as_path()
|
||||||
.ancestors()
|
.ancestors()
|
||||||
@@ -702,6 +708,11 @@ async fn load_project_layers(
|
|||||||
let layer_dir = AbsolutePathBuf::from_absolute_path(dir)?;
|
let layer_dir = AbsolutePathBuf::from_absolute_path(dir)?;
|
||||||
let decision = trust_context.decision_for_dir(&layer_dir);
|
let decision = trust_context.decision_for_dir(&layer_dir);
|
||||||
let dot_codex_abs = AbsolutePathBuf::from_absolute_path(&dot_codex)?;
|
let dot_codex_abs = AbsolutePathBuf::from_absolute_path(&dot_codex)?;
|
||||||
|
let dot_codex_normalized =
|
||||||
|
normalize_path(dot_codex_abs.as_path()).unwrap_or_else(|_| dot_codex_abs.to_path_buf());
|
||||||
|
if dot_codex_abs == codex_home_abs || dot_codex_normalized == codex_home_normalized {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let config_file = dot_codex_abs.join(CONFIG_TOML_FILE)?;
|
let config_file = dot_codex_abs.join(CONFIG_TOML_FILE)?;
|
||||||
match tokio::fs::read_to_string(&config_file).await {
|
match tokio::fs::read_to_string(&config_file).await {
|
||||||
Ok(contents) => {
|
Ok(contents) => {
|
||||||
|
|||||||
@@ -755,6 +755,101 @@ async fn project_layer_is_added_when_dot_codex_exists_without_config_toml() -> s
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn codex_home_is_not_loaded_as_project_layer_from_home_dir() -> std::io::Result<()> {
|
||||||
|
let tmp = tempdir()?;
|
||||||
|
let home_dir = tmp.path().join("home");
|
||||||
|
let codex_home = home_dir.join(".codex");
|
||||||
|
tokio::fs::create_dir_all(&codex_home).await?;
|
||||||
|
tokio::fs::write(codex_home.join(CONFIG_TOML_FILE), "foo = \"user\"\n").await?;
|
||||||
|
|
||||||
|
let cwd = AbsolutePathBuf::from_absolute_path(&home_dir)?;
|
||||||
|
let layers = load_config_layers_state(
|
||||||
|
&codex_home,
|
||||||
|
Some(cwd),
|
||||||
|
&[] as &[(String, TomlValue)],
|
||||||
|
LoaderOverrides::default(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let project_layers: Vec<_> = layers
|
||||||
|
.get_layers(
|
||||||
|
super::ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. }))
|
||||||
|
.collect();
|
||||||
|
let expected: Vec<&ConfigLayerEntry> = Vec::new();
|
||||||
|
assert_eq!(expected, project_layers);
|
||||||
|
assert_eq!(
|
||||||
|
layers.effective_config().get("foo"),
|
||||||
|
Some(&TomlValue::String("user".to_string()))
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn codex_home_within_project_tree_is_not_double_loaded() -> std::io::Result<()> {
|
||||||
|
let tmp = tempdir()?;
|
||||||
|
let project_root = tmp.path().join("project");
|
||||||
|
let nested = project_root.join("child");
|
||||||
|
let project_dot_codex = project_root.join(".codex");
|
||||||
|
let nested_dot_codex = nested.join(".codex");
|
||||||
|
|
||||||
|
tokio::fs::create_dir_all(&nested_dot_codex).await?;
|
||||||
|
tokio::fs::create_dir_all(project_root.join(".git")).await?;
|
||||||
|
tokio::fs::write(nested_dot_codex.join(CONFIG_TOML_FILE), "foo = \"child\"\n").await?;
|
||||||
|
|
||||||
|
tokio::fs::create_dir_all(&project_dot_codex).await?;
|
||||||
|
make_config_for_test(&project_dot_codex, &project_root, TrustLevel::Trusted, None).await?;
|
||||||
|
let user_config_path = project_dot_codex.join(CONFIG_TOML_FILE);
|
||||||
|
let user_config_contents = tokio::fs::read_to_string(&user_config_path).await?;
|
||||||
|
tokio::fs::write(
|
||||||
|
&user_config_path,
|
||||||
|
format!("foo = \"user\"\n{user_config_contents}"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let cwd = AbsolutePathBuf::from_absolute_path(&nested)?;
|
||||||
|
let layers = load_config_layers_state(
|
||||||
|
&project_dot_codex,
|
||||||
|
Some(cwd),
|
||||||
|
&[] as &[(String, TomlValue)],
|
||||||
|
LoaderOverrides::default(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let project_layers: Vec<_> = layers
|
||||||
|
.get_layers(
|
||||||
|
super::ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. }))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let child_config: TomlValue = toml::from_str("foo = \"child\"\n").expect("parse child config");
|
||||||
|
assert_eq!(
|
||||||
|
vec![&ConfigLayerEntry {
|
||||||
|
name: super::ConfigLayerSource::Project {
|
||||||
|
dot_codex_folder: AbsolutePathBuf::from_absolute_path(&nested_dot_codex)?,
|
||||||
|
},
|
||||||
|
config: child_config.clone(),
|
||||||
|
version: version_for_toml(&child_config),
|
||||||
|
disabled_reason: None,
|
||||||
|
}],
|
||||||
|
project_layers
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
layers.effective_config().get("foo"),
|
||||||
|
Some(&TomlValue::String("child".to_string()))
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn project_layers_disabled_when_untrusted_or_unknown() -> std::io::Result<()> {
|
async fn project_layers_disabled_when_untrusted_or_unknown() -> std::io::Result<()> {
|
||||||
let tmp = tempdir()?;
|
let tmp = tempdir()?;
|
||||||
|
|||||||
Reference in New Issue
Block a user