diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index 616225839e..47801f7a72 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -442,6 +442,7 @@ use codex_protocol::protocol::ConversationSpeechParams; use codex_protocol::protocol::ConversationStartParams; use codex_protocol::protocol::ConversationStartTransport; use codex_protocol::protocol::ConversationTextParams; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::EventMsg; #[cfg(test)] use codex_protocol::protocol::GitInfo as CoreGitInfo; @@ -637,6 +638,7 @@ fn resolve_turn_environment_selections( environment_id, cwd, workspace_roots, + config: EnvironmentConfigState::FromThread, }); } thread_manager diff --git a/codex-rs/core-api/src/lib.rs b/codex-rs/core-api/src/lib.rs index 930631b418..effe8c697f 100644 --- a/codex-rs/core-api/src/lib.rs +++ b/codex-rs/core-api/src/lib.rs @@ -112,6 +112,7 @@ pub use codex_protocol::error::Result as CodexResult; pub use codex_protocol::models::PermissionProfile; pub use codex_protocol::openai_models::ModelPreset; pub use codex_protocol::protocol::AskForApproval; +pub use codex_protocol::protocol::EnvironmentConfigState; pub use codex_protocol::protocol::EventMsg; pub use codex_protocol::protocol::Op; pub use codex_protocol::protocol::SessionConfiguredEvent; diff --git a/codex-rs/core/src/agents_md_tests.rs b/codex-rs/core/src/agents_md_tests.rs index 53cf15a838..1703978f0e 100644 --- a/codex-rs/core/src/agents_md_tests.rs +++ b/codex-rs/core/src/agents_md_tests.rs @@ -23,6 +23,7 @@ use codex_exec_server::RemoveOptions; use codex_extension_api::UserInstructions; use codex_features::Feature; use codex_protocol::models::PermissionProfile; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::TurnEnvironmentSelection; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; @@ -337,6 +338,7 @@ fn resolved_local_environments( environment_id: environment_id.to_string(), cwd: PathUri::from_abs_path(&cwd), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, Arc::new( Environment::create_for_tests(/*exec_server_url*/ None) diff --git a/codex-rs/core/src/environment_selection.rs b/codex-rs/core/src/environment_selection.rs index fb674d6b73..bbf2e428a4 100644 --- a/codex-rs/core/src/environment_selection.rs +++ b/codex-rs/core/src/environment_selection.rs @@ -17,6 +17,7 @@ use codex_protocol::capabilities::SelectedCapabilityRoot; use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; use codex_protocol::protocol::EnvironmentConfig; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::EnvironmentConnectionEvent; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; @@ -46,6 +47,7 @@ pub(crate) fn default_thread_environment_selections( environment_id, cwd: PathUri::from_abs_path(cwd), workspace_roots: workspace_roots.iter().map(PathUri::from_abs_path).collect(), + config: EnvironmentConfigState::FromThread, }) .collect() } @@ -80,17 +82,25 @@ pub(crate) struct StartingTurnEnvironment { } impl SelectedTurnEnvironment { - fn has_installed_environment_config(&self) -> bool { - self.config.selected_capability_roots.is_some() - } - - // Thread settings still own permissions, but cannot overwrite shell policy - // that was installed specifically for this environment attachment. - fn update_thread_config(&mut self, config: &TurnEnvironmentConfig) { - if !self.has_installed_environment_config() { - self.config.allow_login_shell = config.allow_login_shell; - } - self.config.permission_profile = config.permission_profile.clone(); + fn apply_configuration( + &mut self, + selection_config: EnvironmentConfigState, + thread_config: &TurnEnvironmentConfig, + ) { + self.config = match &selection_config { + EnvironmentConfigState::FromThread => thread_config.clone(), + EnvironmentConfigState::Ready(config) => TurnEnvironmentConfig { + allow_login_shell: config.allow_login_shell, + // temp read from thread_config; will go away once perms on EnvironmentConfig, + // then we can just assign passed config directly + permission_profile: thread_config.permission_profile.clone(), + selected_capability_roots: Some(config.selected_capability_roots.clone()), + }, + EnvironmentConfigState::Pending => { + unreachable!("pending environment configuration is not supported yet") + } + }; + self.selection.config = selection_config; } } @@ -156,7 +166,10 @@ impl ThreadEnvironments { }; // Child threads get their own settings, but inherit any // environment-owned policy that was already installed. - inherited_environment.update_thread_config(&thread_environment_config); + inherited_environment.apply_configuration( + inherited_environment.selection.config.clone(), + &thread_environment_config, + ); Some(inherited_environment) }) .collect(); @@ -182,13 +195,18 @@ impl ThreadEnvironments { if !seen_environment_ids.insert(selected_environment.environment_id.as_str()) { continue; } - if let Some(environment) = previous - .iter() - .find(|environment| environment.selection == *selected_environment) - && !matches!(environment.resolution.clone().now_or_never(), Some(Err(_))) + if let Some(environment) = previous.iter().find(|environment| { + let previous = &environment.selection; + previous.environment_id == selected_environment.environment_id + && previous.cwd == selected_environment.cwd + && previous.workspace_roots == selected_environment.workspace_roots + }) && !matches!(environment.resolution.clone().now_or_never(), Some(Err(_))) { let mut environment = environment.clone(); - environment.update_thread_config(thread_environment_config); + environment.apply_configuration( + selected_environment.config.clone(), + thread_environment_config, + ); next.push(environment); continue; } @@ -224,13 +242,18 @@ impl ThreadEnvironments { .remote_handle(); drop(tokio::spawn(resolution_task)); let resolution = resolution.boxed().shared(); - next.push(SelectedTurnEnvironment { + let mut selected = SelectedTurnEnvironment { selection: selected_environment.clone(), config: thread_environment_config.clone(), environment, connection_events_task, resolution, - }); + }; + selected.apply_configuration( + selected_environment.config.clone(), + thread_environment_config, + ); + next.push(selected); } let removed_connection_tasks = previous .iter() @@ -290,7 +313,7 @@ impl ThreadEnvironments { .iter() .map(|environment| { let mut environment = environment.clone(); - environment.update_thread_config(config); + environment.apply_configuration(environment.selection.config.clone(), config); environment }) .collect(); @@ -315,8 +338,8 @@ impl ThreadEnvironments { ))); }; - environment.config.allow_login_shell = config.allow_login_shell; - environment.config.selected_capability_roots = Some(config.selected_capability_roots); + let thread_config = environment.config.clone(); + environment.apply_configuration(EnvironmentConfigState::Ready(config), &thread_config); self.environments.store(Arc::new(environments)); Ok(()) } @@ -787,6 +810,7 @@ mod tests { environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: cwd_uri.clone(), workspace_roots: vec![cwd_uri], + config: EnvironmentConfigState::FromThread, }] ); } @@ -820,11 +844,13 @@ url = "ws://127.0.0.1:8765" environment_id: LOCAL_ENVIRONMENT_ID.to_string(), cwd: cwd_uri.clone(), workspace_roots: vec![cwd_uri.clone()], + config: EnvironmentConfigState::FromThread, }, TurnEnvironmentSelection { environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: cwd_uri.clone(), workspace_roots: vec![cwd_uri], + config: EnvironmentConfigState::FromThread, }, ] ); @@ -870,6 +896,7 @@ url = "ws://127.0.0.1:8765" environment_id: LOCAL_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&cwd), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }], &expected_config, ); @@ -890,6 +917,7 @@ url = "ws://127.0.0.1:8765" environment_id: LOCAL_ENVIRONMENT_ID.to_string(), cwd: cwd_uri.clone(), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }; let resolved = resolve_turn_environments( @@ -900,6 +928,7 @@ url = "ws://127.0.0.1:8765" environment_id: LOCAL_ENVIRONMENT_ID.to_string(), cwd: cwd_uri.join("other").expect("other cwd URI"), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, ], ) @@ -921,6 +950,7 @@ url = "ws://127.0.0.1:8765" environment_id: "local".to_string(), cwd: selected_cwd_uri, workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }], ) .await; @@ -960,6 +990,7 @@ url = "ws://127.0.0.1:8765" environment_id: LOCAL_ENVIRONMENT_ID.to_string(), cwd: cwd_uri.clone(), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }; let resolved = resolve_turn_environments( @@ -969,6 +1000,7 @@ url = "ws://127.0.0.1:8765" environment_id: "missing".to_string(), cwd: cwd_uri, workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, local.clone(), ], @@ -997,6 +1029,7 @@ url = "ws://127.0.0.1:8765" environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("cwd")), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }; let environments = Arc::new(ThreadEnvironments::new( manager, @@ -1056,11 +1089,13 @@ url = "ws://127.0.0.1:8765" environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: cwd.clone(), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }; let local = TurnEnvironmentSelection { environment_id: LOCAL_ENVIRONMENT_ID.to_string(), cwd, workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }; let turn_environments = ThreadEnvironments::new( manager, @@ -1163,6 +1198,7 @@ url = "ws://127.0.0.1:8765" environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("cwd")), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }; let environments = ThreadEnvironments::new( Arc::clone(&manager), @@ -1248,6 +1284,7 @@ url = "ws://127.0.0.1:8765" environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&cwd), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }; let (tx_event, rx_event) = async_channel::unbounded(); let environments = Arc::new(ThreadEnvironments::new( @@ -1337,6 +1374,7 @@ url = "ws://127.0.0.1:8765" environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&cwd), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }; let inherited_environment = Arc::new( Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string())) @@ -1391,6 +1429,7 @@ url = "ws://127.0.0.1:8765" environment_id: LOCAL_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&cwd), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }; let root = |id: &str| SelectedCapabilityRoot { id: id.to_string(), @@ -1482,6 +1521,7 @@ url = "ws://127.0.0.1:8765" environment_id: LOCAL_ENVIRONMENT_ID.to_string(), cwd: cwd_uri.clone(), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }], ) .await; @@ -1496,6 +1536,7 @@ url = "ws://127.0.0.1:8765" environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: remote_cwd_uri.clone(), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, remote_environment.clone(), /*shell*/ None, @@ -1509,6 +1550,7 @@ url = "ws://127.0.0.1:8765" environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: remote_cwd_uri, workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, remote_environment, /*shell*/ None, @@ -1535,6 +1577,7 @@ url = "ws://127.0.0.1:8765" environment_id: LOCAL_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&cwd), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, config: test_environment_config(), resolution: futures::future::pending().boxed().shared(), diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 6cd51640dd..55bcf165a7 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -26,6 +26,7 @@ use codex_hooks::HooksConfig; use codex_model_provider::create_model_provider; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::McpInvocation; @@ -1111,6 +1112,7 @@ async fn mcp_sandbox_cwd_uses_matching_server_environment_uri() -> anyhow::Resul environment_id: "remote".to_string(), cwd: secondary_cwd.clone(), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, environment, /*shell*/ None, diff --git a/codex-rs/core/src/session/environment.rs b/codex-rs/core/src/session/environment.rs index 71df217627..6da1a1239f 100644 --- a/codex-rs/core/src/session/environment.rs +++ b/codex-rs/core/src/session/environment.rs @@ -6,6 +6,7 @@ use codex_protocol::capabilities::CapabilityRootLocation; use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; use codex_protocol::protocol::EnvironmentConfig; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::TurnEnvironmentSelection; use crate::config::ConstraintResult; @@ -13,6 +14,51 @@ use crate::session::session::Session; use crate::session::session::SessionConfiguration; use crate::session::session::SessionSettingsUpdate; +pub(super) fn validate_environment_selections( + selections: &[TurnEnvironmentSelection], +) -> CodexResult<()> { + for selection in selections { + match &selection.config { + EnvironmentConfigState::FromThread => {} + EnvironmentConfigState::Pending => { + return Err(CodexErr::InvalidRequest( + "pending environment configuration is not supported yet".to_string(), + )); + } + EnvironmentConfigState::Ready(config) => { + validate_environment_config(selection, config)?; + } + } + } + Ok(()) +} + +fn validate_environment_config( + selection: &TurnEnvironmentSelection, + config: &EnvironmentConfig, +) -> CodexResult<()> { + if config.selected_capability_roots.len() > MAX_SELECTED_CAPABILITY_ROOTS { + return Err(CodexErr::InvalidRequest(format!( + "environment readiness contains more than {MAX_SELECTED_CAPABILITY_ROOTS} selected capability roots" + ))); + } + + let mut root_ids = HashSet::with_capacity(config.selected_capability_roots.len()); + for root in &config.selected_capability_roots { + let CapabilityRootLocation::Environment { environment_id, .. } = &root.location; + if root.id.trim().is_empty() + || environment_id != &selection.environment_id + || !root_ids.insert(root.id.as_str()) + { + return Err(CodexErr::InvalidRequest(format!( + "selected capability roots must have unique non-empty IDs and belong to environment `{}`", + selection.environment_id + ))); + } + } + Ok(()) +} + impl Session { pub(super) fn apply_session_settings( &self, @@ -27,25 +73,7 @@ impl Session { selection: &TurnEnvironmentSelection, config: EnvironmentConfig, ) -> CodexResult<()> { - if config.selected_capability_roots.len() > MAX_SELECTED_CAPABILITY_ROOTS { - return Err(CodexErr::InvalidRequest(format!( - "environment readiness contains more than {MAX_SELECTED_CAPABILITY_ROOTS} selected capability roots" - ))); - } - - let mut root_ids = HashSet::with_capacity(config.selected_capability_roots.len()); - for root in &config.selected_capability_roots { - let CapabilityRootLocation::Environment { environment_id, .. } = &root.location; - if root.id.trim().is_empty() - || environment_id != &selection.environment_id - || !root_ids.insert(root.id.as_str()) - { - return Err(CodexErr::InvalidRequest(format!( - "selected capability roots must have unique non-empty IDs and belong to environment `{}`", - selection.environment_id - ))); - } - } + validate_environment_config(selection, &config)?; // grab session lock so installation can't race w/ thread settings updates let _state = self.state.lock().await; diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 8cc3fe92a4..fb12a66355 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -709,7 +709,7 @@ impl Session { user_shell_override, }; session_configuration - .validate_auto_review_requirement(&environment_selections) + .validate(&environment_selections) .map_err(|err| CodexErr::InvalidRequest(err.to_string()))?; // Generate a unique ID for the lifetime of this session. diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 558d164cfb..84829d90f1 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -243,7 +243,22 @@ impl SessionConfiguration { } } - pub(super) fn validate_auto_review_requirement( + pub(super) fn validate( + &self, + environments: &[TurnEnvironmentSelection], + ) -> ConstraintResult<()> { + self.validate_auto_review_requirement(environments)?; + super::environment::validate_environment_selections(environments).map_err(|error| { + ConstraintError::InvalidValue { + field_name: "environments", + candidate: "environment configuration".to_string(), + allowed: format!("valid selected capability roots ({error})"), + requirement_source: codex_config::RequirementSource::Unknown, + } + }) + } + + fn validate_auto_review_requirement( &self, environments: &[TurnEnvironmentSelection], ) -> ConstraintResult<()> { @@ -463,7 +478,7 @@ impl SessionConfiguration { .map_or(current_environments, |environments| { environments.environments.as_slice() }); - next_configuration.validate_auto_review_requirement(next_environments)?; + next_configuration.validate(next_environments)?; Ok(next_configuration) } diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 33ae7f1c9b..a56c9bbdde 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -66,6 +66,7 @@ use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::ErrorEvent; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::TurnEnvironmentSelections; @@ -6644,6 +6645,7 @@ async fn request_permissions_tool_resolves_relative_paths_against_selected_envir environment_id: "remote".to_string(), cwd: PathUri::from_abs_path(&environment_cwd), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, current_environment.environment, current_environment.shell, @@ -7243,6 +7245,7 @@ async fn primary_environment_uses_first_turn_environment() { environment_id: "second".to_string(), cwd: second_cwd_uri.clone(), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, Arc::clone(&first_environment.environment), /*shell*/ None, @@ -8654,6 +8657,7 @@ async fn conflicting_ready_environment_root_ids_keep_first_location() { environment_id: environment_id.clone(), cwd: local_environment.cwd().clone(), workspace_roots: local_environment.workspace_roots().to_vec(), + config: EnvironmentConfigState::FromThread, }, environment, local_environment.shell.clone(), @@ -8913,6 +8917,7 @@ async fn record_context_updates_emits_environment_item_for_cwd_changes() { environment_id: environment.selection.environment_id, cwd: PathUri::from_abs_path(&cwd), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, environment.environment, environment.shell, @@ -8972,6 +8977,7 @@ async fn record_context_updates_use_environment_permission_profile_and_workspace environment_id: environment.selection.environment_id, cwd, workspace_roots: vec![PathUri::from_abs_path(&workspace_root)], + config: EnvironmentConfigState::FromThread, }, environment.environment, environment.shell, @@ -9047,6 +9053,7 @@ async fn record_context_updates_omits_environment_item_when_disabled() { environment_id: environment.selection.environment_id, cwd: PathUri::from_abs_path(&test_path_buf("/new-repo").abs()), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, environment.environment, environment.shell, @@ -9515,6 +9522,7 @@ async fn turn_context_item_stores_local_cwd() { environment_id: "remote".to_string(), cwd, workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, environment.environment, environment.shell, diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 4e9023822b..525ab9ef08 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -25,6 +25,7 @@ use codex_protocol::models::ReasoningItemReasoningSummary; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::protocol::AgentMessageEvent; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::InternalSessionSource; use codex_protocol::protocol::SessionMeta; use codex_protocol::protocol::SessionMetaLine; @@ -1143,6 +1144,7 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() { environment_id: "local".to_string(), cwd: PathUri::from_abs_path(&selected_cwd), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }]; let default_cwd = config.cwd.clone(); let mut source_config = config.clone(); diff --git a/codex-rs/core/src/tools/handlers/shell_tests.rs b/codex-rs/core/src/tools/handlers/shell_tests.rs index 40a55e903e..742083e247 100644 --- a/codex-rs/core/src/tools/handlers/shell_tests.rs +++ b/codex-rs/core/src/tools/handlers/shell_tests.rs @@ -25,6 +25,7 @@ use crate::tools::handlers::ShellCommandHandler; use crate::tools::hook_names::HookToolName; use crate::tools::registry::CoreToolRuntime; use crate::turn_diff_tracker::TurnDiffTracker; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::TurnEnvironmentSelection; use codex_shell_command::is_safe_command::is_known_safe_command; use codex_shell_command::powershell::try_find_powershell_executable_blocking; @@ -108,6 +109,7 @@ async fn shell_command_handler_to_exec_params_uses_selected_environment() { environment_id: "selected-environment".to_string(), cwd: PathUri::from_abs_path(&selected_cwd), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, Arc::clone( &turn_context diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 5be81de54e..08ddeaa815 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -7,6 +7,7 @@ use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::TurnEnvironmentSelection; use codex_sandboxing::SandboxManager; @@ -23,6 +24,7 @@ fn test_turn_environment(environment_id: &str) -> crate::session::turn_context:: environment_id: environment_id.to_string(), cwd: PathUri::from_abs_path(&std::env::temp_dir().abs()), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, std::sync::Arc::new(codex_exec_server::Environment::default_for_tests()), /*shell*/ None, diff --git a/codex-rs/core/src/tools/runtimes/shell_tests.rs b/codex-rs/core/src/tools/runtimes/shell_tests.rs index 2b84524515..1fe33bb8b2 100644 --- a/codex-rs/core/src/tools/runtimes/shell_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell_tests.rs @@ -4,6 +4,7 @@ use crate::session::turn_context::TurnEnvironmentConfig; use crate::tools::approvals::ApprovalCacheKey; use codex_exec_server::Environment; use codex_protocol::models::PermissionProfile; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::TurnEnvironmentSelection; use codex_utils_path_uri::PathUri; use pretty_assertions::assert_eq; @@ -20,6 +21,7 @@ async fn approval_key_uses_path_uri_and_includes_environment_id() { environment_id: "remote".to_string(), cwd: PathUri::from_abs_path(&cwd), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, Arc::new(Environment::default_for_tests()), /*shell*/ None, diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 12713ae69c..21ca88c384 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -520,6 +520,7 @@ mod tests { use codex_exec_server::Environment; use codex_exec_server::LOCAL_ENVIRONMENT_ID; use codex_protocol::models::PermissionProfile; + use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::TurnEnvironmentSelection; use codex_tools::ZshForkConfig; use codex_utils_absolute_path::AbsolutePathBuf; @@ -535,6 +536,7 @@ mod tests { environment_id: LOCAL_ENVIRONMENT_ID.to_string(), cwd, workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, Arc::new(Environment::default_for_tests()), /*shell*/ None, diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index 76606317d4..ddc34326e3 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -21,6 +21,7 @@ use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ToolMode; use codex_protocol::openai_models::WebSearchToolType; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::MultiAgentVersion; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; @@ -907,6 +908,7 @@ async fn zsh_fork_unified_exec_keeps_shell_parameter_when_remote_environment_ava environment_id: "remote".to_string(), cwd: remote_cwd, workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, Arc::new( codex_exec_server::Environment::create_for_tests(Some( diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 183ac761ab..01ef0af3f5 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -47,6 +47,7 @@ use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::RealtimeConversationVersion as RealtimeWsVersion; use codex_protocol::protocol::SandboxPolicy; @@ -118,6 +119,7 @@ pub fn local(cwd: AbsolutePathBuf) -> TurnEnvironmentSelection { environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&cwd), workspace_roots: vec![PathUri::from_abs_path(&cwd)], + config: EnvironmentConfigState::FromThread, } } @@ -150,6 +152,7 @@ impl TestEnv { environment_id: codex_exec_server::REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&cwd), workspace_roots: vec![PathUri::from_abs_path(&cwd)], + config: EnvironmentConfigState::FromThread, }, None => local(cwd.clone()), }; @@ -218,6 +221,7 @@ pub async fn test_env() -> Result { environment_id: codex_exec_server::REMOTE_ENVIRONMENT_ID.to_string(), cwd: cwd_uri.clone(), workspace_roots: vec![cwd_uri.clone()], + config: EnvironmentConfigState::FromThread, }; let cwd = if remote_env == TestEnvironment::WineExec { // TODO(anp): Convert `Config::cwd` to `LegacyAppPathString` and remove this diff --git a/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs b/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs index 58b5d9799a..53531a2a31 100644 --- a/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs +++ b/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs @@ -9,6 +9,7 @@ use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecCommandStatus; use codex_protocol::protocol::TurnEnvironmentSelection; @@ -115,6 +116,7 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> { environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: cwd.clone(), workspace_roots: vec![cwd], + config: EnvironmentConfigState::FromThread, } }], ); diff --git a/codex-rs/core/tests/suite/agents_md.rs b/codex-rs/core/tests/suite/agents_md.rs index ad7e76cf71..53385ecba4 100644 --- a/codex-rs/core/tests/suite/agents_md.rs +++ b/codex-rs/core/tests/suite/agents_md.rs @@ -10,6 +10,7 @@ use codex_features::Feature; use codex_history::RolloutItem; use codex_history::RolloutLine; use codex_home::CodexHomeUserInstructionsProvider; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::TurnEnvironmentSelection; @@ -658,11 +659,13 @@ async fn multi_environment_project_instructions_share_one_byte_budget() -> Resul environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&test.config.cwd), workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)], + config: EnvironmentConfigState::FromThread, }, TurnEnvironmentSelection { environment_id: LOCAL_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_host_native_path(local_root.path())?, workspace_roots: vec![PathUri::from_host_native_path(local_root.path())?], + config: EnvironmentConfigState::FromThread, }, ]), ..StartThreadOptions::new(test.config.clone()) @@ -736,11 +739,13 @@ async fn multi_environment_thread_loads_every_project_and_keeps_creation_snapsho environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&test.config.cwd), workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)], + config: EnvironmentConfigState::FromThread, }, TurnEnvironmentSelection { environment_id: LOCAL_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_host_native_path(local_root.path())?, workspace_roots: vec![PathUri::from_host_native_path(local_root.path())?], + config: EnvironmentConfigState::FromThread, }, ]), ..StartThreadOptions::new(test.config.clone()) diff --git a/codex-rs/core/tests/suite/apply_patch_cli.rs b/codex-rs/core/tests/suite/apply_patch_cli.rs index ba384c9302..2041d3ccb7 100644 --- a/codex-rs/core/tests/suite/apply_patch_cli.rs +++ b/codex-rs/core/tests/suite/apply_patch_cli.rs @@ -31,6 +31,7 @@ use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::ThreadSettingsOverrides; @@ -1886,6 +1887,7 @@ async fn apply_patch_turn_diff_tracks_local_and_remote_environment_paths() -> Re environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&shared_cwd), workspace_roots: vec![PathUri::from_abs_path(&shared_cwd)], + config: EnvironmentConfigState::FromThread, }, ]; test.codex diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index e6fbd9719d..d2fafb3d13 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -23,6 +23,7 @@ use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ThreadSettingsOverrides; use codex_protocol::protocol::TurnEnvironmentSelection; @@ -217,6 +218,7 @@ async fn model_visible_environment_context_preserves_foreign_workspace_roots() - environment_id: LOCAL_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&test.config.cwd), workspace_roots: vec![foreign_root], + config: EnvironmentConfigState::FromThread, }], )), ..Default::default() diff --git a/codex-rs/core/tests/suite/multi_exec_server_sandbox.rs b/codex-rs/core/tests/suite/multi_exec_server_sandbox.rs index 590b48ff5a..5842575fa1 100644 --- a/codex-rs/core/tests/suite/multi_exec_server_sandbox.rs +++ b/codex-rs/core/tests/suite/multi_exec_server_sandbox.rs @@ -9,6 +9,7 @@ use codex_features::Feature; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ThreadSettingsOverrides; use codex_protocol::protocol::TurnEnvironmentSelection; @@ -190,11 +191,13 @@ async fn two_exec_servers_isolate_workspace_write_roots() -> Result<()> { environment_id: FIRST_ENVIRONMENT_ID.to_string(), cwd: first_workspace_uri.clone(), workspace_roots: vec![first_workspace_uri], + config: EnvironmentConfigState::FromThread, }, TurnEnvironmentSelection { environment_id: SECOND_ENVIRONMENT_ID.to_string(), cwd: second_workspace_uri.clone(), workspace_roots: vec![second_workspace_uri], + config: EnvironmentConfigState::FromThread, }, ]; let permission_profile = PermissionProfile::workspace_write_with( diff --git a/codex-rs/core/tests/suite/network_approval.rs b/codex-rs/core/tests/suite/network_approval.rs index 730ba37990..54fe884139 100644 --- a/codex-rs/core/tests/suite/network_approval.rs +++ b/codex-rs/core/tests/suite/network_approval.rs @@ -19,6 +19,7 @@ use codex_protocol::models::NetworkPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecApprovalRequestEvent; use codex_protocol::protocol::GuardianAssessmentStatus; @@ -1837,6 +1838,7 @@ async fn approved_network_host_for_one_environment_still_prompts_in_another() -> environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&remote_cwd), workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)], + config: EnvironmentConfigState::FromThread, }, ]; diff --git a/codex-rs/core/tests/suite/remote_env.rs b/codex-rs/core/tests/suite/remote_env.rs index 42a4c7d857..89c250aa9b 100644 --- a/codex-rs/core/tests/suite/remote_env.rs +++ b/codex-rs/core/tests/suite/remote_env.rs @@ -56,6 +56,7 @@ use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::ApplyPatchApprovalRequestEvent; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; @@ -415,6 +416,7 @@ async fn explicit_remote_shell_runs_in_remote_cwd() -> Result<()> { environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&test.config.cwd), workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)], + config: EnvironmentConfigState::FromThread, }]), ) .await?; @@ -693,6 +695,7 @@ async fn deferred_executor_promotes_primary_environment_when_startup_completes() environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&test.config.cwd), workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)], + config: EnvironmentConfigState::FromThread, }; test.submit_turn_with_environments( @@ -1005,36 +1008,69 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment( }, }; + for config in [ + EnvironmentConfigState::Pending, + EnvironmentConfigState::Ready(EnvironmentConfig { + allow_login_shell: false, + selected_capability_roots: vec![root("duplicate"), root("duplicate")], + }), + ] { + let invalid_selection = TurnEnvironmentSelection { + config, + ..selection.clone() + }; + assert!( + test.codex + .preview_thread_settings_overrides(CodexThreadSettingsOverrides { + environments: Some(TurnEnvironmentSelections::new( + test.config.cwd.clone(), + vec![invalid_selection], + )), + ..Default::default() + }) + .await + .is_err() + ); + assert_eq!( + test.codex.environment_selections().await, + vec![selection.clone()] + ); + } + let mut second_thread_init = ExtensionDataInit::new(); second_thread_init.insert(vec![root("startup-root")]); let second = test .thread_manager .start_thread(StartThreadOptions { - environments: Some(vec![selection.clone()]), + environments: Some(vec![TurnEnvironmentSelection { + config: EnvironmentConfigState::Ready(EnvironmentConfig { + allow_login_shell: true, + selected_capability_roots: vec![root("startup-root"), root("second-root")], + }), + ..selection.clone() + }]), thread_extension_init: second_thread_init, ..StartThreadOptions::new(test.config.clone()) }) .await?; - test.codex - .environment_ready( - &selection, - EnvironmentConfig { - allow_login_shell: false, - selected_capability_roots: vec![root("first-root")], - }, - ) - .await?; - second - .thread - .environment_ready( - &selection, - EnvironmentConfig { - allow_login_shell: true, - selected_capability_roots: vec![root("startup-root"), root("second-root")], - }, - ) - .await?; + submit_thread_settings( + &test.codex, + ThreadSettingsOverrides { + environments: Some(TurnEnvironmentSelections::new( + test.config.cwd.clone(), + vec![TurnEnvironmentSelection { + config: EnvironmentConfigState::Ready(EnvironmentConfig { + allow_login_shell: false, + selected_capability_roots: vec![root("first-root")], + }), + ..selection.clone() + }], + )), + ..Default::default() + }, + ) + .await?; assert_eq!( test.codex.inspect_selected_capability_roots().ready_roots, @@ -1071,24 +1107,27 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment( .into_iter() .enumerate() { + let mut request = TurnInputRequest::user_input(vec![UserInput::Text { + text: prompt.to_string(), + text_elements: Vec::new(), + }]); if index == 2 { - test.codex - .environment_ready( - &selection, - EnvironmentConfig { - allow_login_shell: false, - selected_capability_roots: vec![root("first-updated-root")], - }, - ) - .await?; + request = request.with_thread_settings(ThreadSettingsOverrides { + environments: Some(TurnEnvironmentSelections::new( + test.config.cwd.clone(), + vec![TurnEnvironmentSelection { + config: EnvironmentConfigState::Ready(EnvironmentConfig { + allow_login_shell: false, + selected_capability_roots: vec![root("first-updated-root")], + }), + ..selection.clone() + }], + )), + ..Default::default() + }); } - thread - .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { - text: prompt.to_string(), - text_elements: Vec::new(), - }])) - .await?; + thread.start_or_steer_turn(request).await?; wait_for_event(thread, |event| matches!(event, EventMsg::TurnComplete(_))).await; } @@ -1288,6 +1327,7 @@ async fn ready_before_selection_exposes_remote_tools_and_capability_context_afte environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&test.config.cwd), workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)], + config: EnvironmentConfigState::FromThread, }]), ) .await?; @@ -1390,6 +1430,7 @@ async fn deferred_executor_stays_pending_after_materialization() -> Result<()> { environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&test.config.cwd), workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)], + config: EnvironmentConfigState::FromThread, }], )), ..Default::default() @@ -1514,6 +1555,7 @@ async fn deferred_executor_spawn_agent_inherits_ready_step_environments( environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&test.config.cwd), workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)], + config: EnvironmentConfigState::FromThread, }; let expected_environments = vec![remote_selection, local(test.config.cwd.clone())]; let mut created_threads = test.thread_manager.subscribe_thread_created(); @@ -1646,6 +1688,7 @@ async fn deferred_executor_guardian_uses_newly_ready_step_environment() -> Resul environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&remote_cwd), workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)], + config: EnvironmentConfigState::FromThread, }; let permission_profile = PermissionProfile::from_runtime_permissions( &FileSystemSandboxPolicy::restricted(vec![ @@ -2142,6 +2185,7 @@ async fn exec_command_routes_to_selected_remote_environment() -> Result<()> { environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&remote_cwd), workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)], + config: EnvironmentConfigState::FromThread, }; let multi_env_output = exec_command_routing_output( &test, @@ -2280,11 +2324,13 @@ async fn remote_exec_materializes_target_roots_before_sandbox_selection() -> Res environment_id: LOCAL_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&local_cwd.path().abs()), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }, TurnEnvironmentSelection { environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: remote_cwd_uri.clone(), workspace_roots: vec![remote_cwd_uri.clone()], + config: EnvironmentConfigState::FromThread, }, ], )), @@ -2453,6 +2499,7 @@ async fn remote_request_permissions_grant_unblocks_later_remote_exec() -> Result environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&remote_cwd), workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)], + config: EnvironmentConfigState::FromThread, }, ], AskForApproval::OnRequest, @@ -2595,6 +2642,7 @@ async fn apply_patch_freeform_routes_to_selected_remote_environment() -> Result< environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&remote_cwd), workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)], + config: EnvironmentConfigState::FromThread, }, ]), ) @@ -2679,6 +2727,7 @@ async fn apply_patch_approvals_are_remembered_per_environment() -> Result<()> { environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&remote_cwd), workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)], + config: EnvironmentConfigState::FromThread, }, ]; let local_patch = format!( @@ -2881,6 +2930,7 @@ async fn apply_patch_intercepted_exec_command_routes_to_selected_remote_environm environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&remote_cwd), workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)], + config: EnvironmentConfigState::FromThread, }, ]), ) diff --git a/codex-rs/core/tests/suite/unified_exec_process_events.rs b/codex-rs/core/tests/suite/unified_exec_process_events.rs index 54d02a2b1b..952f4c5b36 100644 --- a/codex-rs/core/tests/suite/unified_exec_process_events.rs +++ b/codex-rs/core/tests/suite/unified_exec_process_events.rs @@ -17,6 +17,7 @@ use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; @@ -704,6 +705,7 @@ timeout = 900 PathUri::parse("file:///D:/other-workspace") .expect("valid Windows workspace root"), ], + config: EnvironmentConfigState::FromThread, }], ) }), diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index 99f54529bc..89b8936a8a 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -29,6 +29,7 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ThreadSettingsOverrides; use codex_protocol::protocol::TurnEnvironmentSelection; @@ -748,6 +749,7 @@ async fn view_image_routes_to_selected_remote_environment() -> anyhow::Result<() environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: remote_cwd_uri.clone(), workspace_roots: vec![remote_cwd_uri], + config: EnvironmentConfigState::FromThread, }; let relative_call_id = "call-view-image-relative-multi-env"; let absolute_call_id = "call-view-image-absolute-multi-env"; diff --git a/codex-rs/ext/skills/tests/skills_extension.rs b/codex-rs/ext/skills/tests/skills_extension.rs index 2004a1f386..b6fd8b4b83 100644 --- a/codex-rs/ext/skills/tests/skills_extension.rs +++ b/codex-rs/ext/skills/tests/skills_extension.rs @@ -37,6 +37,7 @@ use codex_otel::THREAD_SKILLS_KEPT_TOTAL_METRIC; use codex_otel::THREAD_SKILLS_TRUNCATED_METRIC; use codex_protocol::capabilities::CapabilityRootLocation; use codex_protocol::capabilities::SelectedCapabilityRoot; +use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::Event; use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG; use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG; @@ -1031,6 +1032,7 @@ async fn selected_executor_catalog_follows_step_availability_and_reuses_its_cach environment_id: "turn-env".to_string(), cwd: PathUri::parse("file:///workspace").expect("cwd URI"), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }; let available_sections = registry.context_contributors()[0] .contribute_world_state(WorldStateContributionInput { @@ -1752,6 +1754,7 @@ async fn root_qualified_locator_selects_only_the_matching_executor_skill() -> Te environment_id: "env-1".to_string(), cwd: PathUri::parse("file:///workspace").expect("cwd URI"), workspace_roots: Vec::new(), + config: EnvironmentConfigState::FromThread, }], ready_selected_capability_roots: &selected_roots, executor_capability_discovery: None, diff --git a/codex-rs/protocol/src/environment.rs b/codex-rs/protocol/src/environment.rs index cb68e45962..54a55f3af4 100644 --- a/codex-rs/protocol/src/environment.rs +++ b/codex-rs/protocol/src/environment.rs @@ -1,5 +1,16 @@ use crate::capabilities::SelectedCapabilityRoot; +/// Configuration supplied for a thread's selected environment. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EnvironmentConfigState { + /// Preserve the existing thread-derived environment configuration. + FromThread, + /// The owner will supply environment configuration later. + Pending, + /// The owner supplied configuration for this environment attachment. + Ready(EnvironmentConfig), +} + /// Resolved configuration supplied by the owner of a thread/environment attachment. #[derive(Clone, Debug, PartialEq, Eq)] pub struct EnvironmentConfig { diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index ca6d1ec1c1..bd5579987b 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -91,6 +91,7 @@ pub use crate::approvals::NetworkApprovalProtocol; pub use crate::approvals::NetworkPolicyAmendment; pub use crate::approvals::NetworkPolicyRuleAction; pub use crate::environment::EnvironmentConfig; +pub use crate::environment::EnvironmentConfigState; pub use crate::legacy_events::HasLegacyEvent; pub use crate::permissions::FileSystemAccessMode; pub use crate::permissions::FileSystemPath; @@ -146,6 +147,7 @@ pub struct TurnEnvironmentSelection { pub environment_id: String, pub cwd: PathUri, pub workspace_roots: Vec, + pub config: EnvironmentConfigState, } #[derive(Debug, Clone, PartialEq)]