Compare commits

...

1 Commits

Author SHA1 Message Date
starr-openai
024d89bdae Add standalone exec-server binary
Expose codex-exec-server as a standalone binary while keeping the existing codex exec-server subcommand on the same command runner. Update remote-env coverage to exercise the standalone binary and its sandbox resource layout.

Co-authored-by: Codex <noreply@openai.com>
2026-05-07 17:56:37 -07:00
9 changed files with 183 additions and 75 deletions

View File

@@ -9,3 +9,8 @@ codex_rust_crate(
multiplatform_binaries(
name = "codex",
)
multiplatform_binaries(
name = "codex-exec-server",
release_binaries_name = "codex-exec-server-release_binaries",
)

View File

@@ -9,6 +9,10 @@ build = "build.rs"
name = "codex"
path = "src/main.rs"
[[bin]]
name = "codex-exec-server"
path = "src/bin/codex-exec-server.rs"
[lib]
name = "codex_cli"
path = "src/lib.rs"

View File

@@ -0,0 +1,11 @@
use clap::Parser;
use codex_arg0::Arg0DispatchPaths;
use codex_arg0::arg0_dispatch_or_else;
use codex_cli::ExecServerCommand;
use codex_cli::run_exec_server_command;
fn main() -> anyhow::Result<()> {
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
run_exec_server_command(ExecServerCommand::parse(), &arg0_paths).await
})
}

View File

@@ -0,0 +1,130 @@
use clap::Parser;
use codex_arg0::Arg0DispatchPaths;
#[derive(Debug, Parser)]
pub struct ExecServerCommand {
/// Transport endpoint URL. Supported values: `ws://IP:PORT` (default), `stdio`, `stdio://`.
#[arg(long = "listen", value_name = "URL", conflicts_with = "remote")]
listen: Option<String>,
/// Register this exec-server as a remote executor using the given base URL.
#[arg(long = "remote", value_name = "URL", requires = "executor_id")]
remote: Option<String>,
/// Executor id to attach to when registering remotely.
#[arg(long = "executor-id", value_name = "ID")]
executor_id: Option<String>,
/// Human-readable executor name.
#[arg(long = "name", value_name = "NAME")]
name: Option<String>,
}
pub async fn run_exec_server_command(
cmd: ExecServerCommand,
arg0_paths: &Arg0DispatchPaths,
) -> anyhow::Result<()> {
let codex_self_exe = arg0_paths
.codex_self_exe
.clone()
.ok_or_else(|| anyhow::anyhow!("Codex executable path is not configured"))?;
let runtime_paths = codex_exec_server::ExecServerRuntimePaths::new(
codex_self_exe,
arg0_paths.codex_linux_sandbox_exe.clone(),
)?;
if let Some(base_url) = cmd.remote {
let executor_id = cmd
.executor_id
.ok_or_else(|| anyhow::anyhow!("--executor-id is required when --remote is set"))?;
let mut remote_config =
codex_exec_server::RemoteExecutorConfig::new(base_url, executor_id)?;
if let Some(name) = cmd.name {
remote_config.name = name;
}
codex_exec_server::run_remote_executor(remote_config, runtime_paths).await?;
return Ok(());
}
let listen_url = cmd
.listen
.as_deref()
.unwrap_or(codex_exec_server::DEFAULT_LISTEN_URL);
codex_exec_server::run_main(listen_url, runtime_paths)
.await
.map_err(anyhow::Error::from_boxed)
}
#[cfg(test)]
mod tests {
use clap::error::ErrorKind;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn parses_default_listen_mode() {
let command = ExecServerCommand::try_parse_from(["codex-exec-server"]).unwrap();
assert_eq!(command.listen, None);
assert_eq!(command.remote, None);
assert_eq!(command.executor_id, None);
assert_eq!(command.name, None);
}
#[test]
fn parses_explicit_listen_mode() {
let command =
ExecServerCommand::try_parse_from(["codex-exec-server", "--listen", "stdio"]).unwrap();
assert_eq!(command.listen.as_deref(), Some("stdio"));
assert_eq!(command.remote, None);
assert_eq!(command.executor_id, None);
assert_eq!(command.name, None);
}
#[test]
fn parses_remote_registration_mode() {
let command = ExecServerCommand::try_parse_from([
"codex-exec-server",
"--remote",
"https://example.test",
"--executor-id",
"executor-1",
"--name",
"worker",
])
.unwrap();
assert_eq!(command.listen, None);
assert_eq!(command.remote.as_deref(), Some("https://example.test"));
assert_eq!(command.executor_id.as_deref(), Some("executor-1"));
assert_eq!(command.name.as_deref(), Some("worker"));
}
#[test]
fn rejects_remote_without_executor_id() {
let error = ExecServerCommand::try_parse_from([
"codex-exec-server",
"--remote",
"https://example.test",
])
.unwrap_err();
assert_eq!(error.kind(), ErrorKind::MissingRequiredArgument);
}
#[test]
fn rejects_listen_with_remote() {
let error = ExecServerCommand::try_parse_from([
"codex-exec-server",
"--listen",
"stdio",
"--remote",
"https://example.test",
"--executor-id",
"executor-1",
])
.unwrap_err();
assert_eq!(error.kind(), ErrorKind::ArgumentConflict);
}
}

View File

@@ -1,4 +1,5 @@
pub(crate) mod debug_sandbox;
mod exec_server_command;
mod exit_status;
pub(crate) mod login;
@@ -10,6 +11,8 @@ use std::path::PathBuf;
pub use debug_sandbox::run_command_under_landlock;
pub use debug_sandbox::run_command_under_seatbelt;
pub use debug_sandbox::run_command_under_windows;
pub use exec_server_command::ExecServerCommand;
pub use exec_server_command::run_exec_server_command;
pub use login::read_access_token_from_stdin;
pub use login::read_api_key_from_stdin;
pub use login::run_login_status;

View File

@@ -7,11 +7,13 @@ use codex_arg0::Arg0DispatchPaths;
use codex_arg0::arg0_dispatch_or_else;
use codex_chatgpt::apply_command::ApplyCommand;
use codex_chatgpt::apply_command::run_apply_command;
use codex_cli::ExecServerCommand;
use codex_cli::LandlockCommand;
use codex_cli::SeatbeltCommand;
use codex_cli::WindowsCommand;
use codex_cli::read_access_token_from_stdin;
use codex_cli::read_api_key_from_stdin;
use codex_cli::run_exec_server_command;
use codex_cli::run_login_status;
use codex_cli::run_login_with_access_token;
use codex_cli::run_login_with_api_key;
@@ -447,25 +449,6 @@ struct AppServerCommand {
auth: codex_app_server::AppServerWebsocketAuthArgs,
}
#[derive(Debug, Parser)]
struct ExecServerCommand {
/// Transport endpoint URL. Supported values: `ws://IP:PORT` (default), `stdio`, `stdio://`.
#[arg(long = "listen", value_name = "URL", conflicts_with = "remote")]
listen: Option<String>,
/// Register this exec-server as a remote executor using the given base URL.
#[arg(long = "remote", value_name = "URL", requires = "executor_id")]
remote: Option<String>,
/// Executor id to attach to when registering remotely.
#[arg(long = "executor-id", value_name = "ID")]
executor_id: Option<String>,
/// Human-readable executor name.
#[arg(long = "name", value_name = "NAME")]
name: Option<String>,
}
#[derive(Debug, clap::Subcommand)]
#[allow(clippy::enum_variant_names)]
enum AppServerSubcommand {
@@ -1289,39 +1272,6 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
Ok(())
}
async fn run_exec_server_command(
cmd: ExecServerCommand,
arg0_paths: &Arg0DispatchPaths,
) -> anyhow::Result<()> {
let codex_self_exe = arg0_paths
.codex_self_exe
.clone()
.ok_or_else(|| anyhow::anyhow!("Codex executable path is not configured"))?;
let runtime_paths = codex_exec_server::ExecServerRuntimePaths::new(
codex_self_exe,
arg0_paths.codex_linux_sandbox_exe.clone(),
)?;
if let Some(base_url) = cmd.remote {
let executor_id = cmd
.executor_id
.ok_or_else(|| anyhow::anyhow!("--executor-id is required when --remote is set"))?;
let mut remote_config =
codex_exec_server::RemoteExecutorConfig::new(base_url, executor_id)?;
if let Some(name) = cmd.name {
remote_config.name = name;
}
codex_exec_server::run_remote_executor(remote_config, runtime_paths).await?;
return Ok(());
}
let listen_url = cmd
.listen
.as_deref()
.unwrap_or(codex_exec_server::DEFAULT_LISTEN_URL);
codex_exec_server::run_main(listen_url, runtime_paths)
.await
.map_err(anyhow::Error::from_boxed)
}
async fn enable_feature_in_config(interactive: &TuiCli, feature: &str) -> anyhow::Result<()> {
FeatureToggles::validate_feature(feature)?;
let codex_home = find_codex_home()?;

View File

@@ -1,18 +1,18 @@
# codex-exec-server
`codex-exec-server` is the library backing `codex exec-server`, a small
JSON-RPC server for spawning and controlling subprocesses through
`codex-utils-pty`.
`codex-exec-server` is the library backing `codex exec-server` and the
standalone `codex-exec-server` binary, a small JSON-RPC server for spawning and
controlling subprocesses through `codex-utils-pty`.
It provides:
- a CLI entrypoint: `codex exec-server`
- CLI entrypoints: `codex exec-server` and `codex-exec-server`
- a Rust client: `ExecServerClient`
- a small protocol module with shared request/response types
This crate owns the transport, protocol, and filesystem/process handlers. The
top-level `codex` binary owns hidden helper dispatch for sandboxed
filesystem operations and `codex-linux-sandbox`.
CLI entrypoints run through `codex-arg0`, which owns hidden helper dispatch for
sandboxed filesystem operations and `codex-linux-sandbox`.
## Transport
@@ -316,9 +316,10 @@ The crate exports:
- `RemoteExecutorConfig` and `run_remote_executor()` for embedding remote
registration mode
Callers must pass `ExecServerRuntimePaths` to `run_main()`. The top-level
`codex exec-server` command builds these paths from the `codex` arg0 dispatch
state.
Callers must pass `ExecServerRuntimePaths` to `run_main()`. The CLI entrypoints
build these paths from the `codex-arg0` dispatch state so sandboxed filesystem
helpers and `codex-linux-sandbox` re-entry work the same way from both
`codex exec-server` and `codex-exec-server`.
## Example session

View File

@@ -53,7 +53,7 @@ MACOS_WEBRTC_RUSTC_LINK_FLAGS = select({
"//conditions:default": [],
})
def multiplatform_binaries(name, platforms = PLATFORMS):
def multiplatform_binaries(name, platforms = PLATFORMS, release_binaries_name = "release_binaries"):
for platform in platforms:
platform_data(
name = name + "_" + platform,
@@ -63,7 +63,7 @@ def multiplatform_binaries(name, platforms = PLATFORMS):
)
native.filegroup(
name = "release_binaries",
name = release_binaries_name,
srcs = [name + "_" + platform for platform in platforms],
tags = ["manual"],
)

View File

@@ -17,15 +17,16 @@ is_sourced() {
setup_remote_env() {
local container_name
local codex_binary_path
local exec_server_binary_path
local container_ip
local remote_codex_path
local remote_bwrap_path
local remote_exec_server_path
local remote_exec_server_pid
local remote_exec_server_port
local remote_exec_server_stdout_path
container_name="${CODEX_TEST_REMOTE_ENV_CONTAINER_NAME:-codex-remote-test-env-local-$(date +%s)-${RANDOM}}"
codex_binary_path="${REPO_ROOT}/codex-rs/target/debug/codex"
exec_server_binary_path="${REPO_ROOT}/codex-rs/target/debug/codex-exec-server"
if ! command -v docker >/dev/null 2>&1; then
echo "docker is required (Colima or Docker Desktop)" >&2
@@ -44,11 +45,11 @@ setup_remote_env() {
(
cd "${REPO_ROOT}/codex-rs"
cargo build -p codex-cli --bin codex
cargo build -p codex-cli --bin codex-exec-server
)
if [[ ! -f "${codex_binary_path}" ]]; then
echo "codex binary not found at ${codex_binary_path}" >&2
if [[ ! -f "${exec_server_binary_path}" ]]; then
echo "codex-exec-server binary not found at ${exec_server_binary_path}" >&2
return 1
fi
@@ -59,21 +60,24 @@ setup_remote_env() {
--privileged \
--security-opt seccomp=unconfined \
ubuntu:24.04 sleep infinity >/dev/null
if ! docker exec "${container_name}" sh -lc "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y python3 zsh"; then
if ! docker exec "${container_name}" sh -lc "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y bubblewrap python3 zsh"; then
docker rm -f "${container_name}" >/dev/null 2>&1 || true
return 1
fi
if [[ -z "${CODEX_TEST_REMOTE_EXEC_SERVER_URL:-}" ]]; then
remote_codex_path="/tmp/codex-remote-env/codex"
remote_bwrap_path="/tmp/codex-remote-env/codex-resources/bwrap"
remote_exec_server_path="/tmp/codex-remote-env/codex-exec-server"
remote_exec_server_port="31987"
remote_exec_server_stdout_path="/tmp/codex-remote-env/exec-server.stdout"
docker exec "${container_name}" sh -lc "mkdir -p /tmp/codex-remote-env"
docker cp "${codex_binary_path}" "${container_name}:${remote_codex_path}"
docker exec "${container_name}" chmod +x "${remote_codex_path}"
docker exec "${container_name}" sh -lc "mkdir -p /tmp/codex-remote-env/codex-resources"
docker cp "${exec_server_binary_path}" "${container_name}:${remote_exec_server_path}"
docker exec "${container_name}" chmod +x "${remote_exec_server_path}"
# Match the release resource layout the Linux sandbox probes next to the executable.
docker exec "${container_name}" sh -lc "cp /usr/bin/bwrap ${remote_bwrap_path} && chmod +x ${remote_bwrap_path}"
remote_exec_server_pid="$(
docker exec "${container_name}" sh -lc \
"rm -f ${remote_exec_server_stdout_path}; nohup ${remote_codex_path} exec-server --listen ws://0.0.0.0:${remote_exec_server_port} > ${remote_exec_server_stdout_path} 2>&1 & echo \$!"
"rm -f ${remote_exec_server_stdout_path}; PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin nohup ${remote_exec_server_path} --listen ws://0.0.0.0:${remote_exec_server_port} > ${remote_exec_server_stdout_path} 2>&1 & echo \$!"
)"
wait_for_remote_exec_server_port "${container_name}" "${remote_exec_server_port}" "${remote_exec_server_stdout_path}"
container_ip="$(