@@ -8,10 +8,12 @@ use codex_protocol::account::PlanType as AccountPlanType;
88
99use base64:: Engine ;
1010use codex_protocol:: config_types:: ForcedLoginMethod ;
11+ use codex_protocol:: config_types:: ModelProviderAuthInfo ;
1112use pretty_assertions:: assert_eq;
1213use serde:: Serialize ;
1314use serde_json:: json;
1415use std:: sync:: Arc ;
16+ use tempfile:: TempDir ;
1517use tempfile:: tempdir;
1618
1719#[ tokio:: test]
@@ -265,6 +267,180 @@ fn external_auth_tokens_without_chatgpt_metadata_cannot_seed_chatgpt_auth() {
265267 ) ;
266268}
267269
270+ #[ tokio:: test]
271+ async fn external_bearer_only_auth_manager_uses_cached_provider_token ( ) {
272+ let script = ProviderAuthScript :: new ( & [ "provider-token" , "next-token" ] ) . unwrap ( ) ;
273+ let manager = AuthManager :: external_bearer_only ( script. auth_config ( ) ) ;
274+
275+ let first = manager
276+ . auth ( )
277+ . await
278+ . and_then ( |auth| auth. api_key ( ) . map ( str:: to_string) ) ;
279+ let second = manager
280+ . auth ( )
281+ . await
282+ . and_then ( |auth| auth. api_key ( ) . map ( str:: to_string) ) ;
283+
284+ assert_eq ! ( first. as_deref( ) , Some ( "provider-token" ) ) ;
285+ assert_eq ! ( second. as_deref( ) , Some ( "provider-token" ) ) ;
286+ }
287+
288+ #[ tokio:: test]
289+ async fn external_bearer_only_auth_manager_returns_none_when_command_fails ( ) {
290+ let script = ProviderAuthScript :: new_failing ( ) . unwrap ( ) ;
291+ let manager = AuthManager :: external_bearer_only ( script. auth_config ( ) ) ;
292+
293+ assert_eq ! ( manager. auth( ) . await , None ) ;
294+ }
295+
296+ #[ tokio:: test]
297+ async fn unauthorized_recovery_uses_external_refresh_for_bearer_manager ( ) {
298+ let script = ProviderAuthScript :: new ( & [ "provider-token" , "refreshed-provider-token" ] ) . unwrap ( ) ;
299+ let manager = AuthManager :: external_bearer_only ( script. auth_config ( ) ) ;
300+ let initial_token = manager
301+ . auth ( )
302+ . await
303+ . and_then ( |auth| auth. api_key ( ) . map ( str:: to_string) ) ;
304+ let mut recovery = manager. unauthorized_recovery ( ) ;
305+
306+ assert ! ( recovery. has_next( ) ) ;
307+ assert_eq ! ( recovery. mode_name( ) , "external" ) ;
308+ assert_eq ! ( recovery. step_name( ) , "external_refresh" ) ;
309+
310+ let result = recovery
311+ . next ( )
312+ . await
313+ . expect ( "external refresh should succeed" ) ;
314+
315+ assert_eq ! ( result. auth_state_changed( ) , Some ( true ) ) ;
316+ let refreshed_token = manager
317+ . auth ( )
318+ . await
319+ . and_then ( |auth| auth. api_key ( ) . map ( str:: to_string) ) ;
320+ assert_eq ! ( initial_token. as_deref( ) , Some ( "provider-token" ) ) ;
321+ assert_eq ! ( refreshed_token. as_deref( ) , Some ( "refreshed-provider-token" ) ) ;
322+ }
323+
324+ struct ProviderAuthScript {
325+ tempdir : TempDir ,
326+ command : String ,
327+ args : Vec < String > ,
328+ }
329+
330+ impl ProviderAuthScript {
331+ fn new ( tokens : & [ & str ] ) -> std:: io:: Result < Self > {
332+ let tempdir = tempfile:: tempdir ( ) ?;
333+ let token_file = tempdir. path ( ) . join ( "tokens.txt" ) ;
334+ let mut token_file_contents = String :: new ( ) ;
335+ for token in tokens {
336+ token_file_contents. push_str ( token) ;
337+ token_file_contents. push ( '\n' ) ;
338+ }
339+ std:: fs:: write ( & token_file, token_file_contents) ?;
340+
341+ #[ cfg( unix) ]
342+ let ( command, args) = {
343+ let script_path = tempdir. path ( ) . join ( "print-token.sh" ) ;
344+ std:: fs:: write (
345+ & script_path,
346+ r#"#!/bin/sh
347+ first_line=$(sed -n '1p' tokens.txt)
348+ printf '%s\n' "$first_line"
349+ tail -n +2 tokens.txt > tokens.next
350+ mv tokens.next tokens.txt
351+ "# ,
352+ ) ?;
353+ let mut permissions = std:: fs:: metadata ( & script_path) ?. permissions ( ) ;
354+ {
355+ use std:: os:: unix:: fs:: PermissionsExt ;
356+ permissions. set_mode ( 0o755 ) ;
357+ }
358+ std:: fs:: set_permissions ( & script_path, permissions) ?;
359+ ( "./print-token.sh" . to_string ( ) , Vec :: new ( ) )
360+ } ;
361+
362+ #[ cfg( windows) ]
363+ let ( command, args) = {
364+ let script_path = tempdir. path ( ) . join ( "print-token.ps1" ) ;
365+ std:: fs:: write (
366+ & script_path,
367+ r#"$lines = Get-Content -Path tokens.txt
368+ if ($lines.Count -eq 0) { exit 1 }
369+ Write-Output $lines[0]
370+ $lines | Select-Object -Skip 1 | Set-Content -Path tokens.txt
371+ "# ,
372+ ) ?;
373+ (
374+ "powershell" . to_string ( ) ,
375+ vec ! [
376+ "-NoProfile" . to_string( ) ,
377+ "-ExecutionPolicy" . to_string( ) ,
378+ "Bypass" . to_string( ) ,
379+ "-File" . to_string( ) ,
380+ ".\\ print-token.ps1" . to_string( ) ,
381+ ] ,
382+ )
383+ } ;
384+
385+ Ok ( Self {
386+ tempdir,
387+ command,
388+ args,
389+ } )
390+ }
391+
392+ fn new_failing ( ) -> std:: io:: Result < Self > {
393+ let tempdir = tempfile:: tempdir ( ) ?;
394+
395+ #[ cfg( unix) ]
396+ let ( command, args) = {
397+ let script_path = tempdir. path ( ) . join ( "fail.sh" ) ;
398+ std:: fs:: write (
399+ & script_path,
400+ r#"#!/bin/sh
401+ exit 1
402+ "# ,
403+ ) ?;
404+ let mut permissions = std:: fs:: metadata ( & script_path) ?. permissions ( ) ;
405+ {
406+ use std:: os:: unix:: fs:: PermissionsExt ;
407+ permissions. set_mode ( 0o755 ) ;
408+ }
409+ std:: fs:: set_permissions ( & script_path, permissions) ?;
410+ ( "./fail.sh" . to_string ( ) , Vec :: new ( ) )
411+ } ;
412+
413+ #[ cfg( windows) ]
414+ let ( command, args) = (
415+ "powershell" . to_string ( ) ,
416+ vec ! [
417+ "-NoProfile" . to_string( ) ,
418+ "-ExecutionPolicy" . to_string( ) ,
419+ "Bypass" . to_string( ) ,
420+ "-Command" . to_string( ) ,
421+ "exit 1" . to_string( ) ,
422+ ] ,
423+ ) ;
424+
425+ Ok ( Self {
426+ tempdir,
427+ command,
428+ args,
429+ } )
430+ }
431+
432+ fn auth_config ( & self ) -> ModelProviderAuthInfo {
433+ serde_json:: from_value ( json ! ( {
434+ "command" : self . command,
435+ "args" : self . args,
436+ "timeout_ms" : 1000 ,
437+ "refresh_interval_ms" : 60000 ,
438+ "cwd" : self . tempdir. path( ) ,
439+ } ) )
440+ . expect ( "provider auth config should deserialize" )
441+ }
442+ }
443+
268444struct AuthFileParams {
269445 openai_api_key : Option < String > ,
270446 chatgpt_plan_type : Option < String > ,
0 commit comments