config: model cloud-managed bundle tiers

This commit is contained in:
hefuc-oai
2026-07-06 19:37:57 +00:00
parent 4ec0da7e88
commit d88e24b358
12 changed files with 479 additions and 38 deletions

View File

@@ -110,9 +110,11 @@ pub(crate) fn bundle_from_response(response: ConfigBundleResponse) -> CloudConfi
CloudConfigBundle {
config_toml: CloudConfigTomlBundle {
enterprise_managed: config_toml,
..Default::default()
},
requirements_toml: CloudRequirementsTomlBundle {
enterprise_managed: requirements_toml,
..Default::default()
},
}
}

View File

@@ -16,6 +16,7 @@ fn test_bundle() -> CloudConfigBundle {
name: "Base config".to_string(),
contents: "model = \"gpt-5\"".to_string(),
}],
..Default::default()
},
requirements_toml: CloudRequirementsTomlBundle {
enterprise_managed: vec![CloudRequirementsFragment {
@@ -23,6 +24,7 @@ fn test_bundle() -> CloudConfigBundle {
name: "Base requirements".to_string(),
contents: "allowed_approval_policies = [\"never\"]".to_string(),
}],
..Default::default()
},
}
}

View File

@@ -197,9 +197,11 @@ fn test_bundle() -> CloudConfigBundle {
CloudConfigBundle {
config_toml: CloudConfigTomlBundle {
enterprise_managed: vec![test_config_fragment()],
..Default::default()
},
requirements_toml: CloudRequirementsTomlBundle {
enterprise_managed: vec![test_requirements_fragment()],
..Default::default()
},
}
}
@@ -228,6 +230,7 @@ fn invalid_config_bundle() -> CloudConfigBundle {
name: "Invalid config".to_string(),
contents: "model = [".to_string(),
}],
..Default::default()
},
requirements_toml: CloudRequirementsTomlBundle::default(),
}
@@ -340,6 +343,7 @@ fn bundle_shape_tag_describes_sorted_enterprise_sources() {
bundle_shape_tag(Some(&CloudConfigBundle {
config_toml: CloudConfigTomlBundle {
enterprise_managed: vec![test_config_fragment()],
..Default::default()
},
requirements_toml: CloudRequirementsTomlBundle::default(),
})),
@@ -350,6 +354,7 @@ fn bundle_shape_tag_describes_sorted_enterprise_sources() {
config_toml: CloudConfigTomlBundle::default(),
requirements_toml: CloudRequirementsTomlBundle {
enterprise_managed: vec![test_requirements_fragment()],
..Default::default()
},
})),
"enterprise_requirements"
@@ -358,9 +363,11 @@ fn bundle_shape_tag_describes_sorted_enterprise_sources() {
bundle_shape_tag(Some(&CloudConfigBundle {
config_toml: CloudConfigTomlBundle {
enterprise_managed: vec![test_config_fragment()],
..Default::default()
},
requirements_toml: CloudRequirementsTomlBundle {
enterprise_managed: vec![test_requirements_fragment()],
..Default::default()
},
})),
"enterprise_config,enterprise_requirements"
@@ -602,6 +609,7 @@ async fn get_bundle_ignores_cache_for_different_auth_identity() {
name: "Replacement requirements".to_string(),
contents: "allowed_approval_policies = [\"on-request\"]".to_string(),
}],
..Default::default()
},
};
let fetcher = Arc::new(SequenceBundleClient::new(vec![Ok(
@@ -931,6 +939,7 @@ async fn get_bundle_does_not_use_cache_when_auth_identity_is_incomplete() {
name: "Replacement requirements".to_string(),
contents: "allowed_approval_policies = [\"on-request\"]".to_string(),
}],
..Default::default()
},
};
let fetcher = Arc::new(SequenceBundleClient::new(vec![Ok(
@@ -996,6 +1005,7 @@ async fn refresh_from_remote_updates_cached_bundle() {
name: "Replacement requirements".to_string(),
contents: "allowed_approval_policies = [\"on-request\"]".to_string(),
}],
..Default::default()
},
};
let codex_home = tempdir().expect("tempdir");
@@ -1067,6 +1077,7 @@ fn bundle_response_conversion_preserves_fragment_order() {
contents: "model = \"low\"".to_string(),
},
],
..Default::default()
},
requirements_toml: CloudRequirementsTomlBundle {
enterprise_managed: vec![CloudRequirementsFragment {
@@ -1074,6 +1085,7 @@ fn bundle_response_conversion_preserves_fragment_order() {
name: "High requirements".to_string(),
contents: "allowed_approval_policies = [\"never\"]".to_string(),
}],
..Default::default()
},
}
);

