From 1837038f4e65ba37022d0163894cf29883b4d620 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Thu, 19 Mar 2026 11:25:11 -0700 Subject: [PATCH] Add experimental exec server URL handling (#15196) Add a config and attempt to start the server. --- codex-rs/app-server/src/fs_api.rs | 2 +- codex-rs/core/config.schema.json | 4 ++ codex-rs/core/src/codex.rs | 4 +- codex-rs/core/src/codex_tests.rs | 12 ++++- codex-rs/core/src/config/config_tests.rs | 32 +++++++++++ codex-rs/core/src/config/mod.rs | 9 ++++ codex-rs/exec-server/src/environment.rs | 69 +++++++++++++++++++++++- 7 files changed, 126 insertions(+), 6 deletions(-) diff --git a/codex-rs/app-server/src/fs_api.rs b/codex-rs/app-server/src/fs_api.rs index 1d53bbe18..9baa2b1dc 100644 --- a/codex-rs/app-server/src/fs_api.rs +++ b/codex-rs/app-server/src/fs_api.rs @@ -34,7 +34,7 @@ pub(crate) struct FsApi { impl Default for FsApi { fn default() -> Self { Self { - file_system: Arc::new(Environment.get_filesystem()), + file_system: Arc::new(Environment::default().get_filesystem()), } } } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index b2f88d334..bf0de487b 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1895,6 +1895,10 @@ "experimental_compact_prompt_file": { "$ref": "#/definitions/AbsolutePathBuf" }, + "experimental_exec_server_url": { + "description": "Experimental / do not use. Overrides the URL used when connecting to a remote exec server.", + "type": "string" + }, "experimental_realtime_start_instructions": { "description": "Experimental / do not use. Replaces the built-in realtime start instructions inserted into developer messages when realtime becomes active.", "type": "string" diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 76ef376d1..83fb05626 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1826,7 +1826,9 @@ impl Session { code_mode_service: crate::tools::code_mode::CodeModeService::new( config.js_repl_node_path.clone(), ), - environment: Arc::new(Environment), + environment: Arc::new( + Environment::create(config.experimental_exec_server_url.clone()).await?, + ), }; let js_repl = Arc::new(JsReplHandle::with_node_path( config.js_repl_node_path.clone(), diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 787ad399b..0c115d8be 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -2450,7 +2450,11 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { true, )); let network_approval = Arc::new(NetworkApprovalService::default()); - let environment = Arc::new(codex_exec_server::Environment); + let environment = Arc::new( + codex_exec_server::Environment::create(None) + .await + .expect("create environment"), + ); let file_watcher = Arc::new(FileWatcher::noop()); let services = SessionServices { @@ -3244,7 +3248,11 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( true, )); let network_approval = Arc::new(NetworkApprovalService::default()); - let environment = Arc::new(codex_exec_server::Environment); + let environment = Arc::new( + codex_exec_server::Environment::create(None) + .await + .expect("create environment"), + ); let file_watcher = Arc::new(FileWatcher::noop()); let services = SessionServices { diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index ee856664d..3d3da045b 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -4310,6 +4310,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { model_verbosity: None, personality: Some(Personality::Pragmatic), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), + experimental_exec_server_url: None, realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_start_instructions: None, experimental_realtime_ws_base_url: None, @@ -4451,6 +4452,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { model_verbosity: None, personality: Some(Personality::Pragmatic), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), + experimental_exec_server_url: None, realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_start_instructions: None, experimental_realtime_ws_base_url: None, @@ -4590,6 +4592,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { model_verbosity: None, personality: Some(Personality::Pragmatic), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), + experimental_exec_server_url: None, realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_start_instructions: None, experimental_realtime_ws_base_url: None, @@ -4715,6 +4718,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { model_verbosity: Some(Verbosity::High), personality: Some(Personality::Pragmatic), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), + experimental_exec_server_url: None, realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_start_instructions: None, experimental_realtime_ws_base_url: None, @@ -5974,6 +5978,34 @@ experimental_realtime_start_instructions = "start instructions from config" Ok(()) } +#[test] +fn experimental_exec_server_url_loads_from_config_toml() -> std::io::Result<()> { + let cfg: ConfigToml = toml::from_str( + r#" +experimental_exec_server_url = "http://127.0.0.1:8080" +"#, + ) + .expect("TOML deserialization should succeed"); + + assert_eq!( + cfg.experimental_exec_server_url.as_deref(), + Some("http://127.0.0.1:8080") + ); + + let codex_home = TempDir::new()?; + let config = Config::load_from_base_config_with_overrides( + cfg, + ConfigOverrides::default(), + codex_home.path().to_path_buf(), + )?; + + assert_eq!( + config.experimental_exec_server_url.as_deref(), + Some("http://127.0.0.1:8080") + ); + Ok(()) +} + #[test] fn experimental_realtime_ws_base_url_loads_from_config_toml() -> std::io::Result<()> { let cfg: ConfigToml = toml::from_str( diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index a1f270458..8d0d32330 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -493,6 +493,10 @@ pub struct Config { /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). pub chatgpt_base_url: String, + /// Experimental / do not use. Overrides the URL used when connecting to + /// a remote exec server. + pub experimental_exec_server_url: Option, + /// Machine-local realtime audio device preferences used by realtime voice. pub realtime_audio: RealtimeAudioConfig, @@ -1393,6 +1397,10 @@ pub struct ConfigToml { /// Base URL override for the built-in `openai` model provider. pub openai_base_url: Option, + /// Experimental / do not use. Overrides the URL used when connecting to + /// a remote exec server. + pub experimental_exec_server_url: Option, + /// Machine-local realtime audio device preferences used by realtime voice. #[serde(default)] pub audio: Option, @@ -2745,6 +2753,7 @@ impl Config { .chatgpt_base_url .or(cfg.chatgpt_base_url) .unwrap_or("https://chatgpt.com/backend-api/".to_string()), + experimental_exec_server_url: cfg.experimental_exec_server_url, realtime_audio: cfg .audio .map_or_else(RealtimeAudioConfig::default, |audio| RealtimeAudioConfig { diff --git a/codex-rs/exec-server/src/environment.rs b/codex-rs/exec-server/src/environment.rs index eb8658780..c8635ec03 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -1,11 +1,76 @@ +use crate::ExecServerClient; +use crate::ExecServerError; +use crate::RemoteExecServerConnectArgs; use crate::fs; use crate::fs::ExecutorFileSystem; -#[derive(Clone, Debug, Default)] -pub struct Environment; +#[derive(Clone, Default)] +pub struct Environment { + experimental_exec_server_url: Option, + remote_exec_server_client: Option, +} + +impl std::fmt::Debug for Environment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Environment") + .field( + "experimental_exec_server_url", + &self.experimental_exec_server_url, + ) + .field( + "has_remote_exec_server_client", + &self.remote_exec_server_client.is_some(), + ) + .finish() + } +} impl Environment { + pub async fn create( + experimental_exec_server_url: Option, + ) -> Result { + let remote_exec_server_client = + if let Some(websocket_url) = experimental_exec_server_url.as_deref() { + Some( + ExecServerClient::connect_websocket(RemoteExecServerConnectArgs::new( + websocket_url.to_string(), + "codex-core".to_string(), + )) + .await?, + ) + } else { + None + }; + + Ok(Self { + experimental_exec_server_url, + remote_exec_server_client, + }) + } + + pub fn experimental_exec_server_url(&self) -> Option<&str> { + self.experimental_exec_server_url.as_deref() + } + + pub fn remote_exec_server_client(&self) -> Option<&ExecServerClient> { + self.remote_exec_server_client.as_ref() + } + pub fn get_filesystem(&self) -> impl ExecutorFileSystem + use<> { fs::LocalFileSystem } } + +#[cfg(test)] +mod tests { + use super::Environment; + use pretty_assertions::assert_eq; + + #[tokio::test] + async fn create_without_remote_exec_server_url_does_not_connect() { + let environment = Environment::create(None).await.expect("create environment"); + + assert_eq!(environment.experimental_exec_server_url(), None); + assert!(environment.remote_exec_server_client().is_none()); + } +}