use std::collections::HashSet; use std::sync::Arc; use codex_exec_server::EnvironmentManager; use codex_exec_server::ExecutorFileSystem; use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; use codex_protocol::protocol::TurnEnvironmentSelection; use codex_utils_absolute_path::AbsolutePathBuf; use crate::session::turn_context::TurnEnvironment; pub(crate) fn default_thread_environment_selections( environment_manager: &EnvironmentManager, cwd: &AbsolutePathBuf, ) -> Vec { environment_manager .default_environment_ids() .into_iter() .map(|environment_id| TurnEnvironmentSelection { environment_id, cwd: cwd.clone(), }) .collect() } #[derive(Clone, Debug, Default)] pub(crate) struct ResolvedTurnEnvironments { pub(crate) turn_environments: Vec, } impl ResolvedTurnEnvironments { pub(crate) fn to_selections(&self) -> Vec { self.turn_environments .iter() .map(TurnEnvironment::selection) .collect() } pub(crate) fn primary(&self) -> Option<&TurnEnvironment> { self.turn_environments.first() } pub(crate) fn primary_environment(&self) -> Option> { self.primary() .map(|environment| Arc::clone(&environment.environment)) } pub(crate) fn primary_filesystem(&self) -> Option> { self.primary() .map(|environment| environment.environment.get_filesystem()) } } pub(crate) fn resolve_environment_selections( environment_manager: &EnvironmentManager, environments: &[TurnEnvironmentSelection], ) -> CodexResult { let mut seen_environment_ids = HashSet::with_capacity(environments.len()); let mut turn_environments = Vec::with_capacity(environments.len()); for selected_environment in environments { if !seen_environment_ids.insert(selected_environment.environment_id.as_str()) { return Err(CodexErr::InvalidRequest(format!( "duplicate turn environment id `{}`", selected_environment.environment_id ))); } let environment_id = selected_environment.environment_id.clone(); let environment = environment_manager .get_environment(&environment_id) .ok_or_else(|| { CodexErr::InvalidRequest(format!("unknown turn environment id `{environment_id}`")) })?; turn_environments.push(TurnEnvironment { environment_id, environment, cwd: selected_environment.cwd.clone(), shell: None, }); } Ok(ResolvedTurnEnvironments { turn_environments }) } #[cfg(test)] mod tests { use codex_exec_server::ExecServerRuntimePaths; use codex_exec_server::LOCAL_ENVIRONMENT_ID; use codex_exec_server::REMOTE_ENVIRONMENT_ID; use codex_protocol::protocol::TurnEnvironmentSelection; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use super::*; 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 default_thread_environment_selections_use_manager_default_id() { let cwd = AbsolutePathBuf::current_dir().expect("cwd"); let manager = EnvironmentManager::create_for_tests( Some("ws://127.0.0.1:8765".to_string()), Some(test_runtime_paths()), ) .await; assert_eq!( default_thread_environment_selections(&manager, &cwd), vec![TurnEnvironmentSelection { environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd, }] ); } #[tokio::test] async fn toml_default_thread_environment_selections_include_local_and_remote() { let temp_dir = tempfile::tempdir().expect("tempdir"); std::fs::write( temp_dir.path().join("environments.toml"), r#" [[environments]] id = "remote" url = "ws://127.0.0.1:8765" "#, ) .expect("write environments.toml"); let cwd = AbsolutePathBuf::current_dir().expect("cwd"); let manager = EnvironmentManager::from_codex_home(temp_dir.path(), Some(test_runtime_paths())) .await .expect("environment manager"); assert_eq!( default_thread_environment_selections(&manager, &cwd), vec![ TurnEnvironmentSelection { environment_id: LOCAL_ENVIRONMENT_ID.to_string(), cwd: cwd.clone(), }, TurnEnvironmentSelection { environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd, }, ] ); } #[tokio::test] async fn default_thread_environment_selections_empty_when_default_disabled() { let cwd = AbsolutePathBuf::current_dir().expect("cwd"); let manager = EnvironmentManager::without_environments(); assert_eq!( default_thread_environment_selections(&manager, &cwd), Vec::::new() ); } #[tokio::test] async fn resolve_environment_selections_rejects_duplicate_ids() { let cwd = AbsolutePathBuf::current_dir().expect("cwd"); let manager = EnvironmentManager::default_for_tests(); let err = resolve_environment_selections( &manager, &[ TurnEnvironmentSelection { environment_id: "local".to_string(), cwd: cwd.clone(), }, TurnEnvironmentSelection { environment_id: "local".to_string(), cwd: cwd.join("other"), }, ], ) .expect_err("duplicate environment id should fail"); assert!(err.to_string().contains("duplicate")); } #[tokio::test] async fn resolved_environment_selections_use_first_selection_as_primary() { let cwd = AbsolutePathBuf::current_dir().expect("cwd"); let selected_cwd = cwd.join("selected"); let manager = EnvironmentManager::default_for_tests(); let resolved = resolve_environment_selections( &manager, &[TurnEnvironmentSelection { environment_id: "local".to_string(), cwd: selected_cwd, }], ) .expect("environment selections should resolve"); assert_eq!( resolved .primary() .expect("primary environment") .environment_id, "local" ); assert_eq!(resolved.primary().expect("primary environment").shell, None); } }