diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 597f002c0d7..04e97ed6227 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -52,10 +52,6 @@ Clients must send a single `initialize` request before invoking any other method Applications building on top of `codex app-server` should identify themselves via the `clientInfo` parameter. -**Important**: `clientInfo.name` is used to identify the client for the OpenAI Compliance Logs Platform. If -you are developing a new Codex integration that is intended for enterprise use, please contact us to get it -added to a known clients list. For more context: https://chatgpt.com/admin/api-reference#tag/Logs:-Codex - Example (from OpenAI's official VSCode extension): ```json @@ -64,7 +60,7 @@ Example (from OpenAI's official VSCode extension): "id": 0, "params": { "clientInfo": { - "name": "codex_vscode", + "name": "codex-vscode", "title": "Codex VS Code Extension", "version": "0.1.0" } diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index c3a3e54ff9b..68663a991db 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -92,17 +92,13 @@ pub async fn run_main( let feedback = CodexFeedback::new(); - let otel = codex_core::otel_init::build_provider( - &config, - env!("CARGO_PKG_VERSION"), - Some("codex_app_server"), - ) - .map_err(|e| { - std::io::Error::new( - ErrorKind::InvalidData, - format!("error loading otel config: {e}"), - ) - })?; + let otel = + codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION")).map_err(|e| { + std::io::Error::new( + ErrorKind::InvalidData, + format!("error loading otel config: {e}"), + ) + })?; // Install a simple subscriber so `tracing` output is visible. Users can // control the log level with `RUST_LOG`. diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 1f442b99567..60e938bb18f 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -21,10 +21,8 @@ use codex_core::AuthManager; use codex_core::ThreadManager; use codex_core::config::Config; use codex_core::config_loader::LoaderOverrides; -use codex_core::default_client::SetOriginatorError; use codex_core::default_client::USER_AGENT_SUFFIX; use codex_core::default_client::get_codex_user_agent; -use codex_core::default_client::set_default_originator; use codex_feedback::CodexFeedback; use codex_protocol::protocol::SessionSource; use toml::Value as TomlValue; @@ -123,27 +121,6 @@ impl MessageProcessor { title: _title, version, } = params.client_info; - if let Err(error) = set_default_originator(name.clone()) { - match error { - SetOriginatorError::InvalidHeaderValue => { - let error = JSONRPCErrorError { - code: INVALID_REQUEST_ERROR_CODE, - message: format!( - "Invalid clientInfo.name: '{name}'. Must be a valid HTTP header value." - ), - data: None, - }; - self.outgoing.send_error(request_id, error).await; - return; - } - SetOriginatorError::AlreadyInitialized => { - // No-op. This is expected to happen if the originator is already set via env var. - // TODO(owen): Once we remove support for CODEX_INTERNAL_ORIGINATOR_OVERRIDE, - // this will be an unexpected state and we can return a JSON-RPC error indicating - // internal server error. - } - } - } let user_agent_suffix = format!("{name}; {version}"); if let Ok(mut suffix) = USER_AGENT_SUFFIX.lock() { *suffix = Some(user_agent_suffix); diff --git a/codex-rs/app-server/tests/common/lib.rs b/codex-rs/app-server/tests/common/lib.rs index af4982b846b..594bb78c249 100644 --- a/codex-rs/app-server/tests/common/lib.rs +++ b/codex-rs/app-server/tests/common/lib.rs @@ -17,7 +17,6 @@ pub use core_test_support::format_with_current_shell_non_login; pub use core_test_support::test_path_buf_with_windows; pub use core_test_support::test_tmp_path; pub use core_test_support::test_tmp_path_buf; -pub use mcp_process::DEFAULT_CLIENT_NAME; pub use mcp_process::McpProcess; pub use mock_model_server::create_mock_responses_server_repeating_assistant; pub use mock_model_server::create_mock_responses_server_sequence; diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs index 76d4363014e..36880228ddc 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -66,8 +66,6 @@ pub struct McpProcess { pending_messages: VecDeque, } -pub const DEFAULT_CLIENT_NAME: &str = "codex-app-server-tests"; - impl McpProcess { pub async fn new(codex_home: &Path) -> anyhow::Result { Self::new_with_env(codex_home, &[]).await @@ -140,7 +138,7 @@ impl McpProcess { pub async fn initialize(&mut self) -> anyhow::Result<()> { let params = Some(serde_json::to_value(InitializeParams { client_info: ClientInfo { - name: DEFAULT_CLIENT_NAME.to_string(), + name: "codex-app-server-tests".to_string(), title: None, version: "0.1.0".to_string(), }, @@ -165,38 +163,6 @@ impl McpProcess { Ok(()) } - /// Sends initialize with the provided client info and returns the response/error message. - pub async fn initialize_with_client_info( - &mut self, - client_info: ClientInfo, - ) -> anyhow::Result { - let params = Some(serde_json::to_value(InitializeParams { client_info })?); - let request_id = self.send_request("initialize", params).await?; - let request_id = RequestId::Integer(request_id); - - loop { - let message = self.read_jsonrpc_message().await?; - match message { - JSONRPCMessage::Notification(notification) => { - self.enqueue_user_message(notification); - } - JSONRPCMessage::Response(response) => { - if response.id == request_id { - return Ok(JSONRPCMessage::Response(response)); - } - } - JSONRPCMessage::Error(error) => { - if error.id == request_id { - return Ok(JSONRPCMessage::Error(error)); - } - } - JSONRPCMessage::Request(_) => { - anyhow::bail!("unexpected JSONRPCMessage::Request: {message:?}"); - } - } - } - } - /// Send a `newConversation` JSON-RPC request. pub async fn send_new_conversation_request( &mut self, diff --git a/codex-rs/app-server/tests/suite/user_agent.rs b/codex-rs/app-server/tests/suite/user_agent.rs index 9178a3ef584..5ed6cafdeeb 100644 --- a/codex-rs/app-server/tests/suite/user_agent.rs +++ b/codex-rs/app-server/tests/suite/user_agent.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use app_test_support::DEFAULT_CLIENT_NAME; use app_test_support::McpProcess; use app_test_support::to_response; use codex_app_server_protocol::GetUserAgentResponse; @@ -26,13 +25,13 @@ async fn get_user_agent_returns_current_codex_user_agent() -> Result<()> { .await??; let os_info = os_info::get(); - let originator = DEFAULT_CLIENT_NAME; + let originator = codex_core::default_client::originator().value.as_str(); let os_type = os_info.os_type(); let os_version = os_info.version(); let architecture = os_info.architecture().unwrap_or("unknown"); let terminal_ua = codex_core::terminal::user_agent(); let user_agent = format!( - "{originator}/0.0.0 ({os_type} {os_version}; {architecture}) {terminal_ua} ({DEFAULT_CLIENT_NAME}; 0.1.0)" + "{originator}/0.0.0 ({os_type} {os_version}; {architecture}) {terminal_ua} (codex-app-server-tests; 0.1.0)" ); let received: GetUserAgentResponse = to_response(response)?; diff --git a/codex-rs/app-server/tests/suite/v2/initialize.rs b/codex-rs/app-server/tests/suite/v2/initialize.rs deleted file mode 100644 index 08b7766fd21..00000000000 --- a/codex-rs/app-server/tests/suite/v2/initialize.rs +++ /dev/null @@ -1,98 +0,0 @@ -use anyhow::Result; -use app_test_support::McpProcess; -use app_test_support::to_response; -use codex_app_server_protocol::ClientInfo; -use codex_app_server_protocol::InitializeResponse; -use codex_app_server_protocol::JSONRPCMessage; -use pretty_assertions::assert_eq; -use tempfile::TempDir; -use tokio::time::timeout; - -const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn initialize_uses_client_info_name_as_originator() -> Result<()> { - let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new(codex_home.path()).await?; - - let message = timeout( - DEFAULT_READ_TIMEOUT, - mcp.initialize_with_client_info(ClientInfo { - name: "codex_vscode".to_string(), - title: Some("Codex VS Code Extension".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_vscode/")); - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn initialize_respects_originator_override_env_var() -> Result<()> { - let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new_with_env( - codex_home.path(), - &[( - "CODEX_INTERNAL_ORIGINATOR_OVERRIDE", - Some("codex_originator_via_env_var"), - )], - ) - .await?; - - let message = timeout( - DEFAULT_READ_TIMEOUT, - mcp.initialize_with_client_info(ClientInfo { - name: "codex_vscode".to_string(), - title: Some("Codex VS Code Extension".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_originator_via_env_var/")); - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn initialize_rejects_invalid_client_name() -> Result<()> { - let codex_home = TempDir::new()?; - let mut mcp = McpProcess::new_with_env( - codex_home.path(), - &[("CODEX_INTERNAL_ORIGINATOR_OVERRIDE", None)], - ) - .await?; - - let message = timeout( - DEFAULT_READ_TIMEOUT, - mcp.initialize_with_client_info(ClientInfo { - name: "bad\rname".to_string(), - title: Some("Bad Client".to_string()), - version: "0.1.0".to_string(), - }), - ) - .await??; - - let JSONRPCMessage::Error(error) = message else { - anyhow::bail!("expected initialize error, got {message:?}"); - }; - - assert_eq!(error.error.code, -32600); - assert_eq!( - error.error.message, - "Invalid clientInfo.name: 'bad\rname'. Must be a valid HTTP header value." - ); - assert_eq!(error.error.data, None); - Ok(()) -} diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 5c40c5fc164..44f417d8bdb 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -1,6 +1,5 @@ mod account; mod config_rpc; -mod initialize; mod model_list; mod output_schema; mod rate_limits; diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 741487babe0..1a36433742d 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -8,7 +8,6 @@ use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::create_shell_command_sse_response; use app_test_support::format_with_current_shell_display; use app_test_support::to_response; -use codex_app_server_protocol::ClientInfo; use codex_app_server_protocol::CommandExecutionApprovalDecision; use codex_app_server_protocol::CommandExecutionRequestApprovalResponse; use codex_app_server_protocol::CommandExecutionStatus; @@ -41,76 +40,6 @@ use tempfile::TempDir; use tokio::time::timeout; const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); -const TEST_ORIGINATOR: &str = "codex_vscode"; - -#[tokio::test] -async fn turn_start_sends_originator_header() -> Result<()> { - let responses = vec![create_final_assistant_message_sse_response("Done")?]; - let server = create_mock_chat_completions_server_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?; - timeout( - DEFAULT_READ_TIMEOUT, - mcp.initialize_with_client_info(ClientInfo { - name: TEST_ORIGINATOR.to_string(), - title: Some("Codex VS Code Extension".to_string()), - version: "0.1.0".to_string(), - }), - ) - .await??; - - let thread_req = mcp - .send_thread_start_request(ThreadStartParams { - model: Some("mock-model".to_string()), - ..Default::default() - }) - .await?; - let thread_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), - ) - .await??; - let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; - - let turn_req = mcp - .send_turn_start_request(TurnStartParams { - thread_id: thread.id.clone(), - input: vec![V2UserInput::Text { - text: "Hello".to_string(), - }], - ..Default::default() - }) - .await?; - timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), - ) - .await??; - - timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_notification_message("turn/completed"), - ) - .await??; - - let requests = server - .received_requests() - .await - .expect("failed to fetch received requests"); - assert!(!requests.is_empty()); - for request in requests { - let originator = request - .headers - .get("originator") - .expect("originator header missing"); - assert_eq!(originator.to_str()?, TEST_ORIGINATOR); - } - - Ok(()) -} #[tokio::test] async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<()> { diff --git a/codex-rs/core/src/default_client.rs b/codex-rs/core/src/default_client.rs index 4ded10a3d90..3cd882489d0 100644 --- a/codex-rs/core/src/default_client.rs +++ b/codex-rs/core/src/default_client.rs @@ -4,7 +4,7 @@ pub use codex_client::CodexRequestBuilder; use reqwest::header::HeaderValue; use std::sync::LazyLock; use std::sync::Mutex; -use std::sync::RwLock; +use std::sync::OnceLock; /// Set this to add a suffix to the User-Agent string. /// @@ -30,7 +30,7 @@ pub struct Originator { pub value: String, pub header_value: HeaderValue, } -static ORIGINATOR: LazyLock>> = LazyLock::new(|| RwLock::new(None)); +static ORIGINATOR: OnceLock = OnceLock::new(); #[derive(Debug)] pub enum SetOriginatorError { @@ -60,48 +60,22 @@ fn get_originator_value(provided: Option) -> Originator { } pub fn set_default_originator(value: String) -> Result<(), SetOriginatorError> { - if HeaderValue::from_str(&value).is_err() { - return Err(SetOriginatorError::InvalidHeaderValue); - } let originator = get_originator_value(Some(value)); - let Ok(mut guard) = ORIGINATOR.write() else { - return Err(SetOriginatorError::AlreadyInitialized); - }; - if guard.is_some() { - return Err(SetOriginatorError::AlreadyInitialized); - } - *guard = Some(originator); - Ok(()) + ORIGINATOR + .set(originator) + .map_err(|_| SetOriginatorError::AlreadyInitialized) } -pub fn originator() -> Originator { - if let Ok(guard) = ORIGINATOR.read() - && let Some(originator) = guard.as_ref() - { - return originator.clone(); - } - - if std::env::var(CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR).is_ok() { - let originator = get_originator_value(None); - if let Ok(mut guard) = ORIGINATOR.write() { - match guard.as_ref() { - Some(originator) => return originator.clone(), - None => *guard = Some(originator.clone()), - } - } - return originator; - } - - get_originator_value(None) +pub fn originator() -> &'static Originator { + ORIGINATOR.get_or_init(|| get_originator_value(None)) } pub fn get_codex_user_agent() -> String { let build_version = env!("CARGO_PKG_VERSION"); let os_info = os_info::get(); - let originator = originator(); let prefix = format!( "{}/{build_version} ({} {}; {}) {}", - originator.value.as_str(), + originator().value.as_str(), os_info.os_type(), os_info.version(), os_info.architecture().unwrap_or("unknown"), @@ -149,7 +123,7 @@ fn sanitize_user_agent(candidate: String, fallback: &str) -> String { tracing::warn!( "Falling back to default Codex originator because base user agent string is invalid" ); - originator().value + originator().value.clone() } } @@ -163,7 +137,7 @@ pub fn build_reqwest_client() -> reqwest::Client { use reqwest::header::HeaderMap; let mut headers = HeaderMap::new(); - headers.insert("originator", originator().header_value); + headers.insert("originator", originator().header_value.clone()); let ua = get_codex_user_agent(); let mut builder = reqwest::Client::builder() @@ -189,7 +163,7 @@ mod tests { #[test] fn test_get_codex_user_agent() { let user_agent = get_codex_user_agent(); - let originator = originator().value; + let originator = originator().value.as_str(); let prefix = format!("{originator}/"); assert!(user_agent.starts_with(&prefix)); } diff --git a/codex-rs/core/src/otel_init.rs b/codex-rs/core/src/otel_init.rs index e8dd22f4c48..5ba714959ca 100644 --- a/codex-rs/core/src/otel_init.rs +++ b/codex-rs/core/src/otel_init.rs @@ -15,7 +15,6 @@ use std::error::Error; pub fn build_provider( config: &Config, service_version: &str, - service_name_override: Option<&str>, ) -> Result, Box> { let to_otel_exporter = |kind: &Kind| match kind { Kind::None => OtelExporter::None, @@ -71,11 +70,8 @@ pub fn build_provider( OtelExporter::None }; - let originator = originator(); - let service_name = service_name_override.unwrap_or(originator.value.as_str()); - OtelProvider::from(&OtelSettings { - service_name: service_name.to_string(), + service_name: originator().value.to_owned(), service_version: service_version.to_string(), codex_home: config.codex_home.clone(), environment: config.otel.environment.to_string(), diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index 80d95e6256e..d571ad191e6 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -143,7 +143,7 @@ impl RolloutRecorder { id: session_id, timestamp, cwd: config.cwd.clone(), - originator: originator().value, + originator: originator().value.clone(), cli_version: env!("CARGO_PKG_VERSION").to_string(), instructions, source, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 014257910d5..44ce1d6e2bd 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -223,7 +223,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any std::process::exit(1); } - let otel = codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION"), None); + let otel = codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION")); #[allow(clippy::print_stderr)] let otel = match otel { diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 1680425ddb0..f4e5e771fcc 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -298,7 +298,7 @@ pub async fn run_main( ensure_oss_provider_ready(provider_id, &config).await?; } - let otel = codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION"), None); + let otel = codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION")); #[allow(clippy::print_stderr)] let otel = match otel { diff --git a/codex-rs/tui2/src/lib.rs b/codex-rs/tui2/src/lib.rs index e5b207f5dac..ac062cf667c 100644 --- a/codex-rs/tui2/src/lib.rs +++ b/codex-rs/tui2/src/lib.rs @@ -313,7 +313,7 @@ pub async fn run_main( ensure_oss_provider_ready(provider_id, &config).await?; } - let otel = codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION"), None); + let otel = codex_core::otel_init::build_provider(&config, env!("CARGO_PKG_VERSION")); #[allow(clippy::print_stderr)] let otel = match otel {