Skip to content

Commit 7913ce9

Browse files
committed
core: add stdin-backed sandboxed fs writes
1 parent 1ff97ac commit 7913ce9

File tree

20 files changed

+260
-13
lines changed

20 files changed

+260
-13
lines changed

codex-rs/cli/src/debug_sandbox.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ async fn run_command_under_sandbox(
171171
&cwd_clone,
172172
env_map,
173173
None,
174+
None,
174175
config.permissions.windows_sandbox_private_desktop,
175176
)
176177
} else {
@@ -182,6 +183,7 @@ async fn run_command_under_sandbox(
182183
&cwd_clone,
183184
env_map,
184185
None,
186+
None,
185187
config.permissions.windows_sandbox_private_desktop,
186188
)
187189
}

codex-rs/core/src/codex_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4764,6 +4764,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
47644764
expiration: timeout_ms.into(),
47654765
env: HashMap::new(),
47664766
network: None,
4767+
stdin: crate::exec::ExecStdin::Closed,
47674768
sandbox_permissions,
47684769
windows_sandbox_level: turn_context.windows_sandbox_level,
47694770
windows_sandbox_private_desktop: turn_context
@@ -4781,6 +4782,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
47814782
expiration: timeout_ms.into(),
47824783
env: HashMap::new(),
47834784
network: None,
4785+
stdin: crate::exec::ExecStdin::Closed,
47844786
windows_sandbox_level: turn_context.windows_sandbox_level,
47854787
windows_sandbox_private_desktop: turn_context
47864788
.config

codex-rs/core/src/codex_tests_guardian.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid
126126
expiration: expiration_ms.into(),
127127
env: HashMap::new(),
128128
network: None,
129+
stdin: crate::exec::ExecStdin::Closed,
129130
sandbox_permissions: SandboxPermissions::WithAdditionalPermissions,
130131
windows_sandbox_level: turn_context.windows_sandbox_level,
131132
windows_sandbox_private_desktop: turn_context

codex-rs/core/src/exec.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::time::Instant;
1212
use async_channel::Sender;
1313
use tokio::io::AsyncRead;
1414
use tokio::io::AsyncReadExt;
15+
use tokio::io::AsyncWriteExt;
1516
use tokio::io::BufReader;
1617
use tokio::process::Child;
1718
use tokio_util::sync::CancellationToken;
@@ -80,13 +81,21 @@ pub struct ExecParams {
8081
pub expiration: ExecExpiration,
8182
pub env: HashMap<String, String>,
8283
pub network: Option<NetworkProxy>,
84+
pub stdin: ExecStdin,
8385
pub sandbox_permissions: SandboxPermissions,
8486
pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
8587
pub windows_sandbox_private_desktop: bool,
8688
pub justification: Option<String>,
8789
pub arg0: Option<String>,
8890
}
8991

