From 2fac8460a7024541d50fc9000c91fe45faeb8aa7 Mon Sep 17 00:00:00 2001 From: Darren Hwang Date: Thu, 4 Jun 2026 00:44:09 +0000 Subject: [PATCH] Propagate auth session logging ID in ChatGPT login --- codex-rs/Cargo.lock | 1 + .../schema/json/ClientRequest.json | 7 ++++ .../codex_app_server_protocol.schemas.json | 7 ++++ .../codex_app_server_protocol.v2.schemas.json | 7 ++++ .../schema/json/v2/LoginAccountParams.json | 7 ++++ .../typescript/v2/LoginAccountParams.ts | 6 +++- .../src/protocol/common.rs | 25 ++++++++++++++ .../src/protocol/v2/account.rs | 4 +++ codex-rs/app-server-test-client/src/lib.rs | 1 + .../request_processors/account_processor.rs | 19 ++++++++--- .../tests/common/test_app_server.rs | 16 ++++++++- codex-rs/app-server/tests/suite/v2/account.rs | 34 +++++++++++++++++++ codex-rs/cli/Cargo.toml | 1 + codex-rs/cli/src/login.rs | 12 ++++++- codex-rs/login/src/server.rs | 12 +++++++ .../login/tests/suite/login_server_e2e.rs | 8 +++++ codex-rs/tui/src/onboarding/auth.rs | 6 ++++ 17 files changed, 166 insertions(+), 7 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 2183d91b64..956bf688b4 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2308,6 +2308,7 @@ dependencies = [ "tracing-subscriber", "unicode-segmentation", "url", + "uuid", "which 8.0.0", "windows-sys 0.52.0", ] diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index dc5dd6ad60..e6ad036ecc 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1265,6 +1265,13 @@ }, { "properties": { + "authSessionLoggingId": { + "description": "Optional auth session logging ID propagated to authapi for analytics correlation.", + "type": [ + "string", + "null" + ] + }, "codexStreamlinedLogin": { "type": "boolean" }, 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 6aa4c5f35e..27d9baaace 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 @@ -10569,6 +10569,13 @@ }, { "properties": { + "authSessionLoggingId": { + "description": "Optional auth session logging ID propagated to authapi for analytics correlation.", + "type": [ + "string", + "null" + ] + }, "codexStreamlinedLogin": { "type": "boolean" }, 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 6d859ee5f3..f344c1d54e 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 @@ -7091,6 +7091,13 @@ }, { "properties": { + "authSessionLoggingId": { + "description": "Optional auth session logging ID propagated to authapi for analytics correlation.", + "type": [ + "string", + "null" + ] + }, "codexStreamlinedLogin": { "type": "boolean" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/LoginAccountParams.json b/codex-rs/app-server-protocol/schema/json/v2/LoginAccountParams.json index ab7b852c91..813c199470 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/LoginAccountParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/LoginAccountParams.json @@ -23,6 +23,13 @@ }, { "properties": { + "authSessionLoggingId": { + "description": "Optional auth session logging ID propagated to authapi for analytics correlation.", + "type": [ + "string", + "null" + ] + }, "codexStreamlinedLogin": { "type": "boolean" }, diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/LoginAccountParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/LoginAccountParams.ts index e6f1e2ed43..caa03b896b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/LoginAccountParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/LoginAccountParams.ts @@ -2,7 +2,11 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type LoginAccountParams = { "type": "apiKey", apiKey: string, } | { "type": "chatgpt", codexStreamlinedLogin?: boolean, } | { "type": "chatgptDeviceCode" } | { "type": "chatgptAuthTokens", +export type LoginAccountParams = { "type": "apiKey", apiKey: string, } | { "type": "chatgpt", codexStreamlinedLogin?: boolean, +/** + * Optional auth session logging ID propagated to authapi for analytics correlation. + */ +authSessionLoggingId?: string, } | { "type": "chatgptDeviceCode" } | { "type": "chatgptAuthTokens", /** * Access token (JWT) supplied by the client. * This token is used for backend API requests and email extraction. diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 5a60d8d002..5740451673 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -2507,6 +2507,7 @@ mod tests { request_id: RequestId::Integer(3), params: v2::LoginAccountParams::Chatgpt { codex_streamlined_login: false, + auth_session_logging_id: None, }, }; assert_eq!( @@ -2528,6 +2529,7 @@ mod tests { request_id: RequestId::Integer(3), params: v2::LoginAccountParams::Chatgpt { codex_streamlined_login: true, + auth_session_logging_id: None, }, }; assert_eq!( @@ -2544,6 +2546,29 @@ mod tests { Ok(()) } + #[test] + fn serialize_account_login_chatgpt_with_auth_session_logging_id() -> Result<()> { + let request = ClientRequest::LoginAccount { + request_id: RequestId::Integer(3), + params: v2::LoginAccountParams::Chatgpt { + codex_streamlined_login: false, + auth_session_logging_id: Some("auth-session-123".to_string()), + }, + }; + assert_eq!( + json!({ + "method": "account/login/start", + "id": 3, + "params": { + "type": "chatgpt", + "authSessionLoggingId": "auth-session-123" + } + }), + serde_json::to_value(&request)?, + ); + Ok(()) + } + #[test] fn serialize_account_login_chatgpt_device_code() -> Result<()> { let request = ClientRequest::LoginAccount { 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 83f1bc02b8..564dfe95ae 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/account.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/account.rs @@ -58,6 +58,10 @@ pub enum LoginAccountParams { Chatgpt { #[serde(default, skip_serializing_if = "std::ops::Not::not")] codex_streamlined_login: bool, + /// Optional auth session logging ID propagated to authapi for analytics correlation. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + auth_session_logging_id: Option, }, #[serde(rename = "chatgptDeviceCode")] #[ts(rename = "chatgptDeviceCode")] diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index e89a8dbe99..1952a3989e 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -1616,6 +1616,7 @@ impl CodexClient { request_id: request_id.clone(), params: codex_app_server_protocol::LoginAccountParams::Chatgpt { codex_streamlined_login: false, + auth_session_logging_id: None, }, }; 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 8ebfc037e1..40ab7f7d5f 100644 --- a/codex-rs/app-server/src/request_processors/account_processor.rs +++ b/codex-rs/app-server/src/request_processors/account_processor.rs @@ -218,8 +218,9 @@ impl AccountRequestProcessor { } LoginAccountParams::Chatgpt { codex_streamlined_login, + auth_session_logging_id, } => { - self.login_chatgpt_v2(request_id, codex_streamlined_login) + self.login_chatgpt_v2(request_id, codex_streamlined_login, auth_session_logging_id) .await; } LoginAccountParams::ChatgptDeviceCode => { @@ -304,6 +305,7 @@ impl AccountRequestProcessor { async fn login_chatgpt_common( &self, codex_streamlined_login: bool, + auth_session_logging_id: Option, ) -> std::result::Result { let config = self.config.as_ref(); @@ -320,6 +322,7 @@ impl AccountRequestProcessor { let opts = LoginServerOptions { open_browser: false, codex_streamlined_login, + auth_session_logging_id, ..LoginServerOptions::new( config.codex_home.to_path_buf(), CLIENT_ID.to_string(), @@ -354,16 +357,22 @@ impl AccountRequestProcessor { &self, request_id: ConnectionRequestId, codex_streamlined_login: bool, + auth_session_logging_id: Option, ) { - let result = self.login_chatgpt_response(codex_streamlined_login).await; + let result = self + .login_chatgpt_response(codex_streamlined_login, auth_session_logging_id) + .await; self.outgoing.send_result(request_id, result).await; } async fn login_chatgpt_response( &self, codex_streamlined_login: bool, + auth_session_logging_id: Option, ) -> Result { - let opts = self.login_chatgpt_common(codex_streamlined_login).await?; + let opts = self + .login_chatgpt_common(codex_streamlined_login, auth_session_logging_id) + .await?; let server = run_login_server(opts) .map_err(|err| internal_error(format!("failed to start login server: {err}")))?; let login_id = Uuid::new_v4(); @@ -435,7 +444,9 @@ impl AccountRequestProcessor { &self, ) -> Result { let opts = self - .login_chatgpt_common(/*codex_streamlined_login*/ false) + .login_chatgpt_common( + /*codex_streamlined_login*/ false, /*auth_session_logging_id*/ None, + ) .await?; let device_code = request_device_code(&opts) .await diff --git a/codex-rs/app-server/tests/common/test_app_server.rs b/codex-rs/app-server/tests/common/test_app_server.rs index ec4b30d92f..bb27e5c336 100644 --- a/codex-rs/app-server/tests/common/test_app_server.rs +++ b/codex-rs/app-server/tests/common/test_app_server.rs @@ -1187,9 +1187,23 @@ impl TestAppServer { /// Send an `account/login/start` JSON-RPC request for ChatGPT login. pub async fn send_login_account_chatgpt_request(&mut self) -> anyhow::Result { - let params = serde_json::json!({ + self.send_login_account_chatgpt_request_with_auth_session_logging_id( + /*auth_session_logging_id*/ None, + ) + .await + } + + /// Send an `account/login/start` JSON-RPC request for ChatGPT login. + pub async fn send_login_account_chatgpt_request_with_auth_session_logging_id( + &mut self, + auth_session_logging_id: Option<&str>, + ) -> anyhow::Result { + let mut params = serde_json::json!({ "type": "chatgpt" }); + if let Some(auth_session_logging_id) = auth_session_logging_id { + params["authSessionLoggingId"] = serde_json::json!(auth_session_logging_id); + } self.send_request("account/login/start", Some(params)).await } diff --git a/codex-rs/app-server/tests/suite/v2/account.rs b/codex-rs/app-server/tests/suite/v2/account.rs index 1b95bc7d5b..04ba5d3b70 100644 --- a/codex-rs/app-server/tests/suite/v2/account.rs +++ b/codex-rs/app-server/tests/suite/v2/account.rs @@ -1482,6 +1482,40 @@ async fn login_account_chatgpt_includes_forced_workspace_query_param() -> Result Ok(()) } +#[tokio::test] +// Serialize tests that launch the login server since it binds to a fixed port. +#[serial(login_port)] +async fn login_account_chatgpt_includes_auth_session_logging_id_query_param() -> Result<()> { + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?; + + let mut mcp = TestAppServer::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_login_account_chatgpt_request_with_auth_session_logging_id(Some("auth-session-123")) + .await?; + let resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + + let login: LoginAccountResponse = to_response(resp)?; + let LoginAccountResponse::Chatgpt { auth_url, .. } = login else { + bail!("unexpected login response: {login:?}"); + }; + let auth_url = Url::parse(&auth_url)?; + assert_eq!( + auth_url + .query_pairs() + .find(|(key, _)| key == "auth_session_logging_id") + .map(|(_, value)| value.into_owned()), + Some("auth-session-123".to_string()) + ); + Ok(()) +} + #[tokio::test] // Serialize tests that launch the login server since it binds to a fixed port. #[serial(login_port)] diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 6a4d78fbc7..3d9eafcb1b 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -84,6 +84,7 @@ tracing-appender = { workspace = true } tracing-subscriber = { workspace = true } unicode-segmentation = { workspace = true } url = { workspace = true } +uuid = { workspace = true, features = ["v4"] } which = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index 6fdc62fdb1..13cc7d40c6 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -30,6 +30,7 @@ use tracing_subscriber::EnvFilter; use tracing_subscriber::Layer; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; +use uuid::Uuid; const CHATGPT_LOGIN_DISABLED_MESSAGE: &str = "ChatGPT login is disabled. Use API key login instead."; @@ -118,12 +119,15 @@ pub async fn login_with_chatgpt( forced_chatgpt_workspace_id: Option>, cli_auth_credentials_store_mode: AuthCredentialsStoreMode, ) -> std::io::Result<()> { - let opts = ServerOptions::new( + let auth_session_logging_id = Uuid::new_v4().to_string(); + tracing::info!(auth_session_logging_id = %auth_session_logging_id, "starting browser login attempt"); + let mut opts = ServerOptions::new( codex_home, CLIENT_ID.to_string(), forced_chatgpt_workspace_id, cli_auth_credentials_store_mode, ); + opts.auth_session_logging_id = Some(auth_session_logging_id); let server = run_login_server(opts)?; print_login_server_start(server.actual_port, &server.auth_url); @@ -335,6 +339,12 @@ pub async fn run_login_with_device_code_fallback_to_browser( Err(e) => { if e.kind() == std::io::ErrorKind::NotFound { eprintln!("Device code login is not enabled; falling back to browser login."); + let auth_session_logging_id = Uuid::new_v4().to_string(); + tracing::info!( + auth_session_logging_id = %auth_session_logging_id, + "starting browser login fallback attempt" + ); + opts.auth_session_logging_id = Some(auth_session_logging_id); match run_login_server(opts) { Ok(server) => { print_login_server_start(server.actual_port, &server.auth_url); diff --git a/codex-rs/login/src/server.rs b/codex-rs/login/src/server.rs index b72bc946f2..ca3b11dfa7 100644 --- a/codex-rs/login/src/server.rs +++ b/codex-rs/login/src/server.rs @@ -71,6 +71,7 @@ pub struct ServerOptions { pub force_state: Option, pub forced_chatgpt_workspace_id: Option>, pub codex_streamlined_login: bool, + pub auth_session_logging_id: Option, pub cli_auth_credentials_store_mode: AuthCredentialsStoreMode, } @@ -91,6 +92,7 @@ impl ServerOptions { force_state: None, forced_chatgpt_workspace_id, codex_streamlined_login: false, + auth_session_logging_id: None, cli_auth_credentials_store_mode, } } @@ -161,6 +163,7 @@ pub fn run_login_server(opts: ServerOptions) -> io::Result { &pkce, &state, opts.forced_chatgpt_workspace_id.as_deref(), + opts.auth_session_logging_id.as_deref(), ); if opts.open_browser { @@ -487,6 +490,7 @@ fn build_authorize_url( pkce: &PkceCodes, state: &str, forced_chatgpt_workspace_ids: Option<&[String]>, + auth_session_logging_id: Option<&str>, ) -> String { let mut query = vec![ ("response_type".to_string(), "code".to_string()), @@ -510,6 +514,14 @@ fn build_authorize_url( if let Some(workspace_ids) = forced_chatgpt_workspace_ids { query.push(("allowed_workspace_id".to_string(), workspace_ids.join(","))); } + if let Some(auth_session_logging_id) = auth_session_logging_id + .filter(|auth_session_logging_id| !auth_session_logging_id.is_empty()) + { + query.push(( + "auth_session_logging_id".to_string(), + auth_session_logging_id.to_string(), + )); + } let qs = query .into_iter() .map(|(k, v)| format!("{k}={}", urlencoding::encode(&v))) diff --git a/codex-rs/login/tests/suite/login_server_e2e.rs b/codex-rs/login/tests/suite/login_server_e2e.rs index bc7bb401e8..9ad7b74668 100644 --- a/codex-rs/login/tests/suite/login_server_e2e.rs +++ b/codex-rs/login/tests/suite/login_server_e2e.rs @@ -128,6 +128,7 @@ async fn end_to_end_login_flow_persists_auth_json() -> Result<()> { force_state: Some(state), forced_chatgpt_workspace_id: Some(vec![chatgpt_account_id.to_string()]), codex_streamlined_login: false, + auth_session_logging_id: None, }; let server = run_login_server(opts)?; assert!( @@ -190,6 +191,7 @@ async fn creates_missing_codex_home_dir() -> Result<()> { force_state: Some(state), forced_chatgpt_workspace_id: None, codex_streamlined_login: false, + auth_session_logging_id: None, }; let server = run_login_server(opts)?; let login_port = server.actual_port; @@ -233,6 +235,7 @@ async fn login_server_includes_forced_workspaces_as_one_query_param() -> Result< WORKSPACE_ID_SECOND_ALLOWED.to_string(), ]), codex_streamlined_login: false, + auth_session_logging_id: None, }; let server = run_login_server(opts)?; let auth_url = Url::parse(&server.auth_url)?; @@ -271,6 +274,7 @@ async fn forced_chatgpt_workspace_id_mismatch_blocks_login() -> Result<()> { force_state: Some(state.clone()), forced_chatgpt_workspace_id: Some(vec![WORKSPACE_ID_ALLOWED.to_string()]), codex_streamlined_login: false, + auth_session_logging_id: None, }; let server = run_login_server(opts)?; assert!( @@ -331,6 +335,7 @@ async fn oauth_access_denied_missing_entitlement_blocks_login_with_clear_error() force_state: Some(state.clone()), forced_chatgpt_workspace_id: None, codex_streamlined_login: false, + auth_session_logging_id: None, }; let server = run_login_server(opts)?; let login_port = server.actual_port; @@ -399,6 +404,7 @@ async fn oauth_access_denied_unknown_reason_uses_generic_error_page() -> Result< force_state: Some(state.clone()), forced_chatgpt_workspace_id: None, codex_streamlined_login: false, + auth_session_logging_id: None, }; let server = run_login_server(opts)?; let login_port = server.actual_port; @@ -544,6 +550,7 @@ async fn cancels_previous_login_server_when_port_is_in_use() -> Result<()> { force_state: Some("cancel_state".to_string()), forced_chatgpt_workspace_id: None, codex_streamlined_login: false, + auth_session_logging_id: None, }; let first_server = run_login_server(first_opts)?; @@ -565,6 +572,7 @@ async fn cancels_previous_login_server_when_port_is_in_use() -> Result<()> { force_state: Some("cancel_state_2".to_string()), forced_chatgpt_workspace_id: None, codex_streamlined_login: false, + auth_session_logging_id: None, }; let second_server = run_login_server(second_opts)?; diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index 0d8a1fa773..5e89e49a06 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -865,6 +865,11 @@ impl AuthModeWidget { } self.set_error(/*message*/ None); + let auth_session_logging_id = Uuid::new_v4().to_string(); + tracing::info!( + auth_session_logging_id = %auth_session_logging_id, + "starting browser login attempt" + ); let request_handle = self.app_server_request_handle.clone(); let sign_in_state = self.sign_in_state.clone(); let error = self.error.clone(); @@ -875,6 +880,7 @@ impl AuthModeWidget { request_id: onboarding_request_id(), params: LoginAccountParams::Chatgpt { codex_streamlined_login: false, + auth_session_logging_id: Some(auth_session_logging_id), }, }) .await