Add a loader for executor-local config layers (#37406)

## What changed

- Add `load_local_config_layers` to return raw configuration and requirements layers with their sources, base directories, precedence order, and cloud insertion positions.
- Support projecting layer stacks to selected TOML paths while preserving non-table override blockers and adjusting cloud insertion positions when empty layers are removed.
- Reuse project discovery and trust handling for local reads, including linked-worktree hooks, without resolving relative paths or including profiles, session flags, thread layers, or cloud-provided values.

## Testing

- Cover layer ordering, raw relative paths, legacy requirements, trust filtering, projection semantics, and untrusted linked-worktree hook handling.

GitOrigin-RevId: a1098af5650ac44e5a230ba9a5fe918927986d05
This commit is contained in:
sayan-oai
2026-08-07 08:28:10 +00:00
committed by copyberry
parent c87a218bd3
commit 957f8eedee
5 changed files with 831 additions and 90 deletions
+366
View File
@@ -0,0 +1,366 @@
use super::discover_project_layers;
use super::layer_io;
use super::load_config_toml_for_required_layer_raw;
use super::load_requirements_toml;
use super::load_root_checkout_project_config;
use super::project_root_markers_from_config;
use super::project_trust_context;
use super::requirements_layers_from_legacy_scheme;
use super::system_config_toml_file_with_overrides;
use super::system_requirements_toml_file_with_overrides;
use crate::CONFIG_TOML_FILE;
use crate::ConfigLayerSource;
use crate::LoaderOverrides;
use crate::RequirementSource;
use crate::RequirementsLayerEntry;
use crate::default_project_root_markers;
use crate::merge_toml_values;
use codex_file_system::ExecutorFileSystem;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::BTreeMap;
use std::io;
use std::path::Path;
use toml::Value as TomlValue;
/// Executor-local configuration and requirements layers before schema-specific
/// path resolution or requirements composition.
#[derive(Debug, Clone, PartialEq)]
pub struct LocalConfigLayers {
pub config: LocalTomlLayerStack<ConfigLayerSource>,
pub requirements: LocalTomlLayerStack<RequirementSource>,
}
impl LocalConfigLayers {
/// Retains only the requested TOML paths and drops empty layers.
///
/// An empty path selects the entire document. RPC boundaries should reject
/// that form if whole-document reads are not part of their contract.
pub fn project(self, config_paths: &[Vec<String>], requirements_paths: &[Vec<String>]) -> Self {
Self {
config: self.config.project(config_paths),
requirements: self.requirements.project(requirements_paths),
}
}
}
/// One ordered set of executor-local TOML layers.
#[derive(Debug, Clone, PartialEq)]
pub struct LocalTomlLayerStack<S> {
/// Layers ordered from lowest to highest precedence.
pub layers: Vec<LocalTomlLayer<S>>,
/// Position at which a caller should insert cloud-provided layers.
pub cloud_insertion_index: usize,
}
impl<S> LocalTomlLayerStack<S> {
fn project(self, paths: &[Vec<String>]) -> Self {
let selectors = SelectorNode::from_paths(paths);
let mut projected_layers = Vec::new();
let mut cloud_insertion_index = 0;
for (index, layer) in self.layers.into_iter().enumerate() {
let Some(toml) = project_toml(&layer.toml, &selectors) else {
continue;
};
if index < self.cloud_insertion_index {
cloud_insertion_index += 1;
}
projected_layers.push(LocalTomlLayer { toml, ..layer });
}
Self {
layers: projected_layers,
cloud_insertion_index,
}
}
}
/// One executor-local TOML source with the directory used to interpret its
/// relative paths.
#[derive(Debug, Clone, PartialEq)]
pub struct LocalTomlLayer<S> {
pub source: S,
pub base_dir: AbsolutePathBuf,
pub toml: TomlValue,
}
/// Loads the fixed executor-local configuration sources used by environment
/// config reads.
///
/// Cloud, selected profiles, session flags, and thread-provided layers are not
/// included. Project discovery uses only the executor's system and base-user
/// configuration.
pub async fn load_local_config_layers(
fs: &dyn ExecutorFileSystem,
codex_home: &Path,
cwd: &AbsolutePathBuf,
) -> io::Result<LocalConfigLayers> {
load_local_config_layers_with_overrides(fs, codex_home, cwd, &LoaderOverrides::default()).await
}
pub(super) async fn load_local_config_layers_with_overrides(
fs: &dyn ExecutorFileSystem,
codex_home: &Path,
cwd: &AbsolutePathBuf,
overrides: &LoaderOverrides,
) -> io::Result<LocalConfigLayers> {
let codex_home = AbsolutePathBuf::from_absolute_path(codex_home)?;
let loaded_managed = layer_io::load_config_layers_internal(
fs,
codex_home.as_path(),
overrides.clone(),
/*strict_config*/ false,
)
.await?;
let system_file = system_config_toml_file_with_overrides(overrides)?;
let system =
load_config_toml_for_required_layer_raw(fs, &system_file, /*strict_config*/ false).await?;
let user_file = codex_home.join(CONFIG_TOML_FILE);
let user =
load_config_toml_for_required_layer_raw(fs, &user_file, /*strict_config*/ false).await?;
let mut discovery_config = TomlValue::Table(toml::map::Map::new());
merge_toml_values(&mut discovery_config, &system.toml);
merge_toml_values(&mut discovery_config, &user.toml);
let project_root_markers = project_root_markers_from_config(&discovery_config)?
.unwrap_or_else(default_project_root_markers);
let trust_context = project_trust_context(
fs,
&discovery_config,
cwd,
&project_root_markers,
codex_home.as_path(),
&user_file,
)
.await?;
let project_layers = discover_project_layers(
fs,
cwd,
&trust_context.project_root,
&trust_context,
codex_home.as_path(),
/*strict_config*/ false,
)
.await?;
let mut config_layers = vec![
LocalTomlLayer {
source: ConfigLayerSource::System { file: system_file },
base_dir: system.base_dir,
toml: system.toml,
},
LocalTomlLayer {
source: ConfigLayerSource::User {
file: user_file,
profile: None,
},
base_dir: user.base_dir,
toml: user.toml,
},
];
append_project_layers(fs, &mut config_layers, project_layers.layers).await?;
let requirements =
local_requirements_layers(fs, codex_home.as_path(), overrides, loaded_managed.clone())
.await?;
append_legacy_config_layers(&mut config_layers, loaded_managed, &codex_home)?;
Ok(LocalConfigLayers {
config: LocalTomlLayerStack {
layers: config_layers,
// Cloud config follows the required system layer.
cloud_insertion_index: 1,
},
requirements,
})
}
async fn append_project_layers(
fs: &dyn ExecutorFileSystem,
output: &mut Vec<LocalTomlLayer<ConfigLayerSource>>,
layers: Vec<super::DiscoveredProjectLayer>,
) -> io::Result<()> {
for layer in layers {
if layer.disabled_reason.is_some() {
continue;
}
let mut config = layer.config;
if layer.hooks_config_folder_override.is_some()
&& let Some(table) = config.as_table_mut()
{
table.remove("hooks");
}
output.push(LocalTomlLayer {
source: ConfigLayerSource::Project {
dot_codex_folder: layer.dot_codex_folder.clone(),
},
base_dir: layer.dot_codex_folder,
toml: config,
});
let Some(hooks_config_folder) = layer.hooks_config_folder_override else {
continue;
};
let root_config =
load_root_checkout_project_config(fs, &hooks_config_folder, /*is_trusted*/ true)
.await?;
let Some(hooks) = root_config.get("hooks") else {
continue;
};
output.push(LocalTomlLayer {
source: ConfigLayerSource::Project {
dot_codex_folder: hooks_config_folder.clone(),
},
base_dir: hooks_config_folder,
toml: TomlValue::Table(toml::map::Map::from_iter([(
"hooks".to_string(),
hooks.clone(),
)])),
});
}
Ok(())
}
fn append_legacy_config_layers(
output: &mut Vec<LocalTomlLayer<ConfigLayerSource>>,
loaded: layer_io::LoadedConfigLayers,
codex_home: &AbsolutePathBuf,
) -> io::Result<()> {
if let Some(config) = loaded.managed_config {
let base_dir = config.file.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Managed config file {} has no parent directory",
config.file.as_path().display()
),
)
})?;
output.push(LocalTomlLayer {
source: ConfigLayerSource::LegacyManagedConfigTomlFromFile { file: config.file },
base_dir,
toml: config.managed_config,
});
}
if let Some(config) = loaded.managed_config_from_mdm {
output.push(LocalTomlLayer {
source: ConfigLayerSource::LegacyManagedConfigTomlFromMdm,
base_dir: codex_home.clone(),
toml: config.managed_config,
});
}
Ok(())
}
async fn local_requirements_layers(
fs: &dyn ExecutorFileSystem,
codex_home: &Path,
overrides: &LoaderOverrides,
loaded_managed: layer_io::LoadedConfigLayers,
) -> io::Result<LocalTomlLayerStack<RequirementSource>> {
let system_file = system_requirements_toml_file_with_overrides(overrides)?;
let system = load_requirements_toml(fs, &system_file).await?;
let cloud_insertion_index = usize::from(system.is_some());
let mut entries = Vec::new();
entries.extend(system);
entries.extend(requirements_layers_from_legacy_scheme(
loaded_managed,
codex_home,
)?);
#[cfg(target_os = "macos")]
{
let codex_home = AbsolutePathBuf::from_absolute_path(codex_home)?;
entries.extend(
super::macos::load_managed_admin_requirements_layer(
overrides
.macos_managed_config_requirements_base64
.as_deref(),
)
.await?
.map(|layer| layer.with_base_dir(codex_home)),
);
}
let mut layers = Vec::with_capacity(entries.len());
for entry in entries {
layers.push(local_requirements_layer(entry)?);
}
Ok(LocalTomlLayerStack {
layers,
cloud_insertion_index,
})
}
fn local_requirements_layer(
entry: RequirementsLayerEntry,
) -> io::Result<LocalTomlLayer<RequirementSource>> {
let (source, toml, base_dir) = entry.into_raw_parts()?;
let base_dir = base_dir.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("requirements layer {source} has no base directory"),
)
})?;
Ok(LocalTomlLayer {
source,
base_dir,
toml,
})
}
#[derive(Default)]
struct SelectorNode {
terminal: bool,
children: BTreeMap<String, SelectorNode>,
}
impl SelectorNode {
fn from_paths(paths: &[Vec<String>]) -> Self {
let mut root = Self::default();
for path in paths {
root.insert(path);
}
root
}
fn insert(&mut self, path: &[String]) {
if self.terminal {
return;
}
let Some((segment, remaining)) = path.split_first() else {
self.terminal = true;
self.children.clear();
return;
};
self.children
.entry(segment.clone())
.or_default()
.insert(remaining);
}
}
fn project_toml(value: &TomlValue, selector: &SelectorNode) -> Option<TomlValue> {
let projected = project_toml_value(value, selector);
if !selector.terminal && projected.as_table().is_some_and(toml::map::Map::is_empty) {
return None;
}
Some(projected)
}
fn project_toml_value(value: &TomlValue, selector: &SelectorNode) -> TomlValue {
if selector.terminal {
return value.clone();
}
let Some(table) = value.as_table() else {
// Preserve a non-table ancestor so it can still override lower layers.
return value.clone();
};
let mut projected = toml::map::Map::new();
for (key, value) in table {
let Some(child_selector) = selector.children.get(key) else {
continue;
};
projected.insert(key.clone(), project_toml_value(value, child_selector));
}
TomlValue::Table(projected)
}
+192 -88
View File
@@ -1,4 +1,5 @@
mod layer_io;
mod local;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(test)]
@@ -51,6 +52,11 @@ use std::path::Path;
use std::path::PathBuf;
use toml::Value as TomlValue;
pub use local::LocalConfigLayers;
pub use local::LocalTomlLayer;
pub use local::LocalTomlLayerStack;
pub use local::load_local_config_layers;
#[cfg(unix)]
const SYSTEM_CONFIG_TOML_FILE_UNIX: &str = "/etc/codex/config.toml";
@@ -188,6 +194,7 @@ pub async fn load_config_layers_state(
// requirements layers for backwards compatibility.
requirements_layers.extend(requirements_layers_from_legacy_scheme(
loaded_config_layers.clone(),
codex_home,
)?);
requirements_layers.extend(managed_preferences_requirements_layer);
}
@@ -499,18 +506,36 @@ async fn load_config_toml_for_required_layer(
strict_config: bool,
create_entry: impl FnOnce(TomlValue) -> ConfigLayerEntry,
) -> io::Result<ConfigLayerEntry> {
let loaded = load_config_toml_for_required_layer_raw(fs, toml_file, strict_config).await?;
let toml_value = resolve_relative_paths_in_config_toml(loaded.toml, loaded.base_dir.as_path())?;
Ok(create_entry(toml_value))
}
#[derive(Debug, Clone)]
struct LoadedTomlFile {
toml: TomlValue,
base_dir: AbsolutePathBuf,
}
async fn load_config_toml_for_required_layer_raw(
fs: &dyn ExecutorFileSystem,
toml_file: &AbsolutePathBuf,
strict_config: bool,
) -> io::Result<LoadedTomlFile> {
let config_parent = toml_file.as_path().parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Config file {} has no parent directory",
toml_file.as_path().display()
),
)
})?;
let base_dir = AbsolutePathBuf::from_absolute_path(config_parent)?;
let toml_file_uri = PathUri::from_abs_path(toml_file);
let toml_value = match fs.read_file_text(&toml_file_uri, /*sandbox*/ None).await {
Ok(contents) => {
let config_parent = toml_file.as_path().parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Config file {} has no parent directory",
toml_file.as_path().display()
),
)
})?;
let config: TomlValue = toml::from_str(&contents).map_err(|err| {
let config_error =
config_error_from_toml(toml_file.as_path(), &contents, err.clone());
@@ -524,7 +549,7 @@ async fn load_config_toml_for_required_layer(
config_parent,
)?;
}
resolve_relative_paths_in_config_toml(config, config_parent)
Ok(config)
}
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
@@ -541,7 +566,10 @@ async fn load_config_toml_for_required_layer(
}
}?;
Ok(create_entry(toml_value))
Ok(LoadedTomlFile {
toml: toml_value,
base_dir,
})
}
fn validate_config_toml_strictly(
@@ -750,6 +778,7 @@ fn windows_program_data_dir_from_known_folder() -> io::Result<PathBuf> {
fn requirements_layers_from_legacy_scheme(
loaded_config_layers: LoadedConfigLayers,
codex_home: &Path,
) -> io::Result<Vec<RequirementsLayerEntry>> {
// List the file-backed legacy layer first because requirements layers are
// composed lowest-precedence to highest-precedence, and MDM has higher
@@ -762,18 +791,31 @@ fn requirements_layers_from_legacy_scheme(
let layer_count =
usize::from(managed_config.is_some()) + usize::from(managed_config_from_mdm.is_some());
let mut layers = Vec::with_capacity(layer_count);
for (source, config) in managed_config
let codex_home = AbsolutePathBuf::from_absolute_path(codex_home)?;
for (source, config, base_dir) in managed_config
.map(|c| {
(
let base_dir = c.file.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Managed config file {} has no parent directory",
c.file.as_path().display()
),
)
})?;
Ok::<_, io::Error>((
RequirementSource::LegacyManagedConfigTomlFromFile { file: c.file },
c.managed_config,
)
base_dir,
))
})
.transpose()?
.into_iter()
.chain(managed_config_from_mdm.map(|config| {
(
RequirementSource::LegacyManagedConfigTomlFromMdm,
config.managed_config,
codex_home.clone(),
)
}))
{
@@ -785,10 +827,13 @@ fn requirements_layers_from_legacy_scheme(
)
})?;
layers.push(RequirementsLayerEntry::from_toml_value(
source,
legacy_requirements_to_toml_value(legacy_config)?,
));
layers.push(
RequirementsLayerEntry::from_toml_value(
source,
legacy_requirements_to_toml_value(legacy_config)?,
)
.with_base_dir(base_dir),
);
}
Ok(layers)
@@ -1212,6 +1257,20 @@ struct LoadedProjectLayers {
startup_warnings: Vec<String>,
}
#[derive(Debug, Clone)]
struct DiscoveredProjectLayer {
dot_codex_folder: AbsolutePathBuf,
config: TomlValue,
disabled_reason: Option<String>,
hooks_config_folder_override: Option<AbsolutePathBuf>,
load_root_checkout_hooks: bool,
}
struct DiscoveredProjectLayers {
layers: Vec<DiscoveredProjectLayer>,
startup_warnings: Vec<String>,
}
/// 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,
@@ -1226,6 +1285,52 @@ async fn load_project_layers(
codex_home: &Path,
strict_config: bool,
) -> io::Result<LoadedProjectLayers> {
let discovered = discover_project_layers(
fs,
cwd,
project_root,
trust_context,
codex_home,
strict_config,
)
.await?;
let mut layers = Vec::with_capacity(discovered.layers.len());
for layer in discovered.layers {
let config =
resolve_relative_paths_in_config_toml(layer.config, layer.dot_codex_folder.as_path())?;
let config = if layer.load_root_checkout_hooks {
merge_root_checkout_project_hooks(
fs,
config,
layer.hooks_config_folder_override.as_ref(),
layer.disabled_reason.is_none(),
)
.await?
} else {
config
};
layers.push(project_layer_entry(
&layer.dot_codex_folder,
config,
layer.disabled_reason,
layer.hooks_config_folder_override,
));
}
Ok(LoadedProjectLayers {
layers,
startup_warnings: discovered.startup_warnings,
})
}
async fn discover_project_layers(
fs: &dyn ExecutorFileSystem,
cwd: &AbsolutePathBuf,
project_root: &AbsolutePathBuf,
trust_context: &ProjectTrustContext,
codex_home: &Path,
strict_config: bool,
) -> io::Result<DiscoveredProjectLayers> {
let codex_home_abs = AbsolutePathBuf::from_absolute_path(codex_home)?;
let codex_home_normalized =
normalize_path(codex_home_abs.as_path()).unwrap_or_else(|_| codex_home_abs.to_path_buf());
@@ -1282,12 +1387,13 @@ async fn load_project_layers(
),
));
}
layers.push(project_layer_entry(
&dot_codex_abs,
TomlValue::Table(toml::map::Map::new()),
disabled_reason.clone(),
hooks_config_folder_override.clone(),
));
layers.push(DiscoveredProjectLayer {
dot_codex_folder: dot_codex_abs,
config: TomlValue::Table(toml::map::Map::new()),
disabled_reason,
hooks_config_folder_override,
load_root_checkout_hooks: false,
});
continue;
}
};
@@ -1301,47 +1407,32 @@ async fn load_project_layers(
)?;
}
let ignored_project_config_keys = sanitize_project_config(&mut config);
let config =
resolve_relative_paths_in_config_toml(config, dot_codex_abs.as_path())?;
let config = merge_root_checkout_project_hooks(
fs,
config,
hooks_config_folder_override.as_ref(),
decision.is_trusted(),
)
.await?;
if disabled_reason.is_none() && !ignored_project_config_keys.is_empty() {
startup_warnings.push(project_ignored_config_keys_warning(
&dot_codex_abs,
&ignored_project_config_keys,
));
}
let entry = project_layer_entry(
&dot_codex_abs,
layers.push(DiscoveredProjectLayer {
dot_codex_folder: dot_codex_abs,
config,
disabled_reason.clone(),
hooks_config_folder_override.clone(),
);
layers.push(entry);
disabled_reason,
hooks_config_folder_override,
load_root_checkout_hooks: true,
});
}
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(),
)
.await?;
layers.push(project_layer_entry(
&dot_codex_abs,
config,
layers.push(DiscoveredProjectLayer {
dot_codex_folder: dot_codex_abs,
config: TomlValue::Table(toml::map::Map::new()),
disabled_reason,
hooks_config_folder_override,
));
load_root_checkout_hooks: true,
});
} else {
let config_file_display = config_file.as_path().display();
return Err(io::Error::new(
@@ -1353,7 +1444,7 @@ async fn load_project_layers(
}
}
Ok(LoadedProjectLayers {
Ok(DiscoveredProjectLayers {
layers,
startup_warnings,
})
@@ -1370,43 +1461,10 @@ async fn merge_root_checkout_project_hooks(
let Some(hooks_config_folder) = hooks_config_folder_override else {
return Ok(config);
};
let hooks_config_file = hooks_config_folder.join(CONFIG_TOML_FILE);
let hooks_config_file_uri = PathUri::from_abs_path(&hooks_config_file);
let root_config = match fs
.read_file_text(&hooks_config_file_uri, /*sandbox*/ None)
.await
{
Ok(contents) => {
let parsed: TomlValue = match toml::from_str(&contents) {
Ok(parsed) => parsed,
Err(err) => {
if is_trusted {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Error parsing project hooks config file {}: {err}",
hooks_config_file.as_path().display()
),
));
}
TomlValue::Table(toml::map::Map::new())
}
};
resolve_relative_paths_in_config_toml(parsed, hooks_config_folder.as_path())?
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {
TomlValue::Table(toml::map::Map::new())
}
Err(err) => {
return Err(io::Error::new(
err.kind(),
format!(
"Failed to read project hooks config file {}: {err}",
hooks_config_file.as_path().display()
),
));
}
};
let root_config =
load_root_checkout_project_config(fs, hooks_config_folder, is_trusted).await?;
let root_config =
resolve_relative_paths_in_config_toml(root_config, hooks_config_folder.as_path())?;
let Some(config_table) = config.as_table_mut() else {
return Ok(config);
@@ -1417,6 +1475,52 @@ async fn merge_root_checkout_project_hooks(
}
Ok(config)
}
async fn load_root_checkout_project_config(
fs: &dyn ExecutorFileSystem,
hooks_config_folder: &AbsolutePathBuf,
is_trusted: bool,
) -> io::Result<TomlValue> {
let hooks_config_file = hooks_config_folder.join(CONFIG_TOML_FILE);
let hooks_config_file_uri = PathUri::from_abs_path(&hooks_config_file);
Ok(
match fs
.read_file_text(&hooks_config_file_uri, /*sandbox*/ None)
.await
{
Ok(contents) => {
let parsed: TomlValue = match toml::from_str(&contents) {
Ok(parsed) => parsed,
Err(err) => {
if is_trusted {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Error parsing project hooks config file {}: {err}",
hooks_config_file.as_path().display()
),
));
}
TomlValue::Table(toml::map::Map::new())
}
};
parsed
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {
TomlValue::Table(toml::map::Map::new())
}
Err(err) => {
return Err(io::Error::new(
err.kind(),
format!(
"Failed to read project hooks config file {}: {err}",
hooks_config_file.as_path().display()
),
));
}
},
)
}
/// The legacy mechanism for specifying admin-enforced configuration is to read
/// from a file like `/etc/codex/managed_config.toml` that has the same
/// structure as `config.toml` where fields like `approval_policy` can specify
+214 -2
View File
@@ -70,10 +70,21 @@ impl ExecutorFileSystem for TestFileSystem {
fn get_metadata<'a>(
&'a self,
_path: &'a PathUri,
path: &'a PathUri,
_sandbox: Option<&'a FileSystemSandboxContext>,
) -> ExecutorFileSystemFuture<'a, FileMetadata> {
Box::pin(async move { unimplemented!("test filesystem only supports reads") })
Box::pin(async move {
let path = path.to_abs_path()?;
let metadata = tokio::fs::symlink_metadata(path.as_path()).await?;
Ok(FileMetadata {
is_directory: metadata.is_dir(),
is_file: metadata.is_file(),
is_symlink: metadata.file_type().is_symlink(),
size: metadata.len(),
created_at_ms: 0,
modified_at_ms: 0,
})
})
}
fn read_directory<'a>(
@@ -254,3 +265,204 @@ model = "gpt-dev"
.await
.expect("profile-v2 should allow unrelated legacy profiles in base user config");
}
#[test]
fn local_layer_projection_preserves_override_blockers_and_cloud_position() {
let tmp = tempdir().expect("tempdir");
let base_dir = AbsolutePathBuf::from_absolute_path(tmp.path()).expect("absolute base");
let layer = |source, contents| LocalTomlLayer {
source,
base_dir: base_dir.clone(),
toml: toml::from_str(contents).expect("valid TOML"),
};
let layers = LocalConfigLayers {
config: LocalTomlLayerStack {
layers: vec![
layer(
ConfigLayerSource::System {
file: base_dir.join("system.toml"),
},
r#"ignored=true
"literal.key"="literal"
array=[1,2]
[a]
b=1
c=2
"#,
),
layer(
ConfigLayerSource::SessionFlags,
"a=2\nignored=false\nonly_user=true",
),
layer(
ConfigLayerSource::LegacyManagedConfigTomlFromMdm,
"[a]\nunrequested=true",
),
],
cloud_insertion_index: 1,
},
requirements: LocalTomlLayerStack {
layers: Vec::<LocalTomlLayer<RequirementSource>>::new(),
cloud_insertion_index: 0,
},
};
let only_user = layers.clone().project(&[vec!["only_user".into()]], &[]);
assert_eq!(only_user.config.layers.len(), 1);
assert_eq!(only_user.config.cloud_insertion_index, 0);
let projected = layers.project(
&[
vec!["a".into(), "b".into()],
vec!["array".into(), "unused".into()],
vec!["literal.key".into()],
],
&[],
);
assert_eq!(
projected.config,
LocalTomlLayerStack {
layers: vec![
layer(
ConfigLayerSource::System {
file: base_dir.join("system.toml"),
},
r#""literal.key"="literal"
array=[1,2]
[a]
b=1"#,
),
layer(ConfigLayerSource::SessionFlags, "a=2"),
layer(ConfigLayerSource::LegacyManagedConfigTomlFromMdm, "[a]"),
],
cloud_insertion_index: 1,
}
);
let mut merged = TomlValue::Table(toml::map::Map::new());
for layer in projected.config.layers {
merge_toml_values(&mut merged, &layer.toml);
}
assert_eq!(
merged.get("a"),
Some(&TomlValue::Table(toml::map::Map::new()))
);
}
#[tokio::test]
async fn local_layers_keep_raw_paths_order_and_legacy_requirements() {
let tmp = tempdir().expect("tempdir");
let codex_home = tmp.path().join("codex-home");
let project = tmp.path().join("project");
let dot_codex = project.join(".codex");
let system_dir = tmp.path().join("system");
let managed_dir = tmp.path().join("managed");
for dir in [&codex_home, &dot_codex, &system_dir, &managed_dir] {
std::fs::create_dir_all(dir).expect("create fixture directory");
}
std::fs::write(project.join(".project-root"), "").expect("write project marker");
let project_key = project_trust_key(&project);
let project_key = TomlValue::String(project_key).to_string();
let user_config = |trust_level| {
format!(
"project_root_markers=[\".project-root\"]\nmodel_instructions_file=\"./user.md\"\n[projects.{project_key}]\ntrust_level=\"{trust_level}\""
)
};
let user_file = codex_home.join(CONFIG_TOML_FILE);
std::fs::write(&user_file, user_config("trusted")).expect("write user config");
let system_file = system_dir.join(CONFIG_TOML_FILE);
std::fs::write(&system_file, "model_instructions_file = \"./system.md\"")
.expect("write system config");
std::fs::write(
dot_codex.join(CONFIG_TOML_FILE),
"model_instructions_file = \"./project.md\"\nopenai_base_url = \"https://ignored\"",
)
.expect("write project config");
let managed_file = managed_dir.join("managed_config.toml");
std::fs::write(
&managed_file,
"approval_policy = \"never\"\nsandbox_mode = \"workspace-write\"\nmodel_instructions_file = \"./managed.md\"",
)
.expect("write legacy managed config");
let requirements_file = managed_dir.join("requirements.toml");
std::fs::write(
&requirements_file,
"allowed_sandbox_modes = [\"read-only\"]\nlog_dir = \"./logs\"",
)
.expect("write system requirements");
let mut overrides = LoaderOverrides::with_managed_config_path_for_tests(managed_file.clone());
overrides.system_config_path = Some(system_file.clone());
overrides.system_requirements_path = Some(requirements_file.clone());
let cwd = AbsolutePathBuf::from_absolute_path(&project).expect("absolute cwd");
let layers = local::load_local_config_layers_with_overrides(
&TestFileSystem,
&codex_home,
&cwd,
&overrides,
)
.await
.expect("load local layers");
assert_eq!(
layers
.config
.layers
.iter()
.map(|layer| layer.base_dir.to_path_buf())
.collect::<Vec<_>>(),
vec![
system_dir.clone(),
codex_home.clone(),
dot_codex.clone(),
managed_dir.clone(),
]
);
assert_eq!(layers.config.cloud_insertion_index, 1);
assert_eq!(layers.requirements.cloud_insertion_index, 1);
assert_eq!(
(
layers.config.layers[2].toml.clone(),
layers.config.layers[3]
.toml
.get("model_instructions_file")
.cloned(),
layers.requirements.layers[0]
.toml
.get("log_dir")
.cloned(),
layers.requirements.layers[1].toml.clone(),
),
(
toml::from_str("model_instructions_file = \"./project.md\"")
.expect("project TOML"),
Some(TomlValue::String("./managed.md".into())),
Some(TomlValue::String("./logs".into())),
toml::from_str(
"allowed_approval_policies=[\"never\"]\nallowed_sandbox_modes=[\"read-only\",\"workspace-write\"]"
)
.expect("legacy requirements TOML"),
)
);
std::fs::write(&user_file, user_config("untrusted")).expect("write user config");
let layers = local::load_local_config_layers_with_overrides(
&TestFileSystem,
&codex_home,
&cwd,
&overrides,
)
.await
.expect("load local layers");
assert_eq!(
layers
.config
.layers
.iter()
.filter(|layer| matches!(layer.source, ConfigLayerSource::Project { .. }))
.count(),
0
);
}
@@ -36,6 +36,19 @@ impl RequirementsLayerEntry {
self.base_dir = Some(base_dir);
self
}
pub(crate) fn into_raw_parts(
self,
) -> Result<(RequirementSource, TomlValue, Option<AbsolutePathBuf>), RequirementsCompositionError>
{
let Self {
source,
toml,
base_dir,
} = self;
let toml = parse_layer_toml(&toml, &source)?;
Ok((source, toml, base_dir))
}
}
#[derive(Clone, Debug)]
@@ -2748,6 +2748,52 @@ async fn linked_worktree_project_layers_keep_worktree_config_but_use_root_repo_h
Ok(())
}
#[tokio::test]
async fn malformed_untrusted_linked_worktree_does_not_read_root_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?;
tokio::fs::create_dir_all(repo_root.join(".codex")).await?;
write_linked_worktree_pointer(&repo_root, &worktree_root).await?;
tokio::fs::write(worktree_root.join(".codex").join(CONFIG_TOML_FILE), "foo =").await?;
tokio::fs::write(repo_root.join(".codex").join(CONFIG_TOML_FILE), [0xff]).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(),
&codex_config::NoopThreadConfigLoader,
)
.await?;
let project_layers = layers
.all_layers_high_to_low()
.filter(|layer| matches!(layer.name, ConfigLayerSource::Project { .. }))
.collect::<Vec<_>>();
assert_eq!(project_layers.len(), 1);
assert!(project_layers[0].disabled_reason.is_some());
assert_eq!(
project_layers[0].config,
TomlValue::Table(toml::map::Map::new())
);
Ok(())
}
#[tokio::test]
async fn linked_worktree_project_layers_use_root_repo_hooks_without_worktree_config_toml()
-> std::io::Result<()> {