feat: add --ignore-user-config and --ignore-rules (#18646)

Add those 2 flags to be able to fully isolate a run of `codex exec` from
any rules or tools.
This will be used by Chronicle
This commit is contained in:
jif-oai
2026-04-20 11:27:47 +01:00
committed by GitHub
Unverified
parent 7d8bd69283
commit be4fe9f9b2
9 changed files with 274 additions and 13 deletions
+24
View File
@@ -18,6 +18,8 @@ use toml::Value as TomlValue;
#[derive(Debug, Default, Clone)]
pub struct LoaderOverrides {
pub managed_config_path: Option<PathBuf>,
pub ignore_user_config: bool,
pub ignore_user_and_project_exec_policy_rules: bool,
//TODO(gt): Add a macos_ prefix to this field and remove the target_os check.
#[cfg(target_os = "macos")]
pub managed_preferences_base64: Option<String>,
@@ -42,6 +44,8 @@ impl LoaderOverrides {
pub fn with_managed_config_path_for_tests(managed_config_path: PathBuf) -> Self {
Self {
managed_config_path: Some(managed_config_path),
ignore_user_config: false,
ignore_user_and_project_exec_policy_rules: false,
#[cfg(target_os = "macos")]
managed_preferences_base64: Some(String::new()),
macos_managed_config_requirements_base64: Some(String::new()),
@@ -157,6 +161,9 @@ pub struct ConfigLayerStack {
/// sources. This preserves the original allow-lists so they can be
/// surfaced via APIs.
requirements_toml: ConfigRequirementsToml,
/// Whether execpolicy should skip `.rules` files from user and project config-layer folders.
ignore_user_and_project_exec_policy_rules: bool,
}
impl ConfigLayerStack {
@@ -171,9 +178,22 @@ impl ConfigLayerStack {
user_layer_index,
requirements,
requirements_toml,
ignore_user_and_project_exec_policy_rules: false,
})
}
pub fn with_user_and_project_exec_policy_rules_ignored(
mut self,
ignore_user_and_project_exec_policy_rules: bool,
) -> Self {
self.ignore_user_and_project_exec_policy_rules = ignore_user_and_project_exec_policy_rules;
self
}
pub fn ignore_user_and_project_exec_policy_rules(&self) -> bool {
self.ignore_user_and_project_exec_policy_rules
}
/// Returns the raw user config layer, if any.
///
/// This does not merge other config layers or apply any requirements.
@@ -211,6 +231,8 @@ impl ConfigLayerStack {
user_layer_index: self.user_layer_index,
requirements: self.requirements.clone(),
requirements_toml: self.requirements_toml.clone(),
ignore_user_and_project_exec_policy_rules: self
.ignore_user_and_project_exec_policy_rules,
}
}
None => {
@@ -232,6 +254,8 @@ impl ConfigLayerStack {
user_layer_index: Some(user_layer_index),
requirements: self.requirements.clone(),
requirements_toml: self.requirements_toml.clone(),
ignore_user_and_project_exec_policy_rules: self
.ignore_user_and_project_exec_policy_rules,
}
}
}
+16 -1
View File
@@ -856,13 +856,28 @@ pub async fn load_config_as_toml_with_cli_overrides(
codex_home: &Path,
cwd: Option<&AbsolutePathBuf>,
cli_overrides: Vec<(String, TomlValue)>,
) -> std::io::Result<ConfigToml> {
load_config_as_toml_with_cli_and_loader_overrides(
codex_home,
cwd,
cli_overrides,
LoaderOverrides::default(),
)
.await
}
pub async fn load_config_as_toml_with_cli_and_loader_overrides(
codex_home: &Path,
cwd: Option<&AbsolutePathBuf>,
cli_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
) -> std::io::Result<ConfigToml> {
let config_layer_stack = load_config_layers_state(
LOCAL_FS.as_ref(),
codex_home,
cwd.cloned(),
&cli_overrides,
LoaderOverrides::default(),
loader_overrides,
CloudRequirementsLoader::default(),
)
.await?;
+22 -9
View File
@@ -128,6 +128,9 @@ pub async fn load_config_layers_state(
overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
) -> io::Result<ConfigLayerStack> {
let ignore_user_config = overrides.ignore_user_config;
let ignore_user_and_project_exec_policy_rules =
overrides.ignore_user_and_project_exec_policy_rules;
let mut config_requirements_toml = ConfigRequirementsWithSources::default();
if let Some(requirements) = cloud_requirements.get().await.map_err(io::Error::other)? {
@@ -189,19 +192,28 @@ pub async fn load_config_layers_state(
.await?;
layers.push(system_layer);
// Add a layer for $CODEX_HOME/config.toml if it exists. Note if the file
// exists, but is malformed, then this error should be propagated to the
// user.
// Add a layer for $CODEX_HOME/config.toml so folder-derived resources such
// as rules/ can still be discovered. When user config is ignored, preserve
// the layer metadata without reading config.toml.
let user_file = AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, codex_home);
let user_layer = load_config_toml_for_required_layer(fs, &user_file, |config_toml| {
let user_layer = if ignore_user_config {
ConfigLayerEntry::new(
ConfigLayerSource::User {
file: user_file.clone(),
},
config_toml,
TomlValue::Table(toml::map::Map::new()),
)
})
.await?;
} else {
load_config_toml_for_required_layer(fs, &user_file, |config_toml| {
ConfigLayerEntry::new(
ConfigLayerSource::User {
file: user_file.clone(),
},
config_toml,
)
})
.await?
};
layers.push(user_layer);
if let Some(cwd) = cwd {
@@ -312,11 +324,12 @@ pub async fn load_config_layers_state(
));
}
ConfigLayerStack::new(
Ok(ConfigLayerStack::new(
layers,
config_requirements_toml.clone().try_into()?,
config_requirements_toml.into_toml(),
)
)?
.with_user_and_project_exec_policy_rules_ignored(ignore_user_and_project_exec_policy_rules))
}
/// Attempts to load a config.toml file from `config_toml`.
+57
View File
@@ -111,6 +111,63 @@ async fn returns_config_error_for_invalid_user_config_toml() {
assert_eq!(config_error, &expected_config_error);
}
#[tokio::test]
async fn ignore_user_config_keeps_empty_user_layer() -> std::io::Result<()> {
let tmp = tempdir().expect("tempdir");
std::fs::write(
tmp.path().join(CONFIG_TOML_FILE),
"model = \"from-user-config\"\ninvalid = [",
)
.expect("write config");
let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd");
let layers = load_config_layers_state(
LOCAL_FS.as_ref(),
tmp.path(),
Some(cwd),
&[] as &[(String, TomlValue)],
LoaderOverrides {
ignore_user_config: true,
..Default::default()
},
CloudRequirementsLoader::default(),
)
.await?;
let user_layer = layers
.get_user_layer()
.expect("expected a user layer even when CODEX_HOME/config.toml is ignored");
assert_eq!(
user_layer.config,
TomlValue::Table(toml::map::Map::new()),
"expected ignored user config to preserve only layer metadata"
);
assert_eq!(layers.effective_config().get("model"), None);
Ok(())
}
#[tokio::test]
async fn ignore_rules_marks_config_stack_for_exec_policy_rule_skip() -> std::io::Result<()> {
let tmp = tempdir().expect("tempdir");
let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd");
let layers = load_config_layers_state(
LOCAL_FS.as_ref(),
tmp.path(),
Some(cwd),
&[] as &[(String, TomlValue)],
LoaderOverrides {
ignore_user_and_project_exec_policy_rules: true,
..Default::default()
},
CloudRequirementsLoader::default(),
)
.await?;
assert!(layers.ignore_user_and_project_exec_policy_rules());
Ok(())
}
#[tokio::test]
async fn returns_config_error_for_invalid_managed_config_toml() {
let tmp = tempdir().expect("tempdir");
+9
View File
@@ -7,6 +7,7 @@ use arc_swap::ArcSwap;
use crate::config_loader::ConfigLayerStack;
use crate::config_loader::ConfigLayerStackOrdering;
use codex_app_server_protocol::ConfigLayerSource;
use codex_execpolicy::AmendError;
use codex_execpolicy::Decision;
use codex_execpolicy::Error as ExecPolicyRuleError;
@@ -505,6 +506,14 @@ pub async fn load_exec_policy(config_stack: &ConfigLayerStack) -> Result<Policy,
ConfigLayerStackOrdering::LowestPrecedenceFirst,
/*include_disabled*/ false,
) {
if config_stack.ignore_user_and_project_exec_policy_rules()
&& matches!(
layer.name,
ConfigLayerSource::User { .. } | ConfigLayerSource::Project { .. }
)
{
continue;
}
if let Some(config_folder) = layer.config_folder() {
let policy_dir = config_folder.join(RULES_DIR_NAME);
let layer_policy_paths = collect_policy_files(&policy_dir).await?;
+108
View File
@@ -6,9 +6,11 @@ use crate::config_loader::ConfigLayerStack;
use crate::config_loader::ConfigLayerStackOrdering;
use crate::config_loader::ConfigRequirements;
use crate::config_loader::ConfigRequirementsToml;
use crate::config_loader::LoaderOverrides;
use crate::config_loader::RequirementSource;
use crate::config_loader::Sourced;
use codex_app_server_protocol::ConfigLayerSource;
use codex_config::CONFIG_TOML_FILE;
use codex_config::RequirementsExecPolicy;
use codex_config::config_toml::ConfigToml;
use codex_config::config_toml::ProjectConfig;
@@ -424,6 +426,73 @@ async fn ignores_policies_outside_policy_dir() {
);
}
#[tokio::test]
async fn ignores_policy_files_when_config_stack_disables_exec_policy_rules() {
let temp_dir = tempdir().expect("create temp dir");
let policy_dir = temp_dir.path().join(RULES_DIR_NAME);
fs::create_dir_all(&policy_dir).expect("create policy dir");
fs::write(
policy_dir.join("allow.rules"),
r#"prefix_rule(pattern=["curl"], decision="allow")"#,
)
.expect("write policy file");
let config_stack = config_stack_for_dot_codex_folder(temp_dir.path())
.with_user_and_project_exec_policy_rules_ignored(
/*ignore_user_and_project_exec_policy_rules*/ true,
);
let policy = load_exec_policy(&config_stack)
.await
.expect("policy result");
assert_eq!(
policy
.check_multiple([vec!["curl".to_string()]].iter(), &|_| Decision::Forbidden)
.decision,
Decision::Forbidden,
);
}
#[tokio::test]
async fn ignore_user_project_rules_keeps_system_policy_files() {
let temp_dir = tempdir().expect("create temp dir");
let config_dir = temp_dir.path().join("system");
let policy_dir = config_dir.join(RULES_DIR_NAME);
fs::create_dir_all(&policy_dir).expect("create policy dir");
fs::write(
policy_dir.join("allow.rules"),
r#"prefix_rule(pattern=["curl"], decision="allow")"#,
)
.expect("write policy file");
let config_file =
AbsolutePathBuf::from_absolute_path(config_dir.join(codex_config::CONFIG_TOML_FILE))
.expect("absolute config file");
let layer = ConfigLayerEntry::new(
ConfigLayerSource::System { file: config_file },
TomlValue::Table(Default::default()),
);
let config_stack = ConfigLayerStack::new(
vec![layer],
ConfigRequirements::default(),
ConfigRequirementsToml::default(),
)
.expect("ConfigLayerStack")
.with_user_and_project_exec_policy_rules_ignored(
/*ignore_user_and_project_exec_policy_rules*/ true,
);
let policy = load_exec_policy(&config_stack)
.await
.expect("policy result");
assert_eq!(
policy
.check_multiple([vec!["curl".to_string()]].iter(), &|_| Decision::Forbidden)
.decision,
Decision::Allow,
);
}
#[tokio::test]
async fn ignores_rules_from_untrusted_project_layers() -> anyhow::Result<()> {
let project_dir = tempdir()?;
@@ -575,6 +644,45 @@ fn commands_for_exec_policy_falls_back_for_whitespace_shell_script() {
assert_eq!(commands_for_exec_policy(&command), (vec![command], false));
}
#[tokio::test]
async fn ignore_user_config_keeps_user_policy_files() -> std::io::Result<()> {
let temp = tempdir()?;
let codex_home = temp.path().join("home_ignore_user_config");
let rules_dir = codex_home.join(RULES_DIR_NAME);
fs::create_dir_all(&rules_dir)?;
fs::write(
codex_home.join(CONFIG_TOML_FILE),
"model = \"from-user-config\"\ninvalid = [",
)?;
fs::write(
rules_dir.join("deny-curl.rules"),
r#"prefix_rule(pattern=["curl"], decision="forbidden")"#,
)?;
let config = ConfigBuilder::default()
.codex_home(codex_home)
.fallback_cwd(Some(temp.path().to_path_buf()))
.loader_overrides(LoaderOverrides {
ignore_user_config: true,
..Default::default()
})
.build()
.await?;
let policy = load_exec_policy(&config.config_layer_stack)
.await
.map_err(std::io::Error::other)?;
assert_eq!(
policy
.check_multiple([vec!["curl".to_string()]].iter(), &|_| Decision::Allow)
.decision,
Decision::Forbidden,
);
Ok(())
}
#[tokio::test]
async fn evaluates_heredoc_script_against_prefix_rules() {
let command = vec![
+8
View File
@@ -78,6 +78,14 @@ pub struct Cli {
#[arg(long = "ephemeral", global = true, default_value_t = false)]
pub ephemeral: bool,
/// Do not load `$CODEX_HOME/config.toml`; auth still uses `CODEX_HOME`.
#[arg(long = "ignore-user-config", global = true, default_value_t = false)]
pub ignore_user_config: bool,
/// Do not load user or project execpolicy `.rules` files.
#[arg(long = "ignore-rules", global = true, default_value_t = false)]
pub ignore_rules: bool,
/// Path to a JSON Schema file describing the model's final response shape.
#[arg(long = "output-schema", value_name = "FILE")]
pub output_schema: Option<PathBuf>,
+17
View File
@@ -14,10 +14,14 @@ fn resume_parses_prompt_after_global_flags() {
"--dangerously-bypass-approvals-and-sandbox",
"--skip-git-repo-check",
"--ephemeral",
"--ignore-user-config",
"--ignore-rules",
PROMPT,
]);
assert!(cli.ephemeral);
assert!(cli.ignore_user_config);
assert!(cli.ignore_rules);
let Some(Command::Resume(args)) = cli.command else {
panic!("expected resume command");
};
@@ -53,3 +57,16 @@ fn resume_accepts_output_last_message_flag_after_subcommand() {
assert_eq!(args.session_id.as_deref(), Some("session-123"));
assert_eq!(args.prompt.as_deref(), Some(PROMPT));
}
#[test]
fn parses_config_isolation_flags() {
let cli = Cli::parse_from([
"codex-exec",
"--ignore-user-config",
"--ignore-rules",
"summarize",
]);
assert!(cli.ignore_user_config);
assert!(cli.ignore_rules);
}
+13 -3
View File
@@ -56,7 +56,7 @@ use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigOverrides;
use codex_core::config::find_codex_home;
use codex_core::config::load_config_as_toml_with_cli_overrides;
use codex_core::config::load_config_as_toml_with_cli_and_loader_overrides;
use codex_core::config::resolve_oss_provider;
use codex_core::config_loader::ConfigLoadError;
use codex_core::config_loader::LoaderOverrides;
@@ -230,6 +230,8 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
skip_git_repo_check,
add_dir,
ephemeral,
ignore_user_config,
ignore_rules,
color,
last_message_file,
json: json_mode,
@@ -297,10 +299,17 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
};
#[allow(clippy::print_stderr)]
let config_toml = match load_config_as_toml_with_cli_overrides(
let loader_overrides = LoaderOverrides {
ignore_user_config,
ignore_user_and_project_exec_policy_rules: ignore_rules,
..Default::default()
};
let config_toml = match load_config_as_toml_with_cli_and_loader_overrides(
&codex_home,
Some(&config_cwd),
cli_kv_overrides.clone(),
loader_overrides.clone(),
)
.await
{
@@ -334,7 +343,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
chatgpt_base_url,
);
let run_cli_overrides = cli_kv_overrides.clone();
let run_loader_overrides = LoaderOverrides::default();
let run_loader_overrides = loader_overrides.clone();
let run_cloud_requirements = cloud_requirements.clone();
let model_provider = if oss {
@@ -399,6 +408,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
let config = ConfigBuilder::default()
.cli_overrides(cli_kv_overrides)
.harness_overrides(overrides)
.loader_overrides(loader_overrides)
.cloud_requirements(cloud_requirements)
.build()
.await?;