Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e0aaeba
fix: trust-gate project hooks and exec policies
viyatb-oai Mar 14, 2026
66880f3
fix: update session start hook test cwd type
viyatb-oai Mar 27, 2026
3b7e933
style: format trust directory onboarding text
viyatb-oai Mar 27, 2026
959e719
Merge branch 'main' into codex/viyatb/trusted-project-config-gating
viyatb-oai Mar 27, 2026
2e23ca8
fix: tolerate windows trust path aliases
viyatb-oai Mar 28, 2026
d3e4866
fix: normalize review exit template newlines
viyatb-oai Mar 31, 2026
db2fb27
Merge remote-tracking branch 'origin/main' into codex/viyatb/pr14718-fix
viyatb-oai Mar 31, 2026
c90928c
test: satisfy argument comment lint in config loader tests
viyatb-oai Mar 31, 2026
36eb261
Merge remote-tracking branch 'origin/main' into codex/viyatb/pr14718-fix
viyatb-oai Apr 6, 2026
382471a
Merge remote-tracking branch 'origin/main' into codex/viyatb/pr14718-fix
viyatb-oai Apr 7, 2026
62fdddb
fix: update exec policy tests for config refactor
viyatb-oai Apr 7, 2026
dd08f5e
fix: update app server auth store import
viyatb-oai Apr 7, 2026
66ae65a
fix: stabilize config trust regression tests
viyatb-oai Apr 7, 2026
8010226
test: normalize project trust fixtures
viyatb-oai Apr 7, 2026
3555e24
Merge branch 'main' into codex/viyatb/trusted-project-config-gating
viyatb-oai Apr 7, 2026
fe460cd
test: skip unsupported windows hook trust checks
viyatb-oai Apr 7, 2026
ab48990
test: ignore windows-only unsupported hook coverage
viyatb-oai Apr 7, 2026
e33ebb4
fix: restore deterministic project trust lookups
viyatb-oai Apr 11, 2026
b2f0342
Merge remote-tracking branch 'origin/main' into codex/viyatb/pr14718-fix
viyatb-oai Apr 11, 2026
a0bb307
Merge branch 'main' into codex/viyatb/trusted-project-config-gating
viyatb-oai Apr 11, 2026
95f8b4e
Merge branch 'main' into codex/viyatb/trusted-project-config-gating
viyatb-oai Apr 11, 2026
13f7f05
chore: merge origin/main into PR 14718
viyatb-oai Apr 14, 2026
032c32d
chore: merge origin/main into PR 14718
viyatb-oai Apr 14, 2026
bf62131
chore: drop unrelated app-server test fixes
viyatb-oai Apr 14, 2026
c86f766
chore: merge origin/main into PR 14718
viyatb-oai Apr 14, 2026
363f1cc
chore: merge origin/main into PR 14718
viyatb-oai Apr 16, 2026
2855271
fix: use existing app-server connection id
viyatb-oai Apr 16, 2026
f2e0345
chore: merge origin/main into trusted config gate
viyatb-oai Apr 17, 2026
99d4fda
Merge remote-tracking branch 'origin/main' into codex/viyatb/pr14718-fix
viyatb-oai Apr 17, 2026
f002f3f
docs: preserve project trust key comment
viyatb-oai Apr 17, 2026
66c2395
refactor: collapse project trust key wrapper
viyatb-oai Apr 17, 2026
a62c0d4
Merge branch 'main' into codex/viyatb/trusted-project-config-gating
viyatb-oai Apr 17, 2026
91baa13
test: use canonical trust keys in app-server assertions
viyatb-oai Apr 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 11 additions & 16 deletions codex-rs/app-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,30 +280,25 @@ fn project_config_warning(config: &Config) -> Option<ConfigWarningNotification>
ConfigLayerStackOrdering::LowestPrecedenceFirst,
/*include_disabled*/ true,
) {
if !matches!(layer.name, ConfigLayerSource::Project { .. })
|| layer.disabled_reason.is_none()
{
let ConfigLayerSource::Project { dot_codex_folder } = &layer.name else {
continue;
}
if let ConfigLayerSource::Project { dot_codex_folder } = &layer.name {
disabled_folders.push((
dot_codex_folder.as_path().display().to_string(),
layer
.disabled_reason
.as_ref()
.map(ToString::to_string)
.unwrap_or_else(|| "config.toml is disabled.".to_string()),
));
}
};
let Some(disabled_reason) = &layer.disabled_reason else {
continue;
};
disabled_folders.push((
dot_codex_folder.as_path().display().to_string(),
disabled_reason.clone(),
));
}