View File

@@ -20,6 +20,7 @@ pub(crate) fn validate_bundle(
let CloudConfigBundleLayers {
enterprise_managed_config: _,
enterprise_managed_requirements,
..
} = bundle_layers;
compose_requirements(enterprise_managed_requirements).map_err(|err| {

View File

@@ -5,11 +5,14 @@
//! layer entries while preserving each bucket's insertion semantics.
use crate::CloudConfigFragment;
use crate::CloudManagedLayer;
use crate::ConfigLayerEntry;
use crate::RequirementSource;
use crate::RequirementsLayerEntry;
use crate::cloud_config_layers::CloudConfigLayerError;
use crate::cloud_config_layers::cloud_config_layers_from_fragments_strict;
use crate::cloud_config_layers::cloud_managed_config_layers_from_fragments;
use crate::cloud_config_layers::cloud_managed_config_layers_from_fragments_strict;
use crate::cloud_config_layers_from_fragments;
use codex_utils_absolute_path::AbsolutePathBuf;
use futures::future::BoxFuture;
@@ -35,23 +38,64 @@ impl CloudConfigBundle {
} = self;
let CloudConfigTomlBundle {
enterprise_managed: config_enterprise_managed,
managed_layers: config_managed_layers,
} = config_toml;
let CloudRequirementsTomlBundle {
enterprise_managed: requirements_enterprise_managed,
managed_layers: requirements_managed_layers,
} = requirements_toml;
config_enterprise_managed.is_empty() && requirements_enterprise_managed.is_empty()
config_enterprise_managed.is_empty()
&& config_managed_layers.baseline.is_empty()
&& config_managed_layers.system_overlay.is_empty()
&& requirements_enterprise_managed.is_empty()
&& requirements_managed_layers.baseline.is_empty()
&& requirements_managed_layers.system_overlay.is_empty()
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct CloudConfigTomlBundle {
pub enterprise_managed: Vec<CloudConfigFragment>,
#[serde(
default,
skip_serializing_if = "CloudConfigTomlManagedLayers::is_empty"
)]
pub managed_layers: CloudConfigTomlManagedLayers,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct CloudConfigTomlManagedLayers {
pub baseline: Vec<CloudConfigFragment>,
pub system_overlay: Vec<CloudConfigFragment>,
}
impl CloudConfigTomlManagedLayers {
fn is_empty(&self) -> bool {
self.baseline.is_empty() && self.system_overlay.is_empty()
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct CloudRequirementsTomlBundle {
pub enterprise_managed: Vec<CloudRequirementsFragment>,
#[serde(
default,
skip_serializing_if = "CloudRequirementsTomlManagedLayers::is_empty"
)]
pub managed_layers: CloudRequirementsTomlManagedLayers,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct CloudRequirementsTomlManagedLayers {
pub baseline: Vec<CloudRequirementsFragment>,
pub system_overlay: Vec<CloudRequirementsFragment>,
}
impl CloudRequirementsTomlManagedLayers {
fn is_empty(&self) -> bool {
self.baseline.is_empty() && self.system_overlay.is_empty()
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
@@ -67,9 +111,17 @@ pub struct CloudRequirementsFragment {
/// inserted relative to local/system/user layers.
#[derive(Clone, Debug)]
pub struct CloudConfigBundleLayers {
/// Baseline config layers in `ConfigLayerStack` order.
pub baseline_config: Vec<ConfigLayerEntry>,
/// System-overlay config layers in `ConfigLayerStack` order.
pub system_overlay_config: Vec<ConfigLayerEntry>,
/// Enterprise-managed config layers in `ConfigLayerStack` order.
pub enterprise_managed_config: Vec<ConfigLayerEntry>,
/// Enterprise-managed requirements layers in requirements layer merge order.
/// Baseline requirements layers in requirements merge order.
pub baseline_requirements: Vec<RequirementsLayerEntry>,
/// System-overlay requirements layers in requirements merge order.
pub system_overlay_requirements: Vec<RequirementsLayerEntry>,
/// Enterprise-managed requirements layers in requirements merge order.
pub enterprise_managed_requirements: Vec<RequirementsLayerEntry>,
}
@@ -99,43 +151,97 @@ impl CloudConfigBundleLayers {
config_toml:
CloudConfigTomlBundle {
enterprise_managed: config_enterprise_managed,
managed_layers:
CloudConfigTomlManagedLayers {
baseline: config_baseline,
system_overlay: config_system_overlay,
},
},
requirements_toml:
CloudRequirementsTomlBundle {
enterprise_managed: requirements_enterprise_managed,
managed_layers:
CloudRequirementsTomlManagedLayers {
baseline: requirements_baseline,
system_overlay: requirements_system_overlay,
},
},
} = bundle;
let parse_managed_config_fragments = |fragments, layer| {
if strict_config {
cloud_managed_config_layers_from_fragments_strict(fragments, base_dir, layer)
} else {
cloud_managed_config_layers_from_fragments(fragments, base_dir, layer)
}
};
let baseline_config =
parse_managed_config_fragments(config_baseline, CloudManagedLayer::Baseline)?;
let enterprise_managed_config = if strict_config {
cloud_config_layers_from_fragments_strict(config_enterprise_managed, base_dir)?
} else {
cloud_config_layers_from_fragments(config_enterprise_managed, base_dir)?
};
let system_overlay_config = parse_managed_config_fragments(
config_system_overlay,
CloudManagedLayer::SystemOverlay,
)?;
let mut enterprise_managed_requirements = requirements_enterprise_managed
.into_iter()
.map(|fragment| {
RequirementsLayerEntry::from_toml(
RequirementSource::EnterpriseManaged {
id: fragment.id,
name: fragment.name,
},
fragment.contents,
)
.with_base_dir(base_dir.clone())
})
.collect::<Vec<_>>();
// Bundle fragments arrive highest-priority first, while requirements
// layers are merged lowest-priority to highest-priority.
enterprise_managed_requirements.reverse();
let baseline_requirements =
requirements_layers_from_fragments(requirements_baseline, base_dir, |id, name| {
RequirementSource::CloudManaged {
layer: CloudManagedLayer::Baseline,
id,
name,
}
});
let enterprise_managed_requirements = requirements_layers_from_fragments(
requirements_enterprise_managed,
base_dir,
|id, name| RequirementSource::EnterpriseManaged { id, name },
);
let system_overlay_requirements = requirements_layers_from_fragments(
requirements_system_overlay,
base_dir,
|id, name| RequirementSource::CloudManaged {
layer: CloudManagedLayer::SystemOverlay,
id,
name,
},
);
Ok(Self {
baseline_config,
enterprise_managed_config,
system_overlay_config,
baseline_requirements,
enterprise_managed_requirements,
system_overlay_requirements,
})
}
}
fn requirements_layers_from_fragments(
fragments: Vec<CloudRequirementsFragment>,
base_dir: &AbsolutePathBuf,
source_for_fragment: impl Fn(String, String) -> RequirementSource,
) -> Vec<RequirementsLayerEntry> {
let mut layers = fragments
.into_iter()
.map(|fragment| {
RequirementsLayerEntry::from_toml(
source_for_fragment(fragment.id, fragment.name),
fragment.contents,
)
.with_base_dir(base_dir.clone())
})
.collect::<Vec<_>>();
// Bundle fragments arrive highest-priority first, while requirements
// layers are merged lowest-priority to highest-priority.
layers.reverse();
layers
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CloudConfigBundleLoadErrorCode {
Auth,

View File

@@ -1,6 +1,7 @@
use super::*;
use crate::ConfigLayerSource;
use crate::ConfigRequirementsToml;
use crate::Sourced;
use crate::compose_requirements;
use codex_protocol::protocol::AskForApproval;
use pretty_assertions::assert_eq;
@@ -9,6 +10,22 @@ use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use tempfile::tempdir;
fn config_fragment(id: &str) -> CloudConfigFragment {
CloudConfigFragment {
id: id.to_string(),
name: id.to_string(),
contents: format!("model = \"{id}\""),
}
}
fn requirements_fragment(id: &str) -> CloudRequirementsFragment {
CloudRequirementsFragment {
id: id.to_string(),
name: id.to_string(),
contents: format!("guardian_policy_config = \"{id}\""),
}
}
#[tokio::test]
async fn shared_future_runs_once() {
let counter = Arc::new(AtomicUsize::new(0));
@@ -42,6 +59,7 @@ fn bundle_layers_preserve_enterprise_managed_bucket_order() {
contents: "model = \"low\"".to_string(),
},
],
..Default::default()
},
requirements_toml: CloudRequirementsTomlBundle {
enterprise_managed: vec![
@@ -56,6 +74,7 @@ fn bundle_layers_preserve_enterprise_managed_bucket_order() {
contents: "allowed_approval_policies = [\"never\"]".to_string(),
},
],
..Default::default()
},
},
&base_dir,
@@ -91,6 +110,86 @@ fn bundle_layers_preserve_enterprise_managed_bucket_order() {
);
}
#[test]
fn bundle_layers_preserve_managed_bucket_order_and_provenance() {
let tempdir = tempdir().expect("tempdir");
let base_dir = AbsolutePathBuf::from_absolute_path(tempdir.path()).expect("absolute path");
let layers = CloudConfigBundleLayers::from_bundle(
CloudConfigBundle {
config_toml: CloudConfigTomlBundle {
managed_layers: CloudConfigTomlManagedLayers {
baseline: vec![
config_fragment("baseline_high"),
config_fragment("baseline_low"),
],
system_overlay: vec![
config_fragment("overlay_high"),
config_fragment("overlay_low"),
],
},
..Default::default()
},
requirements_toml: CloudRequirementsTomlBundle {
managed_layers: CloudRequirementsTomlManagedLayers {
baseline: vec![requirements_fragment("baseline_req")],
system_overlay: vec![
requirements_fragment("overlay_req_high"),
requirements_fragment("overlay_req_low"),
],
},
..Default::default()
},
},
&base_dir,
)
.expect("bundle should be converted into layers");
assert_eq!(
layers
.baseline_config
.iter()
.chain(&layers.system_overlay_config)
.map(|entry| match &entry.name {
ConfigLayerSource::CloudManaged { layer, id, .. } => (*layer, id.as_str()),
source => panic!("unexpected config layer source: {source:?}"),
})
.collect::<Vec<_>>(),
vec![
(CloudManagedLayer::Baseline, "baseline_low"),
(CloudManagedLayer::Baseline, "baseline_high"),
(CloudManagedLayer::SystemOverlay, "overlay_low"),
(CloudManagedLayer::SystemOverlay, "overlay_high"),
]
);
let requirements = compose_requirements(
layers
.baseline_requirements
.into_iter()
.chain(layers.system_overlay_requirements),
)
.expect("requirements should compose")
.expect("requirements should be present");
assert_eq!(
requirements.guardian_policy_config,
Some(Sourced::new(
"overlay_req_high".to_string(),
RequirementSource::CloudManaged {
layer: CloudManagedLayer::SystemOverlay,
id: "overlay_req_high".to_string(),
name: "overlay_req_high".to_string(),
},
))
);
assert_eq!(
requirements.into_toml(),
ConfigRequirementsToml {
guardian_policy_config: Some("overlay_req_high".to_string()),
..Default::default()
}
);
}
#[test]
fn bundle_layers_can_strict_validate_enterprise_managed_config() {
let tempdir = tempdir().expect("tempdir");
@@ -103,10 +202,9 @@ fn bundle_layers_can_strict_validate_enterprise_managed_config() {
name: "Cloud config".to_string(),
contents: "unknown_key = true".to_string(),
}],
..Default::default()
},
requirements_toml: CloudRequirementsTomlBundle {
enterprise_managed: Vec::new(),
},
requirements_toml: CloudRequirementsTomlBundle::default(),
},
&base_dir,
)

