Add gRPC TCP transport to the code-mode host (#37745)

## What changed

- Accept `grpc://IP:PORT` endpoints through `--listen` and serve the existing
  code-mode gRPC service over TCP.
- Print the bound HTTP endpoint to stdout so callers can discover the port when
  binding to port `0`.
- Apply the protocol frame-size limits and disable Nagle's algorithm on accepted
  connections.

## Testing

- Add an end-to-end test that starts the host on an ephemeral port, connects a
  gRPC client, and opens a session.
- Verify accepted gRPC sockets have `TCP_NODELAY` enabled.

GitOrigin-RevId: 51d6c21dff8cffb47068c0677ba10ff370385cec
This commit is contained in:
Channing Conger
2026-08-09 23:39:25 +00:00
committed by copyberry
parent 50ef7395fa
commit c0ad3ab014
7 changed files with 144 additions and 3 deletions
@@ -0,0 +1,43 @@
use std::io;
use std::io::Write as _;
use std::net::SocketAddr;
use anyhow::Context;
use anyhow::Result;
use codex_code_mode_protocol::grpc::code_mode_host_server::CodeModeHostServer;
use codex_code_mode_protocol::host::MAX_FRAME_BYTES;
use tokio::net::TcpListener;
use tonic::transport::Server;
use tonic::transport::server::TcpIncoming;
use tracing::info;
use crate::GrpcCodeModeHost;
pub(super) async fn run_tcp_listener(bind_address: SocketAddr) -> Result<()> {
let listener = bind_tcp_listener(bind_address).await?;
let local_address = listener
.local_addr()
.context("failed to read code-mode gRPC listen address")?;
info!("codex-code-mode-host listening on http://{local_address}");
println!("http://{local_address}");
io::stdout()
.flush()
.context("failed to publish code-mode gRPC listen address")?;
Server::builder()
.add_service(
CodeModeHostServer::new(GrpcCodeModeHost::new())
.max_decoding_message_size(MAX_FRAME_BYTES)
.max_encoding_message_size(MAX_FRAME_BYTES),
)
.serve_with_incoming(listener)
.await
.context("code-mode gRPC TCP listener failed")
}
pub(super) async fn bind_tcp_listener(bind_address: SocketAddr) -> Result<TcpIncoming> {
let listener = TcpListener::bind(bind_address)
.await
.with_context(|| format!("failed to bind code-mode gRPC host to {bind_address}"))?;
Ok(TcpIncoming::from(listener).with_nodelay(Some(true)))
}
+1
View File
@@ -50,6 +50,7 @@ pub use self::transport::DEFAULT_LISTEN_URL;
mod delegate;
mod grpc;
mod grpc_transport;
mod peer;
mod transport;
+1 -1
View File
@@ -2,7 +2,7 @@ use clap::Parser;
#[derive(Debug, Parser)]
struct Cli {
/// Transport endpoint: `stdio`, `stdio://`, or `ws://IP:PORT`.
/// Transport endpoint: `stdio`, `stdio://`, `ws://IP:PORT`, or `grpc://IP:PORT`.
#[arg(
long,
value_name = "URL",
+13 -1
View File
@@ -49,6 +49,7 @@ use uuid::Uuid;
use crate::HostLimits;
use crate::MAX_IN_FLIGHT_REQUESTS;
use crate::grpc_transport;
/// The default transport retains the standalone host's original stdio behavior.
pub const DEFAULT_LISTEN_URL: &str = "stdio";
@@ -63,6 +64,7 @@ type BoxedWriter = Box<dyn AsyncWrite + Send + Unpin>;
enum ListenTransport {
Stdio,
WebSocket(SocketAddr),
Grpc(SocketAddr),
}
pub(crate) enum ConnectionReader {
@@ -204,6 +206,7 @@ pub(crate) async fn run_transport(listen_url: &str) -> Result<()> {
match parse_listen_url(listen_url)? {
ListenTransport::Stdio => crate::run_stdio().await,
ListenTransport::WebSocket(bind_address) => run_websocket_listener(bind_address).await,
ListenTransport::Grpc(bind_address) => grpc_transport::run_tcp_listener(bind_address).await,
}
}
@@ -221,8 +224,17 @@ fn parse_listen_url(listen_url: &str) -> Result<ListenTransport> {
});
}
if let Some(socket_addr) = listen_url.strip_prefix("grpc://") {
return socket_addr
.parse::<SocketAddr>()
.map(ListenTransport::Grpc)
.with_context(|| {
format!("invalid gRPC --listen URL `{listen_url}`; expected `grpc://IP:PORT`")
});
}
anyhow::bail!(
"unsupported --listen URL `{listen_url}`; expected `ws://IP:PORT`, `stdio`, or `stdio://`"
"unsupported --listen URL `{listen_url}`; expected `ws://IP:PORT`, `grpc://IP:PORT`, `stdio`, or `stdio://`"
);
}
@@ -1,6 +1,7 @@
use std::net::SocketAddr;
use axum::serve::Listener;
use futures::StreamExt;
use pretty_assertions::assert_eq;
use tokio::net::TcpStream;
@@ -9,6 +10,7 @@ use super::ListenTransport;
use super::MAX_PENDING_BULK_CONNECTIONS;
use super::bind_websocket_listener;
use super::parse_listen_url;
use crate::grpc_transport::bind_tcp_listener;
#[tokio::test]
async fn websocket_listener_disables_nagle() {
@@ -33,6 +35,31 @@ async fn websocket_listener_disables_nagle() {
assert!(stream);
}
#[tokio::test]
async fn grpc_listener_disables_nagle() {
let bind_address = "127.0.0.1:0"
.parse()
.expect("gRPC test listener should have a valid bind address");
let mut listener = bind_tcp_listener(bind_address)
.await
.expect("gRPC test listener should bind");
let local_addr = listener
.local_addr()
.expect("gRPC test listener should have a local address");
let _client = TcpStream::connect(local_addr)
.await
.expect("gRPC test client should connect");
let nodelay = listener
.next()
.await
.expect("gRPC test listener should accept a connection")
.expect("gRPC test listener should return a valid socket")
.nodelay()
.expect("accepted gRPC socket should expose TCP_NODELAY");
assert!(nodelay);
}
#[test]
fn bulk_connection_registration_cleans_up_when_dropped() {
let registry = BulkConnectionRegistry::default();