Preserve environment configuration ownership (#38678)

## Why

Environment attachments can either inherit configuration from their thread or
provide their own. Later thread setting updates must refresh inherited
configuration without overwriting attachment-owned permissions and capability
roots.

## What changed

- Resolve each attachment's configuration when it is selected and retain
  whether it came from the thread or the attachment owner.
- Apply subsequent thread configuration updates only to thread-owned
  attachments, while preserving ownership across snapshots and child threads.
- Keep the resolved configuration with the environment selection so runtime
  consumers use a single canonical value.

## Testing

- Extend remote-environment coverage to verify that thread-owned permissions
  follow thread updates while owner-provided read-only permissions remain in
  effect.
- Cover configuration inheritance, attachment replacement, and owner-configured
  capability roots.

GitOrigin-RevId: fe70c4be5f151432b69bf4b141e316faa89036ae
This commit is contained in:
sayan-oai authored and copyberry committed 2026-08-15 00:43:44 +00:00
1 parent 233739e76a
commit 22bf16a37e
23 files changed
+343 -192

No files matched your search

+28 -8
View File
@@ -1,4 +1,5 @@
use super::*;
use crate::environment_selection::EnvironmentConfigOrigin;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::exec_policy::AllowPrefixRules;
use crate::shell_snapshot::ShellSnapshotFile;
@@ -15,6 +16,7 @@ use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::openai_models::MODEL_SPECIALTY_CYBER;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::protocol::EnvironmentConfig;
use codex_protocol::protocol::EnvironmentConfigState;
use codex_protocol::protocol::ErrorEvent;
use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::ThreadHistoryMode;
@@ -34,28 +36,44 @@ pub(crate) type ShellSnapshotTask = Shared<BoxFuture<'static, Option<Arc<ShellSn
#[derive(Clone)]
pub(crate) struct TurnEnvironment {
pub(crate) selection: TurnEnvironmentSelection,
pub(crate) config_origin: EnvironmentConfigOrigin,
pub(crate) environment: Arc<Environment>,
pub(crate) shell: Option<shell::Shell>,
pub(crate) config: EnvironmentConfig,
pub(crate) shell_snapshot: ShellSnapshotTask,
}
impl TurnEnvironment {
pub(crate) fn new(
selection: TurnEnvironmentSelection,
config_origin: EnvironmentConfigOrigin,
environment: Arc<Environment>,
shell: Option<shell::Shell>,
config: EnvironmentConfig,
) -> Self {
debug_assert!(matches!(selection.config, EnvironmentConfigState::Ready(_)));
Self {
selection,
config_origin,
environment,
shell,
config,
shell_snapshot: futures::future::ready(None).boxed().shared(),
}
}
pub(crate) fn config(&self) -> &EnvironmentConfig {
let EnvironmentConfigState::Ready(config) = &self.selection.config else {
unreachable!("ready turn environments always carry resolved configuration")
};
config
}
#[cfg(test)]
pub(crate) fn config_mut(&mut self) -> &mut EnvironmentConfig {
let EnvironmentConfigState::Ready(config) = &mut self.selection.config else {
unreachable!("ready turn environments always carry resolved configuration")
};
config
}
pub(crate) fn shell_snapshot(&self, cwd: &AbsolutePathBuf) -> Option<AbsolutePathBuf> {
if self.selection.cwd != PathUri::from_abs_path(cwd) {
return None;
@@ -75,11 +93,11 @@ impl TurnEnvironment {
}
pub(crate) fn permission_profile(&self) -> &PermissionProfile {
self.config.permission_profile.permission_profile()
self.config().permission_profile.permission_profile()
}
pub(crate) fn active_permission_profile(&self) -> Option<ActivePermissionProfile> {
self.config.permission_profile.active_permission_profile()
self.config().permission_profile.active_permission_profile()
}
pub(crate) fn permission_profile_with_workspace_roots(&self) -> PermissionProfile {
@@ -94,7 +112,8 @@ impl TurnEnvironment {
}
pub(crate) fn selection(&self) -> TurnEnvironmentSelection {
self.selection.clone()
self.config_origin
.into_input_selection(self.selection.clone())
}
}
@@ -106,7 +125,8 @@ impl std::fmt::Debug for TurnEnvironment {
.field("cwd", &self.selection.cwd)
.field("workspace_roots", &self.selection.workspace_roots)
.field("shell", &self.shell)
.field("config", &self.config)
.field("config", self.config())
.field("config_origin", &self.config_origin)
.finish_non_exhaustive()
}
}
@@ -690,7 +710,7 @@ impl Session {
{
self.services
.turn_environments
.update_environment_configs(&environment_config);
.update_thread_config(&environment_config);
}
if mcp_inputs_changed {
self.mark_mcp_runtime_dirty();