fix: retry agent identity auth after guarded reload

This commit is contained in:
Edward Frazer
2026-05-06 14:31:20 -07:00
parent 67849d950d
commit f3ba12b33e
2 changed files with 67 additions and 4 deletions

View File

@@ -464,6 +464,54 @@ async fn unauthorized_recovery_uses_external_refresh_for_bearer_manager() {
assert_eq!(refreshed_token.as_deref(), Some("refreshed-provider-token"));
}
#[tokio::test]
#[serial(codex_auth_env)]
async fn unauthorized_recovery_reloads_agent_identity_once() {
let codex_home = tempdir().unwrap();
let record = agent_identity_record("account-123");
let agent_identity =
signed_agent_identity_jwt(&record, json!(record.plan_type)).expect("signed agent identity");
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/backend-api/wham/agent-identities/jwks"))
.respond_with(ResponseTemplate::new(200).set_body_json(test_jwks_body()))
.expect(2)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/backend-api/v1/agent/agent-runtime-id/task/register"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"task_id": "task-123",
})))
.expect(2)
.mount(&server)
.await;
let _agent_guard = EnvVarGuard::set(CODEX_AGENT_IDENTITY_ENV_VAR, &agent_identity);
let chatgpt_base_url = format!("{}/backend-api", server.uri());
let _authapi_guard =
EnvVarGuard::set("CODEX_AGENT_IDENTITY_AUTHAPI_BASE_URL", &chatgpt_base_url);
let manager = AuthManager::shared(
codex_home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
Some(chatgpt_base_url),
)
.await;
let mut recovery = manager.unauthorized_recovery();
assert!(recovery.has_next());
assert_eq!(recovery.mode_name(), "managed");
assert_eq!(recovery.step_name(), "reload");
let result = recovery.next().await.expect("reload should succeed");
assert_eq!(result.auth_state_changed(), Some(false));
assert!(!recovery.has_next());
assert_eq!(recovery.step_name(), "done");
server.verify().await;
}
struct ProviderAuthScript {
tempdir: TempDir,
command: String,

View File

@@ -1056,6 +1056,8 @@ enum UnauthorizedRecoveryMode {
// 2. Attempt to refresh the token using OAuth token refresh flow.
// If after both steps the server still responds with 401 we let the error bubble to the user.
//
// For agent identity based authentication, we retry once after the guarded reload step.
//
// For external auth sources, UnauthorizedRecovery retries once.
//
// - External ChatGPT auth tokens (`chatgptAuthTokens`) are refreshed by asking
@@ -1116,7 +1118,7 @@ impl UnauthorizedRecovery {
.manager
.auth_cached()
.as_ref()
.is_some_and(CodexAuth::is_chatgpt_auth)
.is_some_and(CodexAuth::uses_codex_backend)
{
return false;
}
@@ -1141,7 +1143,7 @@ impl UnauthorizedRecovery {
.manager
.auth_cached()
.as_ref()
.is_some_and(CodexAuth::is_chatgpt_auth)
.is_some_and(CodexAuth::uses_codex_backend)
{
return "not_chatgpt_auth";
}
@@ -1189,13 +1191,13 @@ impl UnauthorizedRecovery {
.await
{
ReloadOutcome::ReloadedChanged => {
self.step = UnauthorizedRecoveryStep::RefreshToken;
self.step = self.next_step_after_reload();
return Ok(UnauthorizedRecoveryStepResult {
auth_state_changed: Some(true),
});
}
ReloadOutcome::ReloadedNoChange => {
self.step = UnauthorizedRecoveryStep::RefreshToken;
self.step = self.next_step_after_reload();
return Ok(UnauthorizedRecoveryStepResult {
auth_state_changed: Some(false),
});
@@ -1231,6 +1233,19 @@ impl UnauthorizedRecovery {
auth_state_changed: None,
})
}
fn next_step_after_reload(&self) -> UnauthorizedRecoveryStep {
if self
.manager
.auth_cached()
.as_ref()
.is_some_and(|auth| matches!(auth, CodexAuth::AgentIdentity(_)))
{
UnauthorizedRecoveryStep::Done
} else {
UnauthorizedRecoveryStep::RefreshToken
}
}
}
/// Central manager providing a single source of truth for auth.json derived