From 1fd2a6d328017c58f4863b76f430d4000cbfd1b5 Mon Sep 17 00:00:00 2001 From: joeflorencio-openai Date: Tue, 2 Jun 2026 16:41:48 -0700 Subject: [PATCH] Allow EDU accounts to fetch cloud config bundles (#25963) ## Summary Allow EDU ChatGPT workspaces to fetch cloud config bundles. The existing cloud config eligibility gate only allowed business-like and enterprise plans, which meant EDU admins could configure managed policies in the UI but the Codex client would skip fetching them. This keeps individual/pro and team-like usage-based plans excluded, and adds service-level coverage for both `edu` and `education` plan aliases. ## Validation - `just fmt` - `just test -p codex-cloud-config` - Built the Codex app locally, created a new EDU ChatGPT workspace, and verified config bundles can be fetched and are properly applied. --- codex-rs/cloud-config/src/service.rs | 3 +- codex-rs/cloud-config/src/service_tests.rs | 97 +++++++++------------- 2 files changed, 43 insertions(+), 57 deletions(-) diff --git a/codex-rs/cloud-config/src/service.rs b/codex-rs/cloud-config/src/service.rs index b0e8d49e2..eed94c0a1 100644 --- a/codex-rs/cloud-config/src/service.rs +++ b/codex-rs/cloud-config/src/service.rs @@ -49,7 +49,8 @@ fn cloud_config_eligible_auth(auth: &CodexAuth) -> bool { return false; }; auth.uses_codex_backend() - && (plan_type.is_business_like() || matches!(plan_type, PlanType::Enterprise)) + && (plan_type.is_business_like() + || matches!(plan_type, PlanType::Enterprise | PlanType::Edu)) } fn optional_bundle(bundle: CloudConfigBundle) -> Option { diff --git a/codex-rs/cloud-config/src/service_tests.rs b/codex-rs/cloud-config/src/service_tests.rs index a07443ba0..abedca4b0 100644 --- a/codex-rs/cloud-config/src/service_tests.rs +++ b/codex-rs/cloud-config/src/service_tests.rs @@ -351,7 +351,7 @@ async fn get_bundle_skips_non_chatgpt_auth() { } #[tokio::test] -async fn get_bundle_skips_non_business_or_enterprise_plan() { +async fn get_bundle_skips_individual_plan() { let fetcher = Arc::new(StaticBundleClient::new(test_bundle())); let codex_home = tempdir().expect("tempdir"); let service = CloudConfigBundleService::new( @@ -365,6 +365,46 @@ async fn get_bundle_skips_non_business_or_enterprise_plan() { assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 0); } +#[tokio::test] +async fn get_bundle_allows_eligible_workspace_plans_and_writes_cache() { + for plan_type in [ + "business", + "enterprise_cbp_usage_based", + "enterprise", + "hc", + "edu", + "education", + ] { + let bundle = test_bundle(); + let fetcher = Arc::new(StaticBundleClient::new(bundle.clone())); + let codex_home = tempdir().expect("tempdir"); + let service = CloudConfigBundleService::new( + auth_manager_with_plan(plan_type).await, + fetcher.clone(), + codex_home.path().to_path_buf(), + CLOUD_CONFIG_BUNDLE_TIMEOUT, + ); + + assert_eq!( + service.load_startup_bundle().await, + Ok(Some(bundle)), + "plan_type: {plan_type}" + ); + assert_eq!( + fetcher.request_count.load(Ordering::SeqCst), + 1, + "plan_type: {plan_type}" + ); + assert!( + codex_home + .path() + .join(CLOUD_CONFIG_BUNDLE_CACHE_FILENAME) + .exists(), + "plan_type: {plan_type}" + ); + } +} + #[tokio::test] async fn get_bundle_skips_team_like_usage_based_plan() { let fetcher = Arc::new(StaticBundleClient::new(test_bundle())); @@ -380,31 +420,6 @@ async fn get_bundle_skips_team_like_usage_based_plan() { assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 0); } -#[tokio::test] -async fn get_bundle_allows_business_plan_and_writes_cache() { - let bundle = test_bundle(); - let codex_home = tempdir().expect("tempdir"); - let fetcher = Arc::new(StaticBundleClient::new(bundle.clone())); - let service = CloudConfigBundleService::new( - auth_manager_with_plan("business").await, - fetcher.clone(), - codex_home.path().to_path_buf(), - CLOUD_CONFIG_BUNDLE_TIMEOUT, - ); - - assert_eq!( - service.load_startup_bundle().await, - Ok(Some(bundle.clone())) - ); - assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 1); - assert!( - codex_home - .path() - .join(CLOUD_CONFIG_BUNDLE_CACHE_FILENAME) - .exists() - ); -} - #[tokio::test] async fn get_bundle_rejects_invalid_remote_bundle_before_cache_write() { let codex_home = tempdir().expect("tempdir"); @@ -468,36 +483,6 @@ async fn get_bundle_ignores_invalid_cache_and_refetches() { ); } -#[tokio::test] -async fn get_bundle_allows_business_like_usage_based_plan() { - let fetcher = Arc::new(StaticBundleClient::new(test_bundle())); - let codex_home = tempdir().expect("tempdir"); - let service = CloudConfigBundleService::new( - auth_manager_with_plan("enterprise_cbp_usage_based").await, - fetcher.clone(), - codex_home.path().to_path_buf(), - CLOUD_CONFIG_BUNDLE_TIMEOUT, - ); - - assert_eq!(service.load_startup_bundle().await, Ok(Some(test_bundle()))); - assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 1); -} - -#[tokio::test] -async fn get_bundle_allows_hc_plan_as_enterprise() { - let fetcher = Arc::new(StaticBundleClient::new(test_bundle())); - let codex_home = tempdir().expect("tempdir"); - let service = CloudConfigBundleService::new( - auth_manager_with_plan("hc").await, - fetcher.clone(), - codex_home.path().to_path_buf(), - CLOUD_CONFIG_BUNDLE_TIMEOUT, - ); - - assert_eq!(service.load_startup_bundle().await, Ok(Some(test_bundle()))); - assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 1); -} - #[tokio::test] async fn get_bundle_empty_response_is_success_and_cached() { let codex_home = tempdir().expect("tempdir");