Skip to content

Commit 315ecdf

Browse files
aibrahim-oaicodex
andcommitted
Support piped stdin in exec process API
Add an explicit stdin mode to process/start so non-tty processes can either keep stdin closed or expose a writable pipe. Co-authored-by: Codex <noreply@openai.com>
1 parent e776798 commit 315ecdf

File tree

8 files changed

+47
-6
lines changed

8 files changed

+47
-6
lines changed

codex-rs/core/src/unified_exec/process_manager.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ fn exec_server_params_for_request(
152152
env_policy,
153153
env,
154154
tty,
155+
stdin: codex_exec_server::ExecStdinMode::Closed,
155156
arg0: request.arg0.clone(),
156157
}
157158
}

codex-rs/exec-server/src/environment.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ mod tests {
346346
env_policy: None,
347347
env: Default::default(),
348348
tty: false,
349+
stdin: crate::ExecStdinMode::Closed,
349350
arg0: None,
350351
})
351352
.await

codex-rs/exec-server/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub use protocol::ExecOutputDeltaNotification;
4848
pub use protocol::ExecOutputStream;
4949
pub use protocol::ExecParams;
5050
pub use protocol::ExecResponse;
51+
pub use protocol::ExecStdinMode;
5152
pub use protocol::FsCopyParams;
5253
pub use protocol::FsCopyResponse;
5354
pub use protocol::FsCreateDirectoryParams;

