mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
config: model cloud-managed bundle tiers
This commit is contained in:
@@ -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()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user