Pair thread environment settings (#26687)

## Why

Thread cwd and environment selections are a single logical setting in
core: updating one without the other can silently desynchronize the
next-turn execution context. This change makes that relationship
explicit in the internal thread settings flow while preserving the
existing app-server public API shape.

## What changed

- Moved the cwd/environment pair through internal
`ThreadSettingsOverrides.environment_settings` instead of a top-level
internal `cwd` field.
- Kept `thread/settings/update` public params unchanged, with app-server
translating top-level `cwd` into the paired internal settings shape.
- Moved `Op::UserInput` environment overrides into thread settings so
user turns and settings updates use the same core path.
- Updated core, app-server, MCP, memories, sample, and test callsites to
construct the paired settings shape.

## Verification

- `git diff --check`
- Local test run starting after PR creation.
This commit is contained in:
pakrym-oai
2026-06-08 13:55:15 -07:00
committed by GitHub
parent f9a680b907
commit f3c1283411
90 changed files with 521 additions and 820 deletions
-3
View File
@@ -458,7 +458,6 @@ async fn send_input_submits_user_message() {
let expected = (
thread_id,
Op::UserInput {
environments: None,
items: vec![UserInput::Text {
text: "hello from tests".to_string(),
text_elements: Vec::new(),
@@ -797,7 +796,6 @@ async fn spawn_agent_creates_thread_and_sends_prompt() {
let expected = (
thread_id,
Op::UserInput {
environments: None,
items: vec![UserInput::Text {
text: "spawned".to_string(),
text_elements: Vec::new(),
@@ -1017,7 +1015,6 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
let expected = (
child_thread_id,
Op::UserInput {
environments: None,
items: vec![UserInput::Text {
text: "child task".to_string(),
text_elements: Vec::new(),
-1
View File
@@ -196,7 +196,6 @@ pub(crate) async fn run_codex_thread_one_shot(
// Send the initial input to kick off the one-shot turn.
io.submit(Op::UserInput {
environments: None,
items: input,
final_output_json_schema,
responsesapi_client_metadata: None,
+14 -5
View File
@@ -32,6 +32,7 @@ use codex_protocol::protocol::ThreadMemoryMode;
use codex_protocol::protocol::ThreadSource;
use codex_protocol::protocol::TokenUsageInfo;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::TurnEnvironmentSelections;
use codex_protocol::protocol::W3cTraceContext;
use codex_protocol::user_input::UserInput;
use codex_thread_store::StoredThread;
@@ -59,7 +60,7 @@ pub struct ThreadConfigSnapshot {
pub approvals_reviewer: ApprovalsReviewer,
pub permission_profile: PermissionProfile,
pub active_permission_profile: Option<ActivePermissionProfile>,
pub cwd: AbsolutePathBuf,
pub environments: TurnEnvironmentSelections,
pub workspace_roots: Vec<AbsolutePathBuf>,
pub profile_workspace_roots: Vec<AbsolutePathBuf>,
pub ephemeral: bool,
@@ -114,10 +115,18 @@ impl TryStartTurnIfIdleError {
}
impl ThreadConfigSnapshot {
pub fn cwd(&self) -> &AbsolutePathBuf {
&self.environments.legacy_fallback_cwd
}
pub fn environment_selections(&self) -> &[TurnEnvironmentSelection] {
&self.environments.environments
}
pub fn sandbox_policy(&self) -> SandboxPolicy {
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
&self.permission_profile,
self.cwd.as_path(),
self.cwd().as_path(),
)
}
}
@@ -125,7 +134,7 @@ impl ThreadConfigSnapshot {
/// Thread settings overrides that app-server validates before starting a turn.
#[derive(Clone, Default)]
pub struct CodexThreadSettingsOverrides {
pub cwd: Option<AbsolutePathBuf>,
pub environments: Option<TurnEnvironmentSelections>,
pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub approval_policy: Option<AskForApproval>,
@@ -330,7 +339,7 @@ impl CodexThread {
overrides: CodexThreadSettingsOverrides,
) -> SessionSettingsUpdate {
let CodexThreadSettingsOverrides {
cwd,
environments,
workspace_roots,
profile_workspace_roots,
approval_policy,
@@ -357,7 +366,7 @@ impl CodexThread {
};
SessionSettingsUpdate {
cwd,
environments,
workspace_roots,
profile_workspace_roots,
approval_policy,
+11 -3
View File
@@ -721,19 +721,27 @@ async fn run_review_on_session(
.await
.unwrap_or_default();
let guardian_permission_profile = PermissionProfile::read_only();
let parent_turn_environments = params.parent_turn.environments.to_selections();
let parent_turn_legacy_fallback_cwd = params
.parent_turn
.environments
.primary()
.map(|environment| environment.cwd.clone())
.unwrap_or_else(|| params.parent_turn.config.cwd.clone());
let submit_result = run_before_review_deadline(
deadline,
params.external_cancel.as_ref(),
Box::pin(review_session.codex.submit(Op::UserInput {
items: prompt_items.items,
environments: None,
final_output_json_schema: Some(params.schema.clone()),
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
#[allow(deprecated)]
cwd: Some(params.parent_turn.cwd.clone()),
environments: Some(codex_protocol::protocol::TurnEnvironmentSelections::new(
parent_turn_legacy_fallback_cwd,
parent_turn_environments,
)),
approval_policy: Some(AskForApproval::Never),
sandbox_policy: None,
permission_profile: Some(guardian_permission_profile),
+4 -5
View File
@@ -122,7 +122,7 @@ async fn thread_settings_update(
thread_settings: ThreadSettingsOverrides,
) -> SessionSettingsUpdate {
let ThreadSettingsOverrides {
cwd,
environments,
workspace_roots,
profile_workspace_roots,
approval_policy,
@@ -151,7 +151,7 @@ async fn thread_settings_update(
}
};
SessionSettingsUpdate {
cwd,
environments,
workspace_roots,
profile_workspace_roots,
approval_policy,
@@ -173,6 +173,7 @@ async fn thread_settings_applied_event(sess: &Session) -> EventMsg {
let state = sess.state.lock().await;
state.session_configuration.thread_config_snapshot()
};
let cwd = snapshot.cwd().clone();
EventMsg::ThreadSettingsApplied(ThreadSettingsAppliedEvent {
thread_settings: ThreadSettingsSnapshot {
model: snapshot.model,
@@ -182,7 +183,7 @@ async fn thread_settings_applied_event(sess: &Session) -> EventMsg {
approvals_reviewer: snapshot.approvals_reviewer,
permission_profile: snapshot.permission_profile,
active_permission_profile: snapshot.active_permission_profile,
cwd: snapshot.cwd,
cwd,
reasoning_effort: snapshot.reasoning_effort,
reasoning_summary: snapshot.reasoning_summary,
personality: snapshot.personality,
@@ -200,7 +201,6 @@ pub(super) async fn user_input_or_turn_inner(
) {
let Op::UserInput {
items,
environments,
final_output_json_schema,
responsesapi_client_metadata,
additional_context,
@@ -216,7 +216,6 @@ pub(super) async fn user_input_or_turn_inner(
SessionSettingsUpdate::default()
};
updates.final_output_json_schema = Some(final_output_json_schema);
updates.environments = environments;
let Ok(current_context) = sess.new_turn_with_sub_id(sub_id.clone(), updates).await else {
// new_turn_with_sub_id already emits the error event.
+11 -6
View File
@@ -116,6 +116,7 @@ use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::protocol::TurnContextItem;
use codex_protocol::protocol::TurnContextNetworkItem;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::TurnEnvironmentSelections;
use codex_protocol::protocol::W3cTraceContext;
use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile;
@@ -602,11 +603,13 @@ impl Codex {
approvals_reviewer: config.approvals_reviewer,
permission_profile_state: session_permission_profile_state_from_config(&config)?,
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
environments: TurnEnvironmentSelections::new(
config.cwd.clone(),
environment_selections.to_selections(),
),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: environment_selections.to_selections(),
original_config_do_not_use: Arc::clone(&config),
metrics_service_name,
app_server_client_name: None,
@@ -814,7 +817,10 @@ impl Codex {
pub(crate) async fn thread_environment_selections(&self) -> Vec<TurnEnvironmentSelection> {
let state = self.session.state.lock().await;
state.session_configuration.environments.clone()
state
.session_configuration
.environment_selections()
.to_vec()
}
pub(crate) fn state_db(&self) -> Option<state_db::StateDbHandle> {
@@ -1114,7 +1120,6 @@ impl Session {
self,
self.next_internal_sub_id(),
Op::UserInput {
environments: None,
items: vec![UserInput::Text {
text,
text_elements: Vec::new(),
@@ -1414,12 +1419,12 @@ impl Session {
.then(|| Self::build_effective_session_config(&state.session_configuration));
let new_config =
notify_config_contributors.then(|| Self::build_effective_session_config(&updated));
let previous_cwd = state.session_configuration.cwd.clone();
let previous_cwd = state.session_configuration.cwd().clone();
let previous_permission_profile = state.session_configuration.permission_profile();
let updated_permission_profile = updated.permission_profile();
let permission_profile_changed =
previous_permission_profile != updated_permission_profile;
let next_cwd = updated.cwd.clone();
let next_cwd = updated.cwd().clone();
let codex_home = updated.codex_home.clone();
let session_source = updated.session_source.clone();
state.session_configuration = updated;
+37 -33
View File
@@ -12,6 +12,7 @@ use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::ThreadSource;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::TurnEnvironmentSelections;
use std::sync::OnceLock;
use tokio::sync::Semaphore;
@@ -75,11 +76,9 @@ pub(crate) struct SessionConfiguration {
pub(super) permission_profile_state: PermissionProfileState,
pub(super) windows_sandbox_level: WindowsSandboxLevel,
/// Absolute working directory that should be treated as the *root* of the
/// session. All relative paths supplied by the model as well as the
/// execution sandbox are resolved against this directory **instead** of
/// the process-wide current working directory.
pub(super) cwd: AbsolutePathBuf,
/// Sticky thread-level environment selections plus the legacy cwd used
/// when a turn does not select an environment.
pub(super) environments: TurnEnvironmentSelections,
/// Thread-scoped runtime workspace roots for materializing symbolic
/// workspace permissions at session runtime.
pub(super) workspace_roots: Vec<AbsolutePathBuf>,
@@ -87,8 +86,6 @@ pub(crate) struct SessionConfiguration {
pub(super) codex_home: AbsolutePathBuf,
/// Optional user-facing name for the thread, updated during the session.
pub(super) thread_name: Option<String>,
/// Sticky environments for turns that do not provide a turn-local override.
pub(super) environments: Vec<TurnEnvironmentSelection>,
// TODO(pakrym): Remove config from here
pub(super) original_config_do_not_use: Arc<Config>,
@@ -110,6 +107,14 @@ pub(crate) struct SessionConfiguration {
}
impl SessionConfiguration {
pub(super) fn cwd(&self) -> &AbsolutePathBuf {
&self.environments.legacy_fallback_cwd
}
pub(super) fn environment_selections(&self) -> &[TurnEnvironmentSelection] {
&self.environments.environments
}
pub(crate) fn codex_home(&self) -> &AbsolutePathBuf {
&self.codex_home
}
@@ -153,7 +158,7 @@ impl SessionConfiguration {
let permission_profile = self.permission_profile();
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
&permission_profile,
&self.cwd,
self.cwd(),
)
}
@@ -176,7 +181,7 @@ impl SessionConfiguration {
approvals_reviewer: self.approvals_reviewer,
permission_profile: self.permission_profile(),
active_permission_profile: self.active_permission_profile(),
cwd: self.cwd.clone(),
environments: self.environments.clone(),
workspace_roots: self.workspace_roots.clone(),
profile_workspace_roots: self.profile_workspace_roots().to_vec(),
ephemeral: self.original_config_do_not_use.ephemeral,
@@ -199,11 +204,11 @@ impl SessionConfiguration {
let legacy_file_system_projection =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
&current_sandbox_policy,
&self.cwd,
self.cwd(),
&current_file_system_sandbox_policy,
);
let file_system_policy_matches_legacy = current_file_system_sandbox_policy
.is_semantically_equivalent_to(&legacy_file_system_projection, &self.cwd);
.is_semantically_equivalent_to(&legacy_file_system_projection, self.cwd());
let file_system_policy_has_rebindable_project_root_write =
current_file_system_sandbox_policy
.entries
@@ -249,18 +254,21 @@ impl SessionConfiguration {
next_configuration.windows_sandbox_level = windows_sandbox_level;
}
let absolute_cwd = updates.cwd.clone().unwrap_or_else(|| self.cwd.clone());
let cwd_changed = absolute_cwd.as_path() != self.cwd.as_path();
next_configuration.cwd = absolute_cwd;
let current_cwd = self.cwd().clone();
let next_environments = updates
.environments
.clone()
.unwrap_or_else(|| self.environments.clone());
let cwd_changed = next_environments.legacy_fallback_cwd.as_path() != current_cwd.as_path();
next_configuration.environments = next_environments;
if let Some(workspace_roots) = updates.workspace_roots.clone() {
next_configuration.workspace_roots = workspace_roots;
} else if cwd_changed && self.workspace_roots.contains(&self.cwd) {
} else if cwd_changed && self.workspace_roots.contains(&current_cwd) {
let mut retargeted_workspace_roots =
Vec::with_capacity(next_configuration.workspace_roots.len());
for root in &self.workspace_roots {
let root = if root == &self.cwd {
next_configuration.cwd.clone()
let root = if root == &current_cwd {
next_configuration.cwd().clone()
} else {
root.clone()
};
@@ -317,7 +325,7 @@ impl SessionConfiguration {
let file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
&sandbox_policy,
&next_configuration.cwd,
next_configuration.cwd(),
&current_file_system_sandbox_policy,
);
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
@@ -340,7 +348,7 @@ impl SessionConfiguration {
let file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
&current_sandbox_policy,
&next_configuration.cwd,
next_configuration.cwd(),
&current_file_system_sandbox_policy,
);
next_configuration
@@ -401,7 +409,7 @@ impl SessionConfiguration {
#[derive(Default, Clone)]
pub(crate) struct SessionSettingsUpdate {
pub(crate) cwd: Option<AbsolutePathBuf>,
pub(crate) environments: Option<TurnEnvironmentSelections>,
pub(crate) workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub(crate) profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub(crate) approval_policy: Option<AskForApproval>,
@@ -414,10 +422,6 @@ pub(crate) struct SessionSettingsUpdate {
pub(crate) reasoning_summary: Option<ReasoningSummaryConfig>,
pub(crate) service_tier: Option<Option<String>>,
pub(crate) final_output_json_schema: Option<Option<Value>>,
/// Turn-local environment override. `None` inherits the sticky thread
/// environments stored on `SessionConfiguration`; `Some([])` explicitly
/// disables environments for this turn.
pub(crate) environments: Option<Vec<TurnEnvironmentSelection>>,
pub(crate) personality: Option<Personality>,
pub(crate) app_server_client_name: Option<String>,
pub(crate) app_server_client_version: Option<String>,
@@ -634,7 +638,7 @@ impl Session {
Arc::clone(&environment_manager),
Arc::clone(&plugins_manager),
Arc::clone(&skills_manager),
session_configuration.environments.clone(),
session_configuration.environment_selections().to_vec(),
)
.instrument(info_span!(
"session_init.plugin_skill_warmup",
@@ -686,7 +690,7 @@ impl Session {
nickname: session_configuration.session_source.get_nickname(),
agent_role: session_configuration.session_source.get_agent_role(),
session_source: session_configuration.session_source.clone(),
cwd: session_configuration.cwd.to_path_buf(),
cwd: session_configuration.cwd().to_path_buf(),
rollout_path: rollout_path.clone(),
model: session_configuration.collaboration_mode.model().to_string(),
provider_name: config.model_provider_id.clone(),
@@ -790,7 +794,7 @@ impl Session {
/*inc*/ 1,
&[(
"is_git",
if get_git_repo_root(&session_configuration.cwd).is_some() {
if get_git_repo_root(session_configuration.cwd()).is_some() {
"true"
} else {
"false"
@@ -809,7 +813,7 @@ impl Session {
config.permissions.approval_policy.value(),
config
.permissions
.legacy_sandbox_policy(session_configuration.cwd.as_path()),
.legacy_sandbox_policy(session_configuration.cwd().as_path()),
mcp_servers.keys().map(String::as_str).collect(),
);
@@ -844,7 +848,7 @@ impl Session {
ShellSnapshot::start_snapshotting(
config.codex_home.clone(),
thread_id,
session_configuration.cwd.clone(),
session_configuration.cwd().clone(),
&mut default_shell,
session_telemetry.clone(),
state_db_ctx.clone(),
@@ -1089,7 +1093,7 @@ impl Session {
approvals_reviewer: session_configuration.approvals_reviewer,
permission_profile: session_configuration.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(),
initial_messages,
network_proxy: session_network_proxy.filter(|_| {
@@ -1135,7 +1139,7 @@ impl Session {
}
let turn_environment = crate::environment_selection::resolve_environment_selections(
sess.services.environment_manager.as_ref(),
&session_configuration.environments,
session_configuration.environment_selections(),
)
.map_err(|err| {
CodexErr::InvalidRequest(err.to_string().replace(
@@ -1152,7 +1156,7 @@ impl Session {
),
None => McpRuntimeContext::new(
Arc::clone(&sess.services.environment_manager),
session_configuration.cwd.to_path_buf(),
session_configuration.cwd().to_path_buf(),
),
};
let (mcp_connection_manager, cancel_token) = McpConnectionManager::new(
+135 -127
View File
@@ -22,6 +22,7 @@ use codex_config::RequirementSource;
use codex_config::Sourced;
use codex_config::loader::project_trust_key;
use codex_config::types::ToolSuggestDisabledTool;
use core_test_support::test_codex::local_selections;
use codex_features::Feature;
use codex_login::CodexAuth;
@@ -53,6 +54,7 @@ use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::NonSteerableTurnKind;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::TurnEnvironmentSelections;
use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile;
use tracing::Span;
@@ -142,6 +144,7 @@ use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server;
use core_test_support::test_codex::local;
use core_test_support::test_codex::test_codex;
use core_test_support::test_path_buf;
use core_test_support::tracing::install_test_tracing;
@@ -2392,7 +2395,8 @@ async fn session_permission_profile_rebinds_runtime_workspace_roots() -> anyhow:
);
let mut session_configuration = make_session_configuration_for_tests().await;
session_configuration.cwd = config.cwd.clone();
session_configuration.environments =
TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new());
session_configuration.workspace_roots = config.workspace_roots.clone();
session_configuration.permission_profile_state = session_permission_profile_state;
@@ -2404,8 +2408,8 @@ async fn session_permission_profile_rebinds_runtime_workspace_roots() -> anyhow:
..Default::default()
})?;
let updated_policy = updated.file_system_sandbox_policy();
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd.as_path()));
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd.as_path()));
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd().as_path()));
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd().as_path()));
Ok(())
}
@@ -2437,7 +2441,6 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
initial
.codex
.submit(Op::UserInput {
environments: None,
items: vec![UserInput::Text {
text: "fork seed".into(),
text_elements: Vec::new(),
@@ -2483,7 +2486,6 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
forked
.thread
.submit(Op::UserInput {
environments: None,
items: vec![UserInput::Text {
text: "after fork".into(),
text_elements: Vec::new(),
@@ -3157,11 +3159,10 @@ async fn set_rate_limits_retains_previous_credits() {
approvals_reviewer: config.approvals_reviewer,
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: Vec::new(),
original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None,
app_server_client_name: None,
@@ -3265,11 +3266,10 @@ async fn set_rate_limits_updates_plan_type_when_present() {
approvals_reviewer: config.approvals_reviewer,
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: Vec::new(),
original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None,
app_server_client_name: None,
@@ -3797,11 +3797,10 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
approvals_reviewer: config.approvals_reviewer,
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: Vec::new(),
original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None,
app_server_client_name: None,
@@ -3920,7 +3919,8 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd
let project_root = project_root.abs();
let docs_dir = docs_dir.abs();
session_configuration.cwd = original_cwd.abs();
session_configuration.environments =
TurnEnvironmentSelections::new(original_cwd.abs(), Vec::new());
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
@@ -3954,7 +3954,7 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd
let updated = session_configuration
.apply(&SessionSettingsUpdate {
cwd: Some(project_root),
environments: Some(TurnEnvironmentSelections::new(project_root, Vec::new())),
..Default::default()
})
.expect("cwd-only update should succeed");
@@ -3969,7 +3969,8 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd
async fn session_configuration_apply_permission_profile_preserves_existing_deny_read_entries() {
let mut session_configuration = make_session_configuration_for_tests().await;
let cwd = tempfile::tempdir().expect("create temp dir");
session_configuration.cwd = cwd.path().abs();
session_configuration.environments =
TurnEnvironmentSelections::new(cwd.path().abs(), Vec::new());
let workspace_policy = SandboxPolicy::new_workspace_write_policy();
let deny_entry = FileSystemSandboxEntry {
@@ -3981,7 +3982,7 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_
let mut existing_file_system_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&workspace_policy,
session_configuration.cwd.as_path(),
session_configuration.cwd().as_path(),
);
existing_file_system_policy.glob_scan_max_depth = Some(2);
existing_file_system_policy.entries.push(deny_entry.clone());
@@ -3997,7 +3998,7 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_
let requested_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&workspace_policy,
session_configuration.cwd.as_path(),
session_configuration.cwd().as_path(),
);
let permission_profile = codex_protocol::models::PermissionProfile::from_runtime_permissions(
&requested_file_system_policy,
@@ -4024,7 +4025,8 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_
async fn session_configuration_apply_permission_profile_accepts_direct_write_roots() {
let mut session_configuration = make_session_configuration_for_tests().await;
let cwd = tempfile::tempdir().expect("create cwd");
session_configuration.cwd = cwd.path().abs();
session_configuration.environments =
TurnEnvironmentSelections::new(cwd.path().abs(), Vec::new());
let external_write_dir = tempfile::tempdir().expect("create external write root");
let external_write_path = AbsolutePathBuf::from_absolute_path(
codex_utils_absolute_path::canonicalize_preserving_symlinks(external_write_dir.path())
@@ -4100,8 +4102,8 @@ async fn session_configuration_apply_rebinds_symbolic_profile_to_updated_workspa
.expect("permission profile update should succeed");
let updated_policy = updated.file_system_sandbox_policy();
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd.as_path()));
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd.as_path()));
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd().as_path()));
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd().as_path()));
assert_eq!(
updated.active_permission_profile(),
Some(ActivePermissionProfile::new("dev"))
@@ -4118,7 +4120,8 @@ async fn session_configuration_apply_retargets_implicit_workspace_root_on_cwd_up
let old_root = old_root.path().abs();
let new_root = new_root.path().abs();
let extra_root = extra_root.path().abs();
session_configuration.cwd = old_root.clone();
session_configuration.environments =
TurnEnvironmentSelections::new(old_root.clone(), Vec::new());
session_configuration.workspace_roots = vec![old_root.clone(), extra_root.clone()];
let file_system_sandbox_policy =
@@ -4138,7 +4141,7 @@ async fn session_configuration_apply_retargets_implicit_workspace_root_on_cwd_up
let updated = session_configuration
.apply(&SessionSettingsUpdate {
cwd: Some(new_root.clone()),
environments: Some(TurnEnvironmentSelections::new(new_root.clone(), Vec::new())),
..Default::default()
})
.expect("cwd-only update should succeed");
@@ -4148,9 +4151,9 @@ async fn session_configuration_apply_retargets_implicit_workspace_root_on_cwd_up
vec![new_root.clone(), extra_root.clone()]
);
let updated_policy = updated.file_system_sandbox_policy();
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd.as_path()));
assert!(updated_policy.can_write_path_with_cwd(extra_root.as_path(), updated.cwd.as_path()));
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd.as_path()));
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd().as_path()));
assert!(updated_policy.can_write_path_with_cwd(extra_root.as_path(), updated.cwd().as_path()));
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd().as_path()));
}
#[tokio::test]
@@ -4353,8 +4356,9 @@ async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_upda
let workspace = tempfile::tempdir().expect("create temp dir");
let original_cwd = workspace.path().join("repo-a").abs();
let project_root = workspace.path().join("repo-b").abs();
session_configuration.cwd = original_cwd.clone();
session_configuration.workspace_roots = vec![session_configuration.cwd.clone()];
session_configuration.environments =
TurnEnvironmentSelections::new(original_cwd.clone(), Vec::new());
session_configuration.workspace_roots = vec![session_configuration.cwd().clone()];
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
@@ -4363,7 +4367,7 @@ async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_upda
};
let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&sandbox_policy,
&session_configuration.cwd,
session_configuration.cwd(),
);
session_configuration
.set_permission_profile_for_tests(
@@ -4377,7 +4381,10 @@ async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_upda
let updated = session_configuration
.apply(&SessionSettingsUpdate {
cwd: Some(project_root.clone()),
environments: Some(TurnEnvironmentSelections::new(
project_root.clone(),
Vec::new(),
)),
..Default::default()
})
.expect("cwd-only update should succeed");
@@ -4386,13 +4393,13 @@ async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_upda
assert!(
updated
.file_system_sandbox_policy()
.can_write_path_with_cwd(project_root.as_path(), updated.cwd.as_path()),
.can_write_path_with_cwd(project_root.as_path(), updated.cwd().as_path()),
"cwd-only update should keep the new cwd writable"
);
assert!(
!updated
.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()),
"cwd-only update should not keep the old implicit cwd writable"
);
}
@@ -4408,7 +4415,8 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up
let original_cwd = original_cwd.abs();
let next_cwd = next_cwd.abs();
session_configuration.cwd = original_cwd.clone();
session_configuration.environments =
TurnEnvironmentSelections::new(original_cwd.clone(), Vec::new());
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
@@ -4435,7 +4443,7 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up
let updated = session_configuration
.apply(&SessionSettingsUpdate {
cwd: Some(next_cwd.clone()),
environments: Some(TurnEnvironmentSelections::new(next_cwd.clone(), Vec::new())),
..Default::default()
})
.expect("cwd-only update should succeed");
@@ -4447,13 +4455,13 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up
assert!(
updated
.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"
);
assert!(
!updated
.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"
);
}
@@ -4463,11 +4471,21 @@ async fn session_update_settings_does_not_rewrite_sticky_environment_cwds() {
let (session, turn_context) = make_session_and_context().await;
#[allow(deprecated)]
let updated_cwd = turn_context.cwd.join("project");
let current_environments = {
let state = session.state.lock().await;
state
.session_configuration
.environment_selections()
.to_vec()
};
std::fs::create_dir_all(updated_cwd.as_path()).expect("create project dir");
session
.update_settings(SessionSettingsUpdate {
cwd: Some(updated_cwd.clone()),
environments: Some(TurnEnvironmentSelections::new(
updated_cwd.clone(),
current_environments,
)),
..Default::default()
})
.await
@@ -4475,7 +4493,7 @@ async fn session_update_settings_does_not_rewrite_sticky_environment_cwds() {
let session_cwd = {
let state = session.state.lock().await;
state.session_configuration.cwd.clone()
state.session_configuration.cwd().clone()
};
let config = session.get_config().await;
let next_turn = session.new_default_turn().await;
@@ -4495,55 +4513,65 @@ async fn relative_cwd_update_without_environments_resolves_under_session_cwd() {
let (session, _turn_context) = make_session_and_context().await;
let original_cwd = {
let mut state = session.state.lock().await;
state.session_configuration.environments = Vec::new();
state.session_configuration.cwd.clone()
state.session_configuration.environments.environments = Vec::new();
state.session_configuration.cwd().clone()
};
let updated_cwd = original_cwd.join("project");
std::fs::create_dir_all(updated_cwd.as_path()).expect("create project dir");
session
.update_settings(SessionSettingsUpdate {
cwd: Some(updated_cwd.clone()),
environments: Some(TurnEnvironmentSelections::new(
updated_cwd.clone(),
Vec::new(),
)),
..Default::default()
})
.await
.expect("cwd update should succeed");
let state = session.state.lock().await;
assert_eq!(state.session_configuration.cwd, updated_cwd);
assert!(state.session_configuration.environments.is_empty());
assert_eq!(state.session_configuration.cwd(), &updated_cwd);
assert!(
state
.session_configuration
.environment_selections()
.is_empty()
);
}
#[tokio::test]
async fn cwd_update_does_not_rewrite_sticky_environment_cwd() {
async fn cwd_update_rewrites_sticky_environment_cwd() {
let (session, _turn_context) = make_session_and_context().await;
let (original_cwd, environment_cwd) = {
let (original_cwd, environment_cwd, environments) = {
let mut 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");
state.session_configuration.environments = vec![TurnEnvironmentSelection {
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
cwd: environment_cwd.clone(),
}];
(original_cwd, environment_cwd)
let environments = vec![local(environment_cwd.clone())];
state.session_configuration.environments.environments = environments.clone();
(original_cwd, environment_cwd, environments)
};
let updated_cwd = original_cwd.join("project");
std::fs::create_dir_all(updated_cwd.as_path()).expect("create project dir");
session
.update_settings(SessionSettingsUpdate {
cwd: Some(updated_cwd.clone()),
environments: Some(TurnEnvironmentSelections::new(
updated_cwd.clone(),
environments,
)),
..Default::default()
})
.await
.expect("cwd update should succeed");
let state = session.state.lock().await;
assert_eq!(state.session_configuration.cwd, updated_cwd);
assert_eq!(state.session_configuration.cwd(), &updated_cwd);
assert_eq!(
state.session_configuration.environments[0].cwd,
environment_cwd
state.session_configuration.environment_selections()[0].cwd,
updated_cwd
);
assert_ne!(environment_cwd, updated_cwd);
}
#[tokio::test]
@@ -4551,7 +4579,7 @@ async fn absolute_cwd_update_with_turn_environment_is_allowed() {
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
let absolute_cwd = {
let state = session.state.lock().await;
state.session_configuration.cwd.join("absolute-turn")
state.session_configuration.cwd().join("absolute-turn")
};
std::fs::create_dir_all(absolute_cwd.as_path()).expect("create absolute turn dir");
@@ -4559,11 +4587,10 @@ async fn absolute_cwd_update_with_turn_environment_is_allowed() {
.new_turn_with_sub_id(
"sub-1".to_string(),
SessionSettingsUpdate {
cwd: Some(absolute_cwd.clone()),
environments: Some(vec![TurnEnvironmentSelection {
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
cwd: absolute_cwd.clone(),
}]),
environments: Some(TurnEnvironmentSelections::new(
absolute_cwd.clone(),
vec![local(absolute_cwd.clone())],
)),
..Default::default()
},
)
@@ -4622,11 +4649,10 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
approvals_reviewer: config.approvals_reviewer,
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: Vec::new(),
original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None,
app_server_client_name: None,
@@ -4712,10 +4738,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
developer_instructions: None,
},
};
let default_environments = vec![TurnEnvironmentSelection {
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
cwd: config.cwd.clone(),
}];
let default_environments = vec![local(config.cwd.clone())];
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
collaboration_mode,
@@ -4733,11 +4756,10 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
approvals_reviewer: config.approvals_reviewer,
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: default_environments,
original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None,
app_server_client_name: None,
@@ -4751,7 +4773,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
user_shell_override: None,
};
let per_turn_config =
Session::build_per_turn_config(&session_configuration, session_configuration.cwd.clone());
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(),
@@ -4863,7 +4885,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
.skills_for_config(&skills_input, Some(Arc::clone(&skill_fs)))
.await,
);
let turn_environments = turn_environments_for_tests(&environment, &session_configuration.cwd);
let turn_environments = turn_environments_for_tests(&environment, session_configuration.cwd());
let turn_context = Session::make_turn_context(
thread_id,
SessionId::from(thread_id),
@@ -4880,7 +4902,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
&models_manager,
/*network*/ None,
turn_environments,
session_configuration.cwd.clone(),
session_configuration.cwd().clone(),
"turn_id".to_string(),
skills_outcome,
);
@@ -4948,10 +4970,7 @@ async fn make_session_with_config_and_rx(
developer_instructions: None,
},
};
let default_environments = vec![TurnEnvironmentSelection {
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
cwd: config.cwd.clone(),
}];
let default_environments = vec![local(config.cwd.clone())];
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
collaboration_mode,
@@ -4969,11 +4988,10 @@ async fn make_session_with_config_and_rx(
approvals_reviewer: config.approvals_reviewer,
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: default_environments,
original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None,
app_server_client_name: None,
@@ -5053,10 +5071,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
developer_instructions: None,
},
};
let default_environments = vec![TurnEnvironmentSelection {
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
cwd: config.cwd.clone(),
}];
let default_environments = vec![local(config.cwd.clone())];
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
collaboration_mode,
@@ -5074,11 +5089,10 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
approvals_reviewer: config.approvals_reviewer,
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: default_environments,
original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None,
app_server_client_name: None,
@@ -5937,7 +5951,6 @@ fn submission_dispatch_span_uses_debug_for_realtime_audio() {
fn op_kind_for_input_and_context_ops() {
assert_eq!(
Op::UserInput {
environments: None,
items: vec![],
final_output_json_schema: None,
responsesapi_client_metadata: None,
@@ -5969,12 +5982,11 @@ async fn user_turn_updates_approvals_reviewer() {
text: "hello".to_string(),
text_elements: Vec::new(),
}],
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(config.cwd.clone()),
environments: Some(local_selections(config.cwd.clone())),
approval_policy: Some(config.permissions.approval_policy.value()),
approvals_reviewer: Some(codex_config::types::ApprovalsReviewer::AutoReview),
sandbox_policy: Some(config.legacy_sandbox_policy()),
@@ -6013,10 +6025,10 @@ async fn turn_environments_set_primary_environment() {
.new_turn_with_sub_id(
"sub-1".to_string(),
SessionSettingsUpdate {
environments: Some(vec![TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: selected_cwd.clone(),
}]),
environments: Some(TurnEnvironmentSelections::new(
selected_cwd.clone(),
vec![local(selected_cwd.clone())],
)),
..Default::default()
},
)
@@ -6041,7 +6053,7 @@ async fn turn_environments_set_primary_environment() {
}
#[tokio::test]
async fn default_turn_overlays_session_cwd_onto_stored_thread_environments() {
async fn default_turn_does_not_overlay_legacy_fallback_cwd_onto_stored_thread_environments() {
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
let session_cwd = session.get_config().await.cwd.clone();
let selected_cwd =
@@ -6049,10 +6061,7 @@ async fn default_turn_overlays_session_cwd_onto_stored_thread_environments() {
{
let mut state = session.state.lock().await;
state.session_configuration.environments = vec![TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: selected_cwd.clone(),
}];
state.session_configuration.environments.environments = vec![local(selected_cwd.clone())];
}
let turn_context = session.new_default_turn().await;
@@ -6069,8 +6078,8 @@ async fn default_turn_overlays_session_cwd_onto_stored_thread_environments() {
));
#[allow(deprecated)]
let turn_cwd = turn_context.cwd.clone();
assert_eq!(turn_cwd, session_cwd);
assert_eq!(turn_context.config.cwd, session_cwd);
assert_eq!(turn_cwd, selected_cwd);
assert_eq!(turn_context.config.cwd, selected_cwd);
}
#[tokio::test]
@@ -6080,7 +6089,7 @@ async fn default_turn_honors_empty_stored_thread_environments() {
{
let mut state = session.state.lock().await;
state.session_configuration.environments = Vec::new();
state.session_configuration.environments.environments = Vec::new();
}
let turn_context = session.new_default_turn().await;
@@ -6143,7 +6152,10 @@ async fn empty_turn_environments_clear_primary_environment() {
.new_turn_with_sub_id(
"sub-1".to_string(),
SessionSettingsUpdate {
environments: Some(vec![]),
environments: Some(TurnEnvironmentSelections::new(
session.get_config().await.cwd.clone(),
vec![],
)),
..Default::default()
},
)
@@ -6170,10 +6182,13 @@ async fn unknown_turn_environment_returns_error() {
.new_turn_with_sub_id(
"sub-1".to_string(),
SessionSettingsUpdate {
environments: Some(vec![TurnEnvironmentSelection {
environment_id: "missing".to_string(),
cwd: original_configuration.cwd.clone(),
}]),
environments: Some(TurnEnvironmentSelections::new(
original_configuration.cwd().clone(),
vec![TurnEnvironmentSelection {
environment_id: "missing".to_string(),
cwd: original_configuration.cwd().clone(),
}],
)),
..Default::default()
},
)
@@ -6186,10 +6201,10 @@ async fn unknown_turn_environment_returns_error() {
};
assert!(matches!(err, CodexErr::InvalidRequest(_)));
assert!(err.to_string().contains("missing"));
assert_eq!(current_configuration.cwd, original_configuration.cwd);
assert_eq!(current_configuration.cwd(), original_configuration.cwd());
assert_eq!(
current_configuration.environments,
original_configuration.environments
current_configuration.environment_selections(),
original_configuration.environment_selections()
);
}
@@ -6205,16 +6220,13 @@ async fn duplicate_turn_environment_returns_error_without_mutating_session() {
.new_turn_with_sub_id(
"sub-1".to_string(),
SessionSettingsUpdate {
environments: Some(vec![
TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: original_configuration.cwd.clone(),
},
TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: original_configuration.cwd.join("second"),
},
]),
environments: Some(TurnEnvironmentSelections::new(
original_configuration.cwd().clone(),
vec![
local(original_configuration.cwd().clone()),
local(original_configuration.cwd().join("second")),
],
)),
..Default::default()
},
)
@@ -6227,10 +6239,10 @@ async fn duplicate_turn_environment_returns_error_without_mutating_session() {
};
assert!(matches!(err, CodexErr::InvalidRequest(_)));
assert!(err.to_string().contains("duplicate"));
assert_eq!(current_configuration.cwd, original_configuration.cwd);
assert_eq!(current_configuration.cwd(), original_configuration.cwd());
assert_eq!(
current_configuration.environments,
original_configuration.environments
current_configuration.environment_selections(),
original_configuration.environment_selections()
);
}
@@ -6789,10 +6801,7 @@ where
developer_instructions: None,
},
};
let default_environments = vec![TurnEnvironmentSelection {
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
cwd: config.cwd.clone(),
}];
let default_environments = vec![local(config.cwd.clone())];
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
collaboration_mode,
@@ -6810,11 +6819,10 @@ where
approvals_reviewer: config.approvals_reviewer,
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: default_environments,
original_config_do_not_use: Arc::clone(&config),
metrics_service_name: None,
app_server_client_name: None,
@@ -6828,7 +6836,7 @@ where
user_shell_override: None,
};
let per_turn_config =
Session::build_per_turn_config(&session_configuration, session_configuration.cwd.clone());
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(),
@@ -6940,7 +6948,7 @@ where
.skills_for_config(&skills_input, Some(Arc::clone(&skill_fs)))
.await,
);
let turn_environments = turn_environments_for_tests(&environment, &session_configuration.cwd);
let turn_environments = turn_environments_for_tests(&environment, session_configuration.cwd());
let turn_context = Arc::new(Session::make_turn_context(
thread_id,
SessionId::from(thread_id),
@@ -6957,7 +6965,7 @@ where
&models_manager,
/*network*/ None,
turn_environments,
session_configuration.cwd.clone(),
session_configuration.cwd().clone(),
"turn_id".to_string(),
skills_outcome,
));
+13 -38
View File
@@ -441,7 +441,7 @@ impl Session {
session_configuration: &SessionConfiguration,
) -> 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.permissions.approval_policy = session_configuration.approval_policy.clone();
config.workspace_roots = session_configuration.workspace_roots.clone();
@@ -587,19 +587,9 @@ impl Session {
let mut state = self.state.lock().await;
match state.session_configuration.clone().apply(&updates) {
Ok(next) => {
let mut effective_environments = updates
.environments
.clone()
.unwrap_or_else(|| next.environments.clone());
if updates.environments.is_none() {
Self::overlay_runtime_cwd_on_primary_environment(
&mut effective_environments,
&next.cwd,
);
}
let turn_environments =
self.resolve_turn_environments(&effective_environments)?;
let previous_cwd = state.session_configuration.cwd.clone();
self.resolve_turn_environments(next.environment_selections())?;
let previous_cwd = state.session_configuration.cwd().clone();
let previous_permission_profile =
state.session_configuration.permission_profile();
let next_permission_profile = next.permission_profile();
@@ -656,7 +646,7 @@ impl Session {
self.emit_config_changed_contributors(previous_config.as_ref(), new_config.as_ref());
self.maybe_refresh_shell_snapshot_for_cwd(
&previous_cwd,
&session_configuration.cwd,
session_configuration.cwd(),
&codex_home,
&session_source,
);
@@ -731,7 +721,7 @@ impl Session {
let cwd = primary_turn_environment
.as_ref()
.map(|turn_environment| turn_environment.cwd.clone())
.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 mcp_connection_manager = self.services.mcp_connection_manager.read().await;
@@ -871,29 +861,14 @@ impl Session {
let state = self.state.lock().await;
state.session_configuration.clone()
};
let mut effective_environments = session_configuration.environments.clone();
Self::overlay_runtime_cwd_on_primary_environment(
&mut effective_environments,
&session_configuration.cwd,
);
let turn_environments = match self.resolve_turn_environments(&effective_environments) {
Ok(turn_environments) => turn_environments,
Err(err) => {
warn!("failed to resolve stored session environments: {err}");
ResolvedTurnEnvironments::default()
}
};
let turn_environments =
match self.resolve_turn_environments(session_configuration.environment_selections()) {
Ok(turn_environments) => turn_environments,
Err(err) => {
warn!("failed to resolve stored session environments: {err}");
ResolvedTurnEnvironments::default()
}
};
(session_configuration, turn_environments)
}
fn overlay_runtime_cwd_on_primary_environment(
environments: &mut [TurnEnvironmentSelection],
runtime_cwd: &AbsolutePathBuf,
) {
if let Some(turn_environment) = environments.first_mut()
&& turn_environment.cwd != *runtime_cwd
{
turn_environment.cwd = runtime_cwd.clone();
}
}
}
-94
View File
@@ -21,7 +21,6 @@ use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::ThreadSource;
use codex_protocol::protocol::TurnStartedEvent;
use codex_protocol::protocol::UserMessageEvent;
use codex_protocol::user_input::UserInput;
use core_test_support::PathBufExt;
use core_test_support::PathExt;
use core_test_support::responses::mount_models_once;
@@ -343,99 +342,6 @@ async fn start_thread_rejects_explicit_local_environment_when_default_provider_i
assert!(manager.list_thread_ids().await.is_empty());
}
#[tokio::test]
async fn start_thread_uses_all_default_environments_from_codex_home() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
std::fs::write(
config.codex_home.join("environments.toml"),
r#"
default = "dev"
[[environments]]
id = "dev"
program = "ssh"
args = ["dev", "cd /tmp && true"]
"#,
)
.expect("write environments.toml");
let runtime_paths = codex_exec_server::ExecServerRuntimePaths::new(
std::env::current_exe().expect("current exe path"),
/*codex_linux_sandbox_exe*/ None,
)
.expect("runtime paths");
let environment_manager = Arc::new(
codex_exec_server::EnvironmentManager::from_codex_home(
config.codex_home.clone(),
Some(runtime_paths),
)
.await
.expect("environment manager"),
);
assert_eq!(
environment_manager.default_environment_ids(),
vec!["dev".to_string(), "local".to_string()]
);
let manager = ThreadManager::with_models_provider_and_home_for_tests(
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.to_path_buf(),
environment_manager,
);
let thread = manager
.start_thread(config)
.await
.expect("thread should start");
let prompt_items = crate::prompt_debug::build_prompt_input_from_session(
thread.thread.codex.session.as_ref(),
Vec::<UserInput>::new(),
)
.await
.expect("prompt input");
let environment_context = prompt_items
.iter()
.filter_map(|item| match item {
ResponseItem::Message { content, .. } => Some(content),
_ => None,
})
.flatten()
.find_map(|content| match content {
ContentItem::InputText { text } if text.contains("<environment_context>") => {
Some(text.as_str())
}
_ => None,
})
.expect("environment context prompt item");
assert!(environment_context.contains("<environments>"));
let cwd = thread.session_configured.cwd.display().to_string();
let dev_entry = format!(
r#"<environment id="dev">
<cwd>{cwd}</cwd>
<shell>"#
);
let local_entry = format!(
r#"<environment id="local">
<cwd>{cwd}</cwd>
<shell>"#
);
let dev_position = environment_context
.find(&dev_entry)
.expect("dev environment entry");
let local_position = environment_context
.find(&local_entry)
.expect("local environment entry");
assert!(dev_position < local_position);
assert!(!environment_context.contains("\n <cwd>"));
assert!(!environment_context.contains("\n <shell>"));
}
#[tokio::test]
async fn start_thread_keeps_internal_threads_hidden_from_normal_lookups() {
let temp_dir = tempdir().expect("tempdir");
@@ -2676,7 +2676,6 @@ async fn send_input_accepts_structured_items() {
.expect("send_input should succeed");
let expected = Op::UserInput {
environments: None,
items: vec![
UserInput::Mention {
name: "drive".to_string(),