1use std::collections::HashMap;
2use std::collections::HashSet;
3use std::sync::Arc;
4use std::sync::Mutex;
5use std::sync::OnceLock;
6use std::sync::RwLock;
7
8use codex_protocol::capabilities::CapabilityRootLocation;
9use codex_protocol::capabilities::SelectedCapabilityRoot;
10use futures::FutureExt;
11
12use crate::CapabilityRootsDiscoverParams;
13use crate::CapabilityRootsDiscoverResponse;
14use crate::ExecServerError;
15use crate::ExecServerRuntimePaths;
16use crate::ExecutorFileSystem;
17use crate::HttpClient;
18use crate::NoiseChannelIdentity;
19use crate::NoiseRendezvousConnectProvider;
20use crate::client::LazyRemoteExecServerClient;
21use crate::client::http_client::ReqwestHttpClient;
22use crate::client_api::DEFAULT_REMOTE_EXEC_SERVER_CONNECT_TIMEOUT;
23use crate::client_api::ExecServerTransportParams;
24use crate::environment_provider::DefaultEnvironmentProvider;
25use crate::environment_provider::EnvironmentDefault;
26use crate::environment_provider::EnvironmentProvider;
27use crate::environment_provider::EnvironmentProviderSnapshot;
28use crate::environment_provider::normalize_exec_server_url;
29use crate::environment_toml::environment_provider_from_codex_home;
30use crate::local_file_system::LocalFileSystem;
31use crate::local_process::LocalProcess;
32use crate::process::ExecBackend;
33use crate::protocol::EnvironmentInfo;
34use crate::remote::NoiseRendezvousEnvironmentConfig;
35use crate::remote_file_system::RemoteFileSystem;
36use crate::remote_process::RemoteProcess;
37use tokio::sync::oneshot;
38use tokio::sync::watch;
39use tokio_util::task::AbortOnDropHandle;
40
41pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL";
42pub const CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR: &str =
43 "CODEX_EXEC_SERVER_NOISE_REGISTRY_URL";
44pub const CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR: &str =
45 "CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID";
46pub const CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR: &str = "CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN";
47pub const CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR: &str =
48 "CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID";
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
52pub enum EnvironmentConnectionState {
53 Connected,
55 Disconnected,
57}
58
59#[derive(Debug)]
74pub struct EnvironmentManager {
75 default_environment: Option<String>,
76 pub(super) environments: RwLock<HashMap<String, Arc<Environment>>>,
77 local_environment: Option<Arc<Environment>>,
78 local_runtime_paths: Option<ExecServerRuntimePaths>,
79}
80
81#[derive(Clone, Debug, Default, Eq, PartialEq)]
83pub struct EnvironmentReadyInfo {
84 pub selected_capability_roots: Vec<SelectedCapabilityRoot>,
86}
87
88#[must_use = "the deferred environment cannot connect until registration is completed"]
90pub struct DeferredEnvironmentRegistration {
91 completion: oneshot::Sender<Result<(), String>>,
92 environment_id: String,
93 ready_info: Arc<OnceLock<EnvironmentReadyInfo>>,
94}
95
96pub const MAX_SELECTED_CAPABILITY_ROOTS: usize = 256;
98
99pub const LOCAL_ENVIRONMENT_ID: &str = "local";
100pub const REMOTE_ENVIRONMENT_ID: &str = "remote";
101
102#[derive(Debug, Clone, PartialEq, Eq)]
108pub enum EnvironmentObservedStatus {
109 Ready,
111 Pending,
116 Disconnected {
121 error: String,
123 },
124}
125
126impl EnvironmentManager {
127 pub fn default_for_tests() -> Self {
129 Self {
130 default_environment: Some(LOCAL_ENVIRONMENT_ID.to_string()),
131 environments: RwLock::new(HashMap::from([(
132 LOCAL_ENVIRONMENT_ID.to_string(),
133 Arc::new(Environment::default_for_tests()),
134 )])),
135 local_environment: Some(Arc::new(Environment::default_for_tests())),
136 local_runtime_paths: None,
137 }
138 }
139
140 pub fn without_environments() -> Self {
142 Self {
143 default_environment: None,
144 environments: RwLock::new(HashMap::new()),
145 local_environment: None,
146 local_runtime_paths: None,
147 }
148 }
149
150 pub async fn create_for_tests(
152 exec_server_url: Option<String>,
153 local_runtime_paths: Option<ExecServerRuntimePaths>,
154 ) -> Self {
155 Self::from_default_provider_url(exec_server_url, local_runtime_paths).await
156 }
157
158 pub async fn from_codex_home(
165 codex_home: impl AsRef<std::path::Path>,
166 local_runtime_paths: Option<ExecServerRuntimePaths>,
167 ) -> Result<Self, ExecServerError> {
168 if let Some(config) = noise_environment_config_from_env()? {
169 return Self::from_noise_environment_config(config, local_runtime_paths);
170 }
171 let provider = environment_provider_from_codex_home(codex_home.as_ref())?;
172 Self::from_snapshot(provider.snapshot().await?, local_runtime_paths)
173 }
174
175 pub async fn from_env(
178 local_runtime_paths: Option<ExecServerRuntimePaths>,
179 ) -> Result<Self, ExecServerError> {
180 if let Some(config) = noise_environment_config_from_env()? {
181 return Self::from_noise_environment_config(config, local_runtime_paths);
182 }
183 let provider = DefaultEnvironmentProvider::from_env();
184 Self::from_snapshot(provider.snapshot().await?, local_runtime_paths)
185 }
186
187 async fn from_default_provider_url(
188 exec_server_url: Option<String>,
189 local_runtime_paths: Option<ExecServerRuntimePaths>,
190 ) -> Self {
191 let provider = DefaultEnvironmentProvider::new(exec_server_url);
192 match Self::from_snapshot(provider.snapshot_inner(), local_runtime_paths) {
193 Ok(manager) => manager,
194 Err(err) => panic!("default provider should create valid environments: {err}"),
195 }
196 }
197
198 fn from_noise_environment_config(
199 config: NoiseRendezvousEnvironmentConfig,
200 local_runtime_paths: Option<ExecServerRuntimePaths>,
201 ) -> Result<Self, ExecServerError> {
202 let manager = Self {
203 default_environment: Some(REMOTE_ENVIRONMENT_ID.to_string()),
204 environments: RwLock::new(HashMap::new()),
205 local_environment: None,
206 local_runtime_paths,
207 };
208 manager.upsert_noise_environment(
209 REMOTE_ENVIRONMENT_ID.to_string(),
210 config.connect_provider(),
211 )?;
212 Ok(manager)
213 }
214
215 pub async fn create_for_tests_with_local(
218 exec_server_url: Option<String>,
219 local_runtime_paths: ExecServerRuntimePaths,
220 ) -> Self {
221 let mut snapshot = DefaultEnvironmentProvider::new(exec_server_url).snapshot_inner();
222 snapshot.include_local = true;
223 match Self::from_snapshot(snapshot, Some(local_runtime_paths)) {
224 Ok(manager) => manager,
225 Err(err) => panic!("test provider with local should create valid environments: {err}"),
226 }
227 }
228
229 fn from_snapshot(
230 snapshot: EnvironmentProviderSnapshot,
231 local_runtime_paths: Option<ExecServerRuntimePaths>,
232 ) -> Result<Self, ExecServerError> {
233 let EnvironmentProviderSnapshot {
234 environments,
235 default,
236 include_local,
237 } = snapshot;
238 let mut environment_map =
239 HashMap::with_capacity(environments.len() + usize::from(include_local));
240 let local_environment = if include_local {
241 let local_runtime_paths = local_runtime_paths.clone().ok_or_else(|| {
242 ExecServerError::Protocol(
243 "local environment requires configured runtime paths".to_string(),
244 )
245 })?;
246 let local_environment = Arc::new(Environment::local(local_runtime_paths));
247 environment_map.insert(
248 LOCAL_ENVIRONMENT_ID.to_string(),
249 Arc::clone(&local_environment),
250 );
251 Some(local_environment)
252 } else {
253 None
254 };
255 for (id, environment) in environments {
256 if id.is_empty() {
257 return Err(ExecServerError::Protocol(
258 "environment id cannot be empty".to_string(),
259 ));
260 }
261 if id == LOCAL_ENVIRONMENT_ID {
262 return Err(ExecServerError::Protocol(format!(
263 "environment id `{LOCAL_ENVIRONMENT_ID}` is reserved for EnvironmentManager"
264 )));
265 }
266 if environment_map
267 .insert(id.clone(), Arc::new(environment))
268 .is_some()
269 {
270 return Err(ExecServerError::Protocol(format!(
271 "environment id `{id}` is duplicated"
272 )));
273 }
274 }
275 let default_environment = match default {
276 EnvironmentDefault::Disabled => None,
277 EnvironmentDefault::EnvironmentId(environment_id) => {
278 if !environment_map.contains_key(&environment_id) {
279 return Err(ExecServerError::Protocol(format!(
280 "default environment `{environment_id}` is not configured"
281 )));
282 }
283 Some(environment_id)
284 }
285 };
286 for environment in environment_map.values() {
288 environment.start_connecting();
289 }
290 Ok(Self {
291 default_environment,
292 environments: RwLock::new(environment_map),
293 local_environment,
294 local_runtime_paths,
295 })
296 }
297
298 pub fn default_environment(&self) -> Option<Arc<Environment>> {
300 self.default_environment
301 .as_deref()
302 .and_then(|environment_id| self.get_environment(environment_id))
303 }
304
305 pub fn default_environment_id(&self) -> Option<&str> {
307 self.default_environment.as_deref()
308 }
309
310 pub fn default_environment_ids(&self) -> Vec<String> {
312 let Some(default_environment_id) = self.default_environment.as_ref() else {
313 return Vec::new();
314 };
315 let environments = self
316 .environments
317 .read()
318 .unwrap_or_else(std::sync::PoisonError::into_inner);
319 let mut environment_ids = Vec::with_capacity(environments.len());
320 environment_ids.push(default_environment_id.clone());
321 environment_ids.extend(
322 environments
323 .keys()
324 .filter(|environment_id| *environment_id != default_environment_id)
325 .cloned(),
326 );
327 environment_ids
328 }
329
330 pub fn try_local_environment(&self) -> Option<Arc<Environment>> {
332 self.local_environment.as_ref().map(Arc::clone)
333 }
334
335 pub fn get_environment(&self, environment_id: &str) -> Option<Arc<Environment>> {
337 self.environments
338 .read()
339 .unwrap_or_else(std::sync::PoisonError::into_inner)
340 .get(environment_id)
341 .cloned()
342 }
343
344 pub async fn get_environment_status(
346 &self,
347 environment_id: &str,
348 ) -> Option<EnvironmentObservedStatus> {
349 let environment = self.get_environment(environment_id)?;
350 Some(environment.status().await)
351 }
352
353 pub fn upsert_environment(
357 &self,
358 environment_id: String,
359 exec_server_url: String,
360 connect_timeout: Option<std::time::Duration>,
361 ) -> Result<(), ExecServerError> {
362 validate_environment_id(&environment_id)?;
363 let exec_server_url = validate_remote_exec_server_url(exec_server_url)?;
364 let environment = Arc::new(Environment::remote_with_transport(
365 ExecServerTransportParams::websocket_url(
366 exec_server_url,
367 connect_timeout.unwrap_or(DEFAULT_REMOTE_EXEC_SERVER_CONNECT_TIMEOUT),
368 ),
369 self.local_runtime_paths.clone(),
370 ));
371 self.insert_environment(environment_id, environment);
372 Ok(())
373 }
374
375 pub fn register_deferred_noise_environment(
377 &self,
378 environment_id: String,
379 provider: Arc<dyn NoiseRendezvousConnectProvider>,
380 ) -> Result<DeferredEnvironmentRegistration, ExecServerError> {
381 validate_environment_id(&environment_id)?;
382 let identity = noise_channel_identity()?;
383 let (completion, readiness) = oneshot::channel();
384 let ready_info = Arc::new(OnceLock::new());
385 let mut environment = Environment::remote_with_transport(
386 ExecServerTransportParams::Deferred(Box::new(crate::client_api::Deferred {
387 readiness: readiness.shared(),
388 transport: ExecServerTransportParams::NoiseRendezvous { provider, identity },
389 })),
390 self.local_runtime_paths.clone(),
391 );
392 environment.ready_info = Some(Arc::clone(&ready_info));
393 let environment = Arc::new(environment);
394 self.insert_environment(environment_id.clone(), environment);
395 Ok(DeferredEnvironmentRegistration {
396 completion,
397 environment_id,
398 ready_info,
399 })
400 }
401
402 pub fn upsert_noise_environment(
408 &self,
409 environment_id: String,
410 provider: Arc<dyn NoiseRendezvousConnectProvider>,
411 ) -> Result<(), ExecServerError> {
412 validate_environment_id(&environment_id)?;
413 let identity = noise_channel_identity()?;
414 let environment = Arc::new(Environment::remote_with_transport(
415 ExecServerTransportParams::NoiseRendezvous { provider, identity },
416 self.local_runtime_paths.clone(),
417 ));
418 self.insert_environment(environment_id, environment);
419 Ok(())
420 }
421
422 fn insert_environment(&self, environment_id: String, environment: Arc<Environment>) {
423 self.environments
424 .write()
425 .unwrap_or_else(std::sync::PoisonError::into_inner)
426 .insert(environment_id, Arc::clone(&environment));
427 environment.start_connecting();
428 }
429}
430
431impl DeferredEnvironmentRegistration {
432 pub fn complete(
434 self,
435 result: Result<EnvironmentReadyInfo, String>,
436 ) -> Result<(), ExecServerError> {
437 let result = match result {
438 Ok(ready_info) => {
439 if ready_info.selected_capability_roots.len() > MAX_SELECTED_CAPABILITY_ROOTS {
440 let error = ExecServerError::Protocol(format!(
441 "environment ready info contains more than {MAX_SELECTED_CAPABILITY_ROOTS} selected capability roots"
442 ));
443 let _ = self.completion.send(Err(error.to_string()));
444 return Err(error);
445 }
446 let mut root_ids =
447 HashSet::with_capacity(ready_info.selected_capability_roots.len());
448 for root in &ready_info.selected_capability_roots {
449 let CapabilityRootLocation::Environment { environment_id, .. } = &root.location;
450 if root.id.trim().is_empty()
451 || environment_id != &self.environment_id
452 || !root_ids.insert(root.id.as_str())
453 {
454 let error = ExecServerError::Protocol(format!(
455 "selected capability roots must have unique non-empty IDs and belong to environment `{}`",
456 self.environment_id
457 ));
458 let _ = self.completion.send(Err(error.to_string()));
459 return Err(error);
460 }
461 }
462 if self.ready_info.set(ready_info).is_err() {
463 let error = ExecServerError::Protocol(
464 "deferred environment ready info was already set".to_string(),
465 );
466 let _ = self.completion.send(Err(error.to_string()));
467 return Err(error);
468 }
469 Ok(())
470 }
471 Err(message) => Err(message),
472 };
473 self.completion.send(result).map_err(|_| {
474 ExecServerError::Disconnected("deferred environment registration is inactive".into())
475 })
476 }
477}
478
479fn noise_channel_identity() -> Result<NoiseChannelIdentity, ExecServerError> {
480 NoiseChannelIdentity::generate().map_err(|error| {
481 ExecServerError::Protocol(format!(
482 "failed to generate Noise harness identity: {error}"
483 ))
484 })
485}
486
487fn validate_environment_id(environment_id: &str) -> Result<(), ExecServerError> {
488 if environment_id.is_empty() {
489 return Err(ExecServerError::Protocol(
490 "environment id cannot be empty".to_string(),
491 ));
492 }
493 Ok(())
494}
495
496fn validate_remote_exec_server_url(exec_server_url: String) -> Result<String, ExecServerError> {
497 let (exec_server_url, disabled) = normalize_exec_server_url(Some(exec_server_url));
498 if disabled {
499 return Err(ExecServerError::Protocol(
500 "remote environment cannot use disabled exec-server url".to_string(),
501 ));
502 }
503 exec_server_url.ok_or_else(|| {
504 ExecServerError::Protocol("remote environment requires an exec-server url".to_string())
505 })
506}
507
508fn noise_environment_config_from_env()
509-> Result<Option<NoiseRendezvousEnvironmentConfig>, ExecServerError> {
510 noise_environment_config_from_values(
511 optional_environment_value(CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR),
512 optional_environment_value(CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR),
513 optional_environment_value(CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR),
514 optional_environment_value(CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR),
515 )
516}
517
518fn noise_environment_config_from_values(
519 registry_url: Option<String>,
520 environment_id: Option<String>,
521 auth_token: Option<String>,
522 chatgpt_account_id: Option<String>,
523) -> Result<Option<NoiseRendezvousEnvironmentConfig>, ExecServerError> {
524 let (registry_url, environment_id, auth_token) =
525 match (registry_url, environment_id, auth_token) {
526 (None, None, None) => return Ok(None),
527 (Some(registry_url), Some(environment_id), Some(auth_token)) => {
528 (registry_url, environment_id, auth_token)
529 }
530 _ => {
531 return Err(ExecServerError::EnvironmentRegistryConfig(format!(
532 "Noise environment requires {CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR}, \
533{CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR}, and \
534{CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR}"
535 )));
536 }
537 };
538
539 let config = NoiseRendezvousEnvironmentConfig::new(
540 registry_url,
541 environment_id,
542 auth_token,
543 chatgpt_account_id,
544 )?;
545 Ok(Some(config))
546}
547
548fn optional_environment_value(name: &str) -> Option<String> {
549 std::env::var(name)
550 .ok()
551 .map(|value| value.trim().to_string())
552 .filter(|value| !value.is_empty())
553}
554
555#[derive(Clone)]
560pub struct Environment {
561 remote_client: Option<LazyRemoteExecServerClient>,
562 ready_info: Option<Arc<OnceLock<EnvironmentReadyInfo>>>,
563 startup_task: Arc<Mutex<Option<AbortOnDropHandle<()>>>>,
565 exec_backend: Arc<dyn ExecBackend>,
566 filesystem: Arc<dyn ExecutorFileSystem>,
567 http_client: Arc<dyn HttpClient>,
568 local_runtime_paths: Option<ExecServerRuntimePaths>,
569}
570
571impl Environment {
572 pub fn default_for_tests() -> Self {
574 Self {
575 remote_client: None,
576 ready_info: None,
577 startup_task: Arc::new(Mutex::new(None)),
578 exec_backend: Arc::new(LocalProcess::default()),
579 filesystem: Arc::new(LocalFileSystem::unsandboxed()),
580 http_client: Arc::new(ReqwestHttpClient),
581 local_runtime_paths: None,
582 }
583 }
584}
585
586impl std::fmt::Debug for Environment {
587 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
588 f.debug_struct("Environment").finish_non_exhaustive()
589 }
590}
591
592impl Environment {
593 pub fn create(
595 exec_server_url: Option<String>,
596 local_runtime_paths: ExecServerRuntimePaths,
597 ) -> Result<Self, ExecServerError> {
598 Self::create_inner(exec_server_url, Some(local_runtime_paths))
599 }
600
601 pub fn create_for_tests(exec_server_url: Option<String>) -> Result<Self, ExecServerError> {
603 Self::create_inner(exec_server_url, None)
604 }
605
606 fn create_inner(
609 exec_server_url: Option<String>,
610 local_runtime_paths: Option<ExecServerRuntimePaths>,
611 ) -> Result<Self, ExecServerError> {
612 let (exec_server_url, disabled) = normalize_exec_server_url(exec_server_url);
613 if disabled {
614 return Err(ExecServerError::Protocol(
615 "disabled mode does not create an Environment".to_string(),
616 ));
617 }
618
619 Ok(match exec_server_url {
620 Some(exec_server_url) => Self::remote_inner(exec_server_url, local_runtime_paths),
621 None => match local_runtime_paths {
622 Some(local_runtime_paths) => Self::local(local_runtime_paths),
623 None => Self::default_for_tests(),
624 },
625 })
626 }
627
628 pub(crate) fn local(local_runtime_paths: ExecServerRuntimePaths) -> Self {
629 Self {
630 remote_client: None,
631 ready_info: None,
632 startup_task: Arc::new(Mutex::new(None)),
633 exec_backend: Arc::new(LocalProcess::with_local_runtime_paths(
634 local_runtime_paths.clone(),
635 )),
636 filesystem: Arc::new(LocalFileSystem::with_runtime_paths(
637 local_runtime_paths.clone(),
638 )),
639 http_client: Arc::new(ReqwestHttpClient),
640 local_runtime_paths: Some(local_runtime_paths),
641 }
642 }
643
644 pub(crate) fn remote_inner(
645 exec_server_url: String,
646 local_runtime_paths: Option<ExecServerRuntimePaths>,
647 ) -> Self {
648 Self::remote_with_transport(
649 ExecServerTransportParams::websocket_url(
650 exec_server_url,
651 DEFAULT_REMOTE_EXEC_SERVER_CONNECT_TIMEOUT,
652 ),
653 local_runtime_paths,
654 )
655 }
656
657 pub(crate) fn remote_with_transport(
658 remote_transport: ExecServerTransportParams,
659 local_runtime_paths: Option<ExecServerRuntimePaths>,
660 ) -> Self {
661 let client = LazyRemoteExecServerClient::new(remote_transport);
662 let exec_backend: Arc<dyn ExecBackend> = Arc::new(RemoteProcess::new(client.clone()));
663 let filesystem: Arc<dyn ExecutorFileSystem> =
664 Arc::new(RemoteFileSystem::new(client.clone()));
665
666 Self {
667 remote_client: Some(client.clone()),
668 ready_info: None,
669 startup_task: Arc::new(Mutex::new(None)),
670 exec_backend,
671 filesystem,
672 http_client: Arc::new(client),
673 local_runtime_paths,
674 }
675 }
676
677 pub fn is_remote(&self) -> bool {
678 self.remote_client.is_some()
679 }
680
681 pub fn selected_capability_roots(&self) -> &[SelectedCapabilityRoot] {
683 self.ready_info
684 .as_ref()
685 .and_then(|ready_info| ready_info.get())
686 .map_or(&[], |ready_info| {
687 ready_info.selected_capability_roots.as_slice()
688 })
689 }
690
691 pub fn subscribe_connection_state(
693 &self,
694 ) -> Option<watch::Receiver<EnvironmentConnectionState>> {
695 self.remote_client
696 .as_ref()
697 .map(LazyRemoteExecServerClient::subscribe_connection_state)
698 }
699
700 pub fn local_runtime_paths(&self) -> Option<&ExecServerRuntimePaths> {
701 self.local_runtime_paths.as_ref()
702 }
703
704 pub async fn info(&self) -> Result<EnvironmentInfo, ExecServerError> {
706 match &self.remote_client {
707 Some(client) => client.environment_info().await,
708 None => Ok(EnvironmentInfo::local()),
709 }
710 }
711
712 pub async fn discover_capability_roots(
714 &self,
715 params: CapabilityRootsDiscoverParams,
716 ) -> Result<CapabilityRootsDiscoverResponse, ExecServerError> {
717 match &self.remote_client {
718 Some(client) => client.get().await?.discover_capability_roots(params).await,
719 None => crate::discover_capability_roots(self.filesystem.as_ref(), params)
720 .await
721 .map_err(|error| ExecServerError::Protocol(error.to_string())),
722 }
723 }
724
725 pub fn start_connecting(&self) {
728 let Some(client) = &self.remote_client else {
729 return;
730 };
731 let mut startup_task = self
732 .startup_task
733 .lock()
734 .unwrap_or_else(std::sync::PoisonError::into_inner);
735 if startup_task.is_none() {
736 *startup_task = client.start_connecting();
737 }
738 }
739
740 pub(crate) fn start_connecting_for_use(environment: &Arc<Self>) {
742 if environment.remote_client.is_none() {
743 return;
744 }
745 let mut startup_task = environment
746 .startup_task
747 .lock()
748 .unwrap_or_else(std::sync::PoisonError::into_inner);
749 if startup_task.is_none() {
750 let environment = Arc::clone(environment);
751 *startup_task = Some(AbortOnDropHandle::new(tokio::spawn(async move {
752 if let Err(error) = environment.wait_until_ready().await {
753 tracing::debug!(%error, "exec-server environment startup failed");
754 }
755 })));
756 }
757 }
758
759 pub fn startup_finished(&self) -> bool {
761 self.remote_client
762 .as_ref()
763 .is_none_or(LazyRemoteExecServerClient::startup_finished)
764 }
765
766 pub async fn wait_until_ready(&self) -> Result<(), ExecServerError> {
768 match &self.remote_client {
769 Some(client) => client.wait_until_ready().await,
770 None => Ok(()),
771 }
772 }
773
774 pub(crate) fn readiness_result(&self) -> Option<Result<(), ExecServerError>> {
776 match &self.remote_client {
777 Some(client) => client.readiness_result(),
778 None => Some(Ok(())),
779 }
780 }
781
782 pub async fn status(&self) -> EnvironmentObservedStatus {
789 match &self.remote_client {
790 Some(client) => client.status().await,
791 None => EnvironmentObservedStatus::Ready,
792 }
793 }
794
795 pub fn get_exec_backend(&self) -> Arc<dyn ExecBackend> {
796 Arc::clone(&self.exec_backend)
797 }
798
799 pub fn get_http_client(&self) -> Arc<dyn HttpClient> {
800 Arc::clone(&self.http_client)
801 }
802
803 pub fn get_filesystem(&self) -> Arc<dyn ExecutorFileSystem> {
804 Arc::clone(&self.filesystem)
805 }
806
807 pub fn get_filesystem_without_reconnect(&self) -> Arc<dyn ExecutorFileSystem> {
809 match &self.remote_client {
810 Some(client) => Arc::new(RemoteFileSystem::new(client.fail_fast())),
811 None => Arc::clone(&self.filesystem),
812 }
813 }
814}
815
816#[cfg(test)]
817mod tests {
818 use std::collections::HashMap;
819 use std::sync::Arc;
820 use std::time::Duration;
821
822 use super::Environment;
823 use super::EnvironmentManager;
824 use super::EnvironmentObservedStatus;
825 use super::LOCAL_ENVIRONMENT_ID;
826 use super::REMOTE_ENVIRONMENT_ID;
827 use super::noise_environment_config_from_values;
828 use crate::ExecServerRuntimePaths;
829 use crate::ProcessId;
830 use crate::client_api::ExecServerTransportParams;
831 use crate::client_api::StdioExecServerCommand;
832 use crate::environment_provider::EnvironmentDefault;
833 use crate::environment_provider::EnvironmentProviderSnapshot;
834 use codex_utils_path_uri::PathUri;
835 use pretty_assertions::assert_eq;
836 use tokio::net::TcpListener;
837 use tokio::time::timeout;
838
839 fn test_runtime_paths() -> ExecServerRuntimePaths {
840 ExecServerRuntimePaths::new(
841 std::env::current_exe().expect("current exe"),
842 None,
843 )
844 .expect("runtime paths")
845 }
846
847 fn assert_local_environment_unavailable(manager: &EnvironmentManager) {
848 assert!(manager.try_local_environment().is_none());
849 }
850
851 #[test]
852 fn local_environment_info_includes_current_directory() {
853 let info = super::EnvironmentInfo::local();
854
855 assert_eq!(
856 info.cwd,
857 Some(
858 PathUri::from_host_native_path(std::env::current_dir().expect("current directory"))
859 .expect("cwd URI")
860 )
861 );
862 }
863
864 #[tokio::test]
865 async fn noise_environment_config_selects_remote_as_default() {
866 let config = noise_environment_config_from_values(
867 Some("http://registry.example/api".to_string()),
868 Some("environment-requested".to_string()),
869 Some("registry-token".to_string()),
870 Some("workspace-123".to_string()),
871 )
872 .expect("parse noise environment configuration")
873 .expect("noise environment configuration");
874
875 let manager = EnvironmentManager::from_noise_environment_config(
876 config, None,
877 )
878 .expect("build environment manager");
879
880 assert_eq!(
881 manager.default_environment_id(),
882 Some(REMOTE_ENVIRONMENT_ID)
883 );
884 assert!(
885 manager
886 .default_environment()
887 .expect("remote environment")
888 .is_remote()
889 );
890 assert_local_environment_unavailable(&manager);
891 }
892
893 #[tokio::test]
894 async fn create_local_environment_does_not_connect() {
895 let environment = Environment::create(None, test_runtime_paths())
896 .expect("create environment");
897
898 assert!(!environment.is_remote());
899 assert!(environment.info().await.is_ok());
900 }
901
902 #[tokio::test]
903 async fn environment_manager_normalizes_empty_url() {
904 let manager =
905 EnvironmentManager::create_for_tests(Some(String::new()), Some(test_runtime_paths()))
906 .await;
907
908 let environment = manager.default_environment().expect("default environment");
909 assert_eq!(manager.default_environment_id(), Some(LOCAL_ENVIRONMENT_ID));
910 assert!(Arc::ptr_eq(
911 &environment,
912 &manager
913 .get_environment(LOCAL_ENVIRONMENT_ID)
914 .expect("local environment")
915 ));
916 assert!(Arc::ptr_eq(
917 &environment,
918 &manager.try_local_environment().expect("local environment")
919 ));
920 assert!(manager.try_local_environment().is_some());
921 assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none());
922 assert!(!environment.is_remote());
923 }
924
925 #[tokio::test]
926 async fn disabled_environment_manager_has_no_default_or_local_environment() {
927 let manager = EnvironmentManager::without_environments();
928
929 assert!(manager.default_environment().is_none());
930 assert_eq!(manager.default_environment_id(), None);
931 assert_local_environment_unavailable(&manager);
932 assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
933 assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none());
934 }
935
936 #[tokio::test]
937 async fn environment_manager_creates_remote_environment_for_url() {
938 let manager = EnvironmentManager::create_for_tests(
939 Some("ws://127.0.0.1:8765".to_string()),
940 Some(test_runtime_paths()),
941 )
942 .await;
943
944 let environment = manager.default_environment().expect("default environment");
945 assert_eq!(
946 manager.default_environment_id(),
947 Some(REMOTE_ENVIRONMENT_ID)
948 );
949 assert!(environment.is_remote());
950 assert!(Arc::ptr_eq(
951 &environment,
952 &manager
953 .get_environment(REMOTE_ENVIRONMENT_ID)
954 .expect("remote environment")
955 ));
956 assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
957 assert_local_environment_unavailable(&manager);
958 }
959
960 #[tokio::test]
961 async fn environment_manager_default_environment_caches_environment() {
962 let manager = EnvironmentManager::default_for_tests();
963
964 let first = manager.default_environment().expect("default environment");
965 let second = manager.default_environment().expect("default environment");
966
967 assert!(Arc::ptr_eq(&first, &second));
968 assert!(Arc::ptr_eq(
969 &first.get_filesystem(),
970 &second.get_filesystem()
971 ));
972 }
973
974 #[tokio::test]
975 async fn environment_manager_builds_from_snapshot() {
976 let snapshot = EnvironmentProviderSnapshot {
977 environments: vec![(
978 REMOTE_ENVIRONMENT_ID.to_string(),
979 Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
980 .expect("remote environment"),
981 )],
982 default: EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string()),
983 include_local: false,
984 };
985 let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
986 .expect("environment manager");
987
988 assert_eq!(
989 manager.default_environment_id(),
990 Some(REMOTE_ENVIRONMENT_ID)
991 );
992 assert!(
993 manager
994 .get_environment(REMOTE_ENVIRONMENT_ID)
995 .expect("remote environment")
996 .is_remote()
997 );
998 assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
999 assert_local_environment_unavailable(&manager);
1000 }
1001
1002 #[tokio::test]
1003 async fn environment_manager_rejects_empty_environment_id() {
1004 let snapshot = EnvironmentProviderSnapshot {
1005 environments: vec![("".to_string(), Environment::default_for_tests())],
1006 default: EnvironmentDefault::Disabled,
1007 include_local: false,
1008 };
1009 let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
1010 .expect_err("empty id should fail");
1011
1012 assert_eq!(
1013 err.to_string(),
1014 "exec-server protocol error: environment id cannot be empty"
1015 );
1016 }
1017
1018 #[tokio::test]
1019 async fn environment_manager_rejects_provider_supplied_local_environment() {
1020 let snapshot = EnvironmentProviderSnapshot {
1021 environments: vec![(
1022 LOCAL_ENVIRONMENT_ID.to_string(),
1023 Environment::default_for_tests(),
1024 )],
1025 default: EnvironmentDefault::Disabled,
1026 include_local: false,
1027 };
1028 let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
1029 .expect_err("local id should fail");
1030
1031 assert_eq!(
1032 err.to_string(),
1033 "exec-server protocol error: environment id `local` is reserved for EnvironmentManager"
1034 );
1035 }
1036
1037 #[tokio::test]
1038 async fn environment_manager_uses_explicit_provider_default() {
1039 let snapshot = EnvironmentProviderSnapshot {
1040 environments: vec![(
1041 "devbox".to_string(),
1042 Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
1043 .expect("remote environment"),
1044 )],
1045 default: EnvironmentDefault::EnvironmentId("devbox".to_string()),
1046 include_local: true,
1047 };
1048 let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
1049 .expect("manager");
1050
1051 assert_eq!(manager.default_environment_id(), Some("devbox"));
1052 assert_eq!(
1053 manager.default_environment_ids(),
1054 vec!["devbox".to_string(), LOCAL_ENVIRONMENT_ID.to_string()]
1055 );
1056 assert!(manager.default_environment().expect("default").is_remote());
1057 }
1058
1059 #[tokio::test]
1060 async fn environment_manager_disables_provider_default() {
1061 let snapshot = EnvironmentProviderSnapshot {
1062 environments: vec![(
1063 "devbox".to_string(),
1064 Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
1065 .expect("remote environment"),
1066 )],
1067 default: EnvironmentDefault::Disabled,
1068 include_local: true,
1069 };
1070 let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
1071 .expect("manager");
1072
1073 assert_eq!(manager.default_environment_id(), None);
1074 assert!(manager.default_environment().is_none());
1075 assert!(Arc::ptr_eq(
1076 &manager
1077 .get_environment(LOCAL_ENVIRONMENT_ID)
1078 .expect("local environment"),
1079 &manager.try_local_environment().expect("local environment")
1080 ));
1081 }
1082
1083 #[tokio::test]
1084 async fn environment_manager_rejects_unknown_provider_default() {
1085 let snapshot = EnvironmentProviderSnapshot {
1086 environments: vec![(
1087 "devbox".to_string(),
1088 Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
1089 .expect("remote environment"),
1090 )],
1091 default: EnvironmentDefault::EnvironmentId("missing".to_string()),
1092 include_local: true,
1093 };
1094 let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
1095 .expect_err("unknown default should fail");
1096
1097 assert_eq!(
1098 err.to_string(),
1099 "exec-server protocol error: default environment `missing` is not configured"
1100 );
1101 }
1102
1103 #[tokio::test]
1104 async fn environment_manager_includes_local_for_default_provider_without_url() {
1105 let manager = EnvironmentManager::create_for_tests(
1106 None,
1107 Some(test_runtime_paths()),
1108 )
1109 .await;
1110
1111 let environment = manager.default_environment().expect("default environment");
1112 assert_eq!(manager.default_environment_id(), Some(LOCAL_ENVIRONMENT_ID));
1113 assert!(Arc::ptr_eq(
1114 &environment,
1115 &manager
1116 .get_environment(LOCAL_ENVIRONMENT_ID)
1117 .expect("local environment")
1118 ));
1119 assert!(Arc::ptr_eq(
1120 &environment,
1121 &manager.try_local_environment().expect("local environment")
1122 ));
1123 assert!(!environment.is_remote());
1124 }
1125
1126 #[tokio::test]
1127 async fn environment_manager_carries_local_runtime_paths() {
1128 let runtime_paths = test_runtime_paths();
1129 let manager = EnvironmentManager::create_for_tests(
1130 None,
1131 Some(runtime_paths.clone()),
1132 )
1133 .await;
1134
1135 let environment = manager.try_local_environment().expect("local environment");
1136
1137 assert_eq!(environment.local_runtime_paths(), Some(&runtime_paths));
1138 let manager = EnvironmentManager::create_for_tests(
1139 None,
1140 Some(
1141 environment
1142 .local_runtime_paths()
1143 .expect("local runtime paths")
1144 .clone(),
1145 ),
1146 )
1147 .await;
1148 let environment = manager.try_local_environment().expect("local environment");
1149 assert_eq!(environment.local_runtime_paths(), Some(&runtime_paths));
1150 }
1151
1152 #[tokio::test]
1153 async fn environment_manager_omits_default_provider_local_lookup_when_default_disabled() {
1154 let manager = EnvironmentManager::create_for_tests(
1155 Some("none".to_string()),
1156 Some(test_runtime_paths()),
1157 )
1158 .await;
1159
1160 assert!(manager.default_environment().is_none());
1161 assert_eq!(manager.default_environment_id(), None);
1162 assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
1163 assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none());
1164 assert_local_environment_unavailable(&manager);
1165 }
1166
1167 #[tokio::test]
1168 async fn environment_manager_snapshot_without_local_environment_disables_local_default() {
1169 let mut snapshot = EnvironmentProviderSnapshot {
1170 environments: Vec::new(),
1171 default: EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string()),
1172 include_local: true,
1173 };
1174 snapshot.include_local = false;
1175 snapshot.default = EnvironmentDefault::Disabled;
1176 let manager =
1177 EnvironmentManager::from_snapshot(snapshot, None)
1178 .expect("environment manager");
1179
1180 assert!(manager.default_environment().is_none());
1181 assert_eq!(manager.default_environment_id(), None);
1182 assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
1183 assert_local_environment_unavailable(&manager);
1184 }
1185
1186 #[tokio::test]
1187 async fn get_environment_returns_none_for_unknown_id() {
1188 let manager = EnvironmentManager::default_for_tests();
1189
1190 assert!(manager.get_environment("does-not-exist").is_none());
1191 }
1192
1193 #[tokio::test]
1194 async fn environment_manager_upserts_named_remote_environment() {
1195 let manager = EnvironmentManager::without_environments();
1196
1197 manager
1198 .upsert_environment(
1199 "executor-a".to_string(),
1200 "ws://127.0.0.1:8765".to_string(),
1201 None,
1202 )
1203 .expect("remote environment");
1204 let first = manager
1205 .get_environment("executor-a")
1206 .expect("first remote environment");
1207 assert!(first.is_remote());
1208 assert_eq!(manager.default_environment_id(), None);
1209
1210 manager
1211 .upsert_environment(
1212 "executor-a".to_string(),
1213 "ws://127.0.0.1:9876".to_string(),
1214 None,
1215 )
1216 .expect("updated remote environment");
1217 let second = manager
1218 .get_environment("executor-a")
1219 .expect("second remote environment");
1220 assert!(second.is_remote());
1221 assert!(!Arc::ptr_eq(&first, &second));
1222 }
1223
1224 #[tokio::test]
1225 async fn environment_manager_starts_remote_environment_when_upserted() {
1226 let listener = TcpListener::bind("127.0.0.1:0")
1227 .await
1228 .expect("bind websocket listener");
1229 let manager = EnvironmentManager::without_environments();
1230
1231 manager
1232 .upsert_environment(
1233 "executor-a".to_string(),
1234 format!("ws://{}", listener.local_addr().expect("listener address")),
1235 None,
1236 )
1237 .expect("remote environment");
1238
1239 timeout(Duration::from_secs(5), listener.accept())
1240 .await
1241 .expect("environment should start connecting when registered")
1242 .expect("accept connection");
1243 }
1244
1245 #[tokio::test]
1246 async fn environment_status_keeps_stdio_environment_pending() {
1247 let environment = Environment::remote_with_transport(
1248 ExecServerTransportParams::StdioCommand {
1249 command: StdioExecServerCommand {
1250 program: "codex-missing-exec-server-for-test".to_string(),
1251 args: Vec::new(),
1252 env: HashMap::new(),
1253 cwd: None,
1254 },
1255 initialize_timeout: Duration::from_secs(1),
1256 },
1257 None,
1258 );
1259
1260 assert_eq!(
1261 environment.status().await,
1262 EnvironmentObservedStatus::Pending
1263 );
1264 assert!(!environment.startup_finished());
1265 }
1266
1267 #[tokio::test]
1268 async fn environment_manager_leaves_stdio_environment_lazy() {
1269 let environment = Environment::remote_with_transport(
1270 ExecServerTransportParams::StdioCommand {
1271 command: StdioExecServerCommand {
1272 program: "codex-missing-exec-server-for-test".to_string(),
1273 args: Vec::new(),
1274 env: HashMap::new(),
1275 cwd: None,
1276 },
1277 initialize_timeout: Duration::from_secs(1),
1278 },
1279 None,
1280 );
1281 let manager = EnvironmentManager::from_snapshot(
1282 EnvironmentProviderSnapshot {
1283 environments: vec![("stdio".to_string(), environment)],
1284 default: EnvironmentDefault::Disabled,
1285 include_local: false,
1286 },
1287 None,
1288 )
1289 .expect("environment manager");
1290 let environment = manager.get_environment("stdio").expect("stdio environment");
1291
1292 assert!(!environment.startup_finished());
1293 assert!(environment.wait_until_ready().await.is_err());
1294 assert!(environment.startup_finished());
1295 }
1296
1297 #[tokio::test]
1298 async fn selected_capability_inspection_keeps_stdio_environment_lazy() {
1299 use codex_protocol::capabilities::CapabilityRootLocation;
1300 use codex_protocol::capabilities::SelectedCapabilityRoot;
1301
1302 let environment = Environment::remote_with_transport(
1303 ExecServerTransportParams::StdioCommand {
1304 command: StdioExecServerCommand {
1305 program: "codex-missing-exec-server-for-test".to_string(),
1306 args: Vec::new(),
1307 env: HashMap::new(),
1308 cwd: None,
1309 },
1310 initialize_timeout: Duration::from_secs(1),
1311 },
1312 None,
1313 );
1314 let manager = EnvironmentManager::from_snapshot(
1315 EnvironmentProviderSnapshot {
1316 environments: vec![("stdio".to_string(), environment)],
1317 default: EnvironmentDefault::Disabled,
1318 include_local: false,
1319 },
1320 None,
1321 )
1322 .expect("environment manager");
1323 let environment = manager.get_environment("stdio").expect("stdio environment");
1324 let selected_root = SelectedCapabilityRoot {
1325 id: "demo@1".to_string(),
1326 location: CapabilityRootLocation::Environment {
1327 environment_id: "stdio".to_string(),
1328 path: PathUri::parse("file:///plugins/demo").expect("plugin path URI"),
1329 },
1330 };
1331
1332 let status =
1333 manager.inspect_selected_capability_roots(std::slice::from_ref(&selected_root));
1334 assert!(status.ready_roots.is_empty());
1335 assert_eq!(status.warnings, Vec::<String>::new());
1336 assert!(!environment.startup_finished());
1337
1338 let missing_root = SelectedCapabilityRoot {
1339 id: "missing@1".to_string(),
1340 location: CapabilityRootLocation::Environment {
1341 environment_id: "missing".to_string(),
1342 path: PathUri::parse("file:///plugins/missing").expect("missing plugin path URI"),
1343 },
1344 };
1345 let status = manager.inspect_selected_capability_roots(&[missing_root]);
1346 assert!(status.ready_roots.is_empty());
1347 assert_eq!(
1348 status.warnings,
1349 vec![
1350 "selected capability root `missing@1` references unavailable environment `missing`"
1351 .to_string()
1352 ]
1353 );
1354
1355 assert!(environment.wait_until_ready().await.is_err());
1356
1357 let status = manager.inspect_selected_capability_roots(&[selected_root]);
1358 assert!(status.ready_roots.is_empty());
1359 assert_eq!(status.warnings.len(), 1);
1360 assert!(status.warnings[0].contains("environment `stdio` is unavailable"));
1361 }
1362
1363 #[tokio::test]
1364 async fn replacing_environment_stops_its_startup_task() {
1365 let first_listener = TcpListener::bind("127.0.0.1:0")
1366 .await
1367 .expect("bind first websocket listener");
1368 let second_listener = TcpListener::bind("127.0.0.1:0")
1369 .await
1370 .expect("bind second websocket listener");
1371 let manager = EnvironmentManager::without_environments();
1372 manager
1373 .upsert_environment(
1374 "executor-a".to_string(),
1375 format!(
1376 "ws://{}",
1377 first_listener.local_addr().expect("first listener address")
1378 ),
1379 None,
1380 )
1381 .expect("first remote environment");
1382 let environment = manager
1383 .get_environment("executor-a")
1384 .expect("first remote environment");
1385 let startup_abort = environment
1386 .startup_task
1387 .lock()
1388 .unwrap_or_else(std::sync::PoisonError::into_inner)
1389 .as_ref()
1390 .expect("startup task")
1391 .abort_handle();
1392 assert!(!startup_abort.is_finished());
1393 drop(environment);
1394
1395 manager
1396 .upsert_environment(
1397 "executor-a".to_string(),
1398 format!(
1399 "ws://{}",
1400 second_listener
1401 .local_addr()
1402 .expect("second listener address")
1403 ),
1404 None,
1405 )
1406 .expect("replacement remote environment");
1407
1408 timeout(Duration::from_secs(1), async {
1409 while !startup_abort.is_finished() {
1410 tokio::task::yield_now().await;
1411 }
1412 })
1413 .await
1414 .expect("replacing the environment should cancel its startup task");
1415 }
1416
1417 #[tokio::test]
1418 async fn environment_manager_rejects_empty_remote_environment_url() {
1419 let manager = EnvironmentManager::without_environments();
1420
1421 let err = manager
1422 .upsert_environment(
1423 "executor-a".to_string(),
1424 String::new(),
1425 None,
1426 )
1427 .expect_err("empty URL should fail");
1428
1429 assert_eq!(
1430 err.to_string(),
1431 "exec-server protocol error: remote environment requires an exec-server url"
1432 );
1433 }
1434
1435 #[tokio::test]
1436 async fn default_environment_has_ready_local_executor() {
1437 let environment = Environment::default_for_tests();
1438
1439 let response = environment
1440 .get_exec_backend()
1441 .start(crate::ExecParams {
1442 process_id: ProcessId::from("default-env-proc"),
1443 argv: vec!["true".to_string()],
1444 cwd: PathUri::from_host_native_path(
1445 std::env::current_dir().expect("read current dir"),
1446 )
1447 .expect("cwd URI"),
1448 env_policy: None,
1449 env: Default::default(),
1450 tty: false,
1451 pipe_stdin: false,
1452 arg0: None,
1453 sandbox: None,
1454 enforce_managed_network: false,
1455 managed_network: None,
1456 network_proxy: None,
1457 })
1458 .await
1459 .expect("start process");
1460
1461 assert_eq!(response.process.process_id().as_str(), "default-env-proc");
1462 }
1463
1464 #[tokio::test]
1465 async fn local_environment_passes_runtime_paths_to_exec_backend() {
1466 let environment = Environment::local(test_runtime_paths());
1467 #[cfg(unix)]
1468 let uri = "file://server/share/checkout";
1469 #[cfg(windows)]
1470 let uri = "file:///usr/local/checkout";
1471 let sandbox_cwd = PathUri::parse(uri).expect("non-native sandbox cwd URI");
1472 let source = sandbox_cwd
1473 .to_abs_path()
1474 .expect_err("sandbox cwd should not be native to this host");
1475 let sandbox = crate::FileSystemSandboxContext::from_permission_profile_with_cwd(
1476 codex_protocol::models::PermissionProfile::workspace_write(),
1477 sandbox_cwd.clone(),
1478 );
1479
1480 let result = environment
1481 .get_exec_backend()
1482 .start(crate::ExecParams {
1483 process_id: ProcessId::from("local-sandbox-proc"),
1484 argv: vec!["true".to_string()],
1485 cwd: PathUri::from_host_native_path(
1486 std::env::current_dir().expect("read current dir"),
1487 )
1488 .expect("cwd URI"),
1489 env_policy: None,
1490 env: Default::default(),
1491 tty: false,
1492 pipe_stdin: false,
1493 arg0: None,
1494 sandbox: Some(sandbox),
1495 enforce_managed_network: false,
1496 managed_network: None,
1497 network_proxy: None,
1498 })
1499 .await;
1500 let Err(err) = result else {
1501 panic!("sandbox cwd should be rejected after resolving runtime paths");
1502 };
1503
1504 assert_eq!(
1505 err.to_string(),
1506 format!(
1507 "exec-server rejected request (-32602): sandbox cwd URI `{sandbox_cwd}` is not valid on this exec-server host: {source}"
1508 )
1509 );
1510 }
1511
1512 #[tokio::test]
1513 async fn test_environment_rejects_sandboxed_filesystem_without_runtime_paths() {
1514 let environment = Environment::default_for_tests();
1515 let path = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(
1516 std::env::current_exe().expect("current exe").as_path(),
1517 )
1518 .expect("absolute current exe");
1519 let path = codex_utils_path_uri::PathUri::from_abs_path(&path);
1520 let sandbox = crate::FileSystemSandboxContext::from_permission_profile(
1521 codex_protocol::models::PermissionProfile::from_runtime_permissions(
1522 &codex_protocol::permissions::FileSystemSandboxPolicy::restricted(Vec::new()),
1523 codex_protocol::permissions::NetworkSandboxPolicy::Restricted,
1524 ),
1525 );
1526
1527 let err = environment
1528 .get_filesystem()
1529 .read_file(&path, Some(&sandbox))
1530 .await
1531 .expect_err("sandboxed read should require runtime paths");
1532
1533 assert_eq!(
1534 err.to_string(),
1535 "sandboxed filesystem operations require configured runtime paths"
1536 );
1537 }
1538}