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
12 changes: 12 additions & 0 deletions codex-rs/Cargo.lock

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

5 changes: 4 additions & 1 deletion codex-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ members = [
"protocol-ts",
"rmcp-client",
"responses-api-proxy",
"stdio-to-uds",
"otel",
"tui",
"git-apply",
Expand All @@ -54,10 +55,10 @@ codex-app-server = { path = "app-server" }
codex-app-server-protocol = { path = "app-server-protocol" }
codex-apply-patch = { path = "apply-patch" }
codex-arg0 = { path = "arg0" }
codex-async-utils = { path = "async-utils" }
codex-chatgpt = { path = "chatgpt" }
codex-common = { path = "common" }
codex-core = { path = "core" }
codex-async-utils = { path = "async-utils" }
codex-exec = { path = "exec" }
codex-feedback = { path = "feedback" }
codex-file-search = { path = "file-search" }
Expand All @@ -73,6 +74,7 @@ codex-protocol = { path = "protocol" }
codex-protocol-ts = { path = "protocol-ts" }
codex-responses-api-proxy = { path = "responses-api-proxy" }
codex-rmcp-client = { path = "rmcp-client" }
codex-stdio-to-uds = { path = "stdio-to-uds" }
codex-tui = { path = "tui" }
codex-utils-json-to-toml = { path = "utils/json-to-toml" }
codex-utils-readiness = { path = "utils/readiness" }
Expand Down Expand Up @@ -186,6 +188,7 @@ tree-sitter = "0.25.10"
tree-sitter-bash = "0.25"
tree-sitter-highlight = "0.25.10"
ts-rs = "11"
uds_windows = "1.1.0"
unicode-segmentation = "1.12.0"
unicode-width = "0.2"
url = "2"
Expand Down
9 changes: 5 additions & 4 deletions codex-rs/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,22 @@ anyhow = { workspace = true }
clap = { workspace = true, features = ["derive"] }
clap_complete = { workspace = true }
codex-app-server = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-arg0 = { workspace = true }
codex-chatgpt = { workspace = true }
codex-cloud-tasks = { path = "../cloud-tasks" }
codex-common = { workspace = true, features = ["cli"] }
codex-core = { workspace = true }
codex-exec = { workspace = true }
codex-login = { workspace = true }
codex-mcp-server = { workspace = true }
codex-process-hardening = { workspace = true }
codex-protocol = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-protocol-ts = { workspace = true }
codex-responses-api-proxy = { workspace = true }
codex-tui = { workspace = true }
codex-rmcp-client = { workspace = true }
codex-cloud-tasks = { path = "../cloud-tasks" }
codex-stdio-to-uds = { workspace = true }
codex-tui = { workspace = true }
ctor = { workspace = true }
owo-colors = { workspace = true }
serde_json = { workspace = true }
Expand All @@ -47,8 +48,8 @@ tokio = { workspace = true, features = [
] }

[dev-dependencies]
assert_matches = { workspace = true }
assert_cmd = { workspace = true }
assert_matches = { workspace = true }
predicates = { workspace = true }
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
16 changes: 16 additions & 0 deletions codex-rs/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ enum Subcommand {
#[clap(hide = true)]
ResponsesApiProxy(ResponsesApiProxyArgs),

/// Internal: relay stdio to a Unix domain socket.
#[clap(hide = true, name = "stdio-to-uds")]
StdioToUds(StdioToUdsCommand),

/// Inspect feature flags.
Features(FeaturesCli),
}
Expand Down Expand Up @@ -205,6 +209,13 @@ struct GenerateTsCommand {
prettier: Option<PathBuf>,
}

#[derive(Debug, Parser)]
struct StdioToUdsCommand {
/// Path to the Unix domain socket to connect to.
#[arg(value_name = "SOCKET_PATH")]
socket_path: PathBuf,
}

fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec<String> {
let AppExitInfo {
token_usage,
Expand Down Expand Up @@ -462,6 +473,11 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
tokio::task::spawn_blocking(move || codex_responses_api_proxy::run_main(args))
.await??;
}
Some(Subcommand::StdioToUds(cmd)) => {
let socket_path = cmd.socket_path;
tokio::task::spawn_blocking(move || codex_stdio_to_uds::run(socket_path.as_path()))
.await??;
}
Some(Subcommand::GenerateTs(gen_cli)) => {
codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?;
}
Expand Down
26 changes: 26 additions & 0 deletions codex-rs/stdio-to-uds/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
edition = "2024"
name = "codex-stdio-to-uds"
version = { workspace = true }

[[bin]]
name = "codex-stdio-to-uds"
path = "src/main.rs"

[lib]
name = "codex_stdio_to_uds"
path = "src/lib.rs"

[lints]
workspace = true

[dependencies]
anyhow = { workspace = true }

[target.'cfg(target_os = "windows")'.dependencies]
uds_windows = { workspace = true }

[dev-dependencies]
assert_cmd = { workspace = true }
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
20 changes: 20 additions & 0 deletions codex-rs/stdio-to-uds/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# codex-stdio-to-uds

Traditionally, there are two transport mechanisms for an MCP server: stdio and HTTP.

This crate helps enable a third, which is UNIX domain socket, because it has the advantages that:

- The UDS can be attached to long-running process, like an HTTP server.
- The UDS can leverage UNIX file permissions to restrict access.

To that end, this crate provides an adapter between a UDS and stdio. The idea is that someone could start an MCP server that communicates over `/tmp/mcp.sock`. Then the user could specify this on the fly like so:

```
codex --config mcp_servers.example={command="codex-stdio-to-uds",args=["/tmp/mcp.sock"]}
```

Unfortunately, the Rust standard library does not provide support for UNIX domain sockets on Windows today even though support was added in October 2018 in Windows 10:

https://github.com/rust-lang/rust/issues/56533

As a workaround, this crate leverages https://crates.io/crates/uds_windows as a dependency on Windows.
52 changes: 52 additions & 0 deletions codex-rs/stdio-to-uds/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#![deny(clippy::print_stdout)]

use std::io;
use std::io::Write;
use std::net::Shutdown;
use std::path::Path;
use std::thread;

use anyhow::Context;
use anyhow::anyhow;

#[cfg(unix)]
use std::os::unix::net::UnixStream;

#[cfg(windows)]
use uds_windows::UnixStream;

/// Connects to the Unix Domain Socket at `socket_path` and relays data between
/// standard input/output and the socket.
pub fn run(socket_path: &Path) -> anyhow::Result<()> {
let mut stream = UnixStream::connect(socket_path)
.with_context(|| format!("failed to connect to socket at {}", socket_path.display()))?;

let mut reader = stream
.try_clone()
.context("failed to clone socket for reading")?;

let stdout_thread = thread::spawn(move || -> io::Result<()> {
let stdout = io::stdout();
let mut handle = stdout.lock();
io::copy(&mut reader, &mut handle)?;
handle.flush()?;
Ok(())
});

let stdin = io::stdin();
{
let mut handle = stdin.lock();
io::copy(&mut handle, &mut stream).context("failed to copy data from stdin to socket")?;
}

stream
.shutdown(Shutdown::Write)
.context("failed to shutdown socket writer")?;

let stdout_result = stdout_thread
.join()
.map_err(|_| anyhow!("thread panicked while copying socket data to stdout"))?;
stdout_result.context("failed to copy data from socket to stdout")?;

Ok(())
}
19 changes: 19 additions & 0 deletions codex-rs/stdio-to-uds/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use std::env;
use std::path::PathBuf;
use std::process;

fn main() -> anyhow::Result<()> {
let mut args = env::args_os().skip(1);
let Some(socket_path) = args.next() else {
eprintln!("Usage: codex-stdio-to-uds <socket-path>");
process::exit(1);
};

if args.next().is_some() {
eprintln!("Expected exactly one argument: <socket-path>");
process::exit(1);
}

let socket_path = PathBuf::from(socket_path);
codex_stdio_to_uds::run(&socket_path)
}
68 changes: 68 additions & 0 deletions codex-rs/stdio-to-uds/tests/stdio_to_uds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::io::ErrorKind;
use std::io::Read;
use std::io::Write;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

use anyhow::Context;
use assert_cmd::Command;
use pretty_assertions::assert_eq;

#[cfg(unix)]
use std::os::unix::net::UnixListener;

#[cfg(windows)]
use uds_windows::UnixListener;

#[test]
fn pipes_stdin_and_stdout_through_socket() -> anyhow::Result<()> {
let dir = tempfile::TempDir::new().context("failed to create temp dir")?;
let socket_path = dir.path().join("socket");
let listener = match UnixListener::bind(&socket_path) {
Ok(listener) => listener,
Err(err) if err.kind() == ErrorKind::PermissionDenied => {
eprintln!("skipping test: failed to bind unix socket: {err}");
return Ok(());
}
Err(err) => {
return Err(err).context("failed to bind test unix socket");
}
};

let (tx, rx) = mpsc::channel();
let server_thread = thread::spawn(move || -> anyhow::Result<()> {
let (mut connection, _) = listener
.accept()
.context("failed to accept test connection")?;
let mut received = Vec::new();
connection
.read_to_end(&mut received)
.context("failed to read data from client")?;
tx.send(received)
.map_err(|_| anyhow::anyhow!("failed to send received bytes to test thread"))?;
connection
.write_all(b"response")
.context("failed to write response to client")?;
Ok(())
});

Command::cargo_bin("codex-stdio-to-uds")?
.arg(&socket_path)
.write_stdin("request")
.assert()
.success()
.stdout("response");

let received = rx
.recv_timeout(Duration::from_secs(1))
.context("server did not receive data in time")?;
assert_eq!(received, b"request");

let server_result = server_thread
.join()
.map_err(|_| anyhow::anyhow!("server thread panicked"))?;
server_result.context("server failed")?;

Ok(())
}
Loading