Add support for UDS in codex --remote (#22414)

## Why

Added support for UDS connections in `codex --remote`.

TUI also now connects to local app-server using UDS by default if it is
running and set to listen to UDS connection.

## What Changed

- Introduced `RemoteAppServerEndpoint` with `WebSocket` and `UnixSocket`
variants.
- Reused the existing JSON-RPC-over-WebSocket protocol over either a TCP
WebSocket stream or a UDS stream.
- Updated `codex --remote` to accept `ws://host:port`,
`wss://host:port`, `unix://`, and `unix://PATH`.
- Kept `--remote-auth-token-env` restricted to `wss://` and loopback
`ws://` remotes.
- Added a fast TUI startup probe for the default daemon socket, falling
back to the embedded app server when the daemon is absent or
unresponsive.

## Verification

- Manually verified that the updated remote flow works.
- Added coverage for UDS remote round trips, WebSocket auth headers,
auth-token transport policy, remote address parsing, and missing-daemon
fallback.
- Ran focused remote test coverage locally.
This commit is contained in:
Eric Traut
2026-05-12 21:17:20 -07:00
committed by GitHub
parent 7bf95b39aa
commit ad572709ab
12 changed files with 564 additions and 293 deletions

View File

@@ -722,9 +722,9 @@ struct FeatureToggles {
#[derive(Debug, Default, Parser, Clone)]
struct InteractiveRemoteOptions {
/// Connect the TUI to a remote app server websocket endpoint.
/// Connect the TUI to a remote app server endpoint.
///
/// Accepted forms: `ws://host:port` or `wss://host:port`.
/// Accepted forms: `ws://host:port`, `wss://host:port`, `unix://`, or `unix://PATH`.
#[arg(long = "remote", value_name = "ADDR")]
remote: Option<String>,
@@ -1709,27 +1709,39 @@ async fn run_interactive_tui(
}
}
let normalized_remote = remote
let mut remote_endpoint = remote
.as_deref()
.map(codex_tui::normalize_remote_addr)
.map(codex_tui::resolve_remote_addr)
.transpose()
.map_err(std::io::Error::other)?;
if remote_auth_token_env.is_some() && normalized_remote.is_none() {
return Ok(AppExitInfo::fatal(
"`--remote-auth-token-env` requires `--remote`.",
));
if let Some(remote_auth_token_env) = remote_auth_token_env {
let Some(endpoint) = remote_endpoint.as_mut() else {
return Ok(AppExitInfo::fatal(
"`--remote-auth-token-env` requires `--remote`.",
));
};
if !codex_tui::remote_addr_supports_auth_token(endpoint) {
return Ok(AppExitInfo::fatal(
"`--remote-auth-token-env` requires a `wss://` or loopback `ws://` remote.",
));
}
let auth_token = read_remote_auth_token_from_env_var(&remote_auth_token_env)
.map_err(std::io::Error::other)?;
let codex_tui::RemoteAppServerEndpoint::WebSocket {
auth_token: slot, ..
} = endpoint
else {
return Ok(AppExitInfo::fatal(
"`--remote-auth-token-env` requires a `wss://` or loopback `ws://` remote.",
));
};
*slot = Some(auth_token);
}
let remote_auth_token = remote_auth_token_env
.as_deref()
.map(read_remote_auth_token_from_env_var)
.transpose()
.map_err(std::io::Error::other)?;
codex_tui::run_main(
interactive,
arg0_paths,
codex_config::LoaderOverrides::default(),
normalized_remote,
remote_auth_token,
remote_endpoint,
)
.await
}
@@ -2411,13 +2423,8 @@ mod tests {
#[test]
fn reject_remote_flag_for_remote_control() {
let cli = MultitoolCli::try_parse_from([
"codex",
"--remote",
"ws://127.0.0.1:1234",
"remote-control",
])
.expect("parse");
let cli = MultitoolCli::try_parse_from(["codex", "--remote", "unix://", "remote-control"])
.expect("parse");
assert_matches!(cli.subcommand, Some(Subcommand::RemoteControl));
let err = reject_remote_mode_for_subcommand(
@@ -2432,9 +2439,9 @@ mod tests {
#[test]
fn remote_flag_parses_for_interactive_root() {
let cli = MultitoolCli::try_parse_from(["codex", "--remote", "ws://127.0.0.1:4500"])
let cli = MultitoolCli::try_parse_from(["codex", "--remote", "unix://codex.sock"])
.expect("parse");
assert_eq!(cli.remote.remote.as_deref(), Some("ws://127.0.0.1:4500"));
assert_eq!(cli.remote.remote.as_deref(), Some("unix://codex.sock"));
}
#[test]
@@ -2456,14 +2463,14 @@ mod tests {
#[test]
fn remote_flag_parses_for_resume_subcommand() {
let cli =
MultitoolCli::try_parse_from(["codex", "resume", "--remote", "ws://127.0.0.1:4500"])
MultitoolCli::try_parse_from(["codex", "resume", "--remote", "unix://codex.sock"])
.expect("parse");
let Subcommand::Resume(ResumeCommand { remote, .. }) =
cli.subcommand.expect("resume present")
else {
panic!("expected resume subcommand");
};
assert_eq!(remote.remote.as_deref(), Some("ws://127.0.0.1:4500"));
assert_eq!(remote.remote.as_deref(), Some("unix://codex.sock"));
}
#[test]