mirror of
https://github.com/openai/codex.git
synced 2026-09-06 15:29:32 +00:00
config: simplify override layer foundation
This commit is contained in:
@@ -111,14 +111,10 @@ impl ConfigLayerSource {
|
||||
ConfigLayerSource::Mdm { .. } => 0,
|
||||
ConfigLayerSource::System { .. } => 10,
|
||||
ConfigLayerSource::SystemOverride { .. } => 11,
|
||||
ConfigLayerSource::User { profile, .. } => {
|
||||
if profile.is_some() {
|
||||
21
|
||||
} else {
|
||||
20
|
||||
}
|
||||
}
|
||||
ConfigLayerSource::UserOverride { .. } => 20,
|
||||
ConfigLayerSource::User {
|
||||
profile: Some(_), ..
|
||||
} => 21,
|
||||
ConfigLayerSource::User { .. } | ConfigLayerSource::UserOverride { .. } => 20,
|
||||
ConfigLayerSource::Project { .. } | ConfigLayerSource::ProjectOverride { .. } => 25,
|
||||
ConfigLayerSource::SessionFlags => 30,
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => 40,
|
||||
|
||||
@@ -1207,6 +1207,28 @@ struct LoadedProjectLayers {
|
||||
startup_warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ProjectConfigFileKind {
|
||||
Base,
|
||||
Override,
|
||||
}
|
||||
|
||||
impl ProjectConfigFileKind {
|
||||
fn file_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Base => CONFIG_TOML_FILE,
|
||||
Self::Override => CONFIG_OVERRIDE_TOML_FILE,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_error_label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Base => "project config file",
|
||||
Self::Override => "project override config file",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the appropriate list of layers (each with
|
||||
/// [ConfigLayerSource::Project] as the source) between `cwd` and
|
||||
/// `project_root`, inclusive. The list is ordered in _increasing_ precdence,
|
||||
@@ -1260,126 +1282,75 @@ async fn load_project_layers(
|
||||
if dot_codex_abs == codex_home_abs || dot_codex_normalized == codex_home_normalized {
|
||||
continue;
|
||||
}
|
||||
let config_file = dot_codex_abs.join(CONFIG_TOML_FILE);
|
||||
let config_file = dot_codex_abs.join(ProjectConfigFileKind::Base.file_name());
|
||||
match fs.read_file_text(&config_file, /*sandbox*/ None).await {
|
||||
Ok(contents) => {
|
||||
let config: TomlValue = match toml::from_str(&contents) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
if decision.is_trusted() {
|
||||
let config_file_display = config_file.as_path().display();
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Error parsing project config file {config_file_display}: {e}"
|
||||
),
|
||||
));
|
||||
}
|
||||
layers.push(project_layer_entry(
|
||||
&dot_codex_abs,
|
||||
TomlValue::Table(toml::map::Map::new()),
|
||||
disabled_reason.clone(),
|
||||
hooks_config_folder_override.clone(),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let config = prepare_project_layer_config(
|
||||
let entry = load_project_layer_from_contents(
|
||||
fs,
|
||||
ProjectConfigFileKind::Base,
|
||||
&config_file,
|
||||
CONFIG_TOML_FILE,
|
||||
&contents,
|
||||
config,
|
||||
&dot_codex_abs,
|
||||
hooks_config_folder_override.as_ref(),
|
||||
decision.is_trusted(),
|
||||
strict_config,
|
||||
disabled_reason.is_none(),
|
||||
&mut startup_warnings,
|
||||
)
|
||||
.await?;
|
||||
let entry = project_layer_entry(
|
||||
&dot_codex_abs,
|
||||
config,
|
||||
disabled_reason.clone(),
|
||||
hooks_config_folder_override.clone(),
|
||||
);
|
||||
layers.push(entry);
|
||||
}
|
||||
Err(err) => {
|
||||
if err.kind() == io::ErrorKind::NotFound {
|
||||
// If there is no config.toml file, record an empty entry
|
||||
// for this project layer, as this may still have subfolders
|
||||
// that are significant in the overall ConfigLayerStack.
|
||||
let config = merge_root_checkout_project_hooks(
|
||||
fs,
|
||||
TomlValue::Table(toml::map::Map::new()),
|
||||
hooks_config_folder_override.as_ref(),
|
||||
decision.is_trusted(),
|
||||
CONFIG_TOML_FILE,
|
||||
)
|
||||
.await?;
|
||||
layers.push(project_layer_entry(
|
||||
&dot_codex_abs,
|
||||
config,
|
||||
disabled_reason.clone(),
|
||||
hooks_config_folder_override.clone(),
|
||||
));
|
||||
} else {
|
||||
let config_file_display = config_file.as_path().display();
|
||||
return Err(io::Error::new(
|
||||
err.kind(),
|
||||
format!("Failed to read project config file {config_file_display}: {err}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let override_file = dot_codex_abs.join(CONFIG_OVERRIDE_TOML_FILE);
|
||||
match fs.read_file_text(&override_file, /*sandbox*/ None).await {
|
||||
Ok(contents) => {
|
||||
let config: TomlValue = match toml::from_str(&contents) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
if decision.is_trusted() {
|
||||
let override_file_display = override_file.as_path().display();
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Error parsing project override config file {override_file_display}: {err}"
|
||||
),
|
||||
));
|
||||
}
|
||||
layers.push(project_override_layer_entry(
|
||||
&dot_codex_abs,
|
||||
TomlValue::Table(toml::map::Map::new()),
|
||||
disabled_reason.clone(),
|
||||
hooks_config_folder_override.clone(),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let config = prepare_project_layer_config(
|
||||
fs,
|
||||
&override_file,
|
||||
CONFIG_OVERRIDE_TOML_FILE,
|
||||
&contents,
|
||||
config,
|
||||
&dot_codex_abs,
|
||||
hooks_config_folder_override.as_ref(),
|
||||
decision.is_trusted(),
|
||||
strict_config,
|
||||
disabled_reason.is_none(),
|
||||
&mut startup_warnings,
|
||||
)
|
||||
.await?;
|
||||
layers.push(project_override_layer_entry(
|
||||
layers.push(entry);
|
||||
}
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||
// If there is no config.toml file, record an empty entry
|
||||
// for this project layer, as this may still have subfolders
|
||||
// that are significant in the overall ConfigLayerStack.
|
||||
let config = merge_root_checkout_project_hooks(
|
||||
fs,
|
||||
TomlValue::Table(toml::map::Map::new()),
|
||||
hooks_config_folder_override.as_ref(),
|
||||
decision.is_trusted(),
|
||||
ProjectConfigFileKind::Base.file_name(),
|
||||
)
|
||||
.await?;
|
||||
layers.push(project_layer_entry(
|
||||
&dot_codex_abs,
|
||||
config,
|
||||
disabled_reason.clone(),
|
||||
hooks_config_folder_override.clone(),
|
||||
));
|
||||
}
|
||||
Err(err) => {
|
||||
let config_file_display = config_file.as_path().display();
|
||||
return Err(io::Error::new(
|
||||
err.kind(),
|
||||
format!(
|
||||
"Failed to read {} {config_file_display}: {err}",
|
||||
ProjectConfigFileKind::Base.parse_error_label()
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let override_file = dot_codex_abs.join(ProjectConfigFileKind::Override.file_name());
|
||||
match fs.read_file_text(&override_file, /*sandbox*/ None).await {
|
||||
Ok(contents) => {
|
||||
let entry = load_project_layer_from_contents(
|
||||
fs,
|
||||
ProjectConfigFileKind::Override,
|
||||
&override_file,
|
||||
&contents,
|
||||
&dot_codex_abs,
|
||||
disabled_reason.clone(),
|
||||
hooks_config_folder_override.clone(),
|
||||
decision.is_trusted(),
|
||||
strict_config,
|
||||
disabled_reason.is_none(),
|
||||
&mut startup_warnings,
|
||||
)
|
||||
.await?;
|
||||
layers.push(entry);
|
||||
}
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||
if hooks_config_folder_override.is_some() {
|
||||
let config = merge_root_checkout_project_hooks(
|
||||
@@ -1387,7 +1358,7 @@ async fn load_project_layers(
|
||||
TomlValue::Table(toml::map::Map::new()),
|
||||
hooks_config_folder_override.as_ref(),
|
||||
decision.is_trusted(),
|
||||
CONFIG_OVERRIDE_TOML_FILE,
|
||||
ProjectConfigFileKind::Override.file_name(),
|
||||
)
|
||||
.await?;
|
||||
layers.push(project_override_layer_entry(
|
||||
@@ -1403,7 +1374,8 @@ async fn load_project_layers(
|
||||
return Err(io::Error::new(
|
||||
err.kind(),
|
||||
format!(
|
||||
"Failed to read project override config file {override_file_display}: {err}"
|
||||
"Failed to read {} {override_file_display}: {err}",
|
||||
ProjectConfigFileKind::Override.parse_error_label()
|
||||
),
|
||||
));
|
||||
}
|
||||
@@ -1416,6 +1388,88 @@ async fn load_project_layers(
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn load_project_layer_from_contents(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
kind: ProjectConfigFileKind,
|
||||
config_file: &AbsolutePathBuf,
|
||||
contents: &str,
|
||||
dot_codex_abs: &AbsolutePathBuf,
|
||||
disabled_reason: Option<String>,
|
||||
hooks_config_folder_override: Option<AbsolutePathBuf>,
|
||||
is_trusted: bool,
|
||||
strict_config: bool,
|
||||
is_enabled: bool,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
) -> io::Result<ConfigLayerEntry> {
|
||||
let config: TomlValue = match toml::from_str(contents) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
if is_trusted {
|
||||
let config_file_display = config_file.as_path().display();
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Error parsing {} {config_file_display}: {err}",
|
||||
kind.parse_error_label()
|
||||
),
|
||||
));
|
||||
}
|
||||
return Ok(project_layer_entry_for_kind(
|
||||
kind,
|
||||
dot_codex_abs,
|
||||
TomlValue::Table(toml::map::Map::new()),
|
||||
disabled_reason,
|
||||
hooks_config_folder_override,
|
||||
));
|
||||
}
|
||||
};
|
||||
let config = prepare_project_layer_config(
|
||||
fs,
|
||||
config_file,
|
||||
kind.file_name(),
|
||||
contents,
|
||||
config,
|
||||
dot_codex_abs,
|
||||
hooks_config_folder_override.as_ref(),
|
||||
is_trusted,
|
||||
strict_config,
|
||||
is_enabled,
|
||||
startup_warnings,
|
||||
)
|
||||
.await?;
|
||||
Ok(project_layer_entry_for_kind(
|
||||
kind,
|
||||
dot_codex_abs,
|
||||
config,
|
||||
disabled_reason,
|
||||
hooks_config_folder_override,
|
||||
))
|
||||
}
|
||||
|
||||
fn project_layer_entry_for_kind(
|
||||
kind: ProjectConfigFileKind,
|
||||
dot_codex_abs: &AbsolutePathBuf,
|
||||
config: TomlValue,
|
||||
disabled_reason: Option<String>,
|
||||
hooks_config_folder_override: Option<AbsolutePathBuf>,
|
||||
) -> ConfigLayerEntry {
|
||||
match kind {
|
||||
ProjectConfigFileKind::Base => project_layer_entry(
|
||||
dot_codex_abs,
|
||||
config,
|
||||
disabled_reason,
|
||||
hooks_config_folder_override,
|
||||
),
|
||||
ProjectConfigFileKind::Override => project_override_layer_entry(
|
||||
dot_codex_abs,
|
||||
config,
|
||||
disabled_reason,
|
||||
hooks_config_folder_override,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn prepare_project_layer_config(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
|
||||
@@ -409,19 +409,18 @@ impl ConfigLayerStack {
|
||||
/// Returns a new stack with the user layer copied from `other`, preserving
|
||||
/// every non-user layer already present in this stack.
|
||||
pub fn with_user_layer_from(&self, other: &Self) -> Self {
|
||||
let user_layers = other
|
||||
.layers
|
||||
.iter()
|
||||
.filter(|layer| is_user_config_layer(&layer.name))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let mut layers = self
|
||||
.layers
|
||||
.iter()
|
||||
.filter(|layer| !is_user_config_layer(&layer.name))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
for user_layer in user_layers {
|
||||
for user_layer in other
|
||||
.layers
|
||||
.iter()
|
||||
.filter(|layer| is_user_config_layer(&layer.name))
|
||||
.cloned()
|
||||
{
|
||||
insert_user_layer(&mut layers, user_layer);
|
||||
}
|
||||
self.with_layers(layers)
|
||||
@@ -526,31 +525,29 @@ fn is_user_config_layer(layer: &ConfigLayerSource) -> bool {
|
||||
|
||||
fn insert_user_layer(layers: &mut Vec<ConfigLayerEntry>, user_layer: ConfigLayerEntry) {
|
||||
let index = layers.iter().position(|layer| {
|
||||
let sibling_override_should_insert_before = match (&user_layer.name, &layer.name) {
|
||||
(
|
||||
ConfigLayerSource::User {
|
||||
file,
|
||||
profile: None,
|
||||
},
|
||||
ConfigLayerSource::UserOverride {
|
||||
file: override_file,
|
||||
},
|
||||
) => user_base_file_has_sibling_override(file, override_file),
|
||||
(
|
||||
ConfigLayerSource::UserOverride {
|
||||
file: override_file,
|
||||
},
|
||||
ConfigLayerSource::User {
|
||||
file,
|
||||
profile: None,
|
||||
},
|
||||
) => !user_base_file_has_sibling_override(file, override_file),
|
||||
_ => false,
|
||||
};
|
||||
layer.name.precedence() > user_layer.name.precedence()
|
||||
|| matches!(
|
||||
(&user_layer.name, &layer.name),
|
||||
(
|
||||
ConfigLayerSource::User {
|
||||
file,
|
||||
profile: None,
|
||||
},
|
||||
ConfigLayerSource::UserOverride {
|
||||
file: override_file,
|
||||
}
|
||||
) if user_base_file_has_sibling_override(file, override_file)
|
||||
)
|
||||
|| matches!(
|
||||
(&user_layer.name, &layer.name),
|
||||
(
|
||||
ConfigLayerSource::UserOverride {
|
||||
file: override_file,
|
||||
},
|
||||
ConfigLayerSource::User {
|
||||
file,
|
||||
profile: None,
|
||||
}
|
||||
) if !user_base_file_has_sibling_override(file, override_file)
|
||||
)
|
||||
|| sibling_override_should_insert_before
|
||||
});
|
||||
match index {
|
||||
Some(index) => layers.insert(index, user_layer),
|
||||
@@ -569,9 +566,9 @@ fn user_base_file_has_sibling_override(
|
||||
}
|
||||
|
||||
fn active_user_layer_index(layers: &[ConfigLayerEntry]) -> Option<usize> {
|
||||
layers.iter().enumerate().rev().find_map(|(index, layer)| {
|
||||
matches!(layer.name, ConfigLayerSource::User { .. }).then_some(index)
|
||||
})
|
||||
layers
|
||||
.iter()
|
||||
.rposition(|layer| matches!(layer.name, ConfigLayerSource::User { .. }))
|
||||
}
|
||||
|
||||
/// Ensures precedence ordering of config layers is correct. Returns the index
|
||||
|
||||
@@ -593,10 +593,9 @@ pub async fn load_exec_policy(config_stack: &ConfigLayerStack) -> Result<Policy,
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(config_folder) = layer.config_folder() {
|
||||
if !policy_folders.insert(config_folder.clone()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(config_folder) = layer.config_folder()
|
||||
&& policy_folders.insert(config_folder.clone())
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -114,9 +114,10 @@ pub(crate) fn discover_handlers(
|
||||
continue;
|
||||
}
|
||||
let hooks_config_folder = layer.hooks_config_folder();
|
||||
let json_hooks = if hooks_config_folder.as_ref().is_some_and(|folder| {
|
||||
loaded_hooks_json_folders.insert(folder.as_path().to_path_buf())
|
||||
}) {
|
||||
let json_hooks = if hooks_config_folder
|
||||
.as_ref()
|
||||
.is_some_and(|folder| loaded_hooks_json_folders.insert(folder.clone()))
|
||||
{
|
||||
load_hooks_json(hooks_config_folder.as_deref(), &mut warnings)
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -260,8 +260,11 @@ fn render_non_file_layer_details(layer: &ConfigLayerEntry) -> Vec<Line<'static>>
|
||||
render_mdm_layer_details(layer)
|
||||
}
|
||||
ConfigLayerSource::System { .. }
|
||||
| ConfigLayerSource::SystemOverride { .. }
|
||||
| ConfigLayerSource::User { .. }
|
||||
| ConfigLayerSource::UserOverride { .. }
|
||||
| ConfigLayerSource::Project { .. }
|
||||
| ConfigLayerSource::ProjectOverride { .. }
|
||||
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } => Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -385,15 +388,27 @@ fn format_config_layer_source(source: &ConfigLayerSource) -> String {
|
||||
ConfigLayerSource::System { file } => {
|
||||
format!("system ({})", file.as_path().display())
|
||||
}
|
||||
ConfigLayerSource::SystemOverride { file } => {
|
||||
format!("system override ({})", file.as_path().display())
|
||||
}
|
||||
ConfigLayerSource::User { file, .. } => {
|
||||
format!("user ({})", file.as_path().display())
|
||||
}
|
||||
ConfigLayerSource::UserOverride { file } => {
|
||||
format!("user override ({})", file.as_path().display())
|
||||
}
|
||||
ConfigLayerSource::Project { dot_codex_folder } => {
|
||||
format!(
|
||||
"project ({}/config.toml)",
|
||||
dot_codex_folder.as_path().display()
|
||||
)
|
||||
}
|
||||
ConfigLayerSource::ProjectOverride { dot_codex_folder } => {
|
||||
format!(
|
||||
"project override ({}/config.override.toml)",
|
||||
dot_codex_folder.as_path().display()
|
||||
)
|
||||
}
|
||||
ConfigLayerSource::SessionFlags => "session-flags".to_string(),
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => {
|
||||
format!("legacy managed_config.toml ({})", file.as_path().display())
|
||||
|
||||
Reference in New Issue
Block a user