From fb7cfc813a4fa09a3fe479a33ca071c4c4f97fc3 Mon Sep 17 00:00:00 2001 From: Owen Lin Date: Wed, 13 May 2026 12:38:34 -0700 Subject: [PATCH] fix: prevent codex-backend from stealing originator (#22533) ## Why Remote control starts by letting `codex-backend` initialize against the app-server as an infrastructure health/proxy client before the real remote client connects. App-server initialization also sets the process-wide `originator` from `client_info.name`, so `codex-backend` could become the sticky originator for later model/API requests even after the real client initialized. ## What changed - Treat `codex-backend` as a non-originating initialize client, alongside the existing `codex_app_server_daemon` probe client. - Preserve normal per-connection initialize behavior, including session metadata and initialize analytics. - Add regression coverage that verifies `codex-backend` initialize does not replace the default originator. ## Testing - `cargo test -p codex-app-server --test all initialize_codex_backend_does_not_override_originator` --- .../initialize_processor.rs | 4 +-- .../app-server/tests/suite/v2/initialize.rs | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/codex-rs/app-server/src/request_processors/initialize_processor.rs b/codex-rs/app-server/src/request_processors/initialize_processor.rs index c33af189cf..a40007db11 100644 --- a/codex-rs/app-server/src/request_processors/initialize_processor.rs +++ b/codex-rs/app-server/src/request_processors/initialize_processor.rs @@ -13,7 +13,7 @@ use super::*; use crate::message_processor::ConnectionSessionState; use crate::message_processor::InitializedConnectionSessionState; -const DAEMON_PROBE_CLIENT_NAME: &str = "codex_app_server_daemon"; +const NON_ORIGINATING_CLIENT_NAMES: &[&str] = &["codex_app_server_daemon", "codex-backend"]; #[derive(Clone)] pub(crate) struct InitializeRequestProcessor { @@ -92,7 +92,7 @@ impl InitializeRequestProcessor { } let originator = name.clone(); let user_agent_suffix = format!("{name}; {version}"); - let mutates_global_identity = name != DAEMON_PROBE_CLIENT_NAME; + let mutates_global_identity = !NON_ORIGINATING_CLIENT_NAMES.contains(&name.as_str()); let codex_home = self.config.codex_home.clone(); if session .initialize(InitializedConnectionSessionState { diff --git a/codex-rs/app-server/tests/suite/v2/initialize.rs b/codex-rs/app-server/tests/suite/v2/initialize.rs index 3d3a473fab..47cacdb20e 100644 --- a/codex-rs/app-server/tests/suite/v2/initialize.rs +++ b/codex-rs/app-server/tests/suite/v2/initialize.rs @@ -89,6 +89,33 @@ async fn initialize_probe_does_not_override_originator() -> Result<()> { Ok(()) } +#[tokio::test] +async fn initialize_codex_backend_does_not_override_originator() -> Result<()> { + let responses = Vec::new(); + let server = create_mock_responses_server_sequence_unchecked(responses).await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri(), "never")?; + let mut mcp = McpProcess::new(codex_home.path()).await?; + + let message = timeout( + DEFAULT_READ_TIMEOUT, + mcp.initialize_with_client_info(ClientInfo { + name: "codex-backend".to_string(), + title: Some("Codex Backend".to_string()), + version: "0.1.0".to_string(), + }), + ) + .await??; + + let JSONRPCMessage::Response(response) = message else { + anyhow::bail!("expected initialize response, got {message:?}"); + }; + let InitializeResponse { user_agent, .. } = to_response::(response)?; + + assert!(user_agent.starts_with("codex_cli_rs/")); + Ok(()) +} + #[tokio::test] async fn initialize_respects_originator_override_env_var() -> Result<()> { let responses = Vec::new();