From 3aaee0f0612d4f7c03fbce02f13094d517a58605 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 20 Mar 2026 15:32:26 -0700 Subject: [PATCH] core: add stdin-backed sandboxed fs writes --- codex-rs/cli/src/debug_sandbox.rs | 2 + codex-rs/core/src/codex_tests.rs | 2 + codex-rs/core/src/codex_tests_guardian.rs | 1 + codex-rs/core/src/exec.rs | 41 ++++++++++++++- codex-rs/core/src/exec_tests.rs | 52 +++++++++++++++++++ codex-rs/core/src/landlock.rs | 1 + codex-rs/core/src/sandboxed_fs.rs | 2 + codex-rs/core/src/sandboxing/mod.rs | 3 ++ codex-rs/core/src/seatbelt.rs | 1 + codex-rs/core/src/spawn.rs | 16 ++++-- codex-rs/core/src/tasks/user_shell.rs | 2 + codex-rs/core/src/tools/handlers/shell.rs | 3 ++ .../tools/runtimes/shell/unix_escalation.rs | 2 + codex-rs/core/tests/suite/exec.rs | 1 + codex-rs/fs-ops/src/command.rs | 6 ++- codex-rs/fs-ops/src/command_tests.rs | 14 +++++ codex-rs/fs-ops/src/runner.rs | 8 ++- codex-rs/fs-ops/src/runner_tests.rs | 46 ++++++++++++++++ .../windows-sandbox-rs/src/elevated_impl.rs | 17 +++++- codex-rs/windows-sandbox-rs/src/lib.rs | 27 +++++++++- 20 files changed, 236 insertions(+), 11 deletions(-) diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index c65b6dcad59a..425939f4cf01 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -170,6 +170,7 @@ async fn run_command_under_sandbox( command_vec, &cwd_clone, env_map, + /*stdin_bytes*/ None, /*timeout_ms*/ None, config.permissions.windows_sandbox_private_desktop, ) @@ -181,6 +182,7 @@ async fn run_command_under_sandbox( command_vec, &cwd_clone, env_map, + /*stdin_bytes*/ None, /*timeout_ms*/ None, config.permissions.windows_sandbox_private_desktop, ) diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index a5412eff29f2..76116ebecbbd 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -4798,6 +4798,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { capture_policy: ExecCapturePolicy::ShellTool, env: HashMap::new(), network: None, + stdin: crate::exec::ExecStdin::Closed, sandbox_permissions, windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context @@ -4816,6 +4817,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { capture_policy: ExecCapturePolicy::ShellTool, env: HashMap::new(), network: None, + stdin: crate::exec::ExecStdin::Closed, windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context .config diff --git a/codex-rs/core/src/codex_tests_guardian.rs b/codex-rs/core/src/codex_tests_guardian.rs index af0fccc9ac2d..bc3f97503cad 100644 --- a/codex-rs/core/src/codex_tests_guardian.rs +++ b/codex-rs/core/src/codex_tests_guardian.rs @@ -128,6 +128,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid capture_policy: ExecCapturePolicy::ShellTool, env: HashMap::new(), network: None, + stdin: crate::exec::ExecStdin::Closed, sandbox_permissions: SandboxPermissions::WithAdditionalPermissions, windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 21aa05f60fe1..7cfc5be6194b 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -12,6 +12,7 @@ use std::time::Instant; use async_channel::Sender; use tokio::io::AsyncRead; use tokio::io::AsyncReadExt; +use tokio::io::AsyncWriteExt; use tokio::io::BufReader; use tokio::process::Child; use tokio_util::sync::CancellationToken; @@ -81,6 +82,7 @@ pub struct ExecParams { pub capture_policy: ExecCapturePolicy, pub env: HashMap, pub network: Option, + pub stdin: ExecStdin, pub sandbox_permissions: SandboxPermissions, pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel, pub windows_sandbox_private_desktop: bool, @@ -97,7 +99,12 @@ pub enum ExecCapturePolicy { /// without the shell-oriented output cap or exec-expiration behavior. FullBuffer, } - +#[derive(Clone, Debug, Default)] +pub enum ExecStdin { + #[default] + Closed, + Bytes(Vec), +} fn select_process_exec_tool_sandbox_type( file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, @@ -263,6 +270,7 @@ pub fn build_exec_request( expiration, capture_policy, network, + stdin: _stdin, sandbox_permissions, windows_sandbox_level, windows_sandbox_private_desktop, @@ -380,6 +388,7 @@ fn prepare_exec_request(exec_request: ExecRequest) -> PreparedExecRequest { cwd, env, network, + stdin, expiration, capture_policy, sandbox, @@ -402,6 +411,7 @@ fn prepare_exec_request(exec_request: ExecRequest) -> PreparedExecRequest { capture_policy, env, network, + stdin, sandbox_permissions, windows_sandbox_level, windows_sandbox_private_desktop, @@ -495,6 +505,7 @@ async fn exec_windows_sandbox( cwd, mut env, network, + stdin, expiration, capture_policy, windows_sandbox_level, @@ -536,6 +547,10 @@ async fn exec_windows_sandbox( command, &cwd, env, + match stdin { + ExecStdin::Closed => None, + ExecStdin::Bytes(bytes) => Some(bytes), + }, timeout_ms, windows_sandbox_private_desktop, ) @@ -547,6 +562,10 @@ async fn exec_windows_sandbox( command, &cwd, env, + match stdin { + ExecStdin::Closed => None, + ExecStdin::Bytes(bytes) => Some(bytes), + }, timeout_ms, windows_sandbox_private_desktop, ) @@ -913,6 +932,7 @@ async fn exec( mut env, network, arg0, + stdin, expiration, capture_policy, windows_sandbox_level: _, @@ -941,12 +961,13 @@ async fn exec( network: None, stdio_policy: StdioPolicy::RedirectForShellTool, env, + stdin_open: matches!(stdin, ExecStdin::Bytes(_)), }) .await?; if let Some(after_spawn) = after_spawn { after_spawn(); } - consume_output(child, expiration, capture_policy, stdout_stream).await + consume_output(child, stdin, expiration, capture_policy, stdout_stream).await } #[cfg_attr(not(target_os = "windows"), allow(dead_code))] @@ -1004,10 +1025,19 @@ fn windows_restricted_token_sandbox_support( /// policy. async fn consume_output( mut child: Child, + stdin: ExecStdin, expiration: ExecExpiration, capture_policy: ExecCapturePolicy, stdout_stream: Option, ) -> Result { + let stdin_task = match (child.stdin.take(), stdin) { + (Some(mut child_stdin), ExecStdin::Bytes(bytes)) => Some(tokio::spawn(async move { + child_stdin.write_all(&bytes).await?; + child_stdin.shutdown().await + })), + _ => None, + }; + // Both stdout and stderr were configured with `Stdio::piped()` // above, therefore `take()` should normally return `Some`. If it doesn't // we treat it as an exceptional I/O error @@ -1090,6 +1120,13 @@ async fn consume_output( let stdout = await_output(&mut stdout_handle, capture_policy.io_drain_timeout()).await?; let stderr = await_output(&mut stderr_handle, capture_policy.io_drain_timeout()).await?; + if let Some(stdin_task) = stdin_task { + match stdin_task.await { + Ok(Ok(())) => {} + Ok(Err(err)) => return Err(CodexErr::Io(err)), + Err(join_err) => return Err(CodexErr::Io(io::Error::other(join_err))), + } + } let aggregated_output = aggregate_output(&stdout, &stderr, retained_bytes_cap); Ok(RawExecToolCallOutput { diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index fc312ec88e33..794e5306d8ce 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -105,6 +105,56 @@ async fn read_output_limits_retained_bytes_for_shell_capture() { assert_eq!(out.text.len(), EXEC_OUTPUT_MAX_BYTES); } +#[tokio::test] +async fn exec_passes_stdin_bytes_to_child() -> Result<()> { + let command = if cfg!(windows) { + vec![ + "cmd.exe".to_string(), + "/Q".to_string(), + "/D".to_string(), + "/C".to_string(), + "more".to_string(), + ] + } else { + vec!["/bin/cat".to_string()] + }; + let params = ExecParams { + command, + cwd: std::env::current_dir()?, + expiration: 1_000.into(), + env: std::env::vars().collect(), + network: None, + stdin: ExecStdin::Bytes(b"hello from stdin\n".to_vec()), + sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + windows_sandbox_private_desktop: false, + justification: None, + arg0: None, + }; + + let output = exec( + params, + SandboxType::None, + &SandboxPolicy::DangerFullAccess, + &FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess), + NetworkSandboxPolicy::Enabled, + None, + None, + ) + .await?; + + let expected_stdout = if cfg!(windows) { + "hello from stdin\r\n" + } else { + "hello from stdin\n" + }; + assert_eq!(output.exit_status.code(), Some(0)); + assert_eq!(output.stdout.from_utf8_lossy().text, expected_stdout); + assert_eq!(output.stderr.from_utf8_lossy().text, ""); + + Ok(()) +} + #[test] fn aggregate_output_prefers_stderr_on_contention() { let stdout = StreamOutput { @@ -588,6 +638,7 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()> capture_policy: ExecCapturePolicy::ShellTool, env, network: None, + stdin: ExecStdin::Closed, sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -646,6 +697,7 @@ async fn process_exec_tool_call_respects_cancellation_token() -> Result<()> { capture_policy: ExecCapturePolicy::ShellTool, env, network: None, + stdin: ExecStdin::Closed, sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index 19b3f7c6afa5..bd0e8a398474 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -56,6 +56,7 @@ where network, stdio_policy, env, + stdin_open: false, }) .await } diff --git a/codex-rs/core/src/sandboxed_fs.rs b/codex-rs/core/src/sandboxed_fs.rs index 5af97de6c7da..bdf793788d19 100644 --- a/codex-rs/core/src/sandboxed_fs.rs +++ b/codex-rs/core/src/sandboxed_fs.rs @@ -2,6 +2,7 @@ use crate::codex::Session; use crate::codex::TurnContext; use crate::exec::ExecCapturePolicy; use crate::exec::ExecExpiration; +use crate::exec::ExecStdin; use crate::exec::ExecToolCallRawOutput; use crate::exec::execute_exec_request_raw_output; use crate::sandboxing::CommandSpec; @@ -104,6 +105,7 @@ async fn perform_operation( exit_code: -1, message: error.to_string(), })?; + exec_request.stdin = stdin; let effective_policy = exec_request.sandbox_policy.clone(); let output = execute_exec_request_raw_output( diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 277ff2b2414b..04c59f1c7bf2 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -10,6 +10,7 @@ pub(crate) mod macos_permissions; use crate::exec::ExecCapturePolicy; use crate::exec::ExecExpiration; +use crate::exec::ExecStdin; use crate::exec::ExecToolCallOutput; use crate::exec::SandboxType; use crate::exec::StdoutStream; @@ -68,6 +69,7 @@ pub struct ExecRequest { pub cwd: PathBuf, pub env: HashMap, pub network: Option, + pub stdin: ExecStdin, pub expiration: ExecExpiration, pub capture_policy: ExecCapturePolicy, pub sandbox: SandboxType, @@ -709,6 +711,7 @@ impl SandboxManager { cwd: spec.cwd, env, network: network.cloned(), + stdin: ExecStdin::Closed, expiration: spec.expiration, capture_policy: spec.capture_policy, sandbox, diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index 2d1e0a7b5ddd..62ccf823008e 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -63,6 +63,7 @@ pub async fn spawn_command_under_seatbelt( network, stdio_policy, env, + stdin_open: false, }) .await } diff --git a/codex-rs/core/src/spawn.rs b/codex-rs/core/src/spawn.rs index 575a2f777cd7..83da439d8bb2 100644 --- a/codex-rs/core/src/spawn.rs +++ b/codex-rs/core/src/spawn.rs @@ -45,6 +45,7 @@ pub(crate) struct SpawnChildRequest<'a> { pub network: Option<&'a NetworkProxy>, pub stdio_policy: StdioPolicy, pub env: HashMap, + pub stdin_open: bool, } pub(crate) async fn spawn_child_async(request: SpawnChildRequest<'_>) -> std::io::Result { @@ -57,6 +58,7 @@ pub(crate) async fn spawn_child_async(request: SpawnChildRequest<'_>) -> std::io network, stdio_policy, mut env, + stdin_open, } = request; trace!( @@ -105,11 +107,15 @@ pub(crate) async fn spawn_child_async(request: SpawnChildRequest<'_>) -> std::io match stdio_policy { StdioPolicy::RedirectForShellTool => { - // Do not create a file descriptor for stdin because otherwise some - // commands may hang forever waiting for input. For example, ripgrep has - // a heuristic where it may try to read from stdin as explained here: - // https://github.com/BurntSushi/ripgrep/blob/e2362d4d5185d02fa857bf381e7bd52e66fafc73/crates/core/flags/hiargs.rs#L1101-L1103 - cmd.stdin(Stdio::null()); + if stdin_open { + cmd.stdin(Stdio::piped()); + } else { + // Do not create a file descriptor for stdin because otherwise some + // commands may hang forever waiting for input. For example, ripgrep has + // a heuristic where it may try to read from stdin as explained here: + // https://github.com/BurntSushi/ripgrep/blob/e2362d4d5185d02fa857bf381e7bd52e66fafc73/crates/core/flags/hiargs.rs#L1101-L1103 + cmd.stdin(Stdio::null()); + } cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); } diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 6b42be3cef29..62c6384569d3 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -11,6 +11,7 @@ use uuid::Uuid; use crate::codex::TurnContext; use crate::exec::ExecCapturePolicy; +use crate::exec::ExecStdin; use crate::exec::ExecToolCallOutput; use crate::exec::SandboxType; use crate::exec::StdoutStream; @@ -163,6 +164,7 @@ pub(crate) async fn execute_user_shell_command( Some(session.conversation_id), ), network: turn_context.network.clone(), + stdin: ExecStdin::Closed, // TODO(zhao-oai): Now that we have ExecExpiration::Cancellation, we // should use that instead of an "arbitrarily large" timeout here. expiration: USER_SHELL_TIMEOUT_MS.into(), diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index 446ca100b768..a89f69138f47 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use crate::codex::TurnContext; use crate::exec::ExecCapturePolicy; use crate::exec::ExecParams; +use crate::exec::ExecStdin; use crate::exec_env::create_env; use crate::exec_policy::ExecApprovalRequest; use crate::function_tool::FunctionCallError; @@ -74,6 +75,7 @@ impl ShellHandler { capture_policy: ExecCapturePolicy::ShellTool, env: create_env(&turn_context.shell_environment_policy, Some(thread_id)), network: turn_context.network.clone(), + stdin: ExecStdin::Closed, sandbox_permissions: params.sandbox_permissions.unwrap_or_default(), windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context @@ -129,6 +131,7 @@ impl ShellCommandHandler { capture_policy: ExecCapturePolicy::ShellTool, env: create_env(&turn_context.shell_environment_policy, Some(thread_id)), network: turn_context.network.clone(), + stdin: ExecStdin::Closed, sandbox_permissions: params.sandbox_permissions.unwrap_or_default(), windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 948018dae6a8..71f107f9861e 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -124,6 +124,7 @@ pub(super) async fn try_run_zsh_fork( cwd: sandbox_cwd, env: sandbox_env, network: sandbox_network, + stdin: _stdin, expiration: _sandbox_expiration, capture_policy: _capture_policy, sandbox, @@ -904,6 +905,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { cwd: self.cwd.clone(), env: exec_env, network: self.network.clone(), + stdin: crate::exec::ExecStdin::Closed, expiration: ExecExpiration::Cancellation(cancel_rx), capture_policy: ExecCapturePolicy::ShellTool, sandbox: self.sandbox, diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index 069e824ee573..2edd1f19dfee 100644 --- a/codex-rs/core/tests/suite/exec.rs +++ b/codex-rs/core/tests/suite/exec.rs @@ -41,6 +41,7 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result Ok(FsCommand::ReadFile { path }), - _ => Err(format!("unsupported filesystem operation `{operation}`")), + "write" => Ok(FsCommand::WriteFile { path }), + _ => Err(format!( + "unsupported filesystem operation `{operation}`; expected `read` or `write`" + )), } } diff --git a/codex-rs/fs-ops/src/command_tests.rs b/codex-rs/fs-ops/src/command_tests.rs index 4743ed6c19ee..b5569cfedaa2 100644 --- a/codex-rs/fs-ops/src/command_tests.rs +++ b/codex-rs/fs-ops/src/command_tests.rs @@ -19,3 +19,17 @@ fn parse_read_command() { } ); } + +#[test] +fn parse_write_command() { + let command = + parse_command_from_args(["write", "/tmp/example.png"].into_iter().map(Into::into)) + .expect("command should parse"); + + assert_eq!( + command, + FsCommand::WriteFile { + path: "/tmp/example.png".into(), + } + ); +} diff --git a/codex-rs/fs-ops/src/runner.rs b/codex-rs/fs-ops/src/runner.rs index afaac7cbab04..8ca62ec12701 100644 --- a/codex-rs/fs-ops/src/runner.rs +++ b/codex-rs/fs-ops/src/runner.rs @@ -36,7 +36,7 @@ fn run_from_args( fn try_run_from_args( args: impl Iterator, - _stdin: &mut impl Read, + stdin: &mut impl Read, stdout: &mut impl Write, ) -> std::io::Result<()> { let command = parse_command_from_args(args) @@ -58,6 +58,12 @@ fn try_run_from_args( std::io::copy(&mut file, stdout).map(|_| ()) } + FsCommand::WriteFile { path } => { + let mut file = std::fs::File::create(path).map_err(FsError::from)?; + std::io::copy(stdin, &mut file) + .map(|_| ()) + .map_err(FsError::from) + } } } diff --git a/codex-rs/fs-ops/src/runner_tests.rs b/codex-rs/fs-ops/src/runner_tests.rs index b74da6c5a6e3..4220dd68931e 100644 --- a/codex-rs/fs-ops/src/runner_tests.rs +++ b/codex-rs/fs-ops/src/runner_tests.rs @@ -1,6 +1,7 @@ use super::run_from_args; use crate::READ_FILE_OPERATION_ARG2; use pretty_assertions::assert_eq; +use std::io::Cursor; use tempfile::tempdir; #[test] @@ -112,3 +113,48 @@ fn run_from_args_serializes_errors_to_stderr() { assert!(result.is_err(), "missing file should fail"); assert_eq!(stdout, Vec::::new()); } + +#[test] +fn run_from_args_streams_stdin_bytes_to_file() { + let tempdir = tempdir().expect("tempdir"); + let path = tempdir.path().join("image.bin"); + let expected = b"hello\x00world".to_vec(); + + let mut stdin = Cursor::new(expected.clone()); + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + run_from_args( + ["write", path.to_str().expect("utf-8 test path")] + .into_iter() + .map(Into::into), + &mut stdin, + &mut stdout, + &mut stderr, + ) + .expect("write should succeed"); + + assert_eq!(std::fs::read(&path).expect("read test file"), expected); + assert_eq!(stdout, Vec::::new()); + assert_eq!(stderr, Vec::::new()); +} + +#[test] +fn write_reports_directory_error() { + let tempdir = tempdir().expect("tempdir"); + let mut stdin = Cursor::new(b"hello world".to_vec()); + let mut stdout = Vec::new(); + + let error = execute( + FsCommand::WriteFile { + path: tempdir.path().to_path_buf(), + }, + &mut stdin, + &mut stdout, + ) + .expect_err("writing to a directory should fail"); + + #[cfg(target_os = "windows")] + assert_eq!(error.kind, FsErrorKind::PermissionDenied); + #[cfg(not(target_os = "windows"))] + assert_eq!(error.kind, FsErrorKind::IsADirectory); +} diff --git a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs index f6c4563e1719..fa6eef91b00d 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs @@ -209,6 +209,7 @@ mod windows_impl { command: Vec, cwd: &Path, mut env_map: HashMap, + stdin_bytes: Option>, timeout_ms: Option, use_private_desktop: bool, ) -> Result { @@ -383,13 +384,26 @@ mod windows_impl { cap_sids, timeout_ms, tty: false, - stdin_open: false, + stdin_open: stdin_bytes.is_some(), use_private_desktop, }), }, }; write_frame(&mut pipe_write, &spawn_request)?; read_spawn_ready(&mut pipe_read)?; + if let Some(stdin_bytes) = stdin_bytes { + write_frame( + &mut pipe_write, + &FramedMessage { + version: 1, + message: Message::Stdin { + payload: crate::ipc_framed::StdinPayload { + data_b64: crate::ipc_framed::encode_bytes(&stdin_bytes), + }, + }, + }, + )?; + } drop(pipe_write); let mut stdout = Vec::new(); @@ -503,6 +517,7 @@ mod stub { _command: Vec, _cwd: &Path, _env_map: HashMap, + _stdin_bytes: Option>, _timeout_ms: Option, _use_private_desktop: bool, ) -> Result { diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index cb4be5275006..6e6eb47d0aff 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -265,6 +265,7 @@ mod windows_impl { command: Vec, cwd: &Path, mut env_map: HashMap, + stdin_bytes: Option>, timeout_ms: Option, use_private_desktop: bool, ) -> Result { @@ -406,7 +407,30 @@ mod windows_impl { unsafe { CloseHandle(in_r); - // Close the parent's stdin write end so the child sees EOF immediately. + if let Some(stdin_bytes) = stdin_bytes { + let mut offset = 0; + while offset < stdin_bytes.len() { + let remaining = stdin_bytes.len() - offset; + let chunk_len = remaining.min(u32::MAX as usize); + let mut written: u32 = 0; + let ok = windows_sys::Win32::Storage::FileSystem::WriteFile( + in_w, + stdin_bytes[offset..offset + chunk_len].as_ptr(), + chunk_len as u32, + &mut written, + std::ptr::null_mut(), + ); + if ok == 0 { + CloseHandle(in_w); + CloseHandle(out_w); + CloseHandle(err_w); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + return Err(std::io::Error::from_raw_os_error(GetLastError() as i32).into()); + } + offset += written as usize; + } + } CloseHandle(in_w); CloseHandle(out_w); CloseHandle(err_w); @@ -615,6 +639,7 @@ mod stub { _command: Vec, _cwd: &Path, _env_map: HashMap, + _stdin_bytes: Option>, _timeout_ms: Option, _use_private_desktop: bool, ) -> Result {