Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 12 additions & 3 deletions codex-rs/app-server/src/command_exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,13 +275,22 @@ impl CommandExecManager {
&env,
&arg0,
size.unwrap_or_default(),
&[],
)
.await
} else if stream_stdin {
codex_utils_pty::spawn_pipe_process(program, args, cwd.as_path(), &env, &arg0).await
} else {
codex_utils_pty::spawn_pipe_process_no_stdin(program, args, cwd.as_path(), &env, &arg0)
codex_utils_pty::spawn_pipe_process(program, args, cwd.as_path(), &env, &arg0, &[])
.await
} else {
codex_utils_pty::spawn_pipe_process_no_stdin(
program,
args,
cwd.as_path(),
&env,
&arg0,
&[],
)
.await
};
let spawned = match spawned {
Ok(spawned) => spawned,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,13 +312,22 @@ impl ProcessExecManager {
&env,
&arg0,
size.unwrap_or_default(),
&[],
)
.await
} else if stream_stdin {
codex_utils_pty::spawn_pipe_process(program, args, cwd.as_path(), &env, &arg0).await
} else {
codex_utils_pty::spawn_pipe_process_no_stdin(program, args, cwd.as_path(), &env, &arg0)
codex_utils_pty::spawn_pipe_process(program, args, cwd.as_path(), &env, &arg0, &[])
.await
} else {
codex_utils_pty::spawn_pipe_process_no_stdin(
program,
args,
cwd.as_path(),
&env,
&arg0,
&[],
)
.await
};
let spawned = match spawned {
Ok(spawned) => spawned,
Expand Down
140 changes: 29 additions & 111 deletions codex-rs/core/src/unified_exec/process_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1004,92 +1004,6 @@ impl UnifiedExecProcessManager {
) -> Result<UnifiedExecProcess, UnifiedExecError> {
let inherited_fds = spawn_lifecycle.inherited_fds();

#[cfg(target_os = "windows")]
if request.sandbox == codex_sandboxing::SandboxType::WindowsRestrictedToken {
// TODO(anp): Keep PathUri through the Windows sandbox launch boundary.
let native_cwd =
request
.cwd
.to_abs_path()
.map_err(|_| UnifiedExecError::ForeignPath {
path: request.cwd.clone(),
})?;
let codex_home = crate::config::find_codex_home().map_err(|err| {
UnifiedExecError::create_process(format!(
"windows sandbox: failed to resolve codex_home: {err}"
))
})?;
let additional_deny_write_paths = request
.windows_sandbox_filesystem_overrides
.as_ref()
.map(|overrides| overrides.additional_deny_write_paths.clone())
.unwrap_or_default();
let additional_deny_read_paths = request
.windows_sandbox_filesystem_overrides
.as_ref()
.map(|overrides| overrides.additional_deny_read_paths.clone())
.unwrap_or_default();
let elevated_read_roots_override = request
.windows_sandbox_filesystem_overrides
.as_ref()
.and_then(|overrides| overrides.read_roots_override.clone());
let elevated_read_roots_include_platform_defaults = request
.windows_sandbox_filesystem_overrides
.as_ref()
.is_some_and(|overrides| overrides.read_roots_include_platform_defaults);
let elevated_write_roots_override = request
.windows_sandbox_filesystem_overrides
.as_ref()
.and_then(|overrides| overrides.write_roots_override.clone());
let spawned = match request.windows_sandbox_level {
codex_protocol::config_types::WindowsSandboxLevel::Elevated => {
codex_windows_sandbox::spawn_windows_sandbox_session_elevated_for_permission_profile(
&request.permission_profile,
request.windows_sandbox_workspace_roots.as_slice(),
codex_home.as_ref(),
request.command.clone(),
native_cwd.as_path(),
request.env.clone(),
request.network.is_some(),
None,
elevated_read_roots_override.as_deref(),
elevated_read_roots_include_platform_defaults,
elevated_write_roots_override.as_deref(),
&additional_deny_read_paths,
&additional_deny_write_paths,
tty,
tty,
request.windows_sandbox_private_desktop,
)
.await
}
codex_protocol::config_types::WindowsSandboxLevel::RestrictedToken
| codex_protocol::config_types::WindowsSandboxLevel::Disabled => {
codex_windows_sandbox::spawn_windows_sandbox_session_legacy(
&request.permission_profile,
request.windows_sandbox_workspace_roots.as_slice(),
codex_home.as_ref(),
request.command.clone(),
native_cwd.as_path(),
request.env.clone(),
None,
&additional_deny_read_paths,
&additional_deny_write_paths,
tty,
tty,
request.windows_sandbox_private_desktop,
)
.await
}
};
spawn_lifecycle.after_spawn();
return UnifiedExecProcess::from_spawned(
spawned.map_err(|err| UnifiedExecError::create_process(err.to_string()))?,
request.sandbox,
spawn_lifecycle,
)
.await;
}
if environment.is_remote() {
if !inherited_fds.is_empty() {
return Err(UnifiedExecError::create_process(
Expand All @@ -1114,35 +1028,39 @@ impl UnifiedExecProcessManager {
path: request.cwd.clone(),
})?;

let (program, args) = request
.command
.split_first()
.ok_or(UnifiedExecError::MissingCommandLine)?;
let spawn_result = if tty {
codex_utils_pty::pty::spawn_process_with_inherited_fds(
program,
args,
native_cwd.as_path(),
&request.env,
&request.arg0,
codex_utils_pty::TerminalSize::default(),
&inherited_fds,
)
.await
if request.command.is_empty() {
return Err(UnifiedExecError::MissingCommandLine);
}
let windows_sandbox = if request.sandbox
== codex_sandboxing::SandboxType::WindowsRestrictedToken
{
Some(codex_sandboxing::WindowsSandboxSpawnRequest {
permission_profile: &request.permission_profile,
workspace_roots: &request.windows_sandbox_workspace_roots,
windows_sandbox_level: request.windows_sandbox_level,
proxy_enforced: request.network.is_some(),
proxy_settings_mode: codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
filesystem_overrides: request.windows_sandbox_filesystem_overrides.as_ref(),
use_private_desktop: request.windows_sandbox_private_desktop,
})
} else {
codex_utils_pty::pipe::spawn_process_no_stdin_with_inherited_fds(
program,
args,
native_cwd.as_path(),
&request.env,
&request.arg0,
&inherited_fds,
)
.await
None
};
let spawn_result = codex_sandboxing::spawn_process(codex_sandboxing::SpawnRequest {
command: &request.command,
cwd: native_cwd.as_path(),
env: &request.env,
arg0: &request.arg0,
sandbox: request.sandbox,
windows_sandbox,
tty,
stdin_open: tty,
inherited_fds: &inherited_fds,
})
.await;
spawn_lifecycle.after_spawn();
let spawned =
spawn_result.map_err(|err| UnifiedExecError::create_process(err.to_string()))?;
spawn_lifecycle.after_spawn();
UnifiedExecProcess::from_spawned(spawned, request.sandbox, spawn_lifecycle).await
}

Expand Down
43 changes: 29 additions & 14 deletions codex-rs/core/tests/suite/workspace_roots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use codex_features::Feature;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_utils_path_uri::PathUri;
#[cfg(windows)]
use core_test_support::PathExt;
use core_test_support::TestTargetOs;
use core_test_support::responses::ResponseMock;
use core_test_support::responses::ev_apply_patch_custom_tool_call;
Expand All @@ -15,7 +17,7 @@ use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_sequence;
use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_target_windows;
use core_test_support::skip_if_wine_exec;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::test_codex;
use core_test_support::test_target_os;
Expand All @@ -36,12 +38,19 @@ fn workspace_roots_profile() -> PermissionProfile {

async fn workspace_roots_test(server: &MockServer) -> Result<TestCodex> {
let mut builder = test_codex().with_config(|config| {
#[cfg(windows)]
{
config.cwd = dunce::canonicalize(config.cwd.as_path())
.expect("test workspace should be canonicalizable")
.abs();
}
config.use_experimental_unified_exec_tool = true;
config
.features
.enable(Feature::UnifiedExec)
.expect("test config should allow feature update");
config.workspace_roots = vec![config.cwd.clone()];
config.set_windows_sandbox_enabled(/*value*/ true);
});
builder.build_with_auto_env(server).await
}
Expand All @@ -60,10 +69,7 @@ fn command_arguments(path: &str, contents: &str) -> Result<String> {
TestTargetOs::Linux | TestTargetOs::MacOs => {
("bash", format!("printf %s '{contents}' > '{path}'"))
}
TestTargetOs::Windows => (
"powershell",
format!("Set-Content -NoNewline -Path '{path}' -Value '{contents}'"),
),
TestTargetOs::Windows => ("cmd", format!("echo {contents}>{path}")),
};
Ok(serde_json::to_string(&json!({
"cmd": command,
Expand Down Expand Up @@ -134,9 +140,9 @@ async fn workspace_roots_allow_file_and_command_writes() -> Result<()> {
const PATCH_CONTENTS: &str = "workspace root patch access";
const COMMAND_CONTENTS: &str = "workspace root command access";

skip_if_target_windows!(
skip_if_wine_exec!(
Ok(()),
"sandboxed process launch is not supported by the exec-server Windows backend"
"Wine does not emulate Windows restricted-token and ACL sandbox semantics"
);

let server = start_mock_server().await;
Expand Down Expand Up @@ -173,7 +179,10 @@ async fn workspace_roots_allow_file_and_command_writes() -> Result<()> {
read_file(&test, &patch_path).await?,
format!("{PATCH_CONTENTS}\n")
);
assert_eq!(read_file(&test, &command_path).await?, COMMAND_CONTENTS);
assert_eq!(
read_file(&test, &command_path).await?.trim_end(),
COMMAND_CONTENTS
);

remove_files(&test, &[&patch_path, &command_path]).await
}
Expand All @@ -183,19 +192,24 @@ async fn workspace_roots_deny_file_and_command_writes_outside_roots() -> Result<
const PATCH_CONTENTS: &str = "outside workspace root patch";
const COMMAND_CONTENTS: &str = "outside workspace root command";

skip_if_target_windows!(
skip_if_wine_exec!(
Ok(()),
"sandboxed process launch is not supported by the exec-server Windows backend"
"Wine does not emulate Windows restricted-token and ACL sandbox semantics"
);

let server = start_mock_server().await;
let test = workspace_roots_test(&server).await?;
let patch_path = outside_workspace_path(&test, "outside-patch.txt")?;
let command_path = outside_workspace_path(&test, "outside-command.txt")?;
let patch_path_display = patch_path.inferred_native_path_string();
let patch_relative_path = format!(
"../{}",
patch_path
.basename()
.context("outside patch path should have a file name")?
);
let command_path_display = command_path.inferred_native_path_string();
let patch = format!(
"*** Begin Patch\n*** Add File: {patch_path_display}\n+{PATCH_CONTENTS}\n*** End Patch\n"
"*** Begin Patch\n*** Add File: {patch_relative_path}\n+{PATCH_CONTENTS}\n*** End Patch\n"
);

let response_mock =
Expand All @@ -222,8 +236,9 @@ async fn workspace_roots_deny_file_and_command_writes_outside_roots() -> Result<
.context("denied command result should be present")?;
let command_output = command_output.context("denied command output should be present")?;
assert!(
command_output.contains(&command_path_display),
"denied command output should identify {command_path_display}, got {command_output:?}"
command_output.contains("Access is denied")
|| command_output.contains(&command_path_display),
"outside command should be denied, got {command_output:?}"
);
assert!(
test.fs()
Expand Down
Loading
Loading