mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Restore PowerShell's working directory in the MXC sandbox (#60647)
## Why When a workspace has an unreadable ancestor, PowerShell can start with `$PWD` unset and its filesystem drive at the root, breaking relative paths. ## What changed Prefix PowerShell commands in the MXC sandbox with a guarded workaround that restores the location from the process working directory when `$PWD` is unset. Preserve locations already initialized by startup or a profile. ## Testing Extend the MXC PowerShell integration test to cover an unreadable ancestor, checking that `$PWD` matches the process working directory, relative file access works, and the parent and sibling file remain inaccessible. GitOrigin-RevId: 4fb067db52a7f3c6e819169016fe6bd104a277a1
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# PowerShell/PowerShell#27899: a denied ancestor can leave $PWD unset and the
|
||||
# filesystem drive at its root. Use the process cwd without resolving it again.
|
||||
# A successful startup or a profile that changed location has already set $PWD.
|
||||
& {
|
||||
if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') { return }
|
||||
if ($null -ne $ExecutionContext.SessionState.PSVariable.GetValue('global:PWD')) { return }
|
||||
$drive = $ExecutionContext.SessionState.Drive.Current
|
||||
$cwd = [Environment]::CurrentDirectory
|
||||
if ($drive.Provider.Name -eq 'FileSystem' -and !$drive.CurrentLocation -and
|
||||
$drive.Root -eq [IO.Path]::GetPathRoot($cwd) -and $cwd.Length -gt $drive.Root.Length) {
|
||||
$drive.CurrentLocation = $cwd.Substring($drive.Root.Length)
|
||||
Microsoft.PowerShell.Utility\Set-Variable -Name PWD -Scope Global -Value $ExecutionContext.SessionState.Path.CurrentLocation -Force
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ use crate::unified_exec::UnifiedExecProcess;
|
||||
use crate::unified_exec::UnifiedExecProcessManager;
|
||||
use codex_core_plugins::PluginMetricsSidecar;
|
||||
use codex_features::Feature;
|
||||
use codex_file_system::WindowsSandboxSelection;
|
||||
use codex_network_proxy::CREDENTIAL_BROKER_ACTIVE_ENV_KEY;
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
@@ -545,18 +546,28 @@ impl<'a> ToolRuntime<UnifiedExecRequest<'_>, UnifiedExecAttempt> for UnifiedExec
|
||||
}
|
||||
// The executor selection is Disabled for non-Windows target paths, so this
|
||||
// preparation is a no-op for PowerShell on other platforms.
|
||||
let command = prepare_powershell_command_for_windows_sandbox(
|
||||
let windows_sandbox = executor_windows_sandbox_selection(
|
||||
attempt.windows_sandbox_type,
|
||||
attempt.windows_sandbox_level,
|
||||
attempt.sandbox_cwd,
|
||||
);
|
||||
let mut command = prepare_powershell_command_for_windows_sandbox(
|
||||
&command,
|
||||
Some(&requested_shell.shell_type),
|
||||
attempt.sandbox_requested,
|
||||
executor_windows_sandbox_selection(
|
||||
attempt.windows_sandbox_type,
|
||||
attempt.windows_sandbox_level,
|
||||
attempt.sandbox_cwd,
|
||||
),
|
||||
windows_sandbox,
|
||||
environment_is_remote,
|
||||
);
|
||||
let command = if matches!(requested_shell.shell_type, ShellType::PowerShell) {
|
||||
if attempt.sandbox_requested
|
||||
&& windows_sandbox == WindowsSandboxSelection::Mxc
|
||||
&& let [_, .., flag, script] = command.as_mut_slice()
|
||||
&& flag.eq_ignore_ascii_case("-Command")
|
||||
{
|
||||
// Temporary workaround for https://github.com/PowerShell/PowerShell/issues/27899.
|
||||
// Remove the prefix once supported PowerShell versions initialize $PWD under a denied ancestor.
|
||||
script.insert_str(0, include_str!("../../../assets/tools/mxc_pwsh_cwd.ps1"));
|
||||
}
|
||||
prefix_powershell_script_with_utf8(&command)
|
||||
} else {
|
||||
command
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_shell_command::shell_detect::DetectedShell;
|
||||
use codex_shell_command::shell_detect::ShellType;
|
||||
use core_test_support::responses::mount_function_call_agent_response;
|
||||
@@ -13,8 +17,10 @@ use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test_case::test_case(false; "store_fallback")]
|
||||
#[test_case::test_case(true; "unreadable_ancestor")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn mxc_replaces_store_powershell_before_launch() -> Result<()> {
|
||||
async fn mxc_exec_command_starts_powershell(restrict_parent: bool) -> Result<()> {
|
||||
if !codex_sandboxing::windows_mxc_available() {
|
||||
eprintln!("skipping test: native MXC is unavailable on this host");
|
||||
return Ok(());
|
||||
@@ -38,11 +44,49 @@ async fn mxc_replaces_store_powershell_before_launch() -> Result<()> {
|
||||
});
|
||||
// Discovery is intentionally host-local; remote execution must not use the host's shell.
|
||||
let harness = TestCodexHarness::with_builder(builder).await?;
|
||||
let (workdir, permissions, checks) = if restrict_parent {
|
||||
let ancestor = harness.path_abs("ancestor");
|
||||
let workspace = ancestor.join("Downloads");
|
||||
std::fs::create_dir_all(&workspace)?;
|
||||
std::fs::write(workspace.join("fixture.txt"), b"granted")?;
|
||||
std::fs::write(ancestor.join("private.txt"), b"private")?;
|
||||
let mut policy = FileSystemSandboxPolicy::read_only();
|
||||
policy.entries.extend([
|
||||
FileSystemSandboxEntry::new(ancestor.into(), FileSystemAccessMode::Deny),
|
||||
FileSystemSandboxEntry::new(workspace.clone().into(), FileSystemAccessMode::Write),
|
||||
]);
|
||||
let checks = r#"
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if ($PWD.Path -ne [Environment]::CurrentDirectory) { throw "Unexpected PowerShell location: $PWD" }
|
||||
if ((Get-Content -LiteralPath .\fixture.txt -Raw) -ne 'granted') { throw 'Relative read failed' }
|
||||
$parent = [IO.Path]::GetDirectoryName([Environment]::CurrentDirectory)
|
||||
try {
|
||||
[void][IO.File]::GetAttributes($parent)
|
||||
throw 'The workspace parent allowed an attribute read'
|
||||
} catch [UnauthorizedAccessException] {}
|
||||
try {
|
||||
[void][IO.File]::ReadAllText([IO.Path]::Combine($parent, 'private.txt'))
|
||||
throw 'The sibling was readable'
|
||||
} catch [UnauthorizedAccessException] {} catch [IO.FileNotFoundException] {}
|
||||
"#;
|
||||
(
|
||||
workspace,
|
||||
PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted),
|
||||
checks,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
harness.path_abs(""),
|
||||
PermissionProfile::workspace_write(),
|
||||
"",
|
||||
)
|
||||
};
|
||||
let mock = mount_function_call_agent_response(
|
||||
harness.server(),
|
||||
"mxc-powershell",
|
||||
&json!({
|
||||
"cmd": "Set-Content -LiteralPath fallback.txt -Value mxc-powershell -NoNewline",
|
||||
"cmd": format!("{checks}\nSet-Content -LiteralPath fallback.txt -Value mxc-powershell -NoNewline"),
|
||||
"workdir": workdir,
|
||||
"login": false,
|
||||
"yield_time_ms": 10_000,
|
||||
})
|
||||
@@ -53,7 +97,7 @@ async fn mxc_replaces_store_powershell_before_launch() -> Result<()> {
|
||||
harness
|
||||
.submit_with_permission_profile(
|
||||
"Write a file using PowerShell in the sandbox.",
|
||||
PermissionProfile::workspace_write(),
|
||||
permissions.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -89,7 +133,7 @@ async fn mxc_replaces_store_powershell_before_launch() -> Result<()> {
|
||||
harness
|
||||
.submit_with_permission_profile(
|
||||
"Wait for the PowerShell command to finish.",
|
||||
PermissionProfile::workspace_write(),
|
||||
permissions.clone(),
|
||||
)
|
||||
.await?;
|
||||
let request = poll_mock.completion.single_request();
|
||||
@@ -105,7 +149,7 @@ async fn mxc_replaces_store_powershell_before_launch() -> Result<()> {
|
||||
.context("timed out waiting for MXC PowerShell to exit")??;
|
||||
assert!(output.contains("Process exited with code 0"), "{output}");
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(harness.path("fallback.txt"))?,
|
||||
std::fs::read_to_string(workdir.join("fallback.txt"))?,
|
||||
"mxc-powershell"
|
||||
);
|
||||
let metadata: serde_json::Value = serde_json::from_str(
|
||||
|
||||
Reference in New Issue
Block a user