mirror of
https://github.com/openai/codex.git
synced 2026-05-24 13:04:29 +00: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.
147 lines
4.8 KiB
Rust
147 lines
4.8 KiB
Rust
use base64::Engine as _;
|
|
use chrono::DateTime;
|
|
use chrono::Local;
|
|
use chrono::Utc;
|
|
use reqwest::header::HeaderMap;
|
|
|
|
use codex_core::config::Config;
|
|
use codex_login::AuthManager;
|
|
|
|
pub fn set_user_agent_suffix(suffix: &str) {
|
|
if let Ok(mut guard) = codex_login::default_client::USER_AGENT_SUFFIX.lock() {
|
|
guard.replace(suffix.to_string());
|
|
}
|
|
}
|
|
|
|
pub fn append_error_log(message: impl AsRef<str>) {
|
|
let ts = Utc::now().to_rfc3339();
|
|
if let Ok(mut f) = std::fs::OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open("error.log")
|
|
{
|
|
use std::io::Write as _;
|
|
let _ = writeln!(f, "[{ts}] {}", message.as_ref());
|
|
}
|
|
}
|
|
|
|
/// Normalize the configured base URL to a canonical form used by the backend client.
|
|
/// - trims trailing '/'
|
|
/// - appends '/backend-api' for ChatGPT hosts when missing
|
|
pub fn normalize_base_url(input: &str) -> String {
|
|
let mut base_url = input.to_string();
|
|
while base_url.ends_with('/') {
|
|
base_url.pop();
|
|
}
|
|
if (base_url.starts_with("https://chatgpt.com")
|
|
|| base_url.starts_with("https://chat.openai.com"))
|
|
&& !base_url.contains("/backend-api")
|
|
{
|
|
base_url = format!("{base_url}/backend-api");
|
|
}
|
|
base_url
|
|
}
|
|
|
|
/// Extract the ChatGPT account id from a JWT token, when present.
|
|
pub fn extract_chatgpt_account_id(token: &str) -> Option<String> {
|
|
let mut parts = token.split('.');
|
|
let (_h, payload_b64, _s) = match (parts.next(), parts.next(), parts.next()) {
|
|
(Some(h), Some(p), Some(s)) if !h.is_empty() && !p.is_empty() && !s.is_empty() => (h, p, s),
|
|
_ => return None,
|
|
};
|
|
let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
|
.decode(payload_b64)
|
|
.ok()?;
|
|
let v: serde_json::Value = serde_json::from_slice(&payload_bytes).ok()?;
|
|
v.get("https://api.openai.com/auth")
|
|
.and_then(|auth| auth.get("chatgpt_account_id"))
|
|
.and_then(|id| id.as_str())
|
|
.map(str::to_string)
|
|
}
|
|
|
|
pub async fn load_auth_manager() -> Option<AuthManager> {
|
|
// TODO: pass in cli overrides once cloud tasks properly support them.
|
|
let config = Config::load_with_cli_overrides(Vec::new()).await.ok()?;
|
|
Some(AuthManager::new(
|
|
config.codex_home.to_path_buf(),
|
|
/*enable_codex_api_key_env*/ false,
|
|
config.cli_auth_credentials_store_mode,
|
|
Some(config.chatgpt_base_url),
|
|
))
|
|
}
|
|
|
|
/// Build headers for ChatGPT-backed requests: `User-Agent`, optional `Authorization`,
|
|
/// and optional `ChatGPT-Account-Id`.
|
|
pub async fn build_chatgpt_headers() -> HeaderMap {
|
|
use reqwest::header::AUTHORIZATION;
|
|
use reqwest::header::HeaderName;
|
|
use reqwest::header::HeaderValue;
|
|
use reqwest::header::USER_AGENT;
|
|
|
|
set_user_agent_suffix("codex_cloud_tasks_tui");
|
|
let ua = codex_login::default_client::get_codex_user_agent();
|
|
let mut headers = HeaderMap::new();
|
|
headers.insert(
|
|
USER_AGENT,
|
|
HeaderValue::from_str(&ua).unwrap_or(HeaderValue::from_static("codex-cli")),
|
|
);
|
|
if let Some(am) = load_auth_manager().await
|
|
&& let Some(auth) = am.auth().await
|
|
&& let Ok(tok) = auth.get_token()
|
|
&& !tok.is_empty()
|
|
{
|
|
let v = format!("Bearer {tok}");
|
|
if let Ok(hv) = HeaderValue::from_str(&v) {
|
|
headers.insert(AUTHORIZATION, hv);
|
|
}
|
|
if let Some(acc) = auth
|
|
.get_account_id()
|
|
.or_else(|| extract_chatgpt_account_id(&tok))
|
|
&& let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id")
|
|
&& let Ok(hv) = HeaderValue::from_str(&acc)
|
|
{
|
|
headers.insert(name, hv);
|
|
}
|
|
}
|
|
headers
|
|
}
|
|
|
|
/// Construct a browser-friendly task URL for the given backend base URL.
|
|
pub fn task_url(base_url: &str, task_id: &str) -> String {
|
|
let normalized = normalize_base_url(base_url);
|
|
if let Some(root) = normalized.strip_suffix("/backend-api") {
|
|
return format!("{root}/codex/tasks/{task_id}");
|
|
}
|
|
if let Some(root) = normalized.strip_suffix("/api/codex") {
|
|
return format!("{root}/codex/tasks/{task_id}");
|
|
}
|
|
if normalized.ends_with("/codex") {
|
|
return format!("{normalized}/tasks/{task_id}");
|
|
}
|
|
format!("{normalized}/codex/tasks/{task_id}")
|
|
}
|
|
|
|
pub fn format_relative_time(reference: DateTime<Utc>, ts: DateTime<Utc>) -> String {
|
|
let mut secs = (reference - ts).num_seconds();
|
|
if secs < 0 {
|
|
secs = 0;
|
|
}
|
|
if secs < 60 {
|
|
return format!("{secs}s ago");
|
|
}
|
|
let mins = secs / 60;
|
|
if mins < 60 {
|
|
return format!("{mins}m ago");
|
|
}
|
|
let hours = mins / 60;
|
|
if hours < 24 {
|
|
return format!("{hours}h ago");
|
|
}
|
|
let local = ts.with_timezone(&Local);
|
|
local.format("%b %e %H:%M").to_string()
|
|
}
|
|
|
|
pub fn format_relative_time_now(ts: DateTime<Utc>) -> String {
|
|
format_relative_time(Utc::now(), ts)
|
|
}
|