92+
#[derive(Clone, Debug, Default)]
93+
pub enum ExecStdin {
94+
#[default]
95+
Closed,
96+
Bytes(Vec<u8>),
97+
}
98+
9099
fn select_process_exec_tool_sandbox_type(
91100
file_system_sandbox_policy: &FileSystemSandboxPolicy,
92101
network_sandbox_policy: NetworkSandboxPolicy,
@@ -231,6 +240,7 @@ pub fn build_exec_request(
231240
mut env,
232241
expiration,
233242
network,
243+
stdin: _stdin,
234244
sandbox_permissions,
235245
windows_sandbox_level,
236246
windows_sandbox_private_desktop,
@@ -291,6 +301,7 @@ pub(crate) async fn execute_exec_request(
291301
cwd,
292302
env,
293303
network,
304+
stdin,
294305
expiration,
295306
sandbox,
296307
windows_sandbox_level,
@@ -310,6 +321,7 @@ pub(crate) async fn execute_exec_request(
310321
expiration,
311322
env,
312323
network: network.clone(),
324+
stdin,
313325
sandbox_permissions,
314326
windows_sandbox_level,
315327
windows_sandbox_private_desktop,
@@ -343,6 +355,7 @@ pub(crate) async fn execute_exec_request_raw_output(
343355
cwd,
344356
env,
345357
network,
358+
stdin,
346359
expiration,
347360
sandbox,
348361
windows_sandbox_level,
@@ -362,6 +375,7 @@ pub(crate) async fn execute_exec_request_raw_output(
362375
expiration,
363376
env,
364377
network: network.clone(),
378+
stdin,
365379
sandbox_permissions,
366380
windows_sandbox_level,
367381
windows_sandbox_private_desktop,
@@ -465,6 +479,7 @@ async fn exec_windows_sandbox(
465479
cwd,
466480
mut env,
467481
network,
482+
stdin,
468483
expiration,
469484
windows_sandbox_level,
470485
windows_sandbox_private_desktop,
@@ -501,6 +516,10 @@ async fn exec_windows_sandbox(
501516
command,
502517
&cwd,
503518
env,
519+
match stdin {
520+
ExecStdin::Closed => None,
521+
ExecStdin::Bytes(bytes) => Some(bytes),
522+
},
504523
timeout_ms,
505524
windows_sandbox_private_desktop,
506525
)
@@ -512,6 +531,10 @@ async fn exec_windows_sandbox(
512531
command,
513532
&cwd,
514533
env,
534+
match stdin {
535+
ExecStdin::Closed => None,
536+
ExecStdin::Bytes(bytes) => Some(bytes),
537+
},
515538
timeout_ms,
516539
windows_sandbox_private_desktop,
517540
)
@@ -917,6 +940,7 @@ async fn exec(
917940
mut env,
918941
network,
919942
arg0,
943+
stdin,
920944
expiration,
921945
windows_sandbox_level: _,
922946
..
@@ -944,12 +968,13 @@ async fn exec(
944968
network: None,
945969
stdio_policy: StdioPolicy::RedirectForShellTool,
946970
env,
971+
stdin_open: matches!(stdin, ExecStdin::Bytes(_)),
947972
})
948973
.await?;
949974
if let Some(after_spawn) = after_spawn {
950975
after_spawn();
951976
}
952-
consume_truncated_output(child, expiration, stdout_stream).await
977+
consume_truncated_output(child, stdin, expiration, stdout_stream).await
953978
}
954979

955980
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
@@ -1007,9 +1032,18 @@ fn windows_restricted_token_sandbox_support(
10071032
/// use as the output of a `shell` tool call. Also enforces specified timeout.
10081033
async fn consume_truncated_output(
10091034
mut child: Child,
1035+
stdin: ExecStdin,
10101036
expiration: ExecExpiration,
10111037
stdout_stream: Option<StdoutStream>,
10121038
) -> Result<RawExecToolCallOutput> {
1039+
let stdin_task = match (child.stdin.take(), stdin) {
1040+
(Some(mut child_stdin), ExecStdin::Bytes(bytes)) => Some(tokio::spawn(async move {
1041+
child_stdin.write_all(&bytes).await?;
1042+
child_stdin.shutdown().await
1043+
})),
1044+
_ => None,
1045+
};
1046+
10131047
// Both stdout and stderr were configured with `Stdio::piped()`
10141048
// above, therefore `take()` should normally return `Some`. If it doesn't
10151049
// we treat it as an exceptional I/O error
@@ -1089,6 +1123,13 @@ async fn consume_truncated_output(
10891123
Duration::from_millis(IO_DRAIN_TIMEOUT_MS),
10901124
)
10911125
.await?;
1126+
if let Some(stdin_task) = stdin_task {
1127+
match stdin_task.await {
1128+
Ok(Ok(())) => {}
1129+
Ok(Err(err)) => return Err(CodexErr::Io(err)),
1130+
Err(join_err) => return Err(CodexErr::Io(io::Error::other(join_err))),
1131+
}
1132+
}
10921133
let aggregated_output = aggregate_output(&stdout, &stderr);
10931134

10941135
Ok(RawExecToolCallOutput {

codex-rs/core/src/exec_tests.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,56 @@ async fn read_capped_limits_retained_bytes() {
102102
assert_eq!(out.text.len(), EXEC_OUTPUT_MAX_BYTES);
103103
}
104104

105+
#[tokio::test]
106+
async fn exec_passes_stdin_bytes_to_child() -> Result<()> {
107+
let command = if cfg!(windows) {
108+
vec![
109+
"cmd.exe".to_string(),
110+
"/Q".to_string(),
111+
"/D".to_string(),
112+
"/C".to_string(),
113+
"more".to_string(),
114+
]
115+
} else {
116+
vec!["/bin/cat".to_string()]
117+
};
118+
let params = ExecParams {
119+
command,
120+
cwd: std::env::current_dir()?,
121+
expiration: 1_000.into(),
122+
env: std::env::vars().collect(),
123+
network: None,
124+
stdin: ExecStdin::Bytes(b"hello from stdin\n".to_vec()),
125+
sandbox_permissions: SandboxPermissions::UseDefault,
126+
windows_sandbox_level: WindowsSandboxLevel::Disabled,
127+
windows_sandbox_private_desktop: false,
128+
justification: None,
129+
arg0: None,
130+
};
131+
132+
let output = exec(
133+
params,
134+
SandboxType::None,
135+
&SandboxPolicy::DangerFullAccess,
136+
&FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
137+
NetworkSandboxPolicy::Enabled,
138+
None,
139+
None,
140+
)
141+
.await?;
142+
143+
let expected_stdout = if cfg!(windows) {
144+
"hello from stdin\r\n"
145+
} else {
146+
"hello from stdin\n"
147+
};
148+
assert_eq!(output.exit_status.code(), Some(0));
149+
assert_eq!(output.stdout.from_utf8_lossy().text, expected_stdout);
150+
assert_eq!(output.stderr.from_utf8_lossy().text, "");
151+
152+
Ok(())
153+
}
154+
105155
#[test]
106156
fn aggregate_output_prefers_stderr_on_contention() {
107157
let stdout = StreamOutput {
@@ -398,6 +448,7 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()>
398448
expiration: 500.into(),
399449
env,
400450
network: None,
451+
stdin: ExecStdin::Closed,
401452
sandbox_permissions: SandboxPermissions::UseDefault,
402453
windows_sandbox_level: WindowsSandboxLevel::Disabled,
403454
windows_sandbox_private_desktop: false,
@@ -455,6 +506,7 @@ async fn process_exec_tool_call_respects_cancellation_token() -> Result<()> {
455506
expiration: ExecExpiration::Cancellation(cancel_token),
456507
env,
457508
network: None,
509+
stdin: ExecStdin::Closed,
458510
sandbox_permissions: SandboxPermissions::UseDefault,
459511
windows_sandbox_level: WindowsSandboxLevel::Disabled,
460512
windows_sandbox_private_desktop: false,

codex-rs/core/src/landlock.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ where
5656
network,
5757
stdio_policy,
5858
env,
59+
stdin_open: false,
5960
})
6061
.await
6162
}

codex-rs/core/src/sandboxed_fs.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::codex::TurnContext;
33
use crate::error::CodexErr;
44
use crate::error::SandboxErr;
55
use crate::exec::ExecExpiration;
6+
use crate::exec::ExecStdin;
67
use crate::exec::ExecToolCallRawOutput;
78
use crate::sandboxing::CommandSpec;
89
use crate::sandboxing::SandboxPermissions;
@@ -27,14 +28,34 @@ pub(crate) async fn read_file(
2728
turn: &Arc<TurnContext>,
2829
path: &Path,
2930
) -> Result<Vec<u8>, SandboxedFsError> {
30-
let output = run_request(session, turn, path).await?;
31+
let output = run_request(session, turn, path, "read", ExecStdin::Closed).await?;
3132
Ok(output.stdout.text)
3233
}
3334

35+
#[allow(dead_code)]
36+
pub(crate) async fn write_file(
37+
session: &Arc<Session>,
38+
turn: &Arc<TurnContext>,
39+
path: &Path,
40+
contents: &[u8],
41+
) -> Result<(), SandboxedFsError> {
42+
run_request(
43+
session,
44+
turn,
45+
path,
46+
"write",
47+
ExecStdin::Bytes(contents.to_vec()),
48+
)
49+
.await?;
50+
Ok(())
51+
}
52+
3453
async fn run_request(
3554
session: &Arc<Session>,
3655
turn: &Arc<TurnContext>,
3756
path: &Path,
57+
operation: &str,
58+
stdin: ExecStdin,
3859
) -> Result<ExecToolCallRawOutput, SandboxedFsError> {
3960
let exe = std::env::current_exe().map_err(|error| SandboxedFsError::ResolveExe {
4061
message: error.to_string(),
@@ -60,13 +81,13 @@ async fn run_request(
6081
windows_sandbox_level: turn.windows_sandbox_level,
6182
windows_sandbox_private_desktop: turn.config.permissions.windows_sandbox_private_desktop,
6283
};
63-
let exec_request = attempt
84+
let mut exec_request = attempt
6485
.env_for(
6586
CommandSpec {
6687
program: exe.to_string_lossy().to_string(),
6788
args: vec![
6889
CODEX_CORE_FS_OPS_ARG1.to_string(),
69-
"read".to_string(),
90+
operation.to_string(),
7091
path.to_string_lossy().to_string(),
7192
],
7293
cwd: turn.cwd.clone(),
@@ -83,6 +104,7 @@ async fn run_request(
83104
exit_code: -1,
84105
message: error.to_string(),
85106
})?;
107+
exec_request.stdin = stdin;
86108

87109
let output = execute_env_raw_output(exec_request, /*stdout_stream*/ None)
88110
.await

codex-rs/core/src/sandboxing/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ ready‑to‑spawn environment.
99
pub(crate) mod macos_permissions;
1010

1111
use crate::exec::ExecExpiration;
12+
use crate::exec::ExecStdin;
1213
use crate::exec::ExecToolCallOutput;
1314
use crate::exec::ExecToolCallRawOutput;
1415
use crate::exec::SandboxType;
@@ -68,6 +69,7 @@ pub struct ExecRequest {
6869
pub cwd: PathBuf,
6970
pub env: HashMap<String, String>,
7071
pub network: Option<NetworkProxy>,
72+
pub stdin: ExecStdin,
7173
pub expiration: ExecExpiration,
7274
pub sandbox: SandboxType,
7375
pub windows_sandbox_level: WindowsSandboxLevel,
@@ -708,6 +710,7 @@ impl SandboxManager {
708710
cwd: spec.cwd,
709711
env,
710712
network: network.cloned(),
713+
stdin: ExecStdin::Closed,
711714
expiration: spec.expiration,
712715
sandbox,
713716
windows_sandbox_level,

codex-rs/core/src/seatbelt.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ pub async fn spawn_command_under_seatbelt(
6363
network,
6464
stdio_policy,
6565
env,
66+
stdin_open: false,
6667
})
6768
.await
6869
}

0 commit comments

Comments
 (0)