View File

@@ -4,6 +4,7 @@
//! fragment, resolves relative path fields against the cloud config base
//! directory, and returns layers in `ConfigLayerStack` order.
use crate::CloudManagedLayer;
use crate::ConfigLayerEntry;
use crate::ConfigLayerSource;
use crate::TomlValue;
@@ -22,7 +23,7 @@ use thiserror::Error;
///
/// The bundle orders fragments from highest precedence to lowest precedence.
/// This module returns config layers in stack order, so callers can append the
/// result between system and user config without re-sorting.
/// result at the bucket's stack position without re-sorting.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CloudConfigFragment {
pub id: String,
@@ -69,19 +70,56 @@ pub fn cloud_config_layers_from_fragments(
fragments: impl IntoIterator<Item = CloudConfigFragment>,
base_dir: &AbsolutePathBuf,
) -> Result<Vec<ConfigLayerEntry>, CloudConfigLayerError> {
cloud_config_layers_from_fragments_impl(fragments, base_dir, /*strict_config*/ false)
cloud_config_layers_from_fragments_impl(
fragments,
base_dir,
|id, name| ConfigLayerSource::EnterpriseManaged { id, name },
/*strict_config*/ false,
)
}
pub(crate) fn cloud_config_layers_from_fragments_strict(
fragments: impl IntoIterator<Item = CloudConfigFragment>,
base_dir: &AbsolutePathBuf,
) -> Result<Vec<ConfigLayerEntry>, CloudConfigLayerError> {
cloud_config_layers_from_fragments_impl(fragments, base_dir, /*strict_config*/ true)
cloud_config_layers_from_fragments_impl(
fragments,
base_dir,
|id, name| ConfigLayerSource::EnterpriseManaged { id, name },
/*strict_config*/ true,
)
}
pub(crate) fn cloud_managed_config_layers_from_fragments(
fragments: impl IntoIterator<Item = CloudConfigFragment>,
base_dir: &AbsolutePathBuf,
layer: CloudManagedLayer,
) -> Result<Vec<ConfigLayerEntry>, CloudConfigLayerError> {
cloud_config_layers_from_fragments_impl(
fragments,
base_dir,
move |id, name| ConfigLayerSource::CloudManaged { layer, id, name },
/*strict_config*/ false,
)
}
pub(crate) fn cloud_managed_config_layers_from_fragments_strict(
fragments: impl IntoIterator<Item = CloudConfigFragment>,
base_dir: &AbsolutePathBuf,
layer: CloudManagedLayer,
) -> Result<Vec<ConfigLayerEntry>, CloudConfigLayerError> {
cloud_config_layers_from_fragments_impl(
fragments,
base_dir,
move |id, name| ConfigLayerSource::CloudManaged { layer, id, name },
/*strict_config*/ true,
)
}
fn cloud_config_layers_from_fragments_impl(
fragments: impl IntoIterator<Item = CloudConfigFragment>,
base_dir: &AbsolutePathBuf,
source_for_fragment: impl Fn(String, String) -> ConfigLayerSource,
strict_config: bool,
) -> Result<Vec<ConfigLayerEntry>, CloudConfigLayerError> {
let mut layers = Vec::new();
@@ -104,10 +142,7 @@ fn cloud_config_layers_from_fragments_impl(
}
})?;
layers.push(ConfigLayerEntry::new_with_raw_toml(
ConfigLayerSource::EnterpriseManaged {
id: fragment.id,
name: fragment.name,
},
source_for_fragment(fragment.id, fragment.name),
resolved,
raw_toml,
base_dir.clone(),

View File

@@ -39,8 +39,10 @@ pub use cloud_config_bundle::CloudConfigBundleLoadError;
pub use cloud_config_bundle::CloudConfigBundleLoadErrorCode;
pub use cloud_config_bundle::CloudConfigBundleLoader;
pub use cloud_config_bundle::CloudConfigTomlBundle;
pub use cloud_config_bundle::CloudConfigTomlManagedLayers;
pub use cloud_config_bundle::CloudRequirementsFragment;
pub use cloud_config_bundle::CloudRequirementsTomlBundle;
pub use cloud_config_bundle::CloudRequirementsTomlManagedLayers;
pub use cloud_config_layers::CloudConfigFragment;
pub use cloud_config_layers::CloudConfigFragmentSource;
pub use cloud_config_layers::CloudConfigLayerError;

View File

@@ -31,8 +31,10 @@ Precedence is **top overrides bottom**:
4. `Project` config (`.codex/config.toml`)
5. `User` profile config, when present
6. `User` config (`config.toml`)
7. `EnterpriseManaged` cloud-managed config bundle layers
8. `System` config (`/etc/codex/config.toml` or the Windows system config path)
7. `CloudManaged(SystemOverlay)` config bundle layers
8. `EnterpriseManaged` cloud-managed config bundle layers
9. `System` config (`/etc/codex/config.toml` or the Windows system config path)
10. `CloudManaged(Baseline)` config bundle layers
`ConfigLayerStack` stores layers in the opposite order internally: lowest
precedence first, highest precedence last, so later layers override earlier

View File

@@ -82,9 +82,11 @@ async fn first_layer_config_error_from_entries(layers: &[ConfigLayerEntry]) -> O
/// composed with config-style TOML merging plus field-specific handling for
/// hooks, rules, deny-read permissions, and remote sandbox config:
///
/// - system `/etc/codex/requirements.toml` (Unix) or
/// - baseline: cloud-managed baseline fragments
/// - system: `/etc/codex/requirements.toml` (Unix) or
/// `%ProgramData%\OpenAI\Codex\requirements.toml` (Windows)
/// - cloud: enterprise-managed cloud config bundle requirements
/// - overlay: cloud-managed system-overlay fragments
/// - legacy: managed_config.toml reinterpreted as requirements.toml
/// - admin: managed preferences (*)
///
@@ -93,10 +95,12 @@ async fn first_layer_config_error_from_entries(layers: &[ConfigLayerEntry]) -> O
///
/// Configuration is built up from multiple layers in the following order:
///
/// - baseline: cloud-managed baseline fragments
/// - admin: managed preferences (*)
/// - system `/etc/codex/config.toml` (Unix) or
/// `%ProgramData%\OpenAI\Codex\config.toml` (Windows)
/// - cloud enterprise-managed cloud config bundle fragments
/// - cloud: enterprise-managed cloud config bundle fragments
/// - overlay: cloud-managed system-overlay fragments
/// - user `${CODEX_HOME}/config.toml`
/// - profile `${CODEX_HOME}/<name>.config.toml`, when selected
/// - cwd `${PWD}/config.toml` (loaded but disabled when the directory is untrusted)
@@ -132,10 +136,14 @@ pub async fn load_config_layers_state(
let ignore_user_and_project_exec_policy_rules =
overrides.ignore_user_and_project_exec_policy_rules;
let mut requirements_layers = Vec::new();
let mut bundle_requirements_layers = Vec::new();
let mut cloud_baseline_requirements_layers = Vec::new();
let mut enterprise_managed_requirements_layers = Vec::new();
let mut cloud_system_overlay_requirements_layers = Vec::new();
let mut system_requirements_layer = None;
let managed_preferences_requirements_layer;
let mut cloud_config_layers = Vec::new();
let mut cloud_baseline_config_layers = Vec::new();
let mut enterprise_managed_config_layers = Vec::new();
let mut cloud_system_overlay_config_layers = Vec::new();
if !ignore_managed_requirements {
if let Some(bundle) = cloud_config_bundle.get().await.map_err(io::Error::other)? {
@@ -146,11 +154,19 @@ pub async fn load_config_layers_state(
CloudConfigBundleLayers::from_bundle(bundle, &cloud_config_base_dir)?
};
let CloudConfigBundleLayers {
baseline_config,
system_overlay_config,
enterprise_managed_config,
baseline_requirements,
system_overlay_requirements,
enterprise_managed_requirements,
} = bundle_layers;
bundle_requirements_layers = enterprise_managed_requirements;
cloud_config_layers = enterprise_managed_config;
cloud_baseline_requirements_layers = baseline_requirements;
enterprise_managed_requirements_layers = enterprise_managed_requirements;
cloud_system_overlay_requirements_layers = system_overlay_requirements;
cloud_baseline_config_layers = baseline_config;
enterprise_managed_config_layers = enterprise_managed_config;
cloud_system_overlay_config_layers = system_overlay_config;
}
#[cfg(target_os = "macos")]
@@ -178,8 +194,10 @@ pub async fn load_config_layers_state(
layer_io::load_config_layers_internal(fs, codex_home, overrides.clone(), strict_config)
.await?;
if !ignore_managed_requirements {
requirements_layers.extend(cloud_baseline_requirements_layers);
requirements_layers.extend(system_requirements_layer);
requirements_layers.extend(bundle_requirements_layers);
requirements_layers.extend(enterprise_managed_requirements_layers);
requirements_layers.extend(cloud_system_overlay_requirements_layers);
// Continue to support the legacy `managed_config.toml` locations as
// requirements layers for backwards compatibility.
requirements_layers.extend(requirements_layers_from_legacy_scheme(
@@ -235,8 +253,10 @@ pub async fn load_config_layers_state(
},
)
.await?;
layers.extend(cloud_baseline_config_layers);
layers.push(system_layer);
layers.extend(cloud_config_layers);
layers.extend(enterprise_managed_config_layers);
layers.extend(cloud_system_overlay_config_layers);
// Add the base user config layer. When profile-v2 is selected, add the
// profile config as a second user layer on top so the profile only needs to

View File

@@ -1,4 +1,8 @@
use super::*;
use crate::CloudManagedLayer;
use crate::ConfigLayerStackOrdering;
use crate::ConfigRequirementsToml;
use crate::test_support::CloudConfigBundleFixture;
use codex_file_system::CopyOptions;
use codex_file_system::CreateDirectoryOptions;
use codex_file_system::ExecutorFileSystemFuture;
@@ -104,6 +108,120 @@ impl ExecutorFileSystem for TestFileSystem {
}
}
#[tokio::test]
async fn managed_bundle_layers_follow_contract_precedence() {
let tmp = tempdir().expect("tempdir");
let system_config_path = tmp.path().join("system-config.toml");
let system_requirements_path = tmp.path().join("requirements.toml");
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"user\"\n")
.expect("write user config");
std::fs::write(
&system_config_path,
"model = \"system\"\nmodel_provider = \"system\"\nreview_model = \"system\"\n",
)
.expect("write system config");
std::fs::write(
&system_requirements_path,
"allow_remote_control = true\nguardian_policy_config = \"system\"\n",
)
.expect("write system requirements");
let mut overrides = LoaderOverrides::with_managed_config_path_for_tests(
tmp.path().join("missing-managed-config.toml"),
);
overrides.system_config_path = Some(system_config_path.clone());
let cloud_config_bundle = CloudConfigBundleFixture::default()
.add_managed_config(
CloudManagedLayer::Baseline,
"model = \"baseline\"\nmodel_provider = \"baseline\"",
)
.add_enterprise_config("model_provider = \"enterprise\"\nreview_model = \"enterprise\"")
.add_managed_config(
CloudManagedLayer::SystemOverlay,
"model_provider = \"overlay\"\nreview_model = \"overlay\"",
)
.add_managed_requirement(
CloudManagedLayer::Baseline,
"allow_appshots = true\nguardian_policy_config = \"baseline\"",
)
.add_enterprise_requirement(
"allow_managed_hooks_only = true\nguardian_policy_config = \"enterprise\"",
)
.add_managed_requirement(
CloudManagedLayer::SystemOverlay,
"guardian_policy_config = \"overlay\"",
)
.into_loader();
let stack = load_config_layers_state(
&TestFileSystem,
tmp.path(),
/*cwd*/ None,
&[],
ConfigLoadOptions {
loader_overrides: overrides,
cloud_config_bundle,
..Default::default()
},
&crate::NoopThreadConfigLoader,
)
.await
.expect("managed layers should load");
assert_eq!(
stack
.get_layers(
ConfigLayerStackOrdering::LowestPrecedenceFirst,
/*include_disabled*/ false,
)
.into_iter()
.map(|layer| layer.name.clone())
.collect::<Vec<_>>(),
vec![
ConfigLayerSource::CloudManaged {
layer: CloudManagedLayer::Baseline,
id: "managed_cfg_1".to_string(),
name: "baseline config 1".to_string(),
},
ConfigLayerSource::System {
file: AbsolutePathBuf::from_absolute_path(&system_config_path)
.expect("absolute system config path"),
},
ConfigLayerSource::EnterpriseManaged {
id: "cfg_1".to_string(),
name: "Base config".to_string(),
},
ConfigLayerSource::CloudManaged {
layer: CloudManagedLayer::SystemOverlay,
id: "managed_cfg_1".to_string(),
name: "system-overlay config 1".to_string(),
},
ConfigLayerSource::User {
file: AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, tmp.path()),
profile: None,
},
]
);
assert_eq!(
stack.effective_config(),
TomlValue::Table(toml::toml! {
model = "user"
model_provider = "overlay"
review_model = "overlay"
})
);
assert_eq!(
stack.requirements_toml(),
&ConfigRequirementsToml {
allow_managed_hooks_only: Some(true),
allow_appshots: Some(true),
allow_remote_control: Some(true),
guardian_policy_config: Some("overlay".to_string()),
..Default::default()
}
);
}
#[tokio::test]
async fn profile_v2_rejects_matching_legacy_profile_in_base_user_config() {
let tmp = tempdir().expect("tempdir");

View File

@@ -5,6 +5,7 @@
use crate::CloudConfigBundle;
use crate::CloudConfigBundleLoader;
use crate::CloudConfigFragment;
use crate::CloudManagedLayer;
use crate::CloudRequirementsFragment;
#[derive(Debug, Clone, Default)]
@@ -65,6 +66,48 @@ impl CloudConfigBundleFixture {
self
}
pub fn add_managed_requirement(
mut self,
layer: CloudManagedLayer,
contents: impl Into<String>,
) -> Self {
let fragments = match layer {
CloudManagedLayer::Baseline => {
&mut self.bundle.requirements_toml.managed_layers.baseline
}
CloudManagedLayer::SystemOverlay => {
&mut self.bundle.requirements_toml.managed_layers.system_overlay
}
};
let index = fragments.len() + 1;
fragments.push(CloudRequirementsFragment {
id: format!("managed_req_{index}"),
name: format!("{layer} requirements {index}"),
contents: contents.into(),
});
self
}
pub fn add_managed_config(
mut self,
layer: CloudManagedLayer,
contents: impl Into<String>,
) -> Self {
let fragments = match layer {
CloudManagedLayer::Baseline => &mut self.bundle.config_toml.managed_layers.baseline,
CloudManagedLayer::SystemOverlay => {
&mut self.bundle.config_toml.managed_layers.system_overlay
}
};
let index = fragments.len() + 1;
fragments.push(CloudConfigFragment {
id: format!("managed_cfg_{index}"),
name: format!("{layer} config {index}"),
contents: contents.into(),
});
self
}
pub fn into_bundle(self) -> CloudConfigBundle {
self.bundle
}