fix: fully revert agent identity runtime wiring (#18757)

## Summary

This PR fully reverts the previously merged Agent Identity runtime
integration from the old stack:
https://github.com/openai/codex/pull/17387/changes

It removes the Codex-side task lifecycle wiring, rollout/session
persistence, feature flag plumbing, lazy `auth.json` mutation,
background task auth paths, and request callsite changes introduced by
that stack.

This leaves the repo in a clean pre-AgentIdentity integration state so
the follow-up PRs can reintroduce the pieces in smaller reviewable
layers.

## Stack

1. This PR: full revert
2. https://github.com/openai/codex/pull/18871: move Agent Identity
business logic into a crate
3. https://github.com/openai/codex/pull/18785: add explicit
AgentIdentity auth mode and startup task allocation
4. https://github.com/openai/codex/pull/18811: migrate auth callsites
through AuthProvider

## Testing

Tests: targeted Rust checks, cargo-shear, Bazel lock check, and CI.
This commit is contained in:
efrazer-oai authored and GitHub committed 2026-04-21 14:30:55 -07:00
1 parent 69c3d12274
commit be75785504
85 files changed
+334 -5409

No files matched your search

-834
View File
@@ -1,834 +0,0 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::Duration;
use anyhow::Context;
use anyhow::Result;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use chrono::SecondsFormat;
use chrono::Utc;
use codex_protocol::protocol::SessionSource;
use crypto_box::SecretKey as Curve25519SecretKey;
use ed25519_dalek::Signer as _;
use ed25519_dalek::SigningKey;
use ed25519_dalek::VerifyingKey;
use ed25519_dalek::pkcs8::DecodePrivateKey;
use ed25519_dalek::pkcs8::EncodePrivateKey;
use rand::TryRngCore;
use rand::rngs::OsRng;
use serde::Deserialize;
use serde::Serialize;
use sha2::Digest as _;
use sha2::Sha512;
use tokio::sync::Semaphore;
use tracing::debug;
use tracing::info;
use tracing::warn;
use crate::AgentIdentityAuthRecord;
use crate::AuthManager;
use crate::CodexAuth;
use crate::default_client::create_client;
const AGENT_REGISTRATION_TIMEOUT: Duration = Duration::from_secs(15);
const AGENT_TASK_REGISTRATION_TIMEOUT: Duration = Duration::from_secs(15);
const AGENT_IDENTITY_BISCUIT_TIMEOUT: Duration = Duration::from_secs(15);
#[derive(Clone)]
pub(crate) struct BackgroundAgentTaskManager {
auth_manager: Arc<AuthManager>,
chatgpt_base_url: String,
auth_mode: BackgroundAgentTaskAuthMode,
abom: AgentBillOfMaterials,
ensure_lock: Arc<Semaphore>,
}
impl std::fmt::Debug for BackgroundAgentTaskManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BackgroundAgentTaskManager")
.field("chatgpt_base_url", &self.chatgpt_base_url)
.field("auth_mode", &self.auth_mode)
.field("abom", &self.abom)
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BackgroundAgentTaskAuthMode {
Enabled,
#[default]
Disabled,
}
impl BackgroundAgentTaskAuthMode {
pub fn from_feature_enabled(enabled: bool) -> Self {
if enabled {
Self::Enabled
} else {
Self::Disabled
}
}
fn is_enabled(self) -> bool {
matches!(self, Self::Enabled)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
struct StoredAgentIdentity {
binding_id: String,
chatgpt_account_id: String,
chatgpt_user_id: Option<String>,
agent_runtime_id: String,
private_key_pkcs8_base64: String,
public_key_ssh: String,
registered_at: String,
background_task_id: Option<String>,
abom: AgentBillOfMaterials,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
struct AgentBillOfMaterials {
agent_version: String,
agent_harness_id: String,
running_location: String,
}
#[derive(Debug, Serialize)]
struct RegisterAgentRequest {
abom: AgentBillOfMaterials,
agent_public_key: String,
capabilities: Vec<String>,
}
#[derive(Debug, Deserialize)]
struct RegisterAgentResponse {
agent_runtime_id: String,
}
#[derive(Debug, Serialize)]
struct RegisterTaskRequest {
signature: String,
timestamp: String,
}
#[derive(Debug, Deserialize)]
struct RegisterTaskResponse {
encrypted_task_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct AgentIdentityBinding {
binding_id: String,
chatgpt_account_id: String,
chatgpt_user_id: Option<String>,
access_token: String,
}
struct GeneratedAgentKeyMaterial {
private_key_pkcs8_base64: String,
public_key_ssh: String,
}
impl BackgroundAgentTaskManager {
#[cfg(test)]
pub(crate) fn new(
auth_manager: Arc<AuthManager>,
chatgpt_base_url: String,
session_source: SessionSource,
) -> Self {
Self::new_with_auth_mode(
auth_manager,
chatgpt_base_url,
session_source,
BackgroundAgentTaskAuthMode::Disabled,
)
}
pub(crate) fn new_with_auth_mode(
auth_manager: Arc<AuthManager>,
chatgpt_base_url: String,
session_source: SessionSource,
auth_mode: BackgroundAgentTaskAuthMode,
) -> Self {
Self {
auth_manager,
chatgpt_base_url: normalize_chatgpt_base_url(&chatgpt_base_url),
auth_mode,
abom: build_abom(session_source),
ensure_lock: Arc::new(Semaphore::new(/*permits*/ 1)),
}
}
pub(crate) async fn authorization_header_value_for_auth(
&self,
auth: &CodexAuth,
) -> Result<Option<String>> {
if !self.auth_mode.is_enabled() {
debug!("skipping background agent task auth because agent identity is disabled");
return Ok(None);
}
if !supports_background_agent_task_auth(&self.chatgpt_base_url) {
debug!(
chatgpt_base_url = %self.chatgpt_base_url,
"skipping background agent task auth for unsupported backend host"
);
return Ok(None);
}
let Some(binding) =
AgentIdentityBinding::from_auth(auth, self.auth_manager.forced_chatgpt_workspace_id())
else {
debug!("skipping background agent task auth because ChatGPT auth is unavailable");
return Ok(None);
};
let _guard = self
.ensure_lock
.acquire()
.await
.context("background agent task ensure semaphore closed")?;
let mut stored_identity = self
.ensure_registered_identity_for_binding(auth, &binding)
.await?;
let background_task_id = match stored_identity.background_task_id.clone() {
Some(background_task_id) => background_task_id,
_ => {
let background_task_id = self
.register_background_task_for_identity(&binding, &stored_identity)
.await?;
stored_identity.background_task_id = Some(background_task_id.clone());
self.store_identity(auth, &stored_identity)?;
background_task_id
}
};
Ok(Some(authorization_header_for_task(
&stored_identity,
&background_task_id,
)?))
}
pub(crate) async fn authorization_header_value_or_bearer(
&self,
auth: &CodexAuth,
) -> Option<String> {
match self.authorization_header_value_for_auth(auth).await {
Ok(Some(authorization_header_value)) => Some(authorization_header_value),
Ok(None) => auth
.get_token()
.ok()
.filter(|token| !token.is_empty())
.map(|token| format!("Bearer {token}")),
Err(error) => {
warn!(
error = %error,
"falling back to bearer authorization because background agent task auth failed"
);
auth.get_token()
.ok()
.filter(|token| !token.is_empty())
.map(|token| format!("Bearer {token}"))
}
}
}
async fn ensure_registered_identity_for_binding(
&self,
auth: &CodexAuth,
binding: &AgentIdentityBinding,
) -> Result<StoredAgentIdentity> {
if let Some(stored_identity) = self.load_stored_identity(auth, binding)? {
return Ok(stored_identity);
}
let stored_identity = self.register_agent_identity(binding).await?;
self.store_identity(auth, &stored_identity)?;
Ok(stored_identity)
}
async fn register_agent_identity(
&self,
binding: &AgentIdentityBinding,
) -> Result<StoredAgentIdentity> {
let key_material = generate_agent_key_material()?;
let request_body = RegisterAgentRequest {
abom: self.abom.clone(),
agent_public_key: key_material.public_key_ssh.clone(),
capabilities: Vec::new(),
};
let url = agent_registration_url(&self.chatgpt_base_url);
let human_biscuit = self.mint_human_biscuit(binding, "POST", &url).await?;
let client = create_client();
let response = client
.post(&url)
.header("X-OpenAI-Authorization", human_biscuit)
.json(&request_body)
.timeout(AGENT_REGISTRATION_TIMEOUT)
.send()
.await
.with_context(|| {
format!("failed to send agent identity registration request to {url}")
})?;
if response.status().is_success() {
let response_body = response
.json::<RegisterAgentResponse>()
.await
.with_context(|| format!("failed to parse agent identity response from {url}"))?;
let stored_identity = StoredAgentIdentity {
binding_id: binding.binding_id.clone(),
chatgpt_account_id: binding.chatgpt_account_id.clone(),
chatgpt_user_id: binding.chatgpt_user_id.clone(),
agent_runtime_id: response_body.agent_runtime_id,
private_key_pkcs8_base64: key_material.private_key_pkcs8_base64,
public_key_ssh: key_material.public_key_ssh,
registered_at: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
background_task_id: None,
abom: self.abom.clone(),
};
info!(
agent_runtime_id = %stored_identity.agent_runtime_id,
binding_id = %binding.binding_id,
"registered background agent identity"
);
return Ok(stored_identity);
}
let status = response.status();
let body = response.text().await.unwrap_or_default();
anyhow::bail!("agent identity registration failed with status {status} from {url}: {body}")
}
async fn register_background_task_for_identity(
&self,
binding: &AgentIdentityBinding,
stored_identity: &StoredAgentIdentity,
) -> Result<String> {
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
let request_body = RegisterTaskRequest {
signature: sign_task_registration_payload(stored_identity, &timestamp)?,
timestamp,
};
let client = create_client();
let url =
agent_task_registration_url(&self.chatgpt_base_url, &stored_identity.agent_runtime_id);
let human_biscuit = self.mint_human_biscuit(binding, "POST", &url).await?;
let response = client
.post(&url)
.header("X-OpenAI-Authorization", human_biscuit)
.json(&request_body)
.timeout(AGENT_TASK_REGISTRATION_TIMEOUT)
.send()
.await
.with_context(|| format!("failed to send background agent task request to {url}"))?;
if response.status().is_success() {
let response_body = response
.json::<RegisterTaskResponse>()
.await
.with_context(|| format!("failed to parse background task response from {url}"))?;
let background_task_id =
decrypt_task_id_response(stored_identity, &response_body.encrypted_task_id)?;
info!(
agent_runtime_id = %stored_identity.agent_runtime_id,
task_id = %background_task_id,
"registered background agent task"
);
return Ok(background_task_id);
}
let status = response.status();
let body = response.text().await.unwrap_or_default();
anyhow::bail!(
"background agent task registration failed with status {status} from {url}: {body}"
)
}
async fn mint_human_biscuit(
&self,
binding: &AgentIdentityBinding,
target_method: &str,
target_url: &str,
) -> Result<String> {
let url = agent_identity_biscuit_url(&self.chatgpt_base_url);
let request_id = agent_identity_request_id()?;
let client = create_client();
let response = client
.get(&url)
.bearer_auth(&binding.access_token)
.header("X-Request-Id", request_id.clone())
.header("X-Original-Method", target_method)
.header("X-Original-Url", target_url)
.timeout(AGENT_IDENTITY_BISCUIT_TIMEOUT)
.send()
.await
.with_context(|| format!("failed to send agent identity biscuit request to {url}"))?;
if response.status().is_success() {
let human_biscuit = response
.headers()
.get("x-openai-authorization")
.context("agent identity biscuit response did not include x-openai-authorization")?
.to_str()
.context("agent identity biscuit response header was not valid UTF-8")?
.to_string();
info!(
request_id = %request_id,
"minted human biscuit for background agent task"
);
return Ok(human_biscuit);
}
let status = response.status();
let body = response.text().await.unwrap_or_default();
anyhow::bail!(
"agent identity biscuit minting failed with status {status} from {url}: {body}"
)
}
fn load_stored_identity(
&self,
auth: &CodexAuth,
binding: &AgentIdentityBinding,
) -> Result<Option<StoredAgentIdentity>> {
let Some(record) = auth.get_agent_identity(&binding.chatgpt_account_id) else {
return Ok(None);
};
let stored_identity =
match StoredAgentIdentity::from_auth_record(binding, record, self.abom.clone()) {
Ok(stored_identity) => stored_identity,
Err(error) => {
warn!(
binding_id = %binding.binding_id,
error = %error,
"stored agent identity is invalid; deleting cached value"
);
auth.remove_agent_identity()?;
return Ok(None);
}
};
if !stored_identity.matches_binding(binding) {
warn!(
binding_id = %binding.binding_id,
"stored agent identity binding no longer matches current auth; deleting cached value"
);
auth.remove_agent_identity()?;
return Ok(None);
}
if let Err(error) = stored_identity.validate_key_material() {
warn!(
agent_runtime_id = %stored_identity.agent_runtime_id,
binding_id = %binding.binding_id,
error = %error,
"stored agent identity key material is invalid; deleting cached value"
);
auth.remove_agent_identity()?;
return Ok(None);
}
Ok(Some(stored_identity))
}
fn store_identity(
&self,
auth: &CodexAuth,
stored_identity: &StoredAgentIdentity,
) -> Result<()> {
auth.set_agent_identity(stored_identity.to_auth_record())?;
Ok(())
}
}
pub fn cached_background_agent_task_authorization_header_value(
auth: &CodexAuth,
auth_mode: BackgroundAgentTaskAuthMode,
) -> Result<Option<String>> {
if !auth_mode.is_enabled() {
return Ok(None);
}
let Some(binding) = AgentIdentityBinding::from_auth(auth, /*forced_workspace_id*/ None) else {
return Ok(None);
};
let Some(record) = auth.get_agent_identity(&binding.chatgpt_account_id) else {
return Ok(None);
};
let stored_identity =
StoredAgentIdentity::from_auth_record(&binding, record, build_abom(SessionSource::Cli))?;
if !stored_identity.matches_binding(&binding) {
return Ok(None);
}
stored_identity.validate_key_material()?;
let Some(background_task_id) = stored_identity.background_task_id.as_ref() else {
return Ok(None);
};
authorization_header_for_task(&stored_identity, background_task_id).map(Some)
}
impl StoredAgentIdentity {
fn from_auth_record(
binding: &AgentIdentityBinding,
record: AgentIdentityAuthRecord,
abom: AgentBillOfMaterials,
) -> Result<Self> {
if record.workspace_id != binding.chatgpt_account_id {
anyhow::bail!(
"stored agent identity workspace {:?} does not match current workspace {:?}",
record.workspace_id,
binding.chatgpt_account_id
);
}
let signing_key = signing_key_from_private_key_pkcs8_base64(&record.agent_private_key)?;
Ok(Self {
binding_id: binding.binding_id.clone(),
chatgpt_account_id: binding.chatgpt_account_id.clone(),
chatgpt_user_id: record.chatgpt_user_id,
agent_runtime_id: record.agent_runtime_id.clone(),
private_key_pkcs8_base64: record.agent_private_key,
public_key_ssh: encode_ssh_ed25519_public_key(&signing_key.verifying_key()),
registered_at: record.registered_at,
background_task_id: record.background_task_id,
abom,
})
}
fn to_auth_record(&self) -> AgentIdentityAuthRecord {
AgentIdentityAuthRecord {
workspace_id: self.chatgpt_account_id.clone(),
chatgpt_user_id: self.chatgpt_user_id.clone(),
agent_runtime_id: self.agent_runtime_id.clone(),
agent_private_key: self.private_key_pkcs8_base64.clone(),
registered_at: self.registered_at.clone(),
background_task_id: self.background_task_id.clone(),
}
}
fn matches_binding(&self, binding: &AgentIdentityBinding) -> bool {
binding.matches_parts(
&self.binding_id,
&self.chatgpt_account_id,
self.chatgpt_user_id.as_deref(),
)
}
fn validate_key_material(&self) -> Result<()> {
let signing_key = self.signing_key()?;
let derived_public_key = encode_ssh_ed25519_public_key(&signing_key.verifying_key());
anyhow::ensure!(
self.public_key_ssh == derived_public_key,
"stored public key does not match the private key"
);
Ok(())
}
fn signing_key(&self) -> Result<SigningKey> {
signing_key_from_private_key_pkcs8_base64(&self.private_key_pkcs8_base64)
}
}
impl AgentIdentityBinding {
fn matches_parts(
&self,
binding_id: &str,
chatgpt_account_id: &str,
chatgpt_user_id: Option<&str>,
) -> bool {
binding_id == self.binding_id
&& chatgpt_account_id == self.chatgpt_account_id
&& match self.chatgpt_user_id.as_deref() {
Some(expected_user_id) => chatgpt_user_id == Some(expected_user_id),
None => true,
}
}
fn from_auth(auth: &CodexAuth, forced_workspace_id: Option<String>) -> Option<Self> {
if !auth.is_chatgpt_auth() {
return None;
}
let token_data = auth.get_token_data().ok()?;
let resolved_account_id =
forced_workspace_id
.filter(|value| !value.is_empty())
.or(token_data
.account_id
.clone()
.filter(|value| !value.is_empty()))?;
Some(Self {
binding_id: format!("chatgpt-account-{resolved_account_id}"),
chatgpt_account_id: resolved_account_id,
chatgpt_user_id: token_data
.id_token
.chatgpt_user_id
.filter(|value| !value.is_empty()),
access_token: token_data.access_token,
})
}
}
fn authorization_header_for_task(
stored_identity: &StoredAgentIdentity,
background_task_id: &str,
) -> Result<String> {
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
let signature = sign_agent_assertion_payload(stored_identity, background_task_id, &timestamp)?;
let payload = serde_json::to_vec(&BTreeMap::from([
(
"agent_runtime_id",
stored_identity.agent_runtime_id.as_str(),
),
("signature", signature.as_str()),
("task_id", background_task_id),
("timestamp", timestamp.as_str()),
]))
.context("failed to serialize agent assertion envelope")?;
Ok(format!(
"AgentAssertion {}",
URL_SAFE_NO_PAD.encode(payload)
))
}
fn sign_agent_assertion_payload(
stored_identity: &StoredAgentIdentity,
background_task_id: &str,
timestamp: &str,
) -> Result<String> {
let signing_key = stored_identity.signing_key()?;
let payload = format!(
"{}:{background_task_id}:{timestamp}",
stored_identity.agent_runtime_id
);
Ok(BASE64_STANDARD.encode(signing_key.sign(payload.as_bytes()).to_bytes()))
}
fn sign_task_registration_payload(
stored_identity: &StoredAgentIdentity,
timestamp: &str,
) -> Result<String> {
let signing_key = stored_identity.signing_key()?;
let payload = format!("{}:{timestamp}", stored_identity.agent_runtime_id);
Ok(BASE64_STANDARD.encode(signing_key.sign(payload.as_bytes()).to_bytes()))
}
fn decrypt_task_id_response(
stored_identity: &StoredAgentIdentity,
encrypted_task_id: &str,
) -> Result<String> {
let signing_key = stored_identity.signing_key()?;
let ciphertext = BASE64_STANDARD
.decode(encrypted_task_id)
.context("encrypted task id is not valid base64")?;
let plaintext = curve25519_secret_key_from_signing_key(&signing_key)
.unseal(&ciphertext)
.map_err(|_| anyhow::anyhow!("failed to decrypt encrypted task id"))?;
String::from_utf8(plaintext).context("decrypted task id is not valid UTF-8")
}
fn curve25519_secret_key_from_signing_key(signing_key: &SigningKey) -> Curve25519SecretKey {
let digest = Sha512::digest(signing_key.to_bytes());
let mut secret_key = [0u8; 32];
secret_key.copy_from_slice(&digest[..32]);
secret_key[0] &= 248;
secret_key[31] &= 127;
secret_key[31] |= 64;
Curve25519SecretKey::from(secret_key)
}
fn build_abom(session_source: SessionSource) -> AgentBillOfMaterials {
AgentBillOfMaterials {
agent_version: env!("CARGO_PKG_VERSION").to_string(),
agent_harness_id: match &session_source {
SessionSource::VSCode => "codex-app".to_string(),
SessionSource::Cli
| SessionSource::Exec
| SessionSource::Mcp
| SessionSource::Custom(_)
| SessionSource::SubAgent(_)
| SessionSource::Unknown => "codex-cli".to_string(),
},
running_location: format!("{}-{}", session_source, std::env::consts::OS),
}
}
fn generate_agent_key_material() -> Result<GeneratedAgentKeyMaterial> {
let mut secret_key_bytes = [0u8; 32];
OsRng
.try_fill_bytes(&mut secret_key_bytes)
.context("failed to generate agent identity private key bytes")?;
let signing_key = SigningKey::from_bytes(&secret_key_bytes);
let private_key_pkcs8 = signing_key
.to_pkcs8_der()
.context("failed to encode agent identity private key as PKCS#8")?;
Ok(GeneratedAgentKeyMaterial {
private_key_pkcs8_base64: BASE64_STANDARD.encode(private_key_pkcs8.as_bytes()),
public_key_ssh: encode_ssh_ed25519_public_key(&signing_key.verifying_key()),
})
}
fn encode_ssh_ed25519_public_key(verifying_key: &VerifyingKey) -> String {
let mut blob = Vec::with_capacity(4 + 11 + 4 + 32);
append_ssh_string(&mut blob, b"ssh-ed25519");
append_ssh_string(&mut blob, verifying_key.as_bytes());
format!("ssh-ed25519 {}", BASE64_STANDARD.encode(blob))
}
fn append_ssh_string(buf: &mut Vec<u8>, value: &[u8]) {
buf.extend_from_slice(&(value.len() as u32).to_be_bytes());
buf.extend_from_slice(value);
}
fn signing_key_from_private_key_pkcs8_base64(private_key_pkcs8_base64: &str) -> Result<SigningKey> {
let private_key = BASE64_STANDARD
.decode(private_key_pkcs8_base64)
.context("stored agent identity private key is not valid base64")?;
SigningKey::from_pkcs8_der(&private_key)
.context("stored agent identity private key is not valid PKCS#8")
}
fn agent_registration_url(chatgpt_base_url: &str) -> String {
let trimmed = chatgpt_base_url.trim_end_matches('/');
format!("{trimmed}/v1/agent/register")
}
fn agent_task_registration_url(chatgpt_base_url: &str, agent_runtime_id: &str) -> String {
let trimmed = chatgpt_base_url.trim_end_matches('/');
format!("{trimmed}/v1/agent/{agent_runtime_id}/task/register")
}
fn agent_identity_biscuit_url(chatgpt_base_url: &str) -> String {
let trimmed = chatgpt_base_url.trim_end_matches('/');
format!("{trimmed}/authenticate_app_v2")
}
fn agent_identity_request_id() -> Result<String> {
let mut request_id_bytes = [0u8; 16];
OsRng
.try_fill_bytes(&mut request_id_bytes)
.context("failed to generate agent identity request id")?;
Ok(format!(
"codex-agent-identity-{}",
URL_SAFE_NO_PAD.encode(request_id_bytes)
))
}
fn normalize_chatgpt_base_url(chatgpt_base_url: &str) -> String {
let mut base_url = chatgpt_base_url.trim_end_matches('/').to_string();
for suffix in [
"/wham/remote/control/server/enroll",
"/wham/remote/control/server",
] {
if let Some(stripped) = base_url.strip_suffix(suffix) {
base_url = stripped.to_string();
break;
}
}
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");
}
if let Some(stripped) = base_url.strip_suffix("/codex") {
stripped.to_string()
} else {
base_url
}
}
fn supports_background_agent_task_auth(chatgpt_base_url: &str) -> bool {
let Ok(url) = url::Url::parse(chatgpt_base_url) else {
return false;
};
let Some(host) = url.host_str() else {
return false;
};
host == "chatgpt.com"
|| host == "chat.openai.com"
|| host == "chatgpt-staging.com"
|| host.ends_with(".chatgpt.com")
|| host.ends_with(".chatgpt-staging.com")
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn disabled_background_agent_task_auth_returns_none_for_supported_host() {
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let auth_manager = AuthManager::from_auth_for_testing(auth.clone());
let manager = BackgroundAgentTaskManager::new_with_auth_mode(
auth_manager,
"https://chatgpt.com/backend-api".to_string(),
SessionSource::Cli,
BackgroundAgentTaskAuthMode::Disabled,
);
let authorization_header_value = manager
.authorization_header_value_for_auth(&auth)
.await
.expect("disabled manager should not fail");
assert_eq!(None, authorization_header_value);
}
#[tokio::test]
async fn default_background_agent_task_auth_returns_none_for_supported_host() {
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let auth_manager = AuthManager::from_auth_for_testing(auth.clone());
let manager = BackgroundAgentTaskManager::new(
auth_manager,
"https://chatgpt.com/backend-api".to_string(),
SessionSource::Cli,
);
let authorization_header_value = manager
.authorization_header_value_for_auth(&auth)
.await
.expect("default manager should not fail");
assert_eq!(None, authorization_header_value);
}
#[test]
fn cached_background_agent_task_auth_honors_disabled_mode() {
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let key_material = generate_agent_key_material().expect("generate key material");
auth.set_agent_identity(AgentIdentityAuthRecord {
workspace_id: "account_id".to_string(),
chatgpt_user_id: None,
agent_runtime_id: "agent_123".to_string(),
agent_private_key: key_material.private_key_pkcs8_base64,
registered_at: "2026-04-13T12:00:00Z".to_string(),
background_task_id: Some("task_123".to_string()),
})
.expect("set agent identity");
let disabled_authorization_header_value =
cached_background_agent_task_authorization_header_value(
&auth,
BackgroundAgentTaskAuthMode::Disabled,
)
.expect("disabled cached auth should not fail");
let enabled_authorization_header_value =
cached_background_agent_task_authorization_header_value(
&auth,
BackgroundAgentTaskAuthMode::Enabled,
)
.expect("enabled cached auth should not fail");
assert_eq!(None, disabled_authorization_header_value);
assert!(enabled_authorization_header_value.is_some());
}
}
-173
View File
@@ -1,173 +0,0 @@
use std::collections::BTreeMap;
use anyhow::Context;
use anyhow::Result;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use chrono::SecondsFormat;
use chrono::Utc;
use ed25519_dalek::Signer as _;
use ed25519_dalek::SigningKey;
use ed25519_dalek::pkcs8::DecodePrivateKey;
use serde::Deserialize;
use serde::Serialize;
use super::storage::AgentIdentityAuthRecord;
/// Task binding to use when constructing a task-scoped AgentAssertion.
///
/// The caller owns the task lifecycle. `AuthManager` only uses this target to
/// sign an authorization header with the stored agent identity key material.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AgentTaskAuthorizationTarget<'a> {
pub agent_runtime_id: &'a str,
pub task_id: &'a str,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
struct AgentAssertionEnvelope {
agent_runtime_id: String,
task_id: String,
timestamp: String,
signature: String,
}
pub(super) fn authorization_header_for_agent_task(
record: &AgentIdentityAuthRecord,
target: AgentTaskAuthorizationTarget<'_>,
) -> Result<String> {
anyhow::ensure!(
record.agent_runtime_id == target.agent_runtime_id,
"agent task runtime {} does not match stored agent identity {}",
target.agent_runtime_id,
record.agent_runtime_id
);
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
let envelope = AgentAssertionEnvelope {
agent_runtime_id: target.agent_runtime_id.to_string(),
task_id: target.task_id.to_string(),
timestamp: timestamp.clone(),
signature: sign_agent_assertion_payload(record, target, &timestamp)?,
};
let serialized_assertion = serialize_agent_assertion(&envelope)?;
Ok(format!("AgentAssertion {serialized_assertion}"))
}
fn sign_agent_assertion_payload(
record: &AgentIdentityAuthRecord,
target: AgentTaskAuthorizationTarget<'_>,
timestamp: &str,
) -> Result<String> {
let signing_key = signing_key_from_agent_private_key(&record.agent_private_key)?;
let payload = format!("{}:{}:{timestamp}", target.agent_runtime_id, target.task_id);
Ok(BASE64_STANDARD.encode(signing_key.sign(payload.as_bytes()).to_bytes()))
}
fn serialize_agent_assertion(envelope: &AgentAssertionEnvelope) -> Result<String> {
let payload = serde_json::to_vec(&BTreeMap::from([
("agent_runtime_id", envelope.agent_runtime_id.as_str()),
("signature", envelope.signature.as_str()),
("task_id", envelope.task_id.as_str()),
("timestamp", envelope.timestamp.as_str()),
]))
.context("failed to serialize agent assertion envelope")?;
Ok(URL_SAFE_NO_PAD.encode(payload))
}
fn signing_key_from_agent_private_key(agent_private_key: &str) -> Result<SigningKey> {
let private_key = BASE64_STANDARD
.decode(agent_private_key)
.context("stored agent identity private key is not valid base64")?;
SigningKey::from_pkcs8_der(&private_key)
.context("stored agent identity private key is not valid PKCS#8")
}
#[cfg(test)]
mod tests {
use ed25519_dalek::Signature;
use ed25519_dalek::Verifier as _;
use ed25519_dalek::pkcs8::EncodePrivateKey;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn authorization_header_for_agent_task_serializes_signed_agent_assertion() {
let record = test_agent_identity_record("agent-123");
let target = AgentTaskAuthorizationTarget {
agent_runtime_id: "agent-123",
task_id: "task-123",
};
let header = authorization_header_for_agent_task(&record, target)
.expect("build agent assertion header");
let token = header
.strip_prefix("AgentAssertion ")
.expect("agent assertion scheme");
let payload = URL_SAFE_NO_PAD
.decode(token)
.expect("valid base64url payload");
let envelope: AgentAssertionEnvelope =
serde_json::from_slice(&payload).expect("valid assertion envelope");
assert_eq!(
envelope,
AgentAssertionEnvelope {
agent_runtime_id: "agent-123".to_string(),
task_id: "task-123".to_string(),
timestamp: envelope.timestamp.clone(),
signature: envelope.signature.clone(),
}
);
let signature_bytes = BASE64_STANDARD
.decode(&envelope.signature)
.expect("valid base64 signature");
let signature = Signature::from_slice(&signature_bytes).expect("valid signature bytes");
signing_key_from_agent_private_key(&record.agent_private_key)
.expect("signing key")
.verifying_key()
.verify(
format!(
"{}:{}:{}",
envelope.agent_runtime_id, envelope.task_id, envelope.timestamp
)
.as_bytes(),
&signature,
)
.expect("signature should verify");
}
#[test]
fn authorization_header_for_agent_task_rejects_mismatched_runtime() {
let record = test_agent_identity_record("agent-123");
let target = AgentTaskAuthorizationTarget {
agent_runtime_id: "agent-456",
task_id: "task-123",
};
let error = authorization_header_for_agent_task(&record, target)
.expect_err("runtime mismatch should fail");
assert_eq!(
error.to_string(),
"agent task runtime agent-456 does not match stored agent identity agent-123"
);
}
fn test_agent_identity_record(agent_runtime_id: &str) -> AgentIdentityAuthRecord {
let signing_key = SigningKey::from_bytes(&[7u8; 32]);
let private_key = signing_key
.to_pkcs8_der()
.expect("encode test key material");
AgentIdentityAuthRecord {
workspace_id: "account-123".to_string(),
chatgpt_user_id: Some("user-123".to_string()),
agent_runtime_id: agent_runtime_id.to_string(),
agent_private_key: BASE64_STANDARD.encode(private_key.as_bytes()),
registered_at: "2026-03-23T12:00:00Z".to_string(),
background_task_id: None,
}
}
}
-137
View File
@@ -16,8 +16,6 @@ use serde_json::json;
use std::sync::Arc;
use tempfile::TempDir;
use tempfile::tempdir;
use tokio::time::Duration;
use tokio::time::timeout;
#[tokio::test]
async fn refresh_without_id_token() {
@@ -138,7 +136,6 @@ async fn pro_account_with_no_api_key_uses_chatgpt_auth() {
account_id: None,
}),
last_refresh: Some(last_refresh),
agent_identity: None,
},
auth_dot_json
);
@@ -176,7 +173,6 @@ fn logout_removes_auth_file() -> Result<(), std::io::Error> {
openai_api_key: Some("sk-test-key".to_string()),
tokens: None,
last_refresh: None,
agent_identity: None,
};
super::save_auth(dir.path(), &auth_dot_json, AuthCredentialsStoreMode::File)?;
let auth_file = get_auth_file(dir.path());
@@ -186,78 +182,6 @@ fn logout_removes_auth_file() -> Result<(), std::io::Error> {
Ok(())
}
#[test]
fn chatgpt_auth_persists_agent_identity_for_workspace() {
let codex_home = tempdir().unwrap();
write_auth_file(
AuthFileParams {
openai_api_key: None,
chatgpt_plan_type: Some("pro".to_string()),
chatgpt_account_id: Some("account-123".to_string()),
},
codex_home.path(),
)
.expect("failed to write auth file");
let auth = super::load_auth(
codex_home.path(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
)
.expect("load auth")
.expect("auth available");
let record = AgentIdentityAuthRecord {
workspace_id: "account-123".to_string(),
chatgpt_user_id: Some("user-123".to_string()),
agent_runtime_id: "agent_123".to_string(),
agent_private_key: "pkcs8-base64".to_string(),
registered_at: "2026-04-13T12:00:00Z".to_string(),
background_task_id: None,
};
auth.set_agent_identity(record.clone())
.expect("set agent identity");
assert_eq!(auth.get_agent_identity("account-123"), Some(record.clone()));
assert_eq!(auth.get_agent_identity("other-account"), None);
let storage = FileAuthStorage::new(codex_home.path().to_path_buf());
let persisted = storage
.load()
.expect("load auth")
.expect("auth should exist");
assert_eq!(persisted.agent_identity, Some(record));
assert!(auth.remove_agent_identity().expect("remove agent identity"));
assert_eq!(auth.get_agent_identity("account-123"), None);
}
#[test]
fn dummy_chatgpt_auth_does_not_create_cwd_auth_json_when_identity_is_set() {
let cwd_auth = std::env::current_dir()
.expect("current dir")
.join("auth.json");
let had_auth_json_before_test = cwd_auth.exists();
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let record = AgentIdentityAuthRecord {
workspace_id: "account_id".to_string(),
chatgpt_user_id: None,
agent_runtime_id: "agent_123".to_string(),
agent_private_key: "pkcs8-base64".to_string(),
registered_at: "2026-04-13T12:00:00Z".to_string(),
background_task_id: None,
};
auth.set_agent_identity(record.clone())
.expect("set agent identity");
assert_eq!(auth.get_agent_identity("account_id"), Some(record));
if !had_auth_json_before_test {
assert!(
!cwd_auth.exists(),
"dummy ChatGPT auth must not write auth.json in the test process cwd"
);
}
}
#[test]
fn unauthorized_recovery_reports_mode_and_step_names() {
let dir = tempdir().unwrap();
@@ -551,67 +475,6 @@ exit 1
}
}
#[tokio::test]
async fn auth_manager_notifies_when_auth_state_changes() {
let dir = tempdir().unwrap();
let manager = AuthManager::shared(
dir.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
);
let mut auth_state_rx = manager.subscribe_auth_state();
save_auth(
dir.path(),
&AuthDotJson {
auth_mode: Some(ApiAuthMode::ApiKey),
openai_api_key: Some("sk-test-key".to_string()),
tokens: None,
last_refresh: None,
agent_identity: None,
},
AuthCredentialsStoreMode::File,
)
.expect("save auth");
assert!(
manager.reload(),
"reload should report a changed auth state"
);
timeout(Duration::from_secs(1), auth_state_rx.changed())
.await
.expect("auth change notification should arrive")
.expect("auth state watch should remain open");
save_auth(
dir.path(),
&AuthDotJson {
auth_mode: Some(ApiAuthMode::ApiKey),
openai_api_key: Some("sk-updated-key".to_string()),
tokens: None,
last_refresh: None,
agent_identity: None,
},
AuthCredentialsStoreMode::File,
)
.expect("save updated auth");
assert!(
!manager.reload(),
"reload remains mode-stable even when the underlying credentials change"
);
timeout(Duration::from_secs(1), auth_state_rx.changed())
.await
.expect("auth reload notification should still arrive")
.expect("auth state watch should remain open");
manager.set_forced_chatgpt_workspace_id(Some("workspace-123".to_string()));
timeout(Duration::from_secs(1), auth_state_rx.changed())
.await
.expect("workspace change notification should arrive")
.expect("auth state watch should remain open");
}
struct AuthFileParams {
openai_api_key: Option<String>,
chatgpt_plan_type: Option<String>,
+9 -248
View File
@@ -1,4 +1,3 @@
use anyhow::Context;
use async_trait::async_trait;
use chrono::Utc;
use reqwest::StatusCode;
@@ -16,18 +15,14 @@ use std::sync::RwLock;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use tokio::sync::Semaphore;
use tokio::sync::watch;
use codex_app_server_protocol::AuthMode;
use codex_app_server_protocol::AuthMode as ApiAuthMode;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::ModelProviderAuthInfo;
use super::agent_assertion;
use super::agent_assertion::AgentTaskAuthorizationTarget;
use super::external_bearer::BearerTokenRefresher;
use super::revoke::revoke_auth_tokens;
pub use crate::auth::storage::AgentIdentityAuthRecord;
pub use crate::auth::storage::AuthDotJson;
use crate::auth::storage::AuthStorageBackend;
use crate::auth::storage::create_auth_storage;
@@ -43,12 +38,9 @@ use codex_protocol::auth::KnownPlan as InternalKnownPlan;
use codex_protocol::auth::PlanType as InternalPlanType;
use codex_protocol::auth::RefreshTokenFailedError;
use codex_protocol::auth::RefreshTokenFailedReason;
use codex_protocol::protocol::SessionSource;
use serde_json::Value;
use thiserror::Error;
use crate::agent_identity::BackgroundAgentTaskAuthMode;
use crate::agent_identity::BackgroundAgentTaskManager;
/// Authentication mechanism used by the current user.
#[derive(Debug, Clone)]
pub enum CodexAuth {
@@ -71,7 +63,6 @@ pub struct ChatgptAuth {
#[derive(Debug, Clone)]
pub struct ChatgptAuthTokens {
state: ChatgptAuthState,
storage: Arc<dyn AuthStorageBackend>,
}
#[derive(Debug, Clone)]
@@ -219,13 +210,14 @@ impl CodexAuth {
client,
};
let storage = create_auth_storage(codex_home.to_path_buf(), storage_mode);
match auth_mode {
ApiAuthMode::Chatgpt => Ok(Self::Chatgpt(ChatgptAuth { state, storage })),
ApiAuthMode::ChatgptAuthTokens => Ok(Self::ChatgptAuthTokens(ChatgptAuthTokens {
state,
storage,
})),
ApiAuthMode::Chatgpt => {
let storage = create_auth_storage(codex_home.to_path_buf(), storage_mode);
Ok(Self::Chatgpt(ChatgptAuth { state, storage }))
}
ApiAuthMode::ChatgptAuthTokens => {
Ok(Self::ChatgptAuthTokens(ChatgptAuthTokens { state }))
}
ApiAuthMode::ApiKey => unreachable!("api key mode is handled above"),
}
}
@@ -372,59 +364,6 @@ impl CodexAuth {
self.get_current_auth_json().and_then(|t| t.tokens)
}
pub fn get_agent_identity(&self, workspace_id: &str) -> Option<AgentIdentityAuthRecord> {
self.get_current_auth_json()
.and_then(|auth| auth.agent_identity)
.filter(|identity| identity.workspace_id == workspace_id)
}
pub fn set_agent_identity(&self, mut record: AgentIdentityAuthRecord) -> std::io::Result<()> {
let (state, storage) = match self {
Self::Chatgpt(auth) => (&auth.state, &auth.storage),
Self::ChatgptAuthTokens(auth) => (&auth.state, &auth.storage),
Self::ApiKey(_) => return Ok(()),
};
let mut guard = state
.auth_dot_json
.lock()
.map_err(|_| std::io::Error::other("failed to lock auth state"))?;
let mut auth = guard
.clone()
.ok_or_else(|| std::io::Error::other("auth data is not available"))?;
if record.background_task_id.is_none()
&& let Some(existing) = auth.agent_identity.as_ref()
&& existing.workspace_id == record.workspace_id
&& existing.agent_runtime_id == record.agent_runtime_id
{
record.background_task_id = existing.background_task_id.clone();
}
auth.agent_identity = Some(record);
storage.save(&auth)?;
*guard = Some(auth);
Ok(())
}
pub fn remove_agent_identity(&self) -> std::io::Result<bool> {
let (state, storage) = match self {
Self::Chatgpt(auth) => (&auth.state, &auth.storage),
Self::ChatgptAuthTokens(auth) => (&auth.state, &auth.storage),
Self::ApiKey(_) => return Ok(false),
};
let mut guard = state
.auth_dot_json
.lock()
.map_err(|_| std::io::Error::other("failed to lock auth state"))?;
let Some(mut auth) = guard.clone() else {
return Ok(false);
};
let removed = auth.agent_identity.take().is_some();
if removed {
storage.save(&auth)?;
*guard = Some(auth);
}
Ok(removed)
}
/// Consider this private to integration tests.
pub fn create_dummy_chatgpt_auth_for_testing() -> Self {
let auth_dot_json = AuthDotJson {
@@ -437,7 +376,6 @@ impl CodexAuth {
account_id: Some("account_id".to_string()),
}),
last_refresh: Some(Utc::now()),
agent_identity: None,
};
let client = create_client();
@@ -530,7 +468,6 @@ pub fn login_with_api_key(
openai_api_key: Some(api_key.to_string()),
tokens: None,
last_refresh: None,
agent_identity: None,
};
save_auth(codex_home, &auth_dot_json, auth_credentials_store_mode)
}
@@ -904,7 +841,6 @@ impl AuthDotJson {
openai_api_key: None,
tokens: Some(tokens),
last_refresh: Some(Utc::now()),
agent_identity: None,
})
}
@@ -1198,11 +1134,8 @@ pub struct AuthManager {
enable_codex_api_key_env: bool,
auth_credentials_store_mode: AuthCredentialsStoreMode,
forced_chatgpt_workspace_id: RwLock<Option<String>>,
chatgpt_base_url: RwLock<Option<String>>,
background_agent_task_auth_mode: RwLock<BackgroundAgentTaskAuthMode>,
refresh_lock: Semaphore,
external_auth: RwLock<Option<Arc<dyn ExternalAuth>>>,
auth_state_tx: watch::Sender<()>,
}
/// Configuration view required to construct a shared [`AuthManager`].
@@ -1220,16 +1153,6 @@ pub trait AuthManagerConfig {
/// Returns the workspace ID that ChatGPT auth should be restricted to, if any.
fn forced_chatgpt_workspace_id(&self) -> Option<String>;
/// Returns the ChatGPT backend base URL used for first-party backend authorization.
fn chatgpt_base_url(&self) -> Option<String> {
None
}
/// Returns whether default ChatGPT backend authorization may use background AgentAssertion.
fn background_agent_task_auth_mode(&self) -> BackgroundAgentTaskAuthMode {
BackgroundAgentTaskAuthMode::Disabled
}
}
impl Debug for AuthManager {
@@ -1246,11 +1169,6 @@ impl Debug for AuthManager {
"forced_chatgpt_workspace_id",
&self.forced_chatgpt_workspace_id,
)
.field("chatgpt_base_url", &self.chatgpt_base_url)
.field(
"background_agent_task_auth_mode",
&self.background_agent_task_auth_mode,
)
.field("has_external_auth", &self.has_external_auth())
.finish_non_exhaustive()
}
@@ -1266,7 +1184,6 @@ impl AuthManager {
enable_codex_api_key_env: bool,
auth_credentials_store_mode: AuthCredentialsStoreMode,
) -> Self {
let (auth_state_tx, _) = watch::channel(());
let managed_auth = load_auth(
&codex_home,
enable_codex_api_key_env,
@@ -1283,17 +1200,13 @@ impl AuthManager {
enable_codex_api_key_env,
auth_credentials_store_mode,
forced_chatgpt_workspace_id: RwLock::new(None),
chatgpt_base_url: RwLock::new(None),
background_agent_task_auth_mode: RwLock::new(BackgroundAgentTaskAuthMode::Disabled),
refresh_lock: Semaphore::new(/*permits*/ 1),
external_auth: RwLock::new(None),
auth_state_tx,
}
}
/// Create an AuthManager with a specific CodexAuth, for testing only.
pub fn from_auth_for_testing(auth: CodexAuth) -> Arc<Self> {
let (auth_state_tx, _) = watch::channel(());
let cached = CachedAuth {
auth: Some(auth),
permanent_refresh_failure: None,
@@ -1305,17 +1218,13 @@ impl AuthManager {
enable_codex_api_key_env: false,
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
forced_chatgpt_workspace_id: RwLock::new(None),
chatgpt_base_url: RwLock::new(None),
background_agent_task_auth_mode: RwLock::new(BackgroundAgentTaskAuthMode::Disabled),
refresh_lock: Semaphore::new(/*permits*/ 1),
external_auth: RwLock::new(None),
auth_state_tx,
})
}
/// Create an AuthManager with a specific CodexAuth and codex home, for testing only.
pub fn from_auth_for_testing_with_home(auth: CodexAuth, codex_home: PathBuf) -> Arc<Self> {
let (auth_state_tx, _) = watch::channel(());
let cached = CachedAuth {
auth: Some(auth),
permanent_refresh_failure: None,
@@ -1326,16 +1235,12 @@ impl AuthManager {
enable_codex_api_key_env: false,
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
forced_chatgpt_workspace_id: RwLock::new(None),
chatgpt_base_url: RwLock::new(None),
background_agent_task_auth_mode: RwLock::new(BackgroundAgentTaskAuthMode::Disabled),
refresh_lock: Semaphore::new(/*permits*/ 1),
external_auth: RwLock::new(None),
auth_state_tx,
})
}
pub fn external_bearer_only(config: ModelProviderAuthInfo) -> Arc<Self> {
let (auth_state_tx, _) = watch::channel(());
Arc::new(Self {
codex_home: PathBuf::from("non-existent"),
inner: RwLock::new(CachedAuth {
@@ -1345,13 +1250,10 @@ impl AuthManager {
enable_codex_api_key_env: false,
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
forced_chatgpt_workspace_id: RwLock::new(None),
chatgpt_base_url: RwLock::new(None),
background_agent_task_auth_mode: RwLock::new(BackgroundAgentTaskAuthMode::Disabled),
refresh_lock: Semaphore::new(/*permits*/ 1),
external_auth: RwLock::new(Some(
Arc::new(BearerTokenRefresher::new(config)) as Arc<dyn ExternalAuth>
)),
auth_state_tx,
})
}
@@ -1491,7 +1393,6 @@ impl AuthManager {
}
tracing::info!("Reloaded auth, changed: {changed}");
guard.auth = new_auth;
self.auth_state_tx.send_replace(());
changed
} else {
false
@@ -1501,23 +1402,18 @@ impl AuthManager {
pub fn set_external_auth(&self, external_auth: Arc<dyn ExternalAuth>) {
if let Ok(mut guard) = self.external_auth.write() {
*guard = Some(external_auth);
self.auth_state_tx.send_replace(());
}
}
pub fn clear_external_auth(&self) {
if let Ok(mut guard) = self.external_auth.write() {
*guard = None;
self.auth_state_tx.send_replace(());
}
}
pub fn set_forced_chatgpt_workspace_id(&self, workspace_id: Option<String>) {
if let Ok(mut guard) = self.forced_chatgpt_workspace_id.write()
&& *guard != workspace_id
{
if let Ok(mut guard) = self.forced_chatgpt_workspace_id.write() {
*guard = workspace_id;
self.auth_state_tx.send_replace(());
}
}
@@ -1528,131 +1424,6 @@ impl AuthManager {
.and_then(|guard| guard.clone())
}
pub fn chatgpt_agent_task_authorization_header_for_auth(
&self,
auth: &CodexAuth,
target: AgentTaskAuthorizationTarget<'_>,
) -> anyhow::Result<Option<String>> {
let Some(record) = self.agent_identity_for_chatgpt_auth(auth)? else {
return Ok(None);
};
agent_assertion::authorization_header_for_agent_task(&record, target).map(Some)
}
fn agent_identity_for_chatgpt_auth(
&self,
auth: &CodexAuth,
) -> anyhow::Result<Option<AgentIdentityAuthRecord>> {
if !auth.is_chatgpt_auth() {
return Ok(None);
}
let token_data = auth
.get_token_data()
.context("ChatGPT token data is not available")?;
let workspace_id = self
.forced_chatgpt_workspace_id()
.filter(|value| !value.is_empty())
.or(token_data.account_id.filter(|value| !value.is_empty()));
let Some(workspace_id) = workspace_id else {
return Ok(None);
};
let Some(record) = auth.get_agent_identity(&workspace_id) else {
anyhow::bail!("agent identity is not available for workspace {workspace_id}");
};
Ok(Some(record))
}
pub fn set_chatgpt_backend_auth_config(
&self,
chatgpt_base_url: Option<String>,
background_agent_task_auth_mode: BackgroundAgentTaskAuthMode,
) {
let mut changed = false;
if let Ok(mut guard) = self.chatgpt_base_url.write()
&& *guard != chatgpt_base_url
{
*guard = chatgpt_base_url;
changed = true;
}
if let Ok(mut guard) = self.background_agent_task_auth_mode.write()
&& *guard != background_agent_task_auth_mode
{
*guard = background_agent_task_auth_mode;
changed = true;
}
if changed {
self.auth_state_tx.send_replace(());
}
}
pub fn set_chatgpt_backend_base_url(&self, chatgpt_base_url: Option<String>) {
let (_, auth_mode) = self.chatgpt_backend_auth_config();
self.set_chatgpt_backend_auth_config(chatgpt_base_url, auth_mode);
}
fn chatgpt_backend_auth_config(&self) -> (Option<String>, BackgroundAgentTaskAuthMode) {
let chatgpt_base_url = self
.chatgpt_base_url
.read()
.ok()
.and_then(|guard| guard.clone());
let auth_mode = self
.background_agent_task_auth_mode
.read()
.ok()
.map(|guard| *guard)
.unwrap_or_default();
(chatgpt_base_url, auth_mode)
}
/// Returns the default authorization header for ChatGPT backend requests.
///
/// This uses background AgentAssertion when configured and available, otherwise it falls back
/// to the ChatGPT bearer token. Low-level bootstrap calls that must never use AgentAssertion
/// should use [`Self::chatgpt_bearer_authorization_header_for_auth`] instead.
pub async fn chatgpt_authorization_header(self: &Arc<Self>) -> Option<String> {
let auth = self.auth().await?;
self.chatgpt_authorization_header_for_auth(&auth).await
}
pub async fn chatgpt_authorization_header_for_auth(
self: &Arc<Self>,
auth: &CodexAuth,
) -> Option<String> {
if !auth.is_chatgpt_auth() {
return None;
}
let (chatgpt_base_url, auth_mode) = self.chatgpt_backend_auth_config();
let Some(chatgpt_base_url) = chatgpt_base_url else {
return Self::chatgpt_bearer_authorization_header_for_auth(auth);
};
BackgroundAgentTaskManager::new_with_auth_mode(
Arc::clone(self),
chatgpt_base_url,
SessionSource::Cli,
auth_mode,
)
.authorization_header_value_or_bearer(auth)
.await
}
pub fn chatgpt_bearer_token_for_auth(auth: &CodexAuth) -> Option<String> {
auth.get_token().ok().filter(|token| !token.is_empty())
}
pub fn chatgpt_bearer_authorization_header_for_auth(auth: &CodexAuth) -> Option<String> {
Self::chatgpt_bearer_token_for_auth(auth).map(|token| format!("Bearer {token}"))
}
pub fn subscribe_auth_state(&self) -> watch::Receiver<()> {
self.auth_state_tx.subscribe()
}
pub fn has_external_auth(&self) -> bool {
self.external_auth().is_some()
}
@@ -1691,10 +1462,6 @@ impl AuthManager {
config.cli_auth_credentials_store_mode(),
);
auth_manager.set_forced_chatgpt_workspace_id(config.forced_chatgpt_workspace_id());
auth_manager.set_chatgpt_backend_auth_config(
config.chatgpt_base_url(),
config.background_agent_task_auth_mode(),
);
auth_manager
}
@@ -1923,14 +1690,8 @@ impl AuthManager {
),
)));
}
let mut auth_dot_json =
let auth_dot_json =
AuthDotJson::from_external_tokens(&refreshed).map_err(RefreshTokenError::Transient)?;
if let Some(previous_auth) = self
.auth_cached()
.and_then(|auth| auth.get_current_auth_json())
{
auth_dot_json.agent_identity = previous_auth.agent_identity;
}
save_auth(
&self.codex_home,
&auth_dot_json,
-2
View File
@@ -1,4 +1,3 @@
mod agent_assertion;
pub mod default_client;
pub mod error;
mod storage;
@@ -8,7 +7,6 @@ mod external_bearer;
mod manager;
mod revoke;
pub use agent_assertion::AgentTaskAuthorizationTarget;
pub use error::RefreshTokenFailedError;
pub use error::RefreshTokenFailedReason;
pub use manager::*;
-15
View File
@@ -39,21 +39,6 @@ pub struct AuthDotJson {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_refresh: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_identity: Option<AgentIdentityAuthRecord>,
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
pub struct AgentIdentityAuthRecord {
pub workspace_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chatgpt_user_id: Option<String>,
pub agent_runtime_id: String,
pub agent_private_key: String,
pub registered_at: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub background_task_id: Option<String>,
}
pub(super) fn get_auth_file(codex_home: &Path) -> PathBuf {
-32
View File
@@ -18,7 +18,6 @@ async fn file_storage_load_returns_auth_dot_json() -> anyhow::Result<()> {
openai_api_key: Some("test-key".to_string()),
tokens: None,
last_refresh: Some(Utc::now()),
agent_identity: None,
};
storage
@@ -39,7 +38,6 @@ async fn file_storage_save_persists_auth_dot_json() -> anyhow::Result<()> {
openai_api_key: Some("test-key".to_string()),
tokens: None,
last_refresh: Some(Utc::now()),
agent_identity: None,
};
let file = get_auth_file(codex_home.path());
@@ -54,31 +52,6 @@ async fn file_storage_save_persists_auth_dot_json() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test]
async fn file_storage_persists_agent_identity() -> anyhow::Result<()> {
let codex_home = tempdir()?;
let storage = FileAuthStorage::new(codex_home.path().to_path_buf());
let auth_dot_json = AuthDotJson {
auth_mode: Some(AuthMode::Chatgpt),
openai_api_key: None,
tokens: None,
last_refresh: Some(Utc::now()),
agent_identity: Some(AgentIdentityAuthRecord {
workspace_id: "account-123".to_string(),
chatgpt_user_id: Some("user-123".to_string()),
agent_runtime_id: "agent_123".to_string(),
agent_private_key: "pkcs8-base64".to_string(),
registered_at: "2026-04-13T12:00:00Z".to_string(),
background_task_id: None,
}),
};
storage.save(&auth_dot_json)?;
assert_eq!(storage.load()?, Some(auth_dot_json));
Ok(())
}
#[test]
fn file_storage_delete_removes_auth_file() -> anyhow::Result<()> {
let dir = tempdir()?;
@@ -87,7 +60,6 @@ fn file_storage_delete_removes_auth_file() -> anyhow::Result<()> {
openai_api_key: Some("sk-test-key".to_string()),
tokens: None,
last_refresh: None,
agent_identity: None,
};
let storage = create_auth_storage(dir.path().to_path_buf(), AuthCredentialsStoreMode::File);
storage.save(&auth_dot_json)?;
@@ -111,7 +83,6 @@ fn ephemeral_storage_save_load_delete_is_in_memory_only() -> anyhow::Result<()>
openai_api_key: Some("sk-ephemeral".to_string()),
tokens: None,
last_refresh: Some(Utc::now()),
agent_identity: None,
};
storage.save(&auth_dot_json)?;
@@ -210,7 +181,6 @@ fn auth_with_prefix(prefix: &str) -> AuthDotJson {
account_id: Some(format!("{prefix}-account-id")),
}),
last_refresh: None,
agent_identity: None,
}
}
@@ -227,7 +197,6 @@ fn keyring_auth_storage_load_returns_deserialized_auth() -> anyhow::Result<()> {
openai_api_key: Some("sk-test".to_string()),
tokens: None,
last_refresh: None,
agent_identity: None,
};
seed_keyring_with_auth(
&mock_keyring,
@@ -270,7 +239,6 @@ fn keyring_auth_storage_save_persists_and_removes_fallback_file() -> anyhow::Res
account_id: Some("account".to_string()),
}),
last_refresh: Some(Utc::now()),
agent_identity: None,
};
storage.save(&auth)?;
-5
View File
@@ -1,4 +1,3 @@
pub mod agent_identity;
pub mod auth;
pub mod auth_env_telemetry;
pub mod token_data;
@@ -18,10 +17,6 @@ pub use server::ServerOptions;
pub use server::ShutdownHandle;
pub use server::run_login_server;
pub use agent_identity::BackgroundAgentTaskAuthMode;
pub use agent_identity::cached_background_agent_task_authorization_header_value;
pub use auth::AgentIdentityAuthRecord;
pub use auth::AgentTaskAuthorizationTarget;
pub use auth::AuthConfig;
pub use auth::AuthDotJson;
pub use auth::AuthManager;
-1
View File
@@ -781,7 +781,6 @@ pub(crate) async fn persist_tokens_async(
openai_api_key: api_key,
tokens: Some(tokens),
last_refresh: Some(Utc::now()),
agent_identity: None,
};
save_auth(&codex_home, &auth, auth_credentials_store_mode)
})