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
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

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

1 change: 1 addition & 0 deletions codex-rs/app-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ codex-sandboxing = { workspace = true }
codex-state = { workspace = true }
codex-thread-store = { workspace = true }
codex-tools = { workspace = true }
codex-uds = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-json-to-toml = { workspace = true }
codex-utils-rustls-provider = { workspace = true }
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +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
- 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:
Expand All @@ -35,6 +36,11 @@ When running with `--listen ws://IP:PORT`, the same listener also serves basic H

Websocket transport is currently experimental and unsupported. Do not rely on it for production workloads.

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.

Security note:

- Loopback websocket listeners (`ws://127.0.0.1:PORT`) remain appropriate for localhost and SSH port-forwarding workflows.
Expand Down
5 changes: 3 additions & 2 deletions codex-rs/app-server/src/app_server_tracing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use tracing::info_span;

pub(crate) fn request_span(
request: &JSONRPCRequest,
transport: AppServerTransport,
transport: &AppServerTransport,
connection_id: ConnectionId,
session: &ConnectionSessionState,
) -> Span {
Expand Down Expand Up @@ -82,9 +82,10 @@ pub(crate) fn typed_request_span(
span
}

fn transport_name(transport: AppServerTransport) -> &'static str {
fn transport_name(transport: &AppServerTransport) -> &'static str {
match transport {
AppServerTransport::Stdio => "stdio",
AppServerTransport::UnixSocket { .. } => "unix_socket",
AppServerTransport::WebSocket { .. } => "websocket",
AppServerTransport::Off => "off",
}
Expand Down
27 changes: 19 additions & 8 deletions codex-rs/app-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use crate::transport::OutboundConnectionState;
use crate::transport::TransportEvent;
use crate::transport::auth::policy_from_settings;
use crate::transport::route_outgoing_envelope;
use crate::transport::start_control_socket_acceptor;
use crate::transport::start_remote_control;
use crate::transport::start_stdio_connection;
use crate::transport::start_websocket_acceptor;
Expand Down Expand Up @@ -93,6 +94,7 @@ mod transport;
pub use crate::error_code::INPUT_TOO_LARGE_ERROR_CODE;
pub use crate::error_code::INVALID_PARAMS_ERROR_CODE;
pub use crate::transport::AppServerTransport;
pub use crate::transport::app_server_control_socket_path;
pub use crate::transport::auth::AppServerWebsocketAuthArgs;
pub use crate::transport::auth::AppServerWebsocketAuthSettings;
pub use crate::transport::auth::WebsocketAuthCliMode;
Expand Down Expand Up @@ -542,7 +544,7 @@ pub async fn run_main_with_transport(
let graceful_signal_restart_enabled = !single_client_mode;
let mut app_server_client_name_rx = None;

match transport {
match &transport {
AppServerTransport::Stdio => {
let (stdio_client_name_tx, stdio_client_name_rx) = oneshot::channel::<String>();
app_server_client_name_rx = Some(stdio_client_name_rx);
Expand All @@ -553,9 +555,18 @@ pub async fn run_main_with_transport(
)
.await?;
}
AppServerTransport::UnixSocket { socket_path } => {
let accept_handle = start_control_socket_acceptor(
socket_path.clone(),
transport_event_tx.clone(),
transport_shutdown_token.clone(),
)
.await?;
transport_accept_handles.push(accept_handle);
}
AppServerTransport::WebSocket { bind_address } => {
let accept_handle = start_websocket_acceptor(
bind_address,
*bind_address,
transport_event_tx.clone(),
transport_shutdown_token.clone(),
policy_from_settings(&auth)?,
Expand Down Expand Up @@ -660,7 +671,7 @@ pub async fn run_main_with_transport(
config_warnings,
session_source,
auth_manager,
rpc_transport: analytics_rpc_transport(transport),
rpc_transport: analytics_rpc_transport(&transport),
remote_control_handle: Some(remote_control_handle),
}));
let mut thread_created_rx = processor.thread_created_receiver();
Expand Down Expand Up @@ -772,7 +783,7 @@ pub async fn run_main_with_transport(
.process_request(
connection_id,
request,
transport,
&transport,
Arc::clone(&connection_state.session),
)
.await;
Expand Down Expand Up @@ -892,12 +903,12 @@ pub async fn run_main_with_transport(
Ok(())
}

fn analytics_rpc_transport(transport: AppServerTransport) -> AppServerRpcTransport {
fn analytics_rpc_transport(transport: &AppServerTransport) -> AppServerRpcTransport {
match transport {
AppServerTransport::Stdio => AppServerRpcTransport::Stdio,
AppServerTransport::WebSocket { .. } | AppServerTransport::Off => {
AppServerRpcTransport::Websocket
}
AppServerTransport::UnixSocket { .. }
| AppServerTransport::WebSocket { .. }
| AppServerTransport::Off => AppServerRpcTransport::Websocket,
}
}

Expand Down
2 changes: 1 addition & 1 deletion codex-rs/app-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const DISABLE_MANAGED_CONFIG_ENV_VAR: &str = "CODEX_APP_SERVER_DISABLE_MANAGED_C
#[derive(Debug, Parser)]
struct AppServerArgs {
/// Transport endpoint URL. Supported values: `stdio://` (default),
/// `ws://IP:PORT`, `off`.
/// `unix://`, `unix://PATH`, `ws://IP:PORT`, `off`.
#[arg(
long = "listen",
value_name = "URL",
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ impl MessageProcessor {
self: &Arc<Self>,
connection_id: ConnectionId,
request: JSONRPCRequest,
transport: AppServerTransport,
transport: &AppServerTransport,
session: Arc<ConnectionSessionState>,
) {
let request_method = request.method.as_str();
Expand Down
4 changes: 2 additions & 2 deletions codex-rs/app-server/src/message_processor/tracing_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ impl TracingHarness {
.process_request(
TEST_CONNECTION_ID,
request,
AppServerTransport::Stdio,
&AppServerTransport::Stdio,
Arc::clone(&self.session),
)
.await;
Expand All @@ -210,7 +210,7 @@ impl TracingHarness {
.process_request(
TEST_CONNECTION_ID,
request,
AppServerTransport::Stdio,
&AppServerTransport::Stdio,
Arc::clone(&self.session),
)
.await;
Expand Down
56 changes: 54 additions & 2 deletions codex-rs/app-server/src/transport/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ use crate::outgoing_message::QueuedOutgoingMessage;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::ServerRequest;
use codex_core::config::find_codex_home;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::collections::HashSet;
use std::net::SocketAddr;
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use std::sync::RwLock;
Expand All @@ -31,23 +34,40 @@ pub(crate) const CHANNEL_CAPACITY: usize = 128;

mod remote_control;
mod stdio;
mod unix_socket;
#[cfg(test)]
mod unix_socket_tests;
mod websocket;

pub(crate) use remote_control::RemoteControlHandle;
pub(crate) use remote_control::start_remote_control;
pub(crate) use stdio::start_stdio_connection;
pub(crate) use unix_socket::start_control_socket_acceptor;
pub(crate) use websocket::start_websocket_acceptor;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
const APP_SERVER_CONTROL_SOCKET_DIR_NAME: &str = "app-server-control";
const APP_SERVER_CONTROL_SOCKET_FILE_NAME: &str = "app-server-control.sock";

pub fn app_server_control_socket_path(codex_home: &Path) -> std::io::Result<AbsolutePathBuf> {
AbsolutePathBuf::from_absolute_path(
codex_home
.join(APP_SERVER_CONTROL_SOCKET_DIR_NAME)
.join(APP_SERVER_CONTROL_SOCKET_FILE_NAME),
)
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AppServerTransport {
Stdio,
UnixSocket { socket_path: AbsolutePathBuf },
WebSocket { bind_address: SocketAddr },
Off,
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum AppServerTransportParseError {
UnsupportedListenUrl(String),
InvalidUnixSocketPath { listen_url: String, message: String },
InvalidWebSocketListenUrl(String),
}

Expand All @@ -56,7 +76,14 @@ impl std::fmt::Display for AppServerTransportParseError {
match self {
AppServerTransportParseError::UnsupportedListenUrl(listen_url) => write!(
f,
"unsupported --listen URL `{listen_url}`; expected `stdio://`, `ws://IP:PORT`, or `off`"
"unsupported --listen URL `{listen_url}`; expected `stdio://`, `unix://`, `unix://PATH`, `ws://IP:PORT`, or `off`"
),
AppServerTransportParseError::InvalidUnixSocketPath {
listen_url,
message,
} => write!(
f,
"invalid unix socket --listen URL `{listen_url}`; failed to resolve socket path: {message}"
),
AppServerTransportParseError::InvalidWebSocketListenUrl(listen_url) => write!(
f,
Expand All @@ -76,6 +103,31 @@ impl AppServerTransport {
return Ok(Self::Stdio);
}

if let Some(raw_socket_path) = listen_url.strip_prefix("unix://") {
let socket_path = if raw_socket_path.is_empty() {
let codex_home = find_codex_home().map_err(|err| {
AppServerTransportParseError::InvalidUnixSocketPath {
listen_url: listen_url.to_string(),
message: format!("failed to resolve CODEX_HOME: {err}"),
}
})?;
app_server_control_socket_path(&codex_home).map_err(|err| {
AppServerTransportParseError::InvalidUnixSocketPath {
listen_url: listen_url.to_string(),
message: err.to_string(),
}
})?
} else {
AbsolutePathBuf::relative_to_current_dir(raw_socket_path).map_err(|err| {
AppServerTransportParseError::InvalidUnixSocketPath {
listen_url: listen_url.to_string(),
message: err.to_string(),
}
})?
};
return Ok(Self::UnixSocket { socket_path });
}

if listen_url == "off" {
return Ok(Self::Off);
}
Expand Down
Loading
Loading