Compare commits

...

1 Commits

Author SHA1 Message Date
Ahmed Ibrahim
67ac8c6cc0 Warn on malformed disabled project config 2026-05-20 09:56:28 -07:00
2 changed files with 252 additions and 0 deletions

View File

@@ -906,6 +906,13 @@ fn project_ignored_config_keys_warning(
)
}
fn malformed_disabled_project_config_warning(config_path: &Path, err: &toml::de::Error) -> String {
format!(
"Ignored malformed project-local config in {} because this project is not trusted: {err}",
config_path.display()
)
}
async fn project_trust_context(
fs: &dyn ExecutorFileSystem,
merged_config: &TomlValue,
@@ -1182,6 +1189,10 @@ async fn load_project_layers(
),
));
}
startup_warnings.push(malformed_disabled_project_config_warning(
config_file.as_path(),
&e,
));
layers.push(project_layer_entry(
&dot_codex_abs,
TomlValue::Table(toml::map::Map::new()),
@@ -1208,6 +1219,7 @@ async fn load_project_layers(
config,
hooks_config_folder_override.as_ref(),
decision.is_trusted(),
&mut startup_warnings,
)
.await?;
if disabled_reason.is_none() && !ignored_project_config_keys.is_empty() {
@@ -1234,6 +1246,7 @@ async fn load_project_layers(
TomlValue::Table(toml::map::Map::new()),
hooks_config_folder_override.as_ref(),
decision.is_trusted(),
&mut startup_warnings,
)
.await?;
layers.push(project_layer_entry(
@@ -1266,6 +1279,7 @@ async fn merge_root_checkout_project_hooks(
mut config: TomlValue,
hooks_config_folder_override: Option<&AbsolutePathBuf>,
is_trusted: bool,
startup_warnings: &mut Vec<String>,
) -> io::Result<TomlValue> {
let Some(hooks_config_folder) = hooks_config_folder_override else {
return Ok(config);
@@ -1288,6 +1302,10 @@ async fn merge_root_checkout_project_hooks(
),
));
}
startup_warnings.push(malformed_disabled_project_config_warning(
hooks_config_file.as_path(),
&err,
));
TomlValue::Table(toml::map::Map::new())
}
};

View File

@@ -1830,6 +1830,76 @@ async fn linked_worktree_project_layers_use_root_repo_hooks_without_worktree_con
Ok(())
}
#[tokio::test]
async fn malformed_linked_worktree_root_hooks_config_warns_when_project_is_disabled()
-> std::io::Result<()> {
let tmp = tempdir()?;
let repo_root = tmp.path().join("repo");
let root_dot_codex = repo_root.join(".codex");
let worktree_root = tmp.path().join("worktree");
tokio::fs::create_dir_all(&root_dot_codex).await?;
tokio::fs::create_dir_all(worktree_root.join(".codex")).await?;
write_linked_worktree_pointer(&repo_root, &worktree_root).await?;
let root_config_path = root_dot_codex.join(CONFIG_TOML_FILE);
tokio::fs::write(
&root_config_path,
r#"[hooks
"#,
)
.await?;
let codex_home = tmp.path().join("home");
tokio::fs::create_dir_all(&codex_home).await?;
make_config_for_test(
&codex_home,
&repo_root,
TrustLevel::Untrusted,
/*project_root_markers*/ None,
)
.await?;
let cwd = AbsolutePathBuf::from_absolute_path(&worktree_root)?;
let layers = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home,
Some(cwd),
&[] as &[(String, TomlValue)],
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
)
.await?;
let project_layers: Vec<_> = layers
.get_layers(
ConfigLayerStackOrdering::HighestPrecedenceFirst,
/*include_disabled*/ true,
)
.into_iter()
.filter(|layer| matches!(layer.name, ConfigLayerSource::Project { .. }))
.collect();
assert_eq!(project_layers.len(), 1);
assert!(
project_layers[0].disabled_reason.is_some(),
"expected malformed linked worktree root hooks config layer to be disabled"
);
assert_eq!(project_hook_command(project_layers[0]), None);
let warnings = layers
.startup_warnings()
.expect("expected malformed linked worktree root hooks config warning");
assert_eq!(warnings.len(), 1);
assert!(
warnings[0].contains(&format!(
"Ignored malformed project-local config in {} because this project is not trusted:",
root_config_path.display()
)),
"warning should explain that malformed linked worktree hooks config was ignored"
);
Ok(())
}
#[tokio::test]
async fn nested_project_root_markers_do_not_redirect_regular_repo_hooks() -> std::io::Result<()> {
let tmp = tempdir()?;
@@ -2328,6 +2398,170 @@ profile = "ignored"
Ok(())
}
#[tokio::test]
async fn malformed_project_config_warns_when_disabled_and_fails_when_trusted() -> std::io::Result<()>
{
let tmp = tempdir()?;
let project_root = tmp.path().join("project");
let nested = project_root.join("child");
let dot_codex = nested.join(".codex");
tokio::fs::create_dir_all(&dot_codex).await?;
let project_config_path = dot_codex.join(CONFIG_TOML_FILE);
tokio::fs::write(
&project_config_path,
r#"foo = "child
"#,
)
.await?;
let cwd = AbsolutePathBuf::from_absolute_path(&nested)?;
let codex_home_untrusted = tmp.path().join("home_untrusted_malformed");
tokio::fs::create_dir_all(&codex_home_untrusted).await?;
make_config_for_test(
&codex_home_untrusted,
&project_root,
TrustLevel::Untrusted,
/*project_root_markers*/ None,
)
.await?;
let untrusted_config_path = codex_home_untrusted.join(CONFIG_TOML_FILE);
let untrusted_config_contents = tokio::fs::read_to_string(&untrusted_config_path).await?;
tokio::fs::write(
&untrusted_config_path,
format!(
r#"foo = "user"
{untrusted_config_contents}"#
),
)
.await?;
let layers_untrusted = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home_untrusted,
Some(cwd.clone()),
&[] as &[(String, TomlValue)],
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
)
.await?;
let project_layers_untrusted: Vec<_> = layers_untrusted
.get_layers(
ConfigLayerStackOrdering::HighestPrecedenceFirst,
/*include_disabled*/ true,
)
.into_iter()
.filter(|layer| matches!(layer.name, ConfigLayerSource::Project { .. }))
.collect();
assert_eq!(project_layers_untrusted.len(), 1);
assert!(
project_layers_untrusted[0].disabled_reason.is_some(),
"expected malformed untrusted project layer to be disabled"
);
assert_eq!(
project_layers_untrusted[0].config,
TomlValue::Table(toml::map::Map::new())
);
assert_eq!(
layers_untrusted.effective_config().get("foo"),
Some(&TomlValue::String("user".to_string()))
);
let untrusted_warnings = layers_untrusted
.startup_warnings()
.expect("expected malformed project config warning");
assert_eq!(untrusted_warnings.len(), 1);
assert!(
untrusted_warnings[0].contains(&format!(
"Ignored malformed project-local config in {} because this project is not trusted:",
project_config_path.display()
)),
"warning should explain that malformed untrusted project config was ignored"
);
let codex_home_unknown = tmp.path().join("home_unknown_malformed");
tokio::fs::create_dir_all(&codex_home_unknown).await?;
tokio::fs::write(
codex_home_unknown.join(CONFIG_TOML_FILE),
r#"foo = "user"
"#,
)
.await?;
let layers_unknown = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home_unknown,
Some(cwd.clone()),
&[] as &[(String, TomlValue)],
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
)
.await?;
let project_layers_unknown: Vec<_> = layers_unknown
.get_layers(
ConfigLayerStackOrdering::HighestPrecedenceFirst,
/*include_disabled*/ true,
)
.into_iter()
.filter(|layer| matches!(layer.name, ConfigLayerSource::Project { .. }))
.collect();
assert_eq!(project_layers_unknown.len(), 1);
assert!(
project_layers_unknown[0].disabled_reason.is_some(),
"expected malformed unknown-trust project layer to be disabled"
);
assert_eq!(
project_layers_unknown[0].config,
TomlValue::Table(toml::map::Map::new())
);
assert_eq!(
layers_unknown.effective_config().get("foo"),
Some(&TomlValue::String("user".to_string()))
);
let unknown_warnings = layers_unknown
.startup_warnings()
.expect("expected malformed project config warning");
assert_eq!(unknown_warnings.len(), 1);
assert!(
unknown_warnings[0].contains(&format!(
"Ignored malformed project-local config in {} because this project is not trusted:",
project_config_path.display()
)),
"warning should explain that malformed unknown-trust project config was ignored"
);
let codex_home_trusted = tmp.path().join("home_trusted_malformed");
tokio::fs::create_dir_all(&codex_home_trusted).await?;
make_config_for_test(
&codex_home_trusted,
&project_root,
TrustLevel::Trusted,
/*project_root_markers*/ None,
)
.await?;
let err = load_config_layers_state(
LOCAL_FS.as_ref(),
&codex_home_trusted,
Some(cwd),
&[] as &[(String, TomlValue)],
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
&codex_config::NoopThreadConfigLoader,
)
.await
.expect_err("trusted malformed project config should fail");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
assert!(
err.to_string()
.contains("Error parsing project config file"),
"trusted project config parse errors should still fail loudly"
);
Ok(())
}
#[tokio::test]
async fn project_layer_ignores_unsupported_config_keys() -> std::io::Result<()> {
let tmp = tempdir()?;