mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
core: retain and persist executor environment identity
This commit is contained in:
@@ -52,12 +52,15 @@ pub(crate) async fn load_project_instructions(
|
||||
) -> Option<LoadedAgentsMd> {
|
||||
let mut loaded = LoadedAgentsMd::from_user_instructions(user_instructions);
|
||||
for turn_environment in &environments.turn_environments {
|
||||
let Some(cwd) = turn_environment.compatible_cwd() else {
|
||||
continue;
|
||||
};
|
||||
let filesystem = turn_environment.environment.get_filesystem();
|
||||
match read_agents_md(
|
||||
config,
|
||||
filesystem.as_ref(),
|
||||
&turn_environment.environment_id,
|
||||
turn_environment.cwd(),
|
||||
&cwd,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -266,7 +266,7 @@ fn resolved_local_environments<const N: usize>(
|
||||
.expect("local environment"),
|
||||
),
|
||||
cwd,
|
||||
/*shell*/ None,
|
||||
crate::shell::default_user_shell(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
|
||||
@@ -7,9 +7,11 @@ use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::TurnContextEnvironment;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
use codex_protocol::protocol::TurnContextNetworkItem;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::ApiPathString;
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -28,7 +30,7 @@ pub(crate) struct EnvironmentContext {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct EnvironmentContextEnvironment {
|
||||
pub(crate) id: String,
|
||||
pub(crate) cwd: AbsolutePathBuf,
|
||||
pub(crate) cwd: ApiPathString,
|
||||
pub(crate) shell: String,
|
||||
}
|
||||
|
||||
@@ -36,25 +38,30 @@ impl EnvironmentContextEnvironment {
|
||||
fn legacy(cwd: AbsolutePathBuf, shell: String) -> Self {
|
||||
Self {
|
||||
id: String::new(),
|
||||
cwd,
|
||||
cwd: ApiPathString::new(cwd.to_string_lossy()),
|
||||
shell,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_turn_environments(environments: &[TurnEnvironment], shell: &Shell) -> Vec<Self> {
|
||||
fn from_turn_environments(environments: &[TurnEnvironment]) -> Vec<Self> {
|
||||
environments
|
||||
.iter()
|
||||
.map(|environment| Self {
|
||||
id: environment.environment_id.clone(),
|
||||
cwd: environment.cwd().clone(),
|
||||
shell: environment
|
||||
.shell
|
||||
.as_ref()
|
||||
.map(|shell| shell.name().to_string())
|
||||
.unwrap_or_else(|| shell.name().to_string()),
|
||||
cwd: environment.native_cwd(),
|
||||
shell: environment.shell.name().to_string(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn from_turn_context_environment(environment: &TurnContextEnvironment) -> Option<Self> {
|
||||
Some(Self {
|
||||
id: environment.environment_id.clone(),
|
||||
cwd: ApiPathString::from_path_uri(&environment.cwd, environment.path_convention)
|
||||
.ok()?,
|
||||
shell: environment.shell.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -382,21 +389,24 @@ impl EnvironmentContext {
|
||||
) -> Self {
|
||||
let before_network = Self::network_from_turn_context_item(before);
|
||||
let before_filesystem = Self::filesystem_from_turn_context_item(before);
|
||||
let environments = match &after.environments {
|
||||
EnvironmentContextEnvironments::Single(environment) => {
|
||||
if before.cwd.as_path() != environment.cwd.as_path() {
|
||||
EnvironmentContextEnvironments::Single(EnvironmentContextEnvironment::legacy(
|
||||
environment.cwd.clone(),
|
||||
environment.shell.clone(),
|
||||
))
|
||||
} else {
|
||||
EnvironmentContextEnvironments::None
|
||||
}
|
||||
}
|
||||
EnvironmentContextEnvironments::Multiple(environments) => {
|
||||
EnvironmentContextEnvironments::Multiple(environments.clone())
|
||||
}
|
||||
EnvironmentContextEnvironments::None => EnvironmentContextEnvironments::None,
|
||||
let fallback_shell = match &after.environments {
|
||||
EnvironmentContextEnvironments::Single(environment) => environment.shell.clone(),
|
||||
EnvironmentContextEnvironments::Multiple(environments) => environments
|
||||
.first()
|
||||
.map(|environment| environment.shell.clone())
|
||||
.unwrap_or_default(),
|
||||
EnvironmentContextEnvironments::None => String::new(),
|
||||
};
|
||||
let before_environments = Self::from_turn_context_item(before, fallback_shell).environments;
|
||||
let environments_match = if before.environments.is_some() {
|
||||
before_environments == after.environments
|
||||
} else {
|
||||
before_environments.equals_except_shell(&after.environments)
|
||||
};
|
||||
let environments = if environments_match {
|
||||
EnvironmentContextEnvironments::None
|
||||
} else {
|
||||
after.environments.clone()
|
||||
};
|
||||
let network = if before_network != after.network {
|
||||
after.network.clone()
|
||||
@@ -418,11 +428,10 @@ impl EnvironmentContext {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn from_turn_context(turn_context: &TurnContext, shell: &Shell) -> Self {
|
||||
pub(crate) fn from_turn_context(turn_context: &TurnContext, _shell: &Shell) -> Self {
|
||||
let mut context = Self::new(
|
||||
EnvironmentContextEnvironment::from_turn_environments(
|
||||
&turn_context.environments.turn_environments,
|
||||
shell,
|
||||
),
|
||||
turn_context.current_date.clone(),
|
||||
turn_context.timezone.clone(),
|
||||
@@ -440,14 +449,27 @@ impl EnvironmentContext {
|
||||
turn_context_item: &TurnContextItem,
|
||||
shell: String,
|
||||
) -> Self {
|
||||
let cwd = match AbsolutePathBuf::try_from(turn_context_item.cwd.clone()) {
|
||||
Ok(cwd) => cwd,
|
||||
Err(_) => AbsolutePathBuf::resolve_path_against_base(&turn_context_item.cwd, "/"),
|
||||
let environments = match turn_context_item.environments.as_ref() {
|
||||
Some(environments) => EnvironmentContextEnvironments::from_vec(
|
||||
environments
|
||||
.iter()
|
||||
.filter_map(EnvironmentContextEnvironment::from_turn_context_environment)
|
||||
.collect(),
|
||||
),
|
||||
None => {
|
||||
let cwd = match AbsolutePathBuf::try_from(turn_context_item.cwd.clone()) {
|
||||
Ok(cwd) => cwd,
|
||||
Err(_) => {
|
||||
AbsolutePathBuf::resolve_path_against_base(&turn_context_item.cwd, "/")
|
||||
}
|
||||
};
|
||||
EnvironmentContextEnvironments::from_vec(vec![
|
||||
EnvironmentContextEnvironment::legacy(cwd, shell),
|
||||
])
|
||||
}
|
||||
};
|
||||
Self::new_with_environments(
|
||||
EnvironmentContextEnvironments::from_vec(vec![EnvironmentContextEnvironment::legacy(
|
||||
cwd, shell,
|
||||
)]),
|
||||
environments,
|
||||
turn_context_item.current_date.clone(),
|
||||
turn_context_item.timezone.clone(),
|
||||
Self::network_from_turn_context_item(turn_context_item),
|
||||
@@ -543,20 +565,14 @@ impl ContextualUserFragment for EnvironmentContext {
|
||||
let mut lines = Vec::new();
|
||||
match &self.environments {
|
||||
EnvironmentContextEnvironments::Single(environment) => {
|
||||
lines.push(format!(
|
||||
" <cwd>{}</cwd>",
|
||||
environment.cwd.to_string_lossy()
|
||||
));
|
||||
lines.push(format!(" <cwd>{}</cwd>", environment.cwd));
|
||||
lines.push(format!(" <shell>{}</shell>", environment.shell));
|
||||
}
|
||||
EnvironmentContextEnvironments::Multiple(environments) => {
|
||||
lines.push(" <environments>".to_string());
|
||||
for environment in environments {
|
||||
lines.push(format!(" <environment id=\"{}\">", environment.id));
|
||||
lines.push(format!(
|
||||
" <cwd>{}</cwd>",
|
||||
environment.cwd.to_string_lossy()
|
||||
));
|
||||
lines.push(format!(" <cwd>{}</cwd>", environment.cwd));
|
||||
lines.push(format!(" <shell>{}</shell>", environment.shell));
|
||||
lines.push(" </environment>".to_string());
|
||||
}
|
||||
|
||||
@@ -31,13 +31,17 @@ fn test_abs_path(unix_path: &str) -> AbsolutePathBuf {
|
||||
test_path_buf(unix_path).abs()
|
||||
}
|
||||
|
||||
fn test_native_path(unix_path: &str) -> ApiPathString {
|
||||
ApiPathString::new(test_path_buf(unix_path).display().to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_workspace_write_environment_context() {
|
||||
let cwd = test_path_buf("/repo");
|
||||
let context = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: cwd.abs(),
|
||||
cwd: ApiPathString::new(cwd.display().to_string()),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
Some("2026-02-26".to_string()),
|
||||
@@ -68,7 +72,7 @@ fn serialize_environment_context_with_network() {
|
||||
let context = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_path_buf("/repo").abs(),
|
||||
cwd: test_native_path("/repo"),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
Some("2026-02-26".to_string()),
|
||||
@@ -130,7 +134,7 @@ fn serialize_environment_context_with_full_filesystem_profile() {
|
||||
let mut context = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_path_buf("/repo").abs(),
|
||||
cwd: test_native_path("/repo"),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
@@ -169,6 +173,7 @@ fn turn_context_item_filesystem_uses_workspace_roots_instead_of_cwd() {
|
||||
let item = TurnContextItem {
|
||||
turn_id: None,
|
||||
cwd: test_path_buf("/not-the-workspace"),
|
||||
environments: None,
|
||||
workspace_roots: Some(vec![repo.clone(), other_repo.clone()]),
|
||||
current_date: None,
|
||||
timezone: None,
|
||||
@@ -235,7 +240,7 @@ fn equals_except_shell_compares_cwd() {
|
||||
let context1 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_abs_path("/repo"),
|
||||
cwd: test_native_path("/repo"),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
@@ -246,7 +251,7 @@ fn equals_except_shell_compares_cwd() {
|
||||
let context2 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_abs_path("/repo"),
|
||||
cwd: test_native_path("/repo"),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
@@ -262,7 +267,7 @@ fn equals_except_shell_compares_cwd_differences() {
|
||||
let context1 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_abs_path("/repo1"),
|
||||
cwd: test_native_path("/repo1"),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
@@ -273,7 +278,7 @@ fn equals_except_shell_compares_cwd_differences() {
|
||||
let context2 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_abs_path("/repo2"),
|
||||
cwd: test_native_path("/repo2"),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
@@ -290,7 +295,7 @@ fn equals_except_shell_ignores_shell() {
|
||||
let context1 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_abs_path("/repo"),
|
||||
cwd: test_native_path("/repo"),
|
||||
shell: "bash".to_string(),
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
@@ -301,7 +306,7 @@ fn equals_except_shell_ignores_shell() {
|
||||
let context2 = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "other".to_string(),
|
||||
cwd: test_abs_path("/repo"),
|
||||
cwd: test_native_path("/repo"),
|
||||
shell: "zsh".to_string(),
|
||||
}],
|
||||
/*current_date*/ None,
|
||||
@@ -318,7 +323,7 @@ fn serialize_environment_context_with_subagents() {
|
||||
let context = EnvironmentContext::new(
|
||||
vec![EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: test_path_buf("/repo").abs(),
|
||||
cwd: test_native_path("/repo"),
|
||||
shell: fake_shell_name(),
|
||||
}],
|
||||
Some("2026-02-26".to_string()),
|
||||
@@ -352,12 +357,12 @@ fn serialize_environment_context_with_multiple_selected_environments() {
|
||||
vec![
|
||||
EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: local_cwd.abs(),
|
||||
cwd: ApiPathString::new(local_cwd.display().to_string()),
|
||||
shell: "bash".to_string(),
|
||||
},
|
||||
EnvironmentContextEnvironment {
|
||||
id: "remote".to_string(),
|
||||
cwd: remote_cwd.abs(),
|
||||
cwd: ApiPathString::new(remote_cwd.display().to_string()),
|
||||
shell: "bash".to_string(),
|
||||
},
|
||||
],
|
||||
@@ -397,12 +402,12 @@ fn serialize_environment_context_prefers_environment_shell_when_present() {
|
||||
vec![
|
||||
EnvironmentContextEnvironment {
|
||||
id: "local".to_string(),
|
||||
cwd: local_cwd.abs(),
|
||||
cwd: ApiPathString::new(local_cwd.display().to_string()),
|
||||
shell: "powershell".to_string(),
|
||||
},
|
||||
EnvironmentContextEnvironment {
|
||||
id: "remote".to_string(),
|
||||
cwd: remote_cwd.abs(),
|
||||
cwd: ApiPathString::new(remote_cwd.display().to_string()),
|
||||
shell: "cmd".to_string(),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -121,6 +121,7 @@ fn reference_context_item() -> TurnContextItem {
|
||||
TurnContextItem {
|
||||
turn_id: Some("reference-turn".to_string()),
|
||||
cwd: PathBuf::from("/tmp/reference-cwd"),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: Some("2026-03-23".to_string()),
|
||||
timezone: Some("America/Los_Angeles".to_string()),
|
||||
|
||||
@@ -30,7 +30,12 @@ fn build_environment_update_item(
|
||||
let prev = previous?;
|
||||
let prev_context = EnvironmentContext::from_turn_context_item(prev, shell.name().to_string());
|
||||
let next_context = EnvironmentContext::from_turn_context(next, shell);
|
||||
if prev_context.equals_except_shell(&next_context) {
|
||||
let contexts_match = if prev.environments.is_some() {
|
||||
prev_context == next_context
|
||||
} else {
|
||||
prev_context.equals_except_shell(&next_context)
|
||||
};
|
||||
if contexts_match {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,12 +54,14 @@ impl ResolvedTurnEnvironments {
|
||||
.map(|environment| environment.environment.get_filesystem())
|
||||
}
|
||||
|
||||
pub(crate) fn single_local_environment_cwd(&self) -> Option<&AbsolutePathBuf> {
|
||||
pub(crate) fn single_local_environment_cwd(&self) -> Option<AbsolutePathBuf> {
|
||||
let [environment] = self.turn_environments.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
(!environment.environment.is_remote()).then_some(environment.cwd())
|
||||
(!environment.environment.is_remote())
|
||||
.then(|| environment.compatible_cwd())
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +89,7 @@ pub(crate) async fn resolve_environment_selections(
|
||||
"failed to get info for environment `{environment_id}`: {err}"
|
||||
))
|
||||
})?;
|
||||
let path_convention = info.path_convention;
|
||||
let shell = Shell::from_environment_shell_info(info.shell).map_err(|err| {
|
||||
CodexErr::InvalidRequest(format!(
|
||||
"failed to resolve shell for environment `{environment_id}`: {err}"
|
||||
@@ -96,7 +99,8 @@ pub(crate) async fn resolve_environment_selections(
|
||||
environment_id,
|
||||
environment,
|
||||
selected_environment.cwd.clone(),
|
||||
Some(shell),
|
||||
path_convention,
|
||||
shell,
|
||||
)?);
|
||||
}
|
||||
Ok(ResolvedTurnEnvironments { turn_environments })
|
||||
@@ -233,18 +237,16 @@ url = "ws://127.0.0.1:8765"
|
||||
);
|
||||
assert_eq!(
|
||||
resolved.primary().expect("primary environment").shell,
|
||||
Some(
|
||||
Shell::from_environment_shell_info(
|
||||
manager
|
||||
.get_environment("local")
|
||||
.expect("local environment")
|
||||
.info()
|
||||
.await
|
||||
.expect("local environment info")
|
||||
.shell
|
||||
)
|
||||
.expect("resolved shell")
|
||||
Shell::from_environment_shell_info(
|
||||
manager
|
||||
.get_environment("local")
|
||||
.expect("local environment")
|
||||
.info()
|
||||
.await
|
||||
.expect("local environment info")
|
||||
.shell
|
||||
)
|
||||
.expect("resolved shell")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -270,7 +272,7 @@ url = "ws://127.0.0.1:8765"
|
||||
REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
remote_environment.clone(),
|
||||
cwd.clone(),
|
||||
/*shell*/ None,
|
||||
crate::shell::default_user_shell(),
|
||||
)],
|
||||
};
|
||||
let multiple = ResolvedTurnEnvironments {
|
||||
@@ -280,12 +282,12 @@ url = "ws://127.0.0.1:8765"
|
||||
REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
remote_environment,
|
||||
cwd.clone(),
|
||||
/*shell*/ None,
|
||||
crate::shell::default_user_shell(),
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(local.single_local_environment_cwd(), Some(&cwd));
|
||||
assert_eq!(local.single_local_environment_cwd(), Some(cwd.clone()));
|
||||
assert_eq!(remote.single_local_environment_cwd(), None);
|
||||
assert_eq!(multiple.single_local_environment_cwd(), None);
|
||||
}
|
||||
|
||||
@@ -754,7 +754,7 @@ async fn run_review_on_session(
|
||||
.parent_turn
|
||||
.environments
|
||||
.primary()
|
||||
.map(|environment| environment.cwd().clone())
|
||||
.and_then(super::super::session::turn_context::TurnEnvironment::compatible_cwd)
|
||||
.unwrap_or_else(|| params.parent_turn.config.cwd.clone());
|
||||
|
||||
let submit_result = run_before_review_deadline(
|
||||
|
||||
@@ -317,17 +317,18 @@ impl Session {
|
||||
auth.as_ref(),
|
||||
)
|
||||
.await;
|
||||
let mcp_runtime_context = match turn_context.environments.primary() {
|
||||
Some(turn_environment) => McpRuntimeContext::new(
|
||||
Arc::clone(&self.services.environment_manager),
|
||||
turn_environment.cwd().to_path_buf(),
|
||||
),
|
||||
None => McpRuntimeContext::new(
|
||||
Arc::clone(&self.services.environment_manager),
|
||||
let cwd = turn_context
|
||||
.environments
|
||||
.primary()
|
||||
.and_then(super::turn_context::TurnEnvironment::compatible_cwd)
|
||||
.unwrap_or_else(|| {
|
||||
#[allow(deprecated)]
|
||||
turn_context.cwd.to_path_buf(),
|
||||
),
|
||||
};
|
||||
turn_context.cwd.clone()
|
||||
});
|
||||
let mcp_runtime_context = McpRuntimeContext::new(
|
||||
Arc::clone(&self.services.environment_manager),
|
||||
cwd.into_path_buf(),
|
||||
);
|
||||
let mcp_startup_cancellation_token = {
|
||||
let mut guard = self.services.mcp_startup_cancellation_token.lock().await;
|
||||
guard.cancel();
|
||||
|
||||
@@ -86,6 +86,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -126,6 +127,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -987,6 +989,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -1069,6 +1072,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -1099,6 +1103,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -1221,6 +1226,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
|
||||
turn_id: Some(current_turn_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -1341,6 +1347,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -1504,6 +1511,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
|
||||
@@ -1133,16 +1133,13 @@ impl Session {
|
||||
})?
|
||||
.primary()
|
||||
.cloned();
|
||||
let mcp_runtime_context = match turn_environment {
|
||||
Some(turn_environment) => McpRuntimeContext::new(
|
||||
Arc::clone(&sess.services.environment_manager),
|
||||
turn_environment.cwd().to_path_buf(),
|
||||
),
|
||||
None => McpRuntimeContext::new(
|
||||
Arc::clone(&sess.services.environment_manager),
|
||||
session_configuration.cwd().to_path_buf(),
|
||||
),
|
||||
};
|
||||
let cwd = turn_environment
|
||||
.and_then(|environment| environment.compatible_cwd())
|
||||
.unwrap_or_else(|| session_configuration.cwd().clone());
|
||||
let mcp_runtime_context = McpRuntimeContext::new(
|
||||
Arc::clone(&sess.services.environment_manager),
|
||||
cwd.into_path_buf(),
|
||||
);
|
||||
let mcp_connection_manager = McpConnectionManager::new(
|
||||
&mcp_servers,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
|
||||
@@ -131,10 +131,12 @@ use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
use codex_protocol::protocol::TurnCompleteEvent;
|
||||
use codex_protocol::protocol::TurnContextEnvironment;
|
||||
use codex_protocol::protocol::TurnStartedEvent;
|
||||
use codex_protocol::protocol::UserMessageEvent;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_rmcp_client::ElicitationAction;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::PathExt;
|
||||
use core_test_support::context_snapshot;
|
||||
@@ -2656,6 +2658,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: turn_context.cwd.to_path_buf(),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: turn_context.current_date.clone(),
|
||||
timezone: turn_context.timezone.clone(),
|
||||
@@ -4039,7 +4042,7 @@ fn turn_environments_for_tests(
|
||||
codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
Arc::clone(environment),
|
||||
cwd.clone(),
|
||||
/*shell*/ None,
|
||||
crate::shell::default_user_shell(),
|
||||
)],
|
||||
}
|
||||
}
|
||||
@@ -6218,7 +6221,10 @@ async fn default_turn_keeps_legacy_fallback_cwd_separate_from_stored_thread_envi
|
||||
&turn_environment.environment,
|
||||
&turn_environments.turn_environments[0].environment
|
||||
));
|
||||
assert_eq!(turn_environment.cwd(), &selected_cwd);
|
||||
assert_eq!(
|
||||
turn_environment.cwd(),
|
||||
&PathUri::from_abs_path(&selected_cwd)
|
||||
);
|
||||
#[allow(deprecated)]
|
||||
let turn_cwd = turn_context.cwd.clone();
|
||||
assert_eq!(turn_cwd, session_cwd);
|
||||
@@ -6259,7 +6265,7 @@ async fn primary_environment_uses_first_turn_environment() {
|
||||
"second".to_string(),
|
||||
Arc::clone(&first_environment.environment),
|
||||
second_cwd.clone(),
|
||||
/*shell*/ None,
|
||||
crate::shell::default_user_shell(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -6278,12 +6284,12 @@ async fn primary_environment_uses_first_turn_environment() {
|
||||
.find(|environment| environment.environment_id == "second")
|
||||
.expect("second environment")
|
||||
.cwd(),
|
||||
&second_cwd
|
||||
&PathUri::from_abs_path(&second_cwd)
|
||||
);
|
||||
assert_eq!(turn_context.environments.turn_environments.len(), 2);
|
||||
assert_eq!(
|
||||
turn_context.environments.turn_environments[1].cwd(),
|
||||
&second_cwd
|
||||
&PathUri::from_abs_path(&second_cwd)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7230,39 +7236,25 @@ async fn build_settings_update_items_emits_environment_item_for_network_changes(
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_context_uses_session_shell_when_environment_shell_is_absent() {
|
||||
async fn environment_context_uses_selected_environment_shell() {
|
||||
let (mut session, mut turn_context) = make_session_and_context().await;
|
||||
session.services.user_shell = Arc::new(crate::shell::Shell {
|
||||
shell_type: crate::shell::ShellType::PowerShell,
|
||||
shell_path: PathBuf::from("powershell"),
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
});
|
||||
for environment in &mut turn_context.environments.turn_environments {
|
||||
environment.shell = None;
|
||||
}
|
||||
|
||||
let session_shell = session.user_shell();
|
||||
let environment_context = crate::context::EnvironmentContext::from_turn_context(
|
||||
&turn_context,
|
||||
session_shell.as_ref(),
|
||||
)
|
||||
.render();
|
||||
assert!(
|
||||
environment_context.contains("<shell>powershell</shell>"),
|
||||
"{environment_context}"
|
||||
);
|
||||
|
||||
let primary_environment = turn_context
|
||||
.environments
|
||||
.turn_environments
|
||||
.first_mut()
|
||||
.expect("primary environment");
|
||||
primary_environment.shell = Some(crate::shell::Shell {
|
||||
primary_environment.shell = crate::shell::Shell {
|
||||
shell_type: crate::shell::ShellType::Cmd,
|
||||
shell_path: PathBuf::from("cmd"),
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
});
|
||||
};
|
||||
|
||||
let session_shell = session.user_shell();
|
||||
let environment_context = crate::context::EnvironmentContext::from_turn_context(
|
||||
&turn_context,
|
||||
session_shell.as_ref(),
|
||||
@@ -7272,6 +7264,75 @@ async fn environment_context_uses_session_shell_when_environment_shell_is_absent
|
||||
environment_context.contains("<shell>cmd</shell>"),
|
||||
"{environment_context}"
|
||||
);
|
||||
assert!(!environment_context.contains("<shell>powershell</shell>"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn windows_environment_context_persists_native_identity_without_redundant_update() {
|
||||
let (session, mut turn_context) = make_session_and_context().await;
|
||||
let current_environment = turn_context.environments.turn_environments[0].clone();
|
||||
let windows_cwd = PathUri::parse("file:///C:/workspace").expect("Windows cwd URI");
|
||||
let windows_shell = crate::shell::Shell {
|
||||
shell_type: crate::shell::ShellType::PowerShell,
|
||||
shell_path: PathBuf::from(r"C:\Program Files\PowerShell\7\pwsh.exe"),
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
};
|
||||
turn_context.environments.turn_environments[0] = TurnEnvironment::new_with_uri(
|
||||
"windows".to_string(),
|
||||
current_environment.environment,
|
||||
windows_cwd.clone(),
|
||||
PathConvention::Windows,
|
||||
windows_shell,
|
||||
)
|
||||
.expect("Windows environment");
|
||||
|
||||
let rendered = crate::context::EnvironmentContext::from_turn_context(
|
||||
&turn_context,
|
||||
session.user_shell().as_ref(),
|
||||
)
|
||||
.render();
|
||||
assert!(rendered.contains(r"<cwd>C:\workspace</cwd>"), "{rendered}");
|
||||
assert!(rendered.contains("<shell>powershell</shell>"), "{rendered}");
|
||||
assert!(!rendered.contains("/C:/workspace"), "{rendered}");
|
||||
|
||||
let persisted = turn_context.to_turn_context_item();
|
||||
assert_eq!(
|
||||
persisted.environments,
|
||||
Some(vec![TurnContextEnvironment {
|
||||
environment_id: "windows".to_string(),
|
||||
cwd: windows_cwd,
|
||||
path_convention: PathConvention::Windows,
|
||||
shell: "powershell".to_string(),
|
||||
}])
|
||||
);
|
||||
assert_eq!(
|
||||
crate::context::EnvironmentContext::from_turn_context_item(&persisted, "bash".to_string(),)
|
||||
.render(),
|
||||
rendered
|
||||
);
|
||||
|
||||
let update_items = session
|
||||
.build_settings_update_items(Some(&persisted), &turn_context)
|
||||
.await;
|
||||
assert!(
|
||||
user_input_texts(&update_items)
|
||||
.iter()
|
||||
.all(|text| !text.contains("<environment_context>"))
|
||||
);
|
||||
|
||||
turn_context.environments.turn_environments[0].shell = crate::shell::Shell {
|
||||
shell_type: crate::shell::ShellType::Cmd,
|
||||
shell_path: PathBuf::from("cmd.exe"),
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
};
|
||||
let changed_shell_items = session
|
||||
.build_settings_update_items(Some(&persisted), &turn_context)
|
||||
.await;
|
||||
let changed_environment = user_input_texts(&changed_shell_items)
|
||||
.into_iter()
|
||||
.find(|text| text.contains("<environment_context>"))
|
||||
.expect("changed executor shell should update model context");
|
||||
assert!(changed_environment.contains("<shell>cmd</shell>"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -414,13 +414,14 @@ pub(crate) async fn run_turn(
|
||||
async fn turn_diff_display_roots(turn_context: &TurnContext) -> Vec<(String, PathBuf)> {
|
||||
let mut display_roots = Vec::new();
|
||||
for turn_environment in &turn_context.environments.turn_environments {
|
||||
let root = get_git_repo_root_with_fs(
|
||||
turn_environment.environment.get_filesystem().as_ref(),
|
||||
turn_environment.cwd(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|| turn_environment.cwd().clone())
|
||||
.into_path_buf();
|
||||
let Some(cwd) = turn_environment.compatible_cwd() else {
|
||||
continue;
|
||||
};
|
||||
let root =
|
||||
get_git_repo_root_with_fs(turn_environment.environment.get_filesystem().as_ref(), &cwd)
|
||||
.await
|
||||
.unwrap_or(cwd)
|
||||
.into_path_buf();
|
||||
display_roots.push((turn_environment.environment_id.clone(), root));
|
||||
}
|
||||
display_roots
|
||||
@@ -628,10 +629,14 @@ async fn build_extension_turn_input_items(
|
||||
.turn_environments
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, environment)| TurnInputEnvironment {
|
||||
environment_id: environment.environment_id.clone(),
|
||||
cwd: environment.cwd().as_path().to_path_buf(),
|
||||
is_primary: index == 0,
|
||||
.filter_map(|(index, environment)| {
|
||||
environment
|
||||
.compatible_cwd()
|
||||
.map(|cwd| TurnInputEnvironment {
|
||||
environment_id: environment.environment_id.clone(),
|
||||
cwd: cwd.into_path_buf(),
|
||||
is_primary: index == 0,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
@@ -13,10 +13,13 @@ use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ToolMode;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
use codex_protocol::protocol::TurnContextEnvironment;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile;
|
||||
use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
|
||||
use codex_sandboxing::policy_transforms::effective_network_sandbox_policy;
|
||||
use codex_utils_path_uri::ApiPathString;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -41,14 +44,9 @@ impl TurnSkillsContext {
|
||||
pub(crate) struct TurnEnvironment {
|
||||
pub(crate) environment_id: String,
|
||||
pub(crate) environment: Arc<Environment>,
|
||||
// Keep both representations together while cwd consumers migrate to URI semantics. Keeping
|
||||
// them synchronized means neither representation can be exposed through a mutable reference;
|
||||
// updates must rebuild the validated pair through `TurnEnvironment::new`. Once
|
||||
// `TurnEnvironment::cwd` itself becomes a `PathUri`, convert only at native filesystem and
|
||||
// process-launch boundaries and remove this paired migration state.
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd_uri: PathUri,
|
||||
pub(crate) shell: Option<shell::Shell>,
|
||||
cwd: PathUri,
|
||||
path_convention: PathConvention,
|
||||
pub(crate) shell: shell::Shell,
|
||||
}
|
||||
|
||||
impl TurnEnvironment {
|
||||
@@ -57,13 +55,13 @@ impl TurnEnvironment {
|
||||
environment_id: String,
|
||||
environment: Arc<Environment>,
|
||||
cwd: AbsolutePathBuf,
|
||||
shell: Option<shell::Shell>,
|
||||
shell: shell::Shell,
|
||||
) -> Self {
|
||||
Self {
|
||||
environment_id,
|
||||
environment,
|
||||
cwd_uri: PathUri::from_abs_path(&cwd),
|
||||
cwd,
|
||||
cwd: PathUri::from_abs_path(&cwd),
|
||||
path_convention: PathConvention::native(),
|
||||
shell,
|
||||
}
|
||||
}
|
||||
@@ -71,35 +69,53 @@ impl TurnEnvironment {
|
||||
pub(crate) fn new_with_uri(
|
||||
environment_id: String,
|
||||
environment: Arc<Environment>,
|
||||
cwd_uri: PathUri,
|
||||
shell: Option<shell::Shell>,
|
||||
cwd: PathUri,
|
||||
path_convention: PathConvention,
|
||||
shell: shell::Shell,
|
||||
) -> CodexResult<Self> {
|
||||
let cwd = cwd_uri.to_abs_path().map_err(|err| {
|
||||
ApiPathString::from_path_uri(&cwd, path_convention).map_err(|err| {
|
||||
CodexErr::InvalidRequest(format!(
|
||||
"turn environment cwd `{cwd_uri}` cannot be projected onto this host: {err}"
|
||||
"turn environment cwd `{cwd}` cannot be rendered for {path_convention}: {err}"
|
||||
))
|
||||
})?;
|
||||
Ok(Self {
|
||||
environment_id,
|
||||
environment,
|
||||
cwd,
|
||||
cwd_uri,
|
||||
path_convention,
|
||||
shell,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn cwd(&self) -> &AbsolutePathBuf {
|
||||
pub(crate) fn cwd(&self) -> &PathUri {
|
||||
&self.cwd
|
||||
}
|
||||
|
||||
pub(crate) fn cwd_uri(&self) -> &PathUri {
|
||||
&self.cwd_uri
|
||||
&self.cwd
|
||||
}
|
||||
|
||||
pub(crate) fn path_convention(&self) -> PathConvention {
|
||||
self.path_convention
|
||||
}
|
||||
|
||||
pub(crate) fn native_cwd(&self) -> ApiPathString {
|
||||
match ApiPathString::from_path_uri(&self.cwd, self.path_convention) {
|
||||
Ok(cwd) => cwd,
|
||||
Err(error) => panic!("turn environment cwd was validated at construction: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn compatible_cwd(&self) -> Option<AbsolutePathBuf> {
|
||||
(self.path_convention == PathConvention::native())
|
||||
.then(|| self.cwd.to_abs_path().ok())
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub(crate) fn selection(&self) -> TurnEnvironmentSelection {
|
||||
TurnEnvironmentSelection {
|
||||
environment_id: self.environment_id.clone(),
|
||||
cwd: self.cwd_uri.clone(),
|
||||
cwd: self.cwd.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -400,6 +416,18 @@ impl TurnContext {
|
||||
turn_id: Some(self.sub_id.clone()),
|
||||
#[allow(deprecated)]
|
||||
cwd: self.cwd.to_path_buf(),
|
||||
environments: Some(
|
||||
self.environments
|
||||
.turn_environments
|
||||
.iter()
|
||||
.map(|environment| TurnContextEnvironment {
|
||||
environment_id: environment.environment_id.clone(),
|
||||
cwd: environment.cwd().clone(),
|
||||
path_convention: environment.path_convention(),
|
||||
shell: environment.shell.name().to_string(),
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
workspace_roots: (!workspace_roots.is_empty()).then_some(workspace_roots),
|
||||
current_date: self.current_date.clone(),
|
||||
timezone: self.timezone.clone(),
|
||||
|
||||
@@ -101,6 +101,31 @@ fn should_use_test_thread_manager_behavior() -> bool {
|
||||
FORCE_TEST_THREAD_MANAGER_BEHAVIOR.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn environment_selections_from_history(
|
||||
history: &InitialHistory,
|
||||
) -> Option<Vec<TurnEnvironmentSelection>> {
|
||||
let items = history.get_rollout_items();
|
||||
let context = items.iter().rev().find_map(|item| match item {
|
||||
RolloutItem::TurnContext(context) => Some(context),
|
||||
RolloutItem::SessionMeta(_)
|
||||
| RolloutItem::ResponseItem(_)
|
||||
| RolloutItem::InterAgentCommunication(_)
|
||||
| RolloutItem::Compacted(_)
|
||||
| RolloutItem::EventMsg(_) => None,
|
||||
})?;
|
||||
Some(
|
||||
context
|
||||
.environments
|
||||
.as_ref()?
|
||||
.iter()
|
||||
.map(|environment| TurnEnvironmentSelection {
|
||||
environment_id: environment.environment_id.clone(),
|
||||
cwd: environment.cwd.clone(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
struct TempCodexHomeGuard {
|
||||
path: PathBuf,
|
||||
}
|
||||
@@ -758,10 +783,13 @@ impl ThreadManager {
|
||||
auth_manager: Arc<AuthManager>,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
) -> CodexResult<NewThread> {
|
||||
let environments = default_thread_environment_selections(
|
||||
self.state.environment_manager.as_ref(),
|
||||
&config.cwd,
|
||||
);
|
||||
let environments =
|
||||
environment_selections_from_history(&initial_history).unwrap_or_else(|| {
|
||||
default_thread_environment_selections(
|
||||
self.state.environment_manager.as_ref(),
|
||||
&config.cwd,
|
||||
)
|
||||
});
|
||||
let (session_source, thread_source) = initial_history
|
||||
.get_resumed_session_sources()
|
||||
.unwrap_or_else(|| (self.state.session_source.clone(), None));
|
||||
@@ -821,10 +849,13 @@ impl ThreadManager {
|
||||
user_shell_override: crate::shell::Shell,
|
||||
) -> CodexResult<NewThread> {
|
||||
let initial_history = self.initial_history_from_rollout_path(rollout_path).await?;
|
||||
let environments = default_thread_environment_selections(
|
||||
self.state.environment_manager.as_ref(),
|
||||
&config.cwd,
|
||||
);
|
||||
let environments =
|
||||
environment_selections_from_history(&initial_history).unwrap_or_else(|| {
|
||||
default_thread_environment_selections(
|
||||
self.state.environment_manager.as_ref(),
|
||||
&config.cwd,
|
||||
)
|
||||
});
|
||||
let (session_source, thread_source) = initial_history
|
||||
.get_resumed_session_sources()
|
||||
.unwrap_or_else(|| (self.state.session_source.clone(), None));
|
||||
@@ -996,10 +1027,12 @@ impl ThreadManager {
|
||||
let interrupted_marker =
|
||||
InterruptedTurnHistoryMarker::from_config_and_version(&config, multi_agent_version);
|
||||
let history = fork_history_from_snapshot(snapshot, history, interrupted_marker);
|
||||
let environments = default_thread_environment_selections(
|
||||
self.state.environment_manager.as_ref(),
|
||||
&config.cwd,
|
||||
);
|
||||
let environments = environment_selections_from_history(&history).unwrap_or_else(|| {
|
||||
default_thread_environment_selections(
|
||||
self.state.environment_manager.as_ref(),
|
||||
&config.cwd,
|
||||
)
|
||||
});
|
||||
Box::pin(self.state.spawn_thread(
|
||||
config,
|
||||
history,
|
||||
@@ -1288,7 +1321,12 @@ impl ThreadManagerState {
|
||||
inherited_exec_policy,
|
||||
} = options;
|
||||
let environments =
|
||||
default_thread_environment_selections(self.environment_manager.as_ref(), &config.cwd);
|
||||
environment_selections_from_history(&initial_history).unwrap_or_else(|| {
|
||||
default_thread_environment_selections(
|
||||
self.environment_manager.as_ref(),
|
||||
&config.cwd,
|
||||
)
|
||||
});
|
||||
let thread_source = initial_history.get_resumed_thread_source();
|
||||
Box::pin(self.spawn_thread_with_source(
|
||||
config,
|
||||
@@ -1325,9 +1363,14 @@ impl ThreadManagerState {
|
||||
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
||||
environments: Option<Vec<TurnEnvironmentSelection>>,
|
||||
) -> CodexResult<NewThread> {
|
||||
let environments = environments.unwrap_or_else(|| {
|
||||
default_thread_environment_selections(self.environment_manager.as_ref(), &config.cwd)
|
||||
});
|
||||
let environments = environments
|
||||
.or_else(|| environment_selections_from_history(&initial_history))
|
||||
.unwrap_or_else(|| {
|
||||
default_thread_environment_selections(
|
||||
self.environment_manager.as_ref(),
|
||||
&config.cwd,
|
||||
)
|
||||
});
|
||||
Box::pin(self.spawn_thread_with_source(
|
||||
config,
|
||||
initial_history,
|
||||
|
||||
@@ -643,7 +643,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
||||
async fn resume_and_fork_restore_thread_environments_from_rollout() {
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
let mut config = test_config().await;
|
||||
config.codex_home = temp_dir.path().join("codex-home").abs();
|
||||
@@ -689,6 +689,13 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
||||
})
|
||||
.await
|
||||
.expect("start source thread");
|
||||
let source_turn = source.thread.codex.session.new_default_turn().await;
|
||||
source
|
||||
.thread
|
||||
.codex
|
||||
.session
|
||||
.record_context_updates_and_set_reference_context_item(&source_turn)
|
||||
.await;
|
||||
source.thread.ensure_rollout_materialized().await;
|
||||
source
|
||||
.thread
|
||||
@@ -725,11 +732,11 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
||||
assert_eq!(resumed_turn.environments.turn_environments.len(), 1);
|
||||
assert_eq!(
|
||||
resumed_turn.environments.turn_environments[0].cwd(),
|
||||
&default_cwd
|
||||
&PathUri::from_abs_path(&selected_cwd)
|
||||
);
|
||||
assert_ne!(
|
||||
resumed_turn.environments.turn_environments[0].cwd(),
|
||||
&selected_cwd
|
||||
&PathUri::from_abs_path(&default_cwd)
|
||||
);
|
||||
|
||||
let forked = manager
|
||||
@@ -752,11 +759,44 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
||||
assert_eq!(forked_turn.environments.turn_environments.len(), 1);
|
||||
assert_eq!(
|
||||
forked_turn.environments.turn_environments[0].cwd(),
|
||||
&default_cwd
|
||||
&PathUri::from_abs_path(&selected_cwd)
|
||||
);
|
||||
assert_ne!(
|
||||
forked_turn.environments.turn_environments[0].cwd(),
|
||||
&selected_cwd
|
||||
&PathUri::from_abs_path(&default_cwd)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_selections_from_history_use_latest_context_and_legacy_fallback() {
|
||||
let (_session, turn_context) = make_session_and_context().await;
|
||||
let persisted = turn_context.to_turn_context_item();
|
||||
let expected = persisted
|
||||
.environments
|
||||
.as_ref()
|
||||
.expect("current context persists environments")
|
||||
.iter()
|
||||
.map(|environment| TurnEnvironmentSelection {
|
||||
environment_id: environment.environment_id.clone(),
|
||||
cwd: environment.cwd.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
environment_selections_from_history(&InitialHistory::Forked(vec![
|
||||
RolloutItem::TurnContext(persisted.clone()),
|
||||
])),
|
||||
Some(expected)
|
||||
);
|
||||
|
||||
let mut legacy = persisted.clone();
|
||||
legacy.environments = None;
|
||||
assert_eq!(
|
||||
environment_selections_from_history(&InitialHistory::Forked(vec![
|
||||
RolloutItem::TurnContext(persisted),
|
||||
RolloutItem::TurnContext(legacy),
|
||||
])),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -299,7 +299,7 @@ pub async fn handle(
|
||||
Ok(FunctionToolOutput::from_text(content, Some(true)))
|
||||
}
|
||||
|
||||
fn single_local_environment_cwd(turn: &TurnContext) -> Result<&AbsolutePathBuf, FunctionCallError> {
|
||||
fn single_local_environment_cwd(turn: &TurnContext) -> Result<AbsolutePathBuf, FunctionCallError> {
|
||||
let [turn_environment] = turn.environments.turn_environments.as_slice() else {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"spawn_agents_on_csv requires exactly one local environment".to_string(),
|
||||
@@ -312,5 +312,9 @@ fn single_local_environment_cwd(turn: &TurnContext) -> Result<&AbsolutePathBuf,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(turn_environment.cwd())
|
||||
turn_environment.compatible_cwd().ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"spawn_agents_on_csv requires a host-compatible cwd".to_string(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -359,7 +359,11 @@ impl ApplyPatchHandler {
|
||||
"apply_patch is unavailable in this session".to_string(),
|
||||
));
|
||||
};
|
||||
let cwd = turn_environment.cwd().clone();
|
||||
let cwd = turn_environment.compatible_cwd().ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"apply_patch is not supported for a foreign-path environment".to_string(),
|
||||
)
|
||||
})?;
|
||||
let fs = turn_environment.environment.get_filesystem();
|
||||
let sandbox = turn.file_system_sandbox_context(
|
||||
/*additional_permissions*/ None,
|
||||
|
||||
@@ -114,10 +114,13 @@ async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall {
|
||||
ConversationHistory::new(invocation.session.clone_history().await.into_raw_items());
|
||||
let mut environments = Vec::with_capacity(invocation.turn.environments.turn_environments.len());
|
||||
for environment in &invocation.turn.environments.turn_environments {
|
||||
let Some(cwd) = environment.compatible_cwd() else {
|
||||
continue;
|
||||
};
|
||||
let additional_permissions = apply_granted_turn_permissions(
|
||||
invocation.session.as_ref(),
|
||||
&environment.environment_id,
|
||||
environment.cwd().as_path(),
|
||||
cwd.as_path(),
|
||||
SandboxPermissions::UseDefault,
|
||||
/*additional_permissions*/ None,
|
||||
)
|
||||
@@ -128,7 +131,7 @@ async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall {
|
||||
.file_system_sandbox_context(additional_permissions, environment.cwd_uri());
|
||||
environments.push(ToolEnvironment {
|
||||
environment_id: environment.environment_id.clone(),
|
||||
cwd: environment.cwd().clone(),
|
||||
cwd,
|
||||
file_system: environment.environment.get_filesystem(),
|
||||
file_system_sandbox_context,
|
||||
});
|
||||
|
||||
@@ -70,8 +70,12 @@ impl RequestPermissionsHandler {
|
||||
"request_permissions requires a primary environment".to_string(),
|
||||
));
|
||||
};
|
||||
let mut args: RequestPermissionsArgs =
|
||||
parse_arguments_with_base_path(&arguments, turn_environment.cwd())?;
|
||||
let cwd = turn_environment.compatible_cwd().ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"request_permissions is not supported for a foreign-path environment".to_string(),
|
||||
)
|
||||
})?;
|
||||
let mut args: RequestPermissionsArgs = parse_arguments_with_base_path(&arguments, &cwd)?;
|
||||
args.permissions = normalize_additional_permissions(args.permissions.into())
|
||||
.map(codex_protocol::request_permissions::RequestPermissionProfile::from)
|
||||
.map_err(FunctionCallError::RespondToModel)?;
|
||||
|
||||
@@ -129,14 +129,16 @@ impl ExecCommandHandler {
|
||||
"unified exec is unavailable in this session".to_string(),
|
||||
));
|
||||
};
|
||||
let base_cwd = turn_environment.compatible_cwd().ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"cross-platform unified exec requires native remote routing".to_string(),
|
||||
)
|
||||
})?;
|
||||
let cwd = environment_args
|
||||
.workdir
|
||||
.as_deref()
|
||||
.filter(|workdir| !workdir.is_empty())
|
||||
.map_or_else(
|
||||
|| turn_environment.cwd().clone(),
|
||||
|workdir| turn_environment.cwd().join(workdir),
|
||||
);
|
||||
.map_or_else(|| base_cwd.clone(), |workdir| base_cwd.join(workdir));
|
||||
let environment = Arc::clone(&turn_environment.environment);
|
||||
let fs = environment.get_filesystem();
|
||||
let args: ExecCommandArgs = parse_arguments_with_base_path(&arguments, &cwd)?;
|
||||
@@ -270,7 +272,7 @@ impl ExecCommandHandler {
|
||||
yield_time_ms,
|
||||
max_output_tokens,
|
||||
cwd,
|
||||
sandbox_cwd: turn_environment.cwd().clone(),
|
||||
sandbox_cwd: base_cwd,
|
||||
environment,
|
||||
shell_mode,
|
||||
network: context.turn.network.clone(),
|
||||
|
||||
@@ -143,7 +143,11 @@ impl ViewImageHandler {
|
||||
"view_image is unavailable in this session".to_string(),
|
||||
));
|
||||
};
|
||||
let cwd = turn_environment.cwd().clone();
|
||||
let cwd = turn_environment.compatible_cwd().ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"view_image is not supported for a foreign-path environment".to_string(),
|
||||
)
|
||||
})?;
|
||||
let abs_path = cwd.join(path);
|
||||
let sandbox = turn.file_system_sandbox_context(
|
||||
/*additional_permissions*/ None,
|
||||
|
||||
@@ -20,7 +20,7 @@ fn test_turn_environment(environment_id: &str) -> crate::session::turn_context::
|
||||
environment_id.to_string(),
|
||||
std::sync::Arc::new(codex_exec_server::Environment::default_for_tests()),
|
||||
std::env::temp_dir().abs(),
|
||||
/*shell*/ None,
|
||||
crate::shell::default_user_shell(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -573,8 +573,8 @@ async fn zsh_fork_unified_exec_keeps_shell_parameter_when_remote_environment_ava
|
||||
.environments
|
||||
.primary()
|
||||
.expect("primary environment")
|
||||
.cwd()
|
||||
.clone();
|
||||
.compatible_cwd()
|
||||
.expect("host-compatible test cwd");
|
||||
turn.environments.turn_environments.push(
|
||||
crate::session::turn_context::TurnEnvironment::new(
|
||||
"remote".to_string(),
|
||||
@@ -585,7 +585,7 @@ async fn zsh_fork_unified_exec_keeps_shell_parameter_when_remote_environment_ava
|
||||
.expect("remote test environment"),
|
||||
),
|
||||
remote_cwd,
|
||||
/*shell*/ None,
|
||||
crate::shell::default_user_shell(),
|
||||
),
|
||||
);
|
||||
})
|
||||
|
||||
@@ -6,6 +6,9 @@ wine_rust_test(
|
||||
srcs = ["remote_env_windows_test.rs"],
|
||||
crate_name = "remote_env_windows_test",
|
||||
crate_root = "remote_env_windows_test.rs",
|
||||
# Environment identity validation ends this fixture's host-shell-mismatch premise.
|
||||
# A hermetic target-shell smoke test replaces it once remote shell routing lands.
|
||||
target_compatible_with = ["@platforms//:incompatible"],
|
||||
windows_binaries = {
|
||||
"wine-windows-exec-server": "//codex-rs/exec-server/testing:windows-exec-server",
|
||||
},
|
||||
|
||||
@@ -90,11 +90,12 @@ async fn windows_exec_server_records_host_shell_mismatch() -> Result<()> {
|
||||
let test = builder.build(&server).await?;
|
||||
let (sandbox_policy, permission_profile) =
|
||||
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
|
||||
let selected_cwd = PathUri::parse("file:///C:/workspace")?;
|
||||
let environments = TurnEnvironmentSelections::new(
|
||||
test.config.cwd.clone(),
|
||||
vec![TurnEnvironmentSelection {
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&test.config.cwd),
|
||||
cwd: selected_cwd.clone(),
|
||||
}],
|
||||
);
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ fn resume_history(
|
||||
let turn_ctx = TurnContextItem {
|
||||
turn_id: Some(turn_id.clone()),
|
||||
cwd: config.cwd.to_path_buf(),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: None,
|
||||
timezone: None,
|
||||
|
||||
@@ -52,6 +52,7 @@ use crate::request_permissions::RequestPermissionsResponse;
|
||||
use crate::request_user_input::RequestUserInputResponse;
|
||||
use crate::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@@ -2936,6 +2937,14 @@ pub struct TurnContextNetworkItem {
|
||||
pub denied_domains: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct TurnContextEnvironment {
|
||||
pub environment_id: String,
|
||||
pub cwd: PathUri,
|
||||
pub path_convention: PathConvention,
|
||||
pub shell: String,
|
||||
}
|
||||
|
||||
/// Persist once per real user turn after computing that turn's model-visible
|
||||
/// context updates, and again after mid-turn compaction when replacement
|
||||
/// history re-establishes full context, so resume/fork replay can recover the
|
||||
@@ -2945,6 +2954,10 @@ pub struct TurnContextItem {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub turn_id: Option<String>,
|
||||
pub cwd: PathBuf,
|
||||
/// Selected executor environments for this turn. `None` identifies legacy
|
||||
/// rollout items that only persisted the app-host `cwd`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub environments: Option<Vec<TurnContextEnvironment>>,
|
||||
/// Effective workspace roots used to materialize symbolic
|
||||
/// `:workspace_roots` filesystem permissions in `permission_profile`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -5183,6 +5196,7 @@ mod tests {
|
||||
"summary": "auto",
|
||||
}))?;
|
||||
|
||||
assert_eq!(item.environments, None);
|
||||
assert_eq!(item.network, None);
|
||||
assert_eq!(item.file_system_sandbox_policy, None);
|
||||
assert_eq!(item.comp_hash, None);
|
||||
@@ -5227,6 +5241,7 @@ mod tests {
|
||||
let item = TurnContextItem {
|
||||
turn_id: None,
|
||||
cwd: test_path_buf("/tmp"),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: None,
|
||||
timezone: None,
|
||||
|
||||
@@ -1135,6 +1135,7 @@ async fn resume_candidate_matches_cwd_reads_latest_turn_context() -> std::io::Re
|
||||
item: RolloutItem::TurnContext(TurnContextItem {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
cwd: latest_cwd.clone(),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: None,
|
||||
timezone: None,
|
||||
|
||||
@@ -349,6 +349,7 @@ mod tests {
|
||||
&RolloutItem::TurnContext(TurnContextItem {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
cwd: PathBuf::from("/parent/workspace"),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: None,
|
||||
timezone: None,
|
||||
@@ -388,6 +389,7 @@ mod tests {
|
||||
&RolloutItem::TurnContext(TurnContextItem {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
cwd: PathBuf::from("/workspace"),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: None,
|
||||
timezone: None,
|
||||
@@ -424,6 +426,7 @@ mod tests {
|
||||
&RolloutItem::TurnContext(TurnContextItem {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
cwd: PathBuf::from("/fallback/workspace"),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: None,
|
||||
timezone: None,
|
||||
@@ -456,6 +459,7 @@ mod tests {
|
||||
&RolloutItem::TurnContext(TurnContextItem {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
cwd: PathBuf::from("/fallback/workspace"),
|
||||
environments: None,
|
||||
workspace_roots: None,
|
||||
current_date: None,
|
||||
timezone: None,
|
||||
|
||||
Reference in New Issue
Block a user