mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +00:00
config: add override config foundation
# Conflicts: # codex-rs/config/src/loader/mod.rs
This commit is contained in:
@@ -232,7 +232,10 @@ impl<'a> AgentsMdManager<'a> {
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false,
|
||||
) {
|
||||
if matches!(layer.name, ConfigLayerSource::Project { .. }) {
|
||||
if matches!(
|
||||
layer.name,
|
||||
ConfigLayerSource::Project { .. } | ConfigLayerSource::ProjectOverride { .. }
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
merge_toml_values(&mut merged, &layer.config);
|
||||
|
||||
@@ -30,6 +30,31 @@ pub(crate) async fn load_agent_roles(
|
||||
}
|
||||
|
||||
let mut roles: BTreeMap<String, AgentRoleConfig> = BTreeMap::new();
|
||||
let mut declared_agent_role_files_by_folder =
|
||||
BTreeMap::<AbsolutePathBuf, BTreeSet<PathBuf>>::new();
|
||||
for layer in &layers {
|
||||
let Some(config_folder) = layer.config_folder() else {
|
||||
continue;
|
||||
};
|
||||
let Ok(Some(agents_toml)) =
|
||||
agents_toml_from_layer(&layer.config, Some(config_folder.as_path()))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let declared_files = declared_agent_role_files_by_folder
|
||||
.entry(config_folder)
|
||||
.or_default();
|
||||
for (role_name, role_toml) in &agents_toml.roles {
|
||||
let Ok(role) = agent_role_config_from_toml(fs, role_name, role_toml).await else {
|
||||
continue;
|
||||
};
|
||||
if let Some(config_file) = role.config_file {
|
||||
declared_files.insert(config_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut discovered_agent_role_folders = BTreeSet::new();
|
||||
for layer in layers {
|
||||
let mut layer_roles: BTreeMap<String, AgentRoleConfig> = BTreeMap::new();
|
||||
let mut declared_role_files = BTreeSet::new();
|
||||
@@ -70,11 +95,16 @@ pub(crate) async fn load_agent_roles(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(config_folder) = layer.config_folder() {
|
||||
if let Some(config_folder) = config_folder
|
||||
&& discovered_agent_role_folders.insert(config_folder.clone())
|
||||
{
|
||||
let declared_role_files = declared_agent_role_files_by_folder
|
||||
.get(&config_folder)
|
||||
.unwrap_or(&declared_role_files);
|
||||
for (role_name, role) in discover_agent_roles_in_dir(
|
||||
fs,
|
||||
&config_folder.join("agents"),
|
||||
&declared_role_files,
|
||||
declared_role_files,
|
||||
startup_warnings,
|
||||
)
|
||||
.await?
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::config::ConfigBuilder;
|
||||
use crate::config::ConfigOverrides;
|
||||
use crate::config::ConstraintError;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::CONFIG_OVERRIDE_TOML_FILE;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::CloudRequirementsLoadError;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
@@ -1666,6 +1667,152 @@ async fn project_layers_prefer_closest_cwd() -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sibling_override_layers_load_after_their_base_scope() -> std::io::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let project_root = tmp.path().join("project");
|
||||
let nested = project_root.join("child");
|
||||
let dot_codex = project_root.join(".codex");
|
||||
let codex_home = tmp.path().join("home");
|
||||
tokio::fs::create_dir_all(&nested).await?;
|
||||
tokio::fs::create_dir_all(&dot_codex).await?;
|
||||
tokio::fs::create_dir_all(&codex_home).await?;
|
||||
tokio::fs::write(project_root.join(".git"), "gitdir: here").await?;
|
||||
tokio::fs::write(dot_codex.join(CONFIG_TOML_FILE), "foo = \"project\"\n").await?;
|
||||
tokio::fs::write(
|
||||
dot_codex.join(CONFIG_OVERRIDE_TOML_FILE),
|
||||
"foo = \"project-override\"\n",
|
||||
)
|
||||
.await?;
|
||||
make_config_for_test(
|
||||
&codex_home,
|
||||
&project_root,
|
||||
TrustLevel::Trusted,
|
||||
/*project_root_markers*/ None,
|
||||
)
|
||||
.await?;
|
||||
let trust_config = tokio::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).await?;
|
||||
tokio::fs::write(
|
||||
codex_home.join(CONFIG_TOML_FILE),
|
||||
format!("foo = \"user\"\n{trust_config}"),
|
||||
)
|
||||
.await?;
|
||||
tokio::fs::write(
|
||||
codex_home.join(CONFIG_OVERRIDE_TOML_FILE),
|
||||
"foo = \"user-override\"\n",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let layers = load_config_layers_state(
|
||||
LOCAL_FS.as_ref(),
|
||||
&codex_home,
|
||||
Some(AbsolutePathBuf::from_absolute_path(&nested)?),
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let sources = layers
|
||||
.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false,
|
||||
)
|
||||
.into_iter()
|
||||
.filter_map(|layer| match &layer.name {
|
||||
ConfigLayerSource::User { .. }
|
||||
| ConfigLayerSource::UserOverride { .. }
|
||||
| ConfigLayerSource::Project { .. }
|
||||
| ConfigLayerSource::ProjectOverride { .. } => Some(layer.name.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert!(matches!(
|
||||
sources.as_slice(),
|
||||
[
|
||||
ConfigLayerSource::User { .. },
|
||||
ConfigLayerSource::UserOverride { .. },
|
||||
ConfigLayerSource::Project { .. },
|
||||
ConfigLayerSource::ProjectOverride { .. },
|
||||
]
|
||||
));
|
||||
|
||||
let config = layers.effective_config();
|
||||
assert_eq!(
|
||||
config.get("foo").and_then(TomlValue::as_str),
|
||||
Some("project-override")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn project_override_partially_merges_nested_mcp_server_config() -> std::io::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let project_root = tmp.path().join("project");
|
||||
let nested = project_root.join("child");
|
||||
let dot_codex = project_root.join(".codex");
|
||||
let codex_home = tmp.path().join("home");
|
||||
tokio::fs::create_dir_all(&nested).await?;
|
||||
tokio::fs::create_dir_all(&dot_codex).await?;
|
||||
tokio::fs::create_dir_all(&codex_home).await?;
|
||||
tokio::fs::write(project_root.join(".git"), "gitdir: here").await?;
|
||||
tokio::fs::write(
|
||||
dot_codex.join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[mcp_servers.sentry]
|
||||
url = "https://mcp.sentry.dev/mcp"
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
tokio::fs::write(
|
||||
dot_codex.join(CONFIG_OVERRIDE_TOML_FILE),
|
||||
r#"
|
||||
[mcp_servers.sentry]
|
||||
enabled = false
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
make_config_for_test(
|
||||
&codex_home,
|
||||
&project_root,
|
||||
TrustLevel::Trusted,
|
||||
/*project_root_markers*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home)
|
||||
.fallback_cwd(Some(nested))
|
||||
.build()
|
||||
.await?;
|
||||
let server = config
|
||||
.mcp_servers
|
||||
.get()
|
||||
.get("sentry")
|
||||
.expect("project MCP server should load");
|
||||
assert!(!server.enabled);
|
||||
|
||||
let effective = config.config_layer_stack.effective_config();
|
||||
let sentry = effective
|
||||
.get("mcp_servers")
|
||||
.and_then(TomlValue::as_table)
|
||||
.and_then(|servers| servers.get("sentry"))
|
||||
.and_then(TomlValue::as_table)
|
||||
.expect("merged sentry table");
|
||||
assert_eq!(
|
||||
sentry.get("url").and_then(TomlValue::as_str),
|
||||
Some("https://mcp.sentry.dev/mcp")
|
||||
);
|
||||
assert_eq!(
|
||||
sentry.get("enabled").and_then(TomlValue::as_bool),
|
||||
Some(false)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn linked_worktree_project_layers_keep_worktree_config_but_use_root_repo_hooks()
|
||||
-> std::io::Result<()> {
|
||||
@@ -1769,6 +1916,140 @@ async fn linked_worktree_project_layers_keep_worktree_config_but_use_root_repo_h
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn linked_worktree_project_override_layers_use_root_repo_hooks() -> std::io::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let repo_root = tmp.path().join("repo");
|
||||
let worktree_root = tmp.path().join("worktree");
|
||||
|
||||
tokio::fs::create_dir_all(worktree_root.join(".codex")).await?;
|
||||
write_linked_worktree_pointer(&repo_root, &worktree_root).await?;
|
||||
tokio::fs::create_dir_all(repo_root.join(".codex")).await?;
|
||||
tokio::fs::write(
|
||||
repo_root.join(".codex").join(CONFIG_OVERRIDE_TOML_FILE),
|
||||
r#"[hooks]
|
||||
|
||||
[[hooks.PreToolUse]]
|
||||
matcher = "Bash"
|
||||
|
||||
[[hooks.PreToolUse.hooks]]
|
||||
type = "command"
|
||||
command = "echo repo override hook"
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
tokio::fs::write(
|
||||
worktree_root.join(".codex").join(CONFIG_OVERRIDE_TOML_FILE),
|
||||
r#"[hooks]
|
||||
|
||||
[[hooks.PreToolUse]]
|
||||
matcher = "Bash"
|
||||
|
||||
[[hooks.PreToolUse.hooks]]
|
||||
type = "command"
|
||||
command = "echo worktree override hook"
|
||||
"#,
|
||||
)
|
||||
.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::Trusted,
|
||||
/*project_root_markers*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let layers = load_config_layers_state(
|
||||
LOCAL_FS.as_ref(),
|
||||
&codex_home,
|
||||
Some(AbsolutePathBuf::from_absolute_path(&worktree_root)?),
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
let project_override_layer = layers
|
||||
.layers_high_to_low()
|
||||
.into_iter()
|
||||
.find(|layer| matches!(layer.name, ConfigLayerSource::ProjectOverride { .. }))
|
||||
.expect("project override layer");
|
||||
|
||||
assert_eq!(
|
||||
project_override_layer.hooks_config_folder(),
|
||||
Some(AbsolutePathBuf::from_absolute_path(
|
||||
repo_root.join(".codex")
|
||||
)?)
|
||||
);
|
||||
assert_eq!(
|
||||
project_hook_command(project_override_layer),
|
||||
Some("echo repo override hook")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn linked_worktree_project_override_layers_load_root_repo_hooks_without_worktree_override()
|
||||
-> std::io::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let repo_root = tmp.path().join("repo");
|
||||
let worktree_root = tmp.path().join("worktree");
|
||||
|
||||
tokio::fs::create_dir_all(worktree_root.join(".codex")).await?;
|
||||
write_linked_worktree_pointer(&repo_root, &worktree_root).await?;
|
||||
tokio::fs::create_dir_all(repo_root.join(".codex")).await?;
|
||||
tokio::fs::write(
|
||||
repo_root.join(".codex").join(CONFIG_OVERRIDE_TOML_FILE),
|
||||
r#"[hooks]
|
||||
|
||||
[[hooks.PreToolUse]]
|
||||
matcher = "Bash"
|
||||
|
||||
[[hooks.PreToolUse.hooks]]
|
||||
type = "command"
|
||||
command = "echo repo override hook"
|
||||
"#,
|
||||
)
|
||||
.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::Trusted,
|
||||
/*project_root_markers*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let layers = load_config_layers_state(
|
||||
LOCAL_FS.as_ref(),
|
||||
&codex_home,
|
||||
Some(AbsolutePathBuf::from_absolute_path(&worktree_root)?),
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
)
|
||||
.await?;
|
||||
let project_override_layer = layers
|
||||
.layers_high_to_low()
|
||||
.into_iter()
|
||||
.find(|layer| matches!(layer.name, ConfigLayerSource::ProjectOverride { .. }))
|
||||
.expect("project override layer");
|
||||
|
||||
assert_eq!(
|
||||
project_hook_command(project_override_layer),
|
||||
Some("echo repo override hook")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn linked_worktree_project_layers_use_root_repo_hooks_without_worktree_config_toml()
|
||||
-> std::io::Result<()> {
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::config::edit::ConfigEdit;
|
||||
use crate::config::edit::ConfigEditsBuilder;
|
||||
use crate::config::edit::apply_blocking;
|
||||
use assert_matches::assert_matches;
|
||||
use codex_config::CONFIG_OVERRIDE_TOML_FILE;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ProfileV2Name;
|
||||
@@ -7121,6 +7122,183 @@ developer_instructions = "Write carefully"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sibling_override_layers_scan_standalone_agent_roles_once() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let repo_root = TempDir::new()?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".git"))?;
|
||||
|
||||
let workspace_key = repo_root.path().to_string_lossy().replace('\\', "\\\\");
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
format!(
|
||||
r#"[projects."{workspace_key}"]
|
||||
trust_level = "trusted"
|
||||
"#
|
||||
),
|
||||
)?;
|
||||
|
||||
let project_codex_dir = repo_root.path().join(".codex");
|
||||
std::fs::create_dir_all(project_codex_dir.join("agents"))?;
|
||||
std::fs::write(
|
||||
project_codex_dir.join(CONFIG_TOML_FILE),
|
||||
"model = \"base\"\n",
|
||||
)?;
|
||||
std::fs::write(
|
||||
project_codex_dir.join(CONFIG_OVERRIDE_TOML_FILE),
|
||||
"approval_policy = \"never\"\n",
|
||||
)?;
|
||||
std::fs::write(
|
||||
project_codex_dir.join("agents").join("broken.toml"),
|
||||
r#"
|
||||
name = "broken"
|
||||
description = "Broken role"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
cwd: Some(repo_root.path().to_path_buf()),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let warning_count = config
|
||||
.startup_warnings
|
||||
.iter()
|
||||
.filter(|warning| warning.contains("must define `developer_instructions`"))
|
||||
.count();
|
||||
assert_eq!(warning_count, 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sibling_override_declared_agent_role_files_are_not_scanned_as_standalone()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let repo_root = TempDir::new()?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".git"))?;
|
||||
|
||||
let workspace_key = repo_root.path().to_string_lossy().replace('\\', "\\\\");
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
format!(
|
||||
r#"[projects."{workspace_key}"]
|
||||
trust_level = "trusted"
|
||||
"#
|
||||
),
|
||||
)?;
|
||||
|
||||
let project_codex_dir = repo_root.path().join(".codex");
|
||||
let agents_dir = project_codex_dir.join("agents");
|
||||
std::fs::create_dir_all(&agents_dir)?;
|
||||
std::fs::write(
|
||||
project_codex_dir.join(CONFIG_TOML_FILE),
|
||||
"model = \"base\"\n",
|
||||
)?;
|
||||
std::fs::write(
|
||||
project_codex_dir.join(CONFIG_OVERRIDE_TOML_FILE),
|
||||
r#"[agents.researcher]
|
||||
description = "Research role"
|
||||
config_file = "./agents/researcher.toml"
|
||||
"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
agents_dir.join("researcher.toml"),
|
||||
r#"
|
||||
developer_instructions = "Research carefully"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
cwd: Some(repo_root.path().to_path_buf()),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config
|
||||
.agent_roles
|
||||
.get("researcher")
|
||||
.and_then(|role| role.description.as_deref()),
|
||||
Some("Research role")
|
||||
);
|
||||
assert!(
|
||||
config
|
||||
.startup_warnings
|
||||
.iter()
|
||||
.all(|warning| !warning.contains("must define a non-empty `name`"))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_override_agent_role_declaration_keeps_valid_standalone_discovery()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let repo_root = TempDir::new()?;
|
||||
std::fs::create_dir_all(repo_root.path().join(".git"))?;
|
||||
|
||||
let workspace_key = repo_root.path().to_string_lossy().replace('\\', "\\\\");
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
format!(
|
||||
r#"[projects."{workspace_key}"]
|
||||
trust_level = "trusted"
|
||||
"#
|
||||
),
|
||||
)?;
|
||||
|
||||
let project_codex_dir = repo_root.path().join(".codex");
|
||||
let agents_dir = project_codex_dir.join("agents");
|
||||
std::fs::create_dir_all(&agents_dir)?;
|
||||
std::fs::write(
|
||||
project_codex_dir.join(CONFIG_TOML_FILE),
|
||||
"model = \"base\"\n",
|
||||
)?;
|
||||
std::fs::write(
|
||||
project_codex_dir.join(CONFIG_OVERRIDE_TOML_FILE),
|
||||
r#"[agents.researcher]
|
||||
description = ""
|
||||
config_file = "./agents/researcher.toml"
|
||||
"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
agents_dir.join("researcher.toml"),
|
||||
r#"
|
||||
name = "researcher"
|
||||
description = "Standalone research role"
|
||||
developer_instructions = "Research carefully"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
cwd: Some(repo_root.path().to_path_buf()),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config
|
||||
.agent_roles
|
||||
.get("researcher")
|
||||
.and_then(|role| role.description.as_deref()),
|
||||
Some("Standalone research role")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mixed_legacy_and_standalone_agent_role_sources_merge_with_precedence()
|
||||
-> std::io::Result<()> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::collections::HashSet;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -576,6 +577,7 @@ pub async fn load_exec_policy(config_stack: &ConfigLayerStack) -> Result<Policy,
|
||||
// from each layer, so that higher-precedence layers can override
|
||||
// rules defined in lower-precedence ones.
|
||||
let mut policy_paths = Vec::new();
|
||||
let mut policy_folders = HashSet::new();
|
||||
for layer in config_stack.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false,
|
||||
@@ -583,12 +585,18 @@ pub async fn load_exec_policy(config_stack: &ConfigLayerStack) -> Result<Policy,
|
||||
if config_stack.ignore_user_and_project_exec_policy_rules()
|
||||
&& matches!(
|
||||
layer.name,
|
||||
ConfigLayerSource::User { .. } | ConfigLayerSource::Project { .. }
|
||||
ConfigLayerSource::User { .. }
|
||||
| ConfigLayerSource::UserOverride { .. }
|
||||
| ConfigLayerSource::Project { .. }
|
||||
| ConfigLayerSource::ProjectOverride { .. }
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(config_folder) = layer.config_folder() {
|
||||
if !policy_folders.insert(config_folder.clone()) {
|
||||
continue;
|
||||
}
|
||||
let policy_dir = config_folder.join(RULES_DIR_NAME);
|
||||
let layer_policy_paths = collect_policy_files(&policy_dir).await?;
|
||||
policy_paths.extend(layer_policy_paths);
|
||||
|
||||
@@ -362,6 +362,50 @@ async fn loads_policies_from_policy_subdirectory() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn override_layers_do_not_duplicate_policy_directory_rules() -> anyhow::Result<()> {
|
||||
let temp_dir = tempdir()?;
|
||||
let dot_codex_folder = AbsolutePathBuf::from_absolute_path(temp_dir.path())?;
|
||||
let config_stack = ConfigLayerStack::new(
|
||||
vec![
|
||||
ConfigLayerEntry::new(
|
||||
ConfigLayerSource::Project {
|
||||
dot_codex_folder: dot_codex_folder.clone(),
|
||||
},
|
||||
TomlValue::Table(Default::default()),
|
||||
),
|
||||
ConfigLayerEntry::new(
|
||||
ConfigLayerSource::ProjectOverride { dot_codex_folder },
|
||||
TomlValue::Table(Default::default()),
|
||||
),
|
||||
],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)?;
|
||||
let policy_dir = temp_dir.path().join(RULES_DIR_NAME);
|
||||
fs::create_dir_all(&policy_dir)?;
|
||||
fs::write(
|
||||
policy_dir.join("deny.rules"),
|
||||
r#"prefix_rule(pattern=["rm"], decision="forbidden")"#,
|
||||
)?;
|
||||
|
||||
let policy = load_exec_policy(&config_stack).await?;
|
||||
let command = [vec!["rm".to_string()]];
|
||||
assert_eq!(
|
||||
policy.check_multiple(command.iter(), &|_| Decision::Allow),
|
||||
Evaluation {
|
||||
decision: Decision::Forbidden,
|
||||
matched_rules: vec![RuleMatch::PrefixRuleMatch {
|
||||
matched_prefix: vec!["rm".to_string()],
|
||||
decision: Decision::Forbidden,
|
||||
resolved_program: None,
|
||||
justification: None,
|
||||
}],
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn merges_requirements_exec_policy_network_rules() -> anyhow::Result<()> {
|
||||
let temp_dir = tempdir()?;
|
||||
|
||||
Reference in New Issue
Block a user