diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index a03849344..840b6cb70 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -25,7 +25,7 @@ Supported transports: - stdio (`--listen stdio://`, default): newline-delimited JSON (JSONL) - websocket (`--listen ws://IP:PORT`): one JSON-RPC message per websocket text frame (**experimental / unsupported**) -- unix socket (`--listen unix://` or `--listen unix://PATH`): websocket frames over `$CODEX_HOME/app-server-control/app-server-control.sock` or a custom socket path without HTTP upgrade +- unix socket (`--listen unix://` or `--listen unix://PATH`): websocket connections over `$CODEX_HOME/app-server-control/app-server-control.sock` or a custom socket path, using the standard HTTP Upgrade handshake - off (`--listen off`): do not expose a local transport When running with `--listen ws://IP:PORT`, the same listener also serves basic HTTP health probes: @@ -39,7 +39,7 @@ Websocket transport is currently experimental and unsupported. Do not rely on it The unix socket transport is intended for local app-server control-plane clients. `codex app-server proxy` opens exactly one raw stream connection to `$CODEX_HOME/app-server-control/app-server-control.sock` by default, or to `--sock PATH` when provided, and proxies bytes between that socket and stdin/stdout. -The socket uses websocket framing directly over the Unix socket, without an HTTP upgrade handshake. +The proxied stream carries the websocket HTTP Upgrade handshake followed by websocket frames. Security note: diff --git a/codex-rs/app-server/src/transport/unix_socket.rs b/codex-rs/app-server/src/transport/unix_socket.rs index 3075676da..5ab1377fb 100644 --- a/codex-rs/app-server/src/transport/unix_socket.rs +++ b/codex-rs/app-server/src/transport/unix_socket.rs @@ -11,8 +11,7 @@ use futures::StreamExt; use tokio::sync::mpsc; use tokio::task::JoinHandle; use tokio::time::Duration; -use tokio_tungstenite::WebSocketStream; -use tokio_tungstenite::tungstenite::protocol::Role; +use tokio_tungstenite::accept_async; use tokio_util::sync::CancellationToken; use tracing::error; use tracing::info; @@ -76,8 +75,13 @@ async fn run_control_socket_acceptor( let transport_event_tx = transport_event_tx.clone(); tokio::spawn(async move { - let websocket_stream = - WebSocketStream::from_raw_socket(stream, Role::Server, None).await; + let websocket_stream = match accept_async(stream).await { + Ok(websocket_stream) => websocket_stream, + Err(err) => { + warn!("failed to upgrade control socket websocket connection: {err}"); + return; + } + }; let (websocket_writer, websocket_reader) = websocket_stream.split(); run_websocket_connection(websocket_writer, websocket_reader, transport_event_tx).await; }); diff --git a/codex-rs/app-server/src/transport/unix_socket_tests.rs b/codex-rs/app-server/src/transport/unix_socket_tests.rs index c2f7a7d35..0b7dec0a2 100644 --- a/codex-rs/app-server/src/transport/unix_socket_tests.rs +++ b/codex-rs/app-server/src/transport/unix_socket_tests.rs @@ -16,10 +16,9 @@ use std::path::Path; use tokio::sync::mpsc; use tokio::time::Duration; use tokio::time::timeout; -use tokio_tungstenite::WebSocketStream; +use tokio_tungstenite::client_async; use tokio_tungstenite::tungstenite::Bytes; use tokio_tungstenite::tungstenite::Message as WebSocketMessage; -use tokio_tungstenite::tungstenite::protocol::Role; use tokio_util::sync::CancellationToken; #[test] @@ -54,7 +53,7 @@ fn listen_unix_socket_accepts_relative_custom_path() { } #[tokio::test] -async fn control_socket_acceptor_forwards_websocket_text_messages_and_pings() { +async fn control_socket_acceptor_upgrades_and_forwards_websocket_text_messages_and_pings() { let temp_dir = tempfile::TempDir::new().expect("temp dir"); let socket_path = test_socket_path(temp_dir.path()); let (transport_event_tx, mut transport_event_rx) = @@ -71,7 +70,10 @@ async fn control_socket_acceptor_forwards_websocket_text_messages_and_pings() { let stream = connect_to_socket(socket_path.as_path()) .await .expect("client should connect"); - let mut websocket = WebSocketStream::from_raw_socket(stream, Role::Client, None).await; + let (mut websocket, response) = client_async("ws://localhost/rpc", stream) + .await + .expect("websocket upgrade should complete"); + assert_eq!(response.status().as_u16(), 101); let opened = timeout(Duration::from_secs(1), transport_event_rx.recv()) .await