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
This commit is contained in:
sayan-oai
2026-08-18 20:36:13 +00:00
committed by copyberry
parent 884a193b78
commit 392328ed5d
2 changed files with 53 additions and 1 deletions

View File

@@ -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<SessionConfiguration> {
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, &current_environments)
}
pub(crate) async fn environment_ready(

View File

@@ -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