if disabled_folders.is_empty() {
return None;
}

let mut message = concat!(
"Project config.toml files are disabled in the following folders. ",
"Settings in those files are ignored, but skills and exec policies still load.\n",
"Project-local config, hooks, and exec policies are disabled in the following folders ",
"until the project is trusted, but skills still load.\n",
)
.to_string();
for (index, (folder, reason)) in disabled_folders.iter().enumerate() {
Expand Down
25 changes: 7 additions & 18 deletions codex-rs/app-server/tests/suite/v2/thread_start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use codex_app_server_protocol::ThreadStatus;
use codex_app_server_protocol::ThreadStatusChangedNotification;
use codex_config::types::AuthCredentialsStoreMode;
use codex_core::config::set_project_trust_level;
use codex_core::config_loader::project_trust_key;
use codex_exec_server::LOCAL_FS;
use codex_git_utils::resolve_root_git_project_for_trust;
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
Expand Down Expand Up @@ -722,7 +723,8 @@ model_reasoning_effort = "high"
let trusted_root = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &workspace_abs)
.await
.unwrap_or(workspace_abs);
assert!(config_toml.contains(&persisted_trust_path(trusted_root.as_path())));
let trusted_root_key = project_trust_key(trusted_root.as_path());
assert!(config_toml.contains(&trusted_root_key));
assert!(config_toml.contains("trust_level = \"trusted\""));

Ok(())
Expand Down Expand Up @@ -761,8 +763,10 @@ async fn thread_start_with_nested_git_cwd_trusts_repo_root() -> Result<()> {
let trusted_root = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &nested_abs)
.await
.expect("git root should resolve");
assert!(config_toml.contains(&persisted_trust_path(trusted_root.as_path())));
assert!(!config_toml.contains(&persisted_trust_path(&nested)));
let trusted_root_key = project_trust_key(trusted_root.as_path());
let nested_key = project_trust_key(&nested);
assert!(config_toml.contains(&trusted_root_key));
assert!(!config_toml.contains(&nested_key));

Ok(())
}
Expand Down Expand Up @@ -856,21 +860,6 @@ fn create_config_toml_without_approval_policy(
)
}

