diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 5d47188b7f..1fd83545c5 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -1,7 +1,6 @@ use crate::agent::AgentStatus; use crate::config::ConstraintResult; use crate::elicitation::ElicitationRegistration; -use crate::environment_config::EnvironmentConfig; use crate::session::SessionIo; use crate::session::SessionSettingsUpdate; use crate::session::session::Session; @@ -27,6 +26,7 @@ use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::EnvironmentConfig; use codex_protocol::protocol::Event; use codex_protocol::protocol::MultiAgentVersion; use codex_protocol::protocol::Op; @@ -669,7 +669,7 @@ impl CodexThread { } pub async fn environment_selections(&self) -> Vec { - self.session.thread_environment_selections().await + self.session.services.turn_environments.selections() } /// Installs resolved environment configuration and capability roots on this thread. diff --git a/codex-rs/core/src/environment_selection.rs b/codex-rs/core/src/environment_selection.rs index 0a1336d7c3..4e74360d11 100644 --- a/codex-rs/core/src/environment_selection.rs +++ b/codex-rs/core/src/environment_selection.rs @@ -16,6 +16,7 @@ use codex_protocol::capabilities::CapabilityRootLocation; use codex_protocol::capabilities::SelectedCapabilityRoot; use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; +use codex_protocol::protocol::EnvironmentConfig; use codex_protocol::protocol::EnvironmentConnectionEvent; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; @@ -27,7 +28,6 @@ use futures::future::BoxFuture; use futures::future::Shared; use tokio_util::task::AbortOnDropHandle; -use crate::environment_config::EnvironmentConfig; use crate::session::turn_context::ShellSnapshotTask; use crate::session::turn_context::TurnEnvironment; use crate::session::turn_context::TurnEnvironmentConfig; @@ -251,6 +251,38 @@ impl ThreadEnvironments { } } + pub(crate) fn selections(&self) -> Vec { + self.environments + .load() + .iter() + .map(|environment| environment.selection.clone()) + .collect() + } + + pub(crate) fn primary_workspace_roots(&self) -> Vec { + self.environments + .load() + .first() + .map_or_else(Vec::new, |environment| { + Self::primary_workspace_roots_for(std::slice::from_ref(&environment.selection)) + }) + } + + pub(crate) fn primary_workspace_roots_for( + selections: &[TurnEnvironmentSelection], + ) -> Vec { + selections + .first() + .map(|selection| { + selection + .workspace_roots + .iter() + .filter_map(|workspace_root| workspace_root.to_abs_path().ok()) + .collect() + }) + .unwrap_or_default() + } + pub(crate) fn update_environment_configs(&self, config: &TurnEnvironmentConfig) { let environments = self .environments diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 214a1b188f..e8335a4486 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -31,11 +31,11 @@ mod compact_remote; mod compact_remote_history; mod compact_remote_v2; mod compact_token_budget; +pub use codex_protocol::protocol::EnvironmentConfig; pub use codex_thread::BackgroundTerminalInfo; pub use codex_thread::CodexThread; pub use codex_thread::CodexThreadSettingsOverrides; pub use codex_thread::ThreadConfigSnapshot; -pub use environment_config::EnvironmentConfig; pub use session::turn_context::TurnContext; mod agent; mod agent_communication; @@ -48,7 +48,6 @@ pub mod context; mod context_manager; mod current_time; mod elicitation; -mod environment_config; mod environment_selection; pub mod exec; pub mod exec_env; diff --git a/codex-rs/core/src/session/environment.rs b/codex-rs/core/src/session/environment.rs index fe9fa0504b..71df217627 100644 --- a/codex-rs/core/src/session/environment.rs +++ b/codex-rs/core/src/session/environment.rs @@ -5,12 +5,23 @@ use codex_exec_server::SelectedCapabilityRootsStatus; use codex_protocol::capabilities::CapabilityRootLocation; use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; +use codex_protocol::protocol::EnvironmentConfig; use codex_protocol::protocol::TurnEnvironmentSelection; -use crate::environment_config::EnvironmentConfig; +use crate::config::ConstraintResult; use crate::session::session::Session; +use crate::session::session::SessionConfiguration; +use crate::session::session::SessionSettingsUpdate; impl Session { + pub(super) fn apply_session_settings( + &self, + current: &SessionConfiguration, + updates: &SessionSettingsUpdate, + ) -> ConstraintResult { + current.apply(updates, &self.services.turn_environments.selections()) + } + pub(crate) async fn environment_ready( &self, selection: &TurnEnvironmentSelection, diff --git a/codex-rs/core/src/session/mcp_runtime.rs b/codex-rs/core/src/session/mcp_runtime.rs index 98829ce4c4..d106c4f2aa 100644 --- a/codex-rs/core/src/session/mcp_runtime.rs +++ b/codex-rs/core/src/session/mcp_runtime.rs @@ -24,6 +24,21 @@ pub(super) struct McpDesiredState { } impl Session { + pub(super) fn mcp_inputs_differ( + &self, + current: &SessionConfiguration, + next: &SessionConfiguration, + updates: &SessionSettingsUpdate, + ) -> bool { + current.cwd() != next.cwd() + || current.approval_policy.value() != next.approval_policy.value() + || current.approvals_reviewer != next.approvals_reviewer + || current.permission_profile() != next.permission_profile() + || updates.environments.as_ref().is_some_and(|environments| { + environments.environments != self.services.turn_environments.selections() + }) + } + /// Waits on this session's refreshed server before tool execution is admitted. pub(crate) async fn wait_for_mcp_server(self: &Arc, server: &str) { self.refresh_mcp_if_dirty().await; @@ -60,7 +75,7 @@ impl Session { .primary() .and_then(|environment| environment.cwd().to_abs_path().ok()) .unwrap_or_else(|| session_configuration.cwd().clone()); - let config = Self::build_per_turn_config(&session_configuration, cwd); + let config = self.build_per_turn_config(&session_configuration, cwd); let local_process_cwd = environments .local_environment_cwd() .unwrap_or_else(|| session_configuration.cwd().clone()) @@ -88,7 +103,7 @@ impl Session { ) -> anyhow::Result<()> { let cwd = AbsolutePathBuf::from_absolute_path(mcp_runtime_cwd) .unwrap_or_else(|_| session_configuration.cwd().clone()); - let config = Self::build_per_turn_config(session_configuration, cwd); + let config = self.build_per_turn_config(session_configuration, cwd); let local_process_cwd = resolved_environments .local_environment_cwd() .unwrap_or_else(|| session_configuration.cwd().clone()) diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 6a666e0094..76afb34300 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -127,7 +127,6 @@ use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::TurnContextItem; use codex_protocol::protocol::TurnContextNetworkItem; use codex_protocol::protocol::TurnEnvironmentSelection; -use codex_protocol::protocol::TurnEnvironmentSelections; use codex_protocol::protocol::W3cTraceContext; use codex_protocol::protocol::WorldStateItem; use codex_protocol::request_permissions::PermissionGrantScope; @@ -697,10 +696,7 @@ impl Session { approvals_reviewer: config.approvals_reviewer, permission_profile_state: session_permission_profile_state_from_config(&config)?, windows_sandbox_level: WindowsSandboxLevel::from_config(&config), - environments: TurnEnvironmentSelections::new( - config.cwd.clone(), - environment_selections, - ), + legacy_fallback_cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), @@ -718,7 +714,7 @@ impl Session { user_shell_override, }; session_configuration - .validate_auto_review_requirement() + .validate_auto_review_requirement(&environment_selections) .map_err(|err| CodexErr::InvalidRequest(err.to_string()))?; // Generate a unique ID for the lifetime of this session. @@ -727,6 +723,7 @@ impl Session { let session = Box::pin(Session::new( session_configuration, + &environment_selections, config.clone(), user_instructions, installation_id, @@ -1570,7 +1567,8 @@ impl Session { let notify_config_contributors = !self.services.extensions.config_contributors().is_empty(); let (previous_config, new_config, permission_profile_changed, mcp_inputs_changed) = { let mut state = self.state.lock().await; - let updated = match state.session_configuration.apply(&updates) { + let updated = match self.apply_session_settings(&state.session_configuration, &updates) + { Ok(updated) => updated, Err(err) => { warn!("rejected session settings update: {err}"); @@ -1579,25 +1577,26 @@ impl Session { }; let previous_config = notify_config_contributors - .then(|| Self::build_effective_session_config(&state.session_configuration)); - let new_config = - notify_config_contributors.then(|| Self::build_effective_session_config(&updated)); + .then(|| self.build_effective_session_config(&state.session_configuration)); let previous_permission_profile = state.session_configuration.permission_profile(); let updated_permission_profile = updated.permission_profile(); let permission_profile_changed = previous_permission_profile != updated_permission_profile; - let mcp_inputs_changed = state.session_configuration.mcp_inputs_differ(&updated); + let mcp_inputs_changed = + self.mcp_inputs_differ(&state.session_configuration, &updated, &updates); let environment_config = updated.turn_environment_config(); - if updates.environments.is_some() { + if let Some(environments) = &updates.environments { self.services .turn_environments - .update_selections(updated.environment_selections(), &environment_config); + .update_selections(&environments.environments, &environment_config); } else if state.session_configuration.turn_environment_config() != environment_config { self.services .turn_environments .update_environment_configs(&environment_config); } state.session_configuration = updated; + let new_config = notify_config_contributors + .then(|| self.build_effective_session_config(&state.session_configuration)); if mcp_inputs_changed { self.mark_mcp_runtime_dirty(); } @@ -1624,15 +1623,19 @@ impl Session { updates: &SessionSettingsUpdate, ) -> ConstraintResult { let state = self.state.lock().await; - state - .session_configuration - .apply(updates) - .map(|configuration| configuration.thread_config_snapshot()) + let configuration = self.apply_session_settings(&state.session_configuration, updates)?; + let environments = updates.environments.as_ref().map_or_else( + || self.services.turn_environments.selections(), + |environments| environments.environments.clone(), + ); + Ok(configuration.thread_config_snapshot(environments)) } pub(crate) async fn thread_config_snapshot(&self) -> ThreadConfigSnapshot { let state = self.state.lock().await; - state.session_configuration.thread_config_snapshot() + state + .session_configuration + .thread_config_snapshot(self.services.turn_environments.selections()) } pub(crate) async fn set_app_server_client_info( @@ -1662,14 +1665,6 @@ impl Session { .map_or_else(Vec::new, |instructions| instructions.sources().collect()) } - pub(crate) async fn thread_environment_selections(&self) -> Vec { - let state = self.state.lock().await; - state - .session_configuration - .environment_selections() - .to_vec() - } - pub(crate) async fn set_session_startup_prewarm( &self, startup_prewarm: SessionStartupPrewarmHandle, @@ -1708,7 +1703,7 @@ impl Session { let (previous_config, new_config, config) = { let mut state = self.state.lock().await; let previous_config = notify_config_contributors - .then(|| Self::build_effective_session_config(&state.session_configuration)); + .then(|| self.build_effective_session_config(&state.session_configuration)); let mut config = (*state.session_configuration.original_config_do_not_use).clone(); config.config_layer_stack = config .config_layer_stack @@ -1727,7 +1722,7 @@ impl Session { state.session_configuration.original_config_do_not_use = Arc::clone(&config); self.mark_mcp_runtime_dirty(); let new_config = notify_config_contributors - .then(|| Self::build_effective_session_config(&state.session_configuration)); + .then(|| self.build_effective_session_config(&state.session_configuration)); (previous_config, new_config, config) }; self.emit_config_changed_contributors(previous_config.as_ref(), new_config.as_ref()); diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index c7ac5baed3..4837942567 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -92,9 +92,8 @@ pub(crate) struct SessionConfiguration { pub(super) permission_profile_state: PermissionProfileState, pub(super) windows_sandbox_level: WindowsSandboxLevel, - /// Sticky thread-level environment selections plus the legacy cwd used - /// when a turn does not select an environment. - pub(super) environments: TurnEnvironmentSelections, + /// Legacy thread cwd used when a turn does not select an environment. + pub(super) legacy_fallback_cwd: AbsolutePathBuf, /// Directory containing all Codex state for this session. pub(super) codex_home: AbsolutePathBuf, /// Optional user-facing name for the thread, updated during the session. @@ -126,25 +125,7 @@ pub(crate) struct SessionConfiguration { impl SessionConfiguration { pub(super) fn cwd(&self) -> &AbsolutePathBuf { - &self.environments.legacy_fallback_cwd - } - - pub(super) fn environment_selections(&self) -> &[TurnEnvironmentSelection] { - &self.environments.environments - } - - pub(super) fn primary_workspace_roots(&self) -> Vec { - self.environments - .environments - .first() - .map(|environment| { - environment - .workspace_roots - .iter() - .filter_map(|workspace_root| workspace_root.to_abs_path().ok()) - .collect() - }) - .unwrap_or_default() + &self.legacy_fallback_cwd } pub(crate) fn codex_home(&self) -> &AbsolutePathBuf { @@ -170,16 +151,13 @@ impl SessionConfiguration { self.permission_profile_state.permission_profile().clone() } - pub(super) fn mcp_inputs_differ(&self, next: &Self) -> bool { - self.environments != next.environments - || self.approval_policy.value() != next.approval_policy.value() - || self.approvals_reviewer != next.approvals_reviewer - || self.permission_profile() != next.permission_profile() - } - - fn materialized_permission_profile(&self) -> PermissionProfile { + fn materialized_permission_profile( + &self, + environments: &[TurnEnvironmentSelection], + ) -> PermissionProfile { + let workspace_roots = ThreadEnvironments::primary_workspace_roots_for(environments); self.permission_profile() - .materialize_project_roots_with_workspace_roots(&self.primary_workspace_roots()) + .materialize_project_roots_with_workspace_roots(&workspace_roots) } pub(super) fn active_permission_profile(&self) -> Option { @@ -206,16 +184,22 @@ impl SessionConfiguration { .set_legacy_permission_profile(permission_profile) } - pub(super) fn sandbox_policy(&self) -> SandboxPolicy { - let permission_profile = self.materialized_permission_profile(); + pub(super) fn sandbox_policy( + &self, + environments: &[TurnEnvironmentSelection], + ) -> SandboxPolicy { + let permission_profile = self.materialized_permission_profile(environments); codex_sandboxing::compatibility_sandbox_policy_for_permission_profile( &permission_profile, self.cwd(), ) } - pub(super) fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy { - self.materialized_permission_profile() + pub(super) fn file_system_sandbox_policy( + &self, + environments: &[TurnEnvironmentSelection], + ) -> FileSystemSandboxPolicy { + self.materialized_permission_profile(environments) .file_system_sandbox_policy() } @@ -225,17 +209,25 @@ impl SessionConfiguration { .network_sandbox_policy() } - pub(super) fn thread_config_snapshot(&self) -> ThreadConfigSnapshot { + pub(super) fn thread_config_snapshot( + &self, + environment_selections: Vec, + ) -> ThreadConfigSnapshot { + let workspace_roots = + ThreadEnvironments::primary_workspace_roots_for(&environment_selections); ThreadConfigSnapshot { model: self.collaboration_mode.model().to_string(), model_provider_id: self.original_config_do_not_use.model_provider_id.clone(), service_tier: self.service_tier.clone(), approval_policy: self.approval_policy.value(), approvals_reviewer: self.approvals_reviewer, - permission_profile: self.materialized_permission_profile(), + permission_profile: self.materialized_permission_profile(&environment_selections), active_permission_profile: self.active_permission_profile(), - environments: self.environments.clone(), - workspace_roots: self.primary_workspace_roots(), + environments: TurnEnvironmentSelections::new( + self.legacy_fallback_cwd.clone(), + environment_selections, + ), + workspace_roots, profile_workspace_roots: self.profile_workspace_roots().to_vec(), ephemeral: self.original_config_do_not_use.ephemeral, reasoning_effort: self.collaboration_mode.reasoning_effort(), @@ -251,7 +243,10 @@ impl SessionConfiguration { } } - pub(super) fn validate_auto_review_requirement(&self) -> ConstraintResult<()> { + pub(super) fn validate_auto_review_requirement( + &self, + environments: &[TurnEnvironmentSelection], + ) -> ConstraintResult<()> { if self.trusted_guardian_reviewer { return Ok(()); } @@ -268,7 +263,7 @@ impl SessionConfiguration { if self.approval_policy.value() == AskForApproval::OnRequest && self.approvals_reviewer == ApprovalsReviewer::AutoReview && !self - .file_system_sandbox_policy() + .file_system_sandbox_policy(environments) .has_full_disk_write_access() && self .original_config_do_not_use @@ -283,10 +278,15 @@ impl SessionConfiguration { }) } - pub(crate) fn apply(&self, updates: &SessionSettingsUpdate) -> ConstraintResult { + pub(super) fn apply( + &self, + updates: &SessionSettingsUpdate, + current_environments: &[TurnEnvironmentSelection], + ) -> ConstraintResult { let mut next_configuration = self.clone(); - let current_sandbox_policy = self.sandbox_policy(); - let current_file_system_sandbox_policy = self.file_system_sandbox_policy(); + let current_sandbox_policy = self.sandbox_policy(current_environments); + let current_file_system_sandbox_policy = + self.file_system_sandbox_policy(current_environments); let current_network_sandbox_policy = self.network_sandbox_policy(); let legacy_file_system_projection = FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries( @@ -371,12 +371,10 @@ impl SessionConfiguration { } let current_cwd = self.cwd().clone(); - let next_environments = updates - .environments - .clone() - .unwrap_or_else(|| self.environments.clone()); - let cwd_changed = next_environments.legacy_fallback_cwd != current_cwd; - next_configuration.environments = next_environments; + if let Some(environments) = &updates.environments { + next_configuration.legacy_fallback_cwd = environments.legacy_fallback_cwd.clone(); + } + let cwd_changed = next_configuration.legacy_fallback_cwd != current_cwd; if let Some(permission_profile) = updates.permission_profile.clone() { let active_permission_profile = @@ -466,7 +464,13 @@ impl SessionConfiguration { if let Some(app_server_client_version) = updates.app_server_client_version.clone() { next_configuration.app_server_client_version = Some(app_server_client_version); } - next_configuration.validate_auto_review_requirement()?; + let next_environments = updates + .environments + .as_ref() + .map_or(current_environments, |environments| { + environments.environments.as_slice() + }); + next_configuration.validate_auto_review_requirement(next_environments)?; Ok(next_configuration) } @@ -572,6 +576,7 @@ impl Session { #[allow(clippy::too_many_arguments)] pub(crate) async fn new( mut session_configuration: SessionConfiguration, + environment_selections: &[TurnEnvironmentSelection], config: Arc, user_instructions: Option, installation_id: String, @@ -838,8 +843,7 @@ impl Session { let thread_extension_data_for_mcp = &thread_extension_data; let mcp_originator = session_configuration.originator.clone(); let mcp_session_source = session_configuration.session_source.clone(); - let mcp_runtime_cwd = session_configuration - .environment_selections() + let mcp_runtime_cwd = environment_selections .first() .and_then(|environment| environment.cwd.to_abs_path().ok()) .map(|cwd| cwd.to_path_buf()) @@ -899,7 +903,10 @@ impl Session { model: session_configuration.collaboration_mode.model().to_string(), provider_name: config.model_provider_id.clone(), approval_policy: session_configuration.approval_policy.value().to_string(), - sandbox_policy: format!("{:?}", session_configuration.sandbox_policy()), + sandbox_policy: format!( + "{:?}", + session_configuration.sandbox_policy(environment_selections) + ), }; let rollout_thread_trace = if matches!( session_configuration.session_source, @@ -1062,7 +1069,7 @@ impl Session { config.features.enabled(Feature::DeferredExecutor), )); turn_environments.update_selections( - session_configuration.environment_selections(), + environment_selections, &session_configuration.turn_environment_config(), ); let resolved_environments = turn_environments.snapshot().await; @@ -1194,7 +1201,7 @@ impl Session { config: config.as_ref(), session_source: &session_configuration.session_source, persistent_thread_state_available: state_db_ctx.is_some(), - environments: session_configuration.environment_selections(), + environments: environment_selections, mcp_resource_client: Some(Arc::clone(&mcp_resource_client)), extension_metrics: Some(Arc::clone(&extension_metrics)), session_store: &session_extension_data, @@ -1338,7 +1345,8 @@ impl Session { service_tier: session_configuration.service_tier.clone(), approval_policy: session_configuration.approval_policy.value(), approvals_reviewer: session_configuration.approvals_reviewer, - permission_profile: session_configuration.materialized_permission_profile(), + permission_profile: session_configuration + .materialized_permission_profile(environment_selections), active_permission_profile: session_configuration.active_permission_profile(), cwd: session_configuration.cwd().clone(), reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(), diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 7dac028ffc..1f5fccf9db 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -4181,7 +4181,7 @@ async fn set_rate_limits_retains_previous_credits() { approvals_reviewer: config.approvals_reviewer, permission_profile_state: config.permissions.permission_profile_state().clone(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), - environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()), + legacy_fallback_cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), @@ -4290,7 +4290,7 @@ async fn set_rate_limits_updates_plan_type_when_present() { approvals_reviewer: config.approvals_reviewer, permission_profile_state: config.permissions.permission_profile_state().clone(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), - environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()), + legacy_fallback_cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), @@ -4777,10 +4777,13 @@ async fn session_settings_null_service_tier_update_uses_default_service_tier() { let session_configuration = make_session_configuration_for_tests().await; let updated = session_configuration - .apply(&SessionSettingsUpdate { - service_tier: Some(None), - ..Default::default() - }) + .apply( + &SessionSettingsUpdate { + service_tier: Some(None), + ..Default::default() + }, + &[], + ) .expect("null service tier update should apply"); assert_eq!( @@ -4794,10 +4797,13 @@ async fn session_settings_legacy_fast_service_tier_update_uses_priority_request_ let session_configuration = make_session_configuration_for_tests().await; let updated = session_configuration - .apply(&SessionSettingsUpdate { - service_tier: Some(Some("fast".to_string())), - ..Default::default() - }) + .apply( + &SessionSettingsUpdate { + service_tier: Some(Some("fast".to_string())), + ..Default::default() + }, + &[], + ) .expect("legacy fast service tier update should apply"); assert_eq!( @@ -4838,7 +4844,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati approvals_reviewer: config.approvals_reviewer, permission_profile_state: config.permissions.permission_profile_state().clone(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), - environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()), + legacy_fallback_cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), @@ -4895,7 +4901,7 @@ async fn emit_subagent_session_started_includes_fork_lineage_and_originator() { SessionId::from(child_thread_id), child_thread_id, Some(parent_thread_id), - session_configuration.thread_config_snapshot(), + session_configuration.thread_config_snapshot(Vec::new()), SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, @@ -4942,6 +4948,7 @@ async fn emit_subagent_session_started_includes_fork_lineage_and_originator() { async fn resolved_environments_for_configuration( session_configuration: &SessionConfiguration, + environment_selections: &[TurnEnvironmentSelection], ) -> (Arc, TurnEnvironmentSnapshot) { let environment_manager = Arc::new(EnvironmentManager::default_for_tests()); let turn_environments = ThreadEnvironments::new( @@ -4953,7 +4960,7 @@ async fn resolved_environments_for_configuration( /*non_blocking_snapshots*/ false, ); turn_environments.update_selections( - session_configuration.environment_selections(), + environment_selections, &session_configuration.turn_environment_config(), ); (environment_manager, turn_environments.snapshot().await) @@ -4970,8 +4977,7 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd let project_root = project_root.abs(); let docs_dir = docs_dir.abs(); - session_configuration.environments = - TurnEnvironmentSelections::new(original_cwd.abs(), Vec::new()); + session_configuration.legacy_fallback_cwd = original_cwd.abs(); let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), network_access: false, @@ -5002,20 +5008,21 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd ), ) .expect("set permission profile"); - let expected_file_system_sandbox_policy = file_system_sandbox_policy - .materialize_project_roots_with_workspace_roots( - &session_configuration.primary_workspace_roots(), - ); + let expected_file_system_sandbox_policy = + file_system_sandbox_policy.materialize_project_roots_with_workspace_roots(&[]); let updated = session_configuration - .apply(&SessionSettingsUpdate { - environments: Some(TurnEnvironmentSelections::new(project_root, Vec::new())), - ..Default::default() - }) + .apply( + &SessionSettingsUpdate { + environments: Some(TurnEnvironmentSelections::new(project_root, Vec::new())), + ..Default::default() + }, + &[], + ) .expect("cwd-only update should succeed"); assert_eq!( - updated.file_system_sandbox_policy(), + updated.file_system_sandbox_policy(&[]), expected_file_system_sandbox_policy ); } @@ -5024,8 +5031,7 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd async fn session_configuration_apply_permission_profile_preserves_existing_deny_read_entries() { let mut session_configuration = make_session_configuration_for_tests().await; let cwd = tempfile::tempdir().expect("create temp dir"); - session_configuration.environments = - TurnEnvironmentSelections::new(cwd.path().abs(), Vec::new()); + session_configuration.legacy_fallback_cwd = cwd.path().abs(); let workspace_policy = SandboxPolicy::new_workspace_write_policy(); let deny_entry = FileSystemSandboxEntry { @@ -5061,20 +5067,21 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_ NetworkSandboxPolicy::Restricted, ); let updated = session_configuration - .apply(&SessionSettingsUpdate { - permission_profile: Some(permission_profile), - ..Default::default() - }) + .apply( + &SessionSettingsUpdate { + permission_profile: Some(permission_profile), + ..Default::default() + }, + &[], + ) .expect("permission profile update should succeed"); - let mut expected_file_system_policy = requested_file_system_policy - .materialize_project_roots_with_workspace_roots( - &session_configuration.primary_workspace_roots(), - ); + let mut expected_file_system_policy = + requested_file_system_policy.materialize_project_roots_with_workspace_roots(&[]); expected_file_system_policy.glob_scan_max_depth = Some(2); expected_file_system_policy.entries.push(deny_entry); assert_eq!( - updated.file_system_sandbox_policy(), + updated.file_system_sandbox_policy(&[]), expected_file_system_policy ); } @@ -5083,8 +5090,7 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_ async fn session_configuration_apply_permission_profile_accepts_direct_write_roots() { let mut session_configuration = make_session_configuration_for_tests().await; let cwd = tempfile::tempdir().expect("create cwd"); - session_configuration.environments = - TurnEnvironmentSelections::new(cwd.path().abs(), Vec::new()); + session_configuration.legacy_fallback_cwd = cwd.path().abs(); let external_write_dir = tempfile::tempdir().expect("create external write root"); let external_write_path = AbsolutePathBuf::from_absolute_path( codex_utils_absolute_path::canonicalize_preserving_symlinks(external_write_dir.path()) @@ -5105,19 +5111,22 @@ async fn session_configuration_apply_permission_profile_accepts_direct_write_roo ); let updated = session_configuration - .apply(&SessionSettingsUpdate { - permission_profile: Some(permission_profile.clone()), - ..Default::default() - }) + .apply( + &SessionSettingsUpdate { + permission_profile: Some(permission_profile.clone()), + ..Default::default() + }, + &[], + ) .expect("permission profile update should accept direct runtime permissions"); assert_eq!(updated.permission_profile(), permission_profile); assert_eq!( - updated.file_system_sandbox_policy(), + updated.file_system_sandbox_policy(&[]), file_system_sandbox_policy ); assert_eq!( - updated.sandbox_policy(), + updated.sandbox_policy(&[]), SandboxPolicy::WorkspaceWrite { writable_roots: vec![external_write_path], network_access: false, @@ -5217,11 +5226,14 @@ async fn active_profile_update_rebuilds_network_proxy_config() -> std::io::Resul session_configuration.original_config_do_not_use = Arc::clone(&locked_config); let updated = session_configuration - .apply(&SessionSettingsUpdate { - permission_profile: Some(selected_config.permissions.permission_profile().clone()), - active_permission_profile: selected_config.permissions.active_permission_profile(), - ..Default::default() - }) + .apply( + &SessionSettingsUpdate { + permission_profile: Some(selected_config.permissions.permission_profile().clone()), + active_permission_profile: selected_config.permissions.active_permission_profile(), + ..Default::default() + }, + &[], + ) .expect("active profile update should apply"); let network = updated @@ -5335,8 +5347,7 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up let original_cwd = original_cwd.abs(); let next_cwd = next_cwd.abs(); - session_configuration.environments = - TurnEnvironmentSelections::new(original_cwd.clone(), Vec::new()); + session_configuration.legacy_fallback_cwd = original_cwd.clone(); let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![ FileSystemSandboxEntry { path: FileSystemPath::Special { @@ -5364,25 +5375,28 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up .expect("set permission profile"); let updated = session_configuration - .apply(&SessionSettingsUpdate { - environments: Some(TurnEnvironmentSelections::new(next_cwd.clone(), Vec::new())), - ..Default::default() - }) + .apply( + &SessionSettingsUpdate { + environments: Some(TurnEnvironmentSelections::new(next_cwd.clone(), Vec::new())), + ..Default::default() + }, + &[], + ) .expect("cwd-only update should succeed"); assert_eq!( - updated.file_system_sandbox_policy(), + updated.file_system_sandbox_policy(&[]), file_system_sandbox_policy ); assert!( updated - .file_system_sandbox_policy() + .file_system_sandbox_policy(&[]) .can_write_path_with_cwd(original_cwd.as_path(), updated.cwd().as_path()), "absolute grant to the old cwd must remain writable" ); assert!( !updated - .file_system_sandbox_policy() + .file_system_sandbox_policy(&[]) .can_write_path_with_cwd(next_cwd.as_path(), updated.cwd().as_path()), "cwd-only update must not reinterpret an absolute old-cwd grant as :workspace_roots" ); @@ -5393,13 +5407,7 @@ async fn session_update_settings_does_not_rewrite_sticky_environment_cwds() { let (session, turn_context) = make_session_and_context().await; #[allow(deprecated)] let updated_cwd = turn_context.cwd.join("project"); - let current_environments = { - let state = session.state.lock().await; - state - .session_configuration - .environment_selections() - .to_vec() - }; + let current_environments = session.services.turn_environments.selections(); let expected_environments = current_environments.clone(); std::fs::create_dir_all(updated_cwd.as_path()).expect("create project dir"); @@ -5414,16 +5422,11 @@ async fn session_update_settings_does_not_rewrite_sticky_environment_cwds() { .await .expect("cwd update should succeed"); - let (session_cwd, stored_environments) = { + let session_cwd = { let state = session.state.lock().await; - ( - state.session_configuration.cwd().clone(), - state - .session_configuration - .environment_selections() - .to_vec(), - ) + state.session_configuration.cwd().clone() }; + let stored_environments = session.services.turn_environments.selections(); let config = session.get_config().await; let next_turn = session.new_default_turn().await; @@ -5496,11 +5499,13 @@ async fn permission_profile_updates_apply_to_next_turn_environment() { #[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.environments = Vec::new(); - state.session_configuration.cwd().clone() - }; + let original_cwd = session + .state + .lock() + .await + .session_configuration + .cwd() + .clone(); let updated_cwd = original_cwd.join("project"); std::fs::create_dir_all(updated_cwd.as_path()).expect("create project dir"); @@ -5517,23 +5522,17 @@ async fn relative_cwd_update_without_environments_resolves_under_session_cwd() { let state = session.state.lock().await; assert_eq!(state.session_configuration.cwd(), &updated_cwd); - assert!( - state - .session_configuration - .environment_selections() - .is_empty() - ); + assert!(session.services.turn_environments.selections().is_empty()); } #[tokio::test] async fn environment_settings_preserve_explicit_primary_cwd() { let (session, _turn_context) = make_session_and_context().await; let (original_cwd, environment_cwd, environments) = { - let mut state = session.state.lock().await; + let state = session.state.lock().await; let original_cwd = state.session_configuration.cwd().clone(); let environment_cwd = original_cwd.join("environment"); let environments = vec![local(environment_cwd.clone())]; - state.session_configuration.environments.environments = environments.clone(); (original_cwd, environment_cwd, environments) }; let updated_cwd = original_cwd.join("project"); @@ -5553,7 +5552,7 @@ async fn environment_settings_preserve_explicit_primary_cwd() { let state = session.state.lock().await; assert_eq!(state.session_configuration.cwd(), &updated_cwd); assert_eq!( - state.session_configuration.environment_selections()[0].cwd, + session.services.turn_environments.selections()[0].cwd, PathUri::from_abs_path(&environment_cwd) ); } @@ -5634,7 +5633,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() { approvals_reviewer: config.approvals_reviewer, permission_profile_state: config.permissions.permission_profile_state().clone(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), - environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()), + legacy_fallback_cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), @@ -5663,6 +5662,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() { let environment_manager = Arc::new(EnvironmentManager::default_for_tests()); let result = Session::new( session_configuration, + /*environment_selections*/ &[], Arc::clone(&config), /*user_instructions*/ None, "11111111-1111-4111-8111-111111111111".to_string(), @@ -5775,7 +5775,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { approvals_reviewer: config.approvals_reviewer, permission_profile_state: config.permissions.permission_profile_state().clone(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), - environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments), + legacy_fallback_cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), @@ -5792,12 +5792,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { dynamic_tools: Vec::new(), user_shell_override: None, }; - let per_turn_config = - Session::build_per_turn_config(&session_configuration, session_configuration.cwd().clone()); - let model_info = construct_model_info_offline_for_tests( - session_configuration.collaboration_mode.model(), - &per_turn_config.to_models_manager_config(), - ); let session_telemetry = session_telemetry( thread_id, config.as_ref(), @@ -5807,7 +5801,8 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { let state = SessionState::new(session_configuration.clone()); let (environment_manager, resolved_environments) = - resolved_environments_for_configuration(&session_configuration).await; + resolved_environments_for_configuration(&session_configuration, &default_environments) + .await; let resolved_turn_environments = resolved_environments.clone(); let turn_environments = Arc::new(ThreadEnvironments::new( environment_manager, @@ -5925,43 +5920,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { turn_environments: Arc::clone(&turn_environments), }; - let plugins_input = per_turn_config.plugins_config_input(); - let plugin_outcome = services - .plugins_manager - .plugins_for_config(&plugins_input) - .await; - let effective_skill_roots = plugin_outcome.effective_plugin_skill_roots(); - let plugin_skill_snapshots = services - .plugins_manager - .plugin_skill_snapshots_for_config(&plugins_input); - let skills_input = - crate::skills_load_input_from_config(&per_turn_config, effective_skill_roots) - .with_plugin_skill_snapshots(plugin_skill_snapshots); - let skill_fs = environment.get_filesystem(); - let skills_snapshot = services - .skills_service - .snapshot_for_config(&skills_input, Some(Arc::clone(&skill_fs))) - .await; - let turn_context = Session::make_turn_context( - thread_id, - SessionId::from(thread_id), - Some(Arc::clone(&auth_manager)), - &session_telemetry, - session_configuration.provider.clone(), - &session_configuration, - config.multi_agent_version_from_features(), - services.user_shell.as_ref(), - services.shell_zsh_path.as_ref(), - services.main_execve_wrapper_exe.as_ref(), - per_turn_config, - model_info, - &models_manager, - /*network*/ None, - resolved_turn_environments, - session_configuration.cwd().clone(), - "turn_id".to_string(), - skills_snapshot, - ); let session = Session { thread_id, installation_id: "11111111-1111-4111-8111-111111111111".to_string(), @@ -5989,7 +5947,48 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { fork_persistence: ForkPersistence::Copied, next_internal_sub_id: AtomicU64::new(0), }; - + let per_turn_config = + session.build_per_turn_config(&session_configuration, session_configuration.cwd().clone()); + let plugins_input = per_turn_config.plugins_config_input(); + let plugin_outcome = session + .services + .plugins_manager + .plugins_for_config(&plugins_input) + .await; + let effective_skill_roots = plugin_outcome.effective_plugin_skill_roots(); + let plugin_skill_snapshots = session + .services + .plugins_manager + .plugin_skill_snapshots_for_config(&plugins_input); + let skills_input = + crate::skills_load_input_from_config(&per_turn_config, effective_skill_roots) + .with_plugin_skill_snapshots(plugin_skill_snapshots); + let skill_fs = environment.get_filesystem(); + let skills_snapshot = session + .services + .skills_service + .snapshot_for_config(&skills_input, Some(Arc::clone(&skill_fs))) + .await; + let turn_context = Session::make_turn_context( + thread_id, + SessionId::from(thread_id), + Some(Arc::clone(&auth_manager)), + &session_telemetry, + session_configuration.provider.clone(), + &session_configuration, + config.multi_agent_version_from_features(), + session.services.user_shell.as_ref(), + session.services.shell_zsh_path.as_ref(), + session.services.main_execve_wrapper_exe.as_ref(), + per_turn_config, + model_info, + &models_manager, + /*network*/ None, + resolved_turn_environments, + session_configuration.cwd().clone(), + "turn_id".to_string(), + skills_snapshot, + ); session.mark_mcp_runtime_dirty(); (session, turn_context) } @@ -6054,7 +6053,7 @@ async fn make_session_with_config_and_rx( approvals_reviewer: config.approvals_reviewer, permission_profile_state: config.permissions.permission_profile_state().clone(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), - environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments), + legacy_fallback_cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), @@ -6084,6 +6083,7 @@ async fn make_session_with_config_and_rx( let session = Session::new( session_configuration, + &default_environments, Arc::clone(&config), /*user_instructions*/ None, "11111111-1111-4111-8111-111111111111".to_string(), @@ -6168,7 +6168,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx( approvals_reviewer: config.approvals_reviewer, permission_profile_state: config.permissions.permission_profile_state().clone(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), - environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments), + legacy_fallback_cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), @@ -6198,6 +6198,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx( let session = Session::new( session_configuration, + &default_environments, Arc::clone(&config), /*user_instructions*/ None, "11111111-1111-4111-8111-111111111111".to_string(), @@ -7142,23 +7143,20 @@ async fn turn_environments_set_primary_environment() { #[tokio::test] async fn default_turn_does_not_overlay_legacy_fallback_cwd_onto_stored_thread_environments() { - let (session, initial_turn, _rx) = make_session_and_context_with_rx().await; + let (session, _initial_turn, _rx) = make_session_and_context_with_rx().await; let session_cwd = session.get_config().await.cwd.clone(); let selected_cwd = AbsolutePathBuf::try_from(session_cwd.as_path().join("selected")).expect("absolute path"); - session.services.turn_environments.update_selections( - &[local(selected_cwd.clone())], - &initial_turn - .environments - .primary() - .expect("initial turn environment") - .config, - ); - { - let mut state = session.state.lock().await; - state.session_configuration.environments.environments = vec![local(selected_cwd.clone())]; - } - + session + .update_settings(SessionSettingsUpdate { + environments: Some(TurnEnvironmentSelections::new( + session_cwd.clone(), + vec![local(selected_cwd.clone())], + )), + ..Default::default() + }) + .await + .expect("environment selection update should succeed"); let turn_context = session.new_default_turn().await; let turn_environments = &turn_context.environments; @@ -7182,22 +7180,19 @@ async fn default_turn_does_not_overlay_legacy_fallback_cwd_onto_stored_thread_en #[tokio::test] async fn default_turn_honors_empty_stored_thread_environments() { - let (session, initial_turn, _rx) = make_session_and_context_with_rx().await; + let (session, _initial_turn, _rx) = make_session_and_context_with_rx().await; let session_cwd = session.get_config().await.cwd.clone(); - session.services.turn_environments.update_selections( - &[], - &initial_turn - .environments - .primary() - .expect("initial turn environment") - .config, - ); - { - let mut state = session.state.lock().await; - state.session_configuration.environments.environments = Vec::new(); - } - + session + .update_settings(SessionSettingsUpdate { + environments: Some(TurnEnvironmentSelections::new( + session_cwd.clone(), + Vec::new(), + )), + ..Default::default() + }) + .await + .expect("environment selection update should succeed"); let turn_context = session.new_default_turn().await; assert!(turn_context.environments.primary().is_none()); @@ -7960,7 +7955,7 @@ where approvals_reviewer: config.approvals_reviewer, permission_profile_state: config.permissions.permission_profile_state().clone(), windows_sandbox_level: WindowsSandboxLevel::from_config(&config), - environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments), + legacy_fallback_cwd: config.cwd.clone(), codex_home: config.codex_home.clone(), thread_name: None, original_config_do_not_use: Arc::clone(&config), @@ -7977,12 +7972,6 @@ where dynamic_tools, user_shell_override: None, }; - let per_turn_config = - Session::build_per_turn_config(&session_configuration, session_configuration.cwd().clone()); - let model_info = construct_model_info_offline_for_tests( - session_configuration.collaboration_mode.model(), - &per_turn_config.to_models_manager_config(), - ); let session_telemetry = session_telemetry( thread_id, config.as_ref(), @@ -7992,7 +7981,8 @@ where let state = SessionState::new(session_configuration.clone()); let (environment_manager, resolved_turn_environments) = - resolved_environments_for_configuration(&session_configuration).await; + resolved_environments_for_configuration(&session_configuration, &default_environments) + .await; let turn_environments = Arc::new(ThreadEnvironments::new( environment_manager, default_user_shell(), @@ -8109,43 +8099,6 @@ where turn_environments: Arc::clone(&turn_environments), }; - let plugins_input = per_turn_config.plugins_config_input(); - let plugin_outcome = services - .plugins_manager - .plugins_for_config(&plugins_input) - .await; - let effective_skill_roots = plugin_outcome.effective_plugin_skill_roots(); - let plugin_skill_snapshots = services - .plugins_manager - .plugin_skill_snapshots_for_config(&plugins_input); - let skills_input = - crate::skills_load_input_from_config(&per_turn_config, effective_skill_roots) - .with_plugin_skill_snapshots(plugin_skill_snapshots); - let skill_fs = environment.get_filesystem(); - let skills_snapshot = services - .skills_service - .snapshot_for_config(&skills_input, Some(Arc::clone(&skill_fs))) - .await; - let turn_context = Arc::new(Session::make_turn_context( - thread_id, - SessionId::from(thread_id), - Some(Arc::clone(&auth_manager)), - &session_telemetry, - session_configuration.provider.clone(), - &session_configuration, - config.multi_agent_version_from_features(), - services.user_shell.as_ref(), - services.shell_zsh_path.as_ref(), - services.main_execve_wrapper_exe.as_ref(), - per_turn_config, - model_info, - &models_manager, - /*network*/ None, - resolved_turn_environments, - session_configuration.cwd().clone(), - "turn_id".to_string(), - skills_snapshot, - )); let session = Arc::new(Session { thread_id, installation_id: "11111111-1111-4111-8111-111111111111".to_string(), @@ -8173,7 +8126,48 @@ where fork_persistence: ForkPersistence::Copied, next_internal_sub_id: AtomicU64::new(0), }); - + let per_turn_config = + session.build_per_turn_config(&session_configuration, session_configuration.cwd().clone()); + let plugins_input = per_turn_config.plugins_config_input(); + let plugin_outcome = session + .services + .plugins_manager + .plugins_for_config(&plugins_input) + .await; + let effective_skill_roots = plugin_outcome.effective_plugin_skill_roots(); + let plugin_skill_snapshots = session + .services + .plugins_manager + .plugin_skill_snapshots_for_config(&plugins_input); + let skills_input = + crate::skills_load_input_from_config(&per_turn_config, effective_skill_roots) + .with_plugin_skill_snapshots(plugin_skill_snapshots); + let skill_fs = environment.get_filesystem(); + let skills_snapshot = session + .services + .skills_service + .snapshot_for_config(&skills_input, Some(Arc::clone(&skill_fs))) + .await; + let turn_context = Arc::new(Session::make_turn_context( + thread_id, + SessionId::from(thread_id), + Some(Arc::clone(&auth_manager)), + &session_telemetry, + session_configuration.provider.clone(), + &session_configuration, + config.multi_agent_version_from_features(), + session.services.user_shell.as_ref(), + session.services.shell_zsh_path.as_ref(), + session.services.main_execve_wrapper_exe.as_ref(), + per_turn_config, + model_info, + &models_manager, + /*network*/ None, + resolved_turn_environments, + session_configuration.cwd().clone(), + "turn_id".to_string(), + skills_snapshot, + )); session.mark_mcp_runtime_dirty(); (session, turn_context, rx_event) } diff --git a/codex-rs/core/src/session/thread_settings.rs b/codex-rs/core/src/session/thread_settings.rs index 227b505ec1..6320ab37c9 100644 --- a/codex-rs/core/src/session/thread_settings.rs +++ b/codex-rs/core/src/session/thread_settings.rs @@ -108,10 +108,7 @@ pub(super) async fn emit_applied(session: &Session, submission_id: String) { /// Builds the effective thread-settings event used by live updates and /// synthesized fork history. pub(super) async fn applied_event(session: &Session) -> EventMsg { - let snapshot = { - let state = session.state.lock().await; - state.session_configuration.thread_config_snapshot() - }; + let snapshot = session.thread_config_snapshot().await; EventMsg::ThreadSettingsApplied(ThreadSettingsAppliedEvent { thread_settings: snapshot.into_thread_settings_snapshot(), }) diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index bef1dd3398..2cf00a015a 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -944,10 +944,7 @@ async fn track_turn_resolved_config_analytics( turn_context: &TurnContext, input: &[TurnInput], ) { - let thread_config = { - let state = sess.state.lock().await; - state.session_configuration.thread_config_snapshot() - }; + let thread_config = sess.thread_config_snapshot().await; let is_first_turn = { let mut state = sess.state.lock().await; state.take_next_turn_is_first() diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index eb8cf98608..c5f25854c1 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -508,6 +508,7 @@ fn local_time_context() -> (String, String) { impl Session { /// Don't expand the number of mutated arguments on config. We are in the process of getting rid of it. pub(crate) fn build_per_turn_config( + &self, session_configuration: &SessionConfiguration, cwd: AbsolutePathBuf, ) -> Config { @@ -516,7 +517,7 @@ impl Session { let mut per_turn_config = (*config).clone(); per_turn_config.cwd = cwd; per_turn_config.permissions.approval_policy = session_configuration.approval_policy.clone(); - let workspace_roots = session_configuration.primary_workspace_roots(); + let workspace_roots = self.services.turn_environments.primary_workspace_roots(); per_turn_config.workspace_roots = workspace_roots.clone(); per_turn_config .permissions @@ -552,10 +553,11 @@ impl Session { } pub(crate) fn build_effective_session_config( + &self, session_configuration: &SessionConfiguration, ) -> Config { let mut config = - Self::build_per_turn_config(session_configuration, session_configuration.cwd().clone()); + self.build_per_turn_config(session_configuration, session_configuration.cwd().clone()); config.model = Some(session_configuration.collaboration_mode.model().to_string()); config } @@ -686,24 +688,22 @@ impl Session { let notify_config_contributors = !self.services.extensions.config_contributors().is_empty(); let update_result: CodexResult<_> = { let mut state = self.state.lock().await; - match state.session_configuration.clone().apply(&updates) { + match self.apply_session_settings(&state.session_configuration, &updates) { Ok(next) => { - let mcp_inputs_changed = state.session_configuration.mcp_inputs_differ(&next); + let mcp_inputs_changed = + self.mcp_inputs_differ(&state.session_configuration, &next, &updates); let previous_permission_profile = state.session_configuration.permission_profile(); let next_permission_profile = next.permission_profile(); let permission_profile_changed = previous_permission_profile != next_permission_profile; - let previous_config = notify_config_contributors.then(|| { - Self::build_effective_session_config(&state.session_configuration) - }); - let new_config = notify_config_contributors - .then(|| Self::build_effective_session_config(&next)); + let previous_config = notify_config_contributors + .then(|| self.build_effective_session_config(&state.session_configuration)); let environment_config = next.turn_environment_config(); - if updates.environments.is_some() { + if let Some(environments) = &updates.environments { self.services .turn_environments - .update_selections(next.environment_selections(), &environment_config); + .update_selections(&environments.environments, &environment_config); } else if state.session_configuration.turn_environment_config() != environment_config { @@ -715,6 +715,8 @@ impl Session { self.mark_mcp_runtime_dirty(); } state.session_configuration = next.clone(); + let new_config = notify_config_contributors + .then(|| self.build_effective_session_config(&state.session_configuration)); Ok(( next, mcp_inputs_changed, @@ -814,7 +816,7 @@ impl Session { .as_ref() .and_then(|turn_environment| turn_environment.cwd().to_abs_path().ok()) .unwrap_or_else(|| session_configuration.cwd().clone()); - let per_turn_config = Self::build_per_turn_config(&session_configuration, cwd.clone()); + let per_turn_config = self.build_per_turn_config(&session_configuration, cwd.clone()); let model_info = self .services .models_manager diff --git a/codex-rs/core/tests/suite/remote_env.rs b/codex-rs/core/tests/suite/remote_env.rs index 2c3bb036e8..42a4c7d857 100644 --- a/codex-rs/core/tests/suite/remote_env.rs +++ b/codex-rs/core/tests/suite/remote_env.rs @@ -4,6 +4,7 @@ use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use codex_api::AuthProvider; use codex_config::types::ApprovalsReviewer; +use codex_core::CodexThreadSettingsOverrides; use codex_core::EnvironmentConfig; use codex_core::StartThreadOptions; use codex_core::TurnInputRequest; @@ -537,8 +538,11 @@ async fn settings_update_does_not_retarget_active_turn_environment() -> Result<( }); let test = builder.build(&server).await?; let initial_cwd = test.config.cwd.clone(); + let initial_environments = test.codex.environment_selections().await; let next_workspace = TempDir::new()?; let next_cwd = next_workspace.path().abs(); + let next_environments = + TurnEnvironmentSelections::new(next_cwd.clone(), vec![local(next_cwd.clone())]); test.codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { @@ -552,17 +556,43 @@ async fn settings_update_does_not_retarget_active_turn_environment() -> Result<( }) .await; + let preview = test + .codex + .preview_thread_settings_overrides(CodexThreadSettingsOverrides { + environments: Some(next_environments.clone()), + ..Default::default() + }) + .await?; + assert_eq!( + preview.environment_selections(), + &next_environments.environments + ); + assert_eq!(preview.cwd(), &next_cwd); + assert_eq!(preview.workspace_roots, vec![next_cwd.clone()]); + assert_eq!( + test.codex.environment_selections().await, + initial_environments + ); + submit_thread_settings( &test.codex, ThreadSettingsOverrides { - environments: Some(TurnEnvironmentSelections::new( - next_cwd.clone(), - vec![local(next_cwd.clone())], - )), + environments: Some(next_environments.clone()), ..Default::default() }, ) .await?; + assert_eq!( + test.codex.environment_selections().await, + next_environments.environments + ); + let snapshot = test.codex.config_snapshot().await; + assert_eq!( + snapshot.environment_selections(), + next_environments.environments + ); + assert_eq!(snapshot.cwd(), &next_cwd); + assert_eq!(snapshot.workspace_roots, vec![next_cwd.clone()]); test.codex .submit(Op::UserInputAnswer { id: request.turn_id, diff --git a/codex-rs/core/tests/suite/turn_input_submission.rs b/codex-rs/core/tests/suite/turn_input_submission.rs index 4d70eca48b..09d211f4b1 100644 --- a/codex-rs/core/tests/suite/turn_input_submission.rs +++ b/codex-rs/core/tests/suite/turn_input_submission.rs @@ -13,12 +13,14 @@ use codex_protocol::config_types::Settings; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ThreadSettingsOverrides; +use codex_protocol::protocol::TurnEnvironmentSelections; use codex_protocol::user_input::UserInput; use core_test_support::responses; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_response_created; use core_test_support::streaming_sse::StreamingSseChunk; use core_test_support::streaming_sse::start_streaming_sse_server; +use core_test_support::test_codex::local; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; @@ -292,11 +294,15 @@ async fn turn_input_submission_applies_thread_settings_only_after_accepted_input .await .expect("started turn should reach its first model request"); + let steered_cwd = test.config.cwd.join("steered-environment"); + let steered_environments = + TurnEnvironmentSelections::new(steered_cwd.clone(), vec![local(steered_cwd)]); let steered = codex .start_or_steer_turn( user_message_request("steer active turn").with_thread_settings( ThreadSettingsOverrides { approval_policy: Some(AskForApproval::Never), + environments: Some(steered_environments.clone()), ..Default::default() }, ), @@ -308,16 +314,25 @@ async fn turn_input_submission_applies_thread_settings_only_after_accepted_input codex.config_snapshot().await.approval_policy, AskForApproval::Never ); + assert_eq!( + codex.environment_selections().await, + steered_environments.environments + ); release_response .send(()) .expect("response gate should remain open"); wait_for_event(codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; + let rejected_cwd = test.config.cwd.join("rejected-environment"); let rejected = codex .steer_turn( user_message_request("no active turn").with_thread_settings(ThreadSettingsOverrides { approval_policy: Some(AskForApproval::OnRequest), + environments: Some(TurnEnvironmentSelections::new( + rejected_cwd.clone(), + vec![local(rejected_cwd)], + )), ..Default::default() }), "missing-turn".to_string(), @@ -334,6 +349,10 @@ async fn turn_input_submission_applies_thread_settings_only_after_accepted_input codex.config_snapshot().await.approval_policy, AskForApproval::Never ); + assert_eq!( + codex.environment_selections().await, + steered_environments.environments + ); server.shutdown().await; } diff --git a/codex-rs/core/src/environment_config.rs b/codex-rs/protocol/src/environment.rs similarity index 87% rename from codex-rs/core/src/environment_config.rs rename to codex-rs/protocol/src/environment.rs index 6febe40c3b..cb68e45962 100644 --- a/codex-rs/core/src/environment_config.rs +++ b/codex-rs/protocol/src/environment.rs @@ -1,4 +1,4 @@ -use codex_protocol::capabilities::SelectedCapabilityRoot; +use crate::capabilities::SelectedCapabilityRoot; /// Resolved configuration supplied by the owner of a thread/environment attachment. #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/codex-rs/protocol/src/lib.rs b/codex-rs/protocol/src/lib.rs index b8f4bc65f1..7bb02d316b 100644 --- a/codex-rs/protocol/src/lib.rs +++ b/codex-rs/protocol/src/lib.rs @@ -16,6 +16,7 @@ pub mod approvals; pub mod capabilities; pub mod config_types; pub mod dynamic_tools; +mod environment; pub mod error; pub mod exec_output; pub mod items; diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index e95dd08caf..1b0408c264 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -92,6 +92,7 @@ pub use crate::approvals::NetworkApprovalContext; pub use crate::approvals::NetworkApprovalProtocol; pub use crate::approvals::NetworkPolicyAmendment; pub use crate::approvals::NetworkPolicyRuleAction; +pub use crate::environment::EnvironmentConfig; pub use crate::legacy_events::HasLegacyEvent; pub use crate::permissions::FileSystemAccessMode; pub use crate::permissions::FileSystemPath;