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.
This commit is contained in:
joeflorencio-openai
2026-06-02 16:41:48 -07:00
committed by GitHub
Unverified
parent 271d5cecf2
commit 1fd2a6d328
2 changed files with 43 additions and 57 deletions
+2 -1
View File
@@ -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<CloudConfigBundle> {
+41 -56
View File
@@ -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");