fn persisted_trust_path(project_path: &Path) -> String {
let project_path =
std::fs::canonicalize(project_path).unwrap_or_else(|_| project_path.to_path_buf());
let project_path = project_path.display().to_string();

if let Some(project_path) = project_path.strip_prefix(r"\\?\UNC\") {
return format!(r"\\{project_path}");
}

project_path
.strip_prefix(r"\\?\")
.unwrap_or(&project_path)
.to_string()
}

fn create_config_toml_with_optional_approval_policy(
codex_home: &Path,
server_uri: &str,
Expand Down
70 changes: 50 additions & 20 deletions codex-rs/config/src/config_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -684,25 +684,21 @@ impl ConfigToml {
resolved_cwd: &Path,
repo_root: Option<&Path>,
) -> Option<ProjectConfig> {
let projects = self.projects.clone().unwrap_or_default();
let projects = self.projects.as_ref()?;

let resolved_cwd_key = project_trust_key(resolved_cwd);
let resolved_cwd_raw_key = resolved_cwd.to_string_lossy().to_string();
if let Some(project_config) = projects
.get(&resolved_cwd_key)
.or_else(|| projects.get(&resolved_cwd_raw_key))
{
return Some(project_config.clone());
for normalized_cwd in normalized_project_lookup_keys(resolved_cwd) {
if let Some(project_config) = project_config_for_lookup_key(projects, &normalized_cwd) {
return Some(project_config);
}
}

if let Some(repo_root) = repo_root {
let repo_root_key = project_trust_key(repo_root);
let repo_root_raw_key = repo_root.to_string_lossy().to_string();
if let Some(project_config_for_root) = projects
.get(&repo_root_key)
.or_else(|| projects.get(&repo_root_raw_key))
{
return Some(project_config_for_root.clone());
for normalized_repo_root in normalized_project_lookup_keys(repo_root) {
if let Some(project_config_for_root) =
project_config_for_lookup_key(projects, &normalized_repo_root)
{
return Some(project_config_for_root);
}
}
}

Expand Down Expand Up @@ -734,11 +730,45 @@ impl ConfigToml {
/// Canonicalize the path and convert it to a string to be used as a key in the
/// projects trust map. On Windows, strips UNC, when possible, to try to ensure
/// that different paths that point to the same location have the same key.
fn project_trust_key(project_path: &Path) -> String {
normalize_for_path_comparison(project_path)
.unwrap_or_else(|_| project_path.to_path_buf())
.to_string_lossy()
.to_string()
fn normalized_project_lookup_keys(path: &Path) -> Vec<String> {
let normalized_path = normalize_project_lookup_key(path.to_string_lossy().to_string());
let normalized_canonical_path = normalize_project_lookup_key(
normalize_for_path_comparison(path)
.unwrap_or_else(|_| path.to_path_buf())
.to_string_lossy()
.to_string(),
);
if normalized_path == normalized_canonical_path {
vec![normalized_canonical_path]
} else {
vec![normalized_canonical_path, normalized_path]
}
}

fn normalize_project_lookup_key(key: String) -> String {
if cfg!(windows) {
key.to_ascii_lowercase()
} else {
key
}
}

fn project_config_for_lookup_key(
projects: &HashMap<String, ProjectConfig>,
lookup_key: &str,
) -> Option<ProjectConfig> {
if let Some(project_config) = projects.get(lookup_key) {
return Some(project_config.clone());
}

let mut normalized_matches: Vec<_> = projects
.iter()
.filter(|(key, _)| normalize_project_lookup_key((*key).clone()) == lookup_key)
.collect();
normalized_matches.sort_by(|(left, _), (right, _)| left.cmp(right));
normalized_matches
.first()
.map(|(_, project_config)| (**project_config).clone())
}

pub fn validate_reserved_model_provider_ids(
Expand Down
37 changes: 34 additions & 3 deletions codex-rs/core/src/config/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::config::edit::ConfigEdit;
use crate::config::edit::ConfigEditsBuilder;
use crate::config::edit::apply_blocking;
use crate::config_loader::RequirementSource;
use crate::config_loader::project_trust_key;
use crate::plugins::PluginsManager;
use assert_matches::assert_matches;
use codex_config::CONFIG_TOML_FILE;
Expand Down Expand Up @@ -5449,7 +5450,9 @@ model = "foo""#;

// Since we created the [projects] table as part of migration, it is kept implicit.
// Expect explicit per-project tables, preserving prior entries and appending the new one.
let expected = r#"toplevel = "baz"
let new_project_key = project_trust_key(new_project);
let expected = format!(
r#"toplevel = "baz"
model = "foo"

[projects."/Users/mbolin/code/codex4"]
Expand All @@ -5459,14 +5462,42 @@ foo = "bar"
[projects."/Users/mbolin/code/codex3"]
trust_level = "trusted"

[projects."/Users/mbolin/code/codex2"]
[projects."{new_project_key}"]
trust_level = "trusted"
"#;
"#
);
assert_eq!(contents, expected);

Ok(())
}

#[cfg(unix)]
#[tokio::test]
async fn active_project_does_not_match_configured_alias_for_canonical_cwd() -> anyhow::Result<()> {
let tmp = tempdir()?;
let project_root = tmp.path().join("project");
let alias_root = tmp.path().join("project_alias");
std::fs::create_dir_all(&project_root)?;
std::os::unix::fs::symlink(&project_root, &alias_root)?;

let config = ConfigToml {
projects: Some(HashMap::from([(
alias_root.to_string_lossy().to_string(),
ProjectConfig {
trust_level: Some(TrustLevel::Trusted),
},
)])),
..Default::default()
};

assert_eq!(
config.get_active_project(&project_root, /*repo_root*/ None),
None
);

Ok(())
}

#[test]
fn test_set_default_oss_provider() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
Expand Down
Loading
Loading