codex-rs/exec-server/src/local_process.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use crate::protocol::ExecOutputDeltaNotification;
2828
use crate::protocol::ExecOutputStream;
2929
use crate::protocol::ExecParams;
3030
use crate::protocol::ExecResponse;
31+
use crate::protocol::ExecStdinMode;
3132
use crate::protocol::ProcessOutputChunk;
3233
use crate::protocol::ReadParams;
3334
use crate::protocol::ReadResponse;
@@ -59,6 +60,7 @@ struct RetainedOutputChunk {
5960
struct RunningProcess {
6061
session: ExecCommandSession,
6162
tty: bool,
63+
stdin: ExecStdinMode,
6264
output: VecDeque<RetainedOutputChunk>,
6365
retained_bytes: usize,
6466
next_seq: u64,
@@ -165,6 +167,15 @@ impl LocalProcess {
165167
TerminalSize::default(),
166168
)
167169
.await
170+
} else if matches!(params.stdin, ExecStdinMode::Piped) {
171+
codex_utils_pty::spawn_pipe_process(
172+
program,
173+
args,
174+
params.cwd.as_path(),
175+
&env,
176+
&params.arg0,
177+
)
178+
.await
168179
} else {
169180
codex_utils_pty::spawn_pipe_process_no_stdin(
170181
program,
@@ -195,6 +206,7 @@ impl LocalProcess {
195206
ProcessEntry::Running(Box::new(RunningProcess {
196207
session: spawned.session,
197208
tty: params.tty,
209+
stdin: params.stdin,
198210
output: VecDeque::new(),
199211
retained_bytes: 0,
200212
next_seq: 1,
@@ -339,7 +351,7 @@ impl LocalProcess {
339351
status: WriteStatus::Starting,
340352
});
341353
};
342-
if !process.tty {
354+
if !process.tty && matches!(process.stdin, ExecStdinMode::Closed) {
343355
return Ok(WriteResponse {
344356
status: WriteStatus::StdinClosed,
345357
});
@@ -667,6 +679,7 @@ mod tests {
667679
env_policy: None,
668680
env,
669681
tty: false,
682+
stdin: ExecStdinMode::Closed,
670683
arg0: None,
671684
}
672685
}

codex-rs/exec-server/src/protocol.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,28 @@ pub struct ExecParams {
6969
pub env_policy: Option<ExecEnvPolicy>,
7070
pub env: HashMap<String, String>,
7171
pub tty: bool,
72+
73+
/// Controls whether the executor keeps a writable stdin pipe for this
74+
/// process.
75+
///
76+
/// Normal non-interactive exec commands use `Closed` so programs that read
77+
/// stdin see EOF immediately. Remote MCP stdio uses `Piped` because rmcp
78+
/// must write JSON-RPC request bytes to the child process stdin after the
79+
/// process has started.
80+
pub stdin: ExecStdinMode,
7281
pub arg0: Option<String>,
7382
}
7483

84+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
85+
#[serde(rename_all = "camelCase")]
86+
pub enum ExecStdinMode {
87+
/// Start the process with stdin connected to null/EOF.
88+
Closed,
89+
90+
/// Start the process with stdin open and writable through `process/write`.
91+
Piped,
92+
}
93+
7594
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7695
#[serde(rename_all = "camelCase")]
7796
pub struct ExecEnvPolicy {

codex-rs/exec-server/src/server/handler/tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use super::ExecServerHandler;
1010
use crate::ExecServerRuntimePaths;
1111
use crate::ProcessId;
1212
use crate::protocol::ExecParams;
13+
use crate::protocol::ExecStdinMode;
1314
use crate::protocol::InitializeParams;
1415
use crate::protocol::ReadParams;
1516
use crate::protocol::ReadResponse;
@@ -30,6 +31,7 @@ fn exec_params_with_argv(process_id: &str, argv: Vec<String>) -> ExecParams {
3031
env_policy: None,
3132
env: inherited_path_env(),
3233
tty: false,
34+
stdin: ExecStdinMode::Closed,
3335
arg0: None,
3436
}
3537
}

codex-rs/exec-server/src/server/processor.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ mod tests {
393393
env_policy: None,
394394
env,
395395
tty: false,
396+
stdin: crate::protocol::ExecStdinMode::Closed,
396397
arg0: None,
397398
}
398399
}

codex-rs/exec-server/tests/exec_process.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use codex_exec_server::Environment;
99
use codex_exec_server::ExecBackend;
1010
use codex_exec_server::ExecParams;
1111
use codex_exec_server::ExecProcess;
12+
use codex_exec_server::ExecStdinMode;
1213
use codex_exec_server::ProcessId;
1314
use codex_exec_server::ReadResponse;
1415
use codex_exec_server::StartedExecProcess;
@@ -54,6 +55,7 @@ async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> {
5455
env_policy: /*env_policy*/ None,
5556
env: Default::default(),
5657
tty: false,
58+
stdin: ExecStdinMode::Closed,
5759
arg0: None,
5860
})
5961
.await?;
@@ -131,6 +133,7 @@ async fn assert_exec_process_streams_output(use_remote: bool) -> Result<()> {
131133
env_policy: /*env_policy*/ None,
132134
env: Default::default(),
133135
tty: false,
136+
stdin: ExecStdinMode::Closed,
134137
arg0: None,
135138
})
136139
.await?;
@@ -160,7 +163,8 @@ async fn assert_exec_process_write_then_read(use_remote: bool) -> Result<()> {
160163
cwd: std::env::current_dir()?,
161164
env_policy: /*env_policy*/ None,
162165
env: Default::default(),
163-
tty: true,
166+
tty: false,
167+
stdin: ExecStdinMode::Piped,
164168
arg0: None,
165169
})
166170
.await?;
@@ -172,10 +176,7 @@ async fn assert_exec_process_write_then_read(use_remote: bool) -> Result<()> {
172176
let wake_rx = process.subscribe_wake();
173177
let (output, exit_code, closed) = collect_process_output_from_reads(process, wake_rx).await?;
174178

175-
assert!(
176-
output.contains("from-stdin:hello"),
177-
"unexpected output: {output:?}"
178-
);
179+
assert_eq!(output, "from-stdin:hello\n");
179180
assert_eq!(exit_code, Some(0));
180181
assert!(closed);
181182
Ok(())
@@ -198,6 +199,7 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe(
198199
env_policy: /*env_policy*/ None,
199200
env: Default::default(),
200201
tty: false,
202+
stdin: ExecStdinMode::Closed,
201203
arg0: None,
202204
})
203205
.await?;
@@ -231,6 +233,7 @@ async fn remote_exec_process_reports_transport_disconnect() -> Result<()> {
231233
env_policy: /*env_policy*/ None,
232234
env: Default::default(),
233235
tty: false,
236+
stdin: ExecStdinMode::Closed,
234237
arg0: None,
235238
})
236239
.await?;

0 commit comments

Comments
 (0)