mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
feat: use run agent task auth for inference (#19051)
## Stack This is PR 3 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 moves Agent Identity usage into provider auth resolution. That keeps `AgentAssertion` auth tied to first-party OpenAI provider requests instead of applying a late session-wide override that could affect local, custom, Bedrock, API-key, or external-bearer providers. What changed: - adds a small `ProviderAuthScope` struct carrying the run auth policy and session source needed by provider-scoped auth resolution - lets `Session` opt the existing `ModelClient` into `ChatGptAuth` policy when `use_agent_identity` is enabled, without adding a second model-client constructor - resolves Agent Identity only for first-party OpenAI provider auth paths - uses the persisted run task id from the `AgentIdentityAuth` record to build `AgentAssertion` auth for Responses requests - routes shared request setup through scoped provider auth so unary compact requests use the same run-task assertion path as inference turns - keeps local/custom/Bedrock/env-key/external-bearer provider auth unchanged - lets missing run-task state surface through the existing model-request error path instead of silently falling back to bearer auth This PR intentionally does not create thread-scoped, target-scoped, or background-scoped task identities. The run task is the only task Codex registers in this POC shape. ## Testing - `just test -p codex-model-provider` - `just test -p codex-core client::tests::provider_auth_scope_uses` - `just test -p codex-core remote_compact_uses_agent_identity_assertion`
This commit is contained in:
1 parent
f66d793a2d
commit
51864b0b4b
22 files changed
+1020
-20
No files matched your search
@@ -46,7 +46,7 @@ pub(super) fn require_agent_identity_authapi_base_url(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum AgentIdentityAuthError {
|
||||
#[error(
|
||||
"agent identity bootstrap unavailable after {attempts} attempts during {operation}: {message}"
|
||||
@@ -59,13 +59,14 @@ pub enum AgentIdentityAuthError {
|
||||
}
|
||||
|
||||
impl AgentIdentityAuthError {
|
||||
pub fn is_bootstrap_unavailable(error: &std::io::Error) -> bool {
|
||||
matches!(
|
||||
error
|
||||
.get_ref()
|
||||
.and_then(|source| source.downcast_ref::<Self>()),
|
||||
Some(Self::BootstrapUnavailable { .. })
|
||||
)
|
||||
pub(super) fn bootstrap_unavailable(error: &std::io::Error) -> Option<&Self> {
|
||||
match error
|
||||
.get_ref()
|
||||
.and_then(|source| source.downcast_ref::<Self>())
|
||||
{
|
||||
Some(error @ Self::BootstrapUnavailable { .. }) => Some(error),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -584,7 +584,7 @@ async fn chatgpt_auth_registration_retry_exhaustion_is_fallback_eligible() -> an
|
||||
.await
|
||||
.expect_err("retry exhaustion should return an error");
|
||||
|
||||
assert!(AgentIdentityAuthError::is_bootstrap_unavailable(&err));
|
||||
assert!(AgentIdentityAuthError::bootstrap_unavailable(&err).is_some());
|
||||
assert!(
|
||||
auth.stored_managed_chatgpt_agent_identity_record("account-123")
|
||||
.is_none()
|
||||
@@ -648,7 +648,7 @@ async fn chatgpt_auth_task_registration_retry_exhaustion_is_fallback_eligible()
|
||||
.await
|
||||
.expect_err("task retry exhaustion should return an error");
|
||||
|
||||
assert!(AgentIdentityAuthError::is_bootstrap_unavailable(&err));
|
||||
assert!(AgentIdentityAuthError::bootstrap_unavailable(&err).is_some());
|
||||
record.task_id = None;
|
||||
assert_eq!(
|
||||
auth.stored_managed_chatgpt_agent_identity_record("account-123"),
|
||||
@@ -701,7 +701,7 @@ async fn chatgpt_auth_non_retryable_registration_error_is_hard_failure() -> anyh
|
||||
.await
|
||||
.expect_err("hard registration failure should return an error");
|
||||
|
||||
assert!(!AgentIdentityAuthError::is_bootstrap_unavailable(&err));
|
||||
assert!(AgentIdentityAuthError::bootstrap_unavailable(&err).is_none());
|
||||
assert!(
|
||||
auth.stored_managed_chatgpt_agent_identity_record("account-123")
|
||||
.is_none()
|
||||
@@ -742,7 +742,7 @@ async fn agent_identity_jwt_task_registration_retry_exhaustion_is_strict() -> an
|
||||
.await
|
||||
.expect_err("agent identity jwt task retry exhaustion should fail");
|
||||
|
||||
assert!(!AgentIdentityAuthError::is_bootstrap_unavailable(&err));
|
||||
assert!(AgentIdentityAuthError::bootstrap_unavailable(&err).is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ use std::sync::Mutex;
|
||||
use std::sync::RwLock;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::sync::watch;
|
||||
use tracing::instrument;
|
||||
@@ -83,6 +85,63 @@ pub enum AgentIdentityAuthPolicy {
|
||||
ChatGptAuth,
|
||||
}
|
||||
|
||||
const AGENT_IDENTITY_BOOTSTRAP_FAILURE_COOLDOWN: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CachedAgentIdentityBootstrapFailure {
|
||||
account_id: String,
|
||||
authapi_base_url: String,
|
||||
retry_at: Instant,
|
||||
error: AgentIdentityAuthError,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct AgentIdentityBootstrapCooldown {
|
||||
failure: Option<CachedAgentIdentityBootstrapFailure>,
|
||||
}
|
||||
|
||||
impl AgentIdentityBootstrapCooldown {
|
||||
fn error_for(
|
||||
&mut self,
|
||||
account_id: &str,
|
||||
authapi_base_url: &str,
|
||||
now: Instant,
|
||||
) -> Option<AgentIdentityAuthError> {
|
||||
let error = self
|
||||
.failure
|
||||
.as_ref()
|
||||
.filter(|failure| {
|
||||
failure.account_id == account_id
|
||||
&& failure.authapi_base_url == authapi_base_url
|
||||
&& failure.retry_at > now
|
||||
})
|
||||
.map(|failure| failure.error.clone());
|
||||
if error.is_none() {
|
||||
self.clear();
|
||||
}
|
||||
error
|
||||
}
|
||||
|
||||
fn record_failure(
|
||||
&mut self,
|
||||
account_id: String,
|
||||
authapi_base_url: String,
|
||||
error: AgentIdentityAuthError,
|
||||
now: Instant,
|
||||
) {
|
||||
self.failure = Some(CachedAgentIdentityBootstrapFailure {
|
||||
account_id,
|
||||
authapi_base_url,
|
||||
retry_at: now + AGENT_IDENTITY_BOOTSTRAP_FAILURE_COOLDOWN,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.failure = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for CodexAuth {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
@@ -1742,6 +1801,7 @@ pub struct AuthManager {
|
||||
agent_identity_authapi_base_url: Option<String>,
|
||||
refresh_lock: Semaphore,
|
||||
agent_identity_lock: Semaphore,
|
||||
agent_identity_bootstrap_cooldown: Mutex<AgentIdentityBootstrapCooldown>,
|
||||
external_auth: RwLock<Option<Arc<dyn ExternalAuth>>>,
|
||||
auth_route_config: Option<AuthRouteConfig>,
|
||||
}
|
||||
@@ -1843,6 +1903,7 @@ impl AuthManager {
|
||||
agent_identity_authapi_base_url,
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_bootstrap_cooldown: Mutex::default(),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_route_config,
|
||||
}
|
||||
@@ -1868,6 +1929,7 @@ impl AuthManager {
|
||||
agent_identity_authapi_base_url: default_agent_identity_authapi_base_url(),
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_bootstrap_cooldown: Mutex::default(),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_route_config: None,
|
||||
})
|
||||
@@ -1892,6 +1954,7 @@ impl AuthManager {
|
||||
agent_identity_authapi_base_url: default_agent_identity_authapi_base_url(),
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_bootstrap_cooldown: Mutex::default(),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_route_config: None,
|
||||
})
|
||||
@@ -1924,6 +1987,7 @@ impl AuthManager {
|
||||
),
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_bootstrap_cooldown: Mutex::default(),
|
||||
external_auth: RwLock::new(None),
|
||||
auth_route_config: None,
|
||||
})
|
||||
@@ -1946,6 +2010,7 @@ impl AuthManager {
|
||||
agent_identity_authapi_base_url: default_agent_identity_authapi_base_url(),
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_lock: Semaphore::new(/*permits*/ 1),
|
||||
agent_identity_bootstrap_cooldown: Mutex::default(),
|
||||
external_auth: RwLock::new(Some(
|
||||
Arc::new(BearerTokenRefresher::new(config)) as Arc<dyn ExternalAuth>
|
||||
)),
|
||||
@@ -2006,15 +2071,44 @@ impl AuthManager {
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
return auth
|
||||
let forced_chatgpt_workspace_id = self.forced_chatgpt_workspace_id();
|
||||
let cooldown_key = ManagedChatGptAgentIdentityBinding::from_auth(
|
||||
&auth,
|
||||
forced_chatgpt_workspace_id.clone(),
|
||||
)
|
||||
.and_then(|binding| {
|
||||
self.agent_identity_authapi_base_url
|
||||
.as_ref()
|
||||
.map(|base_url| (binding.account_id, base_url.clone()))
|
||||
});
|
||||
if let Some((account_id, authapi_base_url)) = cooldown_key.as_ref()
|
||||
&& let Ok(mut cooldown) = self.agent_identity_bootstrap_cooldown.lock()
|
||||
&& let Some(error) =
|
||||
cooldown.error_for(account_id, authapi_base_url, Instant::now())
|
||||
{
|
||||
tracing::warn!("agent identity bootstrap retry suppressed during shared cooldown");
|
||||
return Err(std::io::Error::other(error));
|
||||
}
|
||||
|
||||
let result = auth
|
||||
.agent_identity_auth(
|
||||
policy,
|
||||
self.agent_identity_authapi_base_url.as_deref(),
|
||||
self.forced_chatgpt_workspace_id(),
|
||||
forced_chatgpt_workspace_id,
|
||||
self.auth_route_config.as_ref(),
|
||||
session_source,
|
||||
)
|
||||
.await;
|
||||
if let Ok(mut cooldown) = self.agent_identity_bootstrap_cooldown.lock() {
|
||||
if let (Err(err), Some((account_id, authapi_base_url))) = (&result, cooldown_key)
|
||||
&& let Some(error) = AgentIdentityAuthError::bootstrap_unavailable(err).cloned()
|
||||
{
|
||||
cooldown.record_failure(account_id, authapi_base_url, error, Instant::now());
|
||||
} else {
|
||||
cooldown.clear();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
auth.agent_identity_auth(
|
||||
policy,
|
||||
|
||||
Reference in new issue
Block a user