mirror of
https://github.com/openai/codex.git
synced 2026-10-26 18:38:47 +00:00
## Why Shell startup files can introduce credentials or overwrite brokered dummy values. Snapshot capture and replay need to respect the command's sandbox and environment policy without persisting real credentials. ## What changed - Capture and validate brokered snapshots per command using its requested shell, working directory, login mode, sandbox, and environment policy. - Discover shell-initialized credentials and prepare snapshot exports for brokered replay. Preserve shell functions and aliases while preventing startup files from overwriting dummy credentials during replay. - Rebuild snapshots as credential broker state changes and retain each command's snapshot for the lifetime of its process. - Reject brokered POSIX shell execution when a protected snapshot cannot be created, and reject brokerage with `shell_zsh_fork`. Restore real credentials and remove brokerage markers for unsupported PowerShell and Cmd execution; escalated commands bypass protected snapshots. ## Testing Add regression coverage for credential discovery and redaction, environment filtering, readonly exports, shell function and heredoc replay, command working directories and login modes, sandbox enforcement, and missing or disabled snapshots. GitOrigin-RevId: 685356cb9e80f11cf69d053e4d239ee6ccf43a9e
697 lines
24 KiB
Rust
697 lines
24 KiB
Rust
/*
|
|
Module: runtimes
|
|
|
|
Concrete ToolRuntime implementations for specific tools. Each runtime stays
|
|
small and focused and reuses the orchestrator for approvals + sandbox + retry.
|
|
*/
|
|
use crate::exec_env::CODEX_PERMISSION_PROFILE_ENV_VAR;
|
|
use crate::exec_env::CODEX_SESSION_ID_ENV_VAR;
|
|
use crate::exec_env::CODEX_THREAD_ID_ENV_VAR;
|
|
use crate::exec_env::CODEX_VERSION_ENV_VAR;
|
|
use crate::sandboxing::SandboxPermissions;
|
|
use crate::shell::Shell;
|
|
use crate::shell::ShellType;
|
|
use codex_apply_patch::CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR;
|
|
use codex_core_plugins::PLUGIN_METRICS_OUTPUT_ENV_VAR;
|
|
#[cfg(unix)]
|
|
use codex_install_context::InstallContext;
|
|
#[cfg(target_os = "macos")]
|
|
use codex_network_proxy::CODEX_PROXY_GIT_SSH_COMMAND_MARKER;
|
|
use codex_network_proxy::CREDENTIAL_BROKER_ACTIVE_ENV_KEY;
|
|
use codex_network_proxy::CUSTOM_CA_ENV_KEYS;
|
|
use codex_network_proxy::PROXY_ACTIVE_ENV_KEY;
|
|
use codex_network_proxy::PROXY_ENV_KEYS;
|
|
#[cfg(target_os = "macos")]
|
|
use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY;
|
|
pub(crate) use codex_network_proxy::is_managed_proxy_env_var;
|
|
pub(crate) use codex_network_proxy::strip_managed_proxy_env;
|
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
|
use codex_protocol::shell_environment::is_non_inheritable_env_var;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
|
|
pub(crate) mod apply_patch;
|
|
pub(crate) mod unified_exec;
|
|
pub(crate) mod zsh_fork;
|
|
|
|
pub(crate) fn exec_env_for_sandbox_permissions(
|
|
env: &HashMap<String, String>,
|
|
sandbox_permissions: SandboxPermissions,
|
|
) -> HashMap<String, String> {
|
|
let mut env = env.clone();
|
|
// Escalated commands intentionally use the original, unbrokered environment. This branch is
|
|
// defensive cleanup for a caller that passes an environment already prepared by Codex.
|
|
if sandbox_permissions.requires_escalated_permissions()
|
|
&& env.contains_key(PROXY_ACTIVE_ENV_KEY)
|
|
{
|
|
strip_managed_proxy_env(&mut env);
|
|
}
|
|
env
|
|
}
|
|
|
|
/// Prepends `path_entry` to `PATH`, removing duplicate and empty existing
|
|
/// entries.
|
|
///
|
|
/// Returns the updated `PATH` value when `env` was changed. Returns `None` when
|
|
/// `path_entry` is empty, leaving `env` untouched so an empty entry does not add
|
|
/// the current working directory to command lookup.
|
|
#[cfg(unix)]
|
|
fn prepend_path_entry(env: &mut HashMap<String, String>, path_entry: &str) -> Option<String> {
|
|
if path_entry.is_empty() {
|
|
None
|
|
} else {
|
|
let updated_path = match env.get("PATH") {
|
|
Some(path) if !path.is_empty() => std::iter::once(path_entry)
|
|
.chain(
|
|
path.split(':')
|
|
.filter(|entry| !entry.is_empty() && *entry != path_entry),
|
|
)
|
|
.collect::<Vec<_>>()
|
|
.join(":"),
|
|
_ => path_entry.to_string(),
|
|
};
|
|
env.insert("PATH".to_string(), updated_path.clone());
|
|
Some(updated_path)
|
|
}
|
|
}
|
|
|
|
/// PATH entries owned by Codex runtime setup.
|
|
///
|
|
/// These are applied to the live exec environment immediately and replayed after
|
|
/// restoring a shell snapshot, unless the user explicitly overrides `PATH`.
|
|
#[derive(Debug, Default, Eq, PartialEq)]
|
|
pub(crate) struct RuntimePathPrepends {
|
|
entries: Vec<String>,
|
|
}
|
|
|
|
impl RuntimePathPrepends {
|
|
#[cfg(unix)]
|
|
pub(crate) fn prepend(&mut self, env: &mut HashMap<String, String>, path_entry: &Path) {
|
|
let path_entry = path_entry.to_string_lossy().to_string();
|
|
if prepend_path_entry(env, &path_entry).is_some() {
|
|
self.entries.retain(|entry| entry != &path_entry);
|
|
self.entries.push(path_entry);
|
|
}
|
|
}
|
|
|
|
fn shell_exports_after_snapshot(
|
|
&self,
|
|
explicit_env_overrides: &HashMap<String, String>,
|
|
) -> String {
|
|
if explicit_env_overrides.contains_key("PATH") {
|
|
return String::new();
|
|
}
|
|
|
|
self.entries
|
|
.iter()
|
|
.filter(|entry| !entry.is_empty())
|
|
.map(|entry| {
|
|
let entry = shell_single_quote(entry);
|
|
format!(
|
|
"if [ -n \"${{PATH:-}}\" ]; then export PATH='{entry}':\"$PATH\"; else export PATH='{entry}'; fi"
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
pub(crate) fn apply_package_path_prepend(
|
|
env: &mut HashMap<String, String>,
|
|
runtime_path_prepends: &mut RuntimePathPrepends,
|
|
) {
|
|
let Some(path_dir) = InstallContext::current()
|
|
.package_layout
|
|
.as_ref()
|
|
.and_then(|package_layout| package_layout.path_dir.as_ref())
|
|
else {
|
|
return;
|
|
};
|
|
|
|
runtime_path_prepends.prepend(env, path_dir.as_path());
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
pub(crate) fn apply_zsh_fork_path_prepend(
|
|
env: &mut HashMap<String, String>,
|
|
runtime_path_prepends: &mut RuntimePathPrepends,
|
|
shell_zsh_path: &Path,
|
|
) {
|
|
let Some(zsh_bin_dir) = shell_zsh_path.parent() else {
|
|
return;
|
|
};
|
|
runtime_path_prepends.prepend(env, zsh_bin_dir);
|
|
}
|
|
|
|
pub(crate) fn prepare_powershell_command_for_elevated_windows_sandbox(
|
|
command: &[String],
|
|
shell_type: Option<&ShellType>,
|
|
sandbox_requested: bool,
|
|
windows_sandbox_level: WindowsSandboxLevel,
|
|
environment_is_remote: bool,
|
|
) -> Vec<String> {
|
|
prepare_powershell_command_for_elevated_windows_sandbox_with_fallback(
|
|
command,
|
|
shell_type,
|
|
sandbox_requested,
|
|
windows_sandbox_level,
|
|
environment_is_remote,
|
|
|path| {
|
|
codex_shell_command::shell_detect::fallback_powershell_shell_for_elevated_windows_sandbox(
|
|
path,
|
|
)
|
|
},
|
|
)
|
|
}
|
|
|
|
fn prepare_powershell_command_for_elevated_windows_sandbox_with_fallback(
|
|
command: &[String],
|
|
shell_type: Option<&ShellType>,
|
|
sandbox_requested: bool,
|
|
windows_sandbox_level: WindowsSandboxLevel,
|
|
environment_is_remote: bool,
|
|
find_fallback: impl FnOnce(&Path) -> Option<codex_shell_command::shell_detect::DetectedShell>,
|
|
) -> Vec<String> {
|
|
if shell_type != Some(&ShellType::PowerShell)
|
|
|| !sandbox_requested
|
|
|| windows_sandbox_level != WindowsSandboxLevel::Elevated
|
|
|| command.is_empty()
|
|
{
|
|
return command.to_vec();
|
|
}
|
|
|
|
let mut command = command.to_vec();
|
|
if !environment_is_remote && let Some(fallback) = find_fallback(Path::new(&command[0])) {
|
|
command[0] = fallback.shell_path.to_string_lossy().to_string();
|
|
}
|
|
|
|
if command[1..]
|
|
.iter()
|
|
.any(|arg| arg.eq_ignore_ascii_case("-NoProfile"))
|
|
{
|
|
return command;
|
|
}
|
|
|
|
// The elevated Windows sandbox runs as a dedicated sandbox account while
|
|
// HOME/USERPROFILE may still point at the real user profile. Loading
|
|
// PowerShell profiles in that mixed context is not a valid login shell.
|
|
command.insert(1, "-NoProfile".to_string());
|
|
command
|
|
}
|
|
|
|
pub(crate) fn prepare_brokered_shell_snapshot_env(
|
|
env: &mut HashMap<String, String>,
|
|
shell_snapshot: Option<&AbsolutePathBuf>,
|
|
) {
|
|
if shell_snapshot.is_some()
|
|
&& env
|
|
.get(CREDENTIAL_BROKER_ACTIVE_ENV_KEY)
|
|
.is_some_and(|active| active == "1")
|
|
{
|
|
env.remove("BASH_ENV");
|
|
}
|
|
}
|
|
|
|
/// POSIX-only helper: for commands produced by `Shell::derive_exec_args`
|
|
/// for Bash/Zsh/sh of the form `[shell_path, "-lc", "<script>"]` or brokered
|
|
/// `[shell_path, "-c", "<script>"]`, and
|
|
/// when a snapshot is configured on the session shell, rewrite the argv
|
|
/// to a single non-login shell that sources the snapshot before running
|
|
/// the original script:
|
|
///
|
|
/// shell -lc "<script>"
|
|
/// => user_shell -c ". SNAPSHOT (best effort); exec shell -c <script>"
|
|
///
|
|
/// Brokered Bash and Zsh commands targeting the session shell run in that process
|
|
/// to preserve captured shell functions and avoid asking patched Zsh to exec itself.
|
|
/// Brokered Zsh wrappers use `-f` so startup files cannot overwrite credentials
|
|
/// after the proxy has replaced them with dummies.
|
|
///
|
|
/// This wrapper script uses POSIX constructs (`if`, `.`, `exec`) so it can
|
|
/// be run by Bash/Zsh/sh. On non-matching commands, or when command cwd does
|
|
/// not match the snapshot cwd, this is a no-op.
|
|
///
|
|
/// `explicit_env_overrides` and `env` are intentionally separate inputs.
|
|
/// `explicit_env_overrides` contains policy-driven shell env overrides that
|
|
/// should win after the snapshot is sourced, while `env` is the full live exec
|
|
/// environment. We need access to both so snapshot restore logic can preserve
|
|
/// runtime-only vars like `CODEX_THREAD_ID` without pretending they came from
|
|
/// the explicit override policy.
|
|
///
|
|
/// `runtime_path_prepends` contains Codex-owned PATH entries already applied to
|
|
/// the live `env`; snapshot wrapping replays them after restoring the snapshot
|
|
/// PATH unless the user explicitly overrides `PATH`.
|
|
pub(crate) fn maybe_wrap_shell_lc_with_snapshot(
|
|
command: &[String],
|
|
session_shell: &Shell,
|
|
shell_snapshot: Option<&AbsolutePathBuf>,
|
|
explicit_env_overrides: &HashMap<String, String>,
|
|
env: &HashMap<String, String>,
|
|
runtime_path_prepends: &RuntimePathPrepends,
|
|
) -> Vec<String> {
|
|
if cfg!(windows) {
|
|
return command.to_vec();
|
|
}
|
|
|
|
let Some(snapshot) = shell_snapshot else {
|
|
return command.to_vec();
|
|
};
|
|
|
|
if !snapshot.exists() {
|
|
return command.to_vec();
|
|
}
|
|
|
|
if command.len() < 3 {
|
|
return command.to_vec();
|
|
}
|
|
|
|
let brokered = env
|
|
.get(CREDENTIAL_BROKER_ACTIVE_ENV_KEY)
|
|
.is_some_and(|active| active == "1");
|
|
let flag = command[1].as_str();
|
|
if flag != "-lc" && !(brokered && flag == "-c") {
|
|
return command.to_vec();
|
|
}
|
|
|
|
let snapshot_path = snapshot.to_string_lossy();
|
|
let shell_path = session_shell.shell_path.to_string_lossy();
|
|
let command_uses_session_zsh =
|
|
session_shell.shell_type == ShellType::Zsh && command[0] == shell_path.as_ref();
|
|
let reuse_session_shell = brokered
|
|
&& command[0] == shell_path.as_ref()
|
|
&& matches!(session_shell.shell_type, ShellType::Bash | ShellType::Zsh);
|
|
let original_shell_is_zsh = command_uses_session_zsh
|
|
|| codex_shell_command::shell_detect::detect_shell_type(&command[0])
|
|
== Some(ShellType::Zsh);
|
|
let brokered_zsh_flag = if flag == "-lc" { "-lfc" } else { "-fc" };
|
|
let original_shell_flag = if brokered && original_shell_is_zsh {
|
|
brokered_zsh_flag
|
|
} else {
|
|
"-c"
|
|
};
|
|
let original_shell = shell_single_quote(&command[0]);
|
|
let snapshot_path = shell_single_quote(snapshot_path.as_ref());
|
|
let trailing_args = command[3..]
|
|
.iter()
|
|
.map(|arg| format!(" '{}'", shell_single_quote(arg)))
|
|
.collect::<String>();
|
|
let mut override_env = explicit_env_overrides.clone();
|
|
for key in [
|
|
CODEX_SESSION_ID_ENV_VAR,
|
|
CODEX_THREAD_ID_ENV_VAR,
|
|
CODEX_VERSION_ENV_VAR,
|
|
CODEX_PERMISSION_PROFILE_ENV_VAR,
|
|
CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR,
|
|
PLUGIN_METRICS_OUTPUT_ENV_VAR,
|
|
] {
|
|
if let Some(value) = env.get(key) {
|
|
override_env.insert(key.to_string(), value.clone());
|
|
}
|
|
}
|
|
// Do not let a snapshot resurrect stale runtime state when it is inactive.
|
|
let (override_captures, override_exports) = build_override_exports(
|
|
&override_env,
|
|
&[
|
|
CODEX_PERMISSION_PROFILE_ENV_VAR,
|
|
CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR,
|
|
PLUGIN_METRICS_OUTPUT_ENV_VAR,
|
|
],
|
|
);
|
|
let (proxy_captures, proxy_exports) = build_proxy_env_exports(env);
|
|
let runtime_path_prepend_exports =
|
|
runtime_path_prepends.shell_exports_after_snapshot(explicit_env_overrides);
|
|
let override_captures = join_shell_blocks([override_captures, proxy_captures]);
|
|
let override_exports = join_shell_blocks([
|
|
override_exports,
|
|
proxy_exports,
|
|
runtime_path_prepend_exports,
|
|
]);
|
|
let run_original = if reuse_session_shell {
|
|
command[2].clone()
|
|
} else {
|
|
let original_script = shell_single_quote(&command[2]);
|
|
format!("exec '{original_shell}' {original_shell_flag} '{original_script}'{trailing_args}")
|
|
};
|
|
let rewritten_script = if override_exports.is_empty() {
|
|
format!("if . '{snapshot_path}' >/dev/null 2>&1; then :; fi\n\n{run_original}")
|
|
} else {
|
|
format!(
|
|
"{override_captures}\n\nif . '{snapshot_path}' >/dev/null 2>&1; then :; fi\n\n{override_exports}\n\n{run_original}"
|
|
)
|
|
};
|
|
|
|
let wrapper_flag = if brokered && session_shell.shell_type == ShellType::Zsh {
|
|
brokered_zsh_flag
|
|
} else {
|
|
"-c"
|
|
};
|
|
let mut rewritten = vec![
|
|
shell_path.to_string(),
|
|
wrapper_flag.to_string(),
|
|
rewritten_script,
|
|
];
|
|
if reuse_session_shell {
|
|
rewritten.extend_from_slice(&command[3..]);
|
|
}
|
|
rewritten
|
|
}
|
|
|
|
fn build_override_exports(
|
|
explicit_env_overrides: &HashMap<String, String>,
|
|
restore_even_when_absent: &[&str],
|
|
) -> (String, String) {
|
|
let mut keys = explicit_env_overrides
|
|
.keys()
|
|
.map(String::as_str)
|
|
.chain(restore_even_when_absent.iter().copied())
|
|
.filter(|key| !is_non_inheritable_env_var(key))
|
|
.filter(|key| is_valid_shell_variable_name(key))
|
|
.collect::<Vec<_>>();
|
|
keys.sort_unstable();
|
|
keys.dedup();
|
|
|
|
build_override_exports_for_keys("__CODEX_SNAPSHOT_OVERRIDE", &keys)
|
|
}
|
|
|
|
fn build_proxy_env_exports(env: &HashMap<String, String>) -> (String, String) {
|
|
let mut keys = PROXY_ENV_KEYS
|
|
.iter()
|
|
.copied()
|
|
.chain(codex_network_proxy::brokered_credential_env_keys(env))
|
|
.chain(CUSTOM_CA_ENV_KEYS)
|
|
.chain(
|
|
env.get(CREDENTIAL_BROKER_ACTIVE_ENV_KEY)
|
|
.filter(|active| active.as_str() == "1")
|
|
.map(|_| "BASH_ENV"),
|
|
)
|
|
.filter(|key| is_valid_shell_variable_name(key))
|
|
.collect::<Vec<_>>();
|
|
keys.sort_unstable();
|
|
keys.dedup();
|
|
|
|
let (captures, restores) =
|
|
build_override_exports_for_keys("__CODEX_SNAPSHOT_PROXY_OVERRIDE", &keys);
|
|
let key = PROXY_ACTIVE_ENV_KEY;
|
|
let proxy_blocks = (
|
|
format!("{captures}\n__CODEX_SNAPSHOT_PROXY_ENV_SET=\"${{{key}+x}}\""),
|
|
format!(
|
|
"if [ -n \"$__CODEX_SNAPSHOT_PROXY_ENV_SET\" ] || [ -n \"${{{key}+x}}\" ]; then\n{restores}\nfi"
|
|
),
|
|
);
|
|
let git_blocks = build_codex_proxy_git_ssh_command_exports();
|
|
(
|
|
join_shell_blocks([proxy_blocks.0, git_blocks.0]),
|
|
join_shell_blocks([proxy_blocks.1, git_blocks.1]),
|
|
)
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
fn build_codex_proxy_git_ssh_command_exports() -> (String, String) {
|
|
let key = PROXY_GIT_SSH_COMMAND_ENV_KEY;
|
|
let marker_pattern = format!("{}\\ *", CODEX_PROXY_GIT_SSH_COMMAND_MARKER.trim_end());
|
|
(
|
|
format!(
|
|
"__CODEX_SNAPSHOT_PROXY_GIT_SSH_COMMAND_SET=\"${{{key}+x}}\"\n__CODEX_SNAPSHOT_PROXY_GIT_SSH_COMMAND=\"${{{key}-}}\"\ncase \"$__CODEX_SNAPSHOT_PROXY_GIT_SSH_COMMAND\" in\n {marker_pattern}) __CODEX_SNAPSHOT_PROXY_GIT_SSH_COMMAND_LIVE_MARKED=1 ;;\n *) __CODEX_SNAPSHOT_PROXY_GIT_SSH_COMMAND_LIVE_MARKED= ;;\nesac"
|
|
),
|
|
format!(
|
|
"case \"${{{key}-}}\" in\n {marker_pattern}) __CODEX_SNAPSHOT_PROXY_GIT_SSH_COMMAND_AFTER_MARKED=1 ;;\n *) __CODEX_SNAPSHOT_PROXY_GIT_SSH_COMMAND_AFTER_MARKED= ;;\nesac\nif [ -n \"$__CODEX_SNAPSHOT_PROXY_GIT_SSH_COMMAND_LIVE_MARKED\" ]; then\n if [ -z \"${{{key}+x}}\" ] || [ -n \"$__CODEX_SNAPSHOT_PROXY_GIT_SSH_COMMAND_AFTER_MARKED\" ]; then\n export {key}=\"$__CODEX_SNAPSHOT_PROXY_GIT_SSH_COMMAND\"\n fi\nelif [ -n \"$__CODEX_SNAPSHOT_PROXY_GIT_SSH_COMMAND_AFTER_MARKED\" ]; then\n if [ -n \"$__CODEX_SNAPSHOT_PROXY_GIT_SSH_COMMAND_SET\" ]; then\n export {key}=\"$__CODEX_SNAPSHOT_PROXY_GIT_SSH_COMMAND\"\n else\n unset {key}\n fi\nfi"
|
|
),
|
|
)
|
|
}
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
fn build_codex_proxy_git_ssh_command_exports() -> (String, String) {
|
|
(String::new(), String::new())
|
|
}
|
|
|
|
fn build_override_exports_for_keys(variable_prefix: &str, keys: &[&str]) -> (String, String) {
|
|
if keys.is_empty() {
|
|
return (String::new(), String::new());
|
|
}
|
|
|
|
let captures = keys
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(idx, key)| {
|
|
let set_var = format!("{variable_prefix}_SET_{idx}");
|
|
let value_var = format!("{variable_prefix}_{idx}");
|
|
format!("{set_var}=\"${{{key}+x}}\"\n{value_var}=\"${{{key}-}}\"")
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
let restores = keys
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(idx, key)| {
|
|
let set_var = format!("{variable_prefix}_SET_{idx}");
|
|
let value_var = format!("{variable_prefix}_{idx}");
|
|
format!(
|
|
"if [ -n \"${{{set_var}}}\" ]; then\n if [ -z \"${{{key}+x}}\" ] || [ \"${{{key}-}}\" != \"${{{value_var}}}\" ]; then export {key}=\"${{{value_var}}}\"; else export {key}; fi\nelse builtin unset {key} 2>/dev/null || command unset {key}; fi"
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
|
|
(captures, restores)
|
|
}
|
|
|
|
fn join_shell_blocks(blocks: impl IntoIterator<Item = String>) -> String {
|
|
blocks
|
|
.into_iter()
|
|
.filter(|block| !block.is_empty())
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
|
|
fn is_valid_shell_variable_name(name: &str) -> bool {
|
|
let mut chars = name.chars();
|
|
let Some(first) = chars.next() else {
|
|
return false;
|
|
};
|
|
if !(first == '_' || first.is_ascii_alphabetic()) {
|
|
return false;
|
|
}
|
|
chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
|
|
}
|
|
|
|
fn shell_single_quote(input: &str) -> String {
|
|
input.replace('\'', r#"'"'"'"#)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod prepare_powershell_command_tests {
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
#[test]
|
|
fn inserts_no_profile_for_elevated_windows_sandbox() {
|
|
let command = vec![
|
|
"powershell.exe".to_string(),
|
|
"-Command".to_string(),
|
|
"Write-Output ok".to_string(),
|
|
];
|
|
|
|
let rewritten = prepare_powershell_command_for_elevated_windows_sandbox(
|
|
&command,
|
|
Some(&ShellType::PowerShell),
|
|
/*sandbox_requested*/ true,
|
|
WindowsSandboxLevel::Elevated,
|
|
/*environment_is_remote*/ false,
|
|
);
|
|
|
|
assert_eq!(
|
|
rewritten,
|
|
vec![
|
|
"powershell.exe".to_string(),
|
|
"-NoProfile".to_string(),
|
|
"-Command".to_string(),
|
|
"Write-Output ok".to_string(),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn inserts_no_profile_before_encoded_command() {
|
|
let command = vec![
|
|
"powershell.exe".to_string(),
|
|
"-EncodedCommand".to_string(),
|
|
"VwByAGkAdABlAC0ATwB1AHQAcAB1AHQAIABvAGsA".to_string(),
|
|
];
|
|
|
|
let rewritten = prepare_powershell_command_for_elevated_windows_sandbox(
|
|
&command,
|
|
Some(&ShellType::PowerShell),
|
|
/*sandbox_requested*/ true,
|
|
WindowsSandboxLevel::Elevated,
|
|
/*environment_is_remote*/ false,
|
|
);
|
|
|
|
assert_eq!(
|
|
rewritten,
|
|
vec![
|
|
"powershell.exe".to_string(),
|
|
"-NoProfile".to_string(),
|
|
"-EncodedCommand".to_string(),
|
|
"VwByAGkAdABlAC0ATwB1AHQAcAB1AHQAIABvAGsA".to_string(),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn preserves_existing_no_profile() {
|
|
let command = vec![
|
|
"pwsh.exe".to_string(),
|
|
"-NoProfile".to_string(),
|
|
"-Command".to_string(),
|
|
"Write-Output ok".to_string(),
|
|
];
|
|
|
|
let rewritten = prepare_powershell_command_for_elevated_windows_sandbox(
|
|
&command,
|
|
Some(&ShellType::PowerShell),
|
|
/*sandbox_requested*/ true,
|
|
WindowsSandboxLevel::Elevated,
|
|
/*environment_is_remote*/ false,
|
|
);
|
|
|
|
assert_eq!(rewritten, command);
|
|
}
|
|
|
|
#[test]
|
|
fn leaves_legacy_restricted_token_backend_alone() {
|
|
let command = vec![
|
|
r"C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe"
|
|
.to_string(),
|
|
"-Command".to_string(),
|
|
"Write-Output ok".to_string(),
|
|
];
|
|
|
|
let rewritten = prepare_powershell_command_for_elevated_windows_sandbox(
|
|
&command,
|
|
Some(&ShellType::PowerShell),
|
|
/*sandbox_requested*/ true,
|
|
WindowsSandboxLevel::RestrictedToken,
|
|
/*environment_is_remote*/ false,
|
|
);
|
|
|
|
assert_eq!(rewritten, command);
|
|
}
|
|
|
|
#[test]
|
|
fn leaves_unsandboxed_attempts_alone() {
|
|
let command = vec![
|
|
r"C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe"
|
|
.to_string(),
|
|
"-Command".to_string(),
|
|
"Write-Output ok".to_string(),
|
|
];
|
|
|
|
let rewritten = prepare_powershell_command_for_elevated_windows_sandbox(
|
|
&command,
|
|
Some(&ShellType::PowerShell),
|
|
/*sandbox_requested*/ false,
|
|
WindowsSandboxLevel::Elevated,
|
|
/*environment_is_remote*/ false,
|
|
);
|
|
|
|
assert_eq!(rewritten, command);
|
|
}
|
|
|
|
#[test]
|
|
fn leaves_non_powershell_alone() {
|
|
let command = vec![
|
|
"/bin/bash".to_string(),
|
|
"-lc".to_string(),
|
|
"echo ok".to_string(),
|
|
];
|
|
|
|
let rewritten = prepare_powershell_command_for_elevated_windows_sandbox(
|
|
&command,
|
|
Some(&ShellType::Bash),
|
|
/*sandbox_requested*/ true,
|
|
WindowsSandboxLevel::Elevated,
|
|
/*environment_is_remote*/ false,
|
|
);
|
|
|
|
assert_eq!(rewritten, command);
|
|
}
|
|
|
|
#[test]
|
|
fn local_elevated_powershell_uses_discovered_fallback() {
|
|
let command = vec![
|
|
r"C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe"
|
|
.to_string(),
|
|
"-Command".to_string(),
|
|
"Write-Output ok".to_string(),
|
|
];
|
|
let fallback_path = std::path::PathBuf::from(r"C:\Program Files\PowerShell\7\pwsh.exe");
|
|
|
|
let rewritten = prepare_powershell_command_for_elevated_windows_sandbox_with_fallback(
|
|
&command,
|
|
Some(&ShellType::PowerShell),
|
|
/*sandbox_requested*/ true,
|
|
WindowsSandboxLevel::Elevated,
|
|
/*environment_is_remote*/ false,
|
|
|_| {
|
|
Some(codex_shell_command::shell_detect::DetectedShell {
|
|
shell_type: ShellType::PowerShell,
|
|
shell_path: fallback_path.clone(),
|
|
})
|
|
},
|
|
);
|
|
|
|
assert_eq!(
|
|
rewritten,
|
|
vec![
|
|
fallback_path.to_string_lossy().to_string(),
|
|
"-NoProfile".to_string(),
|
|
"-Command".to_string(),
|
|
"Write-Output ok".to_string(),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn remote_elevated_powershell_keeps_remote_store_path_and_no_profile() {
|
|
let command = vec![
|
|
r"C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe"
|
|
.to_string(),
|
|
"-Command".to_string(),
|
|
"Write-Output ok".to_string(),
|
|
];
|
|
|
|
let mut discovery_called = false;
|
|
let rewritten = prepare_powershell_command_for_elevated_windows_sandbox_with_fallback(
|
|
&command,
|
|
Some(&ShellType::PowerShell),
|
|
/*sandbox_requested*/ true,
|
|
WindowsSandboxLevel::Elevated,
|
|
/*environment_is_remote*/ true,
|
|
|_| {
|
|
discovery_called = true;
|
|
Some(codex_shell_command::shell_detect::DetectedShell {
|
|
shell_type: ShellType::PowerShell,
|
|
shell_path: std::path::PathBuf::from(r"C:\Program Files\PowerShell\7\pwsh.exe"),
|
|
})
|
|
},
|
|
);
|
|
|
|
assert!(!discovery_called);
|
|
assert_eq!(
|
|
rewritten,
|
|
vec![
|
|
command[0].clone(),
|
|
"-NoProfile".to_string(),
|
|
"-Command".to_string(),
|
|
"Write-Output ok".to_string(),
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(all(test, unix))]
|
|
#[path = "mod_tests.rs"]
|
|
mod tests;
|