mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +00:00
## Why Reusable clients need fresh authentication for each request while preserving the credential source and transport policy selected when the client was created. ## What changed - Add runtime-aware backend client construction and per-request authentication. Allow token refresh within the original credential source, reject credential replacement, and validate destination and account/organization selectors. - Let runtime authentication finish persisting rotated tokens when the requesting task is cancelled. - Apply asynchronous authentication to file creation and finalization requests, generating fresh assertions while keeping credentials off blob uploads. - Preserve direct sandbox routing without enabling legacy custom-CA fallback for protected clients that reject redirects. - Exclude login account metadata from model cache identities when provider bearer credentials override the login, and record authentication selection outcomes and duration. ## Testing Add regression coverage for credential refresh and replacement, organization selection, destination restrictions, transport policy preservation, cancellation during token rotation, credential stripping on redirects, and file upload authentication in both runtime authentication modes. GitOrigin-RevId: d045595e869dfcdfd9f705a6228cf0eb4c2d2c12
113 lines
4.6 KiB
Rust
113 lines
4.6 KiB
Rust
//! Model catalog cache identity follows provider routing, effective auth, and gateway OAuth config.
|
|
//! Only a digest is persisted; access tokens for ChatGPT are excluded so token
|
|
//! rotation does not discard a catalog for the same account, user, and plan.
|
|
//! Header auth with an account ID uses that identity instead of opaque auth headers.
|
|
|
|
use codex_login::CodexAuth;
|
|
use codex_model_provider_info::ModelProviderInfo;
|
|
use codex_protocol::error::Result as CoreResult;
|
|
use sha2::Digest;
|
|
use sha2::Sha256;
|
|
|
|
use crate::auth::resolve_provider_auth;
|
|
use crate::provider::resolve_api_provider;
|
|
|
|
pub(crate) fn identity(
|
|
provider_info: &ModelProviderInfo,
|
|
auth: Option<&CodexAuth>,
|
|
) -> CoreResult<String> {
|
|
// Provider bearer overrides replace the login credential and its account metadata.
|
|
let auth =
|
|
if provider_info.env_key.is_some() || provider_info.experimental_bearer_token.is_some() {
|
|
None
|
|
} else {
|
|
auth
|
|
};
|
|
let mut api_provider = resolve_api_provider(provider_info, auth)?;
|
|
let mut digest = Sha256::new();
|
|
// Length-prefix every field to avoid ambiguity between adjacent values.
|
|
let mut field = |value: &[u8]| {
|
|
digest.update((value.len() as u64).to_le_bytes());
|
|
digest.update(value);
|
|
};
|
|
field(b"models-cache-v1");
|
|
field(api_provider.name.as_bytes());
|
|
field(api_provider.base_url.as_bytes());
|
|
// Preserve existing cache identities when no explicit catalog is configured.
|
|
// The API provider resolves base_url defaults; model_catalog_url stays on ModelProviderInfo.
|
|
if let Some(catalog_url) = &provider_info.model_catalog_url {
|
|
field(b"model_catalog_url");
|
|
field(catalog_url.as_bytes());
|
|
}
|
|
let mut query: Vec<_> = api_provider.query_params.iter().flatten().collect();
|
|
query.sort();
|
|
field(&(query.len() as u64).to_le_bytes());
|
|
for (name, value) in query {
|
|
field(name.as_bytes());
|
|
field(value.as_bytes());
|
|
}
|
|
field(&[u8::from(provider_info.requires_openai_auth)]);
|
|
field(&[u8::from(provider_info.has_command_auth())]);
|
|
field(format!("{:?}", auth.map(CodexAuth::api_auth_mode)).as_bytes());
|
|
if let Some(auth) = auth {
|
|
field(format!("{:?}", auth.get_account_id()).as_bytes());
|
|
field(format!("{:?}", auth.get_chatgpt_user_id()).as_bytes());
|
|
field(format!("{:?}", auth.get_account_email()).as_bytes());
|
|
field(format!("{:?}", auth.account_plan_type()).as_bytes());
|
|
field(
|
|
format!(
|
|
"{:?}",
|
|
auth.get_token_data()
|
|
.ok()
|
|
.and_then(|tokens| tokens.id_token.get_chatgpt_plan_type_raw())
|
|
)
|
|
.as_bytes(),
|
|
);
|
|
field(&[u8::from(auth.is_fedramp_account())]);
|
|
}
|
|
let has_stable_account = auth.is_some_and(|auth| {
|
|
auth.get_account_id().is_some()
|
|
&& if matches!(auth, CodexAuth::Headers(_)) {
|
|
!provider_info.has_command_auth()
|
|
} else {
|
|
matches!(
|
|
auth,
|
|
CodexAuth::Chatgpt(_)
|
|
| CodexAuth::ChatgptAuthTokens(_)
|
|
| CodexAuth::AgentIdentity(_)
|
|
) && (auth.get_chatgpt_user_id().is_some() || auth.get_account_email().is_some())
|
|
}
|
|
});
|
|
let explicit_bearer =
|
|
provider_info.api_key()?.is_some() || provider_info.experimental_bearer_token.is_some();
|
|
// Command auth also returns opaque API keys. A changed key may belong to a
|
|
// different account, so reuse requires stable owner metadata. Explicit provider
|
|
// keys take precedence over the header auth's account identity.
|
|
if !has_stable_account || explicit_bearer {
|
|
let headers = resolve_provider_auth(auth, provider_info)?
|
|
.to_auth_headers()
|
|
.map_err(|error| codex_api::map_api_error(error.into()))?;
|
|
api_provider.headers.extend(headers);
|
|
}
|
|
let mut headers: Vec<_> = api_provider.headers.iter().collect();
|
|
headers.sort_by(|(a, av), (b, bv)| {
|
|
a.as_str()
|
|
.cmp(b.as_str())
|
|
.then(av.as_bytes().cmp(bv.as_bytes()))
|
|
});
|
|
field(&(headers.len() as u64).to_le_bytes());
|
|
for (name, value) in headers {
|
|
field(name.as_str().as_bytes());
|
|
field(value.as_bytes());
|
|
}
|
|
// Preserve existing identities for providers without gateway OAuth.
|
|
if let Some(gateway_oauth) = &provider_info.gateway_oauth {
|
|
field(&serde_json::to_vec(gateway_oauth)?);
|
|
}
|
|
Ok(format!("{:x}", digest.finalize()))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "models_identity_tests.rs"]
|
|
mod tests;
|