Support workload identity in remote exec-server auth (#38610)

## Why

Remote exec-server registry requests need to refresh managed credentials before sending a request. Static auth-header resolution cannot perform the asynchronous token exchange required by workload identity.

## What changed

- Add asynchronous auth-header resolution to `AuthProvider`, with the existing static-header behavior as the default.
- Resolve fresh managed credentials for each remote environment registry request while preserving the expected account and workspace identity.
- Load the cloud configuration bundle during remote exec-server startup when workload identity is selected.

## Testing

- Update the managed-auth and environment-registry auth tests to exercise asynchronous header resolution.

GitOrigin-RevId: 5d60f1127467aaacdb5d1a8f3d92278bc4bf2e29
This commit is contained in:
cooper-oai authored and copyberry committed 2026-08-14 17:32:37 +00:00
1 parent c0d59bf614
commit fb5aa093e0
6 files changed
+163 -61

No files matched your search

+37 -15
View File
@@ -5,6 +5,8 @@ use std::sync::atomic::Ordering;
use codex_agent_identity::AgentIdentityKey;
use codex_agent_identity::authorization_header_for_agent_task;
use codex_api::AgentIdentityTelemetry;
use codex_api::AuthError;
use codex_api::AuthHeadersFuture;
use codex_api::AuthProvider;
use codex_api::SharedAuthProvider;
use codex_login::AuthHeaders;
@@ -128,26 +130,42 @@ struct AuthManagerAuthProvider {
expected_auth: CodexAuth,
}
impl AuthManagerAuthProvider {
fn is_expected_auth(&self, auth: &CodexAuth) -> bool {
auth.uses_codex_backend()
&& auth.get_account_id() == self.expected_auth.get_account_id()
&& auth.get_chatgpt_user_id() == self.expected_auth.get_chatgpt_user_id()
&& auth.is_workspace_account() == self.expected_auth.is_workspace_account()
}
fn current_auth(&self) -> Option<CodexAuth> {
self.auth_manager
.auth_cached()
.filter(|auth| self.is_expected_auth(auth))
}
}
impl AuthProvider for AuthManagerAuthProvider {
fn add_auth_headers(&self, headers: &mut HeaderMap) {
let Some(auth) = self
.auth_manager
.auth_cached()
.filter(CodexAuth::uses_codex_backend)
else {
let Some(auth) = self.current_auth() else {
return;
};
// The caller's account-scoped state was built for the expected
// identity. Follow token refreshes for that identity, but never cross
// an account or workspace boundary without rebuilding that state.
if auth.get_account_id() != self.expected_auth.get_account_id()
|| auth.get_chatgpt_user_id() != self.expected_auth.get_chatgpt_user_id()
|| auth.is_workspace_account() != self.expected_auth.is_workspace_account()
{
return;
}
auth_provider_from_auth(&auth).add_auth_headers(headers);
}
fn resolve_auth_headers(&self) -> AuthHeadersFuture<'_> {
Box::pin(async move {
let auth = self
.auth_manager
.auth()
.await
.filter(|auth| self.is_expected_auth(auth))
.ok_or_else(|| {
AuthError::Transient("managed authentication is unavailable".to_string())
})?;
Ok(auth_provider_from_auth(&auth).to_auth_headers())
})
}
}
// Some providers are meant to send no auth headers. Examples include local OSS
@@ -523,8 +541,12 @@ mod tests {
.expect("save reloaded auth");
auth_manager.reload().await;
let resolved_headers = provider
.resolve_auth_headers()
.await
.expect("managed auth headers should resolve");
assert_eq!(
provider.to_auth_headers().get(AUTHORIZATION),
resolved_headers.get(AUTHORIZATION),
Some(&HeaderValue::from_static("Bearer header.e30.reloaded"))
);