Add cached environment manager for exec server URL (#15785)

Add environment manager that is a singleton and is created early in
app-server (before skill manager, before config loading).

Use an environment variable to point to a running exec server.
This commit is contained in:
pakrym-oai
2026-03-25 16:14:36 -07:00
committed by GitHub
Unverified
parent f24c55f0d5
commit 8fa88fa8ca
32 changed files with 286 additions and 83 deletions
+21
View File
@@ -75,6 +75,9 @@ impl AgentControlHarness {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.clone(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
);
let control = manager.agent_control();
Self {
@@ -811,6 +814,9 @@ async fn spawn_agent_respects_max_threads_limit() {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.clone(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
);
let control = manager.agent_control();
@@ -854,6 +860,9 @@ async fn spawn_agent_releases_slot_after_shutdown() {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.clone(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
);
let control = manager.agent_control();
@@ -888,6 +897,9 @@ async fn spawn_agent_limit_shared_across_clones() {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.clone(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
);
let control = manager.agent_control();
let cloned = control.clone();
@@ -924,6 +936,9 @@ async fn resume_agent_respects_max_threads_limit() {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.clone(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
);
let control = manager.agent_control();
@@ -971,6 +986,9 @@ async fn resume_agent_releases_slot_after_resume_failure() {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.clone(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
);
let control = manager.agent_control();
@@ -1361,6 +1379,9 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.clone(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
);
let control = manager.agent_control();
let harness = AgentControlHarness {
+6 -3
View File
@@ -57,6 +57,7 @@ use codex_analytics::build_track_events_context;
use codex_app_server_protocol::McpServerElicitationRequest;
use codex_app_server_protocol::McpServerElicitationRequestParams;
use codex_exec_server::Environment;
use codex_exec_server::EnvironmentManager;
use codex_features::FEATURES;
use codex_features::Feature;
use codex_features::unstable_features_warning_event;
@@ -402,6 +403,7 @@ pub(crate) struct CodexSpawnArgs {
pub(crate) config: Config,
pub(crate) auth_manager: Arc<AuthManager>,
pub(crate) models_manager: Arc<ModelsManager>,
pub(crate) environment_manager: Arc<EnvironmentManager>,
pub(crate) skills_manager: Arc<SkillsManager>,
pub(crate) plugins_manager: Arc<PluginsManager>,
pub(crate) mcp_manager: Arc<McpManager>,
@@ -455,6 +457,7 @@ impl Codex {
mut config,
auth_manager,
models_manager,
environment_manager,
skills_manager,
plugins_manager,
mcp_manager,
@@ -646,6 +649,7 @@ impl Codex {
agent_status_tx.clone(),
conversation_history,
session_source_clone,
environment_manager,
skills_manager,
plugins_manager,
mcp_manager.clone(),
@@ -1450,6 +1454,7 @@ impl Session {
agent_status: watch::Sender<AgentStatus>,
initial_history: InitialHistory,
session_source: SessionSource,
environment_manager: Arc<EnvironmentManager>,
skills_manager: Arc<SkillsManager>,
plugins_manager: Arc<PluginsManager>,
mcp_manager: Arc<McpManager>,
@@ -1884,9 +1889,7 @@ impl Session {
code_mode_service: crate::tools::code_mode::CodeModeService::new(
config.js_repl_node_path.clone(),
),
environment: Arc::new(
Environment::create(config.experimental_exec_server_url.clone()).await?,
),
environment: environment_manager.current().await?,
};
let js_repl = Arc::new(JsReplHandle::with_node_path(
config.js_repl_node_path.clone(),
+4
View File
@@ -4,6 +4,7 @@ use std::sync::Arc;
use async_channel::Receiver;
use async_channel::Sender;
use codex_async_utils::OrCancelExt;
use codex_exec_server::EnvironmentManager;
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
@@ -76,6 +77,9 @@ pub(crate) async fn run_codex_thread_interactive(
config,
auth_manager,
models_manager,
environment_manager: Arc::new(EnvironmentManager::new(
parent_ctx.environment.exec_server_url().map(str::to_owned),
)),
skills_manager: Arc::clone(&parent_session.services.skills_manager),
plugins_manager: Arc::clone(&parent_session.services.plugins_manager),
mcp_manager: Arc::clone(&parent_session.services.mcp_manager),
+5 -2
View File
@@ -2551,6 +2551,9 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
agent_status_tx,
InitialHistory::New,
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
skills_manager,
plugins_manager,
mcp_manager,
@@ -2644,7 +2647,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone(), true));
let network_approval = Arc::new(NetworkApprovalService::default());
let environment = Arc::new(
codex_exec_server::Environment::create(None)
codex_exec_server::Environment::create(/*exec_server_url*/ None)
.await
.expect("create environment"),
);
@@ -3482,7 +3485,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone(), true));
let network_approval = Arc::new(NetworkApprovalService::default());
let environment = Arc::new(
codex_exec_server::Environment::create(None)
codex_exec_server::Environment::create(/*exec_server_url*/ None)
.await
.expect("create environment"),
);
@@ -12,6 +12,7 @@ use crate::sandboxing::SandboxPermissions;
use crate::tools::context::FunctionToolOutput;
use crate::turn_diff_tracker::TurnDiffTracker;
use codex_app_server_protocol::ConfigLayerSource;
use codex_exec_server::EnvironmentManager;
use codex_execpolicy::Decision;
use codex_execpolicy::Evaluation;
use codex_execpolicy::RuleMatch;
@@ -437,6 +438,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
config,
auth_manager,
models_manager,
environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
skills_manager,
plugins_manager,
mcp_manager,
-32
View File
@@ -4335,7 +4335,6 @@ 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,
@@ -4478,7 +4477,6 @@ 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,
@@ -4619,7 +4617,6 @@ 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,
@@ -4746,7 +4743,6 @@ 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,
@@ -5964,34 +5960,6 @@ 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(
-9
View File
@@ -526,10 +526,6 @@ 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<String>,
/// Machine-local realtime audio device preferences used by realtime voice.
pub realtime_audio: RealtimeAudioConfig,
@@ -1319,10 +1315,6 @@ pub struct ConfigToml {
/// Base URL override for the built-in `openai` model provider.
pub openai_base_url: Option<String>,
/// Experimental / do not use. Overrides the URL used when connecting to
/// a remote exec server.
pub experimental_exec_server_url: Option<String>,
/// Machine-local realtime audio device preferences used by realtime voice.
#[serde(default)]
pub audio: Option<RealtimeAudioToml>,
@@ -2678,7 +2670,6 @@ 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 {
+3
View File
@@ -484,6 +484,9 @@ mod phase2 {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.clone(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
);
let (mut session, _turn_context) = make_session_and_context().await;
session.services.state_db = Some(Arc::clone(&state_db));
+8 -1
View File
@@ -7,6 +7,7 @@
use std::path::PathBuf;
use std::sync::Arc;
use codex_exec_server::EnvironmentManager;
use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelPreset;
@@ -60,8 +61,14 @@ pub fn thread_manager_with_models_provider_and_home(
auth: CodexAuth,
provider: ModelProviderInfo,
codex_home: PathBuf,
environment_manager: Arc<EnvironmentManager>,
) -> ThreadManager {
ThreadManager::with_models_provider_and_home_for_tests(auth, provider, codex_home)
ThreadManager::with_models_provider_and_home_for_tests(
auth,
provider,
codex_home,
environment_manager,
)
}
pub async fn start_thread_with_user_shell_override(
+13 -2
View File
@@ -28,6 +28,7 @@ use crate::skills_watcher::SkillsWatcherEvent;
use crate::tasks::interrupted_turn_history_marker;
use codex_app_server_protocol::ThreadHistoryBuilder;
use codex_app_server_protocol::TurnStatus;
use codex_exec_server::EnvironmentManager;
use codex_protocol::ThreadId;
use codex_protocol::config_types::CollaborationModeMask;
#[cfg(test)]
@@ -200,6 +201,7 @@ pub(crate) struct ThreadManagerState {
thread_created_tx: broadcast::Sender<ThreadId>,
auth_manager: Arc<AuthManager>,
models_manager: Arc<ModelsManager>,
environment_manager: Arc<EnvironmentManager>,
skills_manager: Arc<SkillsManager>,
plugins_manager: Arc<PluginsManager>,
mcp_manager: Arc<McpManager>,
@@ -215,6 +217,7 @@ impl ThreadManager {
auth_manager: Arc<AuthManager>,
session_source: SessionSource,
collaboration_modes_config: CollaborationModesConfig,
environment_manager: Arc<EnvironmentManager>,
) -> Self {
let codex_home = config.codex_home.clone();
let restriction_product = session_source.restriction_product();
@@ -246,6 +249,7 @@ impl ThreadManager {
collaboration_modes_config,
openai_models_provider,
)),
environment_manager,
skills_manager,
plugins_manager,
mcp_manager,
@@ -272,8 +276,12 @@ impl ThreadManager {
));
std::fs::create_dir_all(&codex_home)
.unwrap_or_else(|err| panic!("temp codex home dir create failed: {err}"));
let mut manager =
Self::with_models_provider_and_home_for_tests(auth, provider, codex_home.clone());
let mut manager = Self::with_models_provider_and_home_for_tests(
auth,
provider,
codex_home.clone(),
Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
);
manager._test_codex_home_guard = Some(TempCodexHomeGuard { path: codex_home });
manager
}
@@ -284,6 +292,7 @@ impl ThreadManager {
auth: CodexAuth,
provider: ModelProviderInfo,
codex_home: PathBuf,
environment_manager: Arc<EnvironmentManager>,
) -> Self {
set_thread_manager_test_mode_for_tests(/*enabled*/ true);
let auth_manager = AuthManager::from_auth_for_testing(auth);
@@ -309,6 +318,7 @@ impl ThreadManager {
auth_manager.clone(),
provider,
)),
environment_manager,
skills_manager,
plugins_manager,
mcp_manager,
@@ -844,6 +854,7 @@ impl ThreadManagerState {
config,
auth_manager,
models_manager: Arc::clone(&self.models_manager),
environment_manager: Arc::clone(&self.environment_manager),
skills_manager: Arc::clone(&self.skills_manager),
plugins_manager: Arc::clone(&self.plugins_manager),
mcp_manager: Arc::clone(&self.mcp_manager),
+15
View File
@@ -244,6 +244,9 @@ async fn shutdown_all_threads_bounded_submits_shutdown_to_every_thread() {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.clone(),
Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
);
let thread_1 = manager
.start_thread(config.clone())
@@ -292,6 +295,9 @@ async fn new_uses_configured_openai_provider_for_model_refresh() {
auth_manager,
SessionSource::Exec,
CollaborationModesConfig::default(),
Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
);
let _ = manager.list_models(RefreshStrategy::Online).await;
@@ -419,6 +425,9 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
auth_manager.clone(),
SessionSource::Exec,
CollaborationModesConfig::default(),
Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
);
let source = manager
@@ -516,6 +525,9 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
auth_manager.clone(),
SessionSource::Exec,
CollaborationModesConfig::default(),
Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
);
let source = manager
@@ -602,6 +614,9 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
auth_manager.clone(),
SessionSource::Exec,
CollaborationModesConfig::default(),
Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
);
let source = manager