[cloud-config] Add managed-layer wire and cache support

This commit is contained in:
hefuc-oai
2026-06-25 03:21:16 +00:00
parent df1199fddb
commit ef42981ba9
17 changed files with 716 additions and 75 deletions
+1
View File
@@ -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;
+1
View File
@@ -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;
+52 -8
View File
@@ -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<T>(
managed_layers: Option<Option<Box<DeliveredManagedLayers>>>,
convert: fn(DeliveredTomlFragment) -> T,
) -> (Option<Vec<T>>, Option<Vec<T>>) {
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,
+67 -7
View File
@@ -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<Sha256>;
@@ -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<String>,
}
#[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<Vec<u8>> {
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<String> {
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<String> {
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<String> {
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],
+150 -1
View File
@@ -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");
+17
View File
@@ -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()
+24
View File
@@ -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));
+101 -25
View File
@@ -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(),
}]),
},
},
}
);
+17 -7
View File
@@ -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(())
}
@@ -21,12 +21,20 @@ pub struct DeliveredConfigToml {
skip_serializing_if = "Option::is_none"
)]
pub enterprise_managed: Option<Option<Vec<models::DeliveredTomlFragment>>>,
#[serde(
rename = "managed_layers",
default,
with = "::serde_with::rust::double_option",
skip_serializing_if = "Option::is_none"
)]
pub managed_layers: Option<Option<Box<models::DeliveredManagedLayers>>>,
}
impl DeliveredConfigToml {
pub fn new() -> DeliveredConfigToml {
DeliveredConfigToml {
enterprise_managed: None,
managed_layers: None,
}
}
}
@@ -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<Option<Vec<models::DeliveredTomlFragment>>>,
#[serde(
rename = "system_overlay",
default,
with = "::serde_with::rust::double_option",
skip_serializing_if = "Option::is_none"
)]
pub system_overlay: Option<Option<Vec<models::DeliveredTomlFragment>>>,
}
impl DeliveredManagedLayers {
pub fn new() -> DeliveredManagedLayers {
DeliveredManagedLayers {
baseline: None,
system_overlay: None,
}
}
}
@@ -21,12 +21,20 @@ pub struct DeliveredRequirementsToml {
skip_serializing_if = "Option::is_none"
)]
pub enterprise_managed: Option<Option<Vec<models::DeliveredTomlFragment>>>,
#[serde(
rename = "managed_layers",
default,
with = "::serde_with::rust::double_option",
skip_serializing_if = "Option::is_none"
)]
pub managed_layers: Option<Option<Box<models::DeliveredManagedLayers>>>,
}
impl DeliveredRequirementsToml {
pub fn new() -> DeliveredRequirementsToml {
DeliveredRequirementsToml {
enterprise_managed: None,
managed_layers: None,
}
}
}
@@ -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;
+115 -21
View File
@@ -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<CloudConfigFragment>,
#[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<Vec<CloudConfigFragment>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system_overlay: Option<Vec<CloudConfigFragment>>,
}
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<CloudRequirementsFragment>,
#[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<Vec<CloudRequirementsFragment>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system_overlay: Option<Vec<CloudRequirementsFragment>>,
}
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<Vec<ConfigLayerEntry>>,
/// System-overlay config layers in `ConfigLayerStack` order, when delivered.
pub system_overlay_config: Option<Vec<ConfigLayerEntry>>,
/// Enterprise-managed config layers in `ConfigLayerStack` order.
pub enterprise_managed_config: Vec<ConfigLayerEntry>,
/// Baseline requirements layers in requirements merge order, when delivered.
pub baseline_requirements: Option<Vec<RequirementsLayerEntry>>,
/// System-overlay requirements layers in requirements merge order, when delivered.
pub system_overlay_requirements: Option<Vec<RequirementsLayerEntry>>,
/// Enterprise-managed requirements layers in requirements layer merge order.
pub enterprise_managed_requirements: Vec<RequirementsLayerEntry>,
}
@@ -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::<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_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<CloudRequirementsFragment>,
base_dir: &AbsolutePathBuf,
) -> Vec<RequirementsLayerEntry> {
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::<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,
@@ -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<_>>(),
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,
+2
View File
@@ -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;
+4
View File
@@ -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;