Centralize thread environment selection state (#38423)

## What changed

- Make `ThreadEnvironments` the source of truth for live environment selections and use it when building configuration snapshots, permission profiles, per-turn configuration, and MCP refresh inputs.
- Keep settings previews side-effect free while applying accepted environment updates consistently to subsequent turns.
- Move `EnvironmentConfig` into `codex-protocol` so environment ownership no longer depends on a core-only type.

## Testing

- Cover environment previews and snapshots, accepted and rejected steer updates, and active-turn environment stability.

GitOrigin-RevId: 7bcf8fb5df1215f720e4e9c419541a51a871aa5e
This commit is contained in:
sayan-oai authored and copyberry committed 2026-08-13 19:25:05 +00:00
1 parent 588e18aae5
commit 781445f7c6
16 files changed
+428 -327

No files matched your search

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