diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index cd35e33f68..cf10f6e4f2 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -19,6 +19,16 @@ "null" ] }, + "onboardingEntrypoint": { + "anyOf": [ + { + "$ref": "#/definitions/DesktopOnboardingEntrypoint" + }, + { + "type": "null" + } + ] + }, "success": { "type": "boolean" } @@ -1082,6 +1092,12 @@ ], "type": "object" }, + "DesktopOnboardingEntrypoint": { + "enum": [ + "life_sciences" + ], + "type": "string" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 3d2546ce29..cc7e3781dc 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -6220,6 +6220,16 @@ "null" ] }, + "onboardingEntrypoint": { + "anyOf": [ + { + "$ref": "#/definitions/v2/DesktopOnboardingEntrypoint" + }, + { + "type": "null" + } + ] + }, "success": { "type": "boolean" } @@ -9395,6 +9405,12 @@ "title": "DeprecationNoticeNotification", "type": "object" }, + "DesktopOnboardingEntrypoint": { + "enum": [ + "life_sciences" + ], + "type": "string" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index a21d72b766..ec948b4080 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -87,6 +87,16 @@ "null" ] }, + "onboardingEntrypoint": { + "anyOf": [ + { + "$ref": "#/definitions/DesktopOnboardingEntrypoint" + }, + { + "type": "null" + } + ] + }, "success": { "type": "boolean" } @@ -5568,6 +5578,12 @@ "title": "DeprecationNoticeNotification", "type": "object" }, + "DesktopOnboardingEntrypoint": { + "enum": [ + "life_sciences" + ], + "type": "string" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/AccountLoginCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/AccountLoginCompletedNotification.json index 128cb643ab..2543d929d2 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/AccountLoginCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/AccountLoginCompletedNotification.json @@ -1,5 +1,13 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "DesktopOnboardingEntrypoint": { + "enum": [ + "life_sciences" + ], + "type": "string" + } + }, "properties": { "error": { "type": [ @@ -13,6 +21,16 @@ "null" ] }, + "onboardingEntrypoint": { + "anyOf": [ + { + "$ref": "#/definitions/DesktopOnboardingEntrypoint" + }, + { + "type": "null" + } + ] + }, "success": { "type": "boolean" } diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 8f94aec386..45fd35e344 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index d4dcd693c4..a0a0fccc51 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AccountLoginCompletedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AccountLoginCompletedNotification.ts index 587237b275..65e251eb3e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AccountLoginCompletedNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AccountLoginCompletedNotification.ts @@ -1,5 +1,6 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DesktopOnboardingEntrypoint } from "./DesktopOnboardingEntrypoint"; -export type AccountLoginCompletedNotification = { loginId: string | null, success: boolean, error: string | null, }; +export type AccountLoginCompletedNotification = { loginId: string | null, success: boolean, error: string | null, onboardingEntrypoint: DesktopOnboardingEntrypoint | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/DesktopOnboardingEntrypoint.ts b/codex-rs/app-server-protocol/schema/typescript/v2/DesktopOnboardingEntrypoint.ts new file mode 100644 index 0000000000..75a73210dd --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/DesktopOnboardingEntrypoint.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DesktopOnboardingEntrypoint = "life_sciences"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 266bc1391b..7f5536853b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -98,6 +98,7 @@ export type { ConsumeAccountRateLimitResetCreditResponse } from "./ConsumeAccoun export type { ContextCompactedNotification } from "./ContextCompactedNotification"; export type { CreditsSnapshot } from "./CreditsSnapshot"; export type { DeprecationNoticeNotification } from "./DeprecationNoticeNotification"; +export type { DesktopOnboardingEntrypoint } from "./DesktopOnboardingEntrypoint"; export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; export type { DynamicToolCallParams } from "./DynamicToolCallParams"; export type { DynamicToolCallResponse } from "./DynamicToolCallResponse"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/account.rs b/codex-rs/app-server-protocol/src/protocol/v2/account.rs index 6b4e78e000..bee574e520 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/account.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/account.rs @@ -672,4 +672,13 @@ pub struct AccountLoginCompletedNotification { pub login_id: Option, pub success: bool, pub error: Option, + pub onboarding_entrypoint: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(rename_all = "snake_case")] +#[ts(export_to = "v2/")] +pub enum DesktopOnboardingEntrypoint { + LifeSciences, } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index c6822d8cd8..c5d16b5661 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -2213,9 +2213,11 @@ Field notes: page artwork; omitted or `null` values default to `"codex"`. 3. Wait for notifications: ```json - { "method": "account/login/completed", "params": { "loginId": "", "success": true, "error": null } } + { "method": "account/login/completed", "params": { "loginId": "", "success": true, "error": null, "onboardingEntrypoint": "life_sciences" } } { "method": "account/updated", "params": { "authMode": "chatgpt", "planType": "plus" } } ``` + `onboardingEntrypoint` is optional and is only emitted when the OAuth callback carries a + recognized onboarding hint. ### 3) Log in with an Amazon Bedrock API key diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs index be22cbe314..2020ef2243 100644 --- a/codex-rs/app-server/src/outgoing_message.rs +++ b/codex-rs/app-server/src/outgoing_message.rs @@ -760,6 +760,7 @@ mod tests { login_id: Some(Uuid::nil().to_string()), success: true, error: None, + onboarding_entrypoint: None, }); let jsonrpc_notification = @@ -774,6 +775,7 @@ mod tests { "loginId": Uuid::nil().to_string(), "success": true, "error": null, + "onboardingEntrypoint": null, }, "emittedAtMs": 1_234, }), @@ -790,6 +792,7 @@ mod tests { login_id: Some(Uuid::nil().to_string()), success: true, error: None, + onboarding_entrypoint: None, }); assert_eq!( @@ -799,6 +802,7 @@ mod tests { "loginId": Uuid::nil().to_string(), "success": true, "error": null, + "onboardingEntrypoint": null, }, }), serde_json::to_value(notification) diff --git a/codex-rs/app-server/src/request_processors/account_processor.rs b/codex-rs/app-server/src/request_processors/account_processor.rs index add390fed7..815a6ee7bd 100644 --- a/codex-rs/app-server/src/request_processors/account_processor.rs +++ b/codex-rs/app-server/src/request_processors/account_processor.rs @@ -4,6 +4,8 @@ use super::*; use crate::auth_mode::auth_mode_to_api; use crate::external_auth::ExternalAuthBridge; use chrono::DateTime; +use codex_app_server_protocol::DesktopOnboardingEntrypoint; +use codex_login::LoginOnboardingEntrypoint; use codex_model_provider::is_supported_amazon_bedrock_region; mod rate_limit_resets; @@ -555,17 +557,25 @@ impl AccountRequestProcessor { let active_login = self.active_login.clone(); let auth_url = server.auth_url.clone(); tokio::spawn(async move { - let (success, error_msg) = match tokio::time::timeout( + let (success, error_msg, onboarding_entrypoint) = match tokio::time::timeout( LOGIN_CHATGPT_TIMEOUT, - server.block_until_done(), + server.block_until_done_with_callback_result(), ) .await { - Ok(Ok(())) => (true, None), - Ok(Err(err)) => (false, Some(format!("Login server error: {err}"))), + Ok(Ok(result)) => ( + true, + None, + result + .onboarding_entrypoint + .map(|LoginOnboardingEntrypoint::LifeSciences| { + DesktopOnboardingEntrypoint::LifeSciences + }), + ), + Ok(Err(err)) => (false, Some(format!("Login server error: {err}")), None), Err(_elapsed) => { shutdown_handle.shutdown(); - (false, Some("Login timed out".to_string())) + (false, Some("Login timed out".to_string()), None) } }; @@ -574,9 +584,12 @@ impl AccountRequestProcessor { config_manager, thread_manager, config, - login_id, - success, - error_msg, + AccountLoginCompletedNotification { + login_id: Some(login_id.to_string()), + success, + error: error_msg, + onboarding_entrypoint, + }, ) .await; @@ -650,9 +663,12 @@ impl AccountRequestProcessor { config_manager, thread_manager, config, - login_id, - success, - error_msg, + AccountLoginCompletedNotification { + login_id: Some(login_id.to_string()), + success, + error: error_msg, + onboarding_entrypoint: None, + }, ) .await; @@ -785,6 +801,7 @@ impl AccountRequestProcessor { login_id: login_id.map(|id| id.to_string()), success: true, error: None, + onboarding_entrypoint: None, }; self.outgoing .send_server_notification(ServerNotification::AccountLoginCompleted( @@ -804,15 +821,9 @@ impl AccountRequestProcessor { config_manager: ConfigManager, thread_manager: Arc, config: Arc, - login_id: Uuid, - success: bool, - error_msg: Option, + payload_v2: AccountLoginCompletedNotification, ) { - let payload_v2 = AccountLoginCompletedNotification { - login_id: Some(login_id.to_string()), - success, - error: error_msg, - }; + let success = payload_v2.success; outgoing .send_server_notification(ServerNotification::AccountLoginCompleted(payload_v2)) .await; diff --git a/codex-rs/app-server/tests/suite/v2/account.rs b/codex-rs/app-server/tests/suite/v2/account.rs index 8ed1791ae5..e72461513f 100644 --- a/codex-rs/app-server/tests/suite/v2/account.rs +++ b/codex-rs/app-server/tests/suite/v2/account.rs @@ -21,6 +21,7 @@ use codex_app_server_protocol::CancelLoginAccountStatus; use codex_app_server_protocol::ChatgptAuthTokensRefreshReason; use codex_app_server_protocol::ChatgptAuthTokensRefreshResponse; use codex_app_server_protocol::ClientInfo; +use codex_app_server_protocol::DesktopOnboardingEntrypoint; use codex_app_server_protocol::GetAccountParams; use codex_app_server_protocol::GetAccountResponse; use codex_app_server_protocol::GetAuthStatusParams; @@ -1115,6 +1116,7 @@ async fn login_amazon_bedrock_replaces_primary_auth_and_persists_provider() -> R login_id: None, success: true, error: None, + onboarding_entrypoint: None, } ); assert_account_updated(&mut mcp, Some(AuthMode::BedrockApiKey)).await?; @@ -2103,7 +2105,7 @@ async fn login_account_chatgpt_redirects_to_hosted_success_page() -> Result<()> .await?; let login: LoginAccountResponse = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; - let LoginAccountResponse::Chatgpt { auth_url, .. } = login else { + let LoginAccountResponse::Chatgpt { login_id, auth_url } = login else { bail!("unexpected login response: {login:?}"); }; let auth_url = Url::parse(&auth_url)?; @@ -2119,16 +2121,50 @@ async fn login_account_chatgpt_redirects_to_hosted_success_page() -> Result<()> .redirect(reqwest::redirect::Policy::none()) .build()?; - let response = client - .get(format!("{callback_url}?code=test-code&state={state}")) - .send() - .await?; + let token_redirect_uri = callback_url.clone(); + let mut callback_url = Url::parse(&callback_url)?; + let callback_state = format!("{state}.onboarding_entrypoint=life_sciences"); + callback_url + .query_pairs_mut() + .append_pair("code", "test-code") + .append_pair("state", &callback_state); + let response = client.get(callback_url).send().await?; assert_eq!(response.status(), 302); assert_eq!( response.headers()["location"].to_str()?, "http://localhost:3000/codex/open-app?source=login&app_brand=chatgpt" ); + let requests = mock_server + .received_requests() + .await + .ok_or_else(|| anyhow::anyhow!("failed to read OAuth requests"))?; + let token_request = requests + .iter() + .find(|request| request.url.path() == "/oauth/token") + .ok_or_else(|| anyhow::anyhow!("missing OAuth token request"))?; + let token_form: std::collections::HashMap<_, _> = + url::form_urlencoded::parse(&token_request.body) + .into_owned() + .collect(); + assert_eq!(token_form.get("redirect_uri"), Some(&token_redirect_uri),); + let notification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("account/login/completed"), + ) + .await??; + let ServerNotification::AccountLoginCompleted(payload) = notification.try_into()? else { + bail!("unexpected notification") + }; + assert_eq!( + payload, + AccountLoginCompletedNotification { + login_id: Some(login_id), + success: true, + error: None, + onboarding_entrypoint: Some(DesktopOnboardingEntrypoint::LifeSciences), + } + ); Ok(()) } diff --git a/codex-rs/login/src/callback_params.rs b/codex-rs/login/src/callback_params.rs new file mode 100644 index 0000000000..981cf1518e --- /dev/null +++ b/codex-rs/login/src/callback_params.rs @@ -0,0 +1,29 @@ +const LIFE_SCIENCES_OAUTH_STATE_SUFFIX: &str = ".onboarding_entrypoint=life_sciences"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LoginOnboardingEntrypoint { + LifeSciences, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct LoginCallbackResult { + pub onboarding_entrypoint: Option, +} + +pub(crate) fn login_callback_result_from_state( + callback_state: &str, + expected_state: &str, +) -> Option { + if callback_state == expected_state { + return Some(LoginCallbackResult::default()); + } + + (callback_state.strip_suffix(LIFE_SCIENCES_OAUTH_STATE_SUFFIX) == Some(expected_state)) + .then_some(LoginCallbackResult { + onboarding_entrypoint: Some(LoginOnboardingEntrypoint::LifeSciences), + }) +} + +#[cfg(test)] +#[path = "callback_params_tests.rs"] +mod tests; diff --git a/codex-rs/login/src/callback_params_tests.rs b/codex-rs/login/src/callback_params_tests.rs new file mode 100644 index 0000000000..1f06614b20 --- /dev/null +++ b/codex-rs/login/src/callback_params_tests.rs @@ -0,0 +1,49 @@ +use super::*; +use pretty_assertions::assert_eq; + +#[test] +fn accepts_the_original_oauth_state() { + assert_eq!( + login_callback_result_from_state("expected-state", "expected-state"), + Some(LoginCallbackResult::default()) + ); +} + +#[test] +fn accepts_the_allowlisted_life_sciences_suffix() { + assert_eq!( + login_callback_result_from_state( + "expected-state.onboarding_entrypoint=life_sciences", + "expected-state", + ), + Some(LoginCallbackResult { + onboarding_entrypoint: Some(LoginOnboardingEntrypoint::LifeSciences), + }) + ); +} + +#[test] +fn rejects_a_suffix_when_the_nonce_does_not_match() { + assert_eq!( + login_callback_result_from_state( + "different-state.onboarding_entrypoint=life_sciences", + "expected-state", + ), + None + ); +} + +#[test] +fn rejects_unrecognized_or_repeated_suffixes() { + for callback_state in [ + "expected-state.onboarding_entrypoint=unknown", + "expected-state.onboarding_entrypoint=life_sciences.onboarding_entrypoint=life_sciences", + "expected-state.extra=value.onboarding_entrypoint=life_sciences", + ] { + assert_eq!( + login_callback_result_from_state(callback_state, "expected-state"), + None, + "unexpectedly accepted {callback_state}", + ); + } +} diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 25f6ddb94f..1576afe548 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -3,12 +3,15 @@ pub mod auth_env_telemetry; pub mod test_support; pub mod token_data; +mod callback_params; mod device_code_auth; mod outbound_proxy; mod pkce; mod server; mod success_page; +pub use callback_params::LoginCallbackResult; +pub use callback_params::LoginOnboardingEntrypoint; pub use codex_config::types::AuthCredentialsStoreMode; pub use codex_http_client::BuildCustomCaTransportError as BuildLoginHttpClientError; pub use device_code_auth::DeviceCode; diff --git a/codex-rs/login/src/server.rs b/codex-rs/login/src/server.rs index 7f42740899..dbfddc3d3b 100644 --- a/codex-rs/login/src/server.rs +++ b/codex-rs/login/src/server.rs @@ -27,6 +27,8 @@ use std::time::Duration; use crate::auth::AuthDotJson; use crate::auth::AuthKeyringBackendKind; use crate::auth::save_auth; +use crate::callback_params::LoginCallbackResult; +use crate::callback_params::login_callback_result_from_state; use crate::default_client::create_raw_auth_client; use crate::default_client::originator; use crate::outbound_proxy::AuthRouteConfig; @@ -111,13 +113,20 @@ impl ServerOptions { pub struct LoginServer { pub auth_url: String, pub actual_port: u16, - server_handle: tokio::task::JoinHandle>, + server_handle: tokio::task::JoinHandle>, shutdown_handle: ShutdownHandle, } impl LoginServer { /// Waits for the login callback loop to finish. pub async fn block_until_done(self) -> io::Result<()> { + self.block_until_done_with_callback_result() + .await + .map(|_| ()) + } + + /// Waits for login to finish and returns allowlisted callback metadata. + pub async fn block_until_done_with_callback_result(self) -> io::Result { self.server_handle .await .map_err(|err| io::Error::other(format!("login server thread panicked: {err:?}")))? @@ -201,6 +210,7 @@ pub fn run_login_server(opts: ServerOptions) -> io::Result { let shutdown_notify = shutdown_notify.clone(); let server = server; tokio::spawn(async move { + let mut callback_result = LoginCallbackResult::default(); let result = loop { tokio::select! { _ = shutdown_notify.notified() => { @@ -228,7 +238,8 @@ pub fn run_login_server(opts: ServerOptions) -> io::Result { let _ = tokio::task::spawn_blocking(move || req.respond(response)).await; None } - HandledRequest::RedirectWithHeader(header) => { + HandledRequest::RedirectWithHeader { header, result } => { + callback_result = result; let redirect = Response::empty(302).with_header(header); let _ = tokio::task::spawn_blocking(move || req.respond(redirect)).await; None @@ -247,9 +258,9 @@ pub fn run_login_server(opts: ServerOptions) -> io::Result { ) }) .await; - Some(result) + Some(result.map(|()| callback_result)) } - HandledRequest::RedirectAndExit(header) => { + HandledRequest::RedirectAndExit { header, result } => { match tokio::task::spawn_blocking(move || { send_response_with_disconnect( req, @@ -268,7 +279,7 @@ pub fn run_login_server(opts: ServerOptions) -> io::Result { warn!("hosted login redirect task failed: {err}"); } } - Some(Ok(())) + Some(Ok(result)) } }; @@ -297,8 +308,14 @@ pub fn run_login_server(opts: ServerOptions) -> io::Result { /// Internal callback handling outcome. enum HandledRequest { Response(Response>>), - RedirectWithHeader(Header), - RedirectAndExit(Header), + RedirectWithHeader { + header: Header, + result: LoginCallbackResult, + }, + RedirectAndExit { + header: Header, + result: LoginCallbackResult, + }, ResponseAndExit { headers: Vec
, body: Vec, @@ -332,7 +349,10 @@ async fn process_request( let has_code = params.get("code").is_some_and(|code| !code.is_empty()); let has_state = params.get("state").is_some_and(|state| !state.is_empty()); let has_error = params.get("error").is_some_and(|error| !error.is_empty()); - let state_valid = params.get("state").map(String::as_str) == Some(state); + let callback_result = params + .get("state") + .and_then(|callback_state| login_callback_result_from_state(callback_state, state)); + let state_valid = callback_result.is_some(); info!( path = %path, has_code, @@ -380,6 +400,7 @@ async fn process_request( ); } }; + let callback_result = callback_result.unwrap_or_default(); match exchange_code_for_tokens( &opts.issuer, @@ -446,12 +467,14 @@ async fn process_request( }; match tiny_http::Header::from_bytes(&b"Location"[..], url.as_bytes()) { Ok(header) => match redirect { - LoginSuccessRedirect::Local(_) => { - HandledRequest::RedirectWithHeader(header) - } - LoginSuccessRedirect::Hosted(_) => { - HandledRequest::RedirectAndExit(header) - } + LoginSuccessRedirect::Local(_) => HandledRequest::RedirectWithHeader { + header, + result: callback_result, + }, + LoginSuccessRedirect::Hosted(_) => HandledRequest::RedirectAndExit { + header, + result: callback_result, + }, }, Err(_) => login_error_response( "Sign-in completed but redirecting back to Codex failed.", diff --git a/codex-rs/login/tests/suite/login_server_e2e.rs b/codex-rs/login/tests/suite/login_server_e2e.rs index 4f015cba74..fc1b4e65bf 100644 --- a/codex-rs/login/tests/suite/login_server_e2e.rs +++ b/codex-rs/login/tests/suite/login_server_e2e.rs @@ -11,6 +11,8 @@ use base64::Engine; use codex_config::types::AuthCredentialsStoreMode; use codex_http_client::HttpClientBuilder; use codex_login::AuthKeyringBackendKind; +use codex_login::LoginCallbackResult; +use codex_login::LoginOnboardingEntrypoint; use codex_login::LoginSuccessPage; use codex_login::LoginSuccessPageBrand; use codex_login::ServerOptions; @@ -149,7 +151,9 @@ async fn end_to_end_login_flow_persists_auth_json() -> Result<()> { let client = HttpClientBuilder::new() .without_redirects() .build_direct()?; - let url = format!("http://127.0.0.1:{login_port}/auth/callback?code=abc&state=test_state_123"); + let url = format!( + "http://127.0.0.1:{login_port}/auth/callback?code=abc&state=test_state_123.onboarding_entrypoint=life_sciences" + ); let resp = client.get(&url).send().await?; assert_eq!(resp.status(), 302); let success_url = resp.headers()["location"].to_str()?; @@ -161,7 +165,13 @@ async fn end_to_end_login_flow_persists_auth_json() -> Result<()> { assert!(success_resp.status().is_success()); // Wait for server shutdown - server.block_until_done().await?; + let callback_result = server.block_until_done_with_callback_result().await?; + assert_eq!( + callback_result, + LoginCallbackResult { + onboarding_entrypoint: Some(LoginOnboardingEntrypoint::LifeSciences), + } + ); // Validate auth.json let auth_path = codex_home.join("auth.json"); diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index d3ac734935..73a0769ab3 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -1359,6 +1359,7 @@ mod tests { login_id: Some("login-1".to_string()), success: true, error: None, + onboarding_entrypoint: None, }); assert!(matches!(