mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
feat: opt ChatGPT auth into agent identity (#19049)
## Stack This is PR 2 of the simplified HAI single-run-task stack: - [#19047](https://github.com/openai/codex/pull/19047) Agent Identity assertion and task-registration primitives, including the shared run-task helper used by existing Agent Identity JWT auth. - [#19049](https://github.com/openai/codex/pull/19049) Disabled-by-default ChatGPT auth opt-in that provisions/reuses persisted Agent Identity runtime auth and its single run task. - [#19051](https://github.com/openai/codex/pull/19051) Run-scoped provider auth that uses one backend-owned task id for first-party inference and compaction requests. [#19054](https://github.com/openai/codex/pull/19054) collapsed out of the active stack because the simplified design no longer needs a separate background/control-plane task helper. ## Summary This PR adds the disabled-by-default path for normal ChatGPT-login Codex sessions to obtain Agent Identity runtime auth through the Codex backend. Existing Agent Identity JWT startup mode remains a separate path and does not require the feature flag. What changed: - adds the experimental `use_agent_identity` feature flag and config schema entry - adds an explicit `AgentIdentityAuthPolicy` so call sites choose `JwtOnly` or `ChatGptAuth` instead of passing a bare boolean - stores standalone Agent Identity JWT credentials separately from backend-registered Agent Identity records - persists the registered Agent Identity record, private key, and single run task id in `auth.json` so process restarts reuse the same identity - derives the agent/task registration base URL from ChatGPT/Codex auth config while keeping JWT JWKS lookup separate - provisions and caches ChatGPT-derived Agent Identity runtime auth when `use_agent_identity` is enabled - reuses the shared run-task registration helper from PR1 rather than adding a second task-registration path This PR intentionally does not switch model inference over to `AgentAssertion` auth. The provider-auth integration lands in the next PR. ## Testing - `just test -p codex-login`
This commit is contained in:
1 parent
765309d5a6
commit
ec848dde0e
24 files changed
+1359
-172
No files matched your search
@@ -20,8 +20,6 @@ use tokio::sync::watch;
|
||||
use tracing::instrument;
|
||||
|
||||
use codex_agent_identity::ChatGptEnvironment;
|
||||
use codex_agent_identity::decode_agent_identity_jwt;
|
||||
use codex_agent_identity::fetch_agent_identity_jwks;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_app_server_protocol::AuthMode as ApiAuthMode;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
@@ -29,18 +27,27 @@ use codex_protocol::config_types::ModelProviderAuthInfo;
|
||||
|
||||
use super::access_token::CodexAccessToken;
|
||||
use super::access_token::classify_codex_access_token;
|
||||
use super::agent_identity::ManagedChatGptAgentIdentityBinding;
|
||||
use super::agent_identity::agent_identity_authapi_base_url;
|
||||
use super::agent_identity::classify_bootstrap_error;
|
||||
use super::agent_identity::record_matches_managed_chatgpt_binding;
|
||||
use super::agent_identity::record_needs_task_registration;
|
||||
use super::agent_identity::register_managed_chatgpt_agent_identity;
|
||||
use super::agent_identity::require_agent_identity_authapi_base_url;
|
||||
use super::agent_identity::verified_record_from_jwt;
|
||||
use super::external_bearer::BearerTokenRefresher;
|
||||
use super::revoke::revoke_auth_tokens;
|
||||
pub use crate::auth::agent_identity::AgentIdentityAuth;
|
||||
pub use crate::auth::agent_identity::AgentIdentityAuthError;
|
||||
pub use crate::auth::bedrock_api_key::BedrockApiKeyAuth;
|
||||
pub use crate::auth::personal_access_token::PersonalAccessTokenAuth;
|
||||
pub use crate::auth::storage::AgentIdentityAuthRecord;
|
||||
pub use crate::auth::storage::AgentIdentityStorage;
|
||||
pub use crate::auth::storage::AuthDotJson;
|
||||
pub use crate::auth::storage::AuthKeyringBackendKind;
|
||||
use crate::auth::storage::AuthStorageBackend;
|
||||
use crate::auth::storage::create_auth_storage;
|
||||
use crate::auth::util::try_parse_error_message;
|
||||
use crate::default_client::build_reqwest_client;
|
||||
use crate::default_client::create_client;
|
||||
use crate::token_data::TokenData;
|
||||
use crate::token_data::parse_chatgpt_jwt_claims;
|
||||
@@ -51,6 +58,7 @@ use codex_protocol::account::PlanType as AccountPlanType;
|
||||
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;
|
||||
|
||||
@@ -65,6 +73,15 @@ pub enum CodexAuth {
|
||||
BedrockApiKey(BedrockApiKeyAuth),
|
||||
}
|
||||
|
||||
/// Policy for resolving Agent Identity auth from a broader Codex auth snapshot.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AgentIdentityAuthPolicy {
|
||||
/// Use Agent Identity auth only when the current auth is already Agent Identity.
|
||||
JwtOnly,
|
||||
/// Allow managed ChatGPT auth to register or reuse Agent Identity auth.
|
||||
ChatGptAuth,
|
||||
}
|
||||
|
||||
impl PartialEq for CodexAuth {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
@@ -222,7 +239,6 @@ impl CodexAuth {
|
||||
agent_identity_authapi_base_url: Option<&str>,
|
||||
) -> std::io::Result<Self> {
|
||||
let auth_mode = auth_dot_json.resolved_mode();
|
||||
let client = create_client();
|
||||
if auth_mode == ApiAuthMode::ApiKey {
|
||||
let Some(api_key) = auth_dot_json.openai_api_key.as_deref() else {
|
||||
return Err(std::io::Error::other("API key auth is missing a key."));
|
||||
@@ -230,17 +246,34 @@ impl CodexAuth {
|
||||
return Ok(Self::from_api_key(api_key));
|
||||
}
|
||||
if auth_mode == ApiAuthMode::AgentIdentity {
|
||||
let Some(agent_identity) = auth_dot_json.agent_identity else {
|
||||
let Some(agent_identity) = auth_dot_json.agent_identity.clone() else {
|
||||
return Err(std::io::Error::other(
|
||||
"agent identity auth is missing an agent identity token.",
|
||||
"agent identity auth is missing agent identity auth material.",
|
||||
));
|
||||
};
|
||||
return Self::from_agent_identity_jwt(
|
||||
&agent_identity,
|
||||
chatgpt_base_url,
|
||||
agent_identity_authapi_base_url,
|
||||
)
|
||||
.await;
|
||||
let base_url = chatgpt_base_url
|
||||
.unwrap_or(ChatGptEnvironment::default().chatgpt_base_url())
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
let agent_identity_authapi_base_url =
|
||||
require_agent_identity_authapi_base_url(agent_identity_authapi_base_url)?;
|
||||
match agent_identity {
|
||||
AgentIdentityStorage::Jwt(jwt) => {
|
||||
let auth = AgentIdentityAuth::from_jwt(
|
||||
&jwt,
|
||||
&base_url,
|
||||
agent_identity_authapi_base_url,
|
||||
)
|
||||
.await?;
|
||||
return Ok(Self::AgentIdentity(auth));
|
||||
}
|
||||
AgentIdentityStorage::Record(record) => {
|
||||
let auth =
|
||||
AgentIdentityAuth::from_record(record, agent_identity_authapi_base_url)
|
||||
.await?;
|
||||
return Ok(Self::AgentIdentity(auth));
|
||||
}
|
||||
}
|
||||
}
|
||||
if auth_mode == ApiAuthMode::PersonalAccessToken {
|
||||
let Some(personal_access_token) = auth_dot_json.personal_access_token.as_deref() else {
|
||||
@@ -262,7 +295,7 @@ impl CodexAuth {
|
||||
let storage_mode = auth_dot_json.storage_mode(auth_credentials_store_mode);
|
||||
let state = ChatgptAuthState {
|
||||
auth_dot_json: Arc::new(Mutex::new(Some(auth_dot_json))),
|
||||
client,
|
||||
client: create_client(),
|
||||
};
|
||||
|
||||
match auth_mode {
|
||||
@@ -292,6 +325,8 @@ impl CodexAuth {
|
||||
chatgpt_base_url: Option<&str>,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> std::io::Result<Option<Self>> {
|
||||
let agent_identity_authapi_base_url =
|
||||
agent_identity_authapi_base_url(chatgpt_base_url).ok();
|
||||
load_auth(
|
||||
codex_home,
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
@@ -299,7 +334,7 @@ impl CodexAuth {
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
chatgpt_base_url,
|
||||
keyring_backend_kind,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
agent_identity_authapi_base_url.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -307,19 +342,28 @@ impl CodexAuth {
|
||||
pub async fn from_agent_identity_jwt(
|
||||
jwt: &str,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
agent_identity_authapi_base_url_override: Option<&str>,
|
||||
) -> std::io::Result<Self> {
|
||||
let agent_identity_authapi_base_url = agent_identity_authapi_base_url(chatgpt_base_url)?;
|
||||
Self::from_agent_identity_jwt_with_authapi_base_url(
|
||||
jwt,
|
||||
chatgpt_base_url,
|
||||
&agent_identity_authapi_base_url,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn from_agent_identity_jwt_with_authapi_base_url(
|
||||
jwt: &str,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
agent_identity_authapi_base_url: &str,
|
||||
) -> std::io::Result<Self> {
|
||||
let base_url = chatgpt_base_url
|
||||
.unwrap_or(ChatGptEnvironment::default().chatgpt_base_url())
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
let resolved_authapi_base_url = resolve_agent_identity_authapi_base_url(
|
||||
Some(&base_url),
|
||||
agent_identity_authapi_base_url_override,
|
||||
)?;
|
||||
let record = verified_agent_identity_record(jwt, &base_url).await?;
|
||||
let auth = AgentIdentityAuth::load(record, &resolved_authapi_base_url).await?;
|
||||
Ok(Self::AgentIdentity(auth))
|
||||
Ok(Self::AgentIdentity(
|
||||
AgentIdentityAuth::from_jwt(jwt, &base_url, agent_identity_authapi_base_url).await?,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn from_personal_access_token(access_token: &str) -> std::io::Result<Self> {
|
||||
@@ -499,6 +543,89 @@ impl CodexAuth {
|
||||
self.get_current_auth_json().and_then(|t| t.tokens)
|
||||
}
|
||||
|
||||
fn stored_managed_chatgpt_agent_identity_record(
|
||||
&self,
|
||||
account_id: &str,
|
||||
) -> Option<AgentIdentityAuthRecord> {
|
||||
self.get_current_auth_json()
|
||||
.and_then(|auth| auth.agent_identity)
|
||||
.and_then(|identity| identity.as_record().cloned())
|
||||
.filter(|identity| identity.account_id == account_id)
|
||||
}
|
||||
|
||||
fn persist_managed_chatgpt_agent_identity_record(
|
||||
&self,
|
||||
record: AgentIdentityAuthRecord,
|
||||
) -> std::io::Result<()> {
|
||||
if let Self::Chatgpt(chatgpt_auth) = self {
|
||||
chatgpt_auth.persist_agent_identity_record(record)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn agent_identity_auth(
|
||||
&self,
|
||||
policy: AgentIdentityAuthPolicy,
|
||||
agent_identity_authapi_base_url: Option<&str>,
|
||||
forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
session_source: SessionSource,
|
||||
) -> std::io::Result<Option<AgentIdentityAuth>> {
|
||||
match self {
|
||||
Self::AgentIdentity(auth) => Ok(Some(auth.clone())),
|
||||
Self::ApiKey(_)
|
||||
| Self::ChatgptAuthTokens(_)
|
||||
| Self::PersonalAccessToken(_)
|
||||
| Self::BedrockApiKey(_) => Ok(None),
|
||||
Self::Chatgpt(_) => {
|
||||
if policy == AgentIdentityAuthPolicy::JwtOnly {
|
||||
return Ok(None);
|
||||
}
|
||||
self.ensure_managed_chatgpt_agent_identity(
|
||||
require_agent_identity_authapi_base_url(agent_identity_authapi_base_url)?,
|
||||
forced_chatgpt_workspace_id,
|
||||
session_source,
|
||||
)
|
||||
.await
|
||||
.map(Some)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_managed_chatgpt_agent_identity(
|
||||
&self,
|
||||
agent_identity_authapi_base_url: &str,
|
||||
forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
session_source: SessionSource,
|
||||
) -> std::io::Result<AgentIdentityAuth> {
|
||||
let binding =
|
||||
ManagedChatGptAgentIdentityBinding::from_auth(self, forced_chatgpt_workspace_id)
|
||||
.ok_or_else(|| std::io::Error::other("ChatGPT auth is unavailable"))?;
|
||||
|
||||
// JWT auth is loaded as CodexAuth::AgentIdentity; this path only reuses
|
||||
// records created by the managed ChatGPT Agent Identity bootstrap.
|
||||
if let Some(record) = self.stored_managed_chatgpt_agent_identity_record(&binding.account_id)
|
||||
&& record_matches_managed_chatgpt_binding(&record, &binding)
|
||||
{
|
||||
let should_persist = record_needs_task_registration(&record);
|
||||
let auth = AgentIdentityAuth::from_record(record, agent_identity_authapi_base_url)
|
||||
.await
|
||||
.map_err(|err| classify_bootstrap_error("agent task registration", err))?;
|
||||
if should_persist {
|
||||
self.persist_managed_chatgpt_agent_identity_record(auth.record().clone())?;
|
||||
}
|
||||
return Ok(auth);
|
||||
}
|
||||
|
||||
let auth = register_managed_chatgpt_agent_identity(
|
||||
binding,
|
||||
agent_identity_authapi_base_url,
|
||||
session_source,
|
||||
)
|
||||
.await?;
|
||||
self.persist_managed_chatgpt_agent_identity_record(auth.record().clone())?;
|
||||
Ok(auth)
|
||||
}
|
||||
|
||||
/// Consider this private to integration tests.
|
||||
pub fn create_dummy_chatgpt_auth_for_testing() -> Self {
|
||||
let auth_dot_json = AuthDotJson {
|
||||
@@ -516,10 +643,9 @@ impl CodexAuth {
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
|
||||
let client = create_client();
|
||||
let state = ChatgptAuthState {
|
||||
auth_dot_json: Arc::new(Mutex::new(Some(auth_dot_json))),
|
||||
client,
|
||||
client: create_client(),
|
||||
};
|
||||
let dummy_auth_id = NEXT_DUMMY_AUTH_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let storage = create_auth_storage(
|
||||
@@ -537,6 +663,43 @@ impl CodexAuth {
|
||||
}
|
||||
}
|
||||
|
||||
impl ManagedChatGptAgentIdentityBinding {
|
||||
fn from_auth(auth: &CodexAuth, forced_workspace_id: Option<Vec<String>>) -> Option<Self> {
|
||||
if !auth.is_chatgpt_auth() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let token_data = auth.get_token_data().ok()?;
|
||||
let forced_workspace_id =
|
||||
forced_workspace_id
|
||||
.as_deref()
|
||||
.and_then(|workspace_ids| match workspace_ids {
|
||||
[workspace_id] if !workspace_id.is_empty() => Some(workspace_id.clone()),
|
||||
_ => None,
|
||||
});
|
||||
let account_id = forced_workspace_id
|
||||
.or(token_data
|
||||
.account_id
|
||||
.clone()
|
||||
.filter(|value| !value.is_empty()))
|
||||
.or(token_data.id_token.chatgpt_account_id.clone())?;
|
||||
let chatgpt_user_id = token_data
|
||||
.id_token
|
||||
.chatgpt_user_id
|
||||
.clone()
|
||||
.filter(|value| !value.is_empty())?;
|
||||
|
||||
Some(Self {
|
||||
account_id,
|
||||
chatgpt_user_id,
|
||||
email: token_data.id_token.email.clone().unwrap_or_default(),
|
||||
plan_type: auth.account_plan_type().unwrap_or(AccountPlanType::Unknown),
|
||||
chatgpt_account_is_fedramp: auth.is_fedramp_account(),
|
||||
access_token: token_data.access_token,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatgptAuth {
|
||||
fn current_auth_json(&self) -> Option<AuthDotJson> {
|
||||
#[expect(clippy::unwrap_used)]
|
||||
@@ -554,6 +717,31 @@ impl ChatgptAuth {
|
||||
fn client(&self) -> &CodexHttpClient {
|
||||
&self.state.client
|
||||
}
|
||||
|
||||
fn persist_agent_identity_record(
|
||||
&self,
|
||||
record: AgentIdentityAuthRecord,
|
||||
) -> std::io::Result<()> {
|
||||
persist_agent_identity_record(&self.state.auth_dot_json, &self.storage, record)
|
||||
}
|
||||
}
|
||||
|
||||
fn persist_agent_identity_record(
|
||||
auth_dot_json: &Arc<Mutex<Option<AuthDotJson>>>,
|
||||
storage: &Arc<dyn AuthStorageBackend>,
|
||||
record: AgentIdentityAuthRecord,
|
||||
) -> std::io::Result<()> {
|
||||
let mut guard = auth_dot_json
|
||||
.lock()
|
||||
.map_err(|_| std::io::Error::other("failed to lock auth state"))?;
|
||||
let mut auth = storage
|
||||
.load()?
|
||||
.or_else(|| guard.clone())
|
||||
.ok_or_else(|| std::io::Error::other("auth data is not available"))?;
|
||||
auth.agent_identity = Some(AgentIdentityStorage::Record(record));
|
||||
storage.save(&auth)?;
|
||||
*guard = Some(auth);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub const OPENAI_API_KEY_ENV_VAR: &str = "OPENAI_API_KEY";
|
||||
@@ -582,34 +770,6 @@ fn read_non_empty_env_var(key: &str) -> Option<String> {
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
async fn verified_agent_identity_record(
|
||||
jwt: &str,
|
||||
chatgpt_base_url: &str,
|
||||
) -> std::io::Result<AgentIdentityAuthRecord> {
|
||||
AgentIdentityAuthRecord::from_agent_identity_jwt(jwt)?;
|
||||
let jwks = fetch_agent_identity_jwks(&build_reqwest_client(), chatgpt_base_url)
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
let claims = decode_agent_identity_jwt(jwt, Some(&jwks)).map_err(std::io::Error::other)?;
|
||||
Ok(claims.into())
|
||||
}
|
||||
|
||||
fn resolve_agent_identity_authapi_base_url(
|
||||
chatgpt_base_url: Option<&str>,
|
||||
agent_identity_authapi_base_url_override: Option<&str>,
|
||||
) -> std::io::Result<String> {
|
||||
if let Some(base_url) = agent_identity_authapi_base_url_override {
|
||||
return Ok(base_url.trim_end_matches('/').to_string());
|
||||
}
|
||||
|
||||
let environment = match chatgpt_base_url {
|
||||
Some(chatgpt_base_url) => ChatGptEnvironment::from_chatgpt_base_url(chatgpt_base_url)
|
||||
.map_err(std::io::Error::other)?,
|
||||
None => ChatGptEnvironment::default(),
|
||||
};
|
||||
Ok(environment.agent_identity_authapi_base_url().to_string())
|
||||
}
|
||||
|
||||
/// Delete the auth.json file inside `codex_home` if it exists. Returns `Ok(true)`
|
||||
/// if a file was removed, `Ok(false)` if no auth file was present.
|
||||
pub fn logout(
|
||||
@@ -705,13 +865,13 @@ pub async fn login_with_access_token(
|
||||
.unwrap_or(ChatGptEnvironment::default().chatgpt_base_url())
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
verified_agent_identity_record(jwt, &base_url).await?;
|
||||
verified_record_from_jwt(jwt, &base_url).await?;
|
||||
AuthDotJson {
|
||||
auth_mode: Some(ApiAuthMode::AgentIdentity),
|
||||
openai_api_key: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: Some(jwt.to_string()),
|
||||
agent_identity: Some(AgentIdentityStorage::Jwt(jwt.to_string())),
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
}
|
||||
@@ -793,11 +953,23 @@ pub struct AuthConfig {
|
||||
pub keyring_backend_kind: AuthKeyringBackendKind,
|
||||
pub forced_login_method: Option<ForcedLoginMethod>,
|
||||
pub chatgpt_base_url: Option<String>,
|
||||
pub agent_identity_authapi_base_url: Option<String>,
|
||||
pub forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<()> {
|
||||
let agent_identity_authapi_base_url =
|
||||
agent_identity_authapi_base_url(config.chatgpt_base_url.as_deref()).ok();
|
||||
enforce_login_restrictions_with_agent_identity_authapi_base_url(
|
||||
config,
|
||||
agent_identity_authapi_base_url.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn enforce_login_restrictions_with_agent_identity_authapi_base_url(
|
||||
config: &AuthConfig,
|
||||
agent_identity_authapi_base_url: Option<&str>,
|
||||
) -> std::io::Result<()> {
|
||||
let Some(auth) = load_auth(
|
||||
&config.codex_home,
|
||||
/*enable_codex_api_key_env*/ true,
|
||||
@@ -805,7 +977,7 @@ pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
config.chatgpt_base_url.as_deref(),
|
||||
config.keyring_backend_kind,
|
||||
config.agent_identity_authapi_base_url.as_deref(),
|
||||
agent_identity_authapi_base_url,
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
@@ -985,11 +1157,13 @@ async fn load_auth(
|
||||
ensure_personal_access_token_workspace_allowed(forced_chatgpt_workspace_id, &auth)?;
|
||||
Ok(Some(CodexAuth::PersonalAccessToken(auth)))
|
||||
}
|
||||
CodexAccessToken::AgentIdentityJwt(jwt) => CodexAuth::from_agent_identity_jwt(
|
||||
jwt,
|
||||
chatgpt_base_url,
|
||||
agent_identity_authapi_base_url,
|
||||
)
|
||||
CodexAccessToken::AgentIdentityJwt(jwt) => {
|
||||
CodexAuth::from_agent_identity_jwt_with_authapi_base_url(
|
||||
jwt,
|
||||
chatgpt_base_url,
|
||||
require_agent_identity_authapi_base_url(agent_identity_authapi_base_url)?,
|
||||
)
|
||||
}
|
||||
.await
|
||||
.map(Some),
|
||||
};
|
||||
@@ -1524,7 +1698,9 @@ pub struct AuthManager {
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
forced_chatgpt_workspace_id: RwLock<Option<Vec<String>>>,
|
||||
chatgpt_base_url: Option<String>,
|
||||
agent_identity_authapi_base_url: Option<String>,
|
||||
refresh_lock: Semaphore,
|
||||
agent_identity_lock: Semaphore,
|
||||
external_auth: RwLock<Option<Arc<dyn ExternalAuth>>>,
|
||||
}
|
||||
|
||||
@@ -1572,30 +1748,16 @@ impl Debug for AuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_agent_identity_authapi_base_url() -> Option<String> {
|
||||
agent_identity_authapi_base_url(/*chatgpt_base_url*/ None).ok()
|
||||
}
|
||||
|
||||
impl AuthManager {
|
||||
/// Create a new manager loading the initial auth using the provided
|
||||
/// preferred auth method. Errors loading auth are swallowed; `auth()` will
|
||||
/// simply return `None` in that case so callers can treat it as an
|
||||
/// unauthenticated state.
|
||||
pub async fn new(
|
||||
codex_home: PathBuf,
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<String>,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> Self {
|
||||
Self::new_with_workspace_restriction(
|
||||
codex_home,
|
||||
enable_codex_api_key_env,
|
||||
auth_credentials_store_mode,
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
chatgpt_base_url,
|
||||
keyring_backend_kind,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn new_with_workspace_restriction(
|
||||
codex_home: PathBuf,
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
@@ -1603,6 +1765,8 @@ impl AuthManager {
|
||||
chatgpt_base_url: Option<String>,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> Self {
|
||||
let agent_identity_authapi_base_url =
|
||||
agent_identity_authapi_base_url(chatgpt_base_url.as_deref()).ok();
|
||||
let managed_auth = load_auth(
|
||||
&codex_home,
|
||||
enable_codex_api_key_env,
|
||||
@@ -1610,7 +1774,7 @@ impl AuthManager {
|
||||
forced_chatgpt_workspace_id.as_deref(),
|
||||
chatgpt_base_url.as_deref(),
|
||||
keyring_backend_kind,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
agent_identity_authapi_base_url.as_deref(),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
@@ -1628,7 +1792,9 @@ impl AuthManager {
|
||||
keyring_backend_kind,
|
||||
forced_chatgpt_workspace_id: RwLock::new(forced_chatgpt_workspace_id),
|
||||
chatgpt_base_url,
|
||||
agent_identity_authapi_base_url,
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_lock: Semaphore::new(/*permits*/ 1),
|
||||
external_auth: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
@@ -1650,7 +1816,9 @@ impl AuthManager {
|
||||
keyring_backend_kind: AuthKeyringBackendKind::default(),
|
||||
forced_chatgpt_workspace_id: RwLock::new(None),
|
||||
chatgpt_base_url: None,
|
||||
agent_identity_authapi_base_url: default_agent_identity_authapi_base_url(),
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_lock: Semaphore::new(/*permits*/ 1),
|
||||
external_auth: RwLock::new(None),
|
||||
})
|
||||
}
|
||||
@@ -1671,7 +1839,40 @@ impl AuthManager {
|
||||
keyring_backend_kind: AuthKeyringBackendKind::default(),
|
||||
forced_chatgpt_workspace_id: RwLock::new(None),
|
||||
chatgpt_base_url: None,
|
||||
agent_identity_authapi_base_url: default_agent_identity_authapi_base_url(),
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_lock: Semaphore::new(/*permits*/ 1),
|
||||
external_auth: RwLock::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create an AuthManager with a specific CodexAuth and Agent Identity AuthAPI base URL, for testing only.
|
||||
#[doc(hidden)]
|
||||
pub fn from_auth_for_testing_with_agent_identity_authapi_base_url(
|
||||
auth: CodexAuth,
|
||||
agent_identity_authapi_base_url: String,
|
||||
) -> Arc<Self> {
|
||||
let cached = CachedAuth {
|
||||
auth: Some(auth),
|
||||
permanent_refresh_failure: None,
|
||||
};
|
||||
let (auth_change_tx, _auth_change_rx) = watch::channel(0);
|
||||
Arc::new(Self {
|
||||
codex_home: PathBuf::from("non-existent"),
|
||||
inner: RwLock::new(cached),
|
||||
auth_change_tx,
|
||||
enable_codex_api_key_env: false,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
keyring_backend_kind: AuthKeyringBackendKind::default(),
|
||||
forced_chatgpt_workspace_id: RwLock::new(None),
|
||||
chatgpt_base_url: None,
|
||||
agent_identity_authapi_base_url: Some(
|
||||
agent_identity_authapi_base_url
|
||||
.trim_end_matches('/')
|
||||
.to_string(),
|
||||
),
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_lock: Semaphore::new(/*permits*/ 1),
|
||||
external_auth: RwLock::new(None),
|
||||
})
|
||||
}
|
||||
@@ -1690,7 +1891,9 @@ impl AuthManager {
|
||||
keyring_backend_kind: AuthKeyringBackendKind::default(),
|
||||
forced_chatgpt_workspace_id: RwLock::new(None),
|
||||
chatgpt_base_url: None,
|
||||
agent_identity_authapi_base_url: default_agent_identity_authapi_base_url(),
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_lock: Semaphore::new(/*permits*/ 1),
|
||||
external_auth: RwLock::new(Some(
|
||||
Arc::new(BearerTokenRefresher::new(config)) as Arc<dyn ExternalAuth>
|
||||
)),
|
||||
@@ -1736,6 +1939,38 @@ impl AuthManager {
|
||||
self.auth_cached()
|
||||
}
|
||||
|
||||
pub async fn agent_identity_auth(
|
||||
&self,
|
||||
policy: AgentIdentityAuthPolicy,
|
||||
session_source: SessionSource,
|
||||
) -> std::io::Result<Option<AgentIdentityAuth>> {
|
||||
let Some(auth) = self.auth().await else {
|
||||
return Ok(None);
|
||||
};
|
||||
if policy == AgentIdentityAuthPolicy::ChatGptAuth && matches!(auth, CodexAuth::Chatgpt(_)) {
|
||||
let _bootstrap_permit = self
|
||||
.agent_identity_lock
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
return auth
|
||||
.agent_identity_auth(
|
||||
policy,
|
||||
self.agent_identity_authapi_base_url.as_deref(),
|
||||
self.forced_chatgpt_workspace_id(),
|
||||
session_source,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
auth.agent_identity_auth(
|
||||
policy,
|
||||
self.agent_identity_authapi_base_url.as_deref(),
|
||||
self.forced_chatgpt_workspace_id(),
|
||||
session_source,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Force a reload of the auth information from auth.json. Returns
|
||||
/// whether the auth value changed.
|
||||
pub async fn reload(&self) -> bool {
|
||||
@@ -1838,7 +2073,7 @@ impl AuthManager {
|
||||
forced_chatgpt_workspace_id.as_deref(),
|
||||
self.chatgpt_base_url.as_deref(),
|
||||
self.keyring_backend_kind,
|
||||
/*agent_identity_authapi_base_url*/ None,
|
||||
self.agent_identity_authapi_base_url.as_deref(),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
@@ -1911,6 +2146,7 @@ impl AuthManager {
|
||||
codex_home: PathBuf,
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
chatgpt_base_url: Option<String>,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> Arc<Self> {
|
||||
@@ -1919,6 +2155,7 @@ impl AuthManager {
|
||||
codex_home,
|
||||
enable_codex_api_key_env,
|
||||
auth_credentials_store_mode,
|
||||
forced_chatgpt_workspace_id,
|
||||
chatgpt_base_url,
|
||||
keyring_backend_kind,
|
||||
)
|
||||
@@ -1931,17 +2168,15 @@ impl AuthManager {
|
||||
config: &impl AuthManagerConfig,
|
||||
enable_codex_api_key_env: bool,
|
||||
) -> Arc<Self> {
|
||||
Arc::new(
|
||||
Self::new_with_workspace_restriction(
|
||||
config.codex_home(),
|
||||
enable_codex_api_key_env,
|
||||
config.cli_auth_credentials_store_mode(),
|
||||
config.forced_chatgpt_workspace_id(),
|
||||
Some(config.chatgpt_base_url()),
|
||||
config.auth_keyring_backend_kind(),
|
||||
)
|
||||
.await,
|
||||
Self::shared(
|
||||
config.codex_home(),
|
||||
enable_codex_api_key_env,
|
||||
config.cli_auth_credentials_store_mode(),
|
||||
config.forced_chatgpt_workspace_id(),
|
||||
Some(config.chatgpt_base_url()),
|
||||
config.auth_keyring_backend_kind(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn unauthorized_recovery(self: &Arc<Self>) -> UnauthorizedRecovery {
|
||||
|
||||
Reference in new issue
Block a user