app-server: add Unix socket transport (#18255)

## Summary
- add unix:// app-server transport backed by the shared codex-uds crate
- reuse the websocket connection loop for axum and tungstenite-backed
streams
- add codex app-server proxy to bridge stdio clients to the control
socket
- tolerate Windows UDS backends that report a missing rendezvous path as
connection refused before binding

## Tests
- cargo test -p codex-app-server
control_socket_acceptor_forwards_websocket_text_messages_and_pings
- cargo test -p codex-app-server
- just fmt
- just fix -p codex-app-server
- git -c core.fsmonitor=false diff --check
This commit is contained in:
Ruslan Nigmatullin
2026-04-23 11:09:25 -07:00
committed by GitHub
Unverified
parent c2423f42d1
commit 8a0ab3fc13
13 changed files with 668 additions and 63 deletions
+101 -3
View File
@@ -28,6 +28,7 @@ use codex_tui::AppExitInfo;
use codex_tui::Cli as TuiCli;
use codex_tui::ExitReason;
use codex_tui::UpdateAction;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_cli::CliConfigOverrides;
use owo_colors::OwoColorize;
use std::io::IsTerminal;
@@ -393,7 +394,7 @@ struct AppServerCommand {
subcommand: Option<AppServerSubcommand>,
/// Transport endpoint URL. Supported values: `stdio://` (default),
/// `ws://IP:PORT`, `off`.
/// `unix://`, `unix://PATH`, `ws://IP:PORT`, `off`.
#[arg(
long = "listen",
value_name = "URL",
@@ -437,6 +438,9 @@ struct ExecServerCommand {
#[derive(Debug, clap::Subcommand)]
#[allow(clippy::enum_variant_names)]
enum AppServerSubcommand {
/// Proxy stdio bytes to the running app-server control socket.
Proxy(AppServerProxyCommand),
/// [experimental] Generate TypeScript bindings for the app server protocol.
GenerateTs(GenerateTsCommand),
@@ -448,6 +452,13 @@ enum AppServerSubcommand {
GenerateInternalJsonSchema(GenerateInternalJsonSchemaCommand),
}
#[derive(Debug, Args)]
struct AppServerProxyCommand {
/// Path to the app-server Unix domain socket to connect to.
#[arg(long = "sock", value_name = "SOCKET_PATH", value_parser = parse_socket_path)]
socket_path: Option<AbsolutePathBuf>,
}
#[derive(Debug, Args)]
struct GenerateTsCommand {
/// Output directory where .ts files will be written
@@ -484,8 +495,13 @@ struct GenerateInternalJsonSchemaCommand {
#[derive(Debug, Parser)]
struct StdioToUdsCommand {
/// Path to the Unix domain socket to connect to.
#[arg(value_name = "SOCKET_PATH")]
socket_path: PathBuf,
#[arg(value_name = "SOCKET_PATH", value_parser = parse_socket_path)]
socket_path: AbsolutePathBuf,
}
fn parse_socket_path(raw: &str) -> Result<AbsolutePathBuf, String> {
AbsolutePathBuf::relative_to_current_dir(raw)
.map_err(|err| format!("failed to resolve socket path `{raw}`: {err}"))
}
fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec<String> {
@@ -804,6 +820,16 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
)
.await?;
}
Some(AppServerSubcommand::Proxy(proxy_cli)) => {
let socket_path = match proxy_cli.socket_path {
Some(socket_path) => socket_path,
None => {
let codex_home = find_codex_home()?;
codex_app_server::app_server_control_socket_path(&codex_home)?
}
};
codex_stdio_to_uds::run(socket_path.as_path()).await?;
}
Some(AppServerSubcommand::GenerateTs(gen_cli)) => {
let options = codex_app_server_protocol::GenerateTsOptions {
experimental_api: gen_cli.experimental,
@@ -1408,6 +1434,7 @@ fn reject_remote_mode_for_app_server_subcommand(
) -> anyhow::Result<()> {
let subcommand_name = match subcommand {
None => "app-server",
Some(AppServerSubcommand::Proxy(_)) => "app-server proxy",
Some(AppServerSubcommand::GenerateTs(_)) => "app-server generate-ts",
Some(AppServerSubcommand::GenerateJsonSchema(_)) => "app-server generate-json-schema",
Some(AppServerSubcommand::GenerateInternalJsonSchema(_)) => {
@@ -1724,6 +1751,12 @@ mod tests {
app_server
}
fn default_app_server_socket_path() -> AbsolutePathBuf {
let codex_home = find_codex_home().expect("codex home");
codex_app_server::app_server_control_socket_path(&codex_home)
.expect("default app-server socket path")
}
#[test]
fn debug_prompt_input_parses_prompt_and_images() {
let cli = MultitoolCli::try_parse_from([
@@ -2223,6 +2256,32 @@ mod tests {
);
}
#[test]
fn app_server_listen_unix_socket_url_parses() {
let app_server =
app_server_from_args(["codex", "app-server", "--listen", "unix://"].as_ref());
assert_eq!(
app_server.listen,
codex_app_server::AppServerTransport::UnixSocket {
socket_path: default_app_server_socket_path()
}
);
}
#[test]
fn app_server_listen_unix_socket_path_parses() {
let app_server = app_server_from_args(
["codex", "app-server", "--listen", "unix:///tmp/codex.sock"].as_ref(),
);
assert_eq!(
app_server.listen,
codex_app_server::AppServerTransport::UnixSocket {
socket_path: AbsolutePathBuf::from_absolute_path("/tmp/codex.sock")
.expect("absolute path should parse")
}
);
}
#[test]
fn app_server_listen_off_parses() {
let app_server = app_server_from_args(["codex", "app-server", "--listen", "off"].as_ref());
@@ -2236,6 +2295,45 @@ mod tests {
assert!(parse_result.is_err());
}
#[test]
fn app_server_proxy_subcommand_parses() {
let app_server = app_server_from_args(["codex", "app-server", "proxy"].as_ref());
assert!(matches!(
app_server.subcommand,
Some(AppServerSubcommand::Proxy(AppServerProxyCommand {
socket_path: None
}))
));
}
#[test]
fn app_server_proxy_sock_path_parses() {
let app_server =
app_server_from_args(["codex", "app-server", "proxy", "--sock", "codex.sock"].as_ref());
let Some(AppServerSubcommand::Proxy(proxy)) = app_server.subcommand else {
panic!("expected proxy subcommand");
};
assert_eq!(
proxy.socket_path,
Some(
AbsolutePathBuf::relative_to_current_dir("codex.sock")
.expect("relative path should resolve")
)
);
}
#[test]
fn reject_remote_auth_token_env_for_app_server_proxy() {
let subcommand = AppServerSubcommand::Proxy(AppServerProxyCommand { socket_path: None });
let err = reject_remote_mode_for_app_server_subcommand(
/*remote*/ None,
Some("CODEX_REMOTE_AUTH_TOKEN"),
Some(&subcommand),
)
.expect_err("app-server proxy should reject --remote-auth-token-env");
assert!(err.to_string().contains("app-server proxy"));
}
#[test]
fn app_server_capability_token_flags_parse() {
let app_server = app_server_from_args(