mirror of
https://github.com/openai/codex.git
synced 2026-05-05 13:51:29 +03:00
## Summary This PR adds `CodexAuth::AgentIdentity` as an explicit auth mode. An AgentIdentity auth record is a standalone `auth.json` mode. When `AuthManager::auth().await` loads that mode, it registers one process-scoped task and stores it in runtime-only state on the auth value. Header creation stays synchronous after that because the task is initialized before callers receive the auth object. This PR also removes the old feature flag path. AgentIdentity is selected by explicit auth mode, not by a hidden flag or lazy mutation of ChatGPT auth records. Reference old stack: https://github.com/openai/codex/pull/17387/changes ## Design Decisions - AgentIdentity is a real auth enum variant because it can be the only credential in `auth.json`. - The process task is ephemeral runtime state. It is not serialized and is not stored in rollout/session data. - Account/user metadata needed by existing Codex backend checks lives on the AgentIdentity record for now. - `is_chatgpt_auth()` remains token-specific. - `uses_codex_backend()` is the broader predicate for ChatGPT-token auth and AgentIdentity auth. ## Stack 1. https://github.com/openai/codex/pull/18757: full revert 2. https://github.com/openai/codex/pull/18871: isolated Agent Identity crate 3. This PR: explicit AgentIdentity auth mode and startup task allocation 4. https://github.com/openai/codex/pull/18811: migrate Codex backend auth callsites through AuthProvider 5. https://github.com/openai/codex/pull/18904: accept AgentIdentity JWTs and load `CODEX_AGENT_IDENTITY` ## Testing Tests: targeted Rust checks, cargo-shear, Bazel lock check, and CI.
215 lines
7.1 KiB
Rust
215 lines
7.1 KiB
Rust
#![cfg(test)]
|
|
|
|
use std::path::Path;
|
|
|
|
use codex_app_server_protocol::AuthMode;
|
|
use codex_config::types::AuthCredentialsStoreMode;
|
|
use codex_login::load_auth_dot_json;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub(crate) struct LocalChatgptAuth {
|
|
pub(crate) access_token: String,
|
|
pub(crate) chatgpt_account_id: String,
|
|
pub(crate) chatgpt_plan_type: Option<String>,
|
|
}
|
|
|
|
pub(crate) fn load_local_chatgpt_auth(
|
|
codex_home: &Path,
|
|
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
|
forced_chatgpt_workspace_id: Option<&str>,
|
|
) -> Result<LocalChatgptAuth, String> {
|
|
let auth = load_auth_dot_json(codex_home, auth_credentials_store_mode)
|
|
.map_err(|err| format!("failed to load local auth: {err}"))?
|
|
.ok_or_else(|| "no local auth available".to_string())?;
|
|
if matches!(auth.auth_mode, Some(AuthMode::ApiKey)) || auth.openai_api_key.is_some() {
|
|
return Err("local auth is not a ChatGPT login".to_string());
|
|
}
|
|
|
|
let tokens = auth
|
|
.tokens
|
|
.ok_or_else(|| "local ChatGPT auth is missing token data".to_string())?;
|
|
let access_token = tokens.access_token;
|
|
let chatgpt_account_id = tokens
|
|
.account_id
|
|
.or(tokens.id_token.chatgpt_account_id.clone())
|
|
.ok_or_else(|| "local ChatGPT auth is missing chatgpt account id".to_string())?;
|
|
if let Some(expected_workspace) = forced_chatgpt_workspace_id
|
|
&& chatgpt_account_id != expected_workspace
|
|
{
|
|
return Err(format!(
|
|
"local ChatGPT auth must use workspace {expected_workspace}, but found {chatgpt_account_id:?}"
|
|
));
|
|
}
|
|
|
|
let chatgpt_plan_type = tokens
|
|
.id_token
|
|
.get_chatgpt_plan_type_raw()
|
|
.map(|plan_type| plan_type.to_ascii_lowercase());
|
|
|
|
Ok(LocalChatgptAuth {
|
|
access_token,
|
|
chatgpt_account_id,
|
|
chatgpt_plan_type,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
use base64::Engine;
|
|
use chrono::Utc;
|
|
use codex_app_server_protocol::AuthMode;
|
|
use codex_login::AuthDotJson;
|
|
use codex_login::auth::login_with_chatgpt_auth_tokens;
|
|
use codex_login::save_auth;
|
|
use codex_login::token_data::TokenData;
|
|
use pretty_assertions::assert_eq;
|
|
use serde::Serialize;
|
|
use serde_json::json;
|
|
use tempfile::TempDir;
|
|
|
|
fn fake_jwt(email: &str, account_id: &str, plan_type: &str) -> String {
|
|
#[derive(Serialize)]
|
|
struct Header {
|
|
alg: &'static str,
|
|
typ: &'static str,
|
|
}
|
|
|
|
let header = Header {
|
|
alg: "none",
|
|
typ: "JWT",
|
|
};
|
|
let payload = json!({
|
|
"email": email,
|
|
"https://api.openai.com/auth": {
|
|
"chatgpt_account_id": account_id,
|
|
"chatgpt_plan_type": plan_type,
|
|
},
|
|
});
|
|
let encode = |bytes: &[u8]| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
|
|
let header_b64 = encode(&serde_json::to_vec(&header).expect("serialize header"));
|
|
let payload_b64 = encode(&serde_json::to_vec(&payload).expect("serialize payload"));
|
|
let signature_b64 = encode(b"sig");
|
|
format!("{header_b64}.{payload_b64}.{signature_b64}")
|
|
}
|
|
|
|
fn write_chatgpt_auth(codex_home: &Path, plan_type: &str) {
|
|
let id_token = fake_jwt("user@example.com", "workspace-1", plan_type);
|
|
let access_token = fake_jwt("user@example.com", "workspace-1", plan_type);
|
|
let auth = AuthDotJson {
|
|
auth_mode: Some(AuthMode::Chatgpt),
|
|
openai_api_key: None,
|
|
tokens: Some(TokenData {
|
|
id_token: codex_login::token_data::parse_chatgpt_jwt_claims(&id_token)
|
|
.expect("id token should parse"),
|
|
access_token,
|
|
refresh_token: "refresh-token".to_string(),
|
|
account_id: Some("workspace-1".to_string()),
|
|
}),
|
|
last_refresh: Some(Utc::now()),
|
|
agent_identity: None,
|
|
};
|
|
save_auth(codex_home, &auth, AuthCredentialsStoreMode::File)
|
|
.expect("chatgpt auth should save");
|
|
}
|
|
|
|
#[test]
|
|
fn loads_local_chatgpt_auth_from_managed_auth() {
|
|
let codex_home = TempDir::new().expect("tempdir");
|
|
write_chatgpt_auth(codex_home.path(), "business");
|
|
|
|
let auth = load_local_chatgpt_auth(
|
|
codex_home.path(),
|
|
AuthCredentialsStoreMode::File,
|
|
Some("workspace-1"),
|
|
)
|
|
.expect("chatgpt auth should load");
|
|
|
|
assert_eq!(auth.chatgpt_account_id, "workspace-1");
|
|
assert_eq!(auth.chatgpt_plan_type.as_deref(), Some("business"));
|
|
assert!(!auth.access_token.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_missing_local_auth() {
|
|
let codex_home = TempDir::new().expect("tempdir");
|
|
|
|
let err = load_local_chatgpt_auth(
|
|
codex_home.path(),
|
|
AuthCredentialsStoreMode::File,
|
|
/*forced_chatgpt_workspace_id*/ None,
|
|
)
|
|
.expect_err("missing auth should fail");
|
|
|
|
assert_eq!(err, "no local auth available");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_api_key_auth() {
|
|
let codex_home = TempDir::new().expect("tempdir");
|
|
save_auth(
|
|
codex_home.path(),
|
|
&AuthDotJson {
|
|
auth_mode: Some(AuthMode::ApiKey),
|
|
openai_api_key: Some("sk-test".to_string()),
|
|
tokens: None,
|
|
last_refresh: None,
|
|
agent_identity: None,
|
|
},
|
|
AuthCredentialsStoreMode::File,
|
|
)
|
|
.expect("api key auth should save");
|
|
|
|
let err = load_local_chatgpt_auth(
|
|
codex_home.path(),
|
|
AuthCredentialsStoreMode::File,
|
|
/*forced_chatgpt_workspace_id*/ None,
|
|
)
|
|
.expect_err("api key auth should fail");
|
|
|
|
assert_eq!(err, "local auth is not a ChatGPT login");
|
|
}
|
|
|
|
#[test]
|
|
fn prefers_managed_auth_over_external_ephemeral_tokens() {
|
|
let codex_home = TempDir::new().expect("tempdir");
|
|
write_chatgpt_auth(codex_home.path(), "business");
|
|
login_with_chatgpt_auth_tokens(
|
|
codex_home.path(),
|
|
&fake_jwt("user@example.com", "workspace-2", "enterprise"),
|
|
"workspace-2",
|
|
Some("enterprise"),
|
|
)
|
|
.expect("external auth should save");
|
|
|
|
let auth = load_local_chatgpt_auth(
|
|
codex_home.path(),
|
|
AuthCredentialsStoreMode::File,
|
|
Some("workspace-1"),
|
|
)
|
|
.expect("managed auth should win");
|
|
|
|
assert_eq!(auth.chatgpt_account_id, "workspace-1");
|
|
assert_eq!(auth.chatgpt_plan_type.as_deref(), Some("business"));
|
|
}
|
|
|
|
#[test]
|
|
fn preserves_usage_based_plan_type_wire_name() {
|
|
let codex_home = TempDir::new().expect("tempdir");
|
|
write_chatgpt_auth(codex_home.path(), "self_serve_business_usage_based");
|
|
|
|
let auth = load_local_chatgpt_auth(
|
|
codex_home.path(),
|
|
AuthCredentialsStoreMode::File,
|
|
Some("workspace-1"),
|
|
)
|
|
.expect("chatgpt auth should load");
|
|
|
|
assert_eq!(
|
|
auth.chatgpt_plan_type.as_deref(),
|
|
Some("self_serve_business_usage_based")
|
|
);
|
|
}
|
|
}
|