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`
This commit is contained in:
Owen Lin
2026-05-13 12:38:34 -07:00
committed by GitHub
parent 9c691b74d6
commit fb7cfc813a
2 changed files with 29 additions and 2 deletions

View File

@@ -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 {

View File

@@ -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::<InitializeResponse>(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();