mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
[codex] Use background agent task auth for backend calls (#18094)
## Summary Introduces a single background/control-plane agent task for ChatGPT backend requests that do not have a thread-scoped task, with `AuthManager` owning the default ChatGPT backend authorization decision. Callers now ask `AuthManager` for the default ChatGPT backend authorization header. `AuthManager` decides whether that is bearer or background AgentAssertion based on config/internal state, while low-level bootstrap paths can explicitly request bearer-only auth. This PR is stacked on PR4 and focuses on the shared background task auth plumbing plus the first tranche of backend/control-plane consumers. The remaining callsite wiring is split into PR4.2 to keep review size down. ## Stack - PR1: https://github.com/openai/codex/pull/17385 - add `features.use_agent_identity` - PR2: https://github.com/openai/codex/pull/17386 - register agent identities when enabled - PR3: https://github.com/openai/codex/pull/17387 - register agent tasks when enabled - PR3.1: https://github.com/openai/codex/pull/17978 - persist and prewarm registered tasks per thread - PR4: https://github.com/openai/codex/pull/17980 - use task-scoped `AgentAssertion` for downstream calls - PR4.1: this PR - introduce AuthManager-owned background/control-plane `AgentAssertion` auth - PR4.2: https://github.com/openai/codex/pull/18260 - use background task auth for additional backend/control-plane calls ## What Changed - add background task registration and assertion minting inside `codex-login` - persist `agent_identity.background_task_id` separately from per-session task state - make `BackgroundAgentTaskManager` private to `codex-login`; call sites do not instantiate or pass it around - teach `AuthManager` the ChatGPT backend base URL and feature-derived background auth mode from resolved config - expose bearer-only helpers for bootstrap/registration/refresh-style paths that must not use AgentAssertion - wire `AuthManager` default ChatGPT authorization through app listing, connector directory listing, remote plugins, MCP status/listing, analytics, and core-skills remote calls - preserve bearer fallback when the feature is disabled, the backend host is unsupported, or background task registration is not available ## Validation - `just fmt` - `cargo check -p codex-core -p codex-login -p codex-analytics -p codex-app-server -p codex-cloud-requirements -p codex-cloud-tasks -p codex-models-manager -p codex-chatgpt -p codex-model-provider -p codex-mcp -p codex-core-skills` - `cargo test -p codex-login agent_identity` - `cargo test -p codex-model-provider bearer_auth_provider` - `cargo test -p codex-core agent_assertion` - `cargo test -p codex-app-server remote_control` - `cargo test -p codex-cloud-requirements fetch_cloud_requirements` - `cargo test -p codex-models-manager manager::tests` - `cargo test -p codex-chatgpt` - `cargo test -p codex-cloud-tasks` - `just fix -p codex-core -p codex-login -p codex-analytics -p codex-app-server -p codex-cloud-requirements -p codex-cloud-tasks -p codex-models-manager -p codex-chatgpt -p codex-model-provider -p codex-mcp -p codex-core-skills` - `just fix -p codex-app-server` - `git diff --check`
This commit is contained in:
1 parent
e1c289e11b
commit
904c751a40
23 files changed
+1297
-149
No files matched your search
@@ -167,6 +167,7 @@ mod tests {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,6 +211,7 @@ fn chatgpt_auth_persists_agent_identity_for_workspace() {
|
||||
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())
|
||||
|
||||
@@ -41,9 +41,12 @@ 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 {
|
||||
@@ -372,7 +375,7 @@ impl CodexAuth {
|
||||
.filter(|identity| identity.workspace_id == workspace_id)
|
||||
}
|
||||
|
||||
pub fn set_agent_identity(&self, record: AgentIdentityAuthRecord) -> std::io::Result<()> {
|
||||
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),
|
||||
@@ -385,6 +388,13 @@ impl CodexAuth {
|
||||
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);
|
||||
@@ -1181,6 +1191,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: AsyncMutex<()>,
|
||||
external_auth: RwLock<Option<Arc<dyn ExternalAuth>>>,
|
||||
auth_state_tx: watch::Sender<()>,
|
||||
@@ -1201,6 +1213,16 @@ 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 {
|
||||
@@ -1217,6 +1239,11 @@ 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()
|
||||
}
|
||||
@@ -1249,6 +1276,8 @@ 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: AsyncMutex::new(()),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_state_tx,
|
||||
@@ -1269,6 +1298,8 @@ 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: AsyncMutex::new(()),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_state_tx,
|
||||
@@ -1288,6 +1319,8 @@ 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: AsyncMutex::new(()),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_state_tx,
|
||||
@@ -1305,6 +1338,8 @@ 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: AsyncMutex::new(()),
|
||||
external_auth: RwLock::new(Some(
|
||||
Arc::new(BearerTokenRefresher::new(config)) as Arc<dyn ExternalAuth>
|
||||
@@ -1523,6 +1558,85 @@ impl AuthManager {
|
||||
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(());
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -1565,6 +1679,10 @@ 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
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ pub struct AgentIdentityAuthRecord {
|
||||
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 {
|
||||
|
||||
@@ -69,6 +69,7 @@ async fn file_storage_persists_agent_identity() -> anyhow::Result<()> {
|
||||
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,
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user