From 392328ed5d2030cee129b9831dbe995a6301d572 Mon Sep 17 00:00:00 2001 From: sayan-oai Date: Tue, 18 Aug 2026 20:36:13 +0000 Subject: [PATCH] Preserve owner-provided environment configuration (#39278) ## Why Thread settings could replace an owner-provided environment configuration with `FromThread`, allowing the thread to take ownership of that configuration. ## What changed Reject environment settings updates that change an existing owner-provided configuration to `FromThread`. ## Testing Cover preview and turn settings updates for pending, ready, and failed owner-provided environments. GitOrigin-RevId: c516954819447ceb29e2ff9f29fe781ca101ae54 --- codex-rs/core/src/session/environment.rs | 21 ++++++++++++++- codex-rs/core/tests/suite/remote_env.rs | 33 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/session/environment.rs b/codex-rs/core/src/session/environment.rs index 07924c9c50..4efb79af9a 100644 --- a/codex-rs/core/src/session/environment.rs +++ b/codex-rs/core/src/session/environment.rs @@ -9,6 +9,7 @@ use codex_protocol::protocol::EnvironmentConfig; use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::TurnEnvironmentSelection; +use crate::config::ConstraintError; use crate::config::ConstraintResult; use crate::session::session::Session; use crate::session::session::SessionConfiguration; @@ -78,7 +79,25 @@ impl Session { current: &SessionConfiguration, updates: &SessionSettingsUpdate, ) -> ConstraintResult { - current.apply(updates, &self.services.turn_environments.selections()) + let current_environments = self.services.turn_environments.selections(); + if let Some(environments) = &updates.environments + && let Some(environment) = environments.environments.iter().find(|environment| { + environment.config == EnvironmentConfigState::FromThread + && current_environments.iter().any(|current| { + current.environment_id == environment.environment_id + && current.config != EnvironmentConfigState::FromThread + }) + }) + { + return Err(ConstraintError::InvalidValue { + field_name: "environments", + candidate: environment.environment_id.clone(), + allowed: "owner-provided environment configuration".to_string(), + requirement_source: codex_config::RequirementSource::Unknown, + }); + } + + current.apply(updates, ¤t_environments) } pub(crate) async fn environment_ready( diff --git a/codex-rs/core/tests/suite/remote_env.rs b/codex-rs/core/tests/suite/remote_env.rs index ef55a4868b..6217626986 100644 --- a/codex-rs/core/tests/suite/remote_env.rs +++ b/codex-rs/core/tests/suite/remote_env.rs @@ -1516,6 +1516,39 @@ async fn pending_attachment_installs_configuration_before_waiting_turn_resumes() ..pending_selection.clone() }] ); + let downgraded_environments = TurnEnvironmentSelections::new( + test.config.cwd.clone(), + vec![TurnEnvironmentSelection { + config: EnvironmentConfigState::FromThread, + workspace_roots: Vec::new(), + ..pending_selection.clone() + }], + ); + for thread in [&waiting.thread, &independent.thread, &failed.thread] { + let error = thread + .preview_thread_settings_overrides(CodexThreadSettingsOverrides { + environments: Some(downgraded_environments.clone()), + ..Default::default() + }) + .await + .expect_err("owner-controlled environment must not become thread-owned"); + assert!(error.to_string().contains("owner-provided")); + } + let error = independent + .thread + .start_or_steer_turn( + TurnInputRequest::user_input(vec![UserInput::Text { + text: "attempt to clear owner configuration".into(), + text_elements: Vec::new(), + }]) + .with_thread_settings(ThreadSettingsOverrides { + environments: Some(downgraded_environments), + ..Default::default() + }), + ) + .await + .expect_err("turn settings must not clear owner configuration"); + assert!(error.to_string().contains("owner-provided")); assert!( waiting .thread