mirror of
https://github.com/openai/codex.git
synced 2026-09-04 15:08:45 +00:00
# Summary Codex required every ChatGPT account to have an email address. A service-account personal access token can return valid account metadata without one, so PAT login failed while decoding the metadata response. This change makes email optional in the account metadata type that owns it and preserves that absence through authentication, provider account state, the app-server API, generated clients, and TUI bootstrap. Existing accounts with email addresses keep the same behavior. ## Behavior-changing call sites | Call site | Behavior after this change | | --- | --- | | `login/src/auth/personal_access_token.rs` | PAT metadata accepts a missing or null email and retains `None`. | | `agent-identity/src/lib.rs` | Agent Identity JWT claims accept an omitted email. | | `login/src/auth/storage.rs` and `login/src/auth/agent_identity.rs` | Stored and managed Agent Identity records carry `Option<String>`. Deserialization maps the legacy empty-string sentinel to `None`. | | `login/src/auth/manager.rs` | `get_account_email` returns the stored option, and managed identity bootstrap no longer converts `None` to an empty string. | | `model-provider/src/provider.rs` and `protocol/src/account.rs` | A ChatGPT provider account requires a plan type but may carry no email. | | `app-server-protocol/src/protocol/v2/account.rs` | `account/read` keeps the `email` field on the wire and returns `null` when the account has no email. Generated TypeScript and JSON schemas describe a required, nullable field. | | `sdk/python/src/openai_codex/generated/v2_all.py` | The generated Python `ChatgptAccount` model accepts `None` for email. | | `tui/src/app_server_session.rs` | Email-less ChatGPT accounts bootstrap normally, keep external feedback routing, omit account-email telemetry, and display the plan in account status. | ## Design decisions - Missing email remains `None` at every layer. The code never uses an empty string as a substitute. - The app-server response includes `"email": null` instead of omitting the field. Clients retain a stable response shape. - Plan type remains required for provider account state. This change relaxes only the email assumption. ## Testing Tests: affected test targets compile, scoped Clippy and formatting pass, a focused TUI snapshot covers plan-only account status, real before/after PAT login smoke covers metadata without email, app-server smoke covers `account/read` with `email: null`, and a regression smoke covers an existing email-bearing PAT. Unit tests run in CI. ## Evidence Visual smoke evidence will be attached here.
186 lines
5.9 KiB
Rust
186 lines
5.9 KiB
Rust
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use ts_rs::TS;
|
|
|
|
use crate::auth::KnownPlan;
|
|
use crate::auth::PlanType as AuthPlanType;
|
|
|
|
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
|
|
#[serde(rename_all = "lowercase")]
|
|
#[ts(rename_all = "lowercase")]
|
|
pub enum PlanType {
|
|
#[default]
|
|
Free,
|
|
Go,
|
|
Plus,
|
|
Pro,
|
|
ProLite,
|
|
Team,
|
|
#[serde(rename = "self_serve_business_usage_based")]
|
|
#[ts(rename = "self_serve_business_usage_based")]
|
|
SelfServeBusinessUsageBased,
|
|
Business,
|
|
#[serde(rename = "enterprise_cbp_usage_based")]
|
|
#[ts(rename = "enterprise_cbp_usage_based")]
|
|
EnterpriseCbpUsageBased,
|
|
Enterprise,
|
|
Edu,
|
|
#[serde(other)]
|
|
Unknown,
|
|
}
|
|
|
|
/// Account state returned by a model provider before it is adapted to an app-facing wire type.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum ProviderAccount {
|
|
ApiKey,
|
|
Chatgpt {
|
|
email: Option<String>,
|
|
plan_type: PlanType,
|
|
},
|
|
AmazonBedrock {
|
|
credential_source: AmazonBedrockCredentialSource,
|
|
},
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(rename_all = "camelCase")]
|
|
pub enum AmazonBedrockCredentialSource {
|
|
CodexManaged,
|
|
AwsManaged,
|
|
}
|
|
|
|
impl PlanType {
|
|
pub fn is_team_like(self) -> bool {
|
|
matches!(self, Self::Team | Self::SelfServeBusinessUsageBased)
|
|
}
|
|
|
|
pub fn is_business_like(self) -> bool {
|
|
matches!(self, Self::Business | Self::EnterpriseCbpUsageBased)
|
|
}
|
|
|
|
pub fn is_workspace_account(self) -> bool {
|
|
matches!(
|
|
self,
|
|
Self::Team
|
|
| Self::SelfServeBusinessUsageBased
|
|
| Self::Business
|
|
| Self::EnterpriseCbpUsageBased
|
|
| Self::Enterprise
|
|
| Self::Edu
|
|
)
|
|
}
|
|
}
|
|
|
|
impl From<AuthPlanType> for PlanType {
|
|
fn from(plan_type: AuthPlanType) -> Self {
|
|
match plan_type {
|
|
AuthPlanType::Known(plan) => plan.into(),
|
|
AuthPlanType::Unknown(_) => Self::Unknown,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<KnownPlan> for PlanType {
|
|
fn from(plan: KnownPlan) -> Self {
|
|
match plan {
|
|
KnownPlan::Free => Self::Free,
|
|
KnownPlan::Go => Self::Go,
|
|
KnownPlan::Plus => Self::Plus,
|
|
KnownPlan::Pro => Self::Pro,
|
|
KnownPlan::ProLite => Self::ProLite,
|
|
KnownPlan::Team => Self::Team,
|
|
KnownPlan::SelfServeBusinessUsageBased => Self::SelfServeBusinessUsageBased,
|
|
KnownPlan::Business => Self::Business,
|
|
KnownPlan::EnterpriseCbpUsageBased => Self::EnterpriseCbpUsageBased,
|
|
KnownPlan::Enterprise => Self::Enterprise,
|
|
KnownPlan::Edu => Self::Edu,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::PlanType;
|
|
use crate::auth::KnownPlan;
|
|
use crate::auth::PlanType as AuthPlanType;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
#[test]
|
|
fn usage_based_plan_types_use_expected_wire_names() {
|
|
assert_eq!(
|
|
serde_json::to_string(&PlanType::SelfServeBusinessUsageBased)
|
|
.expect("self-serve business usage based should serialize"),
|
|
"\"self_serve_business_usage_based\""
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_string(&PlanType::EnterpriseCbpUsageBased)
|
|
.expect("enterprise cbp usage based should serialize"),
|
|
"\"enterprise_cbp_usage_based\""
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_string(&PlanType::ProLite).expect("prolite should serialize"),
|
|
"\"prolite\""
|
|
);
|
|
assert_eq!(
|
|
serde_json::from_str::<PlanType>("\"self_serve_business_usage_based\"")
|
|
.expect("self-serve business usage based should deserialize"),
|
|
PlanType::SelfServeBusinessUsageBased
|
|
);
|
|
assert_eq!(
|
|
serde_json::from_str::<PlanType>("\"prolite\"").expect("prolite should deserialize"),
|
|
PlanType::ProLite
|
|
);
|
|
assert_eq!(
|
|
serde_json::from_str::<PlanType>("\"enterprise_cbp_usage_based\"")
|
|
.expect("enterprise cbp usage based should deserialize"),
|
|
PlanType::EnterpriseCbpUsageBased
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn plan_family_helpers_group_usage_based_variants_with_existing_plans() {
|
|
assert_eq!(PlanType::Team.is_team_like(), true);
|
|
assert_eq!(PlanType::SelfServeBusinessUsageBased.is_team_like(), true);
|
|
assert_eq!(PlanType::Business.is_team_like(), false);
|
|
|
|
assert_eq!(PlanType::Business.is_business_like(), true);
|
|
assert_eq!(PlanType::EnterpriseCbpUsageBased.is_business_like(), true);
|
|
assert_eq!(PlanType::Team.is_business_like(), false);
|
|
}
|
|
|
|
#[test]
|
|
fn workspace_account_helper_includes_usage_based_workspace_plans() {
|
|
assert_eq!(PlanType::Team.is_workspace_account(), true);
|
|
assert_eq!(
|
|
PlanType::SelfServeBusinessUsageBased.is_workspace_account(),
|
|
true
|
|
);
|
|
assert_eq!(PlanType::Business.is_workspace_account(), true);
|
|
assert_eq!(
|
|
PlanType::EnterpriseCbpUsageBased.is_workspace_account(),
|
|
true
|
|
);
|
|
assert_eq!(PlanType::Enterprise.is_workspace_account(), true);
|
|
assert_eq!(PlanType::Edu.is_workspace_account(), true);
|
|
assert_eq!(PlanType::Pro.is_workspace_account(), false);
|
|
}
|
|
|
|
#[test]
|
|
fn auth_plan_type_converts_to_account_plan_type() {
|
|
assert_eq!(
|
|
PlanType::from(AuthPlanType::Known(KnownPlan::EnterpriseCbpUsageBased)),
|
|
PlanType::EnterpriseCbpUsageBased
|
|
);
|
|
assert_eq!(
|
|
PlanType::from(AuthPlanType::Known(KnownPlan::Enterprise)),
|
|
PlanType::Enterprise
|
|
);
|
|
assert_eq!(
|
|
PlanType::from(AuthPlanType::Unknown("mystery-tier".to_string())),
|
|
PlanType::Unknown
|
|
);
|
|
}
|
|
}
|