Files
codex/codex-rs/protocol/src/auth.rs
T
Adam Perry @ OpenAI 31372078d1 auth: move domain mode below app wire types (#29721)
## Why

Authentication mode is a domain concept used by login, model selection,
telemetry, and transports. Keeping the canonical type in app-server
protocol forces those lower-level crates to depend on an unrelated wire
API.

## What changed

- Added canonical `codex_protocol::auth::AuthMode` domain values.
- Kept the app-server wire DTO unchanged and added an explicit app-side
conversion.
- Removed production app-server-protocol dependencies from login,
model-provider-info, models-manager, and otel call paths.

## Stack

This is PR 2 of 6, stacked on [PR
#29714](https://github.com/openai/codex/pull/29714). Review only the
delta from `codex/split-json-rpc-protocols`. Next: [PR
#29722](https://github.com/openai/codex/pull/29722).

## Validation

- Auth and login coverage passed in the focused protocol/domain test
run.
- App-server account and auth conversion coverage passed.
2026-06-24 03:10:20 +00:00

190 lines
5.8 KiB
Rust

use serde::Deserialize;
use serde::Serialize;
use strum_macros::Display;
use thiserror::Error;
/// Authentication mode for OpenAI-backed providers.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AuthMode {
/// OpenAI API key provided by the caller and stored by Codex.
ApiKey,
/// ChatGPT OAuth managed by Codex (tokens persisted and refreshed by Codex).
Chatgpt,
/// ChatGPT auth tokens supplied by an external host application.
#[serde(rename = "chatgptAuthTokens")]
#[strum(serialize = "chatgptAuthTokens")]
ChatgptAuthTokens,
/// Programmatic Codex auth backed by a registered Agent Identity.
#[serde(rename = "agentIdentity")]
#[strum(serialize = "agentIdentity")]
AgentIdentity,
/// Programmatic Codex auth backed by a personal access token.
#[serde(rename = "personalAccessToken")]
#[strum(serialize = "personalAccessToken")]
PersonalAccessToken,
/// Amazon Bedrock bearer token managed by Codex.
#[serde(rename = "bedrockApiKey")]
#[strum(serialize = "bedrockApiKey")]
BedrockApiKey,
}
impl AuthMode {
/// Returns whether this mode represents an authenticated human ChatGPT account.
pub fn has_chatgpt_account(self) -> bool {
match self {
Self::Chatgpt | Self::ChatgptAuthTokens | Self::PersonalAccessToken => true,
Self::ApiKey | Self::AgentIdentity | Self::BedrockApiKey => false,
}
}
/// Returns whether this mode is backed by Codex services rather than a direct model API.
pub fn uses_codex_backend(self) -> bool {
match self {
Self::Chatgpt
| Self::ChatgptAuthTokens
| Self::AgentIdentity
| Self::PersonalAccessToken => true,
Self::ApiKey | Self::BedrockApiKey => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PlanType {
Known(KnownPlan),
Unknown(String),
}
impl PlanType {
pub fn from_raw_value(raw: &str) -> Self {
match raw.to_ascii_lowercase().as_str() {
"free" => Self::Known(KnownPlan::Free),
"go" => Self::Known(KnownPlan::Go),
"plus" => Self::Known(KnownPlan::Plus),
"pro" => Self::Known(KnownPlan::Pro),
"prolite" => Self::Known(KnownPlan::ProLite),
"team" => Self::Known(KnownPlan::Team),
"self_serve_business_usage_based" => {
Self::Known(KnownPlan::SelfServeBusinessUsageBased)
}
"business" => Self::Known(KnownPlan::Business),
"enterprise_cbp_usage_based" => Self::Known(KnownPlan::EnterpriseCbpUsageBased),
"enterprise" | "hc" => Self::Known(KnownPlan::Enterprise),
"education" | "edu" => Self::Known(KnownPlan::Edu),
_ => Self::Unknown(raw.to_string()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum KnownPlan {
Free,
Go,
Plus,
Pro,
ProLite,
Team,
#[serde(rename = "self_serve_business_usage_based")]
SelfServeBusinessUsageBased,
Business,
#[serde(rename = "enterprise_cbp_usage_based")]
EnterpriseCbpUsageBased,
#[serde(alias = "hc")]
Enterprise,
#[serde(alias = "education")]
Edu,
}
impl KnownPlan {
pub fn display_name(self) -> &'static str {
match self {
Self::Free => "Free",
Self::Go => "Go",
Self::Plus => "Plus",
Self::Pro => "Pro",
Self::ProLite => "Pro Lite",
Self::Team => "Team",
Self::SelfServeBusinessUsageBased => "Self Serve Business Usage Based",
Self::Business => "Business",
Self::EnterpriseCbpUsageBased => "Enterprise CBP Usage Based",
Self::Enterprise => "Enterprise",
Self::Edu => "Edu",
}
}
pub fn raw_value(self) -> &'static str {
match self {
Self::Free => "free",
Self::Go => "go",
Self::Plus => "plus",
Self::Pro => "pro",
Self::ProLite => "prolite",
Self::Team => "team",
Self::SelfServeBusinessUsageBased => "self_serve_business_usage_based",
Self::Business => "business",
Self::EnterpriseCbpUsageBased => "enterprise_cbp_usage_based",
Self::Enterprise => "enterprise",
Self::Edu => "edu",
}
}
pub fn is_workspace_account(self) -> bool {
matches!(
self,
Self::Team
| Self::SelfServeBusinessUsageBased
| Self::Business
| Self::EnterpriseCbpUsageBased
| Self::Enterprise
| Self::Edu
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("{message}")]
pub struct RefreshTokenFailedError {
pub reason: RefreshTokenFailedReason,
pub message: String,
}
impl RefreshTokenFailedError {
pub fn new(reason: RefreshTokenFailedReason, message: impl Into<String>) -> Self {
Self {
reason,
message: message.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefreshTokenFailedReason {
Expired,
Exhausted,
Revoked,
Other,
}
#[cfg(test)]
mod tests {
use super::KnownPlan;
use super::PlanType;
use pretty_assertions::assert_eq;
#[test]
fn plan_type_deserializes_raw_aliases() {
assert_eq!(
serde_json::from_str::<PlanType>("\"hc\"").expect("hc should deserialize"),
PlanType::Known(KnownPlan::Enterprise)
);
assert_eq!(
serde_json::from_str::<PlanType>("\"education\"")
.expect("education should deserialize"),
PlanType::Known(KnownPlan::Edu)
);
}
}