core: retain and persist executor environment identity

This commit is contained in:
Adam Perry
2026-06-13 06:26:34 +00:00
parent 13909721f4
commit 4d05d4e172
30 changed files with 448 additions and 187 deletions
+4 -1
View File
@@ -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
{
+1 -1
View File
@@ -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()),
+6 -1
View File
@@ -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;
}
+19 -17
View File
@@ -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);
}
+1 -1
View File
@@ -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(
+11 -10
View File
@@ -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(),
+7 -10
View File
@@ -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,
+84 -23
View File
@@ -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]
+16 -11
View File
@@ -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<_>>();
+47 -19
View File
@@ -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(),
+59 -16
View File
@@ -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,
+45 -5
View File
@@ -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(),
)
}
+3 -3
View File
@@ -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,
+15
View File
@@ -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,
+1
View File
@@ -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,
+4
View File
@@ -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,