1use std::sync::Arc;
2use std::sync::atomic::AtomicBool;
3use std::sync::atomic::Ordering;
4
5use codex_agent_identity::AgentIdentityKey;
6use codex_agent_identity::authorization_header_for_agent_task;
7use codex_api::AgentIdentityTelemetry;
8use codex_api::AuthProvider;
9use codex_api::SharedAuthProvider;
10use codex_login::AuthHeaders;
11use codex_login::AuthManager;
12use codex_login::CodexAuth;
13use codex_login::auth::AgentIdentityAuth;
14use codex_login::auth::AgentIdentityAuthError;
15use codex_login::auth::AgentIdentityAuthPolicy;
16use codex_model_provider_info::ModelProviderInfo;
17use codex_protocol::error::CodexErr;
18use codex_protocol::protocol::SessionSource;
19use http::HeaderMap;
20use http::HeaderValue;
21
22use crate::bearer_auth_provider::BearerAuthProvider;
23
24const BEDROCK_API_KEY_UNSUPPORTED_MESSAGE: &str =
25 "Bedrock API key auth is only supported by the Amazon Bedrock model provider";
26
27#[derive(Clone, Debug)]
28pub struct ProviderAuthScope {
29 pub agent_identity_policy: AgentIdentityAuthPolicy,
30 pub session_source: SessionSource,
31 pub agent_identity_session_fallback: AgentIdentitySessionFallback,
32}
33
34#[derive(Clone, Debug, Default)]
35pub struct AgentIdentitySessionFallback {
36 engaged: Arc<AtomicBool>,
37}
38
39impl AgentIdentitySessionFallback {
40 pub fn is_engaged(&self) -> bool {
41 self.engaged.load(Ordering::Relaxed)
42 }
43
44 fn engage(&self) -> bool {
45 !self.engaged.swap(true, Ordering::Relaxed)
46 }
47}
48
49#[derive(Clone)]
51pub struct ResolvedProviderAuth {
52 pub auth: SharedAuthProvider,
53 pub agent_identity_telemetry: Option<AgentIdentityTelemetry>,
54}
55
56impl ResolvedProviderAuth {
57 pub(crate) fn new(auth: SharedAuthProvider) -> Self {
58 Self {
59 auth,
60 agent_identity_telemetry: None,
61 }
62 }
63
64 fn for_agent_identity(auth: AgentIdentityAuth) -> Self {
65 let agent_identity_telemetry = agent_identity_telemetry(&auth);
66 Self {
67 auth: Arc::new(AgentIdentityAuthProvider { auth }),
68 agent_identity_telemetry: Some(agent_identity_telemetry),
69 }
70 }
71}
72
73pub(crate) fn agent_identity_telemetry(auth: &AgentIdentityAuth) -> AgentIdentityTelemetry {
74 AgentIdentityTelemetry {
75 agent_id: auth.record().agent_runtime_id.clone(),
76 task_id: auth.run_task_id().to_string(),
77 }
78}
79
80#[derive(Clone, Debug)]
81struct AgentIdentityAuthProvider {
82 auth: AgentIdentityAuth,
83}
84
85impl AuthProvider for AgentIdentityAuthProvider {
86 fn add_auth_headers(&self, headers: &mut HeaderMap) {
87 let record = self.auth.record();
88 let header_value = authorization_header_for_agent_task(
89 AgentIdentityKey {
90 agent_runtime_id: &record.agent_runtime_id,
91 private_key_pkcs8_base64: &record.agent_private_key,
92 },
93 self.auth.run_task_id(),
94 )
95 .map_err(std::io::Error::other);
96
97 if let Ok(header_value) = header_value
98 && let Ok(header) = HeaderValue::from_str(&header_value)
99 {
100 let _ = headers.insert(http::header::AUTHORIZATION, header);
101 }
102
103 if let Ok(header) = HeaderValue::from_str(self.auth.account_id()) {
104 let _ = headers.insert("ChatGPT-Account-ID", header);
105 }
106
107 if self.auth.is_fedramp_account() {
108 let _ = headers.insert("X-OpenAI-Fedramp", HeaderValue::from_static("true"));
109 }
110 }
111}
112
113#[derive(Clone, Debug)]
114struct HeaderAuthProvider {
115 auth: AuthHeaders,
116}
117
118impl AuthProvider for HeaderAuthProvider {
119 fn add_auth_headers(&self, headers: &mut HeaderMap) {
120 headers.extend(self.auth.headers().clone());
121 }
122}
123
124struct AuthManagerAuthProvider {
125 auth_manager: Arc<AuthManager>,
126 expected_auth: CodexAuth,
129}
130
131impl AuthProvider for AuthManagerAuthProvider {
132 fn add_auth_headers(&self, headers: &mut HeaderMap) {
133 let Some(auth) = self
134 .auth_manager
135 .auth_cached()
136 .filter(CodexAuth::uses_codex_backend)
137 else {
138 return;
139 };
140 if auth.get_account_id() != self.expected_auth.get_account_id()
144 || auth.get_chatgpt_user_id() != self.expected_auth.get_chatgpt_user_id()
145 || auth.is_workspace_account() != self.expected_auth.is_workspace_account()
146 {
147 return;
148 }
149 auth_provider_from_auth(&auth).add_auth_headers(headers);
150 }
151}
152
153#[derive(Clone, Debug)]
156struct UnauthenticatedAuthProvider;
157
158impl AuthProvider for UnauthenticatedAuthProvider {
159 fn add_auth_headers(&self, _headers: &mut HeaderMap) {}
160}
161
162pub fn unauthenticated_auth_provider() -> SharedAuthProvider {
163 Arc::new(UnauthenticatedAuthProvider)
164}
165
166pub(crate) fn auth_manager_for_provider(
170 auth_manager: Option<Arc<AuthManager>>,
171 provider: &ModelProviderInfo,
172) -> Option<Arc<AuthManager>> {
173 match provider.auth.clone() {
174 Some(config) => Some(AuthManager::external_bearer_only(config)),
175 None => auth_manager,
176 }
177}
178
179pub(crate) fn resolve_provider_auth(
180 auth: Option<&CodexAuth>,
181 provider: &ModelProviderInfo,
182) -> codex_protocol::error::Result<SharedAuthProvider> {
183 if matches!(auth, Some(CodexAuth::BedrockApiKey(_))) {
184 return Err(CodexErr::UnsupportedOperation(
185 BEDROCK_API_KEY_UNSUPPORTED_MESSAGE.to_string(),
186 ));
187 }
188
189 if let Some(auth) = bearer_auth_for_provider(provider)? {
190 return Ok(Arc::new(auth));
191 }
192
193 Ok(match auth {
194 Some(auth) => auth_provider_from_auth(auth),
195 None => unauthenticated_auth_provider(),
196 })
197}
198
199pub(crate) async fn resolve_provider_auth_for_scope(
200 auth_manager: Option<Arc<AuthManager>>,
201 auth: Option<&CodexAuth>,
202 provider: &ModelProviderInfo,
203 scope: ProviderAuthScope,
204) -> codex_protocol::error::Result<ResolvedProviderAuth> {
205 let ProviderAuthScope {
206 agent_identity_policy,
207 session_source,
208 agent_identity_session_fallback,
209 } = scope;
210 if let Some(CodexAuth::AgentIdentity(agent_identity_auth)) = auth {
211 return Ok(ResolvedProviderAuth::for_agent_identity(
212 agent_identity_auth.clone(),
213 ));
214 }
215
216 if !should_bootstrap_chatgpt_agent_identity(agent_identity_policy, auth)
217 || agent_identity_session_fallback.is_engaged()
218 {
219 return resolve_provider_auth(auth, provider).map(ResolvedProviderAuth::new);
220 }
221
222 let Some(auth_manager) = auth_manager else {
223 return resolve_provider_auth(auth, provider).map(ResolvedProviderAuth::new);
224 };
225
226 match auth_manager
227 .agent_identity_auth(agent_identity_policy, session_source)
228 .await
229 {
230 Ok(Some(agent_identity_auth)) => Ok(ResolvedProviderAuth::for_agent_identity(
231 agent_identity_auth,
232 )),
233 Ok(None) => resolve_provider_auth(auth, provider).map(ResolvedProviderAuth::new),
234 Err(err) => {
235 if let Some(AgentIdentityAuthError::BootstrapUnavailable {
236 operation,
237 attempts,
238 message,
239 }) = err
240 .get_ref()
241 .and_then(|source| source.downcast_ref::<AgentIdentityAuthError>())
242 {
243 let newly_engaged = agent_identity_session_fallback.engage();
244 tracing::warn!(
245 operation,
246 attempts = *attempts,
247 error = %message,
248 newly_engaged,
249 "agent identity bootstrap unavailable; using ChatGPT bearer auth for this session"
250 );
251 resolve_provider_auth(auth, provider).map(ResolvedProviderAuth::new)
252 } else {
253 Err(err.into())
254 }
255 }
256 }
257}
258
259fn should_bootstrap_chatgpt_agent_identity(
260 agent_identity_policy: AgentIdentityAuthPolicy,
261 auth: Option<&CodexAuth>,
262) -> bool {
263 agent_identity_policy == AgentIdentityAuthPolicy::ChatGptAuth
264 && matches!(auth, Some(CodexAuth::Chatgpt(_)))
265}
266
267fn bearer_auth_for_provider(
268 provider: &ModelProviderInfo,
269) -> codex_protocol::error::Result<Option<BearerAuthProvider>> {
270 if let Some(api_key) = provider.api_key()? {
271 return Ok(Some(BearerAuthProvider::new(api_key)));
272 }
273
274 if let Some(token) = provider.experimental_bearer_token.clone() {
275 return Ok(Some(BearerAuthProvider::new(token)));
276 }
277
278 Ok(None)
279}
280
281pub fn auth_provider_from_auth(auth: &CodexAuth) -> SharedAuthProvider {
283 match auth {
284 CodexAuth::AgentIdentity(auth) => {
285 Arc::new(AgentIdentityAuthProvider { auth: auth.clone() })
286 }
287 CodexAuth::Headers(auth) => Arc::new(HeaderAuthProvider { auth: auth.clone() }),
288 CodexAuth::BedrockApiKey(_) => unreachable!("{BEDROCK_API_KEY_UNSUPPORTED_MESSAGE}"),
289 CodexAuth::ApiKey(_)
290 | CodexAuth::Chatgpt(_)
291 | CodexAuth::ChatgptAuthTokens(_)
292 | CodexAuth::PersonalAccessToken(_) => Arc::new(BearerAuthProvider {
293 token: auth.get_token().ok(),
294 account_id: auth.get_account_id(),
295 is_fedramp_account: auth.is_fedramp_account(),
296 }),
297 }
298}
299
300pub fn auth_provider_from_auth_manager(
306 auth_manager: Arc<AuthManager>,
307 expected_auth: &CodexAuth,
308) -> SharedAuthProvider {
309 Arc::new(AuthManagerAuthProvider {
310 auth_manager,
311 expected_auth: expected_auth.clone(),
312 })
313}
314
315#[cfg(test)]
316mod tests {
317 use codex_agent_identity::generate_agent_key_material;
318 use codex_login::AuthCredentialsStoreMode;
319 use codex_login::AuthKeyringBackendKind;
320 use codex_login::auth::AgentIdentityAuthRecord;
321 use codex_login::auth::BedrockApiKeyAuth;
322 use codex_login::auth::login_with_chatgpt_auth_tokens;
323 use codex_model_provider_info::WireApi;
324 use codex_model_provider_info::create_oss_provider_with_base_url;
325 use codex_protocol::account::PlanType;
326 use http::header::AUTHORIZATION;
327 use pretty_assertions::assert_eq;
328 use serde_json::json;
329 use std::path::Path;
330 use std::path::PathBuf;
331 use std::sync::atomic::AtomicUsize;
332 use std::sync::atomic::Ordering;
333 use wiremock::Mock;
334 use wiremock::MockServer;
335 use wiremock::ResponseTemplate;
336 use wiremock::matchers::method;
337 use wiremock::matchers::path;
338
339 use super::*;
340
341 static NEXT_CODEX_HOME_ID: AtomicUsize = AtomicUsize::new(0);
342 const TEST_CHATGPT_ID_TOKEN: &str = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9hdXRoIjp7ImNoYXRncHRfdXNlcl9pZCI6InVzZXItMTIzNDUiLCJ1c2VyX2lkIjoidXNlci0xMjM0NSIsImNoYXRncHRfcGxhbl90eXBlIjoicHJvIiwiY2hhdGdwdF9hY2NvdW50X2lkIjoiYWNjb3VudC0xMjMifX0.c2ln";
343
344 async fn agent_identity_auth(chatgpt_account_is_fedramp: bool) -> AgentIdentityAuth {
345 let key_material = generate_agent_key_material().expect("generate key material");
346 AgentIdentityAuth::from_record(
347 AgentIdentityAuthRecord {
348 agent_runtime_id: "agent-runtime-1".to_string(),
349 agent_private_key: key_material.private_key_pkcs8_base64,
350 account_id: "account-1".to_string(),
351 chatgpt_user_id: "user-1".to_string(),
352 email: Some("agent@example.com".to_string()),
353 plan_type: PlanType::Plus,
354 chatgpt_account_is_fedramp,
355 task_id: Some("task-run-1".to_string()),
356 },
357 "https://auth.openai.com/api/accounts",
358 None,
359 )
360 .await
361 .expect("agent identity auth record should include task id")
362 }
363
364 fn provider_auth_scope(
365 policy: AgentIdentityAuthPolicy,
366 fallback: AgentIdentitySessionFallback,
367 ) -> ProviderAuthScope {
368 ProviderAuthScope {
369 agent_identity_policy: policy,
370 session_source: SessionSource::Cli,
371 agent_identity_session_fallback: fallback,
372 }
373 }
374
375 fn test_codex_home() -> PathBuf {
376 let id = NEXT_CODEX_HOME_ID.fetch_add(1, Ordering::Relaxed);
377 let path = std::env::temp_dir().join(format!(
378 "codex-model-provider-agent-identity-{pid}-{id}",
379 pid = std::process::id()
380 ));
381 let _ = std::fs::remove_dir_all(&path);
382 std::fs::create_dir_all(&path).expect("create temp codex home");
383 path
384 }
385
386 fn write_chatgpt_auth_json(codex_home: &Path) {
387 let auth_json = json!({
388 "tokens": {
389 "id_token": TEST_CHATGPT_ID_TOKEN,
390 "access_token": "test-access-token",
391 "refresh_token": "test-refresh-token",
392 "account_id": "account-123"
393 },
394 "last_refresh": "2099-01-01T00:00:00Z"
395 });
396 std::fs::write(
397 codex_home.join("auth.json"),
398 serde_json::to_string_pretty(&auth_json).expect("serialize auth.json"),
399 )
400 .expect("write auth.json");
401 }
402
403 async fn chatgpt_auth_manager(
404 agent_identity_authapi_base_url: String,
405 ) -> (PathBuf, Arc<AuthManager>, CodexAuth) {
406 let codex_home = test_codex_home();
407 write_chatgpt_auth_json(&codex_home);
408 let auth_manager = AuthManager::shared(
409 codex_home.clone(),
410 false,
411 AuthCredentialsStoreMode::File,
412 None,
413 None,
414 AuthKeyringBackendKind::default(),
415 codex_login::test_support::transport_default_auth_route_config(),
416 )
417 .await;
418 let auth = auth_manager.auth().await.expect("auth should load");
419 let auth_manager = AuthManager::from_auth_for_testing_with_agent_identity_authapi_base_url(
420 auth.clone(),
421 agent_identity_authapi_base_url,
422 );
423 (codex_home, auth_manager, auth)
424 }
425
426 async fn mount_transient_agent_registration(
427 server: &MockServer,
428 status: u16,
429 registration_count: Arc<AtomicUsize>,
430 ) {
431 Mock::given(method("POST"))
432 .and(path("/v1/agent/register"))
433 .respond_with(move |_request: &wiremock::Request| {
434 registration_count.fetch_add(1, Ordering::SeqCst);
435 ResponseTemplate::new(status)
436 })
437 .mount(server)
438 .await;
439 }
440
441 #[test]
442 fn unauthenticated_auth_provider_adds_no_headers() {
443 let provider =
444 create_oss_provider_with_base_url("http://localhost:11434/v1", WireApi::Responses);
445 let auth = resolve_provider_auth(None, &provider).expect("auth should resolve");
446
447 assert!(auth.to_auth_headers().is_empty());
448 }
449
450 #[test]
451 fn header_auth_adds_predefined_headers() {
452 let mut expected = HeaderMap::new();
453 expected.insert(
454 http::header::AUTHORIZATION,
455 HeaderValue::from_static("Bearer external"),
456 );
457 expected.insert("x-external-auth", HeaderValue::from_static("enabled"));
458 let auth = CodexAuth::Headers(AuthHeaders::new(expected.clone()));
459
460 let actual = auth_provider_from_auth(&auth).to_auth_headers();
461
462 assert_eq!(actual, expected);
463 }
464
465 #[test]
466 fn openai_provider_rejects_bedrock_api_key_auth() {
467 let provider = ModelProviderInfo::create_openai_provider(None);
468 let auth = CodexAuth::BedrockApiKey(BedrockApiKeyAuth {
469 api_key: "bedrock-api-key-test".to_string(),
470 region: "us-east-1".to_string(),
471 });
472
473 match resolve_provider_auth(Some(&auth), &provider) {
474 Err(CodexErr::UnsupportedOperation(message)) => {
475 assert_eq!(message, BEDROCK_API_KEY_UNSUPPORTED_MESSAGE);
476 }
477 Err(err) => panic!("unexpected auth error: {err:?}"),
478 Ok(_) => panic!("Bedrock API key auth should be rejected"),
479 }
480 }
481
482 #[tokio::test]
483 async fn auth_manager_provider_follows_refreshes_but_not_account_switches() {
484 let codex_home = test_codex_home();
485 login_with_chatgpt_auth_tokens(
486 &codex_home,
487 "header.e30.first",
488 "test-account",
489 None,
490 )
491 .expect("save initial auth");
492 let auth_manager = Arc::new(
493 AuthManager::new(
494 codex_home.clone(),
495 false,
496 AuthCredentialsStoreMode::Ephemeral,
497 None,
498 None,
499 AuthKeyringBackendKind::default(),
500 codex_login::test_support::transport_default_auth_route_config(),
501 )
502 .await,
503 );
504 let expected_auth = auth_manager
505 .auth_cached()
506 .expect("initial auth should be cached");
507 let provider = auth_provider_from_auth_manager(Arc::clone(&auth_manager), &expected_auth);
508
509 assert_eq!(
510 provider.to_auth_headers().get(AUTHORIZATION),
511 Some(&HeaderValue::from_static("Bearer header.e30.first"))
512 );
513
514 login_with_chatgpt_auth_tokens(
515 &codex_home,
516 "header.e30.reloaded",
517 "test-account",
518 None,
519 )
520 .expect("save reloaded auth");
521 auth_manager.reload().await;
522
523 assert_eq!(
524 provider.to_auth_headers().get(AUTHORIZATION),
525 Some(&HeaderValue::from_static("Bearer header.e30.reloaded"))
526 );
527
528 login_with_chatgpt_auth_tokens(
529 &codex_home,
530 "header.e30.other-account",
531 "other-account",
532 None,
533 )
534 .expect("save switched-account auth");
535 auth_manager.reload().await;
536
537 assert!(provider.to_auth_headers().is_empty());
538 }
539
540 #[tokio::test]
541 async fn first_party_run_scope_uses_agent_assertion_and_exposes_telemetry() {
542 let auth = CodexAuth::AgentIdentity(
543 agent_identity_auth(false).await,
544 );
545 let provider = ModelProviderInfo::create_openai_provider(None);
546
547 let auth = resolve_provider_auth_for_scope(
548 None,
549 Some(&auth),
550 &provider,
551 provider_auth_scope(
552 AgentIdentityAuthPolicy::JwtOnly,
553 AgentIdentitySessionFallback::default(),
554 ),
555 )
556 .await
557 .expect("auth should resolve");
558
559 assert_eq!(
560 auth.agent_identity_telemetry,
561 Some(AgentIdentityTelemetry {
562 agent_id: "agent-runtime-1".to_string(),
563 task_id: "task-run-1".to_string(),
564 })
565 );
566 let headers = auth.auth.to_auth_headers();
567 assert!(
568 headers
569 .get(http::header::AUTHORIZATION)
570 .and_then(|value| value.to_str().ok())
571 .is_some_and(|value| value.starts_with("AgentAssertion "))
572 );
573 }
574
575 #[tokio::test]
576 async fn agent_identity_auth_provider_preserves_account_routing_headers() {
577 let auth = agent_identity_auth(true).await;
578 let provider = auth_provider_from_auth(&CodexAuth::AgentIdentity(auth));
579
580 let headers = provider.to_auth_headers();
581
582 assert!(
583 headers
584 .get(http::header::AUTHORIZATION)
585 .and_then(|value| value.to_str().ok())
586 .is_some_and(|value| value.starts_with("AgentAssertion "))
587 );
588 assert_eq!(
589 headers
590 .get("ChatGPT-Account-ID")
591 .and_then(|value| value.to_str().ok()),
592 Some("account-1")
593 );
594 assert_eq!(
595 headers
596 .get("X-OpenAI-Fedramp")
597 .and_then(|value| value.to_str().ok()),
598 Some("true")
599 );
600 }
601
602 #[tokio::test]
603 async fn chatgpt_bootstrap_unavailable_uses_session_bearer_fallback() {
604 let server = MockServer::start().await;
605 let registration_count = Arc::new(AtomicUsize::new(0));
606 mount_transient_agent_registration(
607 &server,
608 503,
609 Arc::clone(®istration_count),
610 )
611 .await;
612 let (_codex_home, auth_manager, auth) = chatgpt_auth_manager(server.uri()).await;
613 let provider = ModelProviderInfo::create_openai_provider(None);
614 let fallback = AgentIdentitySessionFallback::default();
615
616 let provider_auth = resolve_provider_auth_for_scope(
617 Some(auth_manager),
618 Some(&auth),
619 &provider,
620 provider_auth_scope(AgentIdentityAuthPolicy::ChatGptAuth, fallback.clone()),
621 )
622 .await
623 .expect("fallback should resolve bearer auth");
624
625 let headers = provider_auth.auth.to_auth_headers();
626 assert_eq!(
627 headers
628 .get(http::header::AUTHORIZATION)
629 .and_then(|value| value.to_str().ok()),
630 Some("Bearer test-access-token")
631 );
632 assert_eq!(
633 headers
634 .get("ChatGPT-Account-ID")
635 .and_then(|value| value.to_str().ok()),
636 Some("account-123")
637 );
638 assert!(fallback.is_engaged());
639 assert_eq!(registration_count.load(Ordering::SeqCst), 3);
640 }
641
642 #[tokio::test]
643 async fn chatgpt_session_fallback_skips_later_agent_identity_bootstrap() {
644 let server = MockServer::start().await;
645 let registration_count = Arc::new(AtomicUsize::new(0));
646 mount_transient_agent_registration(
647 &server,
648 503,
649 Arc::clone(®istration_count),
650 )
651 .await;
652 let (_codex_home, auth_manager, auth) = chatgpt_auth_manager(server.uri()).await;
653 let provider = ModelProviderInfo::create_openai_provider(None);
654 let fallback = AgentIdentitySessionFallback::default();
655
656 resolve_provider_auth_for_scope(
657 Some(Arc::clone(&auth_manager)),
658 Some(&auth),
659 &provider,
660 provider_auth_scope(AgentIdentityAuthPolicy::ChatGptAuth, fallback.clone()),
661 )
662 .await
663 .expect("first fallback should resolve bearer auth");
664 resolve_provider_auth_for_scope(
665 Some(auth_manager),
666 Some(&auth),
667 &provider,
668 provider_auth_scope(AgentIdentityAuthPolicy::ChatGptAuth, fallback),
669 )
670 .await
671 .expect("second fallback should resolve bearer auth");
672
673 assert_eq!(registration_count.load(Ordering::SeqCst), 3);
674 }
675
676 #[tokio::test]
677 async fn chatgpt_sessions_share_bootstrap_failure_cooldown() {
678 let server = MockServer::start().await;
679 let registration_count = Arc::new(AtomicUsize::new(0));
680 mount_transient_agent_registration(
681 &server,
682 503,
683 Arc::clone(®istration_count),
684 )
685 .await;
686 let (_codex_home, auth_manager, auth) = chatgpt_auth_manager(server.uri()).await;
687 let provider = ModelProviderInfo::create_openai_provider(None);
688 let first_fallback = AgentIdentitySessionFallback::default();
689 let second_fallback = AgentIdentitySessionFallback::default();
690
691 resolve_provider_auth_for_scope(
692 Some(Arc::clone(&auth_manager)),
693 Some(&auth),
694 &provider,
695 provider_auth_scope(AgentIdentityAuthPolicy::ChatGptAuth, first_fallback.clone()),
696 )
697 .await
698 .expect("first session fallback should resolve bearer auth");
699 resolve_provider_auth_for_scope(
700 Some(auth_manager),
701 Some(&auth),
702 &provider,
703 provider_auth_scope(
704 AgentIdentityAuthPolicy::ChatGptAuth,
705 second_fallback.clone(),
706 ),
707 )
708 .await
709 .expect("second session fallback should resolve bearer auth");
710
711 assert!(first_fallback.is_engaged());
712 assert!(second_fallback.is_engaged());
713 assert_eq!(registration_count.load(Ordering::SeqCst), 3);
714 }
715}