Separate thread settings from environment configuration (#39597)

## Why

Environment-owned permission profiles and workspace roots must remain effective
without being persisted or restored as thread-owned settings.

## What changed

- Keep thread settings snapshots and restore data separate from the effective
  configuration supplied by the primary environment.
- Use resolved environment permissions, active profiles, workspace roots, and
  network policy when building turn context and session events.
- Preserve environment selections when agents are evicted, resumed, or spawned,
  and validate owner configuration when it becomes ready.
- Start memory initialization only after the primary environment is configured.

## Testing

- Extend remote-environment and multi-agent tests to cover settings persistence,
  owner permission profiles, workspace roots, spawning, eviction, and resume.

GitOrigin-RevId: d82f126981cd0190233020bbdd61d96c70558868
This commit is contained in:
sayan-oai authored and copyberry committed 2026-08-20 05:29:12 +00:00
1 parent 8f4a48a6ad
commit d75c85f651
35 files changed
+564 -308

No files matched your search

+83 -31
View File
@@ -137,10 +137,6 @@ impl SessionConfiguration {
&self.codex_home
}
pub(super) fn permission_profile_state(&self) -> &PermissionProfileState {
&self.permission_profile_state
}
pub(super) fn inferred_environment_config(&self) -> EnvironmentConfig {
EnvironmentConfig {
allow_login_shell: self.allow_login_shell,
@@ -170,10 +166,6 @@ impl SessionConfiguration {
self.permission_profile_state.active_permission_profile()
}
pub(super) fn profile_workspace_roots(&self) -> &[AbsolutePathBuf] {
self.permission_profile_state.profile_workspace_roots()
}
pub(super) fn apply_permission_profile_to_permissions(
&self,
permissions: &mut crate::config::Permissions,
@@ -221,20 +213,26 @@ impl SessionConfiguration {
) -> ThreadConfigSnapshot {
let workspace_roots =
ThreadEnvironments::primary_workspace_roots_for(&environment_selections);
let permission_profile = ThreadEnvironments::primary_config_for(&environment_selections)
.map(|config| config.permission_profile.clone())
.unwrap_or_else(|| self.permission_profile_state.snapshot());
ThreadConfigSnapshot {
model: self.collaboration_mode.model().to_string(),
model_provider_id: self.original_config_do_not_use.model_provider_id.clone(),
service_tier: self.service_tier.clone(),
approval_policy: self.approval_policy.value(),
approvals_reviewer: self.approvals_reviewer,
permission_profile: self.materialized_permission_profile(&environment_selections),
active_permission_profile: self.active_permission_profile(),
permission_profile: permission_profile
.permission_profile()
.clone()
.materialize_project_roots_with_workspace_roots(&workspace_roots),
active_permission_profile: permission_profile.active_permission_profile(),
environments: TurnEnvironmentSelections::new(
self.legacy_fallback_cwd.clone(),
environment_selections,
),
workspace_roots,
profile_workspace_roots: self.profile_workspace_roots().to_vec(),
profile_workspace_roots: permission_profile.profile_workspace_roots().to_vec(),
ephemeral: self.original_config_do_not_use.ephemeral,
reasoning_effort: self.collaboration_mode.reasoning_effort(),
reasoning_summary: self.model_reasoning_summary,
@@ -249,6 +247,54 @@ impl SessionConfiguration {
}
}
/// Captures thread-owned settings for persistence and resume.
pub(super) fn thread_settings_snapshot(
&self,
environment_selections: &[TurnEnvironmentSelection],
) -> ThreadSettingsSnapshot {
ThreadSettingsSnapshot {
model: self.collaboration_mode.model().to_string(),
model_provider_id: self.original_config_do_not_use.model_provider_id.clone(),
service_tier: self.service_tier.clone(),
approval_policy: self.approval_policy.value(),
approvals_reviewer: self.approvals_reviewer,
permission_profile: self.materialized_permission_profile(environment_selections),
active_permission_profile: self.active_permission_profile(),
cwd: self.legacy_fallback_cwd.clone(),
reasoning_effort: self.collaboration_mode.reasoning_effort(),
reasoning_summary: self.model_reasoning_summary,
personality: self.personality,
collaboration_mode: self.collaboration_mode.clone(),
}
}
/// Captures thread-owned settings and their separately owned environments.
pub(super) fn restorable_thread_settings(
&self,
environment_selections: Vec<TurnEnvironmentSelection>,
) -> CodexThreadSettingsOverrides {
CodexThreadSettingsOverrides {
environments: Some(TurnEnvironmentSelections::new(
self.legacy_fallback_cwd.clone(),
environment_selections,
)),
profile_workspace_roots: Some(
self.permission_profile_state
.profile_workspace_roots()
.to_vec(),
),
approval_policy: Some(self.approval_policy.value()),
approvals_reviewer: Some(self.approvals_reviewer),
permission_profile: Some(self.permission_profile()),
active_permission_profile: self.active_permission_profile(),
summary: self.model_reasoning_summary,
service_tier: Some(self.service_tier.clone()),
collaboration_mode: Some(self.collaboration_mode.clone()),
personality: self.personality,
..Default::default()
}
}
pub(super) fn validate(
&self,
environments: &[TurnEnvironmentSelection],
@@ -258,7 +304,7 @@ impl SessionConfiguration {
ConstraintError::InvalidValue {
field_name: "environments",
candidate: "environment configuration".to_string(),
allowed: format!("valid selected capability roots ({error})"),
allowed: format!("valid environment configuration ({error})"),
requirement_source: codex_config::RequirementSource::Unknown,
}
})
@@ -281,9 +327,16 @@ impl SessionConfiguration {
return Ok(());
}
let permission_profile = ThreadEnvironments::primary_config_for(environments)
.map(|config| config.permission_profile.permission_profile())
.unwrap_or_else(|| self.permission_profile_state.permission_profile())
.clone()
.materialize_project_roots_with_workspace_roots(
&ThreadEnvironments::primary_workspace_roots_for(environments),
);
if self.approvals_reviewer == ApprovalsReviewer::AutoReview
&& !self
.file_system_sandbox_policy(environments)
&& !permission_profile
.file_system_sandbox_policy()
.has_full_disk_write_access()
&& self
.original_config_do_not_use
@@ -1392,33 +1445,32 @@ impl Session {
// Dispatch the SessionConfiguredEvent first and then report any errors.
// If resuming, include converted initial messages in the payload so UIs can render them immediately.
let initial_messages = initial_history.get_event_msgs();
let thread_config =
session_configuration.thread_config_snapshot(turn_environments.selections());
let events = std::iter::once(Event {
id: INITIAL_SUBMIT_ID.to_owned(),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
session_id,
thread_id,
forked_from_id,
parent_thread_id,
thread_source: session_configuration.thread_source.clone(),
cwd: thread_config.cwd().clone(),
forked_from_id: thread_config.forked_from_thread_id,
parent_thread_id: thread_config.parent_thread_id,
thread_source: thread_config.thread_source,
thread_name: session_configuration.thread_name.clone(),
model: session_configuration.collaboration_mode.model().to_string(),
model_provider_id: config.model_provider_id.clone(),
service_tier: session_configuration.service_tier.clone(),
approval_policy: session_configuration.approval_policy.value(),
approvals_reviewer: session_configuration.approvals_reviewer,
permission_profile: session_configuration
.materialized_permission_profile(environment_selections),
active_permission_profile: session_configuration.active_permission_profile(),
cwd: session_configuration.cwd().clone(),
reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(),
initial_messages,
model: thread_config.model,
model_provider_id: thread_config.model_provider_id,
service_tier: thread_config.service_tier,
approval_policy: thread_config.approval_policy,
approvals_reviewer: thread_config.approvals_reviewer,
network_proxy: session_network_proxy.filter(|_| {
Self::managed_network_proxy_active_for_permission_profile(
session_configuration
.permission_profile_state()
.permission_profile(),
&thread_config.permission_profile,
)
}),
permission_profile: thread_config.permission_profile,
active_permission_profile: thread_config.active_permission_profile,
reasoning_effort: thread_config.reasoning_effort,
initial_messages,
rollout_path,
}),
})