Skip to content

Commit cdd7b30

Browse files
committed
feat: experimental codex stdio-to-uds subcommand
1 parent 4f46360 commit cdd7b30

File tree

8 files changed

+202
-5
lines changed

8 files changed

+202
-5
lines changed

codex-rs/Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ members = [
2929
"protocol-ts",
3030
"rmcp-client",
3131
"responses-api-proxy",
32+
"stdio-to-uds",
3233
"otel",
3334
"tui",
3435
"git-apply",
@@ -54,10 +55,10 @@ codex-app-server = { path = "app-server" }
5455
codex-app-server-protocol = { path = "app-server-protocol" }
5556
codex-apply-patch = { path = "apply-patch" }
5657
codex-arg0 = { path = "arg0" }
58+
codex-async-utils = { path = "async-utils" }
5759
codex-chatgpt = { path = "chatgpt" }
5860
codex-common = { path = "common" }
5961
codex-core = { path = "core" }
60-
codex-async-utils = { path = "async-utils" }
6162
codex-exec = { path = "exec" }
6263
codex-feedback = { path = "feedback" }
6364
codex-file-search = { path = "file-search" }
@@ -73,6 +74,7 @@ codex-protocol = { path = "protocol" }
7374
codex-protocol-ts = { path = "protocol-ts" }
7475
codex-responses-api-proxy = { path = "responses-api-proxy" }
7576
codex-rmcp-client = { path = "rmcp-client" }
77+
codex-stdio-to-uds = { path = "stdio-to-uds" }
7678
codex-tui = { path = "tui" }
7779
codex-utils-json-to-toml = { path = "utils/json-to-toml" }
7880
codex-utils-readiness = { path = "utils/readiness" }
@@ -186,6 +188,7 @@ tree-sitter = "0.25.10"
186188
tree-sitter-bash = "0.25"
187189
tree-sitter-highlight = "0.25.10"
188190
ts-rs = "11"
191+
uds_windows = "1.1.0"
189192
unicode-segmentation = "1.12.0"
190193
unicode-width = "0.2"
191194
url = "2"

codex-rs/cli/Cargo.toml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,22 @@ anyhow = { workspace = true }
1919
clap = { workspace = true, features = ["derive"] }
2020
clap_complete = { workspace = true }
2121
codex-app-server = { workspace = true }
22+
codex-app-server-protocol = { workspace = true }
2223
codex-arg0 = { workspace = true }
2324
codex-chatgpt = { workspace = true }
25+
codex-cloud-tasks = { path = "../cloud-tasks" }
2426
codex-common = { workspace = true, features = ["cli"] }
2527
codex-core = { workspace = true }
2628
codex-exec = { workspace = true }
2729
codex-login = { workspace = true }
2830
codex-mcp-server = { workspace = true }
2931
codex-process-hardening = { workspace = true }
3032
codex-protocol = { workspace = true }
31-
codex-app-server-protocol = { workspace = true }
3233
codex-protocol-ts = { workspace = true }
3334
codex-responses-api-proxy = { workspace = true }
34-
codex-tui = { workspace = true }
3535
codex-rmcp-client = { workspace = true }
36-
codex-cloud-tasks = { path = "../cloud-tasks" }
36+
codex-stdio-to-uds = { workspace = true }
37+
codex-tui = { workspace = true }
3738
ctor = { workspace = true }
3839
owo-colors = { workspace = true }
3940
serde_json = { workspace = true }
@@ -47,8 +48,8 @@ tokio = { workspace = true, features = [
4748
] }
4849

4950
[dev-dependencies]
50-
assert_matches = { workspace = true }
5151
assert_cmd = { workspace = true }
52+
assert_matches = { workspace = true }
5253
predicates = { workspace = true }
5354
pretty_assertions = { workspace = true }
5455
tempfile = { workspace = true }

codex-rs/cli/src/main.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ enum Subcommand {
104104
#[clap(hide = true)]
105105
ResponsesApiProxy(ResponsesApiProxyArgs),
106106

107+
/// Internal: relay stdio to a Unix domain socket.
108+
#[clap(hide = true, name = "stdio-to-uds")]
109+
StdioToUds(StdioToUdsCommand),
110+
107111
/// Inspect feature flags.
108112
Features(FeaturesCli),
109113
}
@@ -205,6 +209,13 @@ struct GenerateTsCommand {
205209
prettier: Option<PathBuf>,
206210
}
207211

212+
#[derive(Debug, Parser)]
213+
struct StdioToUdsCommand {
214+
/// Path to the Unix domain socket to connect to.
215+
#[arg(value_name = "SOCKET_PATH")]
216+
socket_path: PathBuf,
217+
}
218+
208219
fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec<String> {
209220
let AppExitInfo {
210221
token_usage,
@@ -462,6 +473,11 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
462473
tokio::task::spawn_blocking(move || codex_responses_api_proxy::run_main(args))
463474
.await??;
464475
}
476+
Some(Subcommand::StdioToUds(cmd)) => {
477+
let socket_path = cmd.socket_path;
478+
tokio::task::spawn_blocking(move || codex_stdio_to_uds::run(socket_path.as_path()))
479+
.await??;
480+
}
465481
Some(Subcommand::GenerateTs(gen_cli)) => {
466482
codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?;
467483
}

codex-rs/stdio-to-uds/Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
edition = "2024"
3+
name = "codex-stdio-to-uds"
4+
version = { workspace = true }
5+
6+
[[bin]]
7+
name = "codex-stdio-to-uds"
8+
path = "src/main.rs"
9+
10+
[lib]
11+
name = "codex_stdio_to_uds"
12+
path = "src/lib.rs"
13+
14+
[lints]
15+
workspace = true
16+
17+
[dependencies]
18+
anyhow = { workspace = true }
19+
20+
[target.'cfg(target_os = "windows")'.dependencies]
21+
uds_windows = { workspace = true }
22+
23+
[dev-dependencies]
24+
assert_cmd = { workspace = true }
25+
pretty_assertions = { workspace = true }
26+
tempfile = { workspace = true }

codex-rs/stdio-to-uds/src/lib.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#![deny(clippy::print_stdout)]
2+
3+
use std::io;
4+
use std::io::Write;
5+
use std::net::Shutdown;
6+
use std::path::Path;
7+
use std::thread;
8+
9+
use anyhow::Context;
10+
use anyhow::anyhow;
11+
12+
#[cfg(unix)]
13+
use std::os::unix::net::UnixStream;
14+
15+
#[cfg(windows)]
16+
use uds_windows::UnixStream;
17+
18+
/// Connects to the Unix Domain Socket at `socket_path` and relays data between
19+
/// standard input/output and the socket.
20+
pub fn run(socket_path: &Path) -> anyhow::Result<()> {
21+
let mut stream = UnixStream::connect(socket_path)
22+
.with_context(|| format!("failed to connect to socket at {}", socket_path.display()))?;
23+
24+
let mut reader = stream
25+
.try_clone()
26+
.context("failed to clone socket for reading")?;
27+
28+
let stdout_thread = thread::spawn(move || -> io::Result<()> {
29+
let stdout = io::stdout();
30+
let mut handle = stdout.lock();
31+
io::copy(&mut reader, &mut handle)?;
32+
handle.flush()?;
33+
Ok(())
34+
});
35+
36+
let stdin = io::stdin();
37+
{
38+
let mut handle = stdin.lock();
39+
io::copy(&mut handle, &mut stream).context("failed to copy data from stdin to socket")?;
40+
}
41+
42+
stream
43+
.shutdown(Shutdown::Write)
44+
.context("failed to shutdown socket writer")?;
45+
46+
let stdout_result = stdout_thread
47+
.join()
48+
.map_err(|_| anyhow!("thread panicked while copying socket data to stdout"))?;
49+
stdout_result.context("failed to copy data from socket to stdout")?;
50+
51+
Ok(())
52+
}

codex-rs/stdio-to-uds/src/main.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use std::env;
2+
use std::path::PathBuf;
3+
use std::process;
4+
5+
fn main() -> anyhow::Result<()> {
6+
let mut args = env::args_os().skip(1);
7+
let Some(socket_path) = args.next() else {
8+
eprintln!("Usage: codex-stdio-to-uds <socket-path>");
9+
process::exit(1);
10+
};
11+
12+
if args.next().is_some() {
13+
eprintln!("Expected exactly one argument: <socket-path>");
14+
process::exit(1);
15+
}
16+
17+
let socket_path = PathBuf::from(socket_path);
18+
codex_stdio_to_uds::run(&socket_path)
19+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use std::io::ErrorKind;
2+
use std::io::Read;
3+
use std::io::Write;
4+
use std::sync::mpsc;
5+
use std::thread;
6+
use std::time::Duration;
7+
8+
use anyhow::Context;
9+
use assert_cmd::Command;
10+
use pretty_assertions::assert_eq;
11+
12+
#[cfg(unix)]
13+
use std::os::unix::net::UnixListener;
14+
15+
#[cfg(windows)]
16+
use uds_windows::UnixListener;
17+
18+
#[test]
19+
fn pipes_stdin_and_stdout_through_socket() -> anyhow::Result<()> {
20+
let dir = tempfile::TempDir::new().context("failed to create temp dir")?;
21+
let socket_path = dir.path().join("socket");
22+
let listener = match UnixListener::bind(&socket_path) {
23+
Ok(listener) => listener,
24+
Err(err) if err.kind() == ErrorKind::PermissionDenied => {
25+
eprintln!("skipping test: failed to bind unix socket: {err}");
26+
return Ok(());
27+
}
28+
Err(err) => {
29+
return Err(err).context("failed to bind test unix socket");
30+
}
31+
};
32+
33+
let (tx, rx) = mpsc::channel();
34+
let server_thread = thread::spawn(move || -> anyhow::Result<()> {
35+
let (mut connection, _) = listener
36+
.accept()
37+
.context("failed to accept test connection")?;
38+
let mut received = Vec::new();
39+
connection
40+
.read_to_end(&mut received)
41+
.context("failed to read data from client")?;
42+
tx.send(received)
43+
.map_err(|_| anyhow::anyhow!("failed to send received bytes to test thread"))?;
44+
connection
45+
.write_all(b"response")
46+
.context("failed to write response to client")?;
47+
Ok(())
48+
});
49+
50+
Command::cargo_bin("codex-stdio-to-uds")?
51+
.arg(&socket_path)
52+
.write_stdin("request")
53+
.assert()
54+
.success()
55+
.stdout("response");
56+
57+
let received = rx
58+
.recv_timeout(Duration::from_secs(1))
59+
.context("server did not receive data in time")?;
60+
assert_eq!(received, b"request");
61+
62+
let server_result = server_thread
63+
.join()
64+
.map_err(|_| anyhow::anyhow!("server thread panicked"))?;
65+
server_result.context("server failed")?;
66+
67+
Ok(())
68+
}

0 commit comments

Comments
 (0)