Support Enterprise automation account plans (#36228)

## What changed

- Recognize `enterprise_cbp_automation` as an Enterprise workspace plan in authentication, backend responses, and app-server account and rate-limit APIs.
- Expose the plan in generated protocol schemas and display it as `Enterprise (Automation)` in account status.
- Apply existing workspace eligibility and Business usage-limit behavior to the new plan.

## Testing

- Cover JWT parsing, plan conversion and serialization, app-server responses, workspace eligibility, usage-limit formatting, and TUI status rendering.

GitOrigin-RevId: d84ae2be3dd60fe398f226e6dc434efe27a387a5
This commit is contained in:
sashank-oai
2026-07-30 21:49:53 +00:00
committed by copyberry
parent a01a2d9146
commit 3016671bb0
22 changed files with 106 additions and 14 deletions
@@ -2925,6 +2925,7 @@
"self_serve_business_usage_based",
"business",
"ent26",
"enterprise_cbp_automation",
"enterprise_cbp_usage_based",
"enterprise",
"edu",
@@ -14155,6 +14155,7 @@
"self_serve_business_usage_based",
"business",
"ent26",
"enterprise_cbp_automation",
"enterprise_cbp_usage_based",
"enterprise",
"edu",
@@ -10497,6 +10497,7 @@
"self_serve_business_usage_based",
"business",
"ent26",
"enterprise_cbp_automation",
"enterprise_cbp_usage_based",
"enterprise",
"edu",
@@ -34,6 +34,7 @@
"self_serve_business_usage_based",
"business",
"ent26",
"enterprise_cbp_automation",
"enterprise_cbp_usage_based",
"enterprise",
"edu",
@@ -67,6 +67,7 @@
"self_serve_business_usage_based",
"business",
"ent26",
"enterprise_cbp_automation",
"enterprise_cbp_usage_based",
"enterprise",
"edu",
@@ -34,6 +34,7 @@
"self_serve_business_usage_based",
"business",
"ent26",
"enterprise_cbp_automation",
"enterprise_cbp_usage_based",
"enterprise",
"edu",
@@ -80,6 +80,7 @@
"self_serve_business_usage_based",
"business",
"ent26",
"enterprise_cbp_automation",
"enterprise_cbp_usage_based",
"enterprise",
"edu",
+1 -1
View File
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type PlanType = "free" | "go" | "plus" | "pro" | "prolite" | "team" | "self_serve_business_prolite" | "self_serve_business_usage_based" | "business" | "ent26" | "enterprise_cbp_usage_based" | "enterprise" | "edu" | "unknown";
export type PlanType = "free" | "go" | "plus" | "pro" | "prolite" | "team" | "self_serve_business_prolite" | "self_serve_business_usage_based" | "business" | "ent26" | "enterprise_cbp_automation" | "enterprise_cbp_usage_based" | "enterprise" | "edu" | "unknown";
+1 -1
View File
@@ -2114,7 +2114,7 @@ The JSON-RPC auth/account surface exposes request/response methods plus server-i
### Authentication modes
Codex supports these authentication modes. The current mode is surfaced in `account/updated` (`authMode`), which also includes the current ChatGPT `planType` when available, and can be inferred from `account/read`. Self-serve Business ProLite accounts use the `self_serve_business_prolite` plan type.
Codex supports these authentication modes. The current mode is surfaced in `account/updated` (`authMode`), which also includes the current ChatGPT `planType` when available, and can be inferred from `account/read`. Self-serve Business ProLite accounts use the `self_serve_business_prolite` plan type; Enterprise automation accounts use `enterprise_cbp_automation`.
- **API key (`apiKey`)**: Caller supplies an OpenAI API key via `account/login/start` with `type: "apiKey"`. The API key is saved and used for API requests.
- **ChatGPT managed (`chatgpt`)** (recommended): Codex owns the ChatGPT OAuth flow and refresh tokens. Start via `account/login/start` with `type: "chatgpt"` for the browser flow or `type: "chatgptDeviceCode"` for device code; Codex persists tokens to disk and refreshes them automatically.
+10 -2
View File
@@ -154,7 +154,7 @@ async fn personal_access_token_without_email_supports_auth_status_and_account_re
"email": null,
"chatgpt_user_id": "user-123",
"chatgpt_account_id": "account-123",
"chatgpt_plan_type": "pro",
"chatgpt_plan_type": "enterprise_cbp_automation",
"chatgpt_account_is_fedramp": false,
})))
.expect(1..)
@@ -208,12 +208,20 @@ async fn personal_access_token_without_email_supports_auth_status_and_account_re
.and_then(|account| account.get("email")),
Some(&serde_json::Value::Null),
);
assert_eq!(
response
.result
.get("account")
.and_then(|account| account.get("planType"))
.and_then(serde_json::Value::as_str),
Some("enterprise_cbp_automation"),
);
assert_eq!(
to_response::<GetAccountResponse>(response)?,
GetAccountResponse {
account: Some(Account::Chatgpt {
email: None,
plan_type: AccountPlanType::Pro,
plan_type: AccountPlanType::EnterpriseCbpAutomation,
}),
requires_openai_auth: true,
}
@@ -127,7 +127,7 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
.expect("parse second reset credit grant timestamp")
.timestamp();
let response_body = json!({
"plan_type": "self_serve_business_prolite",
"plan_type": "enterprise_cbp_automation",
"rate_limit": {
"allowed": true,
"limit_reached": false,
@@ -251,7 +251,7 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
resets_at: secondary_reset_timestamp,
}),
spend_control_reached: Some(false),
plan_type: Some(AccountPlanType::SelfServeBusinessProLite),
plan_type: Some(AccountPlanType::EnterpriseCbpAutomation),
rate_limit_reached_type: Some(RateLimitReachedType::WorkspaceMemberUsageLimitReached),
},
rate_limits_by_limit_id: Some(
@@ -279,7 +279,7 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
resets_at: secondary_reset_timestamp,
}),
spend_control_reached: Some(false),
plan_type: Some(AccountPlanType::SelfServeBusinessProLite),
plan_type: Some(AccountPlanType::EnterpriseCbpAutomation),
rate_limit_reached_type: Some(
RateLimitReachedType::WorkspaceMemberUsageLimitReached,
),
@@ -299,7 +299,7 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
credits: None,
individual_limit: None,
spend_control_reached: None,
plan_type: Some(AccountPlanType::SelfServeBusinessProLite),
plan_type: Some(AccountPlanType::EnterpriseCbpAutomation),
rate_limit_reached_type: None,
},
),
+7
View File
@@ -693,6 +693,9 @@ impl Client {
}
crate::types::PlanType::Business => AccountPlanType::Business,
crate::types::PlanType::Ent26 => AccountPlanType::Ent26,
crate::types::PlanType::EnterpriseCbpAutomation => {
AccountPlanType::EnterpriseCbpAutomation
}
crate::types::PlanType::EnterpriseCbpUsageBased => {
AccountPlanType::EnterpriseCbpUsageBased
}
@@ -751,6 +754,10 @@ mod tests {
Client::map_plan_type(crate::types::PlanType::EnterpriseCbpUsageBased),
AccountPlanType::EnterpriseCbpUsageBased
);
assert_eq!(
Client::map_plan_type(crate::types::PlanType::EnterpriseCbpAutomation),
AccountPlanType::EnterpriseCbpAutomation
);
let ent26 = serde_json::from_str::<crate::types::PlanType>("\"ent26\"")
.expect("ent26 backend plan should deserialize");
assert_eq!(Client::map_plan_type(ent26), AccountPlanType::Ent26);
@@ -417,6 +417,7 @@ async fn get_bundle_allows_eligible_workspace_plans_and_writes_cache() {
for plan_type in [
"business",
"ent26",
"enterprise_cbp_automation",
"enterprise_cbp_usage_based",
"enterprise",
"hc",
@@ -120,6 +120,8 @@ pub enum PlanType {
Business,
#[serde(rename = "ent26")]
Ent26,
#[serde(rename = "enterprise_cbp_automation")]
EnterpriseCbpAutomation,
#[serde(rename = "enterprise_cbp_usage_based")]
EnterpriseCbpUsageBased,
#[serde(rename = "education")]
+21
View File
@@ -122,6 +122,27 @@ fn id_token_info_parses_usage_based_business_plans() {
assert_eq!(enterprise_cbp.is_workspace_account(), true);
}
#[test]
fn id_token_info_parses_enterprise_cbp_automation_plan() {
let jwt = fake_jwt(serde_json::json!({
"email": "service-account@example.com",
"https://api.openai.com/auth": {
"chatgpt_plan_type": "enterprise_cbp_automation"
}
}));
let info = parse_chatgpt_jwt_claims(&jwt).expect("should parse");
assert_eq!(
info.get_chatgpt_plan_type().as_deref(),
Some("Enterprise (Automation)")
);
assert_eq!(
info.get_chatgpt_plan_type_raw().as_deref(),
Some("enterprise_cbp_automation")
);
assert_eq!(info.is_workspace_account(), true);
}
#[test]
fn id_token_info_parses_self_serve_business_prolite_plan() {
let jwt = fake_jwt(serde_json::json!({
+28 -1
View File
@@ -25,6 +25,9 @@ pub enum PlanType {
SelfServeBusinessUsageBased,
Business,
Ent26,
#[serde(rename = "enterprise_cbp_automation")]
#[ts(rename = "enterprise_cbp_automation")]
EnterpriseCbpAutomation,
#[serde(rename = "enterprise_cbp_usage_based")]
#[ts(rename = "enterprise_cbp_usage_based")]
EnterpriseCbpUsageBased,
@@ -58,7 +61,10 @@ impl PlanType {
pub fn is_business_like(self) -> bool {
matches!(
self,
Self::Business | Self::Ent26 | Self::EnterpriseCbpUsageBased
Self::Business
| Self::Ent26
| Self::EnterpriseCbpAutomation
| Self::EnterpriseCbpUsageBased
)
}
@@ -70,6 +76,7 @@ impl PlanType {
| Self::SelfServeBusinessUsageBased
| Self::Business
| Self::Ent26
| Self::EnterpriseCbpAutomation
| Self::EnterpriseCbpUsageBased
| Self::Enterprise
| Self::Edu
@@ -99,6 +106,7 @@ impl From<KnownPlan> for PlanType {
KnownPlan::SelfServeBusinessUsageBased => Self::SelfServeBusinessUsageBased,
KnownPlan::Business => Self::Business,
KnownPlan::Ent26 => Self::Ent26,
KnownPlan::EnterpriseCbpAutomation => Self::EnterpriseCbpAutomation,
KnownPlan::EnterpriseCbpUsageBased => Self::EnterpriseCbpUsageBased,
KnownPlan::Enterprise => Self::Enterprise,
KnownPlan::Edu => Self::Edu,
@@ -130,6 +138,11 @@ mod tests {
.expect("enterprise cbp usage based should serialize"),
"\"enterprise_cbp_usage_based\""
);
assert_eq!(
serde_json::to_string(&PlanType::EnterpriseCbpAutomation)
.expect("enterprise cbp automation should serialize"),
"\"enterprise_cbp_automation\""
);
assert_eq!(
serde_json::to_string(&PlanType::Ent26).expect("ent26 should serialize"),
"\"ent26\""
@@ -157,6 +170,11 @@ mod tests {
.expect("enterprise cbp usage based should deserialize"),
PlanType::EnterpriseCbpUsageBased
);
assert_eq!(
serde_json::from_str::<PlanType>("\"enterprise_cbp_automation\"")
.expect("enterprise cbp automation should deserialize"),
PlanType::EnterpriseCbpAutomation
);
assert_eq!(
serde_json::from_str::<PlanType>("\"ent26\"").expect("ent26 should deserialize"),
PlanType::Ent26
@@ -173,6 +191,7 @@ mod tests {
assert_eq!(PlanType::Business.is_business_like(), true);
assert_eq!(PlanType::Ent26.is_business_like(), true);
assert_eq!(PlanType::EnterpriseCbpAutomation.is_business_like(), true);
assert_eq!(PlanType::EnterpriseCbpUsageBased.is_business_like(), true);
assert_eq!(PlanType::Team.is_business_like(), false);
}
@@ -190,6 +209,10 @@ mod tests {
);
assert_eq!(PlanType::Business.is_workspace_account(), true);
assert_eq!(PlanType::Ent26.is_workspace_account(), true);
assert_eq!(
PlanType::EnterpriseCbpAutomation.is_workspace_account(),
true
);
assert_eq!(
PlanType::EnterpriseCbpUsageBased.is_workspace_account(),
true
@@ -209,6 +232,10 @@ mod tests {
PlanType::from(AuthPlanType::Known(KnownPlan::EnterpriseCbpUsageBased)),
PlanType::EnterpriseCbpUsageBased
);
assert_eq!(
PlanType::from(AuthPlanType::Known(KnownPlan::EnterpriseCbpAutomation)),
PlanType::EnterpriseCbpAutomation
);
assert_eq!(
PlanType::from(AuthPlanType::Known(KnownPlan::Ent26)),
PlanType::Ent26
+11
View File
@@ -77,6 +77,7 @@ impl PlanType {
}
"business" => Self::Known(KnownPlan::Business),
"ent26" => Self::Known(KnownPlan::Ent26),
"enterprise_cbp_automation" => Self::Known(KnownPlan::EnterpriseCbpAutomation),
"enterprise_cbp_usage_based" => Self::Known(KnownPlan::EnterpriseCbpUsageBased),
"enterprise" | "hc" => Self::Known(KnownPlan::Enterprise),
"education" | "edu" => Self::Known(KnownPlan::Edu),
@@ -100,6 +101,8 @@ pub enum KnownPlan {
SelfServeBusinessUsageBased,
Business,
Ent26,
#[serde(rename = "enterprise_cbp_automation")]
EnterpriseCbpAutomation,
#[serde(rename = "enterprise_cbp_usage_based")]
EnterpriseCbpUsageBased,
#[serde(alias = "hc")]
@@ -121,6 +124,7 @@ impl KnownPlan {
Self::SelfServeBusinessUsageBased => "Self Serve Business Usage Based",
Self::Business => "Business",
Self::Ent26 => "Enterprise",
Self::EnterpriseCbpAutomation => "Enterprise (Automation)",
Self::EnterpriseCbpUsageBased => "Enterprise CBP Usage Based",
Self::Enterprise => "Enterprise",
Self::Edu => "Edu",
@@ -139,6 +143,7 @@ impl KnownPlan {
Self::SelfServeBusinessUsageBased => "self_serve_business_usage_based",
Self::Business => "business",
Self::Ent26 => "ent26",
Self::EnterpriseCbpAutomation => "enterprise_cbp_automation",
Self::EnterpriseCbpUsageBased => "enterprise_cbp_usage_based",
Self::Enterprise => "enterprise",
Self::Edu => "edu",
@@ -153,6 +158,7 @@ impl KnownPlan {
| Self::SelfServeBusinessUsageBased
| Self::Business
| Self::Ent26
| Self::EnterpriseCbpAutomation
| Self::EnterpriseCbpUsageBased
| Self::Enterprise
| Self::Edu
@@ -201,5 +207,10 @@ mod tests {
.expect("education should deserialize"),
PlanType::Known(KnownPlan::Edu)
);
assert_eq!(
serde_json::from_str::<PlanType>("\"enterprise_cbp_automation\"")
.expect("enterprise cbp automation should deserialize"),
PlanType::Known(KnownPlan::EnterpriseCbpAutomation)
);
}
}
+1
View File
@@ -691,6 +691,7 @@ impl std::fmt::Display for UsageLimitReachedError {
| KnownPlan::SelfServeBusinessUsageBased
| KnownPlan::Business
| KnownPlan::Ent26
| KnownPlan::EnterpriseCbpAutomation
| KnownPlan::EnterpriseCbpUsageBased,
)) => {
format!(
+5 -1
View File
@@ -336,7 +336,11 @@ fn usage_limit_reached_error_formats_team_plan() {
#[test]
fn usage_limit_reached_error_formats_business_plan_without_reset() {
for plan in [KnownPlan::Business, KnownPlan::Ent26] {
for plan in [
KnownPlan::Business,
KnownPlan::Ent26,
KnownPlan::EnterpriseCbpAutomation,
] {
let err = UsageLimitReachedError {
plan_type: Some(PlanType::Known(plan)),
resets_at: None,
+4 -1
View File
@@ -97,7 +97,9 @@ pub(crate) fn compose_account_display(
}
pub(crate) fn plan_type_display_name(plan_type: PlanType) -> String {
if plan_type.is_team_like() {
if plan_type == PlanType::EnterpriseCbpAutomation {
"Enterprise (Automation)".to_string()
} else if plan_type.is_team_like() {
"Business".to_string()
} else if plan_type.is_business_like() {
"Enterprise".to_string()
@@ -222,6 +224,7 @@ mod tests {
(PlanType::SelfServeBusinessProLite, "Business"),
(PlanType::SelfServeBusinessUsageBased, "Business"),
(PlanType::Business, "Enterprise"),
(PlanType::EnterpriseCbpAutomation, "Enterprise (Automation)"),
(PlanType::EnterpriseCbpUsageBased, "Enterprise"),
(PlanType::Enterprise, "Enterprise"),
(PlanType::Edu, "Edu"),
@@ -14,7 +14,7 @@ expression: sanitized
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace with network access, Ask for approval) │
│ Agents.md: <none> │
│ Account: Enterprise │
│ Account: Enterprise (Automation) │
│ │
│ Limits: data not available yet │
╰──────────────────────────────────────────────────────────────────────────╯
+2 -2
View File
@@ -365,7 +365,7 @@ async fn status_snapshot_shows_chatgpt_plan_without_email() {
write_chatgpt_auth(
temp_home.path(),
ChatGptAuthFixture::new("access-chatgpt").plan_type("enterprise"),
ChatGptAuthFixture::new("access-chatgpt").plan_type("enterprise_cbp_automation"),
AuthCredentialsStoreMode::File,
)
.expect("write email-less ChatGPT auth");
@@ -384,7 +384,7 @@ async fn status_snapshot_shows_chatgpt_plan_without_email() {
account_display,
StatusAccountDisplay::ChatGpt {
email: None,
plan: Some("Enterprise".to_string()),
plan: Some("Enterprise (Automation)".to_string()),
}
);
let usage = TokenUsage::default();