Use selected turn environments for runtime context (#20281)

## Summary
- make selected turn environments the source of truth for session
runtime cwd and MCP runtime environment selection
- keep local/no-selection fallback behavior intact
- add coverage for duplicate selected environments, cwd resolution, and
MCP runtime environment selection

## Validation
- git diff --check
- rustfmt was run on touched Rust files during the implementation
workflow

CI should provide the full Bazel/test signal.

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
starr-openai
2026-05-01 11:00:14 -07:00
committed by GitHub
parent e4d6675632
commit be71b6fcd1
18 changed files with 456 additions and 242 deletions

View File

@@ -30,6 +30,7 @@ use tokio::time::timeout;
use tokio_util::sync::CancellationToken;
use crate::config::Config;
use crate::environment_selection::ResolvedTurnEnvironments;
use crate::guardian::GuardianApprovalRequest;
use crate::guardian::new_guardian_review_id;
use crate::guardian::routes_approval_to_guardian;
@@ -47,7 +48,6 @@ use crate::session::SUBMISSION_CHANNEL_CAPACITY;
use crate::session::emit_subagent_session_started;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
use crate::session::turn_context::TurnEnvironment;
use codex_login::AuthManager;
use codex_models_manager::manager::SharedModelsManager;
use codex_protocol::error::CodexErr;
@@ -94,11 +94,9 @@ pub(crate) async fn run_codex_thread_interactive(
inherited_exec_policy: Some(Arc::clone(&parent_session.services.exec_policy)),
parent_rollout_thread_trace: codex_rollout_trace::ThreadTraceContext::disabled(),
parent_trace: None,
environments: parent_ctx
.environments
.iter()
.map(TurnEnvironment::selection)
.collect(),
environment_selections: ResolvedTurnEnvironments {
turn_environments: parent_ctx.environments.clone(),
},
analytics_events_client: Some(parent_session.services.analytics_events_client.clone()),
thread_store: Arc::clone(&parent_session.services.thread_store),
}))

View File

@@ -1,12 +1,15 @@
use std::collections::HashSet;
use std::sync::Arc;
use codex_exec_server::Environment;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::ExecutorFileSystem;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_utils_absolute_path::AbsolutePathBuf;
use crate::session::turn_context::TurnEnvironment;
pub(crate) fn default_thread_environment_selections(
environment_manager: &EnvironmentManager,
cwd: &AbsolutePathBuf,
@@ -21,42 +24,61 @@ pub(crate) fn default_thread_environment_selections(
.collect()
}
pub(crate) fn validate_environment_selections(
#[derive(Clone, Debug)]
pub(crate) struct ResolvedTurnEnvironments {
pub(crate) turn_environments: Vec<TurnEnvironment>,
}
impl ResolvedTurnEnvironments {
pub(crate) fn to_selections(&self) -> Vec<TurnEnvironmentSelection> {
self.turn_environments
.iter()
.map(TurnEnvironment::selection)
.collect()
}
pub(crate) fn primary_turn_environment(&self) -> Option<&TurnEnvironment> {
self.turn_environments.first()
}
pub(crate) fn primary_environment(&self) -> Option<Arc<codex_exec_server::Environment>> {
self.primary_turn_environment()
.map(|environment| Arc::clone(&environment.environment))
}
pub(crate) fn primary_filesystem(&self) -> Option<Arc<dyn ExecutorFileSystem>> {
self.primary_turn_environment()
.map(|environment| environment.environment.get_filesystem())
}
}
pub(crate) fn resolve_environment_selections(
environment_manager: &EnvironmentManager,
environments: &[TurnEnvironmentSelection],
) -> CodexResult<()> {
) -> CodexResult<ResolvedTurnEnvironments> {
let mut seen_environment_ids = HashSet::with_capacity(environments.len());
let mut turn_environments = Vec::with_capacity(environments.len());
for selected_environment in environments {
if environment_manager
.get_environment(&selected_environment.environment_id)
.is_none()
{
if !seen_environment_ids.insert(selected_environment.environment_id.as_str()) {
return Err(CodexErr::InvalidRequest(format!(
"unknown turn environment id `{}`",
"duplicate turn environment id `{}`",
selected_environment.environment_id
)));
}
let environment_id = selected_environment.environment_id.clone();
let environment = environment_manager
.get_environment(&environment_id)
.ok_or_else(|| {
CodexErr::InvalidRequest(format!("unknown turn environment id `{environment_id}`"))
})?;
turn_environments.push(TurnEnvironment {
environment_id,
environment,
cwd: selected_environment.cwd.clone(),
});
}
Ok(())
}
pub(crate) fn selected_primary_environment(
environment_manager: &EnvironmentManager,
environments: &[TurnEnvironmentSelection],
) -> CodexResult<Option<Arc<Environment>>> {
environments
.first()
.map(|selected_environment| {
environment_manager
.get_environment(&selected_environment.environment_id)
.ok_or_else(|| {
CodexErr::InvalidRequest(format!(
"unknown turn environment id `{}`",
selected_environment.environment_id
))
})
})
.transpose()
Ok(ResolvedTurnEnvironments { turn_environments })
}
#[cfg(test)]
@@ -105,4 +127,51 @@ mod tests {
Vec::<TurnEnvironmentSelection>::new()
);
}
#[tokio::test]
async fn resolve_environment_selections_rejects_duplicate_ids() {
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
let manager = EnvironmentManager::default_for_tests();
let err = resolve_environment_selections(
&manager,
&[
TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: cwd.clone(),
},
TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: cwd.join("other"),
},
],
)
.expect_err("duplicate environment id should fail");
assert!(err.to_string().contains("duplicate"));
}
#[tokio::test]
async fn resolved_environment_selections_use_first_selection_as_primary() {
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
let selected_cwd = cwd.join("selected");
let manager = EnvironmentManager::default_for_tests();
let resolved = resolve_environment_selections(
&manager,
&[TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: selected_cwd,
}],
)
.expect("environment selections should resolve");
assert_eq!(
resolved
.primary_turn_environment()
.expect("primary environment")
.environment_id,
"local"
);
}
}

View File

@@ -221,6 +221,19 @@ impl Session {
let mcp_servers = with_codex_apps_mcp(mcp_servers, auth.as_ref(), &mcp_config);
let auth_statuses =
compute_auth_statuses(mcp_servers.iter(), store_mode, auth.as_ref()).await;
let mcp_runtime_environment = match turn_context.primary_environment() {
Some(turn_environment) => McpRuntimeEnvironment::new(
Arc::clone(&turn_environment.environment),
turn_environment.cwd.to_path_buf(),
),
None => McpRuntimeEnvironment::new(
self.services
.environment_manager
.default_environment()
.unwrap_or_else(|| self.services.environment_manager.local_environment()),
turn_context.cwd.to_path_buf(),
),
};
{
let mut guard = self.services.mcp_startup_cancellation_token.lock().await;
guard.cancel();
@@ -234,13 +247,7 @@ impl Session {
turn_context.sub_id.clone(),
self.get_tx_event(),
turn_context.permission_profile(),
McpRuntimeEnvironment::new(
turn_context
.environment
.clone()
.unwrap_or_else(|| self.services.environment_manager.local_environment()),
turn_context.cwd.to_path_buf(),
),
mcp_runtime_environment,
config.codex_home.to_path_buf(),
codex_apps_tools_cache_key(auth.as_ref()),
tool_plugin_provenance,

View File

@@ -30,8 +30,7 @@ use crate::context::NetworkRuleSaved;
use crate::context::PermissionsInstructions;
use crate::context::PersonalitySpecInstructions;
use crate::default_skill_metadata_budget;
use crate::environment_selection::selected_primary_environment;
use crate::environment_selection::validate_environment_selections;
use crate::environment_selection::ResolvedTurnEnvironments;
use crate::exec_policy::ExecPolicyManager;
use crate::installation_id::resolve_installation_id;
use crate::parse_turn_item;
@@ -113,7 +112,6 @@ use codex_protocol::protocol::SubAgentSource;
use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::protocol::TurnContextItem;
use codex_protocol::protocol::TurnContextNetworkItem;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::W3cTraceContext;
use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile;
@@ -410,7 +408,7 @@ pub(crate) struct CodexSpawnArgs {
pub(crate) parent_rollout_thread_trace: ThreadTraceContext,
pub(crate) user_shell_override: Option<shell::Shell>,
pub(crate) parent_trace: Option<W3cTraceContext>,
pub(crate) environments: Vec<TurnEnvironmentSelection>,
pub(crate) environment_selections: ResolvedTurnEnvironments,
pub(crate) analytics_events_client: Option<AnalyticsEventsClient>,
pub(crate) thread_store: Arc<dyn ThreadStore>,
}
@@ -467,18 +465,13 @@ impl Codex {
inherited_exec_policy,
parent_rollout_thread_trace,
parent_trace: _,
environments,
environment_selections,
analytics_events_client,
thread_store,
} = args;
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
let (tx_event, rx_event) = async_channel::unbounded();
validate_environment_selections(environment_manager.as_ref(), &environments)?;
let environment =
selected_primary_environment(environment_manager.as_ref(), &environments)?;
let fs = environment
.as_ref()
.map(|environment| environment.get_filesystem());
let fs = environment_selections.primary_filesystem();
let plugins_input = config.plugins_config_input();
let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await;
let effective_skill_roots = plugin_outcome.effective_skill_roots();
@@ -501,8 +494,9 @@ impl Codex {
let _ = config.features.disable(Feature::Collab);
}
let primary_environment = environment_selections.primary_environment();
let user_instructions = AgentsMdManager::new(&config)
.user_instructions(environment.as_deref())
.user_instructions(primary_environment.as_deref())
.await;
let exec_policy = if crate::guardian::is_guardian_reviewer_source(&session_source) {
@@ -614,7 +608,7 @@ impl Codex {
cwd: config.cwd.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments,
environments: environment_selections.to_selections(),
original_config_do_not_use: Arc::clone(&config),
metrics_service_name,
app_server_client_name: None,

View File

@@ -123,7 +123,6 @@ pub(super) async fn spawn_review_thread(
reasoning_effort,
reasoning_summary,
session_source,
environment: parent_turn_context.environment.clone(),
environments: parent_turn_context.environments.clone(),
tools_config,
features: parent_turn_context.features.clone(),

View File

@@ -3,6 +3,7 @@ use crate::goals::GoalRuntimeState;
use codex_otel::LEGACY_NOTIFY_CONFIGURED_METRIC;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::TurnEnvironmentSelection;
use tokio::sync::Semaphore;
/// Context for an initialized model agent
@@ -207,12 +208,7 @@ impl SessionConfiguration {
.unwrap_or_else(|| self.cwd.clone());
let cwd_changed = absolute_cwd.as_path() != self.cwd.as_path();
next_configuration.cwd = absolute_cwd.clone();
if cwd_changed
&& let Some(primary_environment) = next_configuration.environments.first_mut()
{
primary_environment.cwd = absolute_cwd;
}
next_configuration.cwd = absolute_cwd;
if let Some(permission_profile) = updates.permission_profile.clone() {
let active_permission_profile =
@@ -962,6 +958,31 @@ impl Session {
cancel_guard.cancel();
*cancel_guard = CancellationToken::new();
}
let turn_environment = crate::environment_selection::resolve_environment_selections(
sess.services.environment_manager.as_ref(),
&session_configuration.environments,
)
.map_err(|err| {
CodexErr::InvalidRequest(err.to_string().replace(
"unknown turn environment id",
"unknown stored MCP environment id",
))
})?
.primary_turn_environment()
.cloned();
let mcp_runtime_environment = match turn_environment {
Some(turn_environment) => McpRuntimeEnvironment::new(
Arc::clone(&turn_environment.environment),
turn_environment.cwd.to_path_buf(),
),
None => McpRuntimeEnvironment::new(
sess.services
.environment_manager
.default_environment()
.unwrap_or_else(|| sess.services.environment_manager.local_environment()),
session_configuration.cwd.to_path_buf(),
),
};
let (mcp_connection_manager, cancel_token) = McpConnectionManager::new(
&mcp_servers,
config.mcp_oauth_credentials_store_mode,
@@ -970,13 +991,7 @@ impl Session {
INITIAL_SUBMIT_ID.to_owned(),
tx_event.clone(),
session_configuration.permission_profile(),
McpRuntimeEnvironment::new(
sess.services
.environment_manager
.default_environment()
.unwrap_or_else(|| sess.services.environment_manager.local_environment()),
session_configuration.cwd.to_path_buf(),
),
mcp_runtime_environment,
config.codex_home.to_path_buf(),
codex_apps_tools_cache_key(auth),
tool_plugin_provenance,

View File

@@ -47,6 +47,7 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::NonSteerableTurnKind;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile;
use tracing::Span;
@@ -3289,7 +3290,7 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up
}
#[tokio::test]
async fn session_update_settings_keeps_runtime_cwds_absolute() {
async fn session_update_settings_does_not_rewrite_sticky_environment_cwds() {
let (session, turn_context) = make_session_and_context().await;
let updated_cwd = turn_context.cwd.join("project");
std::fs::create_dir_all(updated_cwd.as_path()).expect("create project dir");
@@ -3315,6 +3316,91 @@ async fn session_update_settings_keeps_runtime_cwds_absolute() {
assert_eq!(next_turn.config.cwd, updated_cwd);
}
#[tokio::test]
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()
};
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(PathBuf::from("project")),
..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());
}
#[tokio::test]
async fn cwd_update_does_not_rewrite_sticky_environment_cwd() {
let (session, _turn_context) = make_session_and_context().await;
let (original_cwd, environment_cwd) = {
let mut state = session.state.lock().await;
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 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(PathBuf::from("project")),
..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.environments[0].cwd,
environment_cwd
);
}
#[tokio::test]
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")
};
std::fs::create_dir_all(absolute_cwd.as_path()).expect("create absolute turn dir");
let turn_context = session
.new_turn_with_sub_id(
"sub-1".to_string(),
SessionSettingsUpdate {
cwd: Some(absolute_cwd.to_path_buf()),
environments: Some(vec![TurnEnvironmentSelection {
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
cwd: absolute_cwd.clone(),
}]),
..Default::default()
},
)
.await
.expect("absolute cwd with explicit environments should succeed");
assert_eq!(turn_context.cwd, absolute_cwd);
assert_eq!(turn_context.config.cwd, absolute_cwd);
assert_eq!(turn_context.environments.len(), 1);
}
#[tokio::test]
async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
let codex_home = tempfile::tempdir().expect("create temp dir");
@@ -3594,7 +3680,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
model_info,
&models_manager,
/*network*/ None,
Some(environment),
turn_environments,
session_configuration.cwd.clone(),
"turn_id".to_string(),
@@ -4333,23 +4418,24 @@ async fn turn_environments_set_primary_environment() {
let turn_environments = &turn_context.environments;
assert_eq!(turn_environments.len(), 1);
let turn_environment = turn_context
.primary_environment()
.expect("primary environment should be set");
assert!(std::sync::Arc::ptr_eq(
turn_context
.environment
.as_ref()
.expect("primary environment should be set"),
&turn_environment.environment,
&turn_environments[0].environment
));
assert!(!turn_context.environments.is_empty());
assert_eq!(turn_context.cwd.as_path(), selected_cwd.as_path());
assert_eq!(turn_context.config.cwd.as_path(), selected_cwd.as_path());
}
#[tokio::test]
async fn default_turn_uses_stored_thread_environments() {
async fn default_turn_overlays_session_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 =
AbsolutePathBuf::try_from(session.get_config().await.cwd.as_path().join("selected"))
.expect("absolute path");
AbsolutePathBuf::try_from(session_cwd.as_path().join("selected")).expect("absolute path");
{
let mut state = session.state.lock().await;
@@ -4363,15 +4449,15 @@ async fn default_turn_uses_stored_thread_environments() {
let turn_environments = &turn_context.environments;
assert_eq!(turn_environments.len(), 1);
let turn_environment = turn_context
.primary_environment()
.expect("primary environment should be set");
assert!(std::sync::Arc::ptr_eq(
turn_context
.environment
.as_ref()
.expect("primary environment should be set"),
&turn_environment.environment,
&turn_environments[0].environment
));
assert_eq!(turn_context.cwd, selected_cwd);
assert_eq!(turn_context.config.cwd, selected_cwd);
assert_eq!(turn_context.cwd, session_cwd);
assert_eq!(turn_context.config.cwd, session_cwd);
}
#[tokio::test]
@@ -4386,54 +4472,42 @@ async fn default_turn_honors_empty_stored_thread_environments() {
let turn_context = session.new_default_turn().await;
assert!(turn_context.environment.is_none());
assert!(turn_context.primary_environment().is_none());
assert!(turn_context.environments.is_empty());
assert_eq!(turn_context.cwd, session_cwd);
assert_eq!(turn_context.config.cwd, session_cwd);
assert_eq!(turn_context.environments.len(), 0);
}
#[tokio::test]
async fn multiple_turn_environments_use_first_as_primary_environment() {
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
let session_cwd = session.get_config().await.cwd.clone();
let first_cwd =
AbsolutePathBuf::try_from(session_cwd.as_path().join("first")).expect("absolute path");
let second_cwd =
AbsolutePathBuf::try_from(session_cwd.as_path().join("second")).expect("absolute path");
async fn primary_environment_uses_first_turn_environment() {
let (_session, mut turn_context) = make_session_and_context().await;
let first_environment = turn_context.environments[0].clone();
let second_cwd = turn_context.cwd.join("second");
turn_context.environments.push(TurnEnvironment {
environment_id: "second".to_string(),
environment: Arc::clone(&first_environment.environment),
cwd: second_cwd.clone(),
});
let turn_context = session
.new_turn_with_sub_id(
"sub-1".to_string(),
SessionSettingsUpdate {
environments: Some(vec![
TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: first_cwd.clone(),
},
TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: second_cwd.clone(),
},
]),
..Default::default()
},
)
.await
.expect("turn should start");
let turn_environments = &turn_context.environments;
assert_eq!(turn_environments.len(), 2);
assert_eq!(turn_environments[0].cwd, first_cwd);
assert_eq!(turn_environments[1].cwd, second_cwd);
assert!(std::sync::Arc::ptr_eq(
assert_eq!(
turn_context
.environment
.as_ref()
.expect("primary environment should be set"),
&turn_environments[0].environment
));
assert_eq!(turn_context.cwd, first_cwd);
assert_eq!(turn_context.config.cwd, first_cwd);
.primary_environment()
.expect("primary environment")
.environment_id,
first_environment.environment_id
);
assert_eq!(
turn_context
.environments
.iter()
.find(|environment| environment.environment_id == "second")
.expect("second environment")
.cwd,
second_cwd
);
assert_eq!(turn_context.environments.len(), 2);
assert_eq!(turn_context.environments[1].cwd, second_cwd);
}
#[tokio::test]
@@ -4451,15 +4525,19 @@ async fn empty_turn_environments_clear_primary_environment() {
.await
.expect("turn should start");
assert!(turn_context.environment.is_none());
assert!(turn_context.primary_environment().is_none());
assert!(turn_context.environments.is_empty());
assert_eq!(turn_context.cwd, session.get_config().await.cwd);
assert_eq!(turn_context.config.cwd, session.get_config().await.cwd);
assert_eq!(turn_context.environments.len(), 0);
}
#[tokio::test]
async fn unknown_turn_environment_returns_error() {
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
let original_configuration = {
let state = session.state.lock().await;
state.session_configuration.clone()
};
let err = session
.new_turn_with_sub_id(
@@ -4467,7 +4545,7 @@ async fn unknown_turn_environment_returns_error() {
SessionSettingsUpdate {
environments: Some(vec![TurnEnvironmentSelection {
environment_id: "missing".to_string(),
cwd: session.get_config().await.cwd.clone(),
cwd: original_configuration.cwd.clone(),
}]),
..Default::default()
},
@@ -4475,8 +4553,58 @@ async fn unknown_turn_environment_returns_error() {
.await
.expect_err("unknown environment should fail");
let current_configuration = {
let state = session.state.lock().await;
state.session_configuration.clone()
};
assert!(matches!(err, CodexErr::InvalidRequest(_)));
assert!(err.to_string().contains("missing"));
assert_eq!(current_configuration.cwd, original_configuration.cwd);
assert_eq!(
current_configuration.environments,
original_configuration.environments
);
}
#[tokio::test]
async fn duplicate_turn_environment_returns_error_without_mutating_session() {
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
let original_configuration = {
let state = session.state.lock().await;
state.session_configuration.clone()
};
let err = 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"),
},
]),
..Default::default()
},
)
.await
.expect_err("duplicate environment should fail");
let current_configuration = {
let state = session.state.lock().await;
state.session_configuration.clone()
};
assert!(matches!(err, CodexErr::InvalidRequest(_)));
assert!(err.to_string().contains("duplicate"));
assert_eq!(current_configuration.cwd, original_configuration.cwd);
assert_eq!(
current_configuration.environments,
original_configuration.environments
);
}
#[tokio::test]
@@ -5033,7 +5161,6 @@ where
model_info,
&models_manager,
/*network*/ None,
Some(environment),
turn_environments,
session_configuration.cwd.clone(),
"turn_id".to_string(),

View File

@@ -1,5 +1,6 @@
use super::*;
use crate::compact::InitialContextInjection;
use crate::environment_selection::ResolvedTurnEnvironments;
use crate::exec::ExecCapturePolicy;
use crate::exec::ExecParams;
use crate::exec_policy::ExecPolicyManager;
@@ -754,7 +755,9 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
parent_rollout_thread_trace: codex_rollout_trace::ThreadTraceContext::disabled(),
user_shell_override: None,
parent_trace: None,
environments: Vec::new(),
environment_selections: ResolvedTurnEnvironments {
turn_environments: Vec::new(),
},
analytics_events_client: None,
thread_store,
})

View File

@@ -59,7 +59,6 @@ pub(crate) struct TurnContext {
pub(crate) reasoning_effort: Option<ReasoningEffortConfig>,
pub(crate) reasoning_summary: ReasoningSummaryConfig,
pub(crate) session_source: SessionSource,
pub(crate) environment: Option<Arc<Environment>>,
pub(crate) environments: Vec<TurnEnvironment>,
/// The session's absolute working directory. All relative paths provided
/// by the model as well as sandbox policies are resolved against this path
@@ -106,6 +105,10 @@ impl TurnContext {
self.permission_profile.network_sandbox_policy()
}
pub(crate) fn primary_environment(&self) -> Option<&TurnEnvironment> {
self.environments.first()
}
pub(crate) fn sandbox_policy(&self) -> SandboxPolicy {
let file_system_sandbox_policy = self.file_system_sandbox_policy();
let network_sandbox_policy = self.network_sandbox_policy();
@@ -230,7 +233,6 @@ impl TurnContext {
reasoning_effort,
reasoning_summary: self.reasoning_summary,
session_source: self.session_source.clone(),
environment: self.environment.clone(),
environments: self.environments.clone(),
cwd: self.cwd.clone(),
current_date: self.current_date.clone(),
@@ -432,7 +434,6 @@ impl Session {
model_info: ModelInfo,
models_manager: &SharedModelsManager,
network: Option<NetworkProxy>,
environment: Option<Arc<Environment>>,
environments: Vec<TurnEnvironment>,
cwd: AbsolutePathBuf,
sub_id: String,
@@ -474,7 +475,7 @@ impl Session {
)
.with_web_search_config(per_turn_config.web_search_config.clone())
.with_allow_login_shell(per_turn_config.permissions.allow_login_shell)
.with_has_environment(environment.is_some())
.with_has_environment(!environments.is_empty())
.with_spawn_agent_usage_hint(per_turn_config.multi_agent_v2.usage_hint_enabled)
.with_spawn_agent_usage_hint_text(per_turn_config.multi_agent_v2.usage_hint_text.clone())
.with_hide_spawn_agent_metadata(per_turn_config.multi_agent_v2.hide_spawn_agent_metadata)
@@ -522,7 +523,6 @@ impl Session {
reasoning_effort,
reasoning_summary,
session_source,
environment,
environments,
cwd,
current_date: Some(current_date),
@@ -564,10 +564,16 @@ impl Session {
let mut state = self.state.lock().await;
match state.session_configuration.clone().apply(&updates) {
Ok(next) => {
let effective_environments = updates
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();
@@ -641,27 +647,11 @@ impl Session {
&self,
environments: &[TurnEnvironmentSelection],
) -> CodexResult<Vec<TurnEnvironment>> {
let mut turn_environments = Vec::with_capacity(environments.len());
for selected_environment in environments {
let environment_id = selected_environment.environment_id.clone();
let environment = self
.services
.environment_manager
.get_environment(&environment_id)
.ok_or_else(|| {
CodexErr::InvalidRequest(format!(
"unknown turn environment id `{environment_id}`"
))
})?;
let cwd = selected_environment.cwd.clone();
turn_environments.push(TurnEnvironment {
environment_id,
environment,
cwd,
});
}
Ok(turn_environments)
crate::environment_selection::resolve_environment_selections(
self.services.environment_manager.as_ref(),
environments,
)
.map(|resolved| resolved.turn_environments)
}
async fn new_turn_from_configuration(
@@ -672,8 +662,6 @@ impl Session {
turn_environments: Vec<TurnEnvironment>,
) -> Arc<TurnContext> {
let primary_turn_environment = turn_environments.first();
let environment = primary_turn_environment
.map(|turn_environment| Arc::clone(&turn_environment.environment));
let cwd = primary_turn_environment
.map(|turn_environment| turn_environment.cwd.clone())
.unwrap_or_else(|| session_configuration.cwd.clone());
@@ -700,9 +688,8 @@ impl Session {
.await;
let effective_skill_roots = plugin_outcome.effective_skill_roots();
let skills_input = skills_load_input_from_config(&per_turn_config, effective_skill_roots);
let fs = environment
.as_ref()
.map(|environment| environment.get_filesystem());
let fs = primary_turn_environment
.map(|turn_environment| turn_environment.environment.get_filesystem());
let skills_outcome = Arc::new(
self.services
.skills_manager
@@ -731,7 +718,6 @@ impl Session {
)
.then(|| started_proxy.proxy())
}),
environment,
turn_environments,
cwd,
sub_id,
@@ -773,14 +759,18 @@ impl Session {
let state = self.state.lock().await;
state.session_configuration.clone()
};
let turn_environments =
match self.resolve_turn_environments(&session_configuration.environments) {
Ok(turn_environments) => turn_environments,
Err(err) => {
warn!("failed to resolve stored session environments: {err}");
Vec::new()
}
};
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}");
Vec::new()
}
};
self.new_turn_from_configuration(
sub_id,
@@ -790,4 +780,15 @@ impl Session {
)
.await
}
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();
}
}
}

View File

@@ -4,8 +4,7 @@ use crate::codex_thread::CodexThread;
use crate::config::Config;
use crate::config::ThreadStoreConfig;
use crate::environment_selection::default_thread_environment_selections;
use crate::environment_selection::selected_primary_environment;
use crate::environment_selection::validate_environment_selections;
use crate::environment_selection::resolve_environment_selections;
use crate::file_watcher::FileWatcher;
use crate::mcp::McpManager;
use crate::rollout::RolloutRecorder;
@@ -433,7 +432,8 @@ impl ThreadManager {
&self,
environments: &[TurnEnvironmentSelection],
) -> CodexResult<()> {
validate_environment_selections(self.state.environment_manager.as_ref(), environments)
resolve_environment_selections(self.state.environment_manager.as_ref(), environments)
.map(|_| ())
}
pub fn get_models_manager(&self) -> SharedModelsManager {
@@ -1098,16 +1098,16 @@ impl ThreadManagerState {
threads.remove(&resumed.conversation_id);
}
}
let environment =
selected_primary_environment(self.environment_manager.as_ref(), &environments)?;
let watch_registration = match environment.as_ref() {
Some(environment) if !environment.is_remote() => {
let environment_selections =
resolve_environment_selections(self.environment_manager.as_ref(), &environments)?;
let watch_registration = match environment_selections.primary_turn_environment() {
Some(turn_environment) if !turn_environment.environment.is_remote() => {
self.skills_watcher
.register_config(
&config,
self.skills_manager.as_ref(),
self.plugins_manager.as_ref(),
Some(environment.get_filesystem()),
Some(turn_environment.environment.get_filesystem()),
)
.await
}
@@ -1139,7 +1139,7 @@ impl ThreadManagerState {
parent_rollout_thread_trace,
user_shell_override,
parent_trace,
environments,
environment_selections,
analytics_events_client: self.analytics_events_client.clone(),
thread_store: Arc::clone(&self.thread_store),
})

View File

@@ -363,13 +363,14 @@ impl ToolHandler for ApplyPatchHandler {
// Avoid building temporary ExecParams/command vectors; derive directly from inputs.
let cwd = turn.cwd.clone();
let command = vec!["apply_patch".to_string(), patch_input.clone()];
let Some(environment) = turn.environment.as_ref() else {
let Some(turn_environment) = turn.primary_environment() else {
return Err(FunctionCallError::RespondToModel(
"apply_patch is unavailable in this session".to_string(),
));
};
let fs = environment.get_filesystem();
let sandbox = environment
let fs = turn_environment.environment.get_filesystem();
let sandbox = turn_environment
.environment
.is_remote()
.then(|| turn.file_system_sandbox_context(/*additional_permissions*/ None));
match codex_apply_patch::maybe_parse_apply_patch_verified(
@@ -474,9 +475,8 @@ pub(crate) async fn intercept_apply_patch(
tool_name: &str,
) -> Result<Option<FunctionToolOutput>, FunctionCallError> {
let sandbox = turn
.environment
.as_ref()
.filter(|env| env.is_remote())
.primary_environment()
.filter(|env| env.environment.is_remote())
.map(|_| turn.file_system_sandbox_context(/*additional_permissions*/ None));
match codex_apply_patch::maybe_parse_apply_patch_verified(command, cwd, fs, sandbox.as_ref())
.await

View File

@@ -412,12 +412,12 @@ impl ShellHandler {
} = args;
let mut exec_params = exec_params;
let Some(environment) = turn.environment.as_ref() else {
let Some(turn_environment) = turn.primary_environment() else {
return Err(FunctionCallError::RespondToModel(
"shell is unavailable in this session".to_string(),
));
};
let fs = environment.get_filesystem();
let fs = turn_environment.environment.get_filesystem();
let dependency_env = session.dependency_env().await;
if !dependency_env.is_empty() {

View File

@@ -196,12 +196,12 @@ impl ToolHandler for UnifiedExecHandler {
}
};
let Some(environment) = turn.environment.as_ref() else {
let Some(turn_environment) = turn.primary_environment() else {
return Err(FunctionCallError::RespondToModel(
"unified exec is unavailable in this session".to_string(),
));
};
let fs = environment.get_filesystem();
let fs = turn_environment.environment.get_filesystem();
let manager: &UnifiedExecProcessManager = &session.services.unified_exec_manager;
let context = UnifiedExecContext::new(session.clone(), turn.clone(), call_id.clone());

View File

@@ -88,16 +88,18 @@ impl ToolHandler for ViewImageHandler {
};
let abs_path = turn.resolve_path(Some(args.path));
let Some(environment) = turn.environment.as_ref() else {
let Some(environment) = turn.primary_environment() else {
return Err(FunctionCallError::RespondToModel(
"view_image is unavailable in this session".to_string(),
));
};
let sandbox = environment
.environment
.is_remote()
.then(|| turn.file_system_sandbox_context(/*additional_permissions*/ None));
let metadata = environment
.environment
.get_filesystem()
.get_metadata(&abs_path, sandbox.as_ref())
.await
@@ -115,6 +117,7 @@ impl ToolHandler for ViewImageHandler {
)));
}
let file_bytes = environment
.environment
.get_filesystem()
.read_file(&abs_path, sandbox.as_ref())
.await

View File

@@ -191,11 +191,11 @@ impl ToolRuntime<ApplyPatchRequest, ExecToolCallOutput> for ApplyPatchRuntime {
attempt: &SandboxAttempt<'_>,
ctx: &ToolCtx,
) -> Result<ExecToolCallOutput, ToolError> {
let environment = ctx.turn.environment.as_ref().ok_or_else(|| {
let turn_environment = ctx.turn.primary_environment().ok_or_else(|| {
ToolError::Rejected("apply_patch is unavailable in this session".to_string())
})?;
let started_at = Instant::now();
let fs = environment.get_filesystem();
let fs = turn_environment.environment.get_filesystem();
let sandbox = Self::file_system_sandbox_context_for_attempt(req, attempt);
let mut stdout = Vec::new();
let mut stderr = Vec::new();

View File

@@ -254,9 +254,8 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
}
let environment_is_remote = ctx
.turn
.environment
.as_ref()
.is_some_and(|environment| environment.is_remote());
.primary_environment()
.is_some_and(|turn_environment| turn_environment.environment.is_remote());
let command = if environment_is_remote {
base_command.to_vec()
} else {
@@ -293,12 +292,12 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
.await?
{
Some(prepared) => {
let Some(environment) = ctx.turn.environment.as_ref() else {
let Some(turn_environment) = ctx.turn.primary_environment() else {
return Err(ToolError::Rejected(
"exec_command is unavailable in this session".to_string(),
));
};
if environment.is_remote() {
if turn_environment.environment.is_remote() {
return Err(ToolError::Rejected(
"unified_exec zsh-fork is not supported when exec_server_url is configured".to_string(),
));
@@ -310,7 +309,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
&prepared.exec_request,
req.tty,
prepared.spawn_lifecycle,
environment.as_ref(),
turn_environment.environment.as_ref(),
)
.await
.map_err(|err| match err {
@@ -338,7 +337,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
.env_for(command, options, managed_network)
.map_err(|err| ToolError::Codex(err.into()))?;
exec_env.exec_server_env_config = req.exec_server_env_config.clone();
let Some(environment) = ctx.turn.environment.as_ref() else {
let Some(turn_environment) = ctx.turn.primary_environment() else {
return Err(ToolError::Rejected(
"exec_command is unavailable in this session".to_string(),
));
@@ -349,7 +348,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
&exec_env,
req.tty,
Box::new(NoopSpawnLifecycle),
environment.as_ref(),
turn_environment.environment.as_ref(),
)
.await
.map_err(|err| match err {

View File

@@ -96,7 +96,10 @@ async fn exec_command_with_tty(
&request,
tty,
Box::new(NoopSpawnLifecycle),
turn.environment.as_ref().expect("turn environment"),
turn.primary_environment()
.expect("turn environment")
.environment
.as_ref(),
)
.await?,
);
@@ -591,7 +594,7 @@ async fn remote_exec_server_rejects_inherited_fd_launches() -> anyhow::Result<()
let remote_test_env = remote_test_env().await?;
let (_, mut turn) = make_session_and_context().await;
turn.environment = Some(Arc::new(remote_test_env.environment().clone()));
turn.environments[0].environment = Arc::new(remote_test_env.environment().clone());
let request = test_exec_request(
&turn,
@@ -609,7 +612,10 @@ async fn remote_exec_server_rejects_inherited_fd_launches() -> anyhow::Result<()
Box::new(TestSpawnLifecycle {
inherited_fds: vec![42],
}),
turn.environment.as_ref().expect("turn environment"),
turn.primary_environment()
.expect("turn environment")
.environment
.as_ref(),
)
.await
.expect_err("expected inherited fd rejection");