From 65f3528cad65415bc3b724c2cbc046573350d0b8 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 10 Sep 2025 17:03:35 -0700 Subject: [PATCH] feat: add UserInfo request to JSON-RPC server (#3428) This adds a simple endpoint that provides the email address encoded in `$CODEX_HOME/auth.json`. As noted, for now, we do not hit the server to verify this is the user's true email address. --- codex-rs/Cargo.lock | 1 + codex-rs/mcp-server/Cargo.toml | 1 + .../mcp-server/src/codex_message_processor.rs | 18 +++++ .../mcp-server/tests/common/mcp_process.rs | 5 ++ codex-rs/mcp-server/tests/suite/mod.rs | 1 + codex-rs/mcp-server/tests/suite/user_info.rs | 78 +++++++++++++++++++ codex-rs/protocol-ts/src/lib.rs | 1 + codex-rs/protocol/src/mcp_protocol.rs | 14 ++++ 8 files changed, 119 insertions(+) create mode 100644 codex-rs/mcp-server/tests/suite/user_info.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 7bf6c2a7..eab2dede 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -769,6 +769,7 @@ version = "0.0.0" dependencies = [ "anyhow", "assert_cmd", + "base64", "codex-arg0", "codex-common", "codex-core", diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml index c219c729..e068ba64 100644 --- a/codex-rs/mcp-server/Cargo.toml +++ b/codex-rs/mcp-server/Cargo.toml @@ -40,6 +40,7 @@ uuid = { version = "1", features = ["serde", "v4"] } [dev-dependencies] assert_cmd = "2" +base64 = "0.22" mcp_test_support = { path = "tests/common" } os_info = "3.12.0" pretty_assertions = "1.4.1" diff --git a/codex-rs/mcp-server/src/codex_message_processor.rs b/codex-rs/mcp-server/src/codex_message_processor.rs index 53e0b9d5..e2ff119f 100644 --- a/codex-rs/mcp-server/src/codex_message_processor.rs +++ b/codex-rs/mcp-server/src/codex_message_processor.rs @@ -11,6 +11,8 @@ use codex_core::NewConversation; use codex_core::RolloutRecorder; use codex_core::SessionMeta; use codex_core::auth::CLIENT_ID; +use codex_core::auth::get_auth_file; +use codex_core::auth::try_read_auth_json; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::config::ConfigToml; @@ -67,6 +69,7 @@ use codex_protocol::mcp_protocol::SendUserMessageResponse; use codex_protocol::mcp_protocol::SendUserTurnParams; use codex_protocol::mcp_protocol::SendUserTurnResponse; use codex_protocol::mcp_protocol::ServerNotification; +use codex_protocol::mcp_protocol::UserInfoResponse; use codex_protocol::mcp_protocol::UserSavedConfig; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; @@ -187,6 +190,9 @@ impl CodexMessageProcessor { ClientRequest::GetUserAgent { request_id } => { self.get_user_agent(request_id).await; } + ClientRequest::UserInfo { request_id } => { + self.get_user_info(request_id).await; + } ClientRequest::ExecOneOffCommand { request_id, params } => { self.exec_one_off_command(request_id, params).await; } @@ -439,6 +445,18 @@ impl CodexMessageProcessor { self.outgoing.send_response(request_id, response).await; } + async fn get_user_info(&self, request_id: RequestId) { + // Read alleged user email from auth.json (best-effort; not verified). + let auth_path = get_auth_file(&self.config.codex_home); + let alleged_user_email = match try_read_auth_json(&auth_path) { + Ok(auth) => auth.tokens.and_then(|t| t.id_token.email), + Err(_) => None, + }; + + let response = UserInfoResponse { alleged_user_email }; + self.outgoing.send_response(request_id, response).await; + } + async fn exec_one_off_command(&self, request_id: RequestId, params: ExecOneOffCommandParams) { tracing::debug!("ExecOneOffCommand params: {params:?}"); diff --git a/codex-rs/mcp-server/tests/common/mcp_process.rs b/codex-rs/mcp-server/tests/common/mcp_process.rs index fa50c9d8..ec7953de 100644 --- a/codex-rs/mcp-server/tests/common/mcp_process.rs +++ b/codex-rs/mcp-server/tests/common/mcp_process.rs @@ -295,6 +295,11 @@ impl McpProcess { self.send_request("getUserAgent", None).await } + /// Send a `userInfo` JSON-RPC request. + pub async fn send_user_info_request(&mut self) -> anyhow::Result { + self.send_request("userInfo", None).await + } + /// Send a `listConversations` JSON-RPC request. pub async fn send_list_conversations_request( &mut self, diff --git a/codex-rs/mcp-server/tests/suite/mod.rs b/codex-rs/mcp-server/tests/suite/mod.rs index 7f89cc33..4e603e17 100644 --- a/codex-rs/mcp-server/tests/suite/mod.rs +++ b/codex-rs/mcp-server/tests/suite/mod.rs @@ -10,3 +10,4 @@ mod list_resume; mod login; mod send_message; mod user_agent; +mod user_info; diff --git a/codex-rs/mcp-server/tests/suite/user_info.rs b/codex-rs/mcp-server/tests/suite/user_info.rs new file mode 100644 index 00000000..7bcb2acc --- /dev/null +++ b/codex-rs/mcp-server/tests/suite/user_info.rs @@ -0,0 +1,78 @@ +use std::time::Duration; + +use anyhow::Context; +use base64::Engine; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use codex_core::auth::AuthDotJson; +use codex_core::auth::get_auth_file; +use codex_core::auth::write_auth_json; +use codex_core::token_data::IdTokenInfo; +use codex_core::token_data::TokenData; +use codex_protocol::mcp_protocol::UserInfoResponse; +use mcp_test_support::McpProcess; +use mcp_test_support::to_response; +use mcp_types::JSONRPCResponse; +use mcp_types::RequestId; +use pretty_assertions::assert_eq; +use serde_json::json; +use tempfile::TempDir; +use tokio::time::timeout; + +const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10); + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn user_info_returns_email_from_auth_json() { + let codex_home = TempDir::new().expect("create tempdir"); + + let auth_path = get_auth_file(codex_home.path()); + let mut id_token = IdTokenInfo::default(); + id_token.email = Some("user@example.com".to_string()); + id_token.raw_jwt = encode_id_token_with_email("user@example.com").expect("encode id token"); + + let auth = AuthDotJson { + openai_api_key: None, + tokens: Some(TokenData { + id_token, + access_token: "access".to_string(), + refresh_token: "refresh".to_string(), + account_id: None, + }), + last_refresh: None, + }; + write_auth_json(&auth_path, &auth).expect("write auth.json"); + + let mut mcp = McpProcess::new(codex_home.path()) + .await + .expect("spawn mcp process"); + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()) + .await + .expect("initialize timeout") + .expect("initialize request"); + + let request_id = mcp.send_user_info_request().await.expect("send userInfo"); + let response: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await + .expect("userInfo timeout") + .expect("userInfo response"); + + let received: UserInfoResponse = to_response(response).expect("deserialize userInfo response"); + let expected = UserInfoResponse { + alleged_user_email: Some("user@example.com".to_string()), + }; + + assert_eq!(received, expected); +} + +fn encode_id_token_with_email(email: &str) -> anyhow::Result { + let header_b64 = URL_SAFE_NO_PAD.encode( + serde_json::to_vec(&json!({ "alg": "none", "typ": "JWT" })) + .context("serialize jwt header")?, + ); + let payload = + serde_json::to_vec(&json!({ "email": email })).context("serialize jwt payload")?; + let payload_b64 = URL_SAFE_NO_PAD.encode(payload); + Ok(format!("{header_b64}.{payload_b64}.signature")) +} diff --git a/codex-rs/protocol-ts/src/lib.rs b/codex-rs/protocol-ts/src/lib.rs index 776c8ba3..1a1d5356 100644 --- a/codex-rs/protocol-ts/src/lib.rs +++ b/codex-rs/protocol-ts/src/lib.rs @@ -39,6 +39,7 @@ pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> { codex_protocol::mcp_protocol::ExecCommandApprovalResponse::export_all_to(out_dir)?; codex_protocol::mcp_protocol::GetUserSavedConfigResponse::export_all_to(out_dir)?; codex_protocol::mcp_protocol::GetUserAgentResponse::export_all_to(out_dir)?; + codex_protocol::mcp_protocol::UserInfoResponse::export_all_to(out_dir)?; // All notification types reachable from this enum will be generated by // induction, so they do not need to be listed individually. diff --git a/codex-rs/protocol/src/mcp_protocol.rs b/codex-rs/protocol/src/mcp_protocol.rs index 00391c7d..e003abea 100644 --- a/codex-rs/protocol/src/mcp_protocol.rs +++ b/codex-rs/protocol/src/mcp_protocol.rs @@ -152,6 +152,10 @@ pub enum ClientRequest { #[serde(rename = "id")] request_id: RequestId, }, + UserInfo { + #[serde(rename = "id")] + request_id: RequestId, + }, /// Execute a command (argv vector) under the server's sandbox. ExecOneOffCommand { #[serde(rename = "id")] @@ -374,6 +378,16 @@ pub struct GetUserAgentResponse { pub user_agent: String, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct UserInfoResponse { + /// Note: `alleged_user_email` is not currently verified. We read it from + /// the local auth.json, which the user could theoretically modify. In the + /// future, we may add logic to verify the email against the server before + /// returning it. + pub alleged_user_email: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct GetUserSavedConfigResponse {