mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
7d8bd69283
commit
be4fe9f9b2
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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![
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
Reference in New Issue
Block a user