[codex] Add host-provided Codex auth

This commit is contained in:
Liang-Ting Jiang
2026-06-27 03:48:48 +00:00
parent c464468493
commit b204596f06
28 changed files with 275 additions and 7 deletions
@@ -536,6 +536,13 @@
],
"type": "string"
},
{
"description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE.\n\nCodex backend request headers supplied by the process embedding Codex.",
"enum": [
"hostProvided"
],
"type": "string"
},
{
"description": "Programmatic Codex auth backed by a registered Agent Identity.",
"enum": [
@@ -6950,6 +6950,13 @@
],
"type": "string"
},
{
"description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE.\n\nCodex backend request headers supplied by the process embedding Codex.",
"enum": [
"hostProvided"
],
"type": "string"
},
{
"description": "Programmatic Codex auth backed by a registered Agent Identity.",
"enum": [
@@ -1076,6 +1076,13 @@
],
"type": "string"
},
{
"description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE.\n\nCodex backend request headers supplied by the process embedding Codex.",
"enum": [
"hostProvided"
],
"type": "string"
},
{
"description": "Programmatic Codex auth backed by a registered Agent Identity.",
"enum": [
@@ -25,6 +25,13 @@
],
"type": "string"
},
{
"description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE.\n\nCodex backend request headers supplied by the process embedding Codex.",
"enum": [
"hostProvided"
],
"type": "string"
},
{
"description": "Programmatic Codex auth backed by a registered Agent Identity.",
"enum": [
+1 -1
View File
@@ -5,4 +5,4 @@
/**
* Authentication mode for OpenAI-backed providers.
*/
export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "agentIdentity" | "personalAccessToken" | "bedrockApiKey";
export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "hostProvided" | "agentIdentity" | "personalAccessToken" | "bedrockApiKey";
@@ -31,6 +31,13 @@ pub enum AuthMode {
#[ts(rename = "chatgptAuthTokens")]
#[strum(serialize = "chatgptAuthTokens")]
ChatgptAuthTokens,
/// [UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE.
///
/// Codex backend request headers supplied by the process embedding Codex.
#[serde(rename = "hostProvided")]
#[ts(rename = "hostProvided")]
#[strum(serialize = "hostProvided")]
HostProvided,
/// Programmatic Codex auth backed by a registered Agent Identity.
#[serde(rename = "agentIdentity")]
#[ts(rename = "agentIdentity")]
@@ -53,7 +60,7 @@ impl AuthMode {
pub fn has_chatgpt_account(self) -> bool {
match self {
Self::Chatgpt | Self::ChatgptAuthTokens | Self::PersonalAccessToken => true,
Self::ApiKey | Self::AgentIdentity | Self::BedrockApiKey => false,
Self::ApiKey | Self::HostProvided | Self::AgentIdentity | Self::BedrockApiKey => false,
}
}
@@ -62,6 +69,7 @@ impl AuthMode {
match self {
Self::Chatgpt
| Self::ChatgptAuthTokens
| Self::HostProvided
| Self::AgentIdentity
| Self::PersonalAccessToken => true,
Self::ApiKey | Self::BedrockApiKey => false,
+1
View File
@@ -12,6 +12,7 @@ pub(crate) fn auth_mode_to_api(auth_mode: AuthMode) -> ApiAuthMode {
AuthMode::ApiKey => ApiAuthMode::ApiKey,
AuthMode::Chatgpt => ApiAuthMode::Chatgpt,
AuthMode::ChatgptAuthTokens => ApiAuthMode::ChatgptAuthTokens,
AuthMode::HostProvided => ApiAuthMode::HostProvided,
AuthMode::AgentIdentity => ApiAuthMode::AgentIdentity,
AuthMode::PersonalAccessToken => ApiAuthMode::PersonalAccessToken,
AuthMode::BedrockApiKey => ApiAuthMode::BedrockApiKey,
@@ -812,7 +812,9 @@ impl AccountRequestProcessor {
let auth_mode = auth_mode_to_api(auth.api_auth_mode());
let (reported_auth_method, token_opt) = if matches!(
auth,
CodexAuth::AgentIdentity(_) | CodexAuth::PersonalAccessToken(_)
CodexAuth::HostProvided(_)
| CodexAuth::AgentIdentity(_)
| CodexAuth::PersonalAccessToken(_)
) || include_token
&& permanent_refresh_failure
{
+6
View File
@@ -1327,6 +1327,7 @@ fn stored_auth_mode(auth: &codex_login::AuthDotJson) -> &'static str {
AuthMode::ApiKey => "api_key",
AuthMode::Chatgpt => "chatgpt",
AuthMode::ChatgptAuthTokens => "chatgpt_auth_tokens",
AuthMode::HostProvided => "host_provided",
AuthMode::AgentIdentity => "agent_identity",
AuthMode::PersonalAccessToken => "personal_access_token",
AuthMode::BedrockApiKey => "bedrock_api_key",
@@ -1397,6 +1398,9 @@ fn stored_auth_issues(
issues.push("external ChatGPT auth is missing refresh metadata");
}
}
AuthMode::HostProvided => {
issues.push("host-provided auth cannot be loaded from auth storage");
}
AuthMode::AgentIdentity => {
if auth
.agent_identity
@@ -2466,6 +2470,7 @@ fn auth_mode_name(auth: &CodexAuth) -> &'static str {
AuthMode::ApiKey => "api_key",
AuthMode::Chatgpt => "chatgpt",
AuthMode::ChatgptAuthTokens => "chatgpt_auth_tokens",
AuthMode::HostProvided => "host_provided",
AuthMode::AgentIdentity => "agent_identity",
AuthMode::PersonalAccessToken => "personal_access_token",
AuthMode::BedrockApiKey => "bedrock_api_key",
@@ -2605,6 +2610,7 @@ fn provider_auth_reachability_mode_from_auth(
Some(
AuthMode::Chatgpt
| AuthMode::ChatgptAuthTokens
| AuthMode::HostProvided
| AuthMode::AgentIdentity
| AuthMode::PersonalAccessToken,
)
+4
View File
@@ -449,6 +449,10 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
eprintln!("Logged in using ChatGPT");
std::process::exit(0);
}
AuthMode::HostProvided => {
eprintln!("Logged in using host-provided auth");
std::process::exit(0);
}
AuthMode::AgentIdentity => {
eprintln!("Logged in using access token");
std::process::exit(0);
@@ -26,6 +26,8 @@ use codex_config::McpServerToolConfig;
use codex_config::types::AuthKeyringBackendKind;
use codex_config::types::OAuthCredentialsStoreMode;
use codex_exec_server::EnvironmentManager;
use codex_login::CodexAuth;
use codex_login::HostProvidedAuth;
use codex_protocol::ToolName;
use codex_protocol::mcp::McpServerInfo;
use codex_protocol::models::PermissionProfile;
@@ -68,6 +70,34 @@ fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo {
}
}
#[test]
fn host_provided_auth_scopes_codex_apps_cache() {
let auth = CodexAuth::HostProvided(
HostProvidedAuth::new([], "user-123").with_account_id("account-123"),
);
assert_eq!(
crate::codex_apps::codex_apps_tools_cache_key(Some(&auth)),
CodexAppsToolsCacheKey {
account_id: Some("account-123".to_string()),
chatgpt_user_id: Some("user-123".to_string()),
is_workspace_account: false,
}
);
}
fn create_test_tool_with_connector(
server_name: &str,
tool_name: &str,
connector_id: &str,
connector_name: Option<&str>,
) -> ToolInfo {
let mut tool = create_test_tool(server_name, tool_name);
tool.connector_id = Some(connector_id.to_string());
tool.connector_name = connector_name.map(ToOwned::to_owned);
tool
}
fn create_codex_apps_tools_cache_context(
codex_home: PathBuf,
account_id: Option<&str>,
+1
View File
@@ -70,6 +70,7 @@ pub use codex_features::Feature;
pub use codex_features::Features;
pub use codex_home::CodexHomeUserInstructionsProvider;
pub use codex_login::AuthManager;
pub use codex_login::HostProvidedAuth;
pub use codex_login::default_client::set_default_originator;
pub use codex_model_provider_info::OPENAI_PROVIDER_ID;
pub use codex_model_provider_info::built_in_model_providers;
+1
View File
@@ -2088,6 +2088,7 @@ impl AuthRequestTelemetryContext {
AuthMode::ApiKey | AuthMode::BedrockApiKey => "ApiKey",
AuthMode::Chatgpt
| AuthMode::ChatgptAuthTokens
| AuthMode::HostProvided
| AuthMode::AgentIdentity
| AuthMode::PersonalAccessToken => "Chatgpt",
}),
+19
View File
@@ -10,6 +10,7 @@ use codex_extension_api::McpServerContribution;
use codex_extension_api::McpServerContributionContext;
use codex_extension_api::McpServerContributor;
use codex_login::CodexAuth;
use codex_login::HostProvidedAuth;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use pretty_assertions::assert_eq;
@@ -146,6 +147,24 @@ async fn hosted_apps_mcp_requires_chatgpt_auth() -> TestResult {
Ok(())
}
#[tokio::test]
async fn hosted_apps_mcp_accepts_host_provided_codex_auth() -> TestResult {
let codex_home = tempfile::tempdir()?;
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.cli_overrides(vec![("features.apps".to_string(), true.into())])
.build()
.await?;
let auth = CodexAuth::HostProvided(HostProvidedAuth::new([], "user-123"));
let manager = installed_manager(&config);
let servers = manager.effective_servers(&config, Some(&auth)).await;
assert!(servers.contains_key(CODEX_APPS_MCP_SERVER_NAME));
Ok(())
}
#[tokio::test]
async fn disabled_apps_remove_reserved_server_config_for_all_hosts() -> TestResult {
let codex_home = tempfile::tempdir()?;
@@ -85,6 +85,7 @@ async fn login_with_bedrock_api_key_replaces_openai_auth() -> anyhow::Result<()>
CodexAuth::ApiKey(_)
| CodexAuth::Chatgpt(_)
| CodexAuth::ChatgptAuthTokens(_)
| CodexAuth::HostProvided(_)
| CodexAuth::AgentIdentity(_)
| CodexAuth::PersonalAccessToken(_) => None,
}),
@@ -148,6 +149,7 @@ async fn bedrock_only_auth_storage_creates_primary_auth() -> anyhow::Result<()>
CodexAuth::ApiKey(_)
| CodexAuth::Chatgpt(_)
| CodexAuth::ChatgptAuthTokens(_)
| CodexAuth::HostProvided(_)
| CodexAuth::AgentIdentity(_)
| CodexAuth::PersonalAccessToken(_) => None,
}),
+58
View File
@@ -0,0 +1,58 @@
use std::collections::BTreeMap;
use std::fmt;
/// Codex backend authentication supplied by the process embedding Codex.
///
/// The embedding process owns credential validation, rotation, and persistence.
/// Codex keeps this snapshot in memory and uses its headers for backend requests.
#[derive(Clone, PartialEq, Eq)]
pub struct HostProvidedAuth {
headers: BTreeMap<String, String>,
account_id: Option<String>,
user_id: String,
}
impl fmt::Debug for HostProvidedAuth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HostProvidedAuth")
.field("headers", &"<redacted>")
.field("account_id", &self.account_id)
.field("user_id", &self.user_id)
.finish()
}
}
impl HostProvidedAuth {
/// Creates host-provided auth with backend request headers and a stable user identity.
pub fn new(
headers: impl IntoIterator<Item = (String, String)>,
user_id: impl Into<String>,
) -> Self {
Self {
headers: headers.into_iter().collect(),
account_id: None,
user_id: user_id.into(),
}
}
/// Adds the account selected by the embedding process.
pub fn with_account_id(mut self, account_id: impl Into<String>) -> Self {
self.account_id = Some(account_id.into());
self
}
/// Returns the request headers supplied by the embedding process.
pub fn headers(&self) -> &BTreeMap<String, String> {
&self.headers
}
/// Returns the selected account ID, when the embedding process supplied one.
pub fn account_id(&self) -> Option<&str> {
self.account_id.as_deref()
}
/// Returns the stable user ID supplied by the embedding process.
pub fn user_id(&self) -> &str {
&self.user_id
}
}
+38 -3
View File
@@ -41,6 +41,7 @@ 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;
use crate::auth::host_provided::HostProvidedAuth;
pub use crate::auth::personal_access_token::PersonalAccessTokenAuth;
pub use crate::auth::storage::AgentIdentityAuthRecord;
pub use crate::auth::storage::AgentIdentityStorage;
@@ -71,6 +72,7 @@ pub enum CodexAuth {
ApiKey(ApiKeyAuth),
Chatgpt(ChatgptAuth),
ChatgptAuthTokens(ChatgptAuthTokens),
HostProvided(HostProvidedAuth),
AgentIdentity(AgentIdentityAuth),
PersonalAccessToken(PersonalAccessTokenAuth),
BedrockApiKey(BedrockApiKeyAuth),
@@ -145,6 +147,7 @@ impl AgentIdentityBootstrapCooldown {
impl PartialEq for CodexAuth {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::HostProvided(a), Self::HostProvided(b)) => a == b,
(Self::PersonalAccessToken(a), Self::PersonalAccessToken(b)) => a == b,
(Self::BedrockApiKey(a), Self::BedrockApiKey(b)) => a == b,
_ => self.api_auth_mode() == other.api_auth_mode(),
@@ -357,6 +360,11 @@ impl CodexAuth {
};
return Ok(Self::BedrockApiKey(auth));
}
if auth_mode == AuthMode::HostProvided {
return Err(std::io::Error::other(
"host-provided auth cannot be loaded from auth storage.",
));
}
let storage_mode = auth_dot_json.storage_mode(auth_credentials_store_mode);
let client = create_default_auth_client(&refresh_token_endpoint(), auth_route_config)?;
@@ -376,6 +384,9 @@ impl CodexAuth {
}
AuthMode::ChatgptAuthTokens => Ok(Self::ChatgptAuthTokens(ChatgptAuthTokens { state })),
AuthMode::ApiKey => unreachable!("api key mode is handled above"),
AuthMode::HostProvided => {
unreachable!("host-provided auth is never loaded from auth storage")
}
AuthMode::AgentIdentity => unreachable!("agent identity mode is handled above"),
AuthMode::PersonalAccessToken => {
unreachable!("personal access token mode is handled above")
@@ -458,6 +469,7 @@ impl CodexAuth {
match self {
Self::ApiKey(_) => AuthMode::ApiKey,
Self::Chatgpt(_) | Self::ChatgptAuthTokens(_) => AuthMode::Chatgpt,
Self::HostProvided(_) => AuthMode::HostProvided,
Self::AgentIdentity(_) => AuthMode::AgentIdentity,
Self::PersonalAccessToken(_) => AuthMode::PersonalAccessToken,
Self::BedrockApiKey(_) => AuthMode::BedrockApiKey,
@@ -470,6 +482,7 @@ impl CodexAuth {
Self::ApiKey(_) => AuthMode::ApiKey,
Self::Chatgpt(_) => AuthMode::Chatgpt,
Self::ChatgptAuthTokens(_) => AuthMode::ChatgptAuthTokens,
Self::HostProvided(_) => AuthMode::HostProvided,
Self::AgentIdentity(_) => AuthMode::AgentIdentity,
Self::PersonalAccessToken(_) => AuthMode::PersonalAccessToken,
Self::BedrockApiKey(_) => AuthMode::BedrockApiKey,
@@ -506,6 +519,7 @@ impl CodexAuth {
Self::ApiKey(auth) => Some(auth.api_key.as_str()),
Self::Chatgpt(_)
| Self::ChatgptAuthTokens(_)
| Self::HostProvided(_)
| Self::AgentIdentity(_)
| Self::PersonalAccessToken(_)
| Self::BedrockApiKey(_) => None,
@@ -536,6 +550,9 @@ impl CodexAuth {
Self::AgentIdentity(_) => Err(std::io::Error::other(
"agent identity auth does not expose a bearer token",
)),
Self::HostProvided(_) => Err(std::io::Error::other(
"host-provided auth does not expose a bearer token",
)),
Self::PersonalAccessToken(auth) => Ok(auth.access_token().to_string()),
Self::BedrockApiKey(_) => Err(std::io::Error::other(
"Bedrock API key auth does not expose a Codex bearer token",
@@ -546,6 +563,7 @@ impl CodexAuth {
/// Returns `None` if Codex backend auth does not expose an account id.
pub fn get_account_id(&self) -> Option<String> {
match self {
Self::HostProvided(auth) => auth.account_id().map(str::to_string),
Self::AgentIdentity(auth) => Some(auth.account_id().to_string()),
Self::PersonalAccessToken(auth) => Some(auth.account_id().to_string()),
_ => self.get_current_token_data().and_then(|t| t.account_id),
@@ -555,6 +573,7 @@ impl CodexAuth {
/// Returns false if Codex backend auth omits the FedRAMP claim.
pub fn is_fedramp_account(&self) -> bool {
match self {
Self::HostProvided(_) => false,
Self::AgentIdentity(auth) => auth.is_fedramp_account(),
Self::PersonalAccessToken(auth) => auth.is_fedramp_account(),
_ => self
@@ -566,6 +585,7 @@ impl CodexAuth {
/// Returns `None` if Codex backend auth does not expose an account email.
pub fn get_account_email(&self) -> Option<String> {
match self {
Self::HostProvided(_) => None,
Self::AgentIdentity(auth) => auth.email().map(str::to_string),
Self::PersonalAccessToken(auth) => auth.email().map(str::to_string),
_ => self.get_current_token_data().and_then(|t| t.id_token.email),
@@ -575,6 +595,7 @@ impl CodexAuth {
/// Returns `None` if Codex backend auth does not expose a ChatGPT user id.
pub fn get_chatgpt_user_id(&self) -> Option<String> {
match self {
Self::HostProvided(auth) => Some(auth.user_id().to_string()),
Self::AgentIdentity(auth) => Some(auth.chatgpt_user_id().to_string()),
Self::PersonalAccessToken(auth) => Some(auth.chatgpt_user_id().to_string()),
_ => self
@@ -587,6 +608,9 @@ impl CodexAuth {
/// Returns a high-level `AccountPlanType` (e.g., Free/Plus/Pro/Team/…)
/// for UI or product decisions based on the user's subscription.
pub fn account_plan_type(&self) -> Option<AccountPlanType> {
if matches!(self, Self::HostProvided(_)) {
return Some(AccountPlanType::Unknown);
}
if let Self::AgentIdentity(auth) = self {
return Some(auth.plan_type());
}
@@ -613,6 +637,7 @@ impl CodexAuth {
Self::Chatgpt(auth) => &auth.state,
Self::ChatgptAuthTokens(auth) => &auth.state,
Self::ApiKey(_)
| Self::HostProvided(_)
| Self::AgentIdentity(_)
| Self::PersonalAccessToken(_)
| Self::BedrockApiKey(_) => return None,
@@ -658,6 +683,7 @@ impl CodexAuth {
Self::AgentIdentity(auth) => Ok(Some(auth.clone())),
Self::ApiKey(_)
| Self::ChatgptAuthTokens(_)
| Self::HostProvided(_)
| Self::PersonalAccessToken(_)
| Self::BedrockApiKey(_) => Ok(None),
Self::Chatgpt(_) => {
@@ -1086,10 +1112,12 @@ async fn enforce_login_restrictions_with_agent_identity_authapi_base_url(
| (ForcedLoginMethod::Api, AuthMode::BedrockApiKey) => None,
(ForcedLoginMethod::Chatgpt, AuthMode::Chatgpt)
| (ForcedLoginMethod::Chatgpt, AuthMode::ChatgptAuthTokens)
| (ForcedLoginMethod::Chatgpt, AuthMode::HostProvided)
| (ForcedLoginMethod::Chatgpt, AuthMode::AgentIdentity)
| (ForcedLoginMethod::Chatgpt, AuthMode::PersonalAccessToken) => None,
(ForcedLoginMethod::Api, AuthMode::Chatgpt)
| (ForcedLoginMethod::Api, AuthMode::ChatgptAuthTokens)
| (ForcedLoginMethod::Api, AuthMode::HostProvided)
| (ForcedLoginMethod::Api, AuthMode::AgentIdentity)
| (ForcedLoginMethod::Api, AuthMode::PersonalAccessToken) => Some(
"API key login is required, but ChatGPT is currently being used. Logging out."
@@ -1115,9 +1143,9 @@ async fn enforce_login_restrictions_with_agent_identity_authapi_base_url(
if let Some(expected_account_ids) = config.forced_chatgpt_workspace_id.as_deref() {
let chatgpt_account_id = match &auth {
CodexAuth::ApiKey(_) | CodexAuth::BedrockApiKey(_) => return Ok(()),
CodexAuth::AgentIdentity(_) | CodexAuth::PersonalAccessToken(_) => {
auth.get_account_id()
}
CodexAuth::HostProvided(_)
| CodexAuth::AgentIdentity(_)
| CodexAuth::PersonalAccessToken(_) => auth.get_account_id(),
CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_) => {
let token_data = match auth.get_token_data() {
Ok(data) => data,
@@ -2172,6 +2200,7 @@ impl AuthManager {
| (AuthMode::ChatgptAuthTokens, AuthMode::ChatgptAuthTokens) => {
a.get_current_auth_json() == b.get_current_auth_json()
}
(AuthMode::HostProvided, AuthMode::HostProvided) => a == b,
(AuthMode::AgentIdentity, AuthMode::AgentIdentity) => match (a, b) {
(CodexAuth::AgentIdentity(a), CodexAuth::AgentIdentity(b)) => {
a.record() == b.record()
@@ -2256,6 +2285,11 @@ impl AuthManager {
}
}
/// Replaces the current auth snapshot with Codex backend auth supplied by the host.
pub fn set_host_provided_auth(&self, auth: HostProvidedAuth) {
let _ = self.set_cached_auth(Some(CodexAuth::HostProvided(auth)));
}
pub fn clear_external_auth(&self) {
if let Ok(mut guard) = self.external_auth.write() {
*guard = None;
@@ -2452,6 +2486,7 @@ impl AuthManager {
.await
}
CodexAuth::ApiKey(_)
| CodexAuth::HostProvided(_)
| CodexAuth::AgentIdentity(_)
| CodexAuth::PersonalAccessToken(_)
| CodexAuth::BedrockApiKey(_) => Ok(()),
+2
View File
@@ -8,6 +8,7 @@ mod storage;
mod util;
mod external_bearer;
mod host_provided;
mod manager;
mod revoke;
@@ -15,4 +16,5 @@ pub use bedrock_api_key::BedrockApiKeyAuth;
pub use bedrock_api_key::login_with_bedrock_api_key;
pub use error::RefreshTokenFailedError;
pub use error::RefreshTokenFailedReason;
pub use host_provided::HostProvidedAuth;
pub use manager::*;
+1
View File
@@ -35,6 +35,7 @@ pub use auth::ExternalAuthFuture;
pub use auth::ExternalAuthRefreshContext;
pub use auth::ExternalAuthRefreshReason;
pub use auth::ExternalAuthTokens;
pub use auth::HostProvidedAuth;
pub use auth::OPENAI_API_KEY_ENV_VAR;
pub use auth::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
pub use auth::REVOKE_TOKEN_URL_OVERRIDE_ENV_VAR;
+1
View File
@@ -244,6 +244,7 @@ impl ModelProviderInfo {
Some(
AuthMode::Chatgpt
| AuthMode::ChatgptAuthTokens
| AuthMode::HostProvided
| AuthMode::AgentIdentity
| AuthMode::PersonalAccessToken
)
@@ -69,6 +69,7 @@ impl AmazonBedrockModelProvider {
CodexAuth::ApiKey(_)
| CodexAuth::Chatgpt(_)
| CodexAuth::ChatgptAuthTokens(_)
| CodexAuth::HostProvided(_)
| CodexAuth::AgentIdentity(_)
| CodexAuth::PersonalAccessToken(_) => None,
})
+58
View File
@@ -9,6 +9,7 @@ use codex_api::AuthProvider;
use codex_api::SharedAuthProvider;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::HostProvidedAuth;
use codex_login::auth::AgentIdentityAuth;
use codex_login::auth::AgentIdentityAuthError;
use codex_login::auth::AgentIdentityAuthPolicy;
@@ -16,6 +17,7 @@ use codex_model_provider_info::ModelProviderInfo;
use codex_protocol::error::CodexErr;
use codex_protocol::protocol::SessionSource;
use http::HeaderMap;
use http::HeaderName;
use http::HeaderValue;
use crate::bearer_auth_provider::BearerAuthProvider;
@@ -109,6 +111,30 @@ impl AuthProvider for AgentIdentityAuthProvider {
}
}
#[derive(Clone, Debug)]
struct HostProvidedAuthProvider {
auth: HostProvidedAuth,
}
impl AuthProvider for HostProvidedAuthProvider {
fn add_auth_headers(&self, headers: &mut HeaderMap) {
for (name, value) in self.auth.headers() {
let Ok(name) = HeaderName::from_bytes(name.as_bytes()) else {
continue;
};
let Ok(value) = HeaderValue::from_str(value) else {
continue;
};
let _ = headers.insert(name, value);
}
if let Some(account_id) = self.auth.account_id()
&& let Ok(account_id) = HeaderValue::from_str(account_id)
{
let _ = headers.insert("ChatGPT-Account-ID", account_id);
}
}
}
// Some providers are meant to send no auth headers. Examples include local OSS
// providers and custom test providers with `requires_openai_auth = false`.
#[derive(Clone, Debug)]
@@ -240,6 +266,9 @@ fn bearer_auth_for_provider(
/// Builds request-header auth for a first-party Codex auth snapshot.
pub fn auth_provider_from_auth(auth: &CodexAuth) -> SharedAuthProvider {
match auth {
CodexAuth::HostProvided(auth) => {
Arc::new(HostProvidedAuthProvider { auth: auth.clone() })
}
CodexAuth::AgentIdentity(auth) => {
Arc::new(AgentIdentityAuthProvider { auth: auth.clone() })
}
@@ -405,6 +434,35 @@ mod tests {
}
}
#[test]
fn host_provided_auth_provider_uses_host_headers() {
let auth = CodexAuth::HostProvided(
HostProvidedAuth::new(
[(
"x-openai-actor-authorization".to_string(),
"actor-token".to_string(),
)],
"user-123",
)
.with_account_id("account-123"),
);
let headers = auth_provider_from_auth(&auth).to_auth_headers();
assert_eq!(
headers
.get("x-openai-actor-authorization")
.and_then(|value| value.to_str().ok()),
Some("actor-token")
);
assert_eq!(
headers
.get("ChatGPT-Account-ID")
.and_then(|value| value.to_str().ok()),
Some("account-123")
);
}
#[tokio::test]
async fn first_party_run_scope_uses_agent_assertion_and_exposes_telemetry() {
let auth = CodexAuth::AgentIdentity(
+1
View File
@@ -286,6 +286,7 @@ impl ModelProvider for ConfiguredModelProvider {
}
CodexAuth::Chatgpt(_)
| CodexAuth::ChatgptAuthTokens(_)
| CodexAuth::HostProvided(_)
| CodexAuth::AgentIdentity(_)
| CodexAuth::PersonalAccessToken(_) => {
let email = auth.get_account_email();
+1
View File
@@ -59,6 +59,7 @@ impl From<AuthMode> for TelemetryAuthMode {
AuthMode::ApiKey | AuthMode::BedrockApiKey => Self::ApiKey,
AuthMode::Chatgpt
| AuthMode::ChatgptAuthTokens
| AuthMode::HostProvided
| AuthMode::AgentIdentity
| AuthMode::PersonalAccessToken => Self::Chatgpt,
}
+6 -1
View File
@@ -15,6 +15,10 @@ pub enum AuthMode {
#[serde(rename = "chatgptAuthTokens")]
#[strum(serialize = "chatgptAuthTokens")]
ChatgptAuthTokens,
/// Codex backend request headers supplied by the process embedding Codex.
#[serde(rename = "hostProvided")]
#[strum(serialize = "hostProvided")]
HostProvided,
/// Programmatic Codex auth backed by a registered Agent Identity.
#[serde(rename = "agentIdentity")]
#[strum(serialize = "agentIdentity")]
@@ -34,7 +38,7 @@ impl AuthMode {
pub fn has_chatgpt_account(self) -> bool {
match self {
Self::Chatgpt | Self::ChatgptAuthTokens | Self::PersonalAccessToken => true,
Self::ApiKey | Self::AgentIdentity | Self::BedrockApiKey => false,
Self::ApiKey | Self::HostProvided | Self::AgentIdentity | Self::BedrockApiKey => false,
}
}
@@ -43,6 +47,7 @@ impl AuthMode {
match self {
Self::Chatgpt
| Self::ChatgptAuthTokens
| Self::HostProvided
| Self::AgentIdentity
| Self::PersonalAccessToken => true,
Self::ApiKey | Self::BedrockApiKey => false,
@@ -86,6 +86,7 @@ impl App {
Some(
AuthMode::Chatgpt
| AuthMode::ChatgptAuthTokens
| AuthMode::HostProvided
| AuthMode::AgentIdentity
| AuthMode::PersonalAccessToken
)
+1
View File
@@ -1202,6 +1202,7 @@ pub(crate) fn status_account_display_from_auth_mode(
Some(AuthMode::ApiKey) => Some(StatusAccountDisplay::ApiKey),
Some(AuthMode::Chatgpt)
| Some(AuthMode::ChatgptAuthTokens)
| Some(AuthMode::HostProvided)
| Some(AuthMode::AgentIdentity)
| Some(AuthMode::PersonalAccessToken) => Some(StatusAccountDisplay::ChatGpt {
email: None,
+1
View File
@@ -950,6 +950,7 @@ impl AuthModeWidget {
ApiAuthMode::ApiKey => AuthMode::ApiKey,
ApiAuthMode::Chatgpt => AuthMode::Chatgpt,
ApiAuthMode::ChatgptAuthTokens => AuthMode::ChatgptAuthTokens,
ApiAuthMode::HostProvided => AuthMode::HostProvided,
ApiAuthMode::AgentIdentity => AuthMode::AgentIdentity,
ApiAuthMode::PersonalAccessToken => AuthMode::PersonalAccessToken,
ApiAuthMode::BedrockApiKey => AuthMode::BedrockApiKey,