Merge branch 'main' into codex/validate-codex-issue-10216

This commit is contained in:
Eric Traut
2026-01-30 12:45:46 -08:00
committed by GitHub
5 changed files with 115 additions and 22 deletions

View File

@@ -25,6 +25,7 @@ use codex_protocol::config_types::TrustLevel;
use codex_protocol::protocol::AskForApproval;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use dunce::canonicalize as normalize_path;
use serde::Deserialize;
use std::io;
use std::path::Path;
@@ -236,6 +237,7 @@ pub async fn load_config_layers_state(
&cwd,
&project_trust_context.project_root,
&project_trust_context,
codex_home,
)
.await?;
layers.extend(project_layers);
@@ -671,7 +673,11 @@ async fn load_project_layers(
cwd: &AbsolutePathBuf,
project_root: &AbsolutePathBuf,
trust_context: &ProjectTrustContext,
codex_home: &Path,
) -> 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
.as_path()
.ancestors()
@@ -702,6 +708,11 @@ async fn load_project_layers(
let layer_dir = AbsolutePathBuf::from_absolute_path(dir)?;
let decision = trust_context.decision_for_dir(&layer_dir);
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)?;
match tokio::fs::read_to_string(&config_file).await {
Ok(contents) => {

View File

@@ -755,6 +755,101 @@ async fn project_layer_is_added_when_dot_codex_exists_without_config_toml() -> s
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]
async fn project_layers_disabled_when_untrusted_or_unknown() -> std::io::Result<()> {
let tmp = tempdir()?;

View File

@@ -32,7 +32,6 @@ Actions that gather truth, reduce ambiguity, or validate feasibility without cha
Actions that implement the plan or change repo-tracked state. Examples:
* Editing or writing files
* Generating, updating, or accepting snapshots
* Running formatters or linters that rewrite files
* Applying patches, migrations, or codegen that updates repo-tracked files
* Side-effectful commands whose purpose is to carry out the plan rather than refine it
@@ -60,17 +59,13 @@ Do not ask questions that can be answered from the repo or system (for example,
Every assistant turn MUST be exactly one of:
A) a `request_user_input` tool call (questions/options only), OR
B) a non-final status update with no questions and no plan content, OR
C) the final output: a titled, plan-only document.
B) the final output: a titled, plan-only document.
Rules:
* No questions in free text (only via `request_user_input`).
* Never mix a `request_user_input` call with plan content.
* Status updates must not include questions or plan content.
* Internal tool/repo exploration is allowed privately before A, B, or C.
Status updates should be frequent during exploration. Provide 1-2 sentence updates that summarize discoveries, assumption changes, or why you are changing direction. Use Parallel tools for exploration.
* Internal tool/repo exploration is allowed privately before A or B.
## Ask a lot, but never ask trivia
@@ -113,19 +108,15 @@ When you present the official plan, wrap it in a `<proposed_plan>` block so the
Example:
<proposed_plan>
# Plan title
- Step 1
- Step 2
plan content
</proposed_plan>
The final plan must be plan-only and include:
plan content should be human and agent digestible. The final plan must be plan-only and include:
* A clear title
* A TL;DR section (35 bullets)
* Exact file paths to change
* Exact structures or shapes to introduce or modify
* Exact function, method, type, and variable names and signatures
* Test cases
* tldr section. don't necessary call it tldr.
* Important changes or additions of signatures, structs, types.
* Test cases and scenarios
* Explicit assumptions and defaults chosen where needed
Do not ask "should I proceed?" in the final output. The user can easily switch out of Plan mode and request implementation if you have included a `<proposed_plan>` block in your response. Alternatively, they can decide to stay in Plan mode and continue refining the plan.

View File

@@ -1154,6 +1154,7 @@ impl App {
return Ok(AppExitInfo {
token_usage: app.token_usage(),
thread_id: app.chat_widget.thread_id(),
thread_name: app.chat_widget.thread_name(),
update_action: app.pending_update_action,
exit_reason,
});

View File

@@ -40,10 +40,5 @@ pub fn user_message_bg(terminal_bg: (u8, u8, u8)) -> Color {
#[allow(clippy::disallowed_methods)]
pub fn proposed_plan_bg(terminal_bg: (u8, u8, u8)) -> Color {
let (top, alpha) = if is_light(terminal_bg) {
((0, 110, 150), 0.08)
} else {
((80, 170, 220), 0.2)
};
best_color(blend(top, terminal_bg, alpha))
user_message_bg(terminal_bg)
}