use std::collections::HashMap; use std::sync::Arc; use std::sync::RwLock; use futures::FutureExt; use futures::future::BoxFuture; use crate::ExecServerError; use crate::ExecServerRuntimePaths; use crate::ExecutorFileSystem; use crate::HttpClient; use crate::client::LazyRemoteExecServerClient; use crate::client::http_client::ReqwestHttpClient; use crate::client_api::ExecServerTransportParams; use crate::environment_provider::DefaultEnvironmentProvider; use crate::environment_provider::EnvironmentDefault; use crate::environment_provider::EnvironmentProvider; use crate::environment_provider::EnvironmentProviderSnapshot; use crate::environment_provider::normalize_exec_server_url; use crate::environment_toml::environment_provider_from_codex_home; use crate::local_file_system::LocalFileSystem; use crate::local_process::LocalProcess; use crate::process::ExecBackend; use crate::protocol::EnvironmentInfo; use crate::protocol::ShellInfo; use crate::remote_file_system::RemoteFileSystem; use crate::remote_process::RemoteProcess; use codex_shell_command::shell_detect::DetectedShell; pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL"; /// Owns the execution/filesystem environments available to the Codex runtime. /// /// `EnvironmentManager` is a shared registry for environments. Its /// default constructor preserves the legacy `CODEX_EXEC_SERVER_URL` behavior /// while configured construction accepts a provider-supplied snapshot. /// /// Setting `CODEX_EXEC_SERVER_URL=none` disables environment access by leaving /// the default environment unset and omitting the local environment. 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 remote /// transport is not opened when the manager or environment is constructed. #[derive(Debug)] pub struct EnvironmentManager { default_environment: Option, environments: RwLock>>, local_environment: Option>, local_runtime_paths: Option, } pub const LOCAL_ENVIRONMENT_ID: &str = "local"; pub const REMOTE_ENVIRONMENT_ID: &str = "remote"; impl EnvironmentManager { /// 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: RwLock::new(HashMap::from([( LOCAL_ENVIRONMENT_ID.to_string(), Arc::new(Environment::default_for_tests()), )])), local_environment: Some(Arc::new(Environment::default_for_tests())), local_runtime_paths: None, } } /// Builds a manager with no configured execution environments. pub fn without_environments() -> Self { Self { default_environment: None, environments: RwLock::new(HashMap::new()), local_environment: None, local_runtime_paths: None, } } /// Builds a test-only manager from a raw exec-server URL value. pub async fn create_for_tests( exec_server_url: Option, local_runtime_paths: Option, ) -> Self { Self::from_default_provider_url(exec_server_url, local_runtime_paths).await } /// Builds a manager from `CODEX_HOME` and local runtime paths used when /// creating local filesystem helpers. /// /// If `CODEX_HOME/environments.toml` is present, it defines the configured /// environments. Otherwise this preserves the legacy /// `CODEX_EXEC_SERVER_URL` behavior. pub async fn from_codex_home( codex_home: impl AsRef, local_runtime_paths: Option, ) -> Result { let provider = environment_provider_from_codex_home(codex_home.as_ref())?; Self::from_snapshot(provider.snapshot().await?, local_runtime_paths) } /// Builds a manager from the legacy environment-variable provider without /// reading user config files from `CODEX_HOME`. pub async fn from_env( local_runtime_paths: Option, ) -> Result { let provider = DefaultEnvironmentProvider::from_env(); Self::from_snapshot(provider.snapshot().await?, local_runtime_paths) } async fn from_default_provider_url( exec_server_url: Option, local_runtime_paths: Option, ) -> Self { let provider = DefaultEnvironmentProvider::new(exec_server_url); match Self::from_snapshot(provider.snapshot_inner(), local_runtime_paths) { Ok(manager) => manager, Err(err) => panic!("default provider should create valid environments: {err}"), } } /// Builds a test-only manager that keeps the provider default while also /// allowing tests to select the local environment explicitly. pub async fn create_for_tests_with_local( exec_server_url: Option, local_runtime_paths: ExecServerRuntimePaths, ) -> Self { let mut snapshot = DefaultEnvironmentProvider::new(exec_server_url).snapshot_inner(); snapshot.include_local = true; match Self::from_snapshot(snapshot, Some(local_runtime_paths)) { Ok(manager) => manager, Err(err) => panic!("test provider with local should create valid environments: {err}"), } } fn from_snapshot( snapshot: EnvironmentProviderSnapshot, local_runtime_paths: Option, ) -> Result { let EnvironmentProviderSnapshot { environments, default, include_local, } = snapshot; let mut environment_map = HashMap::with_capacity(environments.len() + usize::from(include_local)); let local_environment = if include_local { let local_runtime_paths = local_runtime_paths.clone().ok_or_else(|| { ExecServerError::Protocol( "local environment requires configured runtime paths".to_string(), ) })?; let local_environment = Arc::new(Environment::local(local_runtime_paths)); environment_map.insert( LOCAL_ENVIRONMENT_ID.to_string(), Arc::clone(&local_environment), ); Some(local_environment) } else { None }; for (id, environment) in environments { if id.is_empty() { return Err(ExecServerError::Protocol( "environment id cannot be empty".to_string(), )); } if id == LOCAL_ENVIRONMENT_ID { return Err(ExecServerError::Protocol(format!( "environment id `{LOCAL_ENVIRONMENT_ID}` is reserved for EnvironmentManager" ))); } if environment_map .insert(id.clone(), Arc::new(environment)) .is_some() { return Err(ExecServerError::Protocol(format!( "environment id `{id}` is duplicated" ))); } } let default_environment = match default { EnvironmentDefault::Disabled => None, EnvironmentDefault::EnvironmentId(environment_id) => { if !environment_map.contains_key(&environment_id) { return Err(ExecServerError::Protocol(format!( "default environment `{environment_id}` is not configured" ))); } Some(environment_id) } }; Ok(Self { default_environment, environments: RwLock::new(environment_map), local_environment, local_runtime_paths, }) } /// Returns the default environment instance. pub fn default_environment(&self) -> Option> { self.default_environment .as_deref() .and_then(|environment_id| self.get_environment(environment_id)) } /// Returns the id of the default environment. pub fn default_environment_id(&self) -> Option<&str> { self.default_environment.as_deref() } /// Returns the ordered environment ids used for new thread startup. pub fn default_environment_ids(&self) -> Vec { let Some(default_environment_id) = self.default_environment.as_ref() else { return Vec::new(); }; let environments = self .environments .read() .unwrap_or_else(std::sync::PoisonError::into_inner); let mut environment_ids = Vec::with_capacity(environments.len()); environment_ids.push(default_environment_id.clone()); environment_ids.extend( environments .keys() .filter(|environment_id| *environment_id != default_environment_id) .cloned(), ); environment_ids } /// Returns the local environment instance when one is configured. pub fn try_local_environment(&self) -> Option> { self.local_environment.as_ref().map(Arc::clone) } /// Returns the default environment or local environment when either exists. pub fn default_or_local_environment(&self) -> Option> { self.default_environment() .or_else(|| self.try_local_environment()) } /// Returns a named environment instance. pub fn get_environment(&self, environment_id: &str) -> Option> { self.environments .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .get(environment_id) .cloned() } /// Registers a named remote environment before its exec-server endpoint is available. pub fn register_pending_environment( &self, environment_id: String, ) -> Result, ExecServerError> { validate_environment_id(&environment_id)?; let mut environments = self .environments .write() .unwrap_or_else(std::sync::PoisonError::into_inner); // Reuse the same Arc so early turn and capability handles observe later updates. Ok(Arc::clone( environments.entry(environment_id).or_insert_with(|| { Arc::new(Environment::pending_remote( self.local_runtime_paths.clone(), )) }), )) } /// Adds or replaces a named remote environment without changing the /// manager's default environment selection. pub fn upsert_environment( &self, environment_id: String, exec_server_url: String, ) -> Result<(), ExecServerError> { validate_environment_id(&environment_id)?; let (exec_server_url, disabled) = normalize_exec_server_url(Some(exec_server_url)); if disabled { return Err(ExecServerError::Protocol( "remote environment cannot use disabled exec-server url".to_string(), )); } let Some(exec_server_url) = exec_server_url else { return Err(ExecServerError::Protocol( "remote environment requires an exec-server url".to_string(), )); }; let mut environments = self .environments .write() .unwrap_or_else(std::sync::PoisonError::into_inner); match environments.get(&environment_id) { // Update the shared client inside the stable Environment handle. Some(existing) if existing.is_remote() => { existing.set_remote_transport(ExecServerTransportParams::websocket_url( exec_server_url, )); existing.start_loading_info(); } _ => { let environment = Arc::new(Environment::remote_inner( exec_server_url, self.local_runtime_paths.clone(), )); environment.start_loading_info(); environments.insert(environment_id, environment); } } Ok(()) } } fn validate_environment_id(environment_id: &str) -> Result<(), ExecServerError> { if environment_id.is_empty() { return Err(ExecServerError::Protocol( "environment id cannot be empty".to_string(), )); } if environment_id == LOCAL_ENVIRONMENT_ID { return Err(ExecServerError::Protocol(format!( "environment id `{LOCAL_ENVIRONMENT_ID}` is reserved for EnvironmentManager" ))); } Ok(()) } /// Execution/filesystem environment selected for a session. /// /// The handle remains stable while its concrete backend is supplied or replaced. #[derive(Clone)] pub struct Environment { // None is local; Some is remote and may still be waiting for its transport. remote_client: Option, info_provider: Arc, exec_backend: Arc, filesystem: Arc, http_client: Arc, local_runtime_paths: Option, } /// Provides environment metadata from either a local environment or a remote exec-server. trait EnvironmentInfoProvider: Send + Sync { fn info(&self) -> BoxFuture<'_, Result>; } struct LocalEnvironmentInfoProvider; impl EnvironmentInfoProvider for LocalEnvironmentInfoProvider { fn info(&self) -> BoxFuture<'_, Result> { std::future::ready(Ok(EnvironmentInfo::local())).boxed() } } struct RemoteEnvironmentInfoProvider { client: LazyRemoteExecServerClient, } impl RemoteEnvironmentInfoProvider { fn new(client: LazyRemoteExecServerClient) -> Self { Self { client } } } impl EnvironmentInfoProvider for RemoteEnvironmentInfoProvider { fn info(&self) -> BoxFuture<'_, Result> { async move { self.client.environment_info().await }.boxed() } } impl Environment { /// Builds a test-only local environment without configured sandbox helper paths. pub fn default_for_tests() -> Self { Self { remote_client: None, info_provider: Arc::new(LocalEnvironmentInfoProvider), exec_backend: Arc::new(LocalProcess::default()), filesystem: Arc::new(LocalFileSystem::unsandboxed()), http_client: Arc::new(ReqwestHttpClient), local_runtime_paths: None, } } } impl std::fmt::Debug for Environment { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Environment") .field("is_remote", &self.is_remote()) .field("is_ready", &self.is_ready()) .field("exec_server_url", &self.exec_server_url()) .finish_non_exhaustive() } } impl Environment { /// Builds an environment from the raw `CODEX_EXEC_SERVER_URL` value. pub fn create( exec_server_url: Option, local_runtime_paths: ExecServerRuntimePaths, ) -> Result { 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) -> Result { 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. fn create_inner( exec_server_url: Option, local_runtime_paths: Option, ) -> Result { let (exec_server_url, disabled) = normalize_exec_server_url(exec_server_url); if disabled { return Err(ExecServerError::Protocol( "disabled mode does not create an Environment".to_string(), )); } 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(), }, }) } pub(crate) fn local(local_runtime_paths: ExecServerRuntimePaths) -> Self { Self { remote_client: None, info_provider: Arc::new(LocalEnvironmentInfoProvider), exec_backend: Arc::new(LocalProcess::default()), filesystem: Arc::new(LocalFileSystem::with_runtime_paths( local_runtime_paths.clone(), )), http_client: Arc::new(ReqwestHttpClient), local_runtime_paths: Some(local_runtime_paths), } } fn pending_remote(local_runtime_paths: Option) -> Self { // Build capability wrappers now; their first operation waits inside the lazy client. Self::remote_with_client(LazyRemoteExecServerClient::pending(), local_runtime_paths) } pub(crate) fn remote_inner( exec_server_url: String, local_runtime_paths: Option, ) -> Self { Self::remote_with_transport( ExecServerTransportParams::websocket_url(exec_server_url), local_runtime_paths, ) } pub(crate) fn remote_with_transport( remote_transport: ExecServerTransportParams, local_runtime_paths: Option, ) -> Self { Self::remote_with_client( LazyRemoteExecServerClient::new(remote_transport), local_runtime_paths, ) } fn remote_with_client( client: LazyRemoteExecServerClient, local_runtime_paths: Option, ) -> Self { // These clones share one internal state, so every capability sees transport updates. let exec_backend: Arc = Arc::new(RemoteProcess::new(client.clone())); let filesystem: Arc = Arc::new(RemoteFileSystem::new(client.clone())); Self { remote_client: Some(client.clone()), info_provider: Arc::new(RemoteEnvironmentInfoProvider::new(client.clone())), exec_backend, filesystem, http_client: Arc::new(client), local_runtime_paths, } } pub fn is_remote(&self) -> bool { self.remote_client.is_some() } /// Ready means we know how to connect; the connection is opened only when needed. pub fn is_ready(&self) -> bool { self.remote_client .as_ref() .is_none_or(|client| client.transport_params().is_some()) } /// Returns the currently configured remote exec-server URL. pub fn exec_server_url(&self) -> Option { self.remote_client .as_ref()? .transport_params() .and_then(|transport_params| match transport_params.as_ref() { ExecServerTransportParams::WebSocketUrl { websocket_url, .. } => { Some(websocket_url.clone()) } ExecServerTransportParams::StdioCommand { .. } => None, }) } pub fn local_runtime_paths(&self) -> Option<&ExecServerRuntimePaths> { self.local_runtime_paths.as_ref() } /// Returns environment information from the selected execution/filesystem environment. pub async fn info(&self) -> Result { self.info_provider.info().await } /// Returns already-loaded environment information without starting a connection. pub fn current_info(&self) -> Option { self.remote_client.as_ref().map_or_else( || Some(EnvironmentInfo::local()), LazyRemoteExecServerClient::current_environment_info, ) } /// Starts loading remote metadata without making the caller wait for it. pub fn start_loading_info(self: &Arc) { if self.current_info().is_some() || !self.is_ready() { return; } let Ok(runtime) = tokio::runtime::Handle::try_current() else { return; }; let Some(remote_client) = self.remote_client.clone() else { return; }; let Some(load_token) = remote_client.try_begin_environment_info_load() else { return; }; let environment = Arc::clone(self); runtime.spawn(async move { let result = environment.info().await; let transport_changed = remote_client.environment_info_load_token() != load_token; let reload_requested = remote_client.finish_environment_info_load(load_token); if let Err(err) = result { tracing::warn!("failed to load environment metadata: {err}"); } if transport_changed || reload_requested { environment.start_loading_info(); } }); } pub fn get_exec_backend(&self) -> Arc { Arc::clone(&self.exec_backend) } pub fn get_http_client(&self) -> Arc { Arc::clone(&self.http_client) } pub fn get_filesystem(&self) -> Arc { Arc::clone(&self.filesystem) } fn set_remote_transport(&self, remote_transport: ExecServerTransportParams) { // Mutate the shared client rather than replacing the Environment held by existing turns. if let Some(client) = &self.remote_client { client.set_transport(remote_transport); } } } impl EnvironmentInfo { pub(crate) fn local() -> Self { Self { shell: codex_shell_command::shell_detect::default_user_shell().into(), } } } impl From for ShellInfo { fn from(shell: DetectedShell) -> Self { Self { name: shell.name().to_string(), path: shell.shell_path.to_string_lossy().into_owned(), } } } #[cfg(test)] mod tests { use std::sync::Arc; use std::time::Duration; use super::Environment; use super::EnvironmentManager; use super::LOCAL_ENVIRONMENT_ID; use super::REMOTE_ENVIRONMENT_ID; use crate::ExecServerRuntimePaths; use crate::ProcessId; use crate::environment_provider::EnvironmentDefault; use crate::environment_provider::EnvironmentProviderSnapshot; 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") } fn assert_local_environment_unavailable(manager: &EnvironmentManager) { assert!(manager.try_local_environment().is_none()); } #[tokio::test] async fn create_local_environment_does_not_connect() { let environment = Environment::create(/*exec_server_url*/ None, test_runtime_paths()) .expect("create environment"); assert_eq!(environment.exec_server_url(), None); assert!(!environment.is_remote()); assert!(environment.info().await.is_ok()); } #[tokio::test] async fn environment_manager_normalizes_empty_url() { let manager = EnvironmentManager::create_for_tests(Some(String::new()), Some(test_runtime_paths())) .await; let environment = manager.default_environment().expect("default environment"); assert_eq!(manager.default_environment_id(), Some(LOCAL_ENVIRONMENT_ID)); assert!(Arc::ptr_eq( &environment, &manager .get_environment(LOCAL_ENVIRONMENT_ID) .expect("local environment") )); assert!(Arc::ptr_eq( &environment, &manager.try_local_environment().expect("local environment") )); assert!(manager.try_local_environment().is_some()); assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none()); assert!(!environment.is_remote()); } #[tokio::test] async fn disabled_environment_manager_has_no_default_or_local_environment() { let manager = EnvironmentManager::without_environments(); assert!(manager.default_environment().is_none()); assert_eq!(manager.default_environment_id(), None); assert_local_environment_unavailable(&manager); assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none()); assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none()); } #[tokio::test] async fn environment_manager_reports_remote_url() { let manager = EnvironmentManager::create_for_tests( Some("ws://127.0.0.1:8765".to_string()), Some(test_runtime_paths()), ) .await; let environment = manager.default_environment().expect("default environment"); assert_eq!( manager.default_environment_id(), Some(REMOTE_ENVIRONMENT_ID) ); assert!(environment.is_remote()); assert_eq!( environment.exec_server_url().as_deref(), 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).is_none()); assert_local_environment_unavailable(&manager); } #[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_builds_from_snapshot() { let snapshot = EnvironmentProviderSnapshot { environments: vec![( REMOTE_ENVIRONMENT_ID.to_string(), Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string())) .expect("remote environment"), )], default: EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string()), include_local: false, }; let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths())) .expect("environment manager"); assert_eq!( manager.default_environment_id(), Some(REMOTE_ENVIRONMENT_ID) ); assert!( manager .get_environment(REMOTE_ENVIRONMENT_ID) .expect("remote environment") .is_remote() ); assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none()); assert_local_environment_unavailable(&manager); } #[tokio::test] async fn environment_manager_rejects_empty_environment_id() { let snapshot = EnvironmentProviderSnapshot { environments: vec![("".to_string(), Environment::default_for_tests())], default: EnvironmentDefault::Disabled, include_local: false, }; let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths())) .expect_err("empty id should fail"); assert_eq!( err.to_string(), "exec-server protocol error: environment id cannot be empty" ); } #[tokio::test] async fn environment_manager_rejects_provider_supplied_local_environment() { let snapshot = EnvironmentProviderSnapshot { environments: vec![( LOCAL_ENVIRONMENT_ID.to_string(), Environment::default_for_tests(), )], default: EnvironmentDefault::Disabled, include_local: false, }; let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths())) .expect_err("local id should fail"); assert_eq!( err.to_string(), "exec-server protocol error: environment id `local` is reserved for EnvironmentManager" ); } #[tokio::test] async fn environment_manager_uses_explicit_provider_default() { let snapshot = EnvironmentProviderSnapshot { environments: vec![( "devbox".to_string(), Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string())) .expect("remote environment"), )], default: EnvironmentDefault::EnvironmentId("devbox".to_string()), include_local: true, }; let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths())) .expect("manager"); assert_eq!(manager.default_environment_id(), Some("devbox")); assert_eq!( manager.default_environment_ids(), vec!["devbox".to_string(), LOCAL_ENVIRONMENT_ID.to_string()] ); assert!(manager.default_environment().expect("default").is_remote()); } #[tokio::test] async fn environment_manager_disables_provider_default() { let snapshot = EnvironmentProviderSnapshot { environments: vec![( "devbox".to_string(), Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string())) .expect("remote environment"), )], default: EnvironmentDefault::Disabled, include_local: true, }; let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths())) .expect("manager"); assert_eq!(manager.default_environment_id(), None); assert!(manager.default_environment().is_none()); assert!(Arc::ptr_eq( &manager .get_environment(LOCAL_ENVIRONMENT_ID) .expect("local environment"), &manager.try_local_environment().expect("local environment") )); } #[tokio::test] async fn environment_manager_rejects_unknown_provider_default() { let snapshot = EnvironmentProviderSnapshot { environments: vec![( "devbox".to_string(), Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string())) .expect("remote environment"), )], default: EnvironmentDefault::EnvironmentId("missing".to_string()), include_local: true, }; let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths())) .expect_err("unknown default should fail"); assert_eq!( err.to_string(), "exec-server protocol error: default environment `missing` is not configured" ); } #[tokio::test] async fn environment_manager_includes_local_for_default_provider_without_url() { let manager = EnvironmentManager::create_for_tests( /*exec_server_url*/ None, Some(test_runtime_paths()), ) .await; let environment = manager.default_environment().expect("default environment"); assert_eq!(manager.default_environment_id(), Some(LOCAL_ENVIRONMENT_ID)); assert!(Arc::ptr_eq( &environment, &manager .get_environment(LOCAL_ENVIRONMENT_ID) .expect("local environment") )); assert!(Arc::ptr_eq( &environment, &manager.try_local_environment().expect("local environment") )); assert!(!environment.is_remote()); } #[tokio::test] async fn environment_manager_carries_local_runtime_paths() { let runtime_paths = test_runtime_paths(); let manager = EnvironmentManager::create_for_tests( /*exec_server_url*/ None, Some(runtime_paths.clone()), ) .await; let environment = manager.try_local_environment().expect("local environment"); assert_eq!(environment.local_runtime_paths(), Some(&runtime_paths)); let manager = EnvironmentManager::create_for_tests( environment.exec_server_url(), Some( environment .local_runtime_paths() .expect("local runtime paths") .clone(), ), ) .await; let environment = manager.try_local_environment().expect("local environment"); assert_eq!(environment.local_runtime_paths(), Some(&runtime_paths)); } #[tokio::test] async fn environment_manager_omits_default_provider_local_lookup_when_default_disabled() { let manager = EnvironmentManager::create_for_tests( Some("none".to_string()), Some(test_runtime_paths()), ) .await; assert!(manager.default_environment().is_none()); assert_eq!(manager.default_environment_id(), None); assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none()); assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none()); assert_local_environment_unavailable(&manager); } #[tokio::test] async fn environment_manager_snapshot_without_local_environment_disables_local_default() { let mut snapshot = EnvironmentProviderSnapshot { environments: Vec::new(), default: EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string()), include_local: true, }; snapshot.include_local = false; snapshot.default = EnvironmentDefault::Disabled; let manager = EnvironmentManager::from_snapshot(snapshot, /*local_runtime_paths*/ None) .expect("environment manager"); assert!(manager.default_environment().is_none()); assert_eq!(manager.default_environment_id(), None); assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none()); assert_local_environment_unavailable(&manager); } #[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 environment_manager_upserts_named_remote_environment() { let reserved_listener = tokio::net::TcpListener::bind("127.0.0.1:0") .await .expect("test exec-server port should bind"); let exec_server_address = reserved_listener .local_addr() .expect("test exec-server should have an address"); drop(reserved_listener); let exec_server_url = format!("ws://{exec_server_address}"); let exec_server_url_for_task = exec_server_url.clone(); let exec_server_task = tokio::spawn(async move { crate::run_main(&exec_server_url_for_task, test_runtime_paths()).await }); tokio::time::timeout(Duration::from_secs(1), async { loop { if tokio::net::TcpStream::connect(exec_server_address) .await .is_ok() { break; } tokio::task::yield_now().await; } }) .await .expect("test exec-server should start"); let manager = EnvironmentManager::without_environments(); let pending = manager .register_pending_environment("executor-a".to_string()) .expect("pending environment"); assert!(pending.is_remote() && !pending.is_ready()); let exec_backend = pending.get_exec_backend(); let http_client = pending.get_http_client(); let filesystem = pending.get_filesystem(); let waiting_exec_backend = Arc::clone(&exec_backend); let exec_waiter = tokio::spawn(async move { waiting_exec_backend .start(crate::ExecParams { process_id: ProcessId::from("pending-environment-proc"), argv: vec!["true".to_string()], cwd: std::env::current_dir().expect("read current dir"), env_policy: None, env: Default::default(), tty: false, pipe_stdin: false, arg0: None, }) .await }); tokio::task::yield_now().await; assert!(!exec_waiter.is_finished()); manager .upsert_environment("executor-a".to_string(), exec_server_url.clone()) .expect("remote environment"); let started = tokio::time::timeout(Duration::from_secs(2), exec_waiter) .await .expect("pending capability should wake") .expect("pending capability task should not panic") .expect("pending capability should execute"); assert_eq!( started.process.process_id().as_str(), "pending-environment-proc" ); let first = manager .get_environment("executor-a") .expect("first remote environment"); assert!(Arc::ptr_eq(&pending, &first) && first.is_ready()); assert!(first.is_remote()); assert_eq!( first.exec_server_url().as_deref(), Some(exec_server_url.as_str()) ); let loaded_info = tokio::time::timeout(Duration::from_secs(1), async { loop { if let Some(info) = first.current_info() { break info; } tokio::task::yield_now().await; } }) .await .expect("environment metadata should load in the background"); assert_eq!( first.info().await.expect("cached environment info"), loaded_info ); assert_eq!(manager.default_environment_id(), None); manager .upsert_environment("executor-a".to_string(), "ws://127.0.0.1:9876".to_string()) .expect("updated remote environment"); let second = manager .get_environment("executor-a") .expect("second remote environment"); assert!(second.is_remote()); assert_eq!( second.exec_server_url().as_deref(), Some("ws://127.0.0.1:9876") ); assert_eq!(second.current_info(), None); assert!(Arc::ptr_eq(&first, &second)); assert!(Arc::ptr_eq(&exec_backend, &second.get_exec_backend())); assert!(Arc::ptr_eq(&http_client, &second.get_http_client())); assert!(Arc::ptr_eq(&filesystem, &second.get_filesystem())); exec_server_task.abort(); let _ = exec_server_task.await; } #[tokio::test] async fn environment_manager_rejects_reserved_dynamic_environment_id() { let manager = EnvironmentManager::default_for_tests(); let errors = vec![ manager .register_pending_environment(LOCAL_ENVIRONMENT_ID.to_string()) .expect_err("pending local environment should fail") .to_string(), manager .upsert_environment( LOCAL_ENVIRONMENT_ID.to_string(), "ws://127.0.0.1:8765".to_string(), ) .expect_err("remote local environment should fail") .to_string(), ]; assert_eq!( errors, vec![ "exec-server protocol error: environment id `local` is reserved for EnvironmentManager", "exec-server protocol error: environment id `local` is reserved for EnvironmentManager", ] ); } #[tokio::test] async fn environment_manager_rejects_empty_remote_environment_url() { let manager = EnvironmentManager::without_environments(); let err = manager .upsert_environment("executor-a".to_string(), String::new()) .expect_err("empty URL should fail"); assert_eq!( err.to_string(), "exec-server protocol error: remote environment requires an exec-server url" ); } #[tokio::test] async fn default_environment_has_ready_local_executor() { let environment = Environment::default_for_tests(); let response = environment .get_exec_backend() .start(crate::ExecParams { process_id: ProcessId::from("default-env-proc"), argv: vec!["true".to_string()], cwd: std::env::current_dir().expect("read current dir"), env_policy: None, env: Default::default(), tty: false, pipe_stdin: false, arg0: None, }) .await .expect("start process"); 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 path = codex_utils_path_uri::PathUri::from_abs_path(&path).expect("path URI"); let sandbox = crate::FileSystemSandboxContext::from_permission_profile( codex_protocol::models::PermissionProfile::from_runtime_permissions( &codex_protocol::permissions::FileSystemSandboxPolicy::restricted(Vec::new()), codex_protocol::permissions::NetworkSandboxPolicy::Restricted, ), ); 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" ); } }