From f3c1283411edadcc0522bea376d0adc6961d5ca3 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Mon, 8 Jun 2026 13:55:15 -0700 Subject: [PATCH] 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. --- .../src/protocol/v2/turn.rs | 2 +- .../app-server/src/bespoke_event_handling.rs | 4 +- codex-rs/app-server/src/request_processors.rs | 2 + .../src/request_processors/apps_processor.rs | 2 +- .../request_processors/thread_lifecycle.rs | 5 +- .../request_processors/thread_processor.rs | 15 +- .../thread_processor_tests.rs | 3 +- .../src/request_processors/thread_summary.rs | 46 +-- .../src/request_processors/turn_processor.rs | 55 +++- codex-rs/core/src/agent/control_tests.rs | 3 - codex-rs/core/src/codex_delegate.rs | 1 - codex-rs/core/src/codex_thread.rs | 19 +- codex-rs/core/src/guardian/review_session.rs | 14 +- codex-rs/core/src/session/handlers.rs | 9 +- codex-rs/core/src/session/mod.rs | 17 +- codex-rs/core/src/session/session.rs | 70 ++--- codex-rs/core/src/session/tests.rs | 262 +++++++++--------- codex-rs/core/src/session/turn_context.rs | 51 +--- codex-rs/core/src/thread_manager_tests.rs | 94 ------- .../src/tools/handlers/multi_agents_tests.rs | 1 - codex-rs/core/tests/common/test_codex.rs | 18 +- codex-rs/core/tests/suite/abort_tasks.rs | 5 - .../core/tests/suite/additional_context.rs | 9 - codex-rs/core/tests/suite/apply_patch_cli.rs | 27 +- codex-rs/core/tests/suite/approvals.rs | 13 +- codex-rs/core/tests/suite/auto_review.rs | 4 +- codex-rs/core/tests/suite/client.rs | 40 +-- .../core/tests/suite/client_websockets.rs | 2 - codex-rs/core/tests/suite/code_mode.rs | 6 +- .../tests/suite/collaboration_instructions.rs | 24 +- codex-rs/core/tests/suite/compact.rs | 39 +-- codex-rs/core/tests/suite/compact_remote.rs | 63 +---- .../core/tests/suite/compact_remote_parity.rs | 1 - .../core/tests/suite/compact_resume_fork.rs | 4 +- codex-rs/core/tests/suite/exec_policy.rs | 7 +- codex-rs/core/tests/suite/fork_thread.rs | 2 - codex-rs/core/tests/suite/guardian_review.rs | 4 +- codex-rs/core/tests/suite/hooks.rs | 2 - codex-rs/core/tests/suite/image_rollout.rs | 7 +- codex-rs/core/tests/suite/items.rs | 13 +- codex-rs/core/tests/suite/json_result.rs | 4 +- .../core/tests/suite/mcp_turn_metadata.rs | 4 +- .../tests/suite/model_runtime_selectors.rs | 2 - codex-rs/core/tests/suite/model_switching.rs | 4 +- .../core/tests/suite/model_visible_layout.rs | 21 +- codex-rs/core/tests/suite/models_cache_ttl.rs | 4 +- .../core/tests/suite/models_etag_responses.rs | 4 +- codex-rs/core/tests/suite/otel.rs | 22 -- codex-rs/core/tests/suite/override_updates.rs | 3 +- codex-rs/core/tests/suite/pending_input.rs | 7 +- .../core/tests/suite/permissions_messages.rs | 15 - codex-rs/core/tests/suite/personality.rs | 4 +- codex-rs/core/tests/suite/plugins.rs | 3 - codex-rs/core/tests/suite/prompt_caching.rs | 26 +- codex-rs/core/tests/suite/quota_exceeded.rs | 1 - .../core/tests/suite/realtime_conversation.rs | 3 - codex-rs/core/tests/suite/remote_env.rs | 33 +-- codex-rs/core/tests/suite/remote_models.rs | 12 +- .../core/tests/suite/request_compression.rs | 2 - .../core/tests/suite/request_permissions.rs | 4 +- .../tests/suite/request_permissions_tool.rs | 4 +- .../core/tests/suite/request_user_input.rs | 10 +- .../suite/responses_api_proxy_headers.rs | 4 +- codex-rs/core/tests/suite/resume.rs | 7 - codex-rs/core/tests/suite/review.rs | 4 +- codex-rs/core/tests/suite/rmcp_client.rs | 4 +- .../tests/suite/safety_check_downgrade.rs | 4 +- codex-rs/core/tests/suite/search_tool.rs | 4 - codex-rs/core/tests/suite/shell_snapshot.rs | 13 +- codex-rs/core/tests/suite/skill_approval.rs | 4 +- codex-rs/core/tests/suite/skills.rs | 4 +- codex-rs/core/tests/suite/sqlite_state.rs | 5 +- .../suite/stream_error_allows_next_turn.rs | 2 - .../core/tests/suite/stream_no_completed.rs | 1 - .../tests/suite/subagent_notifications.rs | 4 +- codex-rs/core/tests/suite/tool_harness.rs | 16 +- codex-rs/core/tests/suite/tool_parallelism.rs | 7 +- codex-rs/core/tests/suite/tools.rs | 7 +- codex-rs/core/tests/suite/truncation.rs | 4 +- codex-rs/core/tests/suite/unified_exec.rs | 22 +- .../suite/unified_exec_zsh_fork_approvals.rs | 4 +- .../core/tests/suite/user_notification.rs | 1 - codex-rs/core/tests/suite/user_shell_cmd.rs | 4 +- codex-rs/core/tests/suite/view_image.rs | 15 +- .../core/tests/suite/websocket_fallback.rs | 4 +- codex-rs/core/tests/suite/window_headers.rs | 1 - codex-rs/mcp-server/src/codex_tool_runner.rs | 2 - codex-rs/memories/write/src/runtime.rs | 1 - codex-rs/protocol/src/protocol.rs | 35 ++- codex-rs/thread-manager-sample/src/main.rs | 1 - 90 files changed, 521 insertions(+), 820 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs index 3bac7bf7e..84567fb4c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs @@ -76,7 +76,7 @@ pub struct TurnStartParams { #[experimental("turn/start.additionalContext")] #[ts(optional = nullable)] pub additional_context: Option>, - /// Optional turn-scoped environments. + /// Optional environments for this turn and subsequent turns. /// /// Omitted uses the thread sticky environments. Empty disables /// environment access for this turn. Non-empty selects the first diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 547d0666e..4549eab68 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -767,7 +767,7 @@ pub(crate) async fn apply_bespoke_event_handling( let requested_permissions = request.permissions.clone(); let request_cwd = match request.cwd.clone() { Some(cwd) => cwd, - None => conversation.config_snapshot().await.cwd, + None => conversation.config_snapshot().await.cwd().clone(), }; let params = PermissionsRequestApprovalParams { thread_id: conversation_id.to_string(), @@ -1161,7 +1161,7 @@ pub(crate) async fn apply_bespoke_event_handling( return; } }; - let fallback_cwd = conversation.config_snapshot().await.cwd; + let fallback_cwd = conversation.config_snapshot().await.cwd().clone(); let stored_thread = match conversation .read_thread( /*include_archived*/ true, /*include_history*/ true, diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index 79da49fba..d4e939562 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -330,6 +330,7 @@ use codex_core_plugins::remote::RemotePluginShareContext as RemoteCatalogPluginS use codex_core_plugins::remote::RemotePluginShareSummary as RemoteCatalogPluginShareSummary; use codex_core_plugins::remote::RemotePluginSummary as RemoteCatalogPluginSummary; use codex_exec_server::EnvironmentManager; +use codex_exec_server::LOCAL_ENVIRONMENT_ID; use codex_exec_server::LOCAL_FS; use codex_features::FEATURES; use codex_features::Feature; @@ -400,6 +401,7 @@ use codex_protocol::protocol::SessionConfiguredEvent; #[cfg(test)] use codex_protocol::protocol::SessionMetaLine; use codex_protocol::protocol::TurnEnvironmentSelection; +use codex_protocol::protocol::TurnEnvironmentSelections; use codex_protocol::protocol::USER_MESSAGE_BEGIN; use codex_protocol::protocol::W3cTraceContext; use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; diff --git a/codex-rs/app-server/src/request_processors/apps_processor.rs b/codex-rs/app-server/src/request_processors/apps_processor.rs index 49a6615f1..4a665d8ef 100644 --- a/codex-rs/app-server/src/request_processors/apps_processor.rs +++ b/codex-rs/app-server/src/request_processors/apps_processor.rs @@ -53,7 +53,7 @@ impl AppsRequestProcessor { None }; let fallback_cwd = match thread.as_ref() { - Some(thread) => Some(thread.config_snapshot().await.cwd.to_path_buf()), + Some(thread) => Some(thread.config_snapshot().await.cwd().to_path_buf()), None => None, }; let mut config = self.load_latest_config(fallback_cwd).await?; diff --git a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs index 8bfb282ac..fd9e93e18 100644 --- a/codex-rs/app-server/src/request_processors/thread_lifecycle.rs +++ b/codex-rs/app-server/src/request_processors/thread_lifecycle.rs @@ -627,6 +627,8 @@ pub(super) async fn handle_pending_thread_resume_request( } } + let config_snapshot = pending.config_snapshot; + let cwd = config_snapshot.cwd().clone(); let ThreadConfigSnapshot { model, model_provider_id, @@ -635,11 +637,10 @@ pub(super) async fn handle_pending_thread_resume_request( approvals_reviewer, permission_profile, active_permission_profile, - cwd, workspace_roots, reasoning_effort, .. - } = pending.config_snapshot; + } = config_snapshot; let instruction_sources = pending.instruction_sources; let sandbox = thread_response_sandbox_policy(&permission_profile, cwd.as_path()); let active_permission_profile = diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index eab606bf6..4cc169f80 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -47,11 +47,11 @@ fn collect_resume_override_mismatches( } if let Some(requested_cwd) = request.cwd.as_deref() { let requested_cwd_path = std::path::PathBuf::from(requested_cwd); - if requested_cwd_path != config_snapshot.cwd.as_path() { + if requested_cwd_path != config_snapshot.cwd().as_path() { mismatch_details.push(format!( "cwd requested={} active={}", requested_cwd_path.display(), - config_snapshot.cwd.display() + config_snapshot.cwd().display() )); } } @@ -1154,8 +1154,9 @@ impl ThreadRequestProcessor { let sandbox = thread_response_sandbox_policy( &config_snapshot.permission_profile, - config_snapshot.cwd.as_path(), + config_snapshot.cwd().as_path(), ); + let cwd = config_snapshot.cwd().clone(); let active_permission_profile = thread_response_active_permission_profile(config_snapshot.active_permission_profile); @@ -1164,7 +1165,7 @@ impl ThreadRequestProcessor { model: config_snapshot.model, model_provider: config_snapshot.model_provider_id, service_tier: config_snapshot.service_tier, - cwd: config_snapshot.cwd, + cwd, runtime_workspace_roots: config_snapshot.workspace_roots, instruction_sources, approval_policy: config_snapshot.approval_policy.into(), @@ -2601,7 +2602,7 @@ impl ThreadRequestProcessor { let config_snapshot = codex_thread.config_snapshot().await; let sandbox = thread_response_sandbox_policy( &config_snapshot.permission_profile, - config_snapshot.cwd.as_path(), + config_snapshot.cwd().as_path(), ); let active_permission_profile = thread_response_active_permission_profile( config_snapshot.active_permission_profile, @@ -3335,7 +3336,7 @@ impl ThreadRequestProcessor { let config_snapshot = forked_thread.config_snapshot().await; let sandbox = thread_response_sandbox_policy( &config_snapshot.permission_profile, - config_snapshot.cwd.as_path(), + config_snapshot.cwd().as_path(), ); let active_permission_profile = thread_response_active_permission_profile(config_snapshot.active_permission_profile); @@ -4205,7 +4206,7 @@ fn build_thread_from_snapshot( updated_at: now, status: ThreadStatus::NotLoaded, path, - cwd: config_snapshot.cwd.clone(), + cwd: config_snapshot.cwd().clone(), cli_version: env!("CARGO_PKG_VERSION").to_string(), agent_nickname: config_snapshot.session_source.get_nickname(), agent_role: config_snapshot.session_source.get_agent_role(), diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index c0a909e57..49941dd83 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -77,6 +77,7 @@ mod thread_processor_behavior_tests { use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; + use codex_protocol::protocol::TurnEnvironmentSelections; use codex_state::ThreadMetadataBuilder; use codex_thread_store::StoredThread; use codex_utils_absolute_path::test_support::PathBufExt; @@ -685,7 +686,7 @@ mod thread_processor_behavior_tests { approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, permission_profile: codex_protocol::models::PermissionProfile::Disabled, active_permission_profile: None, - cwd, + environments: TurnEnvironmentSelections::new(cwd, Vec::new()), workspace_roots: Vec::new(), profile_workspace_roots: Vec::new(), ephemeral: false, diff --git a/codex-rs/app-server/src/request_processors/thread_summary.rs b/codex-rs/app-server/src/request_processors/thread_summary.rs index f46707482..0fb132005 100644 --- a/codex-rs/app-server/src/request_processors/thread_summary.rs +++ b/codex-rs/app-server/src/request_processors/thread_summary.rs @@ -190,12 +190,12 @@ pub(crate) fn thread_settings_from_config_snapshot( config_snapshot: &ThreadConfigSnapshot, ) -> ThreadSettings { ThreadSettings { - cwd: config_snapshot.cwd.clone(), + cwd: config_snapshot.cwd().clone(), approval_policy: config_snapshot.approval_policy.into(), approvals_reviewer: config_snapshot.approvals_reviewer.into(), sandbox_policy: thread_response_sandbox_policy( &config_snapshot.permission_profile, - config_snapshot.cwd.as_path(), + config_snapshot.cwd().as_path(), ), active_permission_profile: thread_response_active_permission_profile( config_snapshot.active_permission_profile.clone(), @@ -213,24 +213,36 @@ pub(crate) fn thread_settings_from_config_snapshot( pub(crate) fn thread_settings_from_core_snapshot( snapshot: codex_protocol::protocol::ThreadSettingsSnapshot, ) -> ThreadSettings { + let codex_protocol::protocol::ThreadSettingsSnapshot { + model, + model_provider_id, + service_tier, + approval_policy, + approvals_reviewer, + permission_profile, + active_permission_profile, + cwd, + reasoning_effort, + reasoning_summary, + personality, + collaboration_mode, + } = snapshot; + let sandbox_policy = thread_response_sandbox_policy(&permission_profile, cwd.as_path()); ThreadSettings { - sandbox_policy: thread_response_sandbox_policy( - &snapshot.permission_profile, - snapshot.cwd.as_path(), - ), - cwd: snapshot.cwd, - approval_policy: snapshot.approval_policy.into(), - approvals_reviewer: snapshot.approvals_reviewer.into(), + sandbox_policy, + cwd, + approval_policy: approval_policy.into(), + approvals_reviewer: approvals_reviewer.into(), active_permission_profile: thread_response_active_permission_profile( - snapshot.active_permission_profile, + active_permission_profile, ), - model: snapshot.model, - model_provider: snapshot.model_provider_id, - service_tier: snapshot.service_tier, - effort: snapshot.reasoning_effort, - summary: snapshot.reasoning_summary, - collaboration_mode: snapshot.collaboration_mode, - personality: snapshot.personality, + model, + model_provider: model_provider_id, + service_tier, + effort: reasoning_effort, + summary: reasoning_summary, + collaboration_mode, + personality, } } diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index ce98a2e8e..7bdd9df0d 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -43,7 +43,7 @@ fn map_additional_context( struct ThreadSettingsBuildParams { method: &'static str, - cwd: Option, + environments: Option, runtime_workspace_roots: Option>, approval_policy: Option, approvals_reviewer: Option, @@ -406,12 +406,14 @@ impl TurnRequestProcessor { let additional_context = map_additional_context(params.additional_context); let turn_has_input = !mapped_items.is_empty(); let cwd = resolve_request_cwd(params.cwd)?; + let environments = + Self::build_environment_override(thread.as_ref(), cwd, environment_selections).await; let thread_settings = self .build_thread_settings_overrides( thread.as_ref(), ThreadSettingsBuildParams { method: "turn/start", - cwd, + environments, runtime_workspace_roots: params.runtime_workspace_roots, approval_policy: params.approval_policy, approvals_reviewer: params.approvals_reviewer, @@ -430,7 +432,6 @@ impl TurnRequestProcessor { // Start the turn by submitting the user input. Return its submission id as turn_id. let turn_op = Op::UserInput { items: mapped_items, - environments: environment_selections, final_output_json_schema: params.output_schema, responsesapi_client_metadata: params.responsesapi_client_metadata, additional_context, @@ -478,6 +479,31 @@ impl TurnRequestProcessor { Ok(TurnStartResponse { turn }) } + async fn build_environment_override( + thread: &CodexThread, + cwd: Option, + environment_selections: Option>, + ) -> Option { + if cwd.is_none() && environment_selections.is_none() { + return None; + } + + let snapshot = thread.config_snapshot().await; + let environment_selections = + environment_selections.unwrap_or_else(|| snapshot.environment_selections().to_vec()); + let legacy_fallback_cwd = cwd.unwrap_or_else(|| { + environment_selections + .iter() + .find(|selection| selection.environment_id == LOCAL_ENVIRONMENT_ID) + .map(|selection| selection.cwd.clone()) + .unwrap_or_else(|| snapshot.cwd().clone()) + }); + Some(TurnEnvironmentSelections::new( + legacy_fallback_cwd, + environment_selections, + )) + } + async fn build_thread_settings_overrides( &self, thread: &CodexThread, @@ -485,7 +511,7 @@ impl TurnRequestProcessor { ) -> Result { let ThreadSettingsBuildParams { method, - cwd, + environments, runtime_workspace_roots, approval_policy, approvals_reviewer, @@ -508,6 +534,7 @@ impl TurnRequestProcessor { let collaboration_mode = collaboration_mode.map(|mode| self.normalize_collaboration_mode(mode)); let runtime_workspace_roots_request = runtime_workspace_roots; + let has_environment_override = environments.is_some(); // `thread/settings/update` only acknowledges that the update was queued. // Clients that send dependent partial updates should wait for // `thread/settings/updated` or combine the fields in one request. @@ -517,7 +544,7 @@ impl TurnRequestProcessor { None }; - let has_any_overrides = cwd.is_some() + let has_any_overrides = has_environment_override || runtime_workspace_roots_request.is_some() || approval_policy.is_some() || approvals_reviewer.is_some() @@ -545,7 +572,9 @@ impl TurnRequestProcessor { ))); }; let overrides = ConfigOverrides { - cwd: cwd.as_ref().map(AbsolutePathBuf::to_path_buf), + cwd: environments + .as_ref() + .map(|environments| environments.legacy_fallback_cwd.to_path_buf()), workspace_roots: Some( runtime_workspace_roots .clone() @@ -561,7 +590,7 @@ impl TurnRequestProcessor { .load_for_cwd( /*request_overrides*/ None, overrides, - Some(snapshot.cwd.to_path_buf()), + Some(snapshot.cwd().to_path_buf()), ) .await .map_err(|err| config_load_error(&err))?; @@ -588,7 +617,7 @@ impl TurnRequestProcessor { if has_any_overrides { thread .preview_thread_settings_overrides(CodexThreadSettingsOverrides { - cwd: cwd.clone(), + environments: environments.clone(), workspace_roots: runtime_workspace_roots.clone(), approval_policy, approvals_reviewer, @@ -611,7 +640,7 @@ impl TurnRequestProcessor { } Ok(codex_protocol::protocol::ThreadSettingsOverrides { - cwd, + environments, workspace_roots: runtime_workspace_roots, profile_workspace_roots, approval_policy, @@ -636,12 +665,18 @@ impl TurnRequestProcessor { ) -> Result { let (_, thread) = self.load_thread(¶ms.thread_id).await?; let cwd = resolve_request_cwd(params.cwd)?; + let environments = Self::build_environment_override( + thread.as_ref(), + cwd, + /*environment_selections*/ None, + ) + .await; let thread_settings = self .build_thread_settings_overrides( thread.as_ref(), ThreadSettingsBuildParams { method: "thread/settings/update", - cwd, + environments, runtime_workspace_roots: None, approval_policy: params.approval_policy, approvals_reviewer: params.approvals_reviewer, diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index 3ad1360e5..c626dda4a 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -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(), diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 5a1473b05..5f816bd07 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -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, diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 1697f768e..ab1f6df03 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -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, - pub cwd: AbsolutePathBuf, + pub environments: TurnEnvironmentSelections, pub workspace_roots: Vec, pub profile_workspace_roots: Vec, 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, + pub environments: Option, pub workspace_roots: Option>, pub profile_workspace_roots: Option>, pub approval_policy: Option, @@ -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, diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 8504690b2..fcda9a5a2 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -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), diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index 7730a30ad..c0f62ec66 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -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. diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 37d55256f..f3e506d40 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -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 { 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 { @@ -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; diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 92f973731..37a501fb3 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -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, @@ -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, - /// Sticky environments for turns that do not provide a turn-local override. - pub(super) environments: Vec, // TODO(pakrym): Remove config from here pub(super) original_config_do_not_use: Arc, @@ -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( ¤t_sandbox_policy, - &self.cwd, + self.cwd(), ¤t_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(¤t_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 == ¤t_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(), ¤t_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( ¤t_sandbox_policy, - &next_configuration.cwd, + next_configuration.cwd(), ¤t_file_system_sandbox_policy, ); next_configuration @@ -401,7 +409,7 @@ impl SessionConfiguration { #[derive(Default, Clone)] pub(crate) struct SessionSettingsUpdate { - pub(crate) cwd: Option, + pub(crate) environments: Option, pub(crate) workspace_roots: Option>, pub(crate) profile_workspace_roots: Option>, pub(crate) approval_policy: Option, @@ -414,10 +422,6 @@ pub(crate) struct SessionSettingsUpdate { pub(crate) reasoning_summary: Option, pub(crate) service_tier: Option>, pub(crate) final_output_json_schema: Option>, - /// Turn-local environment override. `None` inherits the sticky thread - /// environments stored on `SessionConfiguration`; `Some([])` explicitly - /// disables environments for this turn. - pub(crate) environments: Option>, pub(crate) personality: Option, pub(crate) app_server_client_name: Option, pub(crate) app_server_client_version: Option, @@ -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( diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 4d914ff98..d44c7fbdc 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -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, )); diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 5f52912d5..be6e316a0 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -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(); - } - } } diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 34ec68211..116473258 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -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::::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("") => { - Some(text.as_str()) - } - _ => None, - }) - .expect("environment context prompt item"); - assert!(environment_context.contains("")); - let cwd = thread.session_configured.cwd.display().to_string(); - let dev_entry = format!( - r#" - {cwd} - "# - ); - let local_entry = format!( - r#" - {cwd} - "# - ); - 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 ")); - assert!(!environment_context.contains("\n ")); -} - #[tokio::test] async fn start_thread_keeps_internal_threads_hidden_from_normal_lookups() { let temp_dir = tempdir().expect("tempdir"); diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index ac78113a3..f55d9c087 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -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(), diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 1e4313590..62f422ba1 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -40,6 +40,7 @@ use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionConfiguredEvent; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::TurnEnvironmentSelection; +use codex_protocol::protocol::TurnEnvironmentSelections; use codex_protocol::user_input::UserInput; use codex_utils_absolute_path::AbsolutePathBuf; use futures::future::BoxFuture; @@ -70,6 +71,17 @@ const REMOTE_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_TEST_REMOTE_EXEC_SERVER_URL" static REMOTE_TEST_INSTANCE_COUNTER: AtomicU64 = AtomicU64::new(0); const SUBMIT_TURN_COMPLETE_TIMEOUT: Duration = Duration::from_secs(30); +pub fn local(cwd: AbsolutePathBuf) -> TurnEnvironmentSelection { + TurnEnvironmentSelection { + environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(), + cwd, + } +} + +pub fn local_selections(cwd: AbsolutePathBuf) -> TurnEnvironmentSelections { + TurnEnvironmentSelections::new(cwd.clone(), vec![local(cwd)]) +} + #[derive(Debug)] pub struct TestEnv { environment: codex_exec_server::Environment, @@ -789,18 +801,20 @@ impl TestCodex { let (sandbox_policy, permission_profile) = turn_permission_fields(permission_profile, self.config.cwd.as_path()); let session_model = self.session_configured.model.clone(); + let turn_environment_selections = environments.map(|environments| { + TurnEnvironmentSelections::new(self.config.cwd.clone(), environments) + }); self.codex .submit(Op::UserInput { items: vec![UserInput::Text { text: prompt.into(), text_elements: Vec::new(), }], - environments, final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { - cwd: Some(self.config.cwd.clone()), + environments: turn_environment_selections, approval_policy: Some(approval_policy), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/abort_tasks.rs b/codex-rs/core/tests/suite/abort_tasks.rs index 82bb8c879..d44b4e5a2 100644 --- a/codex-rs/core/tests/suite/abort_tasks.rs +++ b/codex-rs/core/tests/suite/abort_tasks.rs @@ -46,7 +46,6 @@ async fn interrupt_long_running_tool_emits_turn_aborted() { // Kick off a turn that triggers the function call. codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "start sleep".into(), text_elements: Vec::new(), @@ -104,7 +103,6 @@ async fn interrupt_tool_records_history_entries() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "start history recording".into(), text_elements: Vec::new(), @@ -126,7 +124,6 @@ async fn interrupt_tool_records_history_entries() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "follow up".into(), text_elements: Vec::new(), @@ -210,7 +207,6 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "start interrupt marker".into(), text_elements: Vec::new(), @@ -232,7 +228,6 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "follow up".into(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/additional_context.rs b/codex-rs/core/tests/suite/additional_context.rs index f9bfdb966..6812be95e 100644 --- a/codex-rs/core/tests/suite/additional_context.rs +++ b/codex-rs/core/tests/suite/additional_context.rs @@ -37,7 +37,6 @@ async fn additional_context_is_model_visible_but_not_a_user_message_item() -> Re test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "inspect the active tab".to_string(), text_elements: Vec::new(), @@ -136,7 +135,6 @@ async fn external_context_like_user_text_remains_a_user_message_item() -> Result test.codex .submit(Op::UserInput { - environments: None, items: vec![user_input.clone()], final_output_json_schema: None, responsesapi_client_metadata: None, @@ -182,7 +180,6 @@ async fn additional_context_trust_controls_message_role() -> Result<()> { test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "inspect context".to_string(), text_elements: Vec::new(), @@ -263,7 +260,6 @@ async fn additional_context_is_deduplicated_between_turns_while_retained() -> Re test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "first turn".to_string(), text_elements: Vec::new(), @@ -281,7 +277,6 @@ async fn additional_context_is_deduplicated_between_turns_while_retained() -> Re test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "second turn".to_string(), text_elements: Vec::new(), @@ -343,7 +338,6 @@ async fn additional_context_removes_one_value_while_adding_another() -> Result<( test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "first turn".to_string(), text_elements: Vec::new(), @@ -376,7 +370,6 @@ async fn additional_context_removes_one_value_while_adding_another() -> Result<( test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "second turn".to_string(), text_elements: Vec::new(), @@ -409,7 +402,6 @@ async fn additional_context_removes_one_value_while_adding_another() -> Result<( test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "third turn".to_string(), text_elements: Vec::new(), @@ -506,7 +498,6 @@ async fn additional_context_values_are_truncated_before_model_input() -> Result< test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "summarize context".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/apply_patch_cli.rs b/codex-rs/core/tests/suite/apply_patch_cli.rs index db301e39b..a9ebadceb 100644 --- a/codex-rs/core/tests/suite/apply_patch_cli.rs +++ b/codex-rs/core/tests/suite/apply_patch_cli.rs @@ -7,6 +7,7 @@ use core_test_support::responses::ev_apply_patch_custom_tool_call; use core_test_support::responses::ev_apply_patch_shell_command_call_via_heredoc; use core_test_support::responses::ev_shell_command_call; use core_test_support::test_codex::ApplyPatchModelOutput; +use core_test_support::test_codex::local_selections; use pretty_assertions::assert_eq; use std::fs; use std::path::PathBuf; @@ -52,6 +53,7 @@ use core_test_support::skip_if_no_network; use core_test_support::skip_if_remote; use core_test_support::test_codex::TestCodexBuilder; use core_test_support::test_codex::TestCodexHarness; +use core_test_support::test_codex::local; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -100,12 +102,11 @@ async fn submit_without_wait_with_turn_permissions( text: prompt.into(), 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(harness.cwd_abs()), + environments: Some(local_selections(harness.cwd_abs())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -1631,27 +1632,27 @@ async fn apply_patch_turn_diff_tracks_local_and_remote_environment_paths() -> Re let (sandbox_policy, permission_profile) = turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); + let environments = vec![ + local(shared_cwd.clone()), + TurnEnvironmentSelection { + environment_id: REMOTE_ENVIRONMENT_ID.to_string(), + cwd: shared_cwd.clone(), + }, + ]; test.codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "apply matching patches to local and remote environments".into(), text_elements: Vec::new(), }], - environments: Some(vec![ - TurnEnvironmentSelection { - environment_id: LOCAL_ENVIRONMENT_ID.to_string(), - cwd: shared_cwd.clone(), - }, - TurnEnvironmentSelection { - environment_id: REMOTE_ENVIRONMENT_ID.to_string(), - cwd: shared_cwd.clone(), - }, - ]), final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { - cwd: Some(test.config.cwd.clone()), + environments: Some(codex_protocol::protocol::TurnEnvironmentSelections::new( + test.config.cwd.clone(), + environments, + )), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index 865aabb46..927b1f18f 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -39,6 +39,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -658,12 +659,11 @@ async fn submit_turn( text: prompt.into(), 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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(approval_policy), approvals_reviewer: Some(ApprovalsReviewer::User), sandbox_policy: Some(sandbox_policy), @@ -2619,12 +2619,11 @@ async fn env_zsh_script_spawned_by_python_can_request_escalation_under_zsh_fork( text: "run nested env zsh script through python".into(), 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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(approval_policy), approvals_reviewer: Some(ApprovalsReviewer::User), sandbox_policy: Some(sandbox_policy), @@ -2764,12 +2763,11 @@ async fn matched_prefix_rule_runs_unsandboxed_under_zsh_fork() -> Result<()> { text: "run allowed touch under zsh fork".into(), 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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(approval_policy), approvals_reviewer: Some(ApprovalsReviewer::User), sandbox_policy: Some(sandbox_policy), @@ -3356,12 +3354,11 @@ allow_local_binding = true text: "deny-read network retry".into(), 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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(approval_policy), approvals_reviewer: Some(ApprovalsReviewer::User), sandbox_policy: Some(turn_sandbox_policy), diff --git a/codex-rs/core/tests/suite/auto_review.rs b/codex-rs/core/tests/suite/auto_review.rs index 5de8f421b..5fe1cbc2f 100644 --- a/codex-rs/core/tests/suite/auto_review.rs +++ b/codex-rs/core/tests/suite/auto_review.rs @@ -34,6 +34,7 @@ use core_test_support::responses::sse; use core_test_support::skip_if_no_network; use core_test_support::skip_if_sandbox; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -160,12 +161,11 @@ async fn remote_model_override_uses_catalog_model_for_strict_auto_review() -> Re text: "run the Guardian model override check".into(), 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(cwd_path), + environments: Some(local_selections(cwd_path)), approval_policy: Some(AskForApproval::OnRequest), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 2fcb73e25..9a6f104d0 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -66,6 +66,7 @@ use core_test_support::responses::sse; use core_test_support::responses::sse_failed; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use dunce::canonicalize as normalize_path; @@ -386,7 +387,6 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { // 2) Submit new input; the request body must include the prior items, then initial context, then new user input. codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -755,7 +755,6 @@ async fn includes_session_id_thread_id_and_model_headers_in_request() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -968,7 +967,6 @@ async fn includes_base_instructions_override_in_request() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1026,7 +1024,6 @@ async fn chatgpt_auth_sends_correct_request() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1151,7 +1148,6 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1191,7 +1187,6 @@ async fn includes_user_instructions_message_in_request() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1280,7 +1275,6 @@ async fn includes_apps_guidance_as_developer_message_for_chatgpt_auth() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1344,7 +1338,6 @@ async fn omits_apps_guidance_for_api_key_auth_even_when_feature_enabled() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1404,7 +1397,6 @@ async fn omits_apps_guidance_when_configured_off() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1447,7 +1439,6 @@ async fn omits_environment_context_when_configured_off() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1505,7 +1496,6 @@ async fn skills_append_to_developer_message() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1589,7 +1579,6 @@ async fn skills_use_aliases_in_developer_message_under_budget_pressure() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1651,7 +1640,6 @@ async fn includes_configured_effort_in_request() -> anyhow::Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1694,7 +1682,6 @@ async fn includes_no_effort_in_request() -> anyhow::Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1738,7 +1725,6 @@ async fn includes_default_reasoning_effort_in_request_when_defined_by_model_info codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1794,12 +1780,11 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re text: "hello".into(), 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()), sandbox_policy: Some(config.legacy_sandbox_policy()), summary: Some( @@ -1847,7 +1832,6 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1904,7 +1888,6 @@ async fn responses_lite_sets_all_turns_context_and_disables_parallel_tool_calls( codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1972,12 +1955,11 @@ async fn user_turn_explicit_reasoning_summary_overrides_model_catalog_default() text: "hello".into(), 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()), sandbox_policy: Some(config.legacy_sandbox_policy()), summary: Some(ReasoningSummary::Concise), @@ -2029,7 +2011,6 @@ async fn reasoning_summary_is_omitted_when_disabled() -> anyhow::Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -2089,7 +2070,6 @@ async fn reasoning_summary_none_overrides_model_catalog_default() -> anyhow::Res codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -2129,7 +2109,6 @@ async fn includes_default_verbosity_in_request() -> anyhow::Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -2178,7 +2157,6 @@ async fn configured_verbosity_not_sent_for_models_without_support() -> anyhow::R codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -2226,7 +2204,6 @@ async fn configured_verbosity_is_sent() -> anyhow::Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -2279,7 +2256,6 @@ async fn includes_developer_instructions_message_in_request() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -2577,7 +2553,6 @@ async fn token_count_includes_rate_limits_snapshot() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -2719,7 +2694,6 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> { let submission_id = codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -2797,7 +2771,6 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "seed turn".into(), text_elements: Vec::new(), @@ -2813,7 +2786,6 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "trigger context window".into(), text_elements: Vec::new(), @@ -2899,7 +2871,6 @@ async fn incomplete_response_emits_content_filter_error_message() -> anyhow::Res .await?; codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "trigger incomplete".into(), text_elements: Vec::new(), @@ -3011,7 +2982,6 @@ async fn azure_overrides_assign_properties_used_for_responses_url() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -3101,7 +3071,6 @@ async fn env_var_overrides_loaded_auth() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -3159,7 +3128,6 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() { // Turn 1: user sends U1; wait for completion. codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "U1".into(), text_elements: Vec::new(), @@ -3176,7 +3144,6 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() { // Turn 2: user sends U2; wait for completion. codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "U2".into(), text_elements: Vec::new(), @@ -3193,7 +3160,6 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() { // Turn 3: user sends U3; wait for completion. codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "U3".into(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index 3e1ed2326..4dc674ec3 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -1231,7 +1231,6 @@ async fn responses_websocket_usage_limit_error_emits_rate_limit_event() { let submission_id = test .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1322,7 +1321,6 @@ async fn responses_websocket_invalid_request_error_with_status_is_forwarded() { let submission_id = test .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index f5f17a09b..e1b02292e 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -3199,12 +3199,14 @@ text( text: "use exec to inspect and call hidden tools".into(), 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(cwd), + environments: Some(codex_protocol::protocol::TurnEnvironmentSelections::new( + cwd, + Vec::new(), + )), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index 0d4f4cf6f..d9d511edb 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -13,6 +13,7 @@ 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::skip_if_no_network; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; @@ -72,7 +73,6 @@ async fn no_collaboration_instructions_by_default() -> Result<()> { test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -127,7 +127,6 @@ async fn user_input_includes_collaboration_instructions_after_override() -> Resu test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -169,12 +168,11 @@ async fn collaboration_instructions_added_on_user_turn() -> Result<()> { text: "hello".into(), 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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(test.config.permissions.approval_policy.value()), sandbox_policy: Some(test.config.legacy_sandbox_policy()), summary: Some( @@ -220,12 +218,11 @@ async fn collaboration_instructions_omitted_when_disabled() -> Result<()> { text: "hello".into(), 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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(test.config.permissions.approval_policy.value()), sandbox_policy: Some(test.config.legacy_sandbox_policy()), summary: Some( @@ -276,7 +273,6 @@ async fn override_then_next_turn_uses_updated_collaboration_instructions() -> Re test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -329,12 +325,11 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu text: "hello".into(), 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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(test.config.permissions.approval_policy.value()), sandbox_policy: Some(test.config.legacy_sandbox_policy()), summary: Some( @@ -390,7 +385,6 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), @@ -414,7 +408,6 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), @@ -467,7 +460,6 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), @@ -491,7 +483,6 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), @@ -546,7 +537,6 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), @@ -573,7 +563,6 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), @@ -629,7 +618,6 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged() test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), @@ -656,7 +644,6 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged() test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), @@ -715,7 +702,6 @@ async fn resume_replays_collaboration_instructions() -> Result<()> { initial .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -732,7 +718,6 @@ async fn resume_replays_collaboration_instructions() -> Result<()> { resumed .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "after resume".into(), text_elements: Vec::new(), @@ -785,7 +770,6 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> { test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 5fe90bf7a..ac497aff3 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -31,6 +31,7 @@ use core_test_support::hooks::trust_discovered_hooks; use core_test_support::responses::ev_reasoning_item; use core_test_support::responses::mount_models_once; use core_test_support::skip_if_no_network; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::test_path_buf; @@ -96,12 +97,11 @@ fn disabled_permission_user_turn(text: impl Into, cwd: PathBuf, model: S text: text.into(), 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(cwd.abs()), + environments: Some(local_selections(cwd.abs())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -404,7 +404,6 @@ async fn summarize_context_three_requests_and_instructions() { // 1) Normal user input – should hit server once. codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello world".into(), text_elements: Vec::new(), @@ -430,7 +429,6 @@ async fn summarize_context_three_requests_and_instructions() { // 3) Next user input – third hit; history should include only the summary. codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: THIRD_USER_MSG.into(), text_elements: Vec::new(), @@ -606,7 +604,6 @@ async fn manual_pre_compact_block_decision_does_not_block_compaction() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello before blocked compact".to_string(), text_elements: Vec::new(), @@ -680,7 +677,6 @@ async fn compact_hooks_respect_matchers_and_post_runs_after_compaction() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello before matched compact".to_string(), text_elements: Vec::new(), @@ -751,7 +747,6 @@ async fn manual_compact_uses_custom_prompt() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), @@ -899,7 +894,6 @@ async fn manual_compact_emits_context_compaction_items() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "manual compact".into(), text_elements: Vec::new(), @@ -1066,7 +1060,6 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() { // Start the conversation with the user message codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: user_message.into(), text_elements: Vec::new(), @@ -1539,7 +1532,6 @@ async fn auto_compact_runs_after_token_limit_hit() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: FIRST_AUTO_MSG.into(), text_elements: Vec::new(), @@ -1556,7 +1548,6 @@ async fn auto_compact_runs_after_token_limit_hit() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: SECOND_AUTO_MSG.into(), text_elements: Vec::new(), @@ -1573,7 +1564,6 @@ async fn auto_compact_runs_after_token_limit_hit() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: POST_AUTO_USER_MSG.into(), text_elements: Vec::new(), @@ -1745,7 +1735,6 @@ async fn auto_compact_emits_context_compaction_items() { for user in [FIRST_AUTO_MSG, SECOND_AUTO_MSG, POST_AUTO_USER_MSG] { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: user.into(), text_elements: Vec::new(), @@ -1827,7 +1816,6 @@ async fn auto_compact_starts_after_turn_started() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: FIRST_AUTO_MSG.into(), text_elements: Vec::new(), @@ -1843,7 +1831,6 @@ async fn auto_compact_starts_after_turn_started() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: SECOND_AUTO_MSG.into(), text_elements: Vec::new(), @@ -1859,7 +1846,6 @@ async fn auto_compact_starts_after_turn_started() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: POST_AUTO_USER_MSG.into(), text_elements: Vec::new(), @@ -2388,7 +2374,6 @@ async fn auto_compact_persists_rollout_entries() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: FIRST_AUTO_MSG.into(), text_elements: Vec::new(), @@ -2404,7 +2389,6 @@ async fn auto_compact_persists_rollout_entries() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: SECOND_AUTO_MSG.into(), text_elements: Vec::new(), @@ -2420,7 +2404,6 @@ async fn auto_compact_persists_rollout_entries() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: POST_AUTO_USER_MSG.into(), text_elements: Vec::new(), @@ -2510,7 +2493,6 @@ async fn manual_compact_retries_after_context_window_error() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "first turn".into(), text_elements: Vec::new(), @@ -2615,7 +2597,6 @@ async fn manual_compact_non_context_failure_retries_then_emits_task_error() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "first turn".into(), text_elements: Vec::new(), @@ -2711,7 +2692,6 @@ async fn manual_compact_twice_preserves_latest_user_messages() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: first_user_message.into(), text_elements: Vec::new(), @@ -2730,7 +2710,6 @@ async fn manual_compact_twice_preserves_latest_user_messages() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: second_user_message.into(), text_elements: Vec::new(), @@ -2749,7 +2728,6 @@ async fn manual_compact_twice_preserves_latest_user_messages() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: final_user_message.into(), text_elements: Vec::new(), @@ -2961,7 +2939,6 @@ async fn auto_compact_allows_multiple_attempts_when_interleaved_with_other_turn_ for user in [MULTI_AUTO_MSG, follow_up_user, final_user] { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: user.into(), text_elements: Vec::new(), @@ -3067,7 +3044,6 @@ async fn snapshot_request_shape_mid_turn_continuation_compaction() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: FUNCTION_CALL_LIMIT_MSG.into(), text_elements: Vec::new(), @@ -3499,7 +3475,6 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() { { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: user.into(), text_elements: Vec::new(), @@ -3619,7 +3594,6 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() { for user in [first_user, second_user, third_user] { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: user.into(), text_elements: Vec::new(), @@ -3682,7 +3656,6 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess for user in ["USER_ONE", "USER_TWO"] { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: user.to_string(), text_elements: Vec::new(), @@ -3699,7 +3672,9 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess core_test_support::submit_thread_settings( &codex, codex_protocol::protocol::ThreadSettingsOverrides { - cwd: Some(test_path_buf(PRETURN_CONTEXT_DIFF_CWD).abs()), + environments: Some(local_selections( + test_path_buf(PRETURN_CONTEXT_DIFF_CWD).abs(), + )), ..Default::default() }, ) @@ -3709,7 +3684,6 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess .to_string(); codex .submit(Op::UserInput { - environments: None, items: vec![ UserInput::Image { image_url: image_url.clone(), @@ -3905,7 +3879,6 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), @@ -3921,7 +3894,6 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_TWO".to_string(), text_elements: Vec::new(), @@ -3995,7 +3967,6 @@ async fn snapshot_request_shape_manual_compact_without_previous_user_messages() codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "AFTER_MANUAL_EMPTY_COMPACT".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 12fd570c5..73a500f04 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -1,5 +1,6 @@ #![allow(clippy::expect_used)] +use core_test_support::test_codex::local_selections; use std::fs; use anyhow::Result; @@ -324,7 +325,6 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello remote compact".into(), text_elements: Vec::new(), @@ -342,7 +342,6 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "after compact".into(), text_elements: Vec::new(), @@ -585,7 +584,6 @@ async fn assert_remote_manual_compact_request_parity( codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "TURN_ONE_USER".to_string(), text_elements: Vec::new(), @@ -600,7 +598,6 @@ async fn assert_remote_manual_compact_request_parity( codex .submit(Op::UserInput { - environments: None, items: vec![ UserInput::Text { text: "TURN_TWO_PREFIX".to_string(), @@ -621,7 +618,6 @@ async fn assert_remote_manual_compact_request_parity( codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "TURN_THREE_TOOL_USER".to_string(), text_elements: Vec::new(), @@ -636,7 +632,6 @@ async fn assert_remote_manual_compact_request_parity( codex .submit(Op::UserInput { - environments: None, items: vec![ UserInput::Image { image_url, @@ -657,7 +652,6 @@ async fn assert_remote_manual_compact_request_parity( codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "TURN_FIVE_USER".to_string(), text_elements: Vec::new(), @@ -826,7 +820,6 @@ async fn remote_compact_v2_reuses_compaction_trigger_for_followups() -> Result<( codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello remote compact".into(), text_elements: Vec::new(), @@ -844,7 +837,6 @@ async fn remote_compact_v2_reuses_compaction_trigger_for_followups() -> Result<( codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "after compact".into(), text_elements: Vec::new(), @@ -976,7 +968,6 @@ async fn remote_compact_v2_retries_failures_with_stream_retry_budget() -> Result codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello remote compact".into(), text_elements: Vec::new(), @@ -994,7 +985,6 @@ async fn remote_compact_v2_retries_failures_with_stream_retry_budget() -> Result codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "after compact".into(), text_elements: Vec::new(), @@ -1081,7 +1071,6 @@ async fn remote_compact_v2_accepts_additional_output_items_before_compaction() - codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello remote compact".into(), text_elements: Vec::new(), @@ -1099,7 +1088,6 @@ async fn remote_compact_v2_accepts_additional_output_items_before_compaction() - codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "after compact".into(), text_elements: Vec::new(), @@ -1187,7 +1175,6 @@ async fn remote_compact_filters_deferred_dynamic_tools() -> Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello remote compact".into(), text_elements: Vec::new(), @@ -1260,7 +1247,6 @@ async fn remote_compact_runs_automatically() -> Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello remote compact".into(), text_elements: Vec::new(), @@ -1398,7 +1384,6 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: first_user_message.into(), text_elements: Vec::new(), @@ -1413,7 +1398,6 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: second_user_message.into(), text_elements: Vec::new(), @@ -1528,7 +1512,6 @@ async fn remote_compact_rewrites_multiple_trailing_function_call_outputs() -> Re codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: first_user_message.into(), text_elements: Vec::new(), @@ -1543,7 +1526,6 @@ async fn remote_compact_rewrites_multiple_trailing_function_call_outputs() -> Re codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: second_user_message.into(), text_elements: Vec::new(), @@ -1656,7 +1638,6 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window() codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: first_user_message.into(), text_elements: Vec::new(), @@ -1671,7 +1652,6 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window() codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: second_user_message.into(), text_elements: Vec::new(), @@ -1692,7 +1672,6 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window() codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "turn that triggers auto compact".into(), text_elements: Vec::new(), @@ -1816,7 +1795,6 @@ async fn remote_compact_trims_tool_search_output_to_empty_tools_array() -> Resul codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "Find the oversized deferred tool".to_string(), text_elements: Vec::new(), @@ -1897,7 +1875,6 @@ async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "turn that exceeds token threshold".into(), text_elements: Vec::new(), @@ -1912,7 +1889,6 @@ async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "turn that triggers auto compact".into(), text_elements: Vec::new(), @@ -2007,7 +1983,6 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result baseline_codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: first_user_message.into(), text_elements: Vec::new(), @@ -2025,7 +2000,6 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result baseline_codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: second_user_message.into(), text_elements: Vec::new(), @@ -2117,7 +2091,6 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result override_codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: first_user_message.into(), text_elements: Vec::new(), @@ -2135,7 +2108,6 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result override_codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: second_user_message.into(), text_elements: Vec::new(), @@ -2212,7 +2184,6 @@ async fn remote_manual_compact_emits_context_compaction_items() -> Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "manual remote compact".into(), text_elements: Vec::new(), @@ -2294,7 +2265,6 @@ async fn remote_manual_compact_failure_emits_task_error_event() -> Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "manual remote compact".into(), text_elements: Vec::new(), @@ -2379,7 +2349,6 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()> codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "needs compaction".into(), text_elements: Vec::new(), @@ -2523,7 +2492,6 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res initial .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "start remote compact flow".into(), text_elements: Vec::new(), @@ -2542,7 +2510,6 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res initial .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "after compact in same session".into(), text_elements: Vec::new(), @@ -2568,7 +2535,6 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res resumed .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "after resume".into(), text_elements: Vec::new(), @@ -2665,7 +2631,6 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume() test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "start remote compact flow".into(), text_elements: Vec::new(), @@ -2683,7 +2648,6 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume() test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "after compact in same session".into(), text_elements: Vec::new(), @@ -2756,7 +2720,6 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_sta test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), @@ -2771,7 +2734,6 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_sta test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_TWO".to_string(), text_elements: Vec::new(), @@ -2839,7 +2801,6 @@ async fn remote_request_uses_custom_experimental_realtime_start_instructions() - test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), @@ -2901,7 +2862,6 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), @@ -2918,7 +2878,6 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_TWO".to_string(), text_elements: Vec::new(), @@ -2994,7 +2953,6 @@ async fn snapshot_request_shape_remote_manual_compact_restates_realtime_start() test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), @@ -3012,7 +2970,6 @@ async fn snapshot_request_shape_remote_manual_compact_restates_realtime_start() test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_TWO".to_string(), text_elements: Vec::new(), @@ -3096,7 +3053,6 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_does_not_restate_real test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "SETUP_USER".to_string(), text_elements: Vec::new(), @@ -3113,7 +3069,6 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_does_not_restate_real test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_TWO".to_string(), text_elements: Vec::new(), @@ -3205,7 +3160,6 @@ async fn snapshot_request_shape_remote_compact_resume_restates_realtime_end() -> initial .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), @@ -3236,7 +3190,6 @@ async fn snapshot_request_shape_remote_compact_resume_restates_realtime_end() -> resumed .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_TWO".to_string(), text_elements: Vec::new(), @@ -3318,7 +3271,9 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us core_test_support::submit_thread_settings( &codex, codex_protocol::protocol::ThreadSettingsOverrides { - cwd: Some(test_path_buf(PRETURN_CONTEXT_DIFF_CWD).abs()), + environments: Some(local_selections( + test_path_buf(PRETURN_CONTEXT_DIFF_CWD).abs(), + )), ..Default::default() }, ) @@ -3326,7 +3281,6 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us } codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: user.to_string(), text_elements: Vec::new(), @@ -3414,7 +3368,6 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "BEFORE_SWITCH_USER".to_string(), text_elements: Vec::new(), @@ -3437,7 +3390,6 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model .await?; codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "AFTER_SWITCH_USER".to_string(), text_elements: Vec::new(), @@ -3557,7 +3509,6 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), @@ -3572,7 +3523,6 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_TWO".to_string(), text_elements: Vec::new(), @@ -3658,7 +3608,6 @@ async fn snapshot_request_shape_remote_mid_turn_continuation_compaction() -> Res codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), @@ -3737,7 +3686,6 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinject codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), @@ -3824,7 +3772,6 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjec codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), @@ -3842,7 +3789,6 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjec codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_TWO".to_string(), text_elements: Vec::new(), @@ -3925,7 +3871,6 @@ async fn snapshot_request_shape_remote_manual_compact_without_previous_user_mess codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/compact_remote_parity.rs b/codex-rs/core/tests/suite/compact_remote_parity.rs index 395f3158d..1b7c50936 100644 --- a/codex-rs/core/tests/suite/compact_remote_parity.rs +++ b/codex-rs/core/tests/suite/compact_remote_parity.rs @@ -605,7 +605,6 @@ async fn capture_from_requests( async fn submit_user_input(codex: &codex_core::CodexThread, items: Vec) -> Result<()> { codex .submit(Op::UserInput { - environments: None, items, final_output_json_schema: None, responsesapi_client_metadata: None, diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index 47d1d1bb5..06ef8f62a 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -34,6 +34,7 @@ use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_sse_once_match; use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::sse; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; @@ -551,7 +552,7 @@ async fn snapshot_rollback_followup_turn_trims_context_updates() -> Result<()> { core_test_support::submit_thread_settings( &conversation, codex_protocol::protocol::ThreadSettingsOverrides { - cwd: Some(override_cwd.clone()), + environments: Some(local_selections(override_cwd.clone())), collaboration_mode: Some(CollaborationMode { mode: ModeKind::Default, settings: Settings { @@ -774,7 +775,6 @@ async fn start_test_conversation( async fn user_turn(conversation: &Arc, text: &str) { conversation .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: text.into(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/exec_policy.rs b/codex-rs/core/tests/suite/exec_policy.rs index 25292149a..e0d198771 100644 --- a/codex-rs/core/tests/suite/exec_policy.rs +++ b/codex-rs/core/tests/suite/exec_policy.rs @@ -17,6 +17,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_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -51,12 +52,11 @@ async fn submit_user_turn( text: prompt.into(), 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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(approval_policy), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -210,12 +210,11 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> { text: "run shell command".into(), 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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/fork_thread.rs b/codex-rs/core/tests/suite/fork_thread.rs index a1eb7eba3..b4898ea82 100644 --- a/codex-rs/core/tests/suite/fork_thread.rs +++ b/codex-rs/core/tests/suite/fork_thread.rs @@ -50,7 +50,6 @@ async fn fork_thread_twice_drops_to_first_message() { for text in ["first", "second", "third"] { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: text.to_string(), text_elements: Vec::new(), @@ -173,7 +172,6 @@ async fn fork_thread_from_history_does_not_require_source_rollout_path() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "fork me from stored history".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/guardian_review.rs b/codex-rs/core/tests/suite/guardian_review.rs index 32808dc43..ae87350c3 100644 --- a/codex-rs/core/tests/suite/guardian_review.rs +++ b/codex-rs/core/tests/suite/guardian_review.rs @@ -19,6 +19,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::skip_if_sandbox; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; @@ -111,7 +112,6 @@ printf '%s\n' "${@: -1}" >> "${payload_path}""#, test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "run a command that requires Guardian review".into(), text_elements: Vec::new(), @@ -120,7 +120,7 @@ printf '%s\n' "${@: -1}" >> "${payload_path}""#, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { - cwd: Some(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(approval_policy), approvals_reviewer: Some(ApprovalsReviewer::AutoReview), sandbox_policy: Some(sandbox_policy), diff --git a/codex-rs/core/tests/suite/hooks.rs b/codex-rs/core/tests/suite/hooks.rs index 5813508e7..16412c2d2 100644 --- a/codex-rs/core/tests/suite/hooks.rs +++ b/codex-rs/core/tests/suite/hooks.rs @@ -1843,7 +1843,6 @@ async fn blocked_queued_prompt_does_not_strand_earlier_accepted_prompt() -> Resu test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "initial prompt".to_string(), text_elements: Vec::new(), @@ -1863,7 +1862,6 @@ async fn blocked_queued_prompt_does_not_strand_earlier_accepted_prompt() -> Resu for text in ["accepted queued prompt", "blocked queued prompt"] { test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: text.to_string(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index bcfa0b739..199a7d1db 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -18,6 +18,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -125,12 +126,11 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu 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(cwd.abs()), + environments: Some(local_selections(cwd.abs())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -224,12 +224,11 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()> 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(cwd.abs()), + environments: Some(local_selections(cwd.abs())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/items.rs b/codex-rs/core/tests/suite/items.rs index a0010b6ff..a39a2b5e0 100644 --- a/codex-rs/core/tests/suite/items.rs +++ b/codex-rs/core/tests/suite/items.rs @@ -35,6 +35,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -56,12 +57,11 @@ fn disabled_plan_turn( text: text.into(), 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(cwd), + environments: Some(local_selections(cwd)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -117,7 +117,6 @@ async fn user_message_item_is_emitted() -> anyhow::Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![expected_input.clone()], final_output_json_schema: None, responsesapi_client_metadata: None, @@ -174,7 +173,6 @@ async fn assistant_message_item_is_emitted() -> anyhow::Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "please summarize results".into(), text_elements: Vec::new(), @@ -236,7 +234,6 @@ async fn reasoning_item_is_emitted() -> anyhow::Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "explain your reasoning".into(), text_elements: Vec::new(), @@ -299,7 +296,6 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "find the weather".into(), text_elements: Vec::new(), @@ -380,7 +376,6 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "generate a tiny blue square".into(), text_elements: Vec::new(), @@ -468,7 +463,6 @@ async fn image_generation_call_event_is_emitted_when_image_save_fails() -> anyho codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "generate an image".into(), text_elements: Vec::new(), @@ -525,7 +519,6 @@ async fn agent_message_content_delta_has_item_metadata() -> anyhow::Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "please stream text".into(), text_elements: Vec::new(), @@ -1110,7 +1103,6 @@ async fn reasoning_content_delta_has_item_metadata() -> anyhow::Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "reason through it".into(), text_elements: Vec::new(), @@ -1166,7 +1158,6 @@ async fn reasoning_raw_content_delta_respects_flag() -> anyhow::Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "show raw reasoning".into(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/json_result.rs b/codex-rs/core/tests/suite/json_result.rs index e65d290b8..c9a855e6a 100644 --- a/codex-rs/core/tests/suite/json_result.rs +++ b/codex-rs/core/tests/suite/json_result.rs @@ -8,6 +8,7 @@ use codex_protocol::user_input::UserInput; use core_test_support::responses; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -81,12 +82,11 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> { text: "hello world".into(), text_elements: Vec::new(), }], - environments: None, final_output_json_schema: Some(serde_json::from_str(SCHEMA)?), responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { - cwd: Some(cwd), + environments: Some(local_selections(cwd)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/mcp_turn_metadata.rs b/codex-rs/core/tests/suite/mcp_turn_metadata.rs index b0c1910d6..49d3c0f2a 100644 --- a/codex-rs/core/tests/suite/mcp_turn_metadata.rs +++ b/codex-rs/core/tests/suite/mcp_turn_metadata.rs @@ -32,6 +32,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; @@ -73,12 +74,11 @@ async fn submit_user_turn( text: text.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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(approval_policy), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/model_runtime_selectors.rs b/codex-rs/core/tests/suite/model_runtime_selectors.rs index 15ed519af..38c95394e 100644 --- a/codex-rs/core/tests/suite/model_runtime_selectors.rs +++ b/codex-rs/core/tests/suite/model_runtime_selectors.rs @@ -127,7 +127,6 @@ async fn response_body_for_remote_model( text: "list tools".into(), text_elements: Vec::new(), }], - environments: None, final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), @@ -277,7 +276,6 @@ async fn remote_multi_agent_selector_uses_model_selected_before_first_turn() -> text: ROOT_PROMPT.into(), text_elements: Vec::new(), }], - environments: None, final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index b0d4bb558..d176b5bc2 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -32,6 +32,7 @@ use core_test_support::responses::sse_completed; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -45,12 +46,11 @@ fn read_only_user_turn(test: &TestCodex, items: Vec, model: String) - turn_permission_fields(PermissionProfile::read_only(), test.cwd_path()); Op::UserInput { items, - environments: None, final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { - cwd: Some(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index 25dd52174..162346218 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -1,5 +1,6 @@ #![allow(clippy::expect_used)] +use core_test_support::test_codex::local_selections; use std::fs; use std::sync::Arc; @@ -124,12 +125,11 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { text: "first turn".into(), 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(first_turn_cwd), + environments: Some(local_selections(first_turn_cwd)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(first_sandbox_policy), permission_profile: first_permission_profile, @@ -160,12 +160,11 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { text: "second turn with context updates".into(), 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(preturn_context_diff_cwd), + environments: Some(local_selections(preturn_context_diff_cwd)), approval_policy: Some(AskForApproval::OnRequest), sandbox_policy: Some(second_sandbox_policy), permission_profile: second_permission_profile, @@ -252,12 +251,11 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R text: "first turn in agents_one".into(), 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(cwd_one.clone()), + environments: Some(local_selections(cwd_one.clone())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(first_sandbox_policy), permission_profile: first_permission_profile, @@ -286,12 +284,11 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R text: "second turn in agents_two".into(), 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(cwd_two), + environments: Some(local_selections(cwd_two)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(second_sandbox_policy), permission_profile: second_permission_profile, @@ -366,7 +363,6 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul .await; codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "seed resume history".into(), text_elements: Vec::new(), @@ -413,12 +409,11 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul text: "resume and change personality".into(), 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(resume_override_cwd), + environments: Some(local_selections(resume_override_cwd)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -483,7 +478,6 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() - .await; codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "seed resume history".into(), text_elements: Vec::new(), @@ -517,7 +511,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() - core_test_support::submit_thread_settings( &resumed.codex, codex_protocol::protocol::ThreadSettingsOverrides { - cwd: Some(resume_override_cwd), + environments: Some(local_selections(resume_override_cwd)), model: Some("gpt-5.2".to_string()), ..Default::default() }, @@ -526,7 +520,6 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() - resumed .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "first resumed turn after model override".into(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 485a51346..b343c4bba 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -1,3 +1,4 @@ +use core_test_support::test_codex::local_selections; use std::path::Path; use std::sync::Arc; @@ -97,12 +98,11 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> { text: "hi".into(), 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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(codex_protocol::protocol::AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/models_etag_responses.rs b/codex-rs/core/tests/suite/models_etag_responses.rs index 11935ca27..8b2e9f35b 100644 --- a/codex-rs/core/tests/suite/models_etag_responses.rs +++ b/codex-rs/core/tests/suite/models_etag_responses.rs @@ -1,5 +1,6 @@ #![cfg(not(target_os = "windows"))] +use core_test_support::test_codex::local_selections; use std::sync::Arc; use std::time::Duration; @@ -110,12 +111,11 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch text: "please run a tool".into(), 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(cwd_path), + environments: Some(local_selections(cwd_path)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/otel.rs b/codex-rs/core/tests/suite/otel.rs index d6c7577a7..95811d3f6 100644 --- a/codex-rs/core/tests/suite/otel.rs +++ b/codex-rs/core/tests/suite/otel.rs @@ -120,7 +120,6 @@ async fn responses_api_emits_api_request_event() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -167,7 +166,6 @@ async fn process_sse_emits_tracing_for_output_item() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -214,7 +212,6 @@ async fn process_sse_emits_failed_event_on_parse_error() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -262,7 +259,6 @@ async fn process_sse_records_failed_event_when_stream_closes_without_completed() codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -330,7 +326,6 @@ async fn process_sse_failed_event_records_response_error_message() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -396,7 +391,6 @@ async fn process_sse_failed_event_logs_parse_error() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -449,7 +443,6 @@ async fn process_sse_failed_event_logs_missing_error() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -511,7 +504,6 @@ async fn process_sse_failed_event_logs_response_completed_parse_error() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -567,7 +559,6 @@ async fn process_sse_emits_completed_telemetry() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -647,7 +638,6 @@ async fn turn_and_completed_response_spans_record_token_usage() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -736,7 +726,6 @@ async fn handle_responses_span_records_response_kind_and_tool_name() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -831,7 +820,6 @@ async fn record_responses_sets_span_fields_for_response_events() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -921,7 +909,6 @@ async fn handle_response_item_records_tool_result_for_custom_tool_call() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -998,7 +985,6 @@ async fn handle_response_item_records_tool_result_for_function_call() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1076,7 +1062,6 @@ async fn handle_response_item_records_tool_result_for_shell_command_call() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1250,7 +1235,6 @@ async fn handle_shell_command_autoapprove_from_config_records_tool_decision() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -1306,7 +1290,6 @@ async fn handle_shell_command_user_approved_records_tool_decision() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "approved".into(), text_elements: Vec::new(), @@ -1377,7 +1360,6 @@ async fn handle_shell_command_user_approved_for_session_records_tool_decision() codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "persist".into(), text_elements: Vec::new(), @@ -1448,7 +1430,6 @@ async fn handle_sandbox_error_user_approves_retry_records_tool_decision() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "retry".into(), text_elements: Vec::new(), @@ -1519,7 +1500,6 @@ async fn handle_shell_command_user_denies_records_tool_decision() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "deny".into(), text_elements: Vec::new(), @@ -1590,7 +1570,6 @@ async fn handle_sandbox_error_user_approves_for_session_records_tool_decision() codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "persist".into(), text_elements: Vec::new(), @@ -1662,7 +1641,6 @@ async fn handle_sandbox_error_user_denies_records_tool_decision() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "deny".into(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/override_updates.rs b/codex-rs/core/tests/suite/override_updates.rs index e42703dff..beb553e4e 100644 --- a/codex-rs/core/tests/suite/override_updates.rs +++ b/codex-rs/core/tests/suite/override_updates.rs @@ -9,6 +9,7 @@ use codex_protocol::protocol::Op; use core_test_support::TempDirExt; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use tempfile::TempDir; @@ -68,7 +69,7 @@ async fn thread_settings_update_without_user_turn_does_not_record_environment_up core_test_support::submit_thread_settings( &test.codex, codex_protocol::protocol::ThreadSettingsOverrides { - cwd: Some(new_cwd.abs()), + environments: Some(local_selections(new_cwd.abs())), ..Default::default() }, ) diff --git a/codex-rs/core/tests/suite/pending_input.rs b/codex-rs/core/tests/suite/pending_input.rs index 8a6443b6c..55a00a211 100644 --- a/codex-rs/core/tests/suite/pending_input.rs +++ b/codex-rs/core/tests/suite/pending_input.rs @@ -1,3 +1,4 @@ +use core_test_support::test_codex::local_selections; use std::sync::Arc; use codex_core::CodexThread; @@ -96,7 +97,6 @@ async fn build_codex(server: &StreamingSseServer) -> Arc { async fn submit_user_input(codex: &CodexThread, text: &str) { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: text.to_string(), text_elements: Vec::new(), @@ -119,12 +119,11 @@ async fn submit_danger_full_access_user_turn(test: &TestCodex, text: &str) { text: text.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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -288,7 +287,6 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "first prompt".into(), text_elements: Vec::new(), @@ -308,7 +306,6 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "second prompt".into(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index 38c0fe091..e5a69962b 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -51,7 +51,6 @@ async fn permissions_message_sent_once_on_start() -> Result<()> { test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -92,7 +91,6 @@ async fn permissions_message_added_on_override_change() -> Result<()> { test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), @@ -116,7 +114,6 @@ async fn permissions_message_added_on_override_change() -> Result<()> { test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), @@ -163,7 +160,6 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> { test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), @@ -178,7 +174,6 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> { test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), @@ -225,7 +220,6 @@ async fn permissions_message_omitted_when_disabled() -> Result<()> { test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), @@ -249,7 +243,6 @@ async fn permissions_message_omitted_when_disabled() -> Result<()> { test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), @@ -309,7 +302,6 @@ async fn resume_replays_permissions_messages() -> Result<()> { initial .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), @@ -334,7 +326,6 @@ async fn resume_replays_permissions_messages() -> Result<()> { initial .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), @@ -351,7 +342,6 @@ async fn resume_replays_permissions_messages() -> Result<()> { resumed .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "after resume".into(), text_elements: Vec::new(), @@ -412,7 +402,6 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { initial .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), @@ -437,7 +426,6 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { initial .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), @@ -460,7 +448,6 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { resumed .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "after resume".into(), text_elements: Vec::new(), @@ -496,7 +483,6 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { forked .thread .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "after fork".into(), text_elements: Vec::new(), @@ -558,7 +544,6 @@ async fn permissions_message_includes_writable_roots() -> Result<()> { test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 4bd335bb3..06c2fc41f 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -26,6 +26,7 @@ use core_test_support::responses::sse_completed; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -65,12 +66,11 @@ fn read_only_text_turn_with_personality( text: text.into(), 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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(approval_policy), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/plugins.rs b/codex-rs/core/tests/suite/plugins.rs index 03b1afdfa..412b5edbb 100644 --- a/codex-rs/core/tests/suite/plugins.rs +++ b/codex-rs/core/tests/suite/plugins.rs @@ -180,7 +180,6 @@ async fn capability_sections_render_in_developer_message_in_order() -> Result<() codex .submit(Op::UserInput { - environments: None, items: vec![codex_protocol::user_input::UserInput::Text { text: "hello".into(), text_elements: Vec::new(), @@ -261,7 +260,6 @@ async fn explicit_plugin_mentions_inject_plugin_guidance() -> Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![codex_protocol::user_input::UserInput::Mention { name: "sample".into(), path: format!("plugin://{SAMPLE_PLUGIN_CONFIG_NAME}"), @@ -345,7 +343,6 @@ async fn explicit_plugin_mentions_track_plugin_used_analytics() -> Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![codex_protocol::user_input::UserInput::Mention { name: "sample".into(), path: format!("plugin://{SAMPLE_PLUGIN_CONFIG_NAME}"), diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 640c1c1db..80dee582a 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -25,6 +25,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -153,7 +154,6 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), @@ -168,7 +168,6 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), @@ -251,7 +250,6 @@ async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> an codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), @@ -266,7 +264,6 @@ async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> an wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), @@ -334,7 +331,6 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), @@ -349,7 +345,6 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), @@ -435,7 +430,6 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an // First turn codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), @@ -474,7 +468,6 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an // Second turn after overrides codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), @@ -572,7 +565,6 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "first message".into(), text_elements: Vec::new(), @@ -729,7 +721,6 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res // First turn codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), @@ -759,12 +750,11 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res text: "hello 2".into(), 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(new_cwd.abs()), + environments: Some(local_selections(new_cwd.abs())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -876,12 +866,11 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a text: "hello 1".into(), 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(default_cwd.clone()), + environments: Some(local_selections(default_cwd.clone())), approval_policy: Some(default_approval_policy), sandbox_policy: Some(default_sandbox_policy.clone()), summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)), @@ -905,12 +894,11 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a text: "hello 2".into(), 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(default_cwd.clone()), + environments: Some(local_selections(default_cwd.clone())), approval_policy: Some(default_approval_policy), sandbox_policy: Some(default_sandbox_policy.clone()), summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)), @@ -1019,12 +1007,11 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu text: "hello 1".into(), 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(default_cwd.clone()), + environments: Some(local_selections(default_cwd.clone())), approval_policy: Some(default_approval_policy), sandbox_policy: Some(default_sandbox_policy.clone()), summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)), @@ -1050,12 +1037,11 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu text: "hello 2".into(), 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(default_cwd.clone()), + environments: Some(local_selections(default_cwd.clone())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/quota_exceeded.rs b/codex-rs/core/tests/suite/quota_exceeded.rs index 904c116cf..33f7d77fa 100644 --- a/codex-rs/core/tests/suite/quota_exceeded.rs +++ b/codex-rs/core/tests/suite/quota_exceeded.rs @@ -41,7 +41,6 @@ async fn quota_exceeded_emits_single_error_event() -> Result<()> { test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "quota?".into(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/realtime_conversation.rs b/codex-rs/core/tests/suite/realtime_conversation.rs index a2aec5c95..1c6763aeb 100644 --- a/codex-rs/core/tests/suite/realtime_conversation.rs +++ b/codex-rs/core/tests/suite/realtime_conversation.rs @@ -2158,7 +2158,6 @@ async fn conversation_user_text_turn_is_sent_to_realtime_when_active() -> Result let prefixed_user_text = format!("[USER] {user_text}"); test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: user_text.to_string(), text_elements: Vec::new(), @@ -2294,7 +2293,6 @@ async fn conversation_user_text_turn_is_capped_when_mirrored_to_realtime() -> Re ); test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: user_text.clone(), text_elements: Vec::new(), @@ -3491,7 +3489,6 @@ async fn inbound_handoff_request_steers_active_turn() -> Result<()> { test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "first prompt".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/remote_env.rs b/codex-rs/core/tests/suite/remote_env.rs index 9363bdd3b..00f5a0c51 100644 --- a/codex-rs/core/tests/suite/remote_env.rs +++ b/codex-rs/core/tests/suite/remote_env.rs @@ -41,6 +41,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::test_env; use core_test_support::wait_for_event; @@ -70,18 +71,21 @@ async fn submit_turn_with_approval_and_environments( prompt: &str, environments: Vec, ) -> Result<()> { + let turn_environment_selections = codex_protocol::protocol::TurnEnvironmentSelections::new( + test.config.cwd.clone(), + environments, + ); test.codex .submit(Op::UserInput { items: vec![UserInput::Text { text: prompt.into(), text_elements: Vec::new(), }], - environments: Some(environments), final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { - cwd: Some(test.config.cwd.clone()), + environments: Some(turn_environment_selections), approval_policy: Some(AskForApproval::OnRequest), approvals_reviewer: Some(ApprovalsReviewer::User), sandbox_policy: Some(SandboxPolicy::new_read_only_policy()), @@ -284,10 +288,7 @@ async fn exec_command_routes_to_selected_remote_environment() -> Result<()> { let test = unified_exec_test(&server).await?; let local_cwd = TempDir::new()?; fs::write(local_cwd.path().join("marker.txt"), "local-routing")?; - let local_selection = TurnEnvironmentSelection { - environment_id: LOCAL_ENVIRONMENT_ID.to_string(), - cwd: local_cwd.path().abs(), - }; + let local_selection = local(local_cwd.path().abs()); let remote_cwd = PathBuf::from(format!( "/tmp/codex-remote-routing-{}", SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() @@ -462,10 +463,7 @@ async fn remote_request_permissions_grant_unblocks_later_remote_exec() -> Result &test, "request permissions, then write in the remote environment", vec![ - TurnEnvironmentSelection { - environment_id: LOCAL_ENVIRONMENT_ID.to_string(), - cwd: local_cwd.path().abs(), - }, + local(local_cwd.path().abs()), TurnEnvironmentSelection { environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: remote_cwd.clone(), @@ -601,10 +599,7 @@ async fn apply_patch_freeform_routes_to_selected_remote_environment() -> Result< test.submit_turn_with_environments( "apply patch to remote environment", Some(vec![ - TurnEnvironmentSelection { - environment_id: LOCAL_ENVIRONMENT_ID.to_string(), - cwd: local_cwd.path().abs(), - }, + local(local_cwd.path().abs()), TurnEnvironmentSelection { environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: remote_cwd.clone(), @@ -682,10 +677,7 @@ async fn apply_patch_approvals_are_remembered_per_environment() -> Result<()> { .await?; let environments = vec![ - TurnEnvironmentSelection { - environment_id: LOCAL_ENVIRONMENT_ID.to_string(), - cwd: local_cwd.path().abs(), - }, + local(local_cwd.path().abs()), TurnEnvironmentSelection { environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: remote_cwd.clone(), @@ -882,10 +874,7 @@ async fn apply_patch_intercepted_exec_command_routes_to_selected_remote_environm test.submit_turn_with_environments( "apply patch through exec command to remote environment", Some(vec![ - TurnEnvironmentSelection { - environment_id: LOCAL_ENVIRONMENT_ID.to_string(), - cwd: local_cwd.path().abs(), - }, + local(local_cwd.path().abs()), TurnEnvironmentSelection { environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: remote_cwd.clone(), diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 76e7a1c29..dc8df93c9 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -37,6 +37,7 @@ use core_test_support::responses::sse; use core_test_support::skip_if_no_network; use core_test_support::skip_if_sandbox; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -158,7 +159,6 @@ async fn remote_models_config_context_window_override_clamps_to_max_context_wind text: "check context window".into(), text_elements: Vec::new(), }], - environments: None, final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), @@ -226,7 +226,6 @@ async fn remote_models_config_override_above_max_uses_max_context_window() -> Re text: "check context window".into(), text_elements: Vec::new(), }], - environments: None, final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), @@ -293,7 +292,6 @@ async fn remote_models_use_context_window_when_config_override_is_absent() -> Re text: "check context window".into(), text_elements: Vec::new(), }], - environments: None, final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), @@ -374,7 +372,6 @@ async fn remote_models_long_model_slug_is_sent_with_custom_reasoning() -> Result text: "check model slug".into(), text_elements: Vec::new(), }], - environments: None, final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), @@ -425,7 +422,6 @@ async fn namespaced_model_slug_uses_catalog_metadata_without_fallback_warning() text: "check namespaced model metadata".into(), text_elements: Vec::new(), }], - environments: None, final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), @@ -583,12 +579,11 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { text: "run call".into(), 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(cwd_path), + environments: Some(local_selections(cwd_path)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -811,12 +806,11 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { text: "hello remote".into(), 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(cwd_path), + environments: Some(local_selections(cwd_path)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/request_compression.rs b/codex-rs/core/tests/suite/request_compression.rs index fe9cb1e5f..e70346a5a 100644 --- a/codex-rs/core/tests/suite/request_compression.rs +++ b/codex-rs/core/tests/suite/request_compression.rs @@ -40,7 +40,6 @@ async fn request_body_is_zstd_compressed_for_codex_backend_when_enabled() -> any codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "compress me".into(), text_elements: Vec::new(), @@ -91,7 +90,6 @@ async fn request_body_is_not_compressed_for_api_key_auth_even_when_enabled() -> codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "do not compress".into(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/request_permissions.rs b/codex-rs/core/tests/suite/request_permissions.rs index 3beecfc33..f78ccf1ac 100644 --- a/codex-rs/core/tests/suite/request_permissions.rs +++ b/codex-rs/core/tests/suite/request_permissions.rs @@ -31,6 +31,7 @@ use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::skip_if_sandbox; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -195,12 +196,11 @@ async fn submit_turn( text: prompt.into(), 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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(approval_policy), approvals_reviewer: Some(ApprovalsReviewer::User), sandbox_policy: Some(sandbox_policy), diff --git a/codex-rs/core/tests/suite/request_permissions_tool.rs b/codex-rs/core/tests/suite/request_permissions_tool.rs index 9b8761c85..dc08160d7 100644 --- a/codex-rs/core/tests/suite/request_permissions_tool.rs +++ b/codex-rs/core/tests/suite/request_permissions_tool.rs @@ -28,6 +28,7 @@ use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::skip_if_sandbox; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -147,12 +148,11 @@ async fn submit_turn( text: prompt.into(), 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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(approval_policy), approvals_reviewer, sandbox_policy: Some(sandbox_policy), diff --git a/codex-rs/core/tests/suite/request_user_input.rs b/codex-rs/core/tests/suite/request_user_input.rs index 50cabe416..902c7a8bb 100644 --- a/codex-rs/core/tests/suite/request_user_input.rs +++ b/codex-rs/core/tests/suite/request_user_input.rs @@ -1,5 +1,6 @@ #![allow(clippy::unwrap_used)] +use core_test_support::test_codex::local_selections; use std::collections::HashMap; use codex_features::Feature; @@ -142,12 +143,11 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul text: "please confirm".into(), 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(cwd.abs()), + environments: Some(local_selections(cwd.abs())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -286,12 +286,11 @@ async fn request_user_input_interrupt_emits_deferred_token_count() -> anyhow::Re text: "please confirm".into(), 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(cwd.abs()), + environments: Some(local_selections(cwd.abs())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -391,12 +390,11 @@ where text: "please confirm".into(), 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(cwd.abs()), + environments: Some(local_selections(cwd.abs())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/responses_api_proxy_headers.rs b/codex-rs/core/tests/suite/responses_api_proxy_headers.rs index 8492f08fd..768bbf11d 100644 --- a/codex-rs/core/tests/suite/responses_api_proxy_headers.rs +++ b/codex-rs/core/tests/suite/responses_api_proxy_headers.rs @@ -20,6 +20,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use pretty_assertions::assert_eq; @@ -150,12 +151,11 @@ async fn submit_turn_with_timeout(test: &TestCodex, prompt: &str) -> Result<()> text: prompt.into(), 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(cwd), + environments: Some(local_selections(cwd)), approval_policy: Some(AskForApproval::OnRequest), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/resume.rs b/codex-rs/core/tests/suite/resume.rs index 0dc7eaddc..8f760db41 100644 --- a/codex-rs/core/tests/suite/resume.rs +++ b/codex-rs/core/tests/suite/resume.rs @@ -86,7 +86,6 @@ async fn resume_includes_initial_messages_from_rollout_events() -> Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "Record some messages".into(), text_elements: text_elements.clone(), @@ -173,7 +172,6 @@ async fn resume_includes_initial_messages_from_reasoning_events() -> Result<()> codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "Record reasoning messages".into(), text_elements: Vec::new(), @@ -264,7 +262,6 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "Record initial instructions".into(), text_elements: Vec::new(), @@ -308,7 +305,6 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> { resumed .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "Resume with different model".into(), text_elements: Vec::new(), @@ -327,7 +323,6 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> { resumed .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "Second turn after resume".into(), text_elements: Vec::new(), @@ -401,7 +396,6 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu .await; codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "Record initial instructions".into(), text_elements: Vec::new(), @@ -440,7 +434,6 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu resumed .codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "first turn after override".into(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index 5f07d0b39..3fdd5f83e 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -23,6 +23,7 @@ use core_test_support::responses::ResponseMock; use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; @@ -705,7 +706,6 @@ async fn review_history_surfaces_in_parent_session() { let followup = "back to parent".to_string(); codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: followup.clone(), text_elements: Vec::new(), @@ -821,7 +821,7 @@ async fn review_uses_overridden_cwd_for_base_branch_merge_base() { core_test_support::submit_thread_settings( &codex, codex_protocol::protocol::ThreadSettingsOverrides { - cwd: Some(repo_path.to_path_buf().abs()), + environments: Some(local_selections(repo_path.to_path_buf().abs())), ..Default::default() }, ) diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 974525427..5cb61fa4a 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -2,6 +2,7 @@ use anyhow::Context as _; use anyhow::ensure; +use core_test_support::test_codex::local_selections; use std::collections::HashMap; use std::ffi::OsStr; use std::ffi::OsString; @@ -128,12 +129,11 @@ fn user_turn_with_permission_profile( text: text.into(), 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(cwd), + environments: Some(local_selections(cwd)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/safety_check_downgrade.rs b/codex-rs/core/tests/suite/safety_check_downgrade.rs index d21299b1e..a271702c9 100644 --- a/codex-rs/core/tests/suite/safety_check_downgrade.rs +++ b/codex-rs/core/tests/suite/safety_check_downgrade.rs @@ -21,6 +21,7 @@ use core_test_support::responses::sse_response; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -42,12 +43,11 @@ fn disabled_text_turn(test: &TestCodex, text: &str) -> Op { text: text.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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/search_tool.rs b/codex-rs/core/tests/suite/search_tool.rs index dd8fe60d3..686b9b9e4 100644 --- a/codex-rs/core/tests/suite/search_tool.rs +++ b/codex-rs/core/tests/suite/search_tool.rs @@ -524,7 +524,6 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() - let test = builder.build(&server).await?; test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "Find the calendar create tool".to_string(), text_elements: Vec::new(), @@ -934,7 +933,6 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() - test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "Use the automation tool".to_string(), text_elements: Vec::new(), @@ -1245,7 +1243,6 @@ async fn tool_search_surfaced_mcp_tool_errors_are_returned_to_model() -> Result< test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "Find the rmcp echo tool and call it.".to_string(), text_elements: Vec::new(), @@ -1563,7 +1560,6 @@ async fn tool_search_matches_dynamic_tools_by_name_description_namespace_and_sch test.codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "Search for the dynamic tool".to_string(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/shell_snapshot.rs b/codex-rs/core/tests/suite/shell_snapshot.rs index 61ca28600..b6f38b29b 100644 --- a/codex-rs/core/tests/suite/shell_snapshot.rs +++ b/codex-rs/core/tests/suite/shell_snapshot.rs @@ -14,6 +14,7 @@ use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::sse; use core_test_support::test_codex::TestCodexHarness; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -164,12 +165,11 @@ async fn run_snapshot_command_with_options( text: "run unified exec with shell snapshot".into(), 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(cwd), + environments: Some(local_selections(cwd)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -265,12 +265,11 @@ async fn run_shell_command_snapshot_with_options( text: "run shell_command with shell snapshot".into(), 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(cwd), + environments: Some(local_selections(cwd)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -346,12 +345,11 @@ async fn run_tool_turn_on_harness( text: prompt.into(), 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(cwd), + environments: Some(local_selections(cwd)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -590,12 +588,11 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> { text: "apply patch via shell_command with snapshot".into(), 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(cwd.clone()), + environments: Some(local_selections(cwd.clone())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/skill_approval.rs b/codex-rs/core/tests/suite/skill_approval.rs index d60d719cc..50ea9495a 100644 --- a/codex-rs/core/tests/suite/skill_approval.rs +++ b/codex-rs/core/tests/suite/skill_approval.rs @@ -13,6 +13,7 @@ use core_test_support::responses::mount_function_call_agent_response; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; @@ -51,12 +52,11 @@ async fn submit_turn_with_policies( text: prompt.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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(approval_policy), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/skills.rs b/codex-rs/core/tests/suite/skills.rs index c559615d4..45b52c6ac 100644 --- a/codex-rs/core/tests/suite/skills.rs +++ b/codex-rs/core/tests/suite/skills.rs @@ -16,6 +16,7 @@ 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::skip_if_no_network; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use std::sync::Arc; @@ -85,12 +86,11 @@ async fn user_turn_includes_skill_instructions() -> Result<()> { path: skill_path.clone(), }, ], - environments: None, final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { - cwd: Some(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/sqlite_state.rs b/codex-rs/core/tests/suite/sqlite_state.rs index eb9f11a86..b4809e8a3 100644 --- a/codex-rs/core/tests/suite/sqlite_state.rs +++ b/codex-rs/core/tests/suite/sqlite_state.rs @@ -30,6 +30,7 @@ use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::stdio_server_bin; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -148,7 +149,6 @@ async fn resume_restores_dynamic_tools_from_rollout_with_sqlite_enabled() -> Res started .thread .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "persist this thread".to_string(), text_elements: Vec::new(), @@ -560,12 +560,11 @@ async fn mcp_call_marks_thread_memory_mode_polluted_when_configured() -> Result< text: "call the rmcp echo tool".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(cwd), + environments: Some(local_selections(cwd)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs b/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs index d82692c26..1870feab9 100644 --- a/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs +++ b/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs @@ -94,7 +94,6 @@ async fn continue_after_stream_error() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "first message".into(), text_elements: Vec::new(), @@ -117,7 +116,6 @@ async fn continue_after_stream_error() { // error above, this submission would be rejected/queued indefinitely. codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "follow up".into(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/stream_no_completed.rs b/codex-rs/core/tests/suite/stream_no_completed.rs index 471c60db5..6e028279b 100644 --- a/codex-rs/core/tests/suite/stream_no_completed.rs +++ b/codex-rs/core/tests/suite/stream_no_completed.rs @@ -76,7 +76,6 @@ async fn retries_on_early_close() { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello".into(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index 391b6e523..4d6cd17bb 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -30,6 +30,7 @@ use core_test_support::responses::sse_response; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event_match; @@ -768,12 +769,11 @@ async fn subagent_stop_replaces_stop_and_skips_internal_subagents() -> Result<() text: INTERNAL_SUBAGENT_PROMPT.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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/tool_harness.rs b/codex-rs/core/tests/suite/tool_harness.rs index d628c8c49..7b3f2a690 100644 --- a/codex-rs/core/tests/suite/tool_harness.rs +++ b/codex-rs/core/tests/suite/tool_harness.rs @@ -1,5 +1,6 @@ #![cfg(not(target_os = "windows"))] +use core_test_support::test_codex::local_selections; use std::fs; use assert_matches::assert_matches; @@ -108,12 +109,11 @@ async fn shell_command_tool_executes_command_and_streams_output() -> anyhow::Res text: "please run the shell command".into(), 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(cwd_path), + environments: Some(local_selections(cwd_path)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -190,12 +190,11 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> { text: "please update the plan".into(), 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(cwd_path), + environments: Some(local_selections(cwd_path)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -282,12 +281,11 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> { text: "please update the plan".into(), 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(cwd_path), + environments: Some(local_selections(cwd_path)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -384,12 +382,11 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<() text: "please apply a patch".into(), 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(cwd_path), + environments: Some(local_selections(cwd_path)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -523,12 +520,11 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> { text: "please apply a patch".into(), 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(cwd_path), + environments: Some(local_selections(cwd_path)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/tool_parallelism.rs b/codex-rs/core/tests/suite/tool_parallelism.rs index f7f046fef..8a9db0680 100644 --- a/codex-rs/core/tests/suite/tool_parallelism.rs +++ b/codex-rs/core/tests/suite/tool_parallelism.rs @@ -1,6 +1,7 @@ #![cfg(not(target_os = "windows"))] #![allow(clippy::unwrap_used)] +use core_test_support::test_codex::local_selections; use std::fs; use std::time::Duration; use std::time::Instant; @@ -42,12 +43,11 @@ async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> { text: prompt.into(), 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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -369,12 +369,11 @@ async fn shell_tools_start_before_response_completed_when_stream_delayed() -> an text: "stream delayed completion".into(), 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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/tools.rs b/codex-rs/core/tests/suite/tools.rs index 7949abfb6..fd19a18c7 100644 --- a/codex-rs/core/tests/suite/tools.rs +++ b/codex-rs/core/tests/suite/tools.rs @@ -16,7 +16,6 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::TurnEnvironmentSelection; use core_test_support::assert_regex_match; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -29,6 +28,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::skip_if_sandbox; +use core_test_support::test_codex::local; use core_test_support::test_codex::test_codex; use regex_lite::Regex; use serde_json::Value; @@ -117,10 +117,7 @@ async fn turn_environment_selection_keeps_environment_backed_tools() -> Result<( test.submit_turn_with_environments( "which tools are available?", - Some(vec![TurnEnvironmentSelection { - environment_id: "local".to_string(), - cwd: test.config.cwd.clone(), - }]), + Some(vec![local(test.config.cwd.clone())]), ) .await?; diff --git a/codex-rs/core/tests/suite/truncation.rs b/codex-rs/core/tests/suite/truncation.rs index 35a6b0df6..89ce0013e 100644 --- a/codex-rs/core/tests/suite/truncation.rs +++ b/codex-rs/core/tests/suite/truncation.rs @@ -22,6 +22,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::stdio_server_bin; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use core_test_support::wait_for_mcp_server; @@ -524,12 +525,11 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> { text: "call the rmcp image tool".into(), 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(fixture.cwd.abs()), + environments: Some(local_selections(fixture.cwd.abs())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile: Some(permission_profile), diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index 3eb2c6a6d..caea5ecf5 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -1,3 +1,4 @@ +use core_test_support::test_codex::local_selections; use std::collections::HashMap; use std::ffi::OsStr; use std::fs; @@ -198,12 +199,11 @@ async fn submit_unified_exec_turn( text: prompt.into(), 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(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -290,12 +290,11 @@ async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> { text: "apply patch via unified exec".into(), 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(cwd), + environments: Some(local_selections(cwd)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -2146,12 +2145,11 @@ async fn unified_exec_keeps_long_running_session_after_turn_end() -> Result<()> text: "keep unified exec process after turn end".into(), 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(turn_cwd), + environments: Some(local_selections(turn_cwd)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -2250,12 +2248,11 @@ async fn unified_exec_interrupt_preserves_long_running_session() -> Result<()> { text: "interrupt long-running unified exec".into(), 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(turn_cwd), + environments: Some(local_selections(turn_cwd)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -2723,12 +2720,11 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> { text: "summarize large output".into(), 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(turn_cwd), + environments: Some(local_selections(turn_cwd)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -2846,12 +2842,11 @@ async fn unified_exec_enforces_glob_deny_read_policy() -> Result<()> { text: "read the fixture files".into(), 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(turn_cwd), + environments: Some(local_selections(turn_cwd)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -2985,12 +2980,11 @@ async fn unified_exec_python_prompt_under_seatbelt() -> Result<()> { text: "start python under seatbelt".into(), 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(turn_cwd), + environments: Some(local_selections(turn_cwd)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/unified_exec_zsh_fork_approvals.rs b/codex-rs/core/tests/suite/unified_exec_zsh_fork_approvals.rs index 96ad8ee59..0d913d654 100644 --- a/codex-rs/core/tests/suite/unified_exec_zsh_fork_approvals.rs +++ b/codex-rs/core/tests/suite/unified_exec_zsh_fork_approvals.rs @@ -31,6 +31,7 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; use core_test_support::wait_for_event_with_timeout; @@ -406,12 +407,11 @@ async fn submit_turn_with_session_permissions( text: prompt.into(), text_elements: Vec::new(), }], - environments: None, final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: ThreadSettingsOverrides { - cwd: Some(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(approval_policy), approvals_reviewer: Some(ApprovalsReviewer::User), sandbox_policy: Some(sandbox_policy), diff --git a/codex-rs/core/tests/suite/user_notification.rs b/codex-rs/core/tests/suite/user_notification.rs index 054d926b2..328a1c972 100644 --- a/codex-rs/core/tests/suite/user_notification.rs +++ b/codex-rs/core/tests/suite/user_notification.rs @@ -57,7 +57,6 @@ mv "${tmp_path}" "${payload_path}""#, // 1) Normal user input – should hit server once. codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: "hello world".into(), text_elements: Vec::new(), diff --git a/codex-rs/core/tests/suite/user_shell_cmd.rs b/codex-rs/core/tests/suite/user_shell_cmd.rs index fe9715f61..f7aed6b47 100644 --- a/codex-rs/core/tests/suite/user_shell_cmd.rs +++ b/codex-rs/core/tests/suite/user_shell_cmd.rs @@ -21,6 +21,7 @@ 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::skip_if_no_network; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; @@ -179,12 +180,11 @@ async fn user_shell_command_does_not_replace_active_turn() -> anyhow::Result<()> text: "run model shell command".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(cwd), + environments: Some(local_selections(cwd)), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index 5d7263d54..582078856 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -42,6 +42,8 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event_with_timeout; @@ -74,12 +76,11 @@ fn disabled_user_turn(test: &TestCodex, items: Vec, model: String) -> turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); Op::UserInput { items, - environments: None, final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { - cwd: Some(test.config.cwd.clone()), + environments: Some(local_selections(test.config.cwd.clone())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, @@ -456,10 +457,7 @@ async fn view_image_routes_to_selected_local_environment() -> anyhow::Result<()> test.submit_turn_with_environments( "route local view image", - Some(vec![TurnEnvironmentSelection { - environment_id: LOCAL_ENVIRONMENT_ID.to_string(), - cwd: test.config.cwd.clone(), - }]), + Some(vec![local(test.config.cwd.clone())]), ) .await?; @@ -572,10 +570,7 @@ async fn view_image_routes_to_selected_remote_environment() -> anyhow::Result<() let test = builder.build_with_remote_and_local_env(&server).await?; let local_cwd = TempDir::new()?; fs::write(local_cwd.path().join("remote.png"), b"not a remote image")?; - let local_selection = TurnEnvironmentSelection { - environment_id: LOCAL_ENVIRONMENT_ID.to_string(), - cwd: local_cwd.path().abs(), - }; + let local_selection = local(local_cwd.path().abs()); let remote_cwd = PathBuf::from(format!( "/tmp/codex-view-image-routing-{}", SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() diff --git a/codex-rs/core/tests/suite/websocket_fallback.rs b/codex-rs/core/tests/suite/websocket_fallback.rs index b41f8be96..cdf77694c 100644 --- a/codex-rs/core/tests/suite/websocket_fallback.rs +++ b/codex-rs/core/tests/suite/websocket_fallback.rs @@ -14,6 +14,7 @@ use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::sse; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; +use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use pretty_assertions::assert_eq; @@ -158,12 +159,11 @@ async fn websocket_fallback_hides_first_websocket_retry_stream_error() -> Result text: "hello".into(), 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(cwd.abs()), + environments: Some(local_selections(cwd.abs())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, diff --git a/codex-rs/core/tests/suite/window_headers.rs b/codex-rs/core/tests/suite/window_headers.rs index 08d8867f3..7c28eb80a 100644 --- a/codex-rs/core/tests/suite/window_headers.rs +++ b/codex-rs/core/tests/suite/window_headers.rs @@ -104,7 +104,6 @@ async fn window_id_advances_after_compact_persists_on_resume_and_resets_on_fork( async fn submit_user_turn(codex: &Arc, text: &str) -> Result<()> { codex .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: text.to_string(), text_elements: Vec::new(), diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index fd56805d4..21793cc69 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -103,7 +103,6 @@ pub async fn run_codex_tool_session( let submission = Submission { id: sub_id.clone(), op: Op::UserInput { - environments: None, items: vec![UserInput::Text { text: initial_prompt.clone(), // MCP tool prompts are plain text with no UI element ranges. @@ -155,7 +154,6 @@ pub async fn run_codex_tool_session_reply( .insert(request_id.clone(), thread_id); if let Err(e) = thread .submit(Op::UserInput { - environments: None, items: vec![UserInput::Text { text: prompt, // MCP tool prompts are plain text with no UI element ranges. diff --git a/codex-rs/memories/write/src/runtime.rs b/codex-rs/memories/write/src/runtime.rs index 58bdc9dfc..9c15ff04e 100644 --- a/codex-rs/memories/write/src/runtime.rs +++ b/codex-rs/memories/write/src/runtime.rs @@ -259,7 +259,6 @@ impl MemoryStartupContext { .thread .submit(Op::UserInput { items: prompt, - environments: None, final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index c9e2e8bb2..642f2e5de 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -112,6 +112,34 @@ pub struct TurnEnvironmentSelection { pub cwd: AbsolutePathBuf, } +#[derive(Debug, Clone, PartialEq)] +pub struct TurnEnvironmentSelections { + pub legacy_fallback_cwd: AbsolutePathBuf, + pub environments: Vec, +} + +impl TurnEnvironmentSelections { + pub fn new( + legacy_fallback_cwd: AbsolutePathBuf, + environments: Vec, + ) -> Self { + let mut settings = Self { + legacy_fallback_cwd, + environments, + }; + settings.sync_primary_environment_cwd(); + settings + } + + fn sync_primary_environment_cwd(&mut self) { + if let Some(turn_environment) = self.environments.first_mut() + && turn_environment.cwd != self.legacy_fallback_cwd + { + turn_environment.cwd = self.legacy_fallback_cwd.clone(); + } + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)] #[serde(transparent)] #[ts(type = "string")] @@ -370,8 +398,8 @@ pub struct ConversationTextParams { /// on their own. #[derive(Debug, Clone, Default, PartialEq)] pub struct ThreadSettingsOverrides { - /// Updated `cwd` for sandbox/tool calls. - pub cwd: Option, + /// Updated fallback `cwd` and environments supplied together as a complete pair. + pub environments: Option, /// Updated runtime workspace roots used to materialize symbolic /// `:workspace_roots` filesystem permissions. @@ -472,8 +500,6 @@ pub enum Op { UserInput { /// User input items, see `InputItem` items: Vec, - /// Optional turn-scoped environments. - environments: Option>, /// Optional JSON Schema used to constrain the final assistant message for this turn. final_output_json_schema: Option, /// Optional turn-scoped Responses API `client_metadata`. @@ -612,7 +638,6 @@ pub enum ThreadMemoryMode { impl From> for Op { fn from(value: Vec) -> Self { Op::UserInput { - environments: None, items: value, final_output_json_schema: None, responsesapi_client_metadata: None, diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index 47e0fc391..91fd7a0b3 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -296,7 +296,6 @@ async fn run_turn(thread: &CodexThread, thread_id: &str, prompt: String) -> anyh text: prompt, text_elements: Vec::new(), }], - environments: None, final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(),