diff --git a/codex-rs/backend-client/src/lib.rs b/codex-rs/backend-client/src/lib.rs index 6cbeaf30b2..63989be4d5 100644 --- a/codex-rs/backend-client/src/lib.rs +++ b/codex-rs/backend-client/src/lib.rs @@ -15,6 +15,7 @@ pub use types::ConfigBundleResponse; pub use types::ConsumeRateLimitResetCreditCode; pub use types::ConsumeRateLimitResetCreditResponse; pub use types::DeliveredConfigToml; +pub use types::DeliveredManagedLayers; pub use types::DeliveredRequirementsToml; pub use types::DeliveredTomlFragment; pub use types::PaginatedListTaskListItem; diff --git a/codex-rs/backend-client/src/types.rs b/codex-rs/backend-client/src/types.rs index 18ab323986..6c740afeab 100644 --- a/codex-rs/backend-client/src/types.rs +++ b/codex-rs/backend-client/src/types.rs @@ -1,6 +1,7 @@ pub use codex_backend_openapi_models::models::ConfigBundleResponse; pub use codex_backend_openapi_models::models::CreditStatusDetails; pub use codex_backend_openapi_models::models::DeliveredConfigToml; +pub use codex_backend_openapi_models::models::DeliveredManagedLayers; pub use codex_backend_openapi_models::models::DeliveredRequirementsToml; pub use codex_backend_openapi_models::models::DeliveredTomlFragment; pub use codex_backend_openapi_models::models::PaginatedListTaskListItem; diff --git a/codex-rs/cloud-config/src/backend.rs b/codex-rs/cloud-config/src/backend.rs index cb99316a70..bcef06da3c 100644 --- a/codex-rs/cloud-config/src/backend.rs +++ b/codex-rs/cloud-config/src/backend.rs @@ -1,11 +1,14 @@ use codex_backend_client::Client as BackendClient; use codex_backend_client::ConfigBundleResponse; +use codex_backend_client::DeliveredManagedLayers; use codex_backend_client::DeliveredTomlFragment; use codex_config::CloudConfigBundle; use codex_config::CloudConfigFragment; use codex_config::CloudConfigTomlBundle; +use codex_config::CloudConfigTomlManagedLayers; use codex_config::CloudRequirementsFragment; use codex_config::CloudRequirementsTomlBundle; +use codex_config::CloudRequirementsTomlManagedLayers; use codex_login::CodexAuth; use std::future::Future; @@ -92,16 +95,30 @@ pub(crate) fn bundle_from_response(response: ConfigBundleResponse) -> CloudConfi .config_toml .flatten() .map(|config_toml| *config_toml) - .and_then(|config_toml| config_toml.enterprise_managed.flatten()) - .unwrap_or_default() - .into_iter() - .map(config_fragment_from_delivered) - .collect(); + .unwrap_or_default(); let requirements_toml = response .requirements_toml .flatten() .map(|requirements_toml| *requirements_toml) - .and_then(|requirements_toml| requirements_toml.enterprise_managed.flatten()) + .unwrap_or_default(); + let (config_baseline, config_system_overlay) = managed_fragments_from_delivered( + config_toml.managed_layers, + config_fragment_from_delivered, + ); + let (requirements_baseline, requirements_system_overlay) = managed_fragments_from_delivered( + requirements_toml.managed_layers, + requirements_fragment_from_delivered, + ); + let config_enterprise_managed = config_toml + .enterprise_managed + .flatten() + .unwrap_or_default() + .into_iter() + .map(config_fragment_from_delivered) + .collect(); + let requirements_enterprise_managed = requirements_toml + .enterprise_managed + .flatten() .unwrap_or_default() .into_iter() .map(requirements_fragment_from_delivered) @@ -109,14 +126,41 @@ pub(crate) fn bundle_from_response(response: ConfigBundleResponse) -> CloudConfi CloudConfigBundle { config_toml: CloudConfigTomlBundle { - enterprise_managed: config_toml, + enterprise_managed: config_enterprise_managed, + managed_layers: CloudConfigTomlManagedLayers { + baseline: config_baseline, + system_overlay: config_system_overlay, + }, }, requirements_toml: CloudRequirementsTomlBundle { - enterprise_managed: requirements_toml, + enterprise_managed: requirements_enterprise_managed, + managed_layers: CloudRequirementsTomlManagedLayers { + baseline: requirements_baseline, + system_overlay: requirements_system_overlay, + }, }, } } +fn managed_fragments_from_delivered( + managed_layers: Option>>, + convert: fn(DeliveredTomlFragment) -> T, +) -> (Option>, Option>) { + let managed_layers = managed_layers + .flatten() + .map(|managed_layers| *managed_layers) + .unwrap_or_default(); + let baseline = managed_layers + .baseline + .flatten() + .map(|fragments| fragments.into_iter().map(convert).collect()); + let system_overlay = managed_layers + .system_overlay + .flatten() + .map(|fragments| fragments.into_iter().map(convert).collect()); + (baseline, system_overlay) +} + fn config_fragment_from_delivered(fragment: DeliveredTomlFragment) -> CloudConfigFragment { CloudConfigFragment { id: fragment.id, diff --git a/codex-rs/cloud-config/src/cache.rs b/codex-rs/cloud-config/src/cache.rs index e5d1d4dc84..e5e7bef398 100644 --- a/codex-rs/cloud-config/src/cache.rs +++ b/codex-rs/cloud-config/src/cache.rs @@ -27,6 +27,9 @@ const CLOUD_CONFIG_BUNDLE_CACHE_WRITE_HMAC_KEY: &[u8] = b"codex-cloud-config-bundle-cache-v1-6160ae70-bcfd-4ca8-a99b-40f73b3b072e"; const CLOUD_CONFIG_BUNDLE_CACHE_READ_HMAC_KEYS: &[&[u8]] = &[CLOUD_CONFIG_BUNDLE_CACHE_WRITE_HMAC_KEY]; +const MANAGED_LAYERS_CACHE_WRITE_HMAC_KEY: &[u8] = + b"codex-cloud-config-bundle-cache-v1-managed-layers-8fe40af0-ec6d-4740-900d-d4bdd771ea2f"; +const MANAGED_LAYERS_CACHE_READ_HMAC_KEYS: &[&[u8]] = &[MANAGED_LAYERS_CACHE_WRITE_HMAC_KEY]; type HmacSha256 = Hmac; @@ -71,7 +74,7 @@ impl CloudConfigBundleCache { return Err(CacheLoadStatus::CacheParseFailed(err.to_string())); } }; - let payload_bytes = match cache_payload_bytes(&cache_file.signed_payload) { + let legacy_payload_bytes = match legacy_cache_payload_bytes(&cache_file.signed_payload) { Some(payload_bytes) => payload_bytes, None => { return Err(CacheLoadStatus::CacheParseFailed( @@ -79,9 +82,26 @@ impl CloudConfigBundleCache { )); } }; - if !verify_cache_signature(&payload_bytes, &cache_file.signature) { + if !verify_cache_signature(&legacy_payload_bytes, &cache_file.signature) { return Err(CacheLoadStatus::CacheSignatureInvalid); } + match cache_file.managed_layers_signature.as_deref() { + Some(signature) => { + let payload_bytes = + cache_payload_bytes(&cache_file.signed_payload).ok_or_else(|| { + CacheLoadStatus::CacheParseFailed( + "failed to serialize cache payload".to_string(), + ) + })?; + if !verify_managed_layers_cache_signature(&payload_bytes, signature) { + return Err(CacheLoadStatus::CacheManagedLayersSignatureInvalid); + } + } + None if cache_file.signed_payload.bundle.has_managed_layer_buckets() => { + return Err(CacheLoadStatus::CacheManagedLayersSignatureInvalid); + } + None => {} + } if cache_file.signed_payload.version != CLOUD_CONFIG_BUNDLE_CACHE_VERSION { return Err(CacheLoadStatus::CacheVersionUnsupported( cache_file.signed_payload.version, @@ -116,6 +136,7 @@ impl CloudConfigBundleCache { CacheLoadStatus::CacheReadFailed(_) | CacheLoadStatus::CacheParseFailed(_) | CacheLoadStatus::CacheSignatureInvalid + | CacheLoadStatus::CacheManagedLayersSignatureInvalid ); if warn { @@ -146,10 +167,17 @@ impl CloudConfigBundleCache { account_id, bundle, }; + let legacy_payload_bytes = + legacy_cache_payload_bytes(&signed_payload).ok_or(CloudConfigBundleCacheError)?; let payload_bytes = cache_payload_bytes(&signed_payload).ok_or(CloudConfigBundleCacheError)?; let serialized = serde_json::to_vec_pretty(&CloudConfigBundleCacheFile { - signature: sign_cache_payload(&payload_bytes).ok_or(CloudConfigBundleCacheError)?, + signature: sign_cache_payload(&legacy_payload_bytes) + .ok_or(CloudConfigBundleCacheError)?, + managed_layers_signature: Some( + sign_managed_layers_cache_payload(&payload_bytes) + .ok_or(CloudConfigBundleCacheError)?, + ), signed_payload, }) .map_err(|_| CloudConfigBundleCacheError)?; @@ -179,6 +207,8 @@ pub(super) enum CacheLoadStatus { CacheParseFailed(String), #[error("Cloud config bundle cache failed signature verification.")] CacheSignatureInvalid, + #[error("Cloud config bundle cache failed managed-layers signature verification.")] + CacheManagedLayersSignatureInvalid, #[error("Ignoring cloud config bundle cache because cached identity is incomplete.")] CacheIdentityIncomplete, #[error("Ignoring cloud config bundle cache for different auth identity.")] @@ -199,6 +229,8 @@ pub(super) struct CloudConfigBundleCacheError; pub(super) struct CloudConfigBundleCacheFile { pub(super) signed_payload: CloudConfigBundleCacheSignedPayload, pub(super) signature: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(super) managed_layers_signature: Option, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -217,11 +249,21 @@ pub(super) fn cache_payload_bytes( serde_json::to_vec(&payload).ok() } +pub(super) fn legacy_cache_payload_bytes( + payload: &CloudConfigBundleCacheSignedPayload, +) -> Option> { + let mut legacy_payload = payload.clone(); + legacy_payload.bundle.config_toml.managed_layers = Default::default(); + legacy_payload.bundle.requirements_toml.managed_layers = Default::default(); + cache_payload_bytes(&legacy_payload) +} + pub(super) fn sign_cache_payload(payload_bytes: &[u8]) -> Option { - let mut mac = HmacSha256::new_from_slice(CLOUD_CONFIG_BUNDLE_CACHE_WRITE_HMAC_KEY).ok()?; - mac.update(payload_bytes); - let signature = mac.finalize().into_bytes(); - Some(BASE64_STANDARD.encode(signature)) + sign_cache_payload_with_key(payload_bytes, CLOUD_CONFIG_BUNDLE_CACHE_WRITE_HMAC_KEY) +} + +pub(super) fn sign_managed_layers_cache_payload(payload_bytes: &[u8]) -> Option { + sign_cache_payload_with_key(payload_bytes, MANAGED_LAYERS_CACHE_WRITE_HMAC_KEY) } pub(super) fn verify_cache_signature(payload_bytes: &[u8], signature: &str) -> bool { @@ -235,6 +277,24 @@ pub(super) fn verify_cache_signature(payload_bytes: &[u8], signature: &str) -> b .any(|key| verify_cache_signature_with_key(payload_bytes, &signature_bytes, key)) } +pub(super) fn verify_managed_layers_cache_signature(payload_bytes: &[u8], signature: &str) -> bool { + let signature_bytes = match BASE64_STANDARD.decode(signature) { + Ok(signature_bytes) => signature_bytes, + Err(_) => return false, + }; + + MANAGED_LAYERS_CACHE_READ_HMAC_KEYS + .iter() + .any(|key| verify_cache_signature_with_key(payload_bytes, &signature_bytes, key)) +} + +fn sign_cache_payload_with_key(payload_bytes: &[u8], key: &[u8]) -> Option { + let mut mac = HmacSha256::new_from_slice(key).ok()?; + mac.update(payload_bytes); + let signature = mac.finalize().into_bytes(); + Some(BASE64_STANDARD.encode(signature)) +} + fn verify_cache_signature_with_key( payload_bytes: &[u8], signature_bytes: &[u8], diff --git a/codex-rs/cloud-config/src/cache_tests.rs b/codex-rs/cloud-config/src/cache_tests.rs index 28899f930d..28401865e3 100644 --- a/codex-rs/cloud-config/src/cache_tests.rs +++ b/codex-rs/cloud-config/src/cache_tests.rs @@ -2,8 +2,10 @@ use super::*; use codex_config::AbsolutePathBuf; use codex_config::CloudConfigFragment; use codex_config::CloudConfigTomlBundle; +use codex_config::CloudConfigTomlManagedLayers; use codex_config::CloudRequirementsFragment; use codex_config::CloudRequirementsTomlBundle; +use codex_config::CloudRequirementsTomlManagedLayers; use pretty_assertions::assert_eq; use std::path::Path; use tempfile::tempdir; @@ -16,6 +18,7 @@ fn test_bundle() -> CloudConfigBundle { name: "Base config".to_string(), contents: "model = \"gpt-5\"".to_string(), }], + managed_layers: Default::default(), }, requirements_toml: CloudRequirementsTomlBundle { enterprise_managed: vec![CloudRequirementsFragment { @@ -23,6 +26,7 @@ fn test_bundle() -> CloudConfigBundle { name: "Base requirements".to_string(), contents: "allowed_approval_policies = [\"never\"]".to_string(), }], + managed_layers: Default::default(), }, } } @@ -31,8 +35,24 @@ fn signed_cache_file( signed_payload: CloudConfigBundleCacheSignedPayload, ) -> CloudConfigBundleCacheFile { let payload_bytes = cache_payload_bytes(&signed_payload).expect("payload bytes"); + let legacy_payload_bytes = + legacy_cache_payload_bytes(&signed_payload).expect("legacy payload bytes"); + CloudConfigBundleCacheFile { + signature: sign_cache_payload(&legacy_payload_bytes).expect("signature"), + managed_layers_signature: Some( + sign_managed_layers_cache_payload(&payload_bytes).expect("managed signature"), + ), + signed_payload, + } +} + +fn legacy_signed_cache_file( + signed_payload: CloudConfigBundleCacheSignedPayload, +) -> CloudConfigBundleCacheFile { + let payload_bytes = legacy_cache_payload_bytes(&signed_payload).expect("legacy payload bytes"); CloudConfigBundleCacheFile { signature: sign_cache_payload(&payload_bytes).expect("signature"), + managed_layers_signature: None, signed_payload, } } @@ -49,6 +69,27 @@ fn valid_signed_payload() -> CloudConfigBundleCacheSignedPayload { } } +fn valid_managed_signed_payload() -> CloudConfigBundleCacheSignedPayload { + let mut payload = valid_signed_payload(); + payload.bundle.config_toml.managed_layers = CloudConfigTomlManagedLayers { + baseline: Some(vec![CloudConfigFragment { + id: "cfg_baseline".to_string(), + name: "Config baseline".to_string(), + contents: "model = \"baseline\"".to_string(), + }]), + system_overlay: Some(Vec::new()), + }; + payload.bundle.requirements_toml.managed_layers = CloudRequirementsTomlManagedLayers { + baseline: None, + system_overlay: Some(vec![CloudRequirementsFragment { + id: "req_overlay".to_string(), + name: "Requirements overlay".to_string(), + contents: "allowed_approval_policies = [\"on-request\"]".to_string(), + }]), + }; + payload +} + fn write_cache_file(cache: &CloudConfigBundleCache, cache_file: &CloudConfigBundleCacheFile) { std::fs::write( cache.path(), @@ -61,11 +102,42 @@ fn create_test_cache(codex_home: &Path) -> CloudConfigBundleCache { CloudConfigBundleCache::new(AbsolutePathBuf::resolve_path_against_base(codex_home, "/")) } +#[test] +fn legacy_signature_covers_only_the_legacy_projection() { + let payload = valid_managed_signed_payload(); + let cache_file = signed_cache_file(payload.clone()); + let legacy_payload_bytes = legacy_cache_payload_bytes(&payload).expect("legacy payload bytes"); + let payload_bytes = cache_payload_bytes(&payload).expect("payload bytes"); + let legacy_payload: serde_json::Value = + serde_json::from_slice(&legacy_payload_bytes).expect("legacy payload"); + let full_payload: serde_json::Value = + serde_json::from_slice(&payload_bytes).expect("full payload"); + + assert!( + legacy_payload + .pointer("/bundle/config_toml/managed_layers") + .is_none() + ); + assert!( + full_payload + .pointer("/bundle/config_toml/managed_layers") + .is_some() + ); + assert!(verify_cache_signature( + &legacy_payload_bytes, + &cache_file.signature + )); + assert!(!verify_cache_signature( + &payload_bytes, + &cache_file.signature + )); +} + #[tokio::test] async fn save_writes_signed_payload_and_loads_for_matching_identity() { let codex_home = tempdir().expect("tempdir"); let cache = create_test_cache(codex_home.path()); - let bundle = test_bundle(); + let bundle = valid_managed_signed_payload().bundle; cache .save( @@ -102,6 +174,45 @@ async fn save_writes_signed_payload_and_loads_for_matching_identity() { ); } +#[tokio::test] +async fn load_accepts_legacy_cache_without_managed_data() { + let codex_home = tempdir().expect("tempdir"); + let cache = create_test_cache(codex_home.path()); + let cache_file = legacy_signed_cache_file(valid_signed_payload()); + write_cache_file(&cache, &cache_file); + + assert_eq!( + cache.load(Some("user-12345"), Some("account-12345")).await, + Ok(cache_file.signed_payload) + ); +} + +#[tokio::test] +async fn load_requires_full_signature_for_managed_data() { + let codex_home = tempdir().expect("tempdir"); + let cache = create_test_cache(codex_home.path()); + let cache_file = legacy_signed_cache_file(valid_managed_signed_payload()); + write_cache_file(&cache, &cache_file); + + assert_eq!( + cache.load(Some("user-12345"), Some("account-12345")).await, + Err(CacheLoadStatus::CacheManagedLayersSignatureInvalid) + ); +} + +#[tokio::test] +async fn load_accepts_managed_data_with_both_signatures() { + let codex_home = tempdir().expect("tempdir"); + let cache = create_test_cache(codex_home.path()); + let cache_file = signed_cache_file(valid_managed_signed_payload()); + write_cache_file(&cache, &cache_file); + + assert_eq!( + cache.load(Some("user-12345"), Some("account-12345")).await, + Ok(cache_file.signed_payload) + ); +} + #[tokio::test] async fn load_rejects_missing_request_identity_before_reading_cache_file() { let codex_home = tempdir().expect("tempdir"); @@ -155,6 +266,44 @@ async fn load_rejects_tampered_payload() { ); } +#[tokio::test] +async fn load_rejects_tampered_managed_data() { + let codex_home = tempdir().expect("tempdir"); + let cache = create_test_cache(codex_home.path()); + let mut cache_file = signed_cache_file(valid_managed_signed_payload()); + cache_file + .signed_payload + .bundle + .requirements_toml + .managed_layers + .system_overlay + .as_mut() + .expect("system overlay") + .first_mut() + .expect("requirements fragment") + .contents = "allowed_approval_policies = [\"never\"]".to_string(); + write_cache_file(&cache, &cache_file); + + assert_eq!( + cache.load(Some("user-12345"), Some("account-12345")).await, + Err(CacheLoadStatus::CacheManagedLayersSignatureInvalid) + ); +} + +#[tokio::test] +async fn load_rejects_tampered_managed_layers_signature() { + let codex_home = tempdir().expect("tempdir"); + let cache = create_test_cache(codex_home.path()); + let mut cache_file = signed_cache_file(valid_managed_signed_payload()); + cache_file.managed_layers_signature = Some("tampered".to_string()); + write_cache_file(&cache, &cache_file); + + assert_eq!( + cache.load(Some("user-12345"), Some("account-12345")).await, + Err(CacheLoadStatus::CacheManagedLayersSignatureInvalid) + ); +} + #[tokio::test] async fn load_rejects_cache_for_incomplete_or_different_identity() { let codex_home = tempdir().expect("tempdir"); diff --git a/codex-rs/cloud-config/src/metrics.rs b/codex-rs/cloud-config/src/metrics.rs index c1c02cce9c..52caf68dbb 100644 --- a/codex-rs/cloud-config/src/metrics.rs +++ b/codex-rs/cloud-config/src/metrics.rs @@ -66,9 +66,26 @@ pub(crate) fn bundle_shape_tag(bundle: Option<&CloudConfigBundle>) -> String { if !bundle.config_toml.enterprise_managed.is_empty() { sources.push("enterprise_config"); } + if bundle.config_toml.managed_layers.baseline.is_some() { + sources.push("baseline_config"); + } + if bundle.config_toml.managed_layers.system_overlay.is_some() { + sources.push("system_overlay_config"); + } if !bundle.requirements_toml.enterprise_managed.is_empty() { sources.push("enterprise_requirements"); } + if bundle.requirements_toml.managed_layers.baseline.is_some() { + sources.push("baseline_requirements"); + } + if bundle + .requirements_toml + .managed_layers + .system_overlay + .is_some() + { + sources.push("system_overlay_requirements"); + } if sources.is_empty() { "empty".to_string() diff --git a/codex-rs/cloud-config/src/service.rs b/codex-rs/cloud-config/src/service.rs index 1a7104bf52..83e7c1a7bb 100644 --- a/codex-rs/cloud-config/src/service.rs +++ b/codex-rs/cloud-config/src/service.rs @@ -152,6 +152,30 @@ where elapsed_ms = started_at.elapsed().as_millis(), config_fragments = bundle.config_toml.enterprise_managed.len(), requirements_fragments = bundle.requirements_toml.enterprise_managed.len(), + config_baseline_fragments = ?bundle + .config_toml + .managed_layers + .baseline + .as_ref() + .map(Vec::len), + config_system_overlay_fragments = ?bundle + .config_toml + .managed_layers + .system_overlay + .as_ref() + .map(Vec::len), + requirements_baseline_fragments = ?bundle + .requirements_toml + .managed_layers + .baseline + .as_ref() + .map(Vec::len), + requirements_system_overlay_fragments = ?bundle + .requirements_toml + .managed_layers + .system_overlay + .as_ref() + .map(Vec::len), "Cloud config bundle load completed" ); emit_load_metric("startup", "success", Some(bundle)); diff --git a/codex-rs/cloud-config/src/service_tests.rs b/codex-rs/cloud-config/src/service_tests.rs index dfd4fe50ce..a5676dedbf 100644 --- a/codex-rs/cloud-config/src/service_tests.rs +++ b/codex-rs/cloud-config/src/service_tests.rs @@ -9,12 +9,15 @@ use crate::metrics::bundle_shape_tag; use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use codex_backend_client::ConfigBundleResponse; +use codex_backend_client::DeliveredManagedLayers; use codex_backend_client::DeliveredTomlFragment; use codex_config::AbsolutePathBuf; use codex_config::CloudConfigFragment; use codex_config::CloudConfigTomlBundle; +use codex_config::CloudConfigTomlManagedLayers; use codex_config::CloudRequirementsFragment; use codex_config::CloudRequirementsTomlBundle; +use codex_config::CloudRequirementsTomlManagedLayers; use codex_config::types::AuthCredentialsStoreMode; use codex_login::AuthKeyringBackendKind; use codex_login::auth::AgentIdentityAuth; @@ -197,9 +200,11 @@ fn test_bundle() -> CloudConfigBundle { CloudConfigBundle { config_toml: CloudConfigTomlBundle { enterprise_managed: vec![test_config_fragment()], + managed_layers: Default::default(), }, requirements_toml: CloudRequirementsTomlBundle { enterprise_managed: vec![test_requirements_fragment()], + managed_layers: Default::default(), }, } } @@ -228,11 +233,29 @@ fn invalid_config_bundle() -> CloudConfigBundle { name: "Invalid config".to_string(), contents: "model = [".to_string(), }], + managed_layers: Default::default(), }, requirements_toml: CloudRequirementsTomlBundle::default(), } } +fn invalid_managed_requirements_bundle() -> CloudConfigBundle { + CloudConfigBundle { + config_toml: CloudConfigTomlBundle::default(), + requirements_toml: CloudRequirementsTomlBundle { + enterprise_managed: Vec::new(), + managed_layers: CloudRequirementsTomlManagedLayers { + baseline: None, + system_overlay: Some(vec![CloudRequirementsFragment { + id: "req_invalid".to_string(), + name: "Invalid managed requirements".to_string(), + contents: "allowed_approval_policies = [".to_string(), + }]), + }, + }, + } +} + fn request_error() -> BundleRequestError { BundleRequestError::Retryable(RetryableFailureKind::Request { status_code: None }) } @@ -330,7 +353,7 @@ impl BundleClient for UnauthorizedBundleClient { } #[test] -fn bundle_shape_tag_describes_sorted_enterprise_sources() { +fn bundle_shape_tag_describes_sorted_buckets() { assert_eq!(bundle_shape_tag(/*bundle*/ None), "none"); assert_eq!( bundle_shape_tag(Some(&CloudConfigBundle::default())), @@ -340,6 +363,7 @@ fn bundle_shape_tag_describes_sorted_enterprise_sources() { bundle_shape_tag(Some(&CloudConfigBundle { config_toml: CloudConfigTomlBundle { enterprise_managed: vec![test_config_fragment()], + managed_layers: Default::default(), }, requirements_toml: CloudRequirementsTomlBundle::default(), })), @@ -350,6 +374,7 @@ fn bundle_shape_tag_describes_sorted_enterprise_sources() { config_toml: CloudConfigTomlBundle::default(), requirements_toml: CloudRequirementsTomlBundle { enterprise_managed: vec![test_requirements_fragment()], + managed_layers: Default::default(), }, })), "enterprise_requirements" @@ -358,12 +383,23 @@ fn bundle_shape_tag_describes_sorted_enterprise_sources() { bundle_shape_tag(Some(&CloudConfigBundle { config_toml: CloudConfigTomlBundle { enterprise_managed: vec![test_config_fragment()], + managed_layers: CloudConfigTomlManagedLayers { + baseline: Some(vec![test_config_fragment()]), + system_overlay: Some(Vec::new()), + }, }, requirements_toml: CloudRequirementsTomlBundle { enterprise_managed: vec![test_requirements_fragment()], + managed_layers: CloudRequirementsTomlManagedLayers { + baseline: Some(vec![test_requirements_fragment()]), + system_overlay: Some(Vec::new()), + }, }, })), - "enterprise_config,enterprise_requirements" + concat!( + "baseline_config,baseline_requirements,enterprise_config,", + "enterprise_requirements,system_overlay_config,system_overlay_requirements" + ) ); } @@ -475,30 +511,35 @@ async fn get_bundle_skips_team_like_usage_based_plan() { } #[tokio::test] -async fn get_bundle_rejects_invalid_remote_bundle_before_cache_write() { - let codex_home = tempdir().expect("tempdir"); - let fetcher = Arc::new(StaticBundleClient::new(invalid_config_bundle())); - let service = CloudConfigBundleService::new( - auth_manager_with_plan("business").await, - fetcher.clone(), - codex_home.path().to_path_buf(), - CLOUD_CONFIG_BUNDLE_TIMEOUT, - ); +async fn get_bundle_rejects_invalid_remote_buckets_before_cache_write() { + for bundle in [ + invalid_config_bundle(), + invalid_managed_requirements_bundle(), + ] { + let codex_home = tempdir().expect("tempdir"); + let fetcher = Arc::new(StaticBundleClient::new(bundle)); + let service = CloudConfigBundleService::new( + auth_manager_with_plan("business").await, + fetcher.clone(), + codex_home.path().to_path_buf(), + CLOUD_CONFIG_BUNDLE_TIMEOUT, + ); - let err = service - .load_startup_bundle() - .await - .expect_err("invalid remote bundle should fail closed"); + let err = service + .load_startup_bundle() + .await + .expect_err("invalid remote bundle should fail closed"); - assert_eq!(err.code(), CloudConfigBundleLoadErrorCode::InvalidBundle); - assert!(err.to_string().contains("invalid cloud config bundle")); - assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 1); - assert!( - !codex_home - .path() - .join(CLOUD_CONFIG_BUNDLE_CACHE_FILENAME) - .exists() - ); + assert_eq!(err.code(), CloudConfigBundleLoadErrorCode::InvalidBundle); + assert!(err.to_string().contains("invalid cloud config bundle")); + assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 1); + assert!( + !codex_home + .path() + .join(CLOUD_CONFIG_BUNDLE_CACHE_FILENAME) + .exists() + ); + } } #[tokio::test] @@ -602,6 +643,7 @@ async fn get_bundle_ignores_cache_for_different_auth_identity() { name: "Replacement requirements".to_string(), contents: "allowed_approval_policies = [\"on-request\"]".to_string(), }], + managed_layers: Default::default(), }, }; let fetcher = Arc::new(SequenceBundleClient::new(vec![Ok( @@ -931,6 +973,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(), }], + managed_layers: Default::default(), }, }; let fetcher = Arc::new(SequenceBundleClient::new(vec![Ok( @@ -996,6 +1039,7 @@ async fn refresh_from_remote_updates_cached_bundle() { name: "Replacement requirements".to_string(), contents: "allowed_approval_policies = [\"on-request\"]".to_string(), }], + managed_layers: Default::default(), }, }; let codex_home = tempdir().expect("tempdir"); @@ -1022,7 +1066,7 @@ async fn refresh_from_remote_updates_cached_bundle() { } #[test] -fn bundle_response_conversion_preserves_fragment_order() { +fn bundle_response_conversion_preserves_fragment_order_and_managed_presence() { let response = ConfigBundleResponse { config_toml: Some(Some(Box::new(codex_backend_client::DeliveredConfigToml { enterprise_managed: Some(Some(vec![ @@ -1037,6 +1081,14 @@ fn bundle_response_conversion_preserves_fragment_order() { "model = \"low\"".to_string(), ), ])), + managed_layers: Some(Some(Box::new(DeliveredManagedLayers { + baseline: Some(Some(vec![DeliveredTomlFragment::new( + "cfg_baseline".to_string(), + "Config baseline".to_string(), + "model = \"baseline\"".to_string(), + )])), + system_overlay: Some(Some(Vec::new())), + }))), }))), requirements_toml: Some(Some(Box::new( codex_backend_client::DeliveredRequirementsToml { @@ -1045,6 +1097,14 @@ fn bundle_response_conversion_preserves_fragment_order() { "High requirements".to_string(), "allowed_approval_policies = [\"never\"]".to_string(), )])), + managed_layers: Some(Some(Box::new(DeliveredManagedLayers { + baseline: None, + system_overlay: Some(Some(vec![DeliveredTomlFragment::new( + "req_overlay".to_string(), + "Requirements overlay".to_string(), + "allowed_approval_policies = [\"on-request\"]".to_string(), + )])), + }))), }, ))), }; @@ -1065,6 +1125,14 @@ fn bundle_response_conversion_preserves_fragment_order() { contents: "model = \"low\"".to_string(), }, ], + managed_layers: CloudConfigTomlManagedLayers { + baseline: Some(vec![CloudConfigFragment { + id: "cfg_baseline".to_string(), + name: "Config baseline".to_string(), + contents: "model = \"baseline\"".to_string(), + }]), + system_overlay: Some(Vec::new()), + }, }, requirements_toml: CloudRequirementsTomlBundle { enterprise_managed: vec![CloudRequirementsFragment { @@ -1072,6 +1140,14 @@ fn bundle_response_conversion_preserves_fragment_order() { name: "High requirements".to_string(), contents: "allowed_approval_policies = [\"never\"]".to_string(), }], + managed_layers: CloudRequirementsTomlManagedLayers { + baseline: None, + system_overlay: Some(vec![CloudRequirementsFragment { + id: "req_overlay".to_string(), + name: "Requirements overlay".to_string(), + contents: "allowed_approval_policies = [\"on-request\"]".to_string(), + }]), + }, }, } ); diff --git a/codex-rs/cloud-config/src/validation.rs b/codex-rs/cloud-config/src/validation.rs index ef5ed06e1a..46304a1f55 100644 --- a/codex-rs/cloud-config/src/validation.rs +++ b/codex-rs/cloud-config/src/validation.rs @@ -18,17 +18,27 @@ pub(crate) fn validate_bundle( ) })?; let CloudConfigBundleLayers { + baseline_config: _, + system_overlay_config: _, enterprise_managed_config: _, + baseline_requirements, + system_overlay_requirements, enterprise_managed_requirements, } = bundle_layers; - compose_requirements(enterprise_managed_requirements).map_err(|err| { - CloudConfigBundleLoadError::new( - CloudConfigBundleLoadErrorCode::InvalidBundle, - /*status_code*/ None, - format!("invalid cloud config bundle: {err}"), - ) - })?; + for requirements_layers in [ + baseline_requirements.unwrap_or_default(), + system_overlay_requirements.unwrap_or_default(), + enterprise_managed_requirements, + ] { + compose_requirements(requirements_layers).map_err(|err| { + CloudConfigBundleLoadError::new( + CloudConfigBundleLoadErrorCode::InvalidBundle, + /*status_code*/ None, + format!("invalid cloud config bundle: {err}"), + ) + })?; + } Ok(()) } diff --git a/codex-rs/codex-backend-openapi-models/src/models/delivered_config_toml.rs b/codex-rs/codex-backend-openapi-models/src/models/delivered_config_toml.rs index 0183eced47..081fe4bd9c 100644 --- a/codex-rs/codex-backend-openapi-models/src/models/delivered_config_toml.rs +++ b/codex-rs/codex-backend-openapi-models/src/models/delivered_config_toml.rs @@ -21,12 +21,20 @@ pub struct DeliveredConfigToml { skip_serializing_if = "Option::is_none" )] pub enterprise_managed: Option>>, + #[serde( + rename = "managed_layers", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] + pub managed_layers: Option>>, } impl DeliveredConfigToml { pub fn new() -> DeliveredConfigToml { DeliveredConfigToml { enterprise_managed: None, + managed_layers: None, } } } diff --git a/codex-rs/codex-backend-openapi-models/src/models/delivered_managed_layers.rs b/codex-rs/codex-backend-openapi-models/src/models/delivered_managed_layers.rs new file mode 100644 index 0000000000..6e9b69967e --- /dev/null +++ b/codex-rs/codex-backend-openapi-models/src/models/delivered_managed_layers.rs @@ -0,0 +1,40 @@ +/* + * codex-backend + * + * codex-backend + * + * The version of the OpenAPI document: 0.0.1 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct DeliveredManagedLayers { + #[serde( + rename = "baseline", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] + pub baseline: Option>>, + #[serde( + rename = "system_overlay", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] + pub system_overlay: Option>>, +} + +impl DeliveredManagedLayers { + pub fn new() -> DeliveredManagedLayers { + DeliveredManagedLayers { + baseline: None, + system_overlay: None, + } + } +} diff --git a/codex-rs/codex-backend-openapi-models/src/models/delivered_requirements_toml.rs b/codex-rs/codex-backend-openapi-models/src/models/delivered_requirements_toml.rs index 953087d734..bec357e187 100644 --- a/codex-rs/codex-backend-openapi-models/src/models/delivered_requirements_toml.rs +++ b/codex-rs/codex-backend-openapi-models/src/models/delivered_requirements_toml.rs @@ -21,12 +21,20 @@ pub struct DeliveredRequirementsToml { skip_serializing_if = "Option::is_none" )] pub enterprise_managed: Option>>, + #[serde( + rename = "managed_layers", + default, + with = "::serde_with::rust::double_option", + skip_serializing_if = "Option::is_none" + )] + pub managed_layers: Option>>, } impl DeliveredRequirementsToml { pub fn new() -> DeliveredRequirementsToml { DeliveredRequirementsToml { enterprise_managed: None, + managed_layers: None, } } } diff --git a/codex-rs/codex-backend-openapi-models/src/models/mod.rs b/codex-rs/codex-backend-openapi-models/src/models/mod.rs index c3f4e6beec..b154a044fd 100644 --- a/codex-rs/codex-backend-openapi-models/src/models/mod.rs +++ b/codex-rs/codex-backend-openapi-models/src/models/mod.rs @@ -13,6 +13,9 @@ pub use self::config_file_response::ConfigFileResponse; pub(crate) mod delivered_config_toml; pub use self::delivered_config_toml::DeliveredConfigToml; +pub(crate) mod delivered_managed_layers; +pub use self::delivered_managed_layers::DeliveredManagedLayers; + pub(crate) mod delivered_requirements_toml; pub use self::delivered_requirements_toml::DeliveredRequirementsToml; diff --git a/codex-rs/config/src/cloud_config_bundle.rs b/codex-rs/config/src/cloud_config_bundle.rs index cfece32a01..635be0411a 100644 --- a/codex-rs/config/src/cloud_config_bundle.rs +++ b/codex-rs/config/src/cloud_config_bundle.rs @@ -35,23 +35,75 @@ 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.is_absent() + && requirements_enterprise_managed.is_empty() + && requirements_managed_layers.is_absent() + } + + /// Returns true when any managed bucket was present in the backend payload. + /// + /// An explicitly empty bucket still counts as present because it can suppress + /// compatibility fallback behavior when the bundle is materialized. + pub fn has_managed_layer_buckets(&self) -> bool { + !self.config_toml.managed_layers.is_absent() + || !self.requirements_toml.managed_layers.is_absent() } } #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct CloudConfigTomlBundle { pub enterprise_managed: Vec, + #[serde( + default, + skip_serializing_if = "CloudConfigTomlManagedLayers::is_absent" + )] + pub managed_layers: CloudConfigTomlManagedLayers, +} + +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct CloudConfigTomlManagedLayers { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub baseline: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub system_overlay: Option>, +} + +impl CloudConfigTomlManagedLayers { + fn is_absent(&self) -> bool { + self.baseline.is_none() && self.system_overlay.is_none() + } } #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct CloudRequirementsTomlBundle { pub enterprise_managed: Vec, + #[serde( + default, + skip_serializing_if = "CloudRequirementsTomlManagedLayers::is_absent" + )] + pub managed_layers: CloudRequirementsTomlManagedLayers, +} + +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct CloudRequirementsTomlManagedLayers { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub baseline: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub system_overlay: Option>, +} + +impl CloudRequirementsTomlManagedLayers { + fn is_absent(&self) -> bool { + self.baseline.is_none() && self.system_overlay.is_none() + } } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -67,8 +119,16 @@ pub struct CloudRequirementsFragment { /// inserted relative to local/system/user layers. #[derive(Clone, Debug)] pub struct CloudConfigBundleLayers { + /// Baseline config layers in `ConfigLayerStack` order, when delivered. + pub baseline_config: Option>, + /// System-overlay config layers in `ConfigLayerStack` order, when delivered. + pub system_overlay_config: Option>, /// Enterprise-managed config layers in `ConfigLayerStack` order. pub enterprise_managed_config: Vec, + /// Baseline requirements layers in requirements merge order, when delivered. + pub baseline_requirements: Option>, + /// System-overlay requirements layers in requirements merge order, when delivered. + pub system_overlay_requirements: Option>, /// Enterprise-managed requirements layers in requirements layer merge order. pub enterprise_managed_requirements: Vec, } @@ -99,43 +159,77 @@ 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 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 parse_config_fragments = |fragments| { + if strict_config { + cloud_config_layers_from_fragments_strict(fragments, base_dir) + } else { + cloud_config_layers_from_fragments(fragments, base_dir) + } }; + let baseline_config = config_baseline.map(parse_config_fragments).transpose()?; + let system_overlay_config = config_system_overlay + .map(parse_config_fragments) + .transpose()?; + let enterprise_managed_config = parse_config_fragments(config_enterprise_managed)?; - 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::>(); - // Bundle fragments arrive highest-priority first, while requirements - // layers are merged lowest-priority to highest-priority. - enterprise_managed_requirements.reverse(); + let baseline_requirements = requirements_baseline + .map(|fragments| requirements_layers_from_fragments(fragments, base_dir)); + let system_overlay_requirements = requirements_system_overlay + .map(|fragments| requirements_layers_from_fragments(fragments, base_dir)); + let enterprise_managed_requirements = + requirements_layers_from_fragments(requirements_enterprise_managed, base_dir); Ok(Self { + baseline_config, + system_overlay_config, enterprise_managed_config, + baseline_requirements, + system_overlay_requirements, enterprise_managed_requirements, }) } } +fn requirements_layers_from_fragments( + fragments: Vec, + base_dir: &AbsolutePathBuf, +) -> Vec { + let mut layers = fragments + .into_iter() + .map(|fragment| { + RequirementsLayerEntry::from_toml( + RequirementSource::EnterpriseManaged { + id: fragment.id, + name: fragment.name, + }, + fragment.contents, + ) + .with_base_dir(base_dir.clone()) + }) + .collect::>(); + // 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, diff --git a/codex-rs/config/src/cloud_config_bundle_tests.rs b/codex-rs/config/src/cloud_config_bundle_tests.rs index 714aca6bc6..cc1fcf0ed6 100644 --- a/codex-rs/config/src/cloud_config_bundle_tests.rs +++ b/codex-rs/config/src/cloud_config_bundle_tests.rs @@ -23,6 +23,23 @@ async fn shared_future_runs_once() { assert_eq!(counter.load(Ordering::SeqCst), 1); } +#[test] +fn bundle_with_explicitly_empty_overlay_is_not_empty() { + let bundle = CloudConfigBundle { + config_toml: CloudConfigTomlBundle { + enterprise_managed: Vec::new(), + managed_layers: CloudConfigTomlManagedLayers { + baseline: None, + system_overlay: Some(Vec::new()), + }, + }, + requirements_toml: CloudRequirementsTomlBundle::default(), + }; + + assert!(!bundle.is_empty()); + assert!(bundle.has_managed_layer_buckets()); +} + #[test] fn bundle_layers_preserve_enterprise_managed_bucket_order() { let tempdir = tempdir().expect("tempdir"); @@ -42,6 +59,7 @@ fn bundle_layers_preserve_enterprise_managed_bucket_order() { contents: "model = \"low\"".to_string(), }, ], + managed_layers: 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(), }, ], + managed_layers: Default::default(), }, }, &base_dir, @@ -92,20 +111,101 @@ fn bundle_layers_preserve_enterprise_managed_bucket_order() { } #[test] -fn bundle_layers_can_strict_validate_enterprise_managed_config() { +fn bundle_layers_preserve_managed_bucket_presence_and_order() { + 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 { + enterprise_managed: Vec::new(), + managed_layers: CloudConfigTomlManagedLayers { + baseline: Some(vec![ + CloudConfigFragment { + id: "baseline_high".to_string(), + name: "Baseline high".to_string(), + contents: "model = \"high\"".to_string(), + }, + CloudConfigFragment { + id: "baseline_low".to_string(), + name: "Baseline low".to_string(), + contents: "model = \"low\"".to_string(), + }, + ]), + system_overlay: Some(Vec::new()), + }, + }, + requirements_toml: CloudRequirementsTomlBundle { + enterprise_managed: Vec::new(), + managed_layers: CloudRequirementsTomlManagedLayers { + baseline: None, + system_overlay: Some(vec![CloudRequirementsFragment { + id: "overlay".to_string(), + name: "Overlay".to_string(), + contents: "allowed_approval_policies = [\"never\"]".to_string(), + }]), + }, + }, + }, + &base_dir, + ) + .expect("bundle should be converted into layers"); + + assert_eq!( + layers + .baseline_config + .expect("baseline should be present") + .into_iter() + .map(|layer| layer.name) + .collect::>(), + vec![ + ConfigLayerSource::EnterpriseManaged { + id: "baseline_low".to_string(), + name: "Baseline low".to_string(), + }, + ConfigLayerSource::EnterpriseManaged { + id: "baseline_high".to_string(), + name: "Baseline high".to_string(), + }, + ] + ); + assert_eq!(layers.system_overlay_config, Some(Vec::new())); + assert!(layers.baseline_requirements.is_none()); + assert_eq!( + compose_requirements( + layers + .system_overlay_requirements + .expect("system overlay should be present") + ) + .expect("requirements should compose") + .expect("requirements should be present") + .into_toml(), + ConfigRequirementsToml { + allowed_approval_policies: Some(vec![AskForApproval::Never]), + ..Default::default() + } + ); +} + +#[test] +fn bundle_layers_can_strict_validate_managed_config() { let tempdir = tempdir().expect("tempdir"); let base_dir = AbsolutePathBuf::from_absolute_path(tempdir.path()).expect("absolute path"); let err = CloudConfigBundleLayers::from_bundle_strict_config( CloudConfigBundle { config_toml: CloudConfigTomlBundle { - enterprise_managed: vec![CloudConfigFragment { - id: "cfg".to_string(), - name: "Cloud config".to_string(), - contents: "unknown_key = true".to_string(), - }], + enterprise_managed: Vec::new(), + managed_layers: CloudConfigTomlManagedLayers { + baseline: Some(vec![CloudConfigFragment { + id: "cfg".to_string(), + name: "Cloud config".to_string(), + contents: "unknown_key = true".to_string(), + }]), + system_overlay: None, + }, }, requirements_toml: CloudRequirementsTomlBundle { enterprise_managed: Vec::new(), + managed_layers: Default::default(), }, }, &base_dir, diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index afda2cf9f3..589c1e35df 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -38,8 +38,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; diff --git a/codex-rs/config/src/loader/mod.rs b/codex-rs/config/src/loader/mod.rs index 00110bc08b..8e907db20d 100644 --- a/codex-rs/config/src/loader/mod.rs +++ b/codex-rs/config/src/loader/mod.rs @@ -146,7 +146,11 @@ 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;