Support multiple managed environments (#18401)

## Summary
- refactor EnvironmentManager to own keyed environments with
default/local lookup helpers
- keep remote exec-server client creation lazy until exec/fs use
- preserve disabled agent environment access separately from internal
local environment access

## Validation
- not run (per Codex worktree instruction to avoid tests/builds unless
requested)

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
starr-openai
2026-04-21 15:29:35 -07:00
committed by GitHub
Unverified
parent 27d9673273
commit ddbe2536be
44 changed files with 606 additions and 432 deletions
+17 -5
View File
@@ -46,6 +46,7 @@ use codex_core::config::Config;
use codex_core::config_loader::CloudRequirementsLoader;
use codex_core::config_loader::LoaderOverrides;
pub use codex_exec_server::EnvironmentManager;
pub use codex_exec_server::EnvironmentManagerArgs;
pub use codex_exec_server::ExecServerRuntimePaths;
use codex_feedback::CodexFeedback;
use codex_protocol::protocol::SessionSource;
@@ -968,7 +969,7 @@ mod tests {
cloud_requirements: CloudRequirementsLoader::default(),
feedback: CodexFeedback::new(),
log_db: None,
environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
config_warnings: Vec::new(),
session_source,
enable_codex_api_key_env: false,
@@ -1969,9 +1970,14 @@ mod tests {
#[tokio::test]
async fn runtime_start_args_forward_environment_manager() {
let config = Arc::new(build_test_config().await);
let environment_manager = Arc::new(EnvironmentManager::new(Some(
"ws://127.0.0.1:8765".to_string(),
)));
let environment_manager = Arc::new(EnvironmentManager::new(EnvironmentManagerArgs {
exec_server_url: Some("ws://127.0.0.1:8765".to_string()),
local_runtime_paths: ExecServerRuntimePaths::new(
std::env::current_exe().expect("current exe"),
/*codex_linux_sandbox_exe*/ None,
)
.expect("runtime paths"),
}));
let runtime_args = InProcessClientStartArgs {
arg0_paths: Arg0DispatchPaths::default(),
@@ -1998,7 +2004,13 @@ mod tests {
&runtime_args.environment_manager,
&environment_manager
));
assert!(runtime_args.environment_manager.is_remote());
assert!(
runtime_args
.environment_manager
.default_environment()
.expect("default environment")
.is_remote()
);
}
#[tokio::test]
@@ -3497,9 +3497,7 @@ mod tests {
CodexAuth::create_dummy_chatgpt_auth_for_testing(),
config.model_provider.clone(),
config.codex_home.to_path_buf(),
Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
),
);
let codex_core::NewThread {
@@ -260,6 +260,7 @@ use codex_core_plugins::loader::load_plugin_mcp_servers;
use codex_core_plugins::manifest::PluginManifestInterface;
use codex_core_plugins::marketplace::MarketplaceError;
use codex_core_plugins::marketplace::MarketplacePluginSource;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::LOCAL_FS;
use codex_features::FEATURES;
use codex_features::Feature;
@@ -5677,27 +5678,17 @@ impl CodexMessageProcessor {
.to_mcp_config(self.thread_manager.plugins_manager().as_ref())
.await;
let auth = self.auth_manager.auth().await;
let runtime_environment = match self.thread_manager.environment_manager().current().await {
Ok(Some(environment)) => {
let environment_manager = self.thread_manager.environment_manager();
let runtime_environment = match environment_manager.default_environment() {
Some(environment) => {
// Status listing has no turn cwd. This fallback is used only
// by executor-backed stdio MCPs whose config omits `cwd`.
McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf())
}
Ok(None) => McpRuntimeEnvironment::new(
Arc::new(codex_exec_server::Environment::default()),
None => McpRuntimeEnvironment::new(
environment_manager.local_environment(),
config.cwd.to_path_buf(),
),
Err(err) => {
// TODO(aibrahim): Investigate degrading MCP status listing when
// executor environment creation fails.
let error = JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!("failed to create environment: {err}"),
data: None,
};
self.outgoing.send_error(request, error).await;
return;
}
};
tokio::spawn(async move {
@@ -5856,25 +5847,14 @@ impl CodexMessageProcessor {
.to_mcp_config(self.thread_manager.plugins_manager().as_ref())
.await;
let auth = self.auth_manager.auth().await;
let runtime_environment = match self.thread_manager.environment_manager().current().await {
Ok(Some(environment)) => {
// Resource reads without a thread have no turn cwd. This fallback
// is used only by executor-backed stdio MCPs whose config omits `cwd`.
McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf())
}
Ok(None) => McpRuntimeEnvironment::new(
Arc::new(codex_exec_server::Environment::default()),
config.cwd.to_path_buf(),
),
Err(err) => {
let error = JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!("failed to create environment: {err}"),
data: None,
};
self.outgoing.send_error(request_id, error).await;
return;
}
let runtime_environment = {
let environment_manager = self.thread_manager.environment_manager();
let environment = environment_manager
.default_environment()
.unwrap_or_else(|| environment_manager.local_environment());
// Resource reads without a thread have no turn cwd. This fallback
// is used only by executor-backed stdio MCPs whose config omits `cwd`.
McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf())
};
tokio::spawn(async move {
@@ -6222,8 +6202,9 @@ impl CodexMessageProcessor {
let request = request_id.clone();
let outgoing = Arc::clone(&self.outgoing);
let environment_manager = self.thread_manager.environment_manager();
tokio::spawn(async move {
Self::apps_list_task(outgoing, request, params, config).await;
Self::apps_list_task(outgoing, request, params, config, environment_manager).await;
});
}
@@ -6232,6 +6213,7 @@ impl CodexMessageProcessor {
request_id: ConnectionRequestId,
params: AppsListParams,
config: Config,
environment_manager: Arc<EnvironmentManager>,
) {
let AppsListParams {
cursor,
@@ -6266,12 +6248,15 @@ impl CodexMessageProcessor {
let accessible_config = config.clone();
let accessible_tx = tx.clone();
tokio::spawn(async move {
let result = connectors::list_accessible_connectors_from_mcp_tools_with_options(
&accessible_config,
force_refetch,
)
.await
.map_err(|err| format!("failed to load accessible apps: {err}"));
let result =
connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
&accessible_config,
force_refetch,
&environment_manager,
)
.await
.map(|status| status.connectors)
.map_err(|err| format!("failed to load accessible apps: {err}"));
let _ = accessible_tx.send(AppListLoadResult::Accessible(result));
});
@@ -6461,23 +6446,11 @@ impl CodexMessageProcessor {
};
let skills_manager = self.thread_manager.skills_manager();
let plugins_manager = self.thread_manager.plugins_manager();
let fs = match self.thread_manager.environment_manager().current().await {
Ok(Some(environment)) => Some(environment.get_filesystem()),
Ok(None) => None,
Err(err) => {
self.outgoing
.send_error(
request_id,
JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!("failed to create environment: {err}"),
data: None,
},
)
.await;
return;
}
};
let fs = self
.thread_manager
.environment_manager()
.default_environment()
.map(|environment| environment.get_filesystem());
let mut data = Vec::new();
for cwd in cwds {
let extra_roots = extra_roots_by_cwd
@@ -6793,8 +6766,13 @@ impl CodexMessageProcessor {
return;
}
};
let app_summaries =
plugin_app_helpers::load_plugin_app_summaries(&config, &outcome.plugin.apps).await;
let environment_manager = self.thread_manager.environment_manager();
let app_summaries = plugin_app_helpers::load_plugin_app_summaries(
&config,
&outcome.plugin.apps,
&environment_manager,
)
.await;
let visible_skills = outcome
.plugin
.skills
@@ -6971,10 +6949,11 @@ impl CodexMessageProcessor {
) {
Vec::new()
} else {
let environment_manager = self.thread_manager.environment_manager();
let (all_connectors_result, accessible_connectors_result) = tokio::join!(
connectors::list_all_connectors_with_options(&config, /*force_refetch*/ true),
connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status(
&config, /*force_refetch*/ true
connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
&config, /*force_refetch*/ true, &environment_manager
),
);
@@ -5,11 +5,13 @@ use codex_app_server_protocol::AppSummary;
use codex_chatgpt::connectors;
use codex_core::config::Config;
use codex_core::plugins::AppConnectorId;
use codex_exec_server::EnvironmentManager;
use tracing::warn;
pub(super) async fn load_plugin_app_summaries(
config: &Config,
plugin_apps: &[AppConnectorId],
environment_manager: &EnvironmentManager,
) -> Vec<AppSummary> {
if plugin_apps.is_empty() {
return Vec::new();
@@ -29,8 +31,10 @@ pub(super) async fn load_plugin_app_summaries(
let plugin_connectors = connectors::connectors_for_plugin_apps(connectors, plugin_apps);
let accessible_connectors =
match connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status(
config, /*force_refetch*/ false,
match connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
config,
/*force_refetch*/ false,
environment_manager,
)
.await
{
+4 -9
View File
@@ -20,7 +20,6 @@ use codex_app_server_protocol::FsWriteFileResponse;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_exec_server::CopyOptions;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::Environment;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::RemoveOptions;
use std::io;
@@ -31,15 +30,11 @@ pub(crate) struct FsApi {
file_system: Arc<dyn ExecutorFileSystem>,
}
impl Default for FsApi {
fn default() -> Self {
Self {
file_system: Environment::default().get_filesystem(),
}
}
}
impl FsApi {
pub(crate) fn new(file_system: Arc<dyn ExecutorFileSystem>) -> Self {
Self { file_system }
}
pub(crate) async fn read_file(
&self,
params: FsReadFileParams,
+1 -1
View File
@@ -750,7 +750,7 @@ mod tests {
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
log_db: None,
environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
config_warnings: Vec::new(),
session_source,
enable_codex_api_key_env: false,
+2 -1
View File
@@ -6,6 +6,7 @@ use codex_config::ThreadConfigLoader;
use codex_core::config::Config;
use codex_core::config_loader::ConfigLayerStackOrdering;
use codex_core::config_loader::LoaderOverrides;
use codex_exec_server::EnvironmentManagerArgs;
use codex_features::Feature;
use codex_login::AuthManager;
use codex_utils_cli::CliConfigOverrides;
@@ -360,7 +361,7 @@ pub async fn run_main_with_transport(
session_source: SessionSource,
auth: AppServerWebsocketAuthSettings,
) -> IoResult<()> {
let environment_manager = Arc::new(EnvironmentManager::from_env_with_runtime_paths(Some(
let environment_manager = Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::from_env(
ExecServerRuntimePaths::from_optional_paths(
arg0_paths.codex_self_exe.clone(),
arg0_paths.codex_linux_sandbox_exe.clone(),
+12 -4
View File
@@ -328,7 +328,12 @@ impl MessageProcessor {
let device_key_api = DeviceKeyApi::default();
let external_agent_config_api =
ExternalAgentConfigApi::new(config.codex_home.to_path_buf());
let fs_api = FsApi::default();
let fs_api = FsApi::new(
thread_manager
.environment_manager()
.local_environment()
.get_filesystem(),
);
let fs_watch_manager = FsWatchManager::new(outgoing.clone());
Self {
@@ -1079,11 +1084,14 @@ impl MessageProcessor {
}
let outgoing = Arc::clone(&self.outgoing);
let environment_manager = self.thread_manager.environment_manager();
tokio::spawn(async move {
let (all_connectors_result, accessible_connectors_result) = tokio::join!(
connectors::list_all_connectors_with_options(&config, /*force_refetch*/ true),
connectors::list_accessible_connectors_from_mcp_tools_with_options(
&config, /*force_refetch*/ true,
connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
&config,
/*force_refetch*/ true,
&environment_manager,
),
);
let all_connectors = match all_connectors_result {
@@ -1096,7 +1104,7 @@ impl MessageProcessor {
}
};
let accessible_connectors = match accessible_connectors_result {
Ok(connectors) => connectors,
Ok(status) => status.connectors,
Err(err) => {
tracing::warn!(
"failed to force-refresh accessible apps after experimental feature enablement: {err:#}"
@@ -279,7 +279,7 @@ fn build_test_processor(
arg0_paths: Arg0DispatchPaths::default(),
config,
config_manager,
environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
feedback: CodexFeedback::new(),
log_db: None,
config_warnings: Vec::new(),
@@ -204,7 +204,7 @@ async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()> {
thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader),
feedback: CodexFeedback::new(),
log_db: None,
environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
config_warnings: Vec::new(),
session_source: SessionSource::Cli,
enable_codex_api_key_env: false,
+1
View File
@@ -13,6 +13,7 @@ use codex_connectors::merge::merge_connectors;
use codex_connectors::merge::merge_plugin_connectors;
use codex_core::config::Config;
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools;
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager;
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options;
pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status;
pub use codex_core::connectors::list_cached_accessible_connectors_from_mcp_tools;
+7 -21
View File
@@ -95,9 +95,7 @@ impl AgentControlHarness {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.to_path_buf(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
);
let control = manager.agent_control();
Self {
@@ -911,9 +909,7 @@ async fn spawn_agent_respects_max_threads_limit() {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.to_path_buf(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
);
let control = manager.agent_control();
@@ -965,9 +961,7 @@ async fn spawn_agent_releases_slot_after_shutdown() {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.to_path_buf(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
);
let control = manager.agent_control();
@@ -1010,9 +1004,7 @@ async fn spawn_agent_limit_shared_across_clones() {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.to_path_buf(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
);
let control = manager.agent_control();
let cloned = control.clone();
@@ -1057,9 +1049,7 @@ async fn resume_agent_respects_max_threads_limit() {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.to_path_buf(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
);
let control = manager.agent_control();
@@ -1115,9 +1105,7 @@ async fn resume_agent_releases_slot_after_resume_failure() {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.to_path_buf(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
);
let control = manager.agent_control();
@@ -1512,9 +1500,7 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.to_path_buf(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
);
let control = manager.agent_control();
let harness = AgentControlHarness {
+1 -4
View File
@@ -4,7 +4,6 @@ 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;
@@ -78,9 +77,7 @@ pub(crate) async fn run_codex_thread_interactive(
config,
auth_manager,
models_manager,
environment_manager: Arc::new(EnvironmentManager::from_environment(
parent_ctx.environment.as_deref(),
)),
environment_manager: Arc::clone(&parent_session.services.environment_manager),
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),
+30 -2
View File
@@ -13,7 +13,9 @@ pub use codex_app_server_protocol::AppInfo;
pub use codex_app_server_protocol::AppMetadata;
use codex_connectors::AllConnectorsCacheKey;
use codex_connectors::DirectoryListResponse;
use codex_exec_server::Environment;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::EnvironmentManagerArgs;
use codex_exec_server::ExecServerRuntimePaths;
use codex_login::token_data::TokenData;
use codex_protocol::protocol::SandboxPolicy;
use codex_tools::DiscoverableTool;
@@ -190,6 +192,28 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options(
pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
config: &Config,
force_refetch: bool,
) -> anyhow::Result<AccessibleConnectorsStatus> {
// TODO: Wire callers that already own an EnvironmentManager into
// list_accessible_connectors_from_mcp_tools_with_environment_manager instead
// of constructing a temporary manager here.
let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths(
config.codex_self_exe.clone(),
config.codex_linux_sandbox_exe.clone(),
)?;
let environment_manager =
EnvironmentManager::new(EnvironmentManagerArgs::from_env(local_runtime_paths));
list_accessible_connectors_from_mcp_tools_with_environment_manager(
config,
force_refetch,
&environment_manager,
)
.await
}
pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
config: &Config,
force_refetch: bool,
environment_manager: &EnvironmentManager,
) -> anyhow::Result<AccessibleConnectorsStatus> {
let auth_manager =
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false);
@@ -235,6 +259,10 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
let (tx_event, rx_event) = unbounded();
drop(rx_event);
let environment = environment_manager
.default_environment()
.unwrap_or_else(|| environment_manager.local_environment());
let (mcp_connection_manager, cancel_token) = McpConnectionManager::new(
&mcp_servers,
config.mcp_oauth_credentials_store_mode,
@@ -243,7 +271,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
INITIAL_SUBMIT_ID.to_owned(),
tx_event,
SandboxPolicy::new_read_only_policy(),
McpRuntimeEnvironment::new(Arc::new(Environment::default()), config.cwd.to_path_buf()),
McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf()),
config.codex_home.to_path_buf(),
codex_apps_tools_cache_key(auth.as_ref()),
ToolPluginProvenance::default(),
+1 -3
View File
@@ -491,9 +491,7 @@ mod phase2 {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.to_path_buf(),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
);
let (mut session, _turn_context) = make_session_and_context().await;
session.services.state_db = Some(Arc::clone(&state_db));
+10 -1
View File
@@ -2,6 +2,8 @@ use std::collections::HashSet;
use std::sync::Arc;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::EnvironmentManagerArgs;
use codex_exec_server::ExecServerRuntimePaths;
use codex_features::Feature;
use codex_login::AuthManager;
use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig;
@@ -29,6 +31,11 @@ pub async fn build_prompt_input(
let auth_manager =
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false);
let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths(
config.codex_self_exe.clone(),
config.codex_linux_sandbox_exe.clone(),
)?;
let thread_manager = ThreadManager::new(
&config,
Arc::clone(&auth_manager),
@@ -38,7 +45,9 @@ pub async fn build_prompt_input(
.features
.enabled(Feature::DefaultModeRequestUserInput),
},
Arc::new(EnvironmentManager::from_env()),
Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::from_env(
local_runtime_paths,
))),
/*analytics_events_client*/ None,
);
let thread = thread_manager.start_thread(config).await?;
+2 -2
View File
@@ -507,8 +507,8 @@ pub async fn list_skills(sess: &Session, sub_id: String, cwds: Vec<PathBuf>, for
let plugins_manager = &sess.services.plugins_manager;
let fs = sess
.services
.environment
.as_ref()
.environment_manager
.default_environment()
.map(|environment| environment.get_filesystem());
let config = sess.get_config().await;
let codex_home = sess.codex_home().await;
+1 -1
View File
@@ -237,7 +237,7 @@ impl Session {
turn_context
.environment
.clone()
.unwrap_or_else(|| Arc::new(Environment::default())),
.unwrap_or_else(|| self.services.environment_manager.local_environment()),
turn_context.cwd.to_path_buf(),
),
config.codex_home.to_path_buf(),
+2 -5
View File
@@ -458,10 +458,7 @@ impl Codex {
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
let (tx_event, rx_event) = async_channel::unbounded();
let environment = environment_manager
.current()
.await
.map_err(|err| CodexErr::Fatal(format!("failed to create environment: {err}")))?;
let environment = environment_manager.default_environment();
let fs = environment
.as_ref()
.map(|environment| environment.get_filesystem());
@@ -648,7 +645,7 @@ impl Codex {
mcp_manager.clone(),
skills_watcher,
agent_control,
environment,
environment_manager,
analytics_events_client,
)
.await
+1 -1
View File
@@ -46,7 +46,7 @@ pub(super) async fn spawn_review_thread(
)
.with_web_search_config(/*web_search_config*/ None)
.with_allow_login_shell(config.permissions.allow_login_shell)
.with_has_environment(parent_turn_context.environment.is_some())
.with_has_environment(parent_turn_context.tools_config.has_environment)
.with_spawn_agent_usage_hint(config.multi_agent_v2.usage_hint_enabled)
.with_spawn_agent_usage_hint_text(config.multi_agent_v2.usage_hint_text.clone())
.with_hide_spawn_agent_metadata(config.multi_agent_v2.hide_spawn_agent_metadata)
+6 -5
View File
@@ -227,7 +227,7 @@ impl Session {
mcp_manager: Arc<McpManager>,
skills_watcher: Arc<SkillsWatcher>,
agent_control: AgentControl,
environment: Option<Arc<Environment>>,
environment_manager: Arc<EnvironmentManager>,
analytics_events_client: Option<AnalyticsEventsClient>,
) -> anyhow::Result<Arc<Self>> {
debug!(
@@ -676,7 +676,7 @@ impl Session {
code_mode_service: crate::tools::code_mode::CodeModeService::new(
config.js_repl_node_path.clone(),
),
environment: environment.clone(),
environment_manager,
};
services
.model_client
@@ -770,9 +770,10 @@ impl Session {
tx_event.clone(),
session_configuration.sandbox_policy.get().clone(),
McpRuntimeEnvironment::new(
environment
.clone()
.unwrap_or_else(|| Arc::new(Environment::default())),
sess.services
.environment_manager
.default_environment()
.unwrap_or_else(|| sess.services.environment_manager.local_environment()),
session_configuration.cwd.to_path_buf(),
),
config.codex_home.to_path_buf(),
+8 -18
View File
@@ -2718,8 +2718,8 @@ async fn new_default_turn_uses_config_aware_skills_for_role_overrides() {
let skill_fs = session
.services
.environment
.as_ref()
.environment_manager
.default_environment()
.map(|environment| environment.get_filesystem())
.unwrap_or_else(|| std::sync::Arc::clone(&codex_exec_server::LOCAL_FS));
let parent_outcome = session
@@ -2945,11 +2945,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
mcp_manager,
Arc::new(SkillsWatcher::noop()),
AgentControl::default(),
Some(Arc::new(
codex_exec_server::Environment::create(/*exec_server_url*/ None)
.await
.expect("create environment"),
)),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
)
.await;
@@ -3046,8 +3042,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
));
let network_approval = Arc::new(NetworkApprovalService::default());
let environment = Arc::new(
codex_exec_server::Environment::create(/*exec_server_url*/ None)
.await
codex_exec_server::Environment::create_for_tests(/*exec_server_url*/ None)
.expect("create environment"),
);
@@ -3107,7 +3102,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
code_mode_service: crate::tools::code_mode::CodeModeService::new(
config.js_repl_node_path.clone(),
),
environment: Some(Arc::clone(&environment)),
environment_manager: Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
};
let js_repl = Arc::new(JsReplHandle::with_node_path(
config.js_repl_node_path.clone(),
@@ -3262,11 +3257,7 @@ async fn make_session_with_config_and_rx(
mcp_manager,
Arc::new(SkillsWatcher::noop()),
AgentControl::default(),
Some(Arc::new(
codex_exec_server::Environment::create(/*exec_server_url*/ None)
.await
.expect("create environment"),
)),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
)
.await?;
@@ -4137,8 +4128,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
));
let network_approval = Arc::new(NetworkApprovalService::default());
let environment = Arc::new(
codex_exec_server::Environment::create(/*exec_server_url*/ None)
.await
codex_exec_server::Environment::create_for_tests(/*exec_server_url*/ None)
.expect("create environment"),
);
@@ -4198,7 +4188,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
code_mode_service: crate::tools::code_mode::CodeModeService::new(
config.js_repl_node_path.clone(),
),
environment: Some(Arc::clone(&environment)),
environment_manager: Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
};
let js_repl = Arc::new(JsReplHandle::with_node_path(
config.js_repl_node_path.clone(),
@@ -635,7 +635,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)),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
skills_manager,
plugins_manager,
mcp_manager,
+3 -4
View File
@@ -544,9 +544,8 @@ impl Session {
.await;
let effective_skill_roots = plugin_outcome.effective_skill_roots();
let skills_input = skills_load_input_from_config(&per_turn_config, effective_skill_roots);
let fs = self
.services
.environment
let environment = self.services.environment_manager.default_environment();
let fs = environment
.as_ref()
.map(|environment| environment.get_filesystem());
let skills_outcome = Arc::new(
@@ -576,7 +575,7 @@ impl Session {
)
.then(|| started_proxy.proxy())
}),
self.services.environment.clone(),
environment,
sub_id,
Arc::clone(&self.js_repl),
skills_outcome,
+4 -2
View File
@@ -16,7 +16,7 @@ use crate::tools::network_approval::NetworkApprovalService;
use crate::tools::sandboxing::ApprovalStore;
use crate::unified_exec::UnifiedExecProcessManager;
use codex_analytics::AnalyticsEventsClient;
use codex_exec_server::Environment;
use codex_exec_server::EnvironmentManager;
use codex_hooks::Hooks;
use codex_login::AuthManager;
use codex_mcp::McpConnectionManager;
@@ -62,5 +62,7 @@ pub(crate) struct SessionServices {
/// Session-scoped model client shared across turns.
pub(crate) model_client: ModelClient,
pub(crate) code_mode_service: CodeModeService,
pub(crate) environment: Option<Arc<Environment>>,
/// Shared process-level environment registry. Sessions carry an `Arc` handle so they can pass
/// the same manager through child-thread spawn paths without reconstructing it.
pub(crate) environment_manager: Arc<EnvironmentManager>,
}
+2 -6
View File
@@ -301,7 +301,7 @@ impl ThreadManager {
auth,
provider,
codex_home.clone(),
Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
Arc::new(EnvironmentManager::default_for_tests()),
);
manager._test_codex_home_guard = Some(TempCodexHomeGuard { path: codex_home });
manager
@@ -920,11 +920,7 @@ impl ThreadManagerState {
parent_trace: Option<W3cTraceContext>,
user_shell_override: Option<crate::shell::Shell>,
) -> CodexResult<NewThread> {
let environment = self
.environment_manager
.current()
.await
.map_err(|err| CodexErr::Fatal(format!("failed to create environment: {err}")))?;
let environment = self.environment_manager.default_environment();
let watch_registration = match environment.as_ref() {
Some(environment) if !environment.is_remote() => {
self.skills_watcher
+5 -15
View File
@@ -246,9 +246,7 @@ async fn shutdown_all_threads_bounded_submits_shutdown_to_every_thread() {
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.to_path_buf(),
Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
);
let thread_1 = manager
.start_thread(config.clone())
@@ -297,9 +295,7 @@ 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,
)),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
);
@@ -434,9 +430,7 @@ 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,
)),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
);
@@ -537,9 +531,7 @@ 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,
)),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
);
@@ -630,9 +622,7 @@ 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,
)),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
);
+1 -1
View File
@@ -508,7 +508,7 @@ async fn completed_pipe_commands_preserve_exit_code() -> anyhow::Result<()> {
shell_env(),
);
let environment = codex_exec_server::Environment::default();
let environment = codex_exec_server::Environment::default_for_tests();
let process = UnifiedExecProcessManager::default()
.open_session_with_exec_env(
/*process_id*/ 1234,
+22 -3
View File
@@ -76,7 +76,8 @@ impl TestEnv {
pub async fn local() -> Result<Self> {
let local_cwd_temp_dir = Arc::new(TempDir::new()?);
let cwd = local_cwd_temp_dir.abs();
let environment = codex_exec_server::Environment::create(/*exec_server_url*/ None).await?;
let environment =
codex_exec_server::Environment::create_for_tests(/*exec_server_url*/ None)?;
Ok(Self {
environment,
cwd,
@@ -115,7 +116,8 @@ pub async fn test_env() -> Result<TestEnv> {
match get_remote_test_env() {
Some(remote_env) => {
let websocket_url = remote_exec_server_url()?;
let environment = codex_exec_server::Environment::create(Some(websocket_url)).await?;
let environment =
codex_exec_server::Environment::create_for_tests(Some(websocket_url))?;
let cwd = remote_aware_cwd_path();
environment
.get_filesystem()
@@ -204,6 +206,7 @@ pub struct TestCodexBuilder {
workspace_setups: Vec<Box<WorkspaceSetup>>,
home: Option<Arc<TempDir>>,
user_shell_override: Option<Shell>,
exec_server_url: Option<String>,
}
impl TestCodexBuilder {
@@ -255,6 +258,11 @@ impl TestCodexBuilder {
self
}
pub fn with_exec_server_url(mut self, exec_server_url: impl Into<String>) -> Self {
self.exec_server_url = Some(exec_server_url.into());
self
}
pub fn with_windows_cmd_shell(self) -> Self {
if cfg!(windows) {
self.with_user_shell(get_shell_by_model_provided_path(&PathBuf::from("cmd.exe")))
@@ -350,8 +358,18 @@ impl TestCodexBuilder {
let (config, fallback_cwd) = self
.prepare_config(base_url, &home, test_env.cwd().clone())
.await?;
let exec_server_url = self
.exec_server_url
.clone()
.or_else(|| test_env.exec_server_url().map(str::to_owned));
let environment_manager = Arc::new(codex_exec_server::EnvironmentManager::new(
test_env.exec_server_url().map(str::to_owned),
codex_exec_server::EnvironmentManagerArgs {
exec_server_url,
local_runtime_paths: codex_exec_server::ExecServerRuntimePaths::new(
std::env::current_exe()?,
/*codex_linux_sandbox_exe*/ None,
)?,
},
));
let file_system = test_env.environment().get_filesystem();
let mut workspace_setups = vec![];
@@ -885,6 +903,7 @@ pub fn test_codex() -> TestCodexBuilder {
workspace_setups: vec![],
home: None,
user_shell_override: None,
exec_server_url: None,
}
}
+1 -3
View File
@@ -1103,9 +1103,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
.features
.enabled(Feature::DefaultModeRequestUserInput),
},
Arc::new(codex_exec_server::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
);
let NewThread { thread: codex, .. } = thread_manager
+10 -1
View File
@@ -5,6 +5,7 @@ use anyhow::Result;
use codex_core::ThreadManager;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::ExecServerRuntimePaths;
use codex_exec_server::ExecutorFileSystem;
use codex_login::CodexAuth;
use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig;
@@ -234,7 +235,15 @@ async fn list_skills_skips_cwd_roots_when_environment_disabled() -> Result<()> {
codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("dummy")),
SessionSource::Exec,
CollaborationModesConfig::default(),
Arc::new(EnvironmentManager::new(Some("none".to_string()))),
Arc::new(EnvironmentManager::new(
codex_exec_server::EnvironmentManagerArgs {
exec_server_url: Some("none".to_string()),
local_runtime_paths: ExecServerRuntimePaths::new(
std::env::current_exe()?,
/*codex_linux_sandbox_exe*/ None,
)?,
},
)),
/*analytics_events_client*/ None,
);
let new_thread = thread_manager.start_thread(config.clone()).await?;
+32
View File
@@ -10,6 +10,7 @@ use arc_swap::ArcSwap;
use codex_app_server_protocol::JSONRPCNotification;
use serde_json::Value;
use tokio::sync::Mutex;
use tokio::sync::OnceCell;
use tokio::sync::mpsc;
use tokio::sync::watch;
@@ -174,6 +175,37 @@ pub struct ExecServerClient {
inner: Arc<Inner>,
}
#[derive(Clone)]
pub(crate) struct LazyRemoteExecServerClient {
websocket_url: String,
client: Arc<OnceCell<ExecServerClient>>,
}
impl LazyRemoteExecServerClient {
pub(crate) fn new(websocket_url: String) -> Self {
Self {
websocket_url,
client: Arc::new(OnceCell::new()),
}
}
pub(crate) async fn get(&self) -> Result<ExecServerClient, ExecServerError> {
self.client
.get_or_try_init(|| async {
ExecServerClient::connect_websocket(RemoteExecServerConnectArgs {
websocket_url: self.websocket_url.clone(),
client_name: "codex-environment".to_string(),
connect_timeout: Duration::from_secs(5),
initialize_timeout: Duration::from_secs(5),
resume_session_id: None,
})
.await
})
.await
.cloned()
}
}
#[derive(Debug, thiserror::Error)]
pub enum ExecServerError {
#[error("failed to spawn exec-server: {0}")]
+299 -195
View File
@@ -1,11 +1,9 @@
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::OnceCell;
use crate::ExecServerClient;
use crate::ExecServerError;
use crate::ExecServerRuntimePaths;
use crate::RemoteExecServerConnectArgs;
use crate::client::LazyRemoteExecServerClient;
use crate::file_system::ExecutorFileSystem;
use crate::local_file_system::LocalFileSystem;
use crate::local_process::LocalProcess;
@@ -15,130 +13,139 @@ use crate::remote_process::RemoteProcess;
pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL";
/// Lazily creates and caches the active environment for a session.
/// Owns the execution/filesystem environments available to the Codex runtime.
///
/// The manager keeps the session's environment selection stable so subagents
/// and follow-up turns preserve an explicit disabled state.
/// `EnvironmentManager` is a shared registry for concrete environments. It
/// always creates a local environment under [`LOCAL_ENVIRONMENT_ID`]. When
/// `CODEX_EXEC_SERVER_URL` is set to a websocket URL, it also creates a remote
/// environment under [`REMOTE_ENVIRONMENT_ID`] and makes that the default
/// environment. Otherwise the local environment is the default.
///
/// Setting `CODEX_EXEC_SERVER_URL=none` disables environment access by leaving
/// the default environment unset while still keeping the local environment
/// available for internal callers by id. Callers use
/// `default_environment().is_some()` as the signal for model-facing
/// shell/filesystem tool availability.
///
/// Remote environments create remote filesystem and execution backends that
/// lazy-connect to the configured exec-server on first use. The websocket is
/// not opened when the manager or environment is constructed.
#[derive(Debug)]
pub struct EnvironmentManager {
exec_server_url: Option<String>,
local_runtime_paths: Option<ExecServerRuntimePaths>,
disabled: bool,
current_environment: OnceCell<Option<Arc<Environment>>>,
default_environment: Option<String>,
environments: HashMap<String, Arc<Environment>>,
}
impl Default for EnvironmentManager {
fn default() -> Self {
Self::new(/*exec_server_url*/ None)
pub const LOCAL_ENVIRONMENT_ID: &str = "local";
pub const REMOTE_ENVIRONMENT_ID: &str = "remote";
#[derive(Clone, Debug)]
pub struct EnvironmentManagerArgs {
pub exec_server_url: Option<String>,
pub local_runtime_paths: ExecServerRuntimePaths,
}
impl EnvironmentManagerArgs {
pub fn new(local_runtime_paths: ExecServerRuntimePaths) -> Self {
Self {
exec_server_url: None,
local_runtime_paths,
}
}
pub fn from_env(local_runtime_paths: ExecServerRuntimePaths) -> Self {
Self {
exec_server_url: std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok(),
local_runtime_paths,
}
}
}
impl EnvironmentManager {
/// Builds a manager from the raw `CODEX_EXEC_SERVER_URL` value.
pub fn new(exec_server_url: Option<String>) -> Self {
Self::new_with_runtime_paths(exec_server_url, /*local_runtime_paths*/ None)
/// Builds a test-only manager without configured sandbox helper paths.
pub fn default_for_tests() -> Self {
Self {
default_environment: Some(LOCAL_ENVIRONMENT_ID.to_string()),
environments: HashMap::from([(
LOCAL_ENVIRONMENT_ID.to_string(),
Arc::new(Environment::default_for_tests()),
)]),
}
}
/// Builds a manager from the raw `CODEX_EXEC_SERVER_URL` value and local
/// runtime paths used when creating local filesystem helpers.
pub fn new_with_runtime_paths(
exec_server_url: Option<String>,
local_runtime_paths: Option<ExecServerRuntimePaths>,
) -> Self {
let (exec_server_url, disabled) = normalize_exec_server_url(exec_server_url);
Self {
pub fn new(args: EnvironmentManagerArgs) -> Self {
let EnvironmentManagerArgs {
exec_server_url,
local_runtime_paths,
disabled,
current_environment: OnceCell::new(),
}
}
/// Builds a manager from process environment variables.
pub fn from_env() -> Self {
Self::from_env_with_runtime_paths(/*local_runtime_paths*/ None)
}
/// Builds a manager from process environment variables and local runtime
/// paths used when creating local filesystem helpers.
pub fn from_env_with_runtime_paths(
local_runtime_paths: Option<ExecServerRuntimePaths>,
) -> Self {
Self::new_with_runtime_paths(
std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok(),
local_runtime_paths,
)
}
/// Builds a manager from the currently selected environment, or from the
/// disabled mode when no environment is available.
pub fn from_environment(environment: Option<&Environment>) -> Self {
match environment {
Some(environment) => Self {
exec_server_url: environment.exec_server_url().map(str::to_owned),
local_runtime_paths: environment.local_runtime_paths().cloned(),
disabled: false,
current_environment: OnceCell::new(),
},
None => Self {
exec_server_url: None,
local_runtime_paths: None,
disabled: true,
current_environment: OnceCell::new(),
},
}
}
/// Returns the remote exec-server URL when one is configured.
pub fn exec_server_url(&self) -> Option<&str> {
self.exec_server_url.as_deref()
}
/// Returns true when this manager is configured to use a remote exec server.
pub fn is_remote(&self) -> bool {
self.exec_server_url.is_some()
}
/// Returns the cached environment, creating it on first access.
pub async fn current(&self) -> Result<Option<Arc<Environment>>, ExecServerError> {
self.current_environment
.get_or_try_init(|| async {
if self.disabled {
Ok(None)
} else {
Ok(Some(Arc::new(
Environment::create_with_runtime_paths(
self.exec_server_url.clone(),
self.local_runtime_paths.clone(),
)
.await?,
)))
} = args;
let (exec_server_url, environment_disabled) = normalize_exec_server_url(exec_server_url);
let mut environments = HashMap::from([(
LOCAL_ENVIRONMENT_ID.to_string(),
Arc::new(Environment::local(local_runtime_paths.clone())),
)]);
let default_environment = if environment_disabled {
None
} else {
match exec_server_url {
Some(exec_server_url) => {
environments.insert(
REMOTE_ENVIRONMENT_ID.to_string(),
Arc::new(Environment::remote(exec_server_url, local_runtime_paths)),
);
Some(REMOTE_ENVIRONMENT_ID.to_string())
}
})
.await
.map(Option::as_ref)
.map(std::option::Option::<&Arc<Environment>>::cloned)
None => Some(LOCAL_ENVIRONMENT_ID.to_string()),
}
};
Self {
default_environment,
environments,
}
}
/// Returns the default environment instance.
pub fn default_environment(&self) -> Option<Arc<Environment>> {
self.default_environment
.as_deref()
.and_then(|environment_id| self.get_environment(environment_id))
}
/// Returns the local environment instance used for internal runtime work.
pub fn local_environment(&self) -> Arc<Environment> {
match self.get_environment(LOCAL_ENVIRONMENT_ID) {
Some(environment) => environment,
None => unreachable!("EnvironmentManager always has a local environment"),
}
}
/// Returns a named environment instance.
pub fn get_environment(&self, environment_id: &str) -> Option<Arc<Environment>> {
self.environments.get(environment_id).cloned()
}
}
/// Concrete execution/filesystem environment selected for a session.
///
/// This bundles the selected backend together with the corresponding remote
/// client, if any.
/// This bundles the selected backend metadata together with the local runtime
/// paths used by filesystem helpers.
#[derive(Clone)]
pub struct Environment {
exec_server_url: Option<String>,
remote_exec_server_client: Option<ExecServerClient>,
exec_backend: Arc<dyn ExecBackend>,
filesystem: Arc<dyn ExecutorFileSystem>,
local_runtime_paths: Option<ExecServerRuntimePaths>,
}
impl Default for Environment {
fn default() -> Self {
impl Environment {
/// Builds a test-only local environment without configured sandbox helper paths.
pub fn default_for_tests() -> Self {
Self {
exec_server_url: None,
remote_exec_server_client: None,
exec_backend: Arc::new(LocalProcess::default()),
filesystem: Arc::new(LocalFileSystem::unsandboxed()),
local_runtime_paths: None,
}
}
@@ -154,13 +161,21 @@ impl std::fmt::Debug for Environment {
impl Environment {
/// Builds an environment from the raw `CODEX_EXEC_SERVER_URL` value.
pub async fn create(exec_server_url: Option<String>) -> Result<Self, ExecServerError> {
Self::create_with_runtime_paths(exec_server_url, /*local_runtime_paths*/ None).await
pub fn create(
exec_server_url: Option<String>,
local_runtime_paths: ExecServerRuntimePaths,
) -> Result<Self, ExecServerError> {
Self::create_inner(exec_server_url, Some(local_runtime_paths))
}
/// Builds a test-only environment without configured sandbox helper paths.
pub fn create_for_tests(exec_server_url: Option<String>) -> Result<Self, ExecServerError> {
Self::create_inner(exec_server_url, /*local_runtime_paths*/ None)
}
/// Builds an environment from the raw `CODEX_EXEC_SERVER_URL` value and
/// local runtime paths used when creating local filesystem helpers.
pub async fn create_with_runtime_paths(
fn create_inner(
exec_server_url: Option<String>,
local_runtime_paths: Option<ExecServerRuntimePaths>,
) -> Result<Self, ExecServerError> {
@@ -171,36 +186,46 @@ impl Environment {
));
}
let remote_exec_server_client = if let Some(exec_server_url) = &exec_server_url {
Some(
ExecServerClient::connect_websocket(RemoteExecServerConnectArgs {
websocket_url: exec_server_url.clone(),
client_name: "codex-environment".to_string(),
connect_timeout: std::time::Duration::from_secs(5),
initialize_timeout: std::time::Duration::from_secs(5),
resume_session_id: None,
})
.await?,
)
} else {
None
};
let exec_backend: Arc<dyn ExecBackend> =
if let Some(client) = remote_exec_server_client.clone() {
Arc::new(RemoteProcess::new(client))
} else {
Arc::new(LocalProcess::default())
};
Ok(Self {
exec_server_url,
remote_exec_server_client,
exec_backend,
local_runtime_paths,
Ok(match exec_server_url {
Some(exec_server_url) => Self::remote_inner(exec_server_url, local_runtime_paths),
None => match local_runtime_paths {
Some(local_runtime_paths) => Self::local(local_runtime_paths),
None => Self::default_for_tests(),
},
})
}
fn local(local_runtime_paths: ExecServerRuntimePaths) -> Self {
Self {
exec_server_url: None,
exec_backend: Arc::new(LocalProcess::default()),
filesystem: Arc::new(LocalFileSystem::with_runtime_paths(
local_runtime_paths.clone(),
)),
local_runtime_paths: Some(local_runtime_paths),
}
}
fn remote(exec_server_url: String, local_runtime_paths: ExecServerRuntimePaths) -> Self {
Self::remote_inner(exec_server_url, Some(local_runtime_paths))
}
fn remote_inner(
exec_server_url: String,
local_runtime_paths: Option<ExecServerRuntimePaths>,
) -> Self {
let client = LazyRemoteExecServerClient::new(exec_server_url.clone());
let exec_backend: Arc<dyn ExecBackend> = Arc::new(RemoteProcess::new(client.clone()));
let filesystem: Arc<dyn ExecutorFileSystem> = Arc::new(RemoteFileSystem::new(client));
Self {
exec_server_url: Some(exec_server_url),
exec_backend,
filesystem,
local_runtime_paths,
}
}
pub fn is_remote(&self) -> bool {
self.exec_server_url.is_some()
}
@@ -219,13 +244,7 @@ impl Environment {
}
pub fn get_filesystem(&self) -> Arc<dyn ExecutorFileSystem> {
match self.remote_exec_server_client.clone() {
Some(client) => Arc::new(RemoteFileSystem::new(client)),
None => match self.local_runtime_paths.clone() {
Some(runtime_paths) => Arc::new(LocalFileSystem::with_runtime_paths(runtime_paths)),
None => Arc::new(LocalFileSystem::unsandboxed()),
},
}
Arc::clone(&self.filesystem)
}
}
@@ -242,100 +261,162 @@ mod tests {
use super::Environment;
use super::EnvironmentManager;
use super::EnvironmentManagerArgs;
use super::LOCAL_ENVIRONMENT_ID;
use super::REMOTE_ENVIRONMENT_ID;
use crate::ExecServerRuntimePaths;
use crate::ProcessId;
use pretty_assertions::assert_eq;
fn test_runtime_paths() -> ExecServerRuntimePaths {
ExecServerRuntimePaths::new(
std::env::current_exe().expect("current exe"),
/*codex_linux_sandbox_exe*/ None,
)
.expect("runtime paths")
}
#[tokio::test]
async fn create_local_environment_does_not_connect() {
let environment = Environment::create(/*exec_server_url*/ None)
.await
let environment = Environment::create(/*exec_server_url*/ None, test_runtime_paths())
.expect("create environment");
assert_eq!(environment.exec_server_url(), None);
assert!(environment.remote_exec_server_client.is_none());
}
#[test]
fn environment_manager_normalizes_empty_url() {
let manager = EnvironmentManager::new(Some(String::new()));
assert!(!manager.disabled);
assert_eq!(manager.exec_server_url(), None);
assert!(!manager.is_remote());
}
#[test]
fn environment_manager_treats_none_value_as_disabled() {
let manager = EnvironmentManager::new(Some("none".to_string()));
assert!(manager.disabled);
assert_eq!(manager.exec_server_url(), None);
assert!(!manager.is_remote());
}
#[test]
fn environment_manager_reports_remote_url() {
let manager = EnvironmentManager::new(Some("ws://127.0.0.1:8765".to_string()));
assert!(manager.is_remote());
assert_eq!(manager.exec_server_url(), Some("ws://127.0.0.1:8765"));
assert!(!environment.is_remote());
}
#[tokio::test]
async fn environment_manager_current_caches_environment() {
let manager = EnvironmentManager::new(/*exec_server_url*/ None);
async fn environment_manager_normalizes_empty_url() {
let manager = EnvironmentManager::new(EnvironmentManagerArgs {
exec_server_url: Some(String::new()),
local_runtime_paths: test_runtime_paths(),
});
let first = manager.current().await.expect("get current environment");
let second = manager.current().await.expect("get current environment");
let environment = manager.default_environment().expect("default environment");
assert!(!environment.is_remote());
assert!(
!manager
.get_environment(LOCAL_ENVIRONMENT_ID)
.expect("local environment")
.is_remote()
);
assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none());
}
let first = first.expect("local environment");
let second = second.expect("local environment");
#[tokio::test]
async fn environment_manager_treats_none_value_as_disabled() {
let manager = EnvironmentManager::new(EnvironmentManagerArgs {
exec_server_url: Some("none".to_string()),
local_runtime_paths: test_runtime_paths(),
});
assert!(manager.default_environment().is_none());
assert!(
!manager
.get_environment(LOCAL_ENVIRONMENT_ID)
.expect("local environment")
.is_remote()
);
assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none());
}
#[tokio::test]
async fn environment_manager_reports_remote_url() {
let manager = EnvironmentManager::new(EnvironmentManagerArgs {
exec_server_url: Some("ws://127.0.0.1:8765".to_string()),
local_runtime_paths: test_runtime_paths(),
});
let environment = manager.default_environment().expect("default environment");
assert!(environment.is_remote());
assert_eq!(environment.exec_server_url(), Some("ws://127.0.0.1:8765"));
assert!(Arc::ptr_eq(
&environment,
&manager
.get_environment(REMOTE_ENVIRONMENT_ID)
.expect("remote environment")
));
assert!(
!manager
.get_environment(LOCAL_ENVIRONMENT_ID)
.expect("local environment")
.is_remote()
);
}
#[tokio::test]
async fn environment_manager_default_environment_caches_environment() {
let manager = EnvironmentManager::default_for_tests();
let first = manager.default_environment().expect("default environment");
let second = manager.default_environment().expect("default environment");
assert!(Arc::ptr_eq(&first, &second));
assert!(Arc::ptr_eq(
&first.get_filesystem(),
&second.get_filesystem()
));
}
#[tokio::test]
async fn environment_manager_carries_local_runtime_paths() {
let runtime_paths = ExecServerRuntimePaths::new(
std::env::current_exe().expect("current exe"),
/*codex_linux_sandbox_exe*/ None,
)
.expect("runtime paths");
let manager = EnvironmentManager::new_with_runtime_paths(
/*exec_server_url*/ None,
Some(runtime_paths.clone()),
);
let runtime_paths = test_runtime_paths();
let manager = EnvironmentManager::new(EnvironmentManagerArgs {
exec_server_url: None,
local_runtime_paths: runtime_paths.clone(),
});
let environment = manager
.current()
.await
.expect("get current environment")
.expect("local environment");
let environment = manager.default_environment().expect("default environment");
assert_eq!(environment.local_runtime_paths(), Some(&runtime_paths));
assert_eq!(
EnvironmentManager::from_environment(Some(&environment)).local_runtime_paths,
Some(runtime_paths)
);
let manager = EnvironmentManager::new(EnvironmentManagerArgs {
exec_server_url: environment.exec_server_url().map(str::to_owned),
local_runtime_paths: environment
.local_runtime_paths()
.expect("local runtime paths")
.clone(),
});
let environment = manager.default_environment().expect("default environment");
assert_eq!(environment.local_runtime_paths(), Some(&runtime_paths));
}
#[tokio::test]
async fn disabled_environment_manager_has_no_current_environment() {
let manager = EnvironmentManager::new(Some("none".to_string()));
async fn disabled_environment_manager_has_no_default_environment() {
let manager = EnvironmentManager::new(EnvironmentManagerArgs {
exec_server_url: Some("none".to_string()),
local_runtime_paths: test_runtime_paths(),
});
assert!(manager.default_environment().is_none());
}
#[tokio::test]
async fn environment_manager_keeps_local_lookup_when_default_disabled() {
let manager = EnvironmentManager::new(EnvironmentManagerArgs {
exec_server_url: Some("none".to_string()),
local_runtime_paths: test_runtime_paths(),
});
assert!(manager.default_environment().is_none());
assert!(
manager
.current()
.await
.expect("get current environment")
.is_none()
!manager
.get_environment(LOCAL_ENVIRONMENT_ID)
.expect("local environment")
.is_remote()
);
assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none());
}
#[tokio::test]
async fn get_environment_returns_none_for_unknown_id() {
let manager = EnvironmentManager::default_for_tests();
assert!(manager.get_environment("does-not-exist").is_none());
}
#[tokio::test]
async fn default_environment_has_ready_local_executor() {
let environment = Environment::default();
let environment = Environment::default_for_tests();
let response = environment
.get_exec_backend()
@@ -354,4 +435,27 @@ mod tests {
assert_eq!(response.process.process_id().as_str(), "default-env-proc");
}
#[tokio::test]
async fn test_environment_rejects_sandboxed_filesystem_without_runtime_paths() {
let environment = Environment::default_for_tests();
let path = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(
std::env::current_exe().expect("current exe").as_path(),
)
.expect("absolute current exe");
let sandbox = crate::FileSystemSandboxContext::new(
codex_protocol::protocol::SandboxPolicy::new_read_only_policy(),
);
let err = environment
.get_filesystem()
.read_file(&path, Some(&sandbox))
.await
.expect_err("sandboxed read should require runtime paths");
assert_eq!(
err.to_string(),
"sandboxed filesystem operations require configured runtime paths"
);
}
}
+1
View File
@@ -25,6 +25,7 @@ pub use client_api::RemoteExecServerConnectArgs;
pub use environment::CODEX_EXEC_SERVER_URL_ENV_VAR;
pub use environment::Environment;
pub use environment::EnvironmentManager;
pub use environment::EnvironmentManagerArgs;
pub use file_system::CopyOptions;
pub use file_system::CreateDirectoryOptions;
pub use file_system::ExecutorFileSystem;
+17 -13
View File
@@ -7,7 +7,6 @@ use tracing::trace;
use crate::CopyOptions;
use crate::CreateDirectoryOptions;
use crate::ExecServerClient;
use crate::ExecServerError;
use crate::ExecutorFileSystem;
use crate::FileMetadata;
@@ -15,6 +14,7 @@ use crate::FileSystemResult;
use crate::FileSystemSandboxContext;
use crate::ReadDirectoryEntry;
use crate::RemoveOptions;
use crate::client::LazyRemoteExecServerClient;
use crate::protocol::FsCopyParams;
use crate::protocol::FsCreateDirectoryParams;
use crate::protocol::FsGetMetadataParams;
@@ -28,11 +28,11 @@ const NOT_FOUND_ERROR_CODE: i64 = -32004;
#[derive(Clone)]
pub(crate) struct RemoteFileSystem {
client: ExecServerClient,
client: LazyRemoteExecServerClient,
}
impl RemoteFileSystem {
pub(crate) fn new(client: ExecServerClient) -> Self {
pub(crate) fn new(client: LazyRemoteExecServerClient) -> Self {
trace!("remote fs new");
Self { client }
}
@@ -46,8 +46,8 @@ impl ExecutorFileSystem for RemoteFileSystem {
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<u8>> {
trace!("remote fs read_file");
let response = self
.client
let client = self.client.get().await.map_err(map_remote_error)?;
let response = client
.fs_read_file(FsReadFileParams {
path: path.clone(),
sandbox: sandbox.cloned(),
@@ -69,7 +69,8 @@ impl ExecutorFileSystem for RemoteFileSystem {
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
trace!("remote fs write_file");
self.client
let client = self.client.get().await.map_err(map_remote_error)?;
client
.fs_write_file(FsWriteFileParams {
path: path.clone(),
data_base64: STANDARD.encode(contents),
@@ -87,7 +88,8 @@ impl ExecutorFileSystem for RemoteFileSystem {
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
trace!("remote fs create_directory");
self.client
let client = self.client.get().await.map_err(map_remote_error)?;
client
.fs_create_directory(FsCreateDirectoryParams {
path: path.clone(),
recursive: Some(options.recursive),
@@ -104,8 +106,8 @@ impl ExecutorFileSystem for RemoteFileSystem {
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<FileMetadata> {
trace!("remote fs get_metadata");
let response = self
.client
let client = self.client.get().await.map_err(map_remote_error)?;
let response = client
.fs_get_metadata(FsGetMetadataParams {
path: path.clone(),
sandbox: sandbox.cloned(),
@@ -127,8 +129,8 @@ impl ExecutorFileSystem for RemoteFileSystem {
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
trace!("remote fs read_directory");
let response = self
.client
let client = self.client.get().await.map_err(map_remote_error)?;
let response = client
.fs_read_directory(FsReadDirectoryParams {
path: path.clone(),
sandbox: sandbox.cloned(),
@@ -153,7 +155,8 @@ impl ExecutorFileSystem for RemoteFileSystem {
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
trace!("remote fs remove");
self.client
let client = self.client.get().await.map_err(map_remote_error)?;
client
.fs_remove(FsRemoveParams {
path: path.clone(),
recursive: Some(options.recursive),
@@ -173,7 +176,8 @@ impl ExecutorFileSystem for RemoteFileSystem {
sandbox: Option<&FileSystemSandboxContext>,
) -> FileSystemResult<()> {
trace!("remote fs copy");
self.client
let client = self.client.get().await.map_err(map_remote_error)?;
client
.fs_copy(FsCopyParams {
source_path: source_path.clone(),
destination_path: destination_path.clone(),
+6 -5
View File
@@ -9,7 +9,7 @@ use crate::ExecProcess;
use crate::ExecProcessEventReceiver;
use crate::ExecServerError;
use crate::StartedExecProcess;
use crate::client::ExecServerClient;
use crate::client::LazyRemoteExecServerClient;
use crate::client::Session;
use crate::protocol::ExecParams;
use crate::protocol::ReadResponse;
@@ -17,7 +17,7 @@ use crate::protocol::WriteResponse;
#[derive(Clone)]
pub(crate) struct RemoteProcess {
client: ExecServerClient,
client: LazyRemoteExecServerClient,
}
struct RemoteExecProcess {
@@ -25,7 +25,7 @@ struct RemoteExecProcess {
}
impl RemoteProcess {
pub(crate) fn new(client: ExecServerClient) -> Self {
pub(crate) fn new(client: LazyRemoteExecServerClient) -> Self {
trace!("remote process new");
Self { client }
}
@@ -35,8 +35,9 @@ impl RemoteProcess {
impl ExecBackend for RemoteProcess {
async fn start(&self, params: ExecParams) -> Result<StartedExecProcess, ExecServerError> {
let process_id = params.process_id.clone();
let session = self.client.register_session(&process_id).await?;
if let Err(err) = self.client.exec(params).await {
let client = self.client.get().await?;
let session = client.register_session(&process_id).await?;
if let Err(err) = client.exec(params).await {
session.unregister().await;
return Err(err);
}
+2 -2
View File
@@ -49,13 +49,13 @@ enum ProcessEventSnapshot {
async fn create_process_context(use_remote: bool) -> Result<ProcessContext> {
if use_remote {
let server = exec_server().await?;
let environment = Environment::create(Some(server.websocket_url().to_string())).await?;
let environment = Environment::create_for_tests(Some(server.websocket_url().to_string()))?;
Ok(ProcessContext {
backend: environment.get_exec_backend(),
server: Some(server),
})
} else {
let environment = Environment::create(/*exec_server_url*/ None).await?;
let environment = Environment::create_for_tests(/*exec_server_url*/ None)?;
Ok(ProcessContext {
backend: environment.get_exec_backend(),
server: None,
+2 -2
View File
@@ -46,7 +46,7 @@ struct FileSystemContext {
async fn create_file_system_context(use_remote: bool) -> Result<FileSystemContext> {
if use_remote {
let server = exec_server().await?;
let environment = Environment::create(Some(server.websocket_url().to_string())).await?;
let environment = Environment::create_for_tests(Some(server.websocket_url().to_string()))?;
Ok(FileSystemContext {
file_system: environment.get_filesystem(),
_helper_paths: None,
@@ -214,7 +214,7 @@ async fn sandboxed_file_system_helper_finds_bwrap_on_preserved_path() -> Result<
let helper_path = std::env::join_paths(path_entries)?;
let server = exec_server_with_env([("PATH", helper_path.as_os_str())]).await?;
let environment = Environment::create(Some(server.websocket_url().to_string())).await?;
let environment = Environment::create_for_tests(Some(server.websocket_url().to_string()))?;
let file_system = environment.get_filesystem();
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace)?;
+3 -2
View File
@@ -15,6 +15,7 @@ pub use cli::Command;
pub use cli::ReviewArgs;
use codex_app_server_client::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY;
use codex_app_server_client::EnvironmentManager;
use codex_app_server_client::EnvironmentManagerArgs;
use codex_app_server_client::ExecServerRuntimePaths;
use codex_app_server_client::InProcessAppServerClient;
use codex_app_server_client::InProcessClientStartArgs;
@@ -497,8 +498,8 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
cloud_requirements: run_cloud_requirements,
feedback: CodexFeedback::new(),
log_db: None,
environment_manager: std::sync::Arc::new(EnvironmentManager::from_env_with_runtime_paths(
Some(local_runtime_paths),
environment_manager: std::sync::Arc::new(EnvironmentManager::new(
EnvironmentManagerArgs::from_env(local_runtime_paths),
)),
config_warnings,
session_source: SessionSource::Exec,
+2 -1
View File
@@ -8,6 +8,7 @@ use std::sync::Arc;
use codex_arg0::Arg0DispatchPaths;
use codex_core::config::Config;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::EnvironmentManagerArgs;
use codex_exec_server::ExecServerRuntimePaths;
use codex_login::default_client::set_default_client_residency_requirement;
use codex_utils_cli::CliConfigOverrides;
@@ -59,7 +60,7 @@ pub async fn run_main(
arg0_paths: Arg0DispatchPaths,
cli_config_overrides: CliConfigOverrides,
) -> IoResult<()> {
let environment_manager = Arc::new(EnvironmentManager::from_env_with_runtime_paths(Some(
let environment_manager = Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::from_env(
ExecServerRuntimePaths::from_optional_paths(
arg0_paths.codex_self_exe.clone(),
arg0_paths.codex_linux_sandbox_exe.clone(),
+1 -1
View File
@@ -38,7 +38,7 @@ pub(super) async fn make_test_app() -> App {
backtrack_render_pending: false,
feedback: codex_feedback::CodexFeedback::new(),
feedback_audience: FeedbackAudience::External,
environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
remote_app_server_url: None,
remote_app_server_auth_token: None,
pending_update_action: None,
+2 -2
View File
@@ -3576,7 +3576,7 @@ async fn make_test_app() -> App {
backtrack_render_pending: false,
feedback: codex_feedback::CodexFeedback::new(),
feedback_audience: FeedbackAudience::External,
environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
remote_app_server_url: None,
remote_app_server_auth_token: None,
pending_update_action: None,
@@ -3633,7 +3633,7 @@ async fn make_test_app_with_channels() -> (
backtrack_render_pending: false,
feedback: codex_feedback::CodexFeedback::new(),
feedback_audience: FeedbackAudience::External,
environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
remote_app_server_url: None,
remote_app_server_auth_token: None,
pending_update_action: None,
+31 -18
View File
@@ -36,6 +36,7 @@ use codex_config::ConfigLoadError;
use codex_config::LoaderOverrides;
use codex_config::format_config_error_with_source;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::EnvironmentManagerArgs;
use codex_exec_server::ExecServerRuntimePaths;
use codex_login::AuthConfig;
use codex_login::default_client::set_default_client_residency_requirement;
@@ -425,7 +426,7 @@ pub(crate) async fn start_embedded_app_server_for_picker(
start_app_server_for_picker(
config,
&AppServerTarget::Embedded,
Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
Arc::new(EnvironmentManager::default_for_tests()),
)
.await
}
@@ -623,7 +624,9 @@ fn config_cwd_for_app_server_target(
app_server_target: &AppServerTarget,
environment_manager: &EnvironmentManager,
) -> std::io::Result<Option<AbsolutePathBuf>> {
if environment_manager.is_remote()
if environment_manager
.default_environment()
.is_some_and(|environment| environment.is_remote())
|| matches!(app_server_target, AppServerTarget::Remote { .. })
{
return Ok(None);
@@ -726,7 +729,7 @@ pub async fn run_main(
}
};
let environment_manager = Arc::new(EnvironmentManager::from_env_with_runtime_paths(Some(
let environment_manager = Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::from_env(
ExecServerRuntimePaths::from_optional_paths(
arg0_paths.codex_self_exe.clone(),
arg0_paths.codex_linux_sandbox_exe.clone(),
@@ -1771,7 +1774,7 @@ mod tests {
CloudRequirementsLoader::default(),
codex_feedback::CodexFeedback::new(),
/*log_db*/ None,
Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
Arc::new(EnvironmentManager::default_for_tests()),
)
.await
}
@@ -1919,8 +1922,9 @@ mod tests {
Ok(())
}
#[test]
fn config_cwd_for_app_server_target_omits_cwd_for_remote_sessions() -> std::io::Result<()> {
#[tokio::test]
async fn config_cwd_for_app_server_target_omits_cwd_for_remote_sessions() -> std::io::Result<()>
{
let remote_only_cwd = if cfg!(windows) {
Path::new(r"C:\definitely\not\local\to\this\test")
} else {
@@ -1930,7 +1934,7 @@ mod tests {
websocket_url: "ws://127.0.0.1:1234/".to_string(),
auth_token: None,
};
let environment_manager = EnvironmentManager::new(/*exec_server_url*/ None);
let environment_manager = EnvironmentManager::default_for_tests();
let config_cwd =
config_cwd_for_app_server_target(Some(remote_only_cwd), &target, &environment_manager)?;
@@ -1939,11 +1943,12 @@ mod tests {
Ok(())
}
#[test]
fn config_cwd_for_app_server_target_canonicalizes_embedded_cli_cwd() -> std::io::Result<()> {
#[tokio::test]
async fn config_cwd_for_app_server_target_canonicalizes_embedded_cli_cwd() -> std::io::Result<()>
{
let temp_dir = TempDir::new()?;
let target = AppServerTarget::Embedded;
let environment_manager = EnvironmentManager::new(/*exec_server_url*/ None);
let environment_manager = EnvironmentManager::default_for_tests();
let config_cwd =
config_cwd_for_app_server_target(Some(temp_dir.path()), &target, &environment_manager)?;
@@ -1957,13 +1962,13 @@ mod tests {
Ok(())
}
#[test]
fn config_cwd_for_app_server_target_errors_for_missing_embedded_cli_cwd() -> std::io::Result<()>
{
#[tokio::test]
async fn config_cwd_for_app_server_target_errors_for_missing_embedded_cli_cwd()
-> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let missing = temp_dir.path().join("missing");
let target = AppServerTarget::Embedded;
let environment_manager = EnvironmentManager::new(/*exec_server_url*/ None);
let environment_manager = EnvironmentManager::default_for_tests();
let err = config_cwd_for_app_server_target(Some(&missing), &target, &environment_manager)
.expect_err("missing embedded cwd should fail");
@@ -1972,15 +1977,23 @@ mod tests {
Ok(())
}
#[test]
fn config_cwd_for_app_server_target_omits_cwd_for_remote_exec_server() -> std::io::Result<()> {
#[tokio::test]
async fn config_cwd_for_app_server_target_omits_cwd_for_remote_exec_server()
-> std::io::Result<()> {
let remote_only_cwd = if cfg!(windows) {
Path::new(r"C:\definitely\not\local\to\this\test")
} else {
Path::new("/definitely/not/local/to/this/test")
};
let target = AppServerTarget::Embedded;
let environment_manager = EnvironmentManager::new(Some("ws://127.0.0.1:8765".to_string()));
let environment_manager =
EnvironmentManager::new(codex_exec_server::EnvironmentManagerArgs {
exec_server_url: Some("ws://127.0.0.1:8765".to_string()),
local_runtime_paths: ExecServerRuntimePaths::new(
std::env::current_exe().expect("current exe"),
/*codex_linux_sandbox_exe*/ None,
)?,
});
let config_cwd =
config_cwd_for_app_server_target(Some(remote_only_cwd), &target, &environment_manager)?;
@@ -2107,7 +2120,7 @@ mod tests {
CloudRequirementsLoader::default(),
codex_feedback::CodexFeedback::new(),
/*log_db*/ None,
Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
Arc::new(EnvironmentManager::default_for_tests()),
|_args| async { Err(std::io::Error::other("boom")) },
)
.await;
+3 -3
View File
@@ -989,9 +989,9 @@ mod tests {
),
feedback: codex_feedback::CodexFeedback::new(),
log_db: None,
environment_manager: Arc::new(codex_app_server_client::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
environment_manager: Arc::new(
codex_app_server_client::EnvironmentManager::default_for_tests(),
),
config_warnings: Vec::new(),
session_source: SessionSource::Cli,
enable_codex_api_key_env: false,