11use acp:: schema:: {
2- AgentAuthCapabilities , AgentCapabilities , AuthEnvVar , AuthMethod , AuthMethodAgent ,
3- AuthMethodEnvVar , AuthMethodId , AuthenticateRequest , AuthenticateResponse , CancelNotification ,
4- ClientCapabilities , CloseSessionRequest , CloseSessionResponse , Implementation ,
5- InitializeRequest , InitializeResponse , ListSessionsRequest , ListSessionsResponse ,
6- LoadSessionRequest , LoadSessionResponse , LogoutCapabilities , LogoutRequest , LogoutResponse ,
7- McpCapabilities , McpServer , McpServerHttp , McpServerStdio , NewSessionRequest ,
8- NewSessionResponse , PromptCapabilities , PromptRequest , PromptResponse , ProtocolVersion ,
2+ AgentAuthCapabilities , AgentCapabilities , AgentRequest , AuthEnvVar , AuthMethod ,
3+ AuthMethodAgent , AuthMethodEnvVar , AuthMethodId , AuthenticateRequest , AuthenticateResponse ,
4+ CancelNotification , ClientCapabilities , CloseSessionRequest , CloseSessionResponse ,
5+ CreateElicitationRequest , CreateElicitationResponse , ElicitationAction ,
6+ ElicitationRequestScope , ElicitationUrlMode , Implementation , InitializeRequest ,
7+ InitializeResponse , ListSessionsRequest , ListSessionsResponse , LoadSessionRequest ,
8+ LoadSessionResponse , LogoutCapabilities , LogoutRequest , LogoutResponse , McpCapabilities ,
9+ McpServer , McpServerHttp , McpServerStdio , NewSessionRequest , NewSessionResponse ,
10+ PromptCapabilities , PromptRequest , PromptResponse , ProtocolVersion , RequestId ,
911 SessionCapabilities , SessionCloseCapabilities , SessionId , SessionInfo , SessionListCapabilities ,
1012 SetSessionConfigOptionRequest , SetSessionConfigOptionResponse , SetSessionModeRequest ,
1113 SetSessionModeResponse , SetSessionModelRequest , SetSessionModelResponse ,
1214} ;
13- use acp:: { Agent , Client , ConnectTo , ConnectionTo , Error } ;
15+ use acp:: { Agent , Client , ConnectTo , ConnectionTo , Error , Responder } ;
1416use agent_client_protocol as acp;
1517use codex_config:: { McpServerConfig , McpServerTransportConfig } ;
1618use codex_core:: {
@@ -32,7 +34,7 @@ use std::{
3234 path:: { Path , PathBuf } ,
3335 sync:: { Arc , Mutex } ,
3436} ;
35- use tracing:: { debug, info} ;
37+ use tracing:: { debug, error , info} ;
3638use unicode_segmentation:: UnicodeSegmentation ;
3739
3840use crate :: thread:: Thread ;
@@ -130,11 +132,16 @@ impl CodexAgent {
130132 {
131133 let agent = agent. clone ( ) ;
132134 async move |request : AuthenticateRequest ,
133- responder,
135+ responder : Responder < AuthenticateResponse > ,
134136 cx : ConnectionTo < Client > | {
135137 let agent = agent. clone ( ) ;
138+ let auth_cx = cx. clone ( ) ;
139+ let request_id = request_id_from_json ( responder. id ( ) )
140+ . expect ( "request id must be a string or number" ) ;
136141 cx. spawn ( async move {
137- responder. respond_with_result ( agent. authenticate ( request) . await )
142+ responder. respond_with_result (
143+ agent. authenticate ( request, auth_cx, request_id) . await ,
144+ )
138145 } ) ?;
139146 Ok ( ( ) )
140147 }
@@ -234,7 +241,7 @@ impl CodexAgent {
234241 let agent = agent. clone ( ) ;
235242 cx. spawn ( async move {
236243 if let Err ( e) = agent. cancel ( notification) . await {
237- tracing :: error!( "Error handling cancel: {:?}" , e) ;
244+ error ! ( "Error handling cancel: {:?}" , e) ;
238245 }
239246 Ok ( ( ) )
240247 } ) ?;
@@ -433,6 +440,11 @@ impl CodexAgent {
433440 debug ! ( "Received initialize request with protocol version {protocol_version:?}" , ) ;
434441 let protocol_version = ProtocolVersion :: V1 ;
435442
443+ let client_supports_url_elicitation = client_capabilities
444+ . elicitation
445+ . as_ref ( )
446+ . is_some_and ( |elicitation| elicitation. url . is_some ( ) ) ;
447+
436448 * self . client_capabilities . lock ( ) . unwrap ( ) = client_capabilities;
437449
438450 let mut agent_capabilities = AgentCapabilities :: new ( )
@@ -450,8 +462,8 @@ impl CodexAgent {
450462 CodexAuthMethod :: CodexApiKey . into( ) ,
451463 CodexAuthMethod :: OpenAiApiKey . into( ) ,
452464 ] ;
453- // Until codex device code auth works, we can't use this in remote ssh projects
454- if std:: env:: var ( "NO_BROWSER" ) . is_ok ( ) {
465+
466+ if std:: env:: var ( "NO_BROWSER" ) . is_ok ( ) && !client_supports_url_elicitation {
455467 auth_methods. remove ( 0 ) ;
456468 }
457469
@@ -464,6 +476,8 @@ impl CodexAgent {
464476 async fn authenticate (
465477 & self ,
466478 request : AuthenticateRequest ,
479+ cx : ConnectionTo < Client > ,
480+ request_id : RequestId ,
467481 ) -> Result < AuthenticateResponse , Error > {
468482 let auth_method = CodexAuthMethod :: try_from ( request. method_id ) ?;
469483
@@ -483,21 +497,25 @@ impl CodexAgent {
483497
484498 match auth_method {
485499 CodexAuthMethod :: ChatGpt => {
486- // Perform browser/device login via codex-rs, then report success/failure to the client.
487500 let opts = codex_login:: ServerOptions :: new (
488501 self . config . codex_home . to_path_buf ( ) ,
489502 codex_login:: auth:: CLIENT_ID . to_string ( ) ,
490503 None ,
491504 self . config . cli_auth_credentials_store_mode ,
492505 ) ;
493506
494- let server =
495- codex_login:: run_login_server ( opts) . map_err ( Error :: into_internal_error) ?;
496-
497- server
498- . block_until_done ( )
499- . await
500- . map_err ( Error :: into_internal_error) ?;
507+ if std:: env:: var ( "NO_BROWSER" ) . is_ok ( ) {
508+ Self :: device_code_auth ( opts, cx, request_id) . await ?;
509+ } else {
510+ // Perform browser/device login via codex-rs, then report success/failure to the client.
511+ let server =
512+ codex_login:: run_login_server ( opts) . map_err ( Error :: into_internal_error) ?;
513+
514+ server
515+ . block_until_done ( )
516+ . await
517+ . map_err ( Error :: into_internal_error) ?;
518+ }
501519 }
502520 CodexAuthMethod :: CodexApiKey => {
503521 let api_key = read_codex_api_key_from_env ( ) . ok_or_else ( || {
@@ -528,6 +546,51 @@ impl CodexAgent {
528546 Ok ( AuthenticateResponse :: new ( ) )
529547 }
530548
549+ async fn device_code_auth (
550+ opts : codex_login:: ServerOptions ,
551+ cx : ConnectionTo < Client > ,
552+ request_id : RequestId ,
553+ ) -> Result < ( ) , Error > {
554+ let device_code = codex_login:: request_device_code ( & opts)
555+ . await
556+ . map_err ( Error :: into_internal_error) ?;
557+
558+ let url = device_code. verification_url . clone ( ) ;
559+ let user_code = device_code. user_code . clone ( ) ;
560+ let message = format ! (
561+ "Follow these steps to sign in with ChatGPT using device code authorization:\n \
562+ \n 1. Open this link in your browser and sign in to your account\n {url}\n \
563+ \n 2. Enter this one-time code (expires in 15 minutes)\n {user_code}\n \
564+ \n Device codes are a common phishing target. Never share this code.\n "
565+ ) ;
566+
567+ let elicitation_id = format ! ( "chatgpt-login-{}" , uuid:: Uuid :: new_v4( ) ) ;
568+ let response = cx
569+ . send_request ( AgentRequest :: CreateElicitationRequest (
570+ CreateElicitationRequest :: new (
571+ ElicitationUrlMode :: new (
572+ ElicitationRequestScope :: new ( request_id) ,
573+ elicitation_id,
574+ url,
575+ ) ,
576+ message,
577+ ) ,
578+ ) )
579+ . block_task ( )
580+ . await ?;
581+
582+ let response: CreateElicitationResponse =
583+ serde_json:: from_value ( response) . map_err ( Error :: into_internal_error) ?;
584+ if !matches ! ( response. action, ElicitationAction :: Accept ( _) ) {
585+ return Err ( Error :: auth_required ( ) ) ;
586+ }
587+
588+ codex_login:: complete_device_code_login ( opts, device_code)
589+ . await
590+ . map_err ( Error :: into_internal_error) ?;
591+ Ok ( ( ) )
592+ }
593+
531594 async fn logout ( & self , _request : LogoutRequest ) -> Result < LogoutResponse , Error > {
532595 self . auth_manager
533596 . logout ( )
@@ -873,6 +936,14 @@ impl TryFrom<AuthMethodId> for CodexAuthMethod {
873936 }
874937}
875938
939+ fn request_id_from_json ( id : serde_json:: Value ) -> Option < RequestId > {
940+ match id {
941+ serde_json:: Value :: String ( id) => Some ( id. into ( ) ) ,
942+ serde_json:: Value :: Number ( id) => id. as_u64 ( ) . map ( Into :: into) ,
943+ _ => None ,
944+ }
945+ }
946+
876947fn truncate_graphemes ( text : & str , max_graphemes : usize ) -> String {
877948 let mut graphemes = text. grapheme_indices ( true ) ;
878949
0 commit comments