Add in-memory shell snapshots to unified exec (#39957)

## What changed

- Add the under-development `shell_snapshot_v2` feature and advertise executor support through environment capabilities.
- Use executor-managed, in-memory snapshots for eligible direct `bash`, `zsh`, and `sh` login commands, while applying the configured shell environment policy and avoiding duplicate automatic startup-file effects.
- Keep file-backed snapshots available for user-shell commands and fall back to the existing execution path when in-memory snapshots are unsupported or inapplicable.

## Testing

- Cover policy filtering, snapshot reuse without snapshot files, automatic startup files, local and remote execution, and legacy user-shell snapshots.

GitOrigin-RevId: 4ad6cdf13824913ac8c393ba38b9844230677579
This commit is contained in:
jif
2026-08-21 15:24:56 +00:00
committed by copyberry
parent d446960657
commit 3882ced09c
17 changed files with 299 additions and 34 deletions
+6
View File
@@ -754,6 +754,9 @@
"shell_snapshot": {
"type": "boolean"
},
"shell_snapshot_v2": {
"type": "boolean"
},
"shell_tool": {
"type": "boolean"
},
@@ -5712,6 +5715,9 @@
"shell_snapshot": {
"type": "boolean"
},
"shell_snapshot_v2": {
"type": "boolean"
},
"shell_tool": {
"type": "boolean"
},
+20 -12
View File
@@ -137,6 +137,7 @@ struct ResolvedEnvironment {
environment: Arc<Environment>,
shell: Option<Shell>,
shell_snapshot: ShellSnapshotTask,
shell_snapshot_v2_supported: bool,
installed_config: Option<EnvironmentConfig>,
}
@@ -239,6 +240,7 @@ impl ThreadEnvironments {
environment: environment.environment,
shell: environment.shell,
shell_snapshot: environment.shell_snapshot,
shell_snapshot_v2_supported: environment.shell_snapshot_v2_supported,
installed_config: None,
}))
.boxed()
@@ -602,24 +604,27 @@ impl ThreadEnvironments {
};
// Resolve the attachment only after both prerequisites are ready.
let ((), installed_config) = tokio::try_join!(connection_ready, configuration_ready)?;
let shell = if environment.is_remote() {
let (shell, shell_snapshot_v2_supported) = if environment.is_remote() {
match environment.info().await {
Ok(info) => match Shell::from_environment_shell_info(info.shell) {
Ok(shell) => Some(shell),
Err(err) => {
tracing::warn!(
"failed to resolve shell for environment `{environment_id}`: {err}"
);
None
}
},
Ok(info) => (
match Shell::from_environment_shell_info(info.shell) {
Ok(shell) => Some(shell),
Err(err) => {
tracing::warn!(
"failed to resolve shell for environment `{environment_id}`: {err}"
);
None
}
},
info.capabilities.shell_snapshot_v2,
),
Err(err) => {
tracing::warn!("failed to get info for environment `{environment_id}`: {err}");
None
(None, false)
}
}
} else {
Some(local_shell)
(Some(local_shell), cfg!(unix))
};
let task = shell_snapshot
.build(Arc::clone(&environment), selection.cwd, shell.clone())
@@ -632,6 +637,7 @@ impl ThreadEnvironments {
environment,
shell,
shell_snapshot: task,
shell_snapshot_v2_supported,
installed_config,
})
}
@@ -707,6 +713,8 @@ impl TurnEnvironmentState {
environment.shell,
);
turn_environment.shell_snapshot = environment.shell_snapshot;
turn_environment.shell_snapshot_v2_supported =
environment.shell_snapshot_v2_supported;
Some(Self::Ready(turn_environment))
}
Some(Err(err)) => {
+1
View File
@@ -408,6 +408,7 @@ pub(crate) async fn execute_exec_request(
cwd,
env,
exec_server_env_config: _,
exec_server_shell_snapshot: _,
network,
expiration,
capture_policy,
+3
View File
@@ -52,6 +52,7 @@ pub struct ExecRequest {
pub cwd: PathUri,
pub env: HashMap<String, String>,
pub(crate) exec_server_env_config: Option<ExecServerEnvConfig>,
pub(crate) exec_server_shell_snapshot: Option<codex_exec_server::ShellSnapshotRequest>,
pub network: Option<NetworkProxy>,
pub network_environment_id: Option<String>,
pub expiration: ExecExpiration,
@@ -94,6 +95,7 @@ impl ExecRequest {
cwd,
env,
exec_server_env_config: None,
exec_server_shell_snapshot: None,
network,
network_environment_id,
expiration,
@@ -182,6 +184,7 @@ impl ExecRequest {
cwd,
env,
exec_server_env_config: None,
exec_server_shell_snapshot: None,
network,
network_environment_id,
expiration,
+15 -1
View File
@@ -1164,7 +1164,21 @@ impl Session {
} else {
shell::default_user_shell()
};
let shell_snapshot = if config.features.enabled(Feature::ShellSnapshot) {
let use_executor_shell_snapshots = config.features.enabled(Feature::ShellSnapshotV2)
&& config.features.enabled(Feature::ShellTool)
&& config.features.enabled(Feature::UnifiedExec)
&& matches!(
codex_tools::UnifiedExecShellMode::for_session(
config.features.get(),
crate::tools::tool_user_shell_type(&default_shell),
config.zsh_path.as_ref(),
config.main_execve_wrapper_exe.as_ref(),
),
codex_tools::UnifiedExecShellMode::Direct
);
let shell_snapshot = if config.features.enabled(Feature::ShellSnapshot)
&& !use_executor_shell_snapshots
{
ShellSnapshot::new(
config.codex_home.clone(),
thread_id,
@@ -42,6 +42,7 @@ pub(crate) struct TurnEnvironment {
pub(crate) environment: Arc<Environment>,
pub(crate) shell: Option<shell::Shell>,
pub(crate) shell_snapshot: ShellSnapshotTask,
pub(crate) shell_snapshot_v2_supported: bool,
}
impl TurnEnvironment {
@@ -58,6 +59,7 @@ impl TurnEnvironment {
environment,
shell,
shell_snapshot: futures::future::ready(None).boxed().shared(),
shell_snapshot_v2_supported: false,
}
}
+1
View File
@@ -207,6 +207,7 @@ pub(crate) async fn execute_user_shell_command(
cwd: cwd.clone().into(),
env: exec_env_map,
exec_server_env_config: None,
exec_server_shell_snapshot: None,
// `/shell` is the explicit full-access escape hatch, so it must not
// inherit a managed proxy from the surrounding session or turn.
network: None,
@@ -71,6 +71,7 @@ pub struct UnifiedExecRequest {
pub turn_environment: TurnEnvironment,
pub env: HashMap<String, String>,
pub exec_server_env_config: Option<ExecServerEnvConfig>,
pub shell_snapshot: Option<codex_exec_server::ShellSnapshotRequest>,
pub explicit_env_overrides: HashMap<String, String>,
pub network: Option<NetworkProxy>,
pub tty: bool,
@@ -201,7 +202,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecAttempt> for UnifiedExecRunt
}
fn uses_executor_managed_process_sandbox(&self, req: &UnifiedExecRequest) -> bool {
req.turn_environment.environment.is_remote()
req.turn_environment.environment.is_remote() || req.shell_snapshot.is_some()
}
fn sandbox_cwd<'b>(&self, req: &'b UnifiedExecRequest) -> Option<&'b PathUri> {
@@ -368,7 +369,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecAttempt> for UnifiedExecRunt
};
#[cfg(not(unix))]
let runtime_path_prepends = RuntimePathPrepends::default();
let command = if environment_is_remote {
let mut command = if environment_is_remote {
base_command.to_vec()
} else {
maybe_wrap_shell_lc_with_snapshot(
@@ -380,6 +381,15 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecAttempt> for UnifiedExecRunt
&runtime_path_prepends,
)
};
if req.shell_snapshot.is_some() {
let exports =
runtime_path_prepends.shell_exports_after_snapshot(&explicit_env_overrides);
if !exports.is_empty()
&& let Some(script) = command.get_mut(2)
{
*script = format!("{exports}\n{script}");
}
}
let command = disable_powershell_profile_for_elevated_windows_sandbox(
&command,
Some(&req.shell_type),
@@ -491,6 +501,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecAttempt> for UnifiedExecRunt
network_proxy_launch,
/*environment_id*/ Some(&req.turn_environment.selection.environment_id),
req.exec_server_env_config.clone(),
req.shell_snapshot.clone(),
windows_sandbox_proxy_settings_mode,
req.tty,
Box::new(NoopSpawnLifecycle),
@@ -616,6 +627,7 @@ mod tests {
turn_environment: test_turn_environment(sandbox_cwd.clone().into()),
env: HashMap::new(),
exec_server_env_config: None,
shell_snapshot: None,
explicit_env_overrides: HashMap::new(),
network: None,
tty: false,
@@ -718,6 +730,7 @@ mod tests {
turn_environment: test_turn_environment(cwd.into()),
env: HashMap::new(),
exec_server_env_config: None,
shell_snapshot: None,
explicit_env_overrides: HashMap::new(),
network: None,
tty: false,
@@ -632,6 +632,7 @@ impl CoreShellCommandExecutor {
cwd: self.cwd.clone().into(),
env: exec_env,
exec_server_env_config: None,
exec_server_shell_snapshot: None,
network: self.network.clone(),
network_environment_id: self.network_environment_id.clone(),
expiration: ExecExpiration::Cancellation(cancel_rx),
+1
View File
@@ -51,6 +51,7 @@ mod head_tail_buffer;
mod process;
mod process_manager;
mod process_state;
mod shell_snapshot;
pub(crate) fn set_deterministic_process_ids_for_tests(enabled: bool) {
process_manager::set_deterministic_process_ids_for_tests(enabled);
@@ -60,6 +60,7 @@ use crate::unified_exec::head_tail_buffer::HeadTailBuffer;
use crate::unified_exec::process::OutputHandles;
use crate::unified_exec::process::SpawnLifecycleHandle;
use crate::unified_exec::process::UnifiedExecProcess;
use crate::unified_exec::shell_snapshot::shell_snapshot_request;
use crate::unified_exec::take_plugin_metrics_sidecar;
use codex_core_plugins::PLUGIN_METRICS_OUTPUT_ENV_VAR;
use codex_core_plugins::PluginCommandAttribution;
@@ -186,6 +187,11 @@ fn exec_server_env_for_request(
if let Some(exec_server_env_config) = &request.exec_server_env_config {
let mut env =
env_overlay_for_exec_server(&request.env, &exec_server_env_config.local_policy_env);
if request.exec_server_shell_snapshot.is_some()
&& !exec_server_env_config.policy.r#set.contains_key("PATH")
{
env.remove("PATH");
}
if request.exec_server_managed_network.is_some() {
for (key, value) in &request.env {
if is_managed_proxy_env_var(key, value) {
@@ -210,18 +216,20 @@ fn exec_server_params_for_request(
sandbox.windows_sandbox_proxy_settings_mode = Some(windows_sandbox_proxy_settings_mode);
sandbox
});
// Sandbox retries reuse the unified-exec ID but start a distinct executor process.
let exec_server_process_id = if request.exec_server_sandbox.is_some() {
format!("{process_id}-{}", Uuid::new_v4())
} else {
process_id.to_string()
};
// Sandbox retries and memory-backed local launches can reuse a unified-exec
// ID while the executor still retains the previous process.
let exec_server_process_id =
if request.exec_server_sandbox.is_some() || request.exec_server_shell_snapshot.is_some() {
format!("{process_id}-{}", Uuid::new_v4())
} else {
process_id.to_string()
};
codex_exec_server::ExecParams {
process_id: exec_server_process_id.into(),
argv: request.command.clone(),
cwd: request.cwd.clone(),
env_policy,
shell_snapshot: None,
shell_snapshot: request.exec_server_shell_snapshot.clone(),
env,
tty,
pipe_stdin: false,
@@ -1060,12 +1068,13 @@ impl UnifiedExecProcessManager {
network_proxy_launch: Option<codex_network_proxy::RemoteNetworkProxyLaunchConfig>,
environment_id: Option<&str>,
exec_server_env_config: Option<ExecServerEnvConfig>,
shell_snapshot: Option<codex_exec_server::ShellSnapshotRequest>,
windows_sandbox_proxy_settings_mode: codex_sandboxing::WindowsSandboxProxySettingsMode,
tty: bool,
spawn_lifecycle: SpawnLifecycleHandle,
environment: &codex_exec_server::Environment,
) -> Result<UnifiedExecProcess, ToolError> {
let mut request = if environment.is_remote() {
let mut request = if environment.is_remote() || shell_snapshot.is_some() {
attempt.env_for_exec_server(command, options)
} else {
attempt.env_for(command, options, network, environment_id)
@@ -1077,6 +1086,7 @@ impl UnifiedExecProcessManager {
.and_then(|_| network.and_then(NetworkProxy::remote_policy_decider));
request.exec_server_network_proxy = network_proxy_launch;
request.exec_server_env_config = exec_server_env_config;
request.exec_server_shell_snapshot = shell_snapshot;
self.open_session_with_prepared_exec_env(
process_id,
&request,
@@ -1111,7 +1121,7 @@ impl UnifiedExecProcessManager {
) -> Result<UnifiedExecProcess, UnifiedExecError> {
let inherited_fds = spawn_lifecycle.inherited_fds();
if environment.is_remote() {
if environment.is_remote() || request.exec_server_shell_snapshot.is_some() {
if !inherited_fds.is_empty() {
return Err(UnifiedExecError::create_process(
"remote exec-server does not support inherited file descriptors".to_string(),
@@ -1237,6 +1247,7 @@ impl UnifiedExecProcessManager {
policy: exec_env_policy_from_shell_policy(shell_environment_policy),
local_policy_env,
};
let shell_snapshot = shell_snapshot_request(request, &cwd, context);
let mut orchestrator = ToolOrchestrator::new();
let mut runtime = UnifiedExecRuntime::new(self, request.shell_mode.clone());
let session_shell = context.session.user_shell();
@@ -1278,6 +1289,7 @@ impl UnifiedExecProcessManager {
turn_environment: request.turn_environment.clone(),
env,
exec_server_env_config: Some(exec_server_env_config),
shell_snapshot,
explicit_env_overrides,
network: request.network.clone(),
tty: request.tty,
@@ -184,6 +184,7 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
),
]),
}),
exec_server_shell_snapshot: None,
network: None,
network_environment_id: None,
expiration: crate::exec::ExecExpiration::DefaultTimeout,
@@ -230,6 +231,18 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
("CODEX_NETWORK_PROXY_ACTIVE".to_string(), "1".to_string(),),
])
);
request.exec_server_shell_snapshot = Some(codex_exec_server::ShellSnapshotRequest {
scope_id: "attachment-1".to_string(),
shell: codex_exec_server::ShellInfo {
name: "bash".to_string(),
path: "/bin/bash".to_string(),
},
});
let mut snapshot_env = params.env;
snapshot_env.remove("PATH");
assert_eq!(params_for_request(&request).env, snapshot_env);
request.exec_server_shell_snapshot = None;
request.exec_server_sandbox = Some(
codex_exec_server::FileSystemSandboxContext::from_permission_profile(permission_profile),
);
@@ -0,0 +1,40 @@
use codex_exec_server::ShellInfo;
use codex_exec_server::ShellSnapshotRequest;
use codex_features::Feature;
use codex_tools::UnifiedExecShellMode;
use codex_utils_path_uri::PathUri;
use super::ExecCommandRequest;
use super::UnifiedExecContext;
use crate::shell::ShellType;
pub(super) fn shell_snapshot_request(
request: &ExecCommandRequest,
cwd: &PathUri,
context: &UnifiedExecContext,
) -> Option<ShellSnapshotRequest> {
if !context.session.features().enabled(Feature::ShellSnapshotV2)
|| !request.turn_environment.shell_snapshot_v2_supported
|| request.turn_environment.selection.cwd != *cwd
|| !matches!(request.shell_mode, UnifiedExecShellMode::Direct)
|| !matches!(
request.shell_type,
ShellType::Bash | ShellType::Zsh | ShellType::Sh
)
|| request.command.get(1).is_none_or(|flag| flag != "-lc")
{
return None;
}
Some(ShellSnapshotRequest {
scope_id: format!(
"{}:{}",
context.session.thread_id(),
request.turn_environment.selection.environment_id
),
shell: ShellInfo {
name: request.shell_type.name().to_string(),
path: request.command.first()?.clone(),
},
})
}
+109 -1
View File
@@ -1,6 +1,6 @@
use anyhow::Result;
use codex_core::TurnInputRequest;
#[cfg(target_os = "macos")]
#[cfg(unix)]
use codex_core::shell::get_shell_by_model_provided_path;
use codex_features::Feature;
use codex_protocol::config_types::CollaborationMode;
@@ -20,6 +20,8 @@ use core_test_support::responses::ev_function_call;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_sequence;
use core_test_support::responses::sse;
#[cfg(unix)]
use core_test_support::skip_if_remote;
use core_test_support::test_codex::TestCodexHarness;
use core_test_support::test_codex::local_selections;
use core_test_support::test_codex::test_codex;
@@ -290,6 +292,112 @@ fn assert_posix_snapshot_sections(snapshot: &str) {
);
}
#[cfg(unix)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn shell_snapshot_v2_filters_profile_secrets_without_creating_files() -> Result<()> {
skip_if_remote!(Ok(()), "profile fixture uses a host-local HOME directory");
let profile_home = tempfile::tempdir()?;
fs::write(
profile_home.path().join(".bashrc"),
"printf x >> \"$HOME/captures\"\nexport PATH=\"$HOME/profile-bin:$PATH\"\nexport PROFILE_ALLOWED=profile\nexport PROFILE_SECRET=secret\nprofile_helper() { printf helper; }\n",
)
.await?;
fs::write(
profile_home.path().join(".bash_profile"),
"export PROFILE_SECRET=secret\n",
)
.await?;
let configured_home = profile_home.path().to_string_lossy().into_owned();
let shell = get_shell_by_model_provided_path(&PathBuf::from("/bin/sh"));
let builder = test_codex()
.with_user_shell(shell)
.with_config(move |config| {
config
.features
.enable(Feature::UnifiedExec)
.expect("test config should enable unified exec");
config
.features
.enable(Feature::ShellSnapshotV2)
.expect("test config should enable in-memory snapshots");
config
.permissions
.shell_environment_policy
.ignore_default_excludes = false;
config.permissions.shell_environment_policy.r#set = HashMap::from([
("HOME".to_string(), configured_home),
("PROFILE_ALLOWED".to_string(), "policy".to_string()),
]);
});
let harness = TestCodexHarness::with_auto_env_builder(builder).await?;
for attempt in 0..2 {
let end = run_tool_turn_on_harness(
&harness,
"run an in-memory shell snapshot",
&format!("shell-snapshot-v2-{attempt}"),
"exec_command",
json!({
"cmd": "profile_helper; case \":$PATH:\" in *\":$HOME/profile-bin:\"*) printf '|path';; *) printf '|missing';; esac; printf '|%s|%s' \"$PROFILE_ALLOWED\" \"${PROFILE_SECRET-missing}\"",
"shell": "/bin/bash",
"yield_time_ms": 1_000,
}),
)
.await?;
assert_eq!(end.exit_code, 0);
assert_eq!(
normalize_newlines(&end.stdout).trim(),
"helper|path|policy|missing"
);
}
assert_eq!(
fs::read_to_string(profile_home.path().join("captures")).await?,
"x"
);
assert!(!harness.test().home.path().join("shell_snapshots").exists());
Ok(())
}
#[cfg(unix)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn shell_snapshot_v2_preserves_legacy_snapshots_for_user_shell() -> Result<()> {
skip_if_remote!(Ok(()), "legacy shell snapshots require a local environment");
let builder = test_codex().with_config(|config| {
config
.features
.disable(Feature::ShellTool)
.expect("test config should disable model shell tools");
config
.features
.enable(Feature::ShellSnapshot)
.expect("test config should enable legacy snapshots");
config
.features
.enable(Feature::ShellSnapshotV2)
.expect("test config should enable in-memory snapshots");
});
let harness = TestCodexHarness::with_auto_env_builder(builder).await?;
let snapshot_path = wait_for_snapshot(harness.test().home.path()).await?;
assert_posix_snapshot_sections(&fs::read_to_string(snapshot_path).await?);
let codex = &harness.test().codex;
codex
.submit(Op::RunUserShellCommand {
command: "printf legacy".to_string(),
})
.await?;
let end = wait_for_event_match(codex, |event| match event {
EventMsg::ExecCommandEnd(event) => Some(event.clone()),
_ => None,
})
.await;
assert_eq!(end.exit_code, 0);
assert_eq!(normalize_newlines(&end.stdout).trim(), "legacy");
Ok(())
}
#[cfg_attr(not(target_os = "linux"), ignore)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn linux_unified_exec_uses_shell_snapshot() -> Result<()> {
+10 -2
View File
@@ -172,9 +172,17 @@ impl ShellSnapshotCache {
.collect::<String>();
let state_variables = state_variables.join(" ");
let shell_start = prepared.command.len() - params.argv.len();
prepared.command[shell_start + 1] = "-c".to_string();
// Automatic startup files run before the restoration script and could
// reintroduce environment variables that the snapshot already filtered.
let (shell_flag, startup) = match shell_type {
ShellType::Bash => ("-pc", "set +o privileged\n"),
ShellType::Zsh => ("-fc", "setopt RCS\n"),
ShellType::Sh => ("-c", ""),
ShellType::PowerShell | ShellType::Cmd => unreachable!(),
};
prepared.command[shell_start + 1] = shell_flag.to_string();
prepared.command[shell_start + 2] = format!(
"if ! eval \"unset {state_variables}\n{state_expansion}\" >/dev/null; then printf 'failed to restore shell snapshot\\n' >&2; fi\n{}",
"{startup}if ! eval \"unset {state_variables}\n{state_expansion}\" >/dev/null; then printf 'failed to restore shell snapshot\\n' >&2; fi\n{}",
params.argv[2]
);
+33 -7
View File
@@ -103,15 +103,25 @@ async fn create_process_context(use_remote: bool) -> Result<ProcessContext> {
}
#[cfg(unix)]
#[test_case(false, false, false, "bash"; "local_pipe")]
#[test_case(false, true, false, "bash"; "local_tty")]
#[test_case(true, false, false, "bash"; "remote_pipe")]
#[test_case(true, true, false, "bash"; "remote_tty")]
#[test_case(true, false, true, "bash"; "remote_sandbox")]
#[test_case(false, false, false, "sh"; "local_sh_pipe")]
#[test_case(false, false, false, false, "bash"; "local_pipe")]
#[test_case(false, true, false, false, "bash"; "local_tty")]
#[test_case(true, false, false, false, "bash"; "remote_pipe")]
#[test_case(true, true, false, false, "bash"; "remote_tty")]
#[test_case(true, false, true, false, "bash"; "remote_sandbox")]
#[test_case(false, false, false, false, "sh"; "local_sh_pipe")]
#[test_case(false, false, false, true, "bash"; "local_bash_env")]
#[test_case(true, false, false, true, "bash"; "remote_bash_env")]
#[cfg_attr(
target_os = "macos",
test_case(false, false, false, "zsh"; "local_zsh_pipe")
test_case(false, false, false, false, "zsh"; "local_zsh_pipe")
)]
#[cfg_attr(
target_os = "macos",
test_case(false, false, false, true, "zsh"; "local_zshenv")
)]
#[cfg_attr(
target_os = "macos",
test_case(true, false, false, true, "zsh"; "remote_zshenv")
)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
// Serialize tests that launch a real exec-server process through the full CLI.
@@ -120,6 +130,7 @@ async fn shell_snapshot_v2_filters_profile_exports_and_stays_in_memory(
use_remote: bool,
tty: bool,
use_sandbox: bool,
automatic_startup: bool,
shell_name: &str,
) -> Result<()> {
if use_sandbox
@@ -133,8 +144,10 @@ async fn shell_snapshot_v2_filters_profile_exports_and_stays_in_memory(
let home = TempDir::new()?;
let cwd = PathUri::from_host_native_path(home.path())?;
let (shell_path, profile_name) = match shell_name {
"bash" if automatic_startup => ("/bin/bash", ".bash-env"),
"bash" => ("/bin/bash", ".bashrc"),
"sh" => ("/bin/sh", ".snapshot-env"),
"zsh" if automatic_startup => ("/bin/zsh", ".zshenv"),
"zsh" => ("/bin/zsh", ".zshrc"),
name => anyhow::bail!("unsupported test shell {name}"),
};
@@ -160,6 +173,12 @@ async fn shell_snapshot_v2_filters_profile_exports_and_stays_in_memory(
"printf x >> \"$HOME/captures\"\nexport PATH=\"$HOME/profile-bin:/usr/bin:/bin\"\nexport PROFILE_ALLOWED=profile\nexport PROFILE_SECRET=secret\nexport PROFILE_DENIED=denied\nprofile_helper() {{ printf helper; }}\n{shadowed_builtins}{padding}"
),
)?;
if shell_name == "zsh" && automatic_startup {
std::fs::write(
home.path().join(".zshrc"),
"export PATH=\"$HOME/profile-bin:/usr/bin:/bin\"\n",
)?;
}
let mut configured_environment = HashMap::from([(
"HOME".to_string(),
home.path().to_string_lossy().into_owned(),
@@ -170,12 +189,19 @@ async fn shell_snapshot_v2_filters_profile_exports_and_stays_in_memory(
profile_path.to_string_lossy().into_owned(),
);
}
if shell_name == "bash" && automatic_startup {
configured_environment.insert(
"BASH_ENV".to_string(),
profile_path.to_string_lossy().into_owned(),
);
}
let policy = ExecEnvPolicy {
inherit: ShellEnvironmentPolicyInherit::All,
ignore_default_excludes: false,
exclude: vec!["PROFILE_DENIED".to_string()],
r#set: configured_environment,
include_only: vec![
"BASH_ENV".to_string(),
"ENV".to_string(),
"HOME".to_string(),
"PATH".to_string(),
+8
View File
@@ -145,6 +145,8 @@ pub enum Feature {
UseLegacyLandlock,
/// Experimental shell snapshotting.
ShellSnapshot,
/// Keep policy-filtered shell snapshots entirely in executor memory.
ShellSnapshotV2,
/// Allow turns to start while selected executors are still starting.
DeferredExecutor,
/// Use the current working directory for turn diff display paths.
@@ -871,6 +873,12 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::ShellSnapshotV2,
key: "shell_snapshot_v2",
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::DeferredExecutor,
key: "deferred_executor",