From be71b6fcd1ae5650822609908f1b723f3c12a310 Mon Sep 17 00:00:00 2001 From: starr-openai Date: Fri, 1 May 2026 11:00:14 -0700 Subject: [PATCH] Use selected turn environments for runtime context (#20281) ## Summary - make selected turn environments the source of truth for session runtime cwd and MCP runtime environment selection - keep local/no-selection fallback behavior intact - add coverage for duplicate selected environments, cwd resolution, and MCP runtime environment selection ## Validation - git diff --check - rustfmt was run on touched Rust files during the implementation workflow CI should provide the full Bazel/test signal. --------- Co-authored-by: Codex --- .../app-server/src/codex_message_processor.rs | 73 +++--- codex-rs/core/src/codex_delegate.rs | 10 +- codex-rs/core/src/environment_selection.rs | 125 +++++++-- codex-rs/core/src/session/mcp.rs | 21 +- codex-rs/core/src/session/mod.rs | 20 +- codex-rs/core/src/session/review.rs | 1 - codex-rs/core/src/session/session.rs | 41 ++- codex-rs/core/src/session/tests.rs | 245 +++++++++++++----- .../core/src/session/tests/guardian_tests.rs | 5 +- codex-rs/core/src/session/turn_context.rs | 83 +++--- codex-rs/core/src/thread_manager.rs | 18 +- .../core/src/tools/handlers/apply_patch.rs | 12 +- codex-rs/core/src/tools/handlers/shell.rs | 4 +- .../core/src/tools/handlers/unified_exec.rs | 4 +- .../core/src/tools/handlers/view_image.rs | 5 +- .../core/src/tools/runtimes/apply_patch.rs | 4 +- .../core/src/tools/runtimes/unified_exec.rs | 15 +- codex-rs/core/src/unified_exec/mod_tests.rs | 12 +- 18 files changed, 456 insertions(+), 242 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index c3ed076440..edc142c840 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -225,6 +225,7 @@ use codex_app_server_protocol::ThreadUnsubscribeParams; use codex_app_server_protocol::ThreadUnsubscribeResponse; use codex_app_server_protocol::ThreadUnsubscribeStatus; use codex_app_server_protocol::Turn; +use codex_app_server_protocol::TurnEnvironmentParams; use codex_app_server_protocol::TurnError; use codex_app_server_protocol::TurnInterruptParams; use codex_app_server_protocol::TurnInterruptResponse; @@ -2527,28 +2528,13 @@ impl CodexMessageProcessor { .await; return; } - let environments = environments.map(|environments| { - environments - .into_iter() - .map(|environment| TurnEnvironmentSelection { - environment_id: environment.environment_id, - cwd: environment.cwd, - }) - .collect::>() - }); - if let Some(environments) = environments.as_ref() - && let Err(err) = self - .thread_manager - .validate_environment_selections(environments) - { - self.outgoing - .send_error( - request_id, - invalid_request(environment_selection_error_message(err)), - ) - .await; - return; - } + let environment_selections = match self.parse_environment_selections(environments) { + Ok(environment_selections) => environment_selections, + Err(error) => { + self.outgoing.send_error(request_id, error).await; + return; + } + }; let mut typesafe_overrides = self.build_thread_config_overrides( model, model_provider, @@ -2587,7 +2573,7 @@ impl CodexMessageProcessor { typesafe_overrides, dynamic_tools, session_start_source, - environments, + environment_selections, persist_extended_history, service_name, experimental_raw_events, @@ -3012,6 +2998,27 @@ impl CodexMessageProcessor { overrides } + fn parse_environment_selections( + &self, + environments: Option>, + ) -> Result>, JSONRPCErrorError> { + let environment_selections = environments.map(|environments| { + environments + .into_iter() + .map(|environment| TurnEnvironmentSelection { + environment_id: environment.environment_id, + cwd: environment.cwd, + }) + .collect::>() + }); + if let Some(environment_selections) = environment_selections.as_ref() { + self.thread_manager + .validate_environment_selections(environment_selections) + .map_err(|err| invalid_request(environment_selection_error_message(err)))?; + } + Ok(environment_selections) + } + async fn thread_archive(&self, request_id: ConnectionRequestId, params: ThreadArchiveParams) { let _thread_list_state_permit = match self.acquire_thread_list_state_permit().await { Ok(permit) => permit, @@ -6686,21 +6693,7 @@ impl CodexMessageProcessor { let collaboration_mode = params .collaboration_mode .map(|mode| self.normalize_turn_start_collaboration_mode(mode)); - let environments: Option> = - params.environments.map(|environments| { - environments - .into_iter() - .map(|environment| TurnEnvironmentSelection { - environment_id: environment.environment_id, - cwd: environment.cwd, - }) - .collect() - }); - if let Some(environments) = environments.as_ref() { - self.thread_manager - .validate_environment_selections(environments) - .map_err(|err| invalid_request(environment_selection_error_message(err)))?; - } + let environment_selections = self.parse_environment_selections(params.environments)?; // Map v2 input items to core input items. let mapped_items: Vec = params @@ -6809,7 +6802,7 @@ impl CodexMessageProcessor { let turn_op = if has_any_overrides { Op::UserInputWithTurnContext { items: mapped_items, - environments, + environments: environment_selections, final_output_json_schema: params.output_schema, responsesapi_client_metadata: params.responsesapi_client_metadata, cwd, @@ -6829,7 +6822,7 @@ impl CodexMessageProcessor { } else { Op::UserInput { items: mapped_items, - environments, + environments: environment_selections, final_output_json_schema: params.output_schema, responsesapi_client_metadata: params.responsesapi_client_metadata, } diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 01907a5594..d142d33a2f 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -30,6 +30,7 @@ use tokio::time::timeout; use tokio_util::sync::CancellationToken; use crate::config::Config; +use crate::environment_selection::ResolvedTurnEnvironments; use crate::guardian::GuardianApprovalRequest; use crate::guardian::new_guardian_review_id; use crate::guardian::routes_approval_to_guardian; @@ -47,7 +48,6 @@ use crate::session::SUBMISSION_CHANNEL_CAPACITY; use crate::session::emit_subagent_session_started; use crate::session::session::Session; use crate::session::turn_context::TurnContext; -use crate::session::turn_context::TurnEnvironment; use codex_login::AuthManager; use codex_models_manager::manager::SharedModelsManager; use codex_protocol::error::CodexErr; @@ -94,11 +94,9 @@ pub(crate) async fn run_codex_thread_interactive( inherited_exec_policy: Some(Arc::clone(&parent_session.services.exec_policy)), parent_rollout_thread_trace: codex_rollout_trace::ThreadTraceContext::disabled(), parent_trace: None, - environments: parent_ctx - .environments - .iter() - .map(TurnEnvironment::selection) - .collect(), + environment_selections: ResolvedTurnEnvironments { + turn_environments: parent_ctx.environments.clone(), + }, analytics_events_client: Some(parent_session.services.analytics_events_client.clone()), thread_store: Arc::clone(&parent_session.services.thread_store), })) diff --git a/codex-rs/core/src/environment_selection.rs b/codex-rs/core/src/environment_selection.rs index a33aae92b0..06f2dcba01 100644 --- a/codex-rs/core/src/environment_selection.rs +++ b/codex-rs/core/src/environment_selection.rs @@ -1,12 +1,15 @@ +use std::collections::HashSet; use std::sync::Arc; -use codex_exec_server::Environment; 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, @@ -21,42 +24,61 @@ pub(crate) fn default_thread_environment_selections( .collect() } -pub(crate) fn validate_environment_selections( +#[derive(Clone, Debug)] +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_turn_environment(&self) -> Option<&TurnEnvironment> { + self.turn_environments.first() + } + + pub(crate) fn primary_environment(&self) -> Option> { + self.primary_turn_environment() + .map(|environment| Arc::clone(&environment.environment)) + } + + pub(crate) fn primary_filesystem(&self) -> Option> { + self.primary_turn_environment() + .map(|environment| environment.environment.get_filesystem()) + } +} + +pub(crate) fn resolve_environment_selections( environment_manager: &EnvironmentManager, environments: &[TurnEnvironmentSelection], -) -> CodexResult<()> { +) -> 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 environment_manager - .get_environment(&selected_environment.environment_id) - .is_none() - { + if !seen_environment_ids.insert(selected_environment.environment_id.as_str()) { return Err(CodexErr::InvalidRequest(format!( - "unknown turn environment id `{}`", + "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(), + }); } - Ok(()) -} - -pub(crate) fn selected_primary_environment( - environment_manager: &EnvironmentManager, - environments: &[TurnEnvironmentSelection], -) -> CodexResult>> { - environments - .first() - .map(|selected_environment| { - environment_manager - .get_environment(&selected_environment.environment_id) - .ok_or_else(|| { - CodexErr::InvalidRequest(format!( - "unknown turn environment id `{}`", - selected_environment.environment_id - )) - }) - }) - .transpose() + Ok(ResolvedTurnEnvironments { turn_environments }) } #[cfg(test)] @@ -105,4 +127,51 @@ mod tests { 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_turn_environment() + .expect("primary environment") + .environment_id, + "local" + ); + } } diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index 18cc19a727..2aa5adee28 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -221,6 +221,19 @@ impl Session { let mcp_servers = with_codex_apps_mcp(mcp_servers, auth.as_ref(), &mcp_config); let auth_statuses = compute_auth_statuses(mcp_servers.iter(), store_mode, auth.as_ref()).await; + let mcp_runtime_environment = match turn_context.primary_environment() { + Some(turn_environment) => McpRuntimeEnvironment::new( + Arc::clone(&turn_environment.environment), + turn_environment.cwd.to_path_buf(), + ), + None => McpRuntimeEnvironment::new( + self.services + .environment_manager + .default_environment() + .unwrap_or_else(|| self.services.environment_manager.local_environment()), + turn_context.cwd.to_path_buf(), + ), + }; { let mut guard = self.services.mcp_startup_cancellation_token.lock().await; guard.cancel(); @@ -234,13 +247,7 @@ impl Session { turn_context.sub_id.clone(), self.get_tx_event(), turn_context.permission_profile(), - McpRuntimeEnvironment::new( - turn_context - .environment - .clone() - .unwrap_or_else(|| self.services.environment_manager.local_environment()), - turn_context.cwd.to_path_buf(), - ), + mcp_runtime_environment, config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth.as_ref()), tool_plugin_provenance, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index c45a8b638a..860b1e0f30 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -30,8 +30,7 @@ use crate::context::NetworkRuleSaved; use crate::context::PermissionsInstructions; use crate::context::PersonalitySpecInstructions; use crate::default_skill_metadata_budget; -use crate::environment_selection::selected_primary_environment; -use crate::environment_selection::validate_environment_selections; +use crate::environment_selection::ResolvedTurnEnvironments; use crate::exec_policy::ExecPolicyManager; use crate::installation_id::resolve_installation_id; use crate::parse_turn_item; @@ -113,7 +112,6 @@ use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::TurnContextItem; use codex_protocol::protocol::TurnContextNetworkItem; -use codex_protocol::protocol::TurnEnvironmentSelection; use codex_protocol::protocol::W3cTraceContext; use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::RequestPermissionProfile; @@ -410,7 +408,7 @@ pub(crate) struct CodexSpawnArgs { pub(crate) parent_rollout_thread_trace: ThreadTraceContext, pub(crate) user_shell_override: Option, pub(crate) parent_trace: Option, - pub(crate) environments: Vec, + pub(crate) environment_selections: ResolvedTurnEnvironments, pub(crate) analytics_events_client: Option, pub(crate) thread_store: Arc, } @@ -467,18 +465,13 @@ impl Codex { inherited_exec_policy, parent_rollout_thread_trace, parent_trace: _, - environments, + environment_selections, analytics_events_client, thread_store, } = args; let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY); let (tx_event, rx_event) = async_channel::unbounded(); - validate_environment_selections(environment_manager.as_ref(), &environments)?; - let environment = - selected_primary_environment(environment_manager.as_ref(), &environments)?; - let fs = environment - .as_ref() - .map(|environment| environment.get_filesystem()); + let fs = environment_selections.primary_filesystem(); let plugins_input = config.plugins_config_input(); let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); @@ -501,8 +494,9 @@ impl Codex { let _ = config.features.disable(Feature::Collab); } + let primary_environment = environment_selections.primary_environment(); let user_instructions = AgentsMdManager::new(&config) - .user_instructions(environment.as_deref()) + .user_instructions(primary_environment.as_deref()) .await; let exec_policy = if crate::guardian::is_guardian_reviewer_source(&session_source) { @@ -614,7 +608,7 @@ impl Codex { cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), thread_name: None, - environments, + environments: environment_selections.to_selections(), original_config_do_not_use: Arc::clone(&config), metrics_service_name, app_server_client_name: None, diff --git a/codex-rs/core/src/session/review.rs b/codex-rs/core/src/session/review.rs index 73671d3061..b879c78fae 100644 --- a/codex-rs/core/src/session/review.rs +++ b/codex-rs/core/src/session/review.rs @@ -123,7 +123,6 @@ pub(super) async fn spawn_review_thread( reasoning_effort, reasoning_summary, session_source, - environment: parent_turn_context.environment.clone(), environments: parent_turn_context.environments.clone(), tools_config, features: parent_turn_context.features.clone(), diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index c5a0c98499..50b3345d61 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -3,6 +3,7 @@ use crate::goals::GoalRuntimeState; use codex_otel::LEGACY_NOTIFY_CONFIGURED_METRIC; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSpecialPath; +use codex_protocol::protocol::TurnEnvironmentSelection; use tokio::sync::Semaphore; /// Context for an initialized model agent @@ -207,12 +208,7 @@ impl SessionConfiguration { .unwrap_or_else(|| self.cwd.clone()); let cwd_changed = absolute_cwd.as_path() != self.cwd.as_path(); - next_configuration.cwd = absolute_cwd.clone(); - if cwd_changed - && let Some(primary_environment) = next_configuration.environments.first_mut() - { - primary_environment.cwd = absolute_cwd; - } + next_configuration.cwd = absolute_cwd; if let Some(permission_profile) = updates.permission_profile.clone() { let active_permission_profile = @@ -962,6 +958,31 @@ impl Session { cancel_guard.cancel(); *cancel_guard = CancellationToken::new(); } + let turn_environment = crate::environment_selection::resolve_environment_selections( + sess.services.environment_manager.as_ref(), + &session_configuration.environments, + ) + .map_err(|err| { + CodexErr::InvalidRequest(err.to_string().replace( + "unknown turn environment id", + "unknown stored MCP environment id", + )) + })? + .primary_turn_environment() + .cloned(); + let mcp_runtime_environment = match turn_environment { + Some(turn_environment) => McpRuntimeEnvironment::new( + Arc::clone(&turn_environment.environment), + turn_environment.cwd.to_path_buf(), + ), + None => McpRuntimeEnvironment::new( + sess.services + .environment_manager + .default_environment() + .unwrap_or_else(|| sess.services.environment_manager.local_environment()), + session_configuration.cwd.to_path_buf(), + ), + }; let (mcp_connection_manager, cancel_token) = McpConnectionManager::new( &mcp_servers, config.mcp_oauth_credentials_store_mode, @@ -970,13 +991,7 @@ impl Session { INITIAL_SUBMIT_ID.to_owned(), tx_event.clone(), session_configuration.permission_profile(), - McpRuntimeEnvironment::new( - sess.services - .environment_manager - .default_environment() - .unwrap_or_else(|| sess.services.environment_manager.local_environment()), - session_configuration.cwd.to_path_buf(), - ), + mcp_runtime_environment, config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth), tool_plugin_provenance, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index af729dc264..5f791ff887 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -47,6 +47,7 @@ use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::protocol::NonSteerableTurnKind; use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::protocol::TurnEnvironmentSelection; use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::RequestPermissionProfile; use tracing::Span; @@ -3289,7 +3290,7 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up } #[tokio::test] -async fn session_update_settings_keeps_runtime_cwds_absolute() { +async fn session_update_settings_does_not_rewrite_sticky_environment_cwds() { let (session, turn_context) = make_session_and_context().await; let updated_cwd = turn_context.cwd.join("project"); std::fs::create_dir_all(updated_cwd.as_path()).expect("create project dir"); @@ -3315,6 +3316,91 @@ async fn session_update_settings_keeps_runtime_cwds_absolute() { assert_eq!(next_turn.config.cwd, updated_cwd); } +#[tokio::test] +async fn relative_cwd_update_without_environments_resolves_under_session_cwd() { + let (session, _turn_context) = make_session_and_context().await; + let original_cwd = { + let mut state = session.state.lock().await; + state.session_configuration.environments = Vec::new(); + state.session_configuration.cwd.clone() + }; + let updated_cwd = original_cwd.join("project"); + std::fs::create_dir_all(updated_cwd.as_path()).expect("create project dir"); + + session + .update_settings(SessionSettingsUpdate { + cwd: Some(PathBuf::from("project")), + ..Default::default() + }) + .await + .expect("cwd update should succeed"); + + let state = session.state.lock().await; + assert_eq!(state.session_configuration.cwd, updated_cwd); + assert!(state.session_configuration.environments.is_empty()); +} + +#[tokio::test] +async fn cwd_update_does_not_rewrite_sticky_environment_cwd() { + let (session, _turn_context) = make_session_and_context().await; + let (original_cwd, environment_cwd) = { + let mut state = session.state.lock().await; + let original_cwd = state.session_configuration.cwd.clone(); + let environment_cwd = original_cwd.join("environment"); + state.session_configuration.environments = vec![TurnEnvironmentSelection { + environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(), + cwd: environment_cwd.clone(), + }]; + (original_cwd, environment_cwd) + }; + let updated_cwd = original_cwd.join("project"); + std::fs::create_dir_all(updated_cwd.as_path()).expect("create project dir"); + + session + .update_settings(SessionSettingsUpdate { + cwd: Some(PathBuf::from("project")), + ..Default::default() + }) + .await + .expect("cwd update should succeed"); + + let state = session.state.lock().await; + assert_eq!(state.session_configuration.cwd, updated_cwd); + assert_eq!( + state.session_configuration.environments[0].cwd, + environment_cwd + ); +} + +#[tokio::test] +async fn absolute_cwd_update_with_turn_environment_is_allowed() { + let (session, _turn_context, _rx) = make_session_and_context_with_rx().await; + let absolute_cwd = { + let state = session.state.lock().await; + state.session_configuration.cwd.join("absolute-turn") + }; + std::fs::create_dir_all(absolute_cwd.as_path()).expect("create absolute turn dir"); + + let turn_context = session + .new_turn_with_sub_id( + "sub-1".to_string(), + SessionSettingsUpdate { + cwd: Some(absolute_cwd.to_path_buf()), + environments: Some(vec![TurnEnvironmentSelection { + environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(), + cwd: absolute_cwd.clone(), + }]), + ..Default::default() + }, + ) + .await + .expect("absolute cwd with explicit environments should succeed"); + + assert_eq!(turn_context.cwd, absolute_cwd); + assert_eq!(turn_context.config.cwd, absolute_cwd); + assert_eq!(turn_context.environments.len(), 1); +} + #[tokio::test] async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { let codex_home = tempfile::tempdir().expect("create temp dir"); @@ -3594,7 +3680,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { model_info, &models_manager, /*network*/ None, - Some(environment), turn_environments, session_configuration.cwd.clone(), "turn_id".to_string(), @@ -4333,23 +4418,24 @@ async fn turn_environments_set_primary_environment() { let turn_environments = &turn_context.environments; assert_eq!(turn_environments.len(), 1); + let turn_environment = turn_context + .primary_environment() + .expect("primary environment should be set"); assert!(std::sync::Arc::ptr_eq( - turn_context - .environment - .as_ref() - .expect("primary environment should be set"), + &turn_environment.environment, &turn_environments[0].environment )); + assert!(!turn_context.environments.is_empty()); assert_eq!(turn_context.cwd.as_path(), selected_cwd.as_path()); assert_eq!(turn_context.config.cwd.as_path(), selected_cwd.as_path()); } #[tokio::test] -async fn default_turn_uses_stored_thread_environments() { +async fn default_turn_overlays_session_cwd_onto_stored_thread_environments() { let (session, _turn_context, _rx) = make_session_and_context_with_rx().await; + let session_cwd = session.get_config().await.cwd.clone(); let selected_cwd = - AbsolutePathBuf::try_from(session.get_config().await.cwd.as_path().join("selected")) - .expect("absolute path"); + AbsolutePathBuf::try_from(session_cwd.as_path().join("selected")).expect("absolute path"); { let mut state = session.state.lock().await; @@ -4363,15 +4449,15 @@ async fn default_turn_uses_stored_thread_environments() { let turn_environments = &turn_context.environments; assert_eq!(turn_environments.len(), 1); + let turn_environment = turn_context + .primary_environment() + .expect("primary environment should be set"); assert!(std::sync::Arc::ptr_eq( - turn_context - .environment - .as_ref() - .expect("primary environment should be set"), + &turn_environment.environment, &turn_environments[0].environment )); - assert_eq!(turn_context.cwd, selected_cwd); - assert_eq!(turn_context.config.cwd, selected_cwd); + assert_eq!(turn_context.cwd, session_cwd); + assert_eq!(turn_context.config.cwd, session_cwd); } #[tokio::test] @@ -4386,54 +4472,42 @@ async fn default_turn_honors_empty_stored_thread_environments() { let turn_context = session.new_default_turn().await; - assert!(turn_context.environment.is_none()); + assert!(turn_context.primary_environment().is_none()); + assert!(turn_context.environments.is_empty()); assert_eq!(turn_context.cwd, session_cwd); assert_eq!(turn_context.config.cwd, session_cwd); assert_eq!(turn_context.environments.len(), 0); } #[tokio::test] -async fn multiple_turn_environments_use_first_as_primary_environment() { - let (session, _turn_context, _rx) = make_session_and_context_with_rx().await; - let session_cwd = session.get_config().await.cwd.clone(); - let first_cwd = - AbsolutePathBuf::try_from(session_cwd.as_path().join("first")).expect("absolute path"); - let second_cwd = - AbsolutePathBuf::try_from(session_cwd.as_path().join("second")).expect("absolute path"); +async fn primary_environment_uses_first_turn_environment() { + let (_session, mut turn_context) = make_session_and_context().await; + let first_environment = turn_context.environments[0].clone(); + let second_cwd = turn_context.cwd.join("second"); + turn_context.environments.push(TurnEnvironment { + environment_id: "second".to_string(), + environment: Arc::clone(&first_environment.environment), + cwd: second_cwd.clone(), + }); - let turn_context = session - .new_turn_with_sub_id( - "sub-1".to_string(), - SessionSettingsUpdate { - environments: Some(vec![ - TurnEnvironmentSelection { - environment_id: "local".to_string(), - cwd: first_cwd.clone(), - }, - TurnEnvironmentSelection { - environment_id: "local".to_string(), - cwd: second_cwd.clone(), - }, - ]), - ..Default::default() - }, - ) - .await - .expect("turn should start"); - - let turn_environments = &turn_context.environments; - assert_eq!(turn_environments.len(), 2); - assert_eq!(turn_environments[0].cwd, first_cwd); - assert_eq!(turn_environments[1].cwd, second_cwd); - assert!(std::sync::Arc::ptr_eq( + assert_eq!( turn_context - .environment - .as_ref() - .expect("primary environment should be set"), - &turn_environments[0].environment - )); - assert_eq!(turn_context.cwd, first_cwd); - assert_eq!(turn_context.config.cwd, first_cwd); + .primary_environment() + .expect("primary environment") + .environment_id, + first_environment.environment_id + ); + assert_eq!( + turn_context + .environments + .iter() + .find(|environment| environment.environment_id == "second") + .expect("second environment") + .cwd, + second_cwd + ); + assert_eq!(turn_context.environments.len(), 2); + assert_eq!(turn_context.environments[1].cwd, second_cwd); } #[tokio::test] @@ -4451,15 +4525,19 @@ async fn empty_turn_environments_clear_primary_environment() { .await .expect("turn should start"); - assert!(turn_context.environment.is_none()); + assert!(turn_context.primary_environment().is_none()); + assert!(turn_context.environments.is_empty()); assert_eq!(turn_context.cwd, session.get_config().await.cwd); assert_eq!(turn_context.config.cwd, session.get_config().await.cwd); - assert_eq!(turn_context.environments.len(), 0); } #[tokio::test] async fn unknown_turn_environment_returns_error() { let (session, _turn_context, _rx) = make_session_and_context_with_rx().await; + let original_configuration = { + let state = session.state.lock().await; + state.session_configuration.clone() + }; let err = session .new_turn_with_sub_id( @@ -4467,7 +4545,7 @@ async fn unknown_turn_environment_returns_error() { SessionSettingsUpdate { environments: Some(vec![TurnEnvironmentSelection { environment_id: "missing".to_string(), - cwd: session.get_config().await.cwd.clone(), + cwd: original_configuration.cwd.clone(), }]), ..Default::default() }, @@ -4475,8 +4553,58 @@ async fn unknown_turn_environment_returns_error() { .await .expect_err("unknown environment should fail"); + let current_configuration = { + let state = session.state.lock().await; + state.session_configuration.clone() + }; assert!(matches!(err, CodexErr::InvalidRequest(_))); assert!(err.to_string().contains("missing")); + assert_eq!(current_configuration.cwd, original_configuration.cwd); + assert_eq!( + current_configuration.environments, + original_configuration.environments + ); +} + +#[tokio::test] +async fn duplicate_turn_environment_returns_error_without_mutating_session() { + let (session, _turn_context, _rx) = make_session_and_context_with_rx().await; + let original_configuration = { + let state = session.state.lock().await; + state.session_configuration.clone() + }; + + let err = session + .new_turn_with_sub_id( + "sub-1".to_string(), + SessionSettingsUpdate { + environments: Some(vec![ + TurnEnvironmentSelection { + environment_id: "local".to_string(), + cwd: original_configuration.cwd.clone(), + }, + TurnEnvironmentSelection { + environment_id: "local".to_string(), + cwd: original_configuration.cwd.join("second"), + }, + ]), + ..Default::default() + }, + ) + .await + .expect_err("duplicate environment should fail"); + + let current_configuration = { + let state = session.state.lock().await; + state.session_configuration.clone() + }; + assert!(matches!(err, CodexErr::InvalidRequest(_))); + assert!(err.to_string().contains("duplicate")); + assert_eq!(current_configuration.cwd, original_configuration.cwd); + assert_eq!( + current_configuration.environments, + original_configuration.environments + ); } #[tokio::test] @@ -5033,7 +5161,6 @@ where model_info, &models_manager, /*network*/ None, - Some(environment), turn_environments, session_configuration.cwd.clone(), "turn_id".to_string(), diff --git a/codex-rs/core/src/session/tests/guardian_tests.rs b/codex-rs/core/src/session/tests/guardian_tests.rs index 7f9673255d..d6a87d466a 100644 --- a/codex-rs/core/src/session/tests/guardian_tests.rs +++ b/codex-rs/core/src/session/tests/guardian_tests.rs @@ -1,5 +1,6 @@ use super::*; use crate::compact::InitialContextInjection; +use crate::environment_selection::ResolvedTurnEnvironments; use crate::exec::ExecCapturePolicy; use crate::exec::ExecParams; use crate::exec_policy::ExecPolicyManager; @@ -754,7 +755,9 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() { parent_rollout_thread_trace: codex_rollout_trace::ThreadTraceContext::disabled(), user_shell_override: None, parent_trace: None, - environments: Vec::new(), + environment_selections: ResolvedTurnEnvironments { + turn_environments: Vec::new(), + }, analytics_events_client: None, thread_store, }) diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 410e16703a..622588fbe8 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -59,7 +59,6 @@ pub(crate) struct TurnContext { pub(crate) reasoning_effort: Option, pub(crate) reasoning_summary: ReasoningSummaryConfig, pub(crate) session_source: SessionSource, - pub(crate) environment: Option>, pub(crate) environments: Vec, /// The session's absolute working directory. All relative paths provided /// by the model as well as sandbox policies are resolved against this path @@ -106,6 +105,10 @@ impl TurnContext { self.permission_profile.network_sandbox_policy() } + pub(crate) fn primary_environment(&self) -> Option<&TurnEnvironment> { + self.environments.first() + } + pub(crate) fn sandbox_policy(&self) -> SandboxPolicy { let file_system_sandbox_policy = self.file_system_sandbox_policy(); let network_sandbox_policy = self.network_sandbox_policy(); @@ -230,7 +233,6 @@ impl TurnContext { reasoning_effort, reasoning_summary: self.reasoning_summary, session_source: self.session_source.clone(), - environment: self.environment.clone(), environments: self.environments.clone(), cwd: self.cwd.clone(), current_date: self.current_date.clone(), @@ -432,7 +434,6 @@ impl Session { model_info: ModelInfo, models_manager: &SharedModelsManager, network: Option, - environment: Option>, environments: Vec, cwd: AbsolutePathBuf, sub_id: String, @@ -474,7 +475,7 @@ impl Session { ) .with_web_search_config(per_turn_config.web_search_config.clone()) .with_allow_login_shell(per_turn_config.permissions.allow_login_shell) - .with_has_environment(environment.is_some()) + .with_has_environment(!environments.is_empty()) .with_spawn_agent_usage_hint(per_turn_config.multi_agent_v2.usage_hint_enabled) .with_spawn_agent_usage_hint_text(per_turn_config.multi_agent_v2.usage_hint_text.clone()) .with_hide_spawn_agent_metadata(per_turn_config.multi_agent_v2.hide_spawn_agent_metadata) @@ -522,7 +523,6 @@ impl Session { reasoning_effort, reasoning_summary, session_source, - environment, environments, cwd, current_date: Some(current_date), @@ -564,10 +564,16 @@ impl Session { let mut state = self.state.lock().await; match state.session_configuration.clone().apply(&updates) { Ok(next) => { - let effective_environments = updates + let mut effective_environments = updates .environments .clone() .unwrap_or_else(|| next.environments.clone()); + if updates.environments.is_none() { + Self::overlay_runtime_cwd_on_primary_environment( + &mut effective_environments, + &next.cwd, + ); + } let turn_environments = self.resolve_turn_environments(&effective_environments)?; let previous_cwd = state.session_configuration.cwd.clone(); @@ -641,27 +647,11 @@ impl Session { &self, environments: &[TurnEnvironmentSelection], ) -> CodexResult> { - let mut turn_environments = Vec::with_capacity(environments.len()); - for selected_environment in environments { - let environment_id = selected_environment.environment_id.clone(); - let environment = self - .services - .environment_manager - .get_environment(&environment_id) - .ok_or_else(|| { - CodexErr::InvalidRequest(format!( - "unknown turn environment id `{environment_id}`" - )) - })?; - let cwd = selected_environment.cwd.clone(); - turn_environments.push(TurnEnvironment { - environment_id, - environment, - cwd, - }); - } - - Ok(turn_environments) + crate::environment_selection::resolve_environment_selections( + self.services.environment_manager.as_ref(), + environments, + ) + .map(|resolved| resolved.turn_environments) } async fn new_turn_from_configuration( @@ -672,8 +662,6 @@ impl Session { turn_environments: Vec, ) -> Arc { let primary_turn_environment = turn_environments.first(); - let environment = primary_turn_environment - .map(|turn_environment| Arc::clone(&turn_environment.environment)); let cwd = primary_turn_environment .map(|turn_environment| turn_environment.cwd.clone()) .unwrap_or_else(|| session_configuration.cwd.clone()); @@ -700,9 +688,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 = environment - .as_ref() - .map(|environment| environment.get_filesystem()); + let fs = primary_turn_environment + .map(|turn_environment| turn_environment.environment.get_filesystem()); let skills_outcome = Arc::new( self.services .skills_manager @@ -731,7 +718,6 @@ impl Session { ) .then(|| started_proxy.proxy()) }), - environment, turn_environments, cwd, sub_id, @@ -773,14 +759,18 @@ impl Session { let state = self.state.lock().await; state.session_configuration.clone() }; - let turn_environments = - match self.resolve_turn_environments(&session_configuration.environments) { - Ok(turn_environments) => turn_environments, - Err(err) => { - warn!("failed to resolve stored session environments: {err}"); - Vec::new() - } - }; + let mut effective_environments = session_configuration.environments.clone(); + Self::overlay_runtime_cwd_on_primary_environment( + &mut effective_environments, + &session_configuration.cwd, + ); + let turn_environments = match self.resolve_turn_environments(&effective_environments) { + Ok(turn_environments) => turn_environments, + Err(err) => { + warn!("failed to resolve stored session environments: {err}"); + Vec::new() + } + }; self.new_turn_from_configuration( sub_id, @@ -790,4 +780,15 @@ impl Session { ) .await } + + fn overlay_runtime_cwd_on_primary_environment( + environments: &mut [TurnEnvironmentSelection], + runtime_cwd: &AbsolutePathBuf, + ) { + if let Some(turn_environment) = environments.first_mut() + && turn_environment.cwd != *runtime_cwd + { + turn_environment.cwd = runtime_cwd.clone(); + } + } } diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index c42b7f0c25..eb7419076d 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -4,8 +4,7 @@ use crate::codex_thread::CodexThread; use crate::config::Config; use crate::config::ThreadStoreConfig; use crate::environment_selection::default_thread_environment_selections; -use crate::environment_selection::selected_primary_environment; -use crate::environment_selection::validate_environment_selections; +use crate::environment_selection::resolve_environment_selections; use crate::file_watcher::FileWatcher; use crate::mcp::McpManager; use crate::rollout::RolloutRecorder; @@ -433,7 +432,8 @@ impl ThreadManager { &self, environments: &[TurnEnvironmentSelection], ) -> CodexResult<()> { - validate_environment_selections(self.state.environment_manager.as_ref(), environments) + resolve_environment_selections(self.state.environment_manager.as_ref(), environments) + .map(|_| ()) } pub fn get_models_manager(&self) -> SharedModelsManager { @@ -1098,16 +1098,16 @@ impl ThreadManagerState { threads.remove(&resumed.conversation_id); } } - let environment = - selected_primary_environment(self.environment_manager.as_ref(), &environments)?; - let watch_registration = match environment.as_ref() { - Some(environment) if !environment.is_remote() => { + let environment_selections = + resolve_environment_selections(self.environment_manager.as_ref(), &environments)?; + let watch_registration = match environment_selections.primary_turn_environment() { + Some(turn_environment) if !turn_environment.environment.is_remote() => { self.skills_watcher .register_config( &config, self.skills_manager.as_ref(), self.plugins_manager.as_ref(), - Some(environment.get_filesystem()), + Some(turn_environment.environment.get_filesystem()), ) .await } @@ -1139,7 +1139,7 @@ impl ThreadManagerState { parent_rollout_thread_trace, user_shell_override, parent_trace, - environments, + environment_selections, analytics_events_client: self.analytics_events_client.clone(), thread_store: Arc::clone(&self.thread_store), }) diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index d71eb7931a..294e161483 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -363,13 +363,14 @@ impl ToolHandler for ApplyPatchHandler { // Avoid building temporary ExecParams/command vectors; derive directly from inputs. let cwd = turn.cwd.clone(); let command = vec!["apply_patch".to_string(), patch_input.clone()]; - let Some(environment) = turn.environment.as_ref() else { + let Some(turn_environment) = turn.primary_environment() else { return Err(FunctionCallError::RespondToModel( "apply_patch is unavailable in this session".to_string(), )); }; - let fs = environment.get_filesystem(); - let sandbox = environment + let fs = turn_environment.environment.get_filesystem(); + let sandbox = turn_environment + .environment .is_remote() .then(|| turn.file_system_sandbox_context(/*additional_permissions*/ None)); match codex_apply_patch::maybe_parse_apply_patch_verified( @@ -474,9 +475,8 @@ pub(crate) async fn intercept_apply_patch( tool_name: &str, ) -> Result, FunctionCallError> { let sandbox = turn - .environment - .as_ref() - .filter(|env| env.is_remote()) + .primary_environment() + .filter(|env| env.environment.is_remote()) .map(|_| turn.file_system_sandbox_context(/*additional_permissions*/ None)); match codex_apply_patch::maybe_parse_apply_patch_verified(command, cwd, fs, sandbox.as_ref()) .await diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index b7512b7076..fb80845bdd 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -412,12 +412,12 @@ impl ShellHandler { } = args; let mut exec_params = exec_params; - let Some(environment) = turn.environment.as_ref() else { + let Some(turn_environment) = turn.primary_environment() else { return Err(FunctionCallError::RespondToModel( "shell is unavailable in this session".to_string(), )); }; - let fs = environment.get_filesystem(); + let fs = turn_environment.environment.get_filesystem(); let dependency_env = session.dependency_env().await; if !dependency_env.is_empty() { diff --git a/codex-rs/core/src/tools/handlers/unified_exec.rs b/codex-rs/core/src/tools/handlers/unified_exec.rs index 10c8deeb3f..5aec8c8ba5 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec.rs @@ -196,12 +196,12 @@ impl ToolHandler for UnifiedExecHandler { } }; - let Some(environment) = turn.environment.as_ref() else { + let Some(turn_environment) = turn.primary_environment() else { return Err(FunctionCallError::RespondToModel( "unified exec is unavailable in this session".to_string(), )); }; - let fs = environment.get_filesystem(); + let fs = turn_environment.environment.get_filesystem(); let manager: &UnifiedExecProcessManager = &session.services.unified_exec_manager; let context = UnifiedExecContext::new(session.clone(), turn.clone(), call_id.clone()); diff --git a/codex-rs/core/src/tools/handlers/view_image.rs b/codex-rs/core/src/tools/handlers/view_image.rs index 8f3f69701f..fc0a50d65b 100644 --- a/codex-rs/core/src/tools/handlers/view_image.rs +++ b/codex-rs/core/src/tools/handlers/view_image.rs @@ -88,16 +88,18 @@ impl ToolHandler for ViewImageHandler { }; let abs_path = turn.resolve_path(Some(args.path)); - let Some(environment) = turn.environment.as_ref() else { + let Some(environment) = turn.primary_environment() else { return Err(FunctionCallError::RespondToModel( "view_image is unavailable in this session".to_string(), )); }; let sandbox = environment + .environment .is_remote() .then(|| turn.file_system_sandbox_context(/*additional_permissions*/ None)); let metadata = environment + .environment .get_filesystem() .get_metadata(&abs_path, sandbox.as_ref()) .await @@ -115,6 +117,7 @@ impl ToolHandler for ViewImageHandler { ))); } let file_bytes = environment + .environment .get_filesystem() .read_file(&abs_path, sandbox.as_ref()) .await diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index a25a06aac3..e720243f2b 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -191,11 +191,11 @@ impl ToolRuntime for ApplyPatchRuntime { attempt: &SandboxAttempt<'_>, ctx: &ToolCtx, ) -> Result { - let environment = ctx.turn.environment.as_ref().ok_or_else(|| { + let turn_environment = ctx.turn.primary_environment().ok_or_else(|| { ToolError::Rejected("apply_patch is unavailable in this session".to_string()) })?; let started_at = Instant::now(); - let fs = environment.get_filesystem(); + let fs = turn_environment.environment.get_filesystem(); let sandbox = Self::file_system_sandbox_context_for_attempt(req, attempt); let mut stdout = Vec::new(); let mut stderr = Vec::new(); diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index dbdd6efb51..5206168230 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -254,9 +254,8 @@ impl<'a> ToolRuntime for UnifiedExecRunt } let environment_is_remote = ctx .turn - .environment - .as_ref() - .is_some_and(|environment| environment.is_remote()); + .primary_environment() + .is_some_and(|turn_environment| turn_environment.environment.is_remote()); let command = if environment_is_remote { base_command.to_vec() } else { @@ -293,12 +292,12 @@ impl<'a> ToolRuntime for UnifiedExecRunt .await? { Some(prepared) => { - let Some(environment) = ctx.turn.environment.as_ref() else { + let Some(turn_environment) = ctx.turn.primary_environment() else { return Err(ToolError::Rejected( "exec_command is unavailable in this session".to_string(), )); }; - if environment.is_remote() { + if turn_environment.environment.is_remote() { return Err(ToolError::Rejected( "unified_exec zsh-fork is not supported when exec_server_url is configured".to_string(), )); @@ -310,7 +309,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt &prepared.exec_request, req.tty, prepared.spawn_lifecycle, - environment.as_ref(), + turn_environment.environment.as_ref(), ) .await .map_err(|err| match err { @@ -338,7 +337,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt .env_for(command, options, managed_network) .map_err(|err| ToolError::Codex(err.into()))?; exec_env.exec_server_env_config = req.exec_server_env_config.clone(); - let Some(environment) = ctx.turn.environment.as_ref() else { + let Some(turn_environment) = ctx.turn.primary_environment() else { return Err(ToolError::Rejected( "exec_command is unavailable in this session".to_string(), )); @@ -349,7 +348,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt &exec_env, req.tty, Box::new(NoopSpawnLifecycle), - environment.as_ref(), + turn_environment.environment.as_ref(), ) .await .map_err(|err| match err { diff --git a/codex-rs/core/src/unified_exec/mod_tests.rs b/codex-rs/core/src/unified_exec/mod_tests.rs index fe87c62613..4420f11e3c 100644 --- a/codex-rs/core/src/unified_exec/mod_tests.rs +++ b/codex-rs/core/src/unified_exec/mod_tests.rs @@ -96,7 +96,10 @@ async fn exec_command_with_tty( &request, tty, Box::new(NoopSpawnLifecycle), - turn.environment.as_ref().expect("turn environment"), + turn.primary_environment() + .expect("turn environment") + .environment + .as_ref(), ) .await?, ); @@ -591,7 +594,7 @@ async fn remote_exec_server_rejects_inherited_fd_launches() -> anyhow::Result<() let remote_test_env = remote_test_env().await?; let (_, mut turn) = make_session_and_context().await; - turn.environment = Some(Arc::new(remote_test_env.environment().clone())); + turn.environments[0].environment = Arc::new(remote_test_env.environment().clone()); let request = test_exec_request( &turn, @@ -609,7 +612,10 @@ async fn remote_exec_server_rejects_inherited_fd_launches() -> anyhow::Result<() Box::new(TestSpawnLifecycle { inherited_fds: vec![42], }), - turn.environment.as_ref().expect("turn environment"), + turn.primary_environment() + .expect("turn environment") + .environment + .as_ref(), ) .await .expect_err("expected inherited fd rejection");