mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:04:37 +00:00
## Why Windows MXC needs valid host startup variables, while an empty child environment must remain empty for the SDK to select Windows user-profile defaults. ## What changed - Replace caller overrides of `SystemDrive` and `LOCALAPPDATA` case-insensitively with nonempty Unicode host values, failing before launch if either is unavailable. - Track whether the original child environment was empty, ignoring launcher transport keys. Supply host `SystemRoot` for launcher bootstrap in that case, then clear the bootstrap environment before the SDK selects defaults. ## Testing Extend transport tests to cover host-value replacement and preservation of empty child environments. Strengthen the PowerShell fallback RPC test to require `SystemDrive`, `LOCALAPPDATA`, `USERPROFILE`, and `PATH` in the child. GitOrigin-RevId: a097e1079069ef897ebb5ba5e63546328c5faeaa
237 lines
9.3 KiB
Rust
237 lines
9.3 KiB
Rust
//! Shared Windows sandbox behavior over the real exec-server RPC connection.
|
|
|
|
use super::*;
|
|
use codex_exec_server::ExecEnvPolicy;
|
|
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_protocol::permissions::FileSystemAccessMode;
|
|
use codex_protocol::permissions::FileSystemPath;
|
|
use codex_protocol::permissions::FileSystemSandboxEntry;
|
|
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
|
use codex_protocol::permissions::FileSystemSpecialPath;
|
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
/// Windows alias casing must not prevent fallback when the primary runtime is absent from PATH.
|
|
#[test_case::test_case("pwsh.exe"; "lowercase_pwsh")]
|
|
#[test_case::test_case("PWSH.exe"; "uppercase_pwsh")]
|
|
#[test_case::test_case("powershell.exe"; "lowercase_powershell")]
|
|
#[test_case::test_case("PowerShell.exe"; "mixed_case_powershell")]
|
|
#[cfg_attr(not(windows), ignore = "requires a native Windows sandbox")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[serial_test::serial(remote_exec_server)]
|
|
async fn powershell_alias_falls_back_without_primary_runtime_over_rpc(
|
|
shell_name: &str,
|
|
) -> Result<()> {
|
|
crate::skip_if_mxc_unavailable!(Ok(()));
|
|
let root = TempDir::new()?;
|
|
let system32 = std::path::PathBuf::from(std::env::var("SystemRoot")?).join("System32");
|
|
let server =
|
|
common::exec_server::exec_server_with_env([("PATH", system32.as_os_str())], &[]).await?;
|
|
let environment = Environment::create_for_tests(Some(server.websocket_url().to_owned()))?;
|
|
let cwd = PathUri::from_host_native_path(root.path())?;
|
|
let mut sandbox = FileSystemSandboxContext::from_permission_profile(
|
|
PermissionProfile::read_only(),
|
|
cwd.clone(),
|
|
);
|
|
sandbox.windows_sandbox_selection = codex_exec_server::WindowsSandboxSelection::Mxc;
|
|
let started = environment
|
|
.get_exec_backend()
|
|
.start(ExecParams {
|
|
process_id: ProcessId::from("windows-powershell-fallback"),
|
|
metadata: None,
|
|
argv: vec![
|
|
root.path()
|
|
.join("WindowsApps")
|
|
.join(shell_name)
|
|
.to_string_lossy()
|
|
.into_owned(),
|
|
"-NoProfile".to_owned(),
|
|
"-Command".to_owned(),
|
|
"if (!$env:SystemDrive -or !$env:LOCALAPPDATA -or !$env:USERPROFILE -or !$env:PATH) { throw 'missing Windows defaults' }; Write-Output 'fallback-ok'".to_owned(),
|
|
],
|
|
cwd,
|
|
env_policy: None,
|
|
shell_snapshot: None,
|
|
env: HashMap::new(),
|
|
tty: false,
|
|
pipe_stdin: false,
|
|
arg0: None,
|
|
sandbox: Some(sandbox),
|
|
enforce_managed_network: false,
|
|
managed_network: None,
|
|
network_proxy: None,
|
|
})
|
|
.await?;
|
|
assert_eq!(
|
|
started.sandbox_type,
|
|
Some(codex_sandboxing::SandboxType::WindowsMxc)
|
|
);
|
|
let (stdout, stderr, exit_code, exited) = collect_process_output_from_events_with_timeout(
|
|
started.process,
|
|
Duration::from_secs(/*secs*/ 30),
|
|
)
|
|
.await?;
|
|
assert_eq!(
|
|
(stdout.trim(), stderr, exit_code, exited),
|
|
("fallback-ok", String::new(), Some(0), true)
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case::test_case(WindowsSandboxSelection::Mxc, codex_sandboxing::SandboxType::WindowsMxc; "mxc")]
|
|
#[test_case::test_case(WindowsSandboxSelection::Elevated, codex_sandboxing::SandboxType::WindowsRestrictedToken; "elevated")]
|
|
#[cfg_attr(not(windows), ignore = "requires a native Windows sandbox")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[serial_test::serial(remote_exec_server)]
|
|
async fn tmpdir_uses_command_environment_over_rpc(
|
|
selection: WindowsSandboxSelection,
|
|
expected_type: codex_sandboxing::SandboxType,
|
|
) -> Result<()> {
|
|
if selection == WindowsSandboxSelection::Mxc {
|
|
crate::skip_if_mxc_unavailable!(Ok(()));
|
|
}
|
|
#[cfg(windows)]
|
|
let _account_guard = if selection == WindowsSandboxSelection::Elevated {
|
|
let guard = codex_windows_sandbox_test_support::WindowsSandboxAccountTestGuard::acquire()?;
|
|
// The RPC server re-enters this test executable, as in file_system_windows.
|
|
let executable = std::env::current_exe()?;
|
|
let resources = executable
|
|
.parent()
|
|
.context("Windows test executable should have a parent directory")?
|
|
.join("codex-resources");
|
|
if let Err(error) = std::fs::create_dir_all(&resources)
|
|
&& !(error.kind() == std::io::ErrorKind::PermissionDenied && resources.is_dir())
|
|
{
|
|
return Err(error).context("create Windows sandbox test resources");
|
|
}
|
|
for name in ["codex-windows-sandbox-setup", "codex-command-runner"] {
|
|
let source = codex_utils_cargo_bin::cargo_bin(name)?;
|
|
let destination = resources.join(std::path::Path::new(name).with_extension("exe"));
|
|
if let Err(error) = codex_utils_cargo_bin::copy_executable(&source, &destination)
|
|
&& !(error.kind() == std::io::ErrorKind::PermissionDenied && destination.is_file())
|
|
{
|
|
return Err(error).with_context(|| format!("stage Windows sandbox helper {name}"));
|
|
}
|
|
}
|
|
Some(guard)
|
|
} else {
|
|
None
|
|
};
|
|
let root = TempDir::new()?;
|
|
let command_temp = root.path().join("command temp");
|
|
let server_temp = root.path().join("server temp");
|
|
std::fs::create_dir(&command_temp)?;
|
|
std::fs::create_dir(&server_temp)?;
|
|
let outside = server_temp.join("outside.txt");
|
|
std::fs::write(&outside, "original")?;
|
|
let server = common::exec_server::exec_server_with_env(
|
|
[
|
|
("TEMP", server_temp.as_os_str()),
|
|
("TMP", server_temp.as_os_str()),
|
|
],
|
|
&[],
|
|
)
|
|
.await?;
|
|
let environment = Environment::create_for_tests(Some(server.websocket_url().to_owned()))?;
|
|
let cwd = PathUri::from_host_native_path(root.path())?;
|
|
let fs = FileSystemSandboxPolicy::restricted(vec![
|
|
FileSystemSandboxEntry::new(
|
|
FileSystemPath::Special {
|
|
value: FileSystemSpecialPath::Root,
|
|
},
|
|
FileSystemAccessMode::Read,
|
|
),
|
|
FileSystemSandboxEntry::new(
|
|
FileSystemPath::Special {
|
|
value: FileSystemSpecialPath::Tmpdir,
|
|
},
|
|
FileSystemAccessMode::Write,
|
|
),
|
|
]);
|
|
let mut sandbox = FileSystemSandboxContext::from_permission_profile(
|
|
PermissionProfile::from_runtime_permissions(&fs, NetworkSandboxPolicy::Restricted),
|
|
cwd.clone(),
|
|
);
|
|
sandbox.windows_sandbox_selection = selection;
|
|
let env = HashMap::from([
|
|
("SystemRoot".to_owned(), std::env::var("SystemRoot")?),
|
|
(
|
|
"ALLOWED_FILE".to_owned(),
|
|
format!("\"{}\"", command_temp.join("allowed.txt").display()),
|
|
),
|
|
(
|
|
"OUTSIDE_FILE".to_owned(),
|
|
format!("\"{}\"", outside.display()),
|
|
),
|
|
]);
|
|
// Raw params have no TEMP. The executor must finish the env policy before
|
|
// resolving permissions. No inheritance: omitted TMP must not grant server_temp.
|
|
let mut configured_temp = HashMap::from([(
|
|
"TEMP".to_owned(),
|
|
command_temp.to_string_lossy().into_owned(),
|
|
)]);
|
|
if selection == WindowsSandboxSelection::Mxc {
|
|
configured_temp.insert(
|
|
"TMP".to_owned(),
|
|
command_temp.to_string_lossy().into_owned(),
|
|
);
|
|
}
|
|
let started = environment
|
|
.get_exec_backend()
|
|
.start(ExecParams {
|
|
process_id: ProcessId::from("windows-sandbox-temp"),
|
|
metadata: None,
|
|
argv: vec![
|
|
r"C:\Windows\System32\cmd.exe".to_owned(),
|
|
"/D".to_owned(),
|
|
"/S".to_owned(),
|
|
"/C".to_owned(),
|
|
"echo CHILD-TEMP:%TEMP% & echo allowed>%ALLOWED_FILE% & echo modified>%OUTSIDE_FILE% & exit /b 0".to_owned(),
|
|
],
|
|
cwd,
|
|
env_policy: Some(ExecEnvPolicy {
|
|
inherit: ShellEnvironmentPolicyInherit::None,
|
|
ignore_default_excludes: false,
|
|
exclude: Vec::new(),
|
|
r#set: configured_temp,
|
|
include_only: Vec::new(),
|
|
}),
|
|
shell_snapshot: None,
|
|
env,
|
|
tty: false,
|
|
pipe_stdin: false,
|
|
arg0: None,
|
|
sandbox: Some(sandbox),
|
|
enforce_managed_network: false,
|
|
managed_network: None,
|
|
network_proxy: None,
|
|
})
|
|
.await?;
|
|
assert_eq!(started.sandbox_type, Some(expected_type));
|
|
let (stdout, stderr, code, exited) = collect_process_output_from_events_with_timeout(
|
|
started.process,
|
|
Duration::from_secs(/*secs*/ 30),
|
|
)
|
|
.await?;
|
|
assert_eq!(
|
|
(stdout.trim(), code, exited),
|
|
(
|
|
format!("CHILD-TEMP:{}", command_temp.display()).as_str(),
|
|
Some(0),
|
|
true
|
|
),
|
|
"unexpected launch result, stderr: {stderr}"
|
|
);
|
|
assert_eq!(
|
|
(
|
|
std::fs::read_to_string(command_temp.join("allowed.txt"))?
|
|
.trim_end()
|
|
.to_owned(),
|
|
std::fs::read_to_string(outside)?
|
|
),
|
|
("allowed".to_owned(), "original".to_owned())
|
|
);
|
|
Ok(())
|
|
}
|