mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
## What changed - Add `--chatgpt-token-stdin` for remote HTTPS registration and optional `--chatgpt-account-id`, which must match the token's account claim when present. Read a bounded, newline-terminated token from a pipe without persisting it to `auth.json`. - Load managed configuration using the supplied account, enforce account restrictions, and preserve configured policy-server authentication. Skip ChatGPT policy lookup only for known plans that do not require managed configuration. - Allow `--exit-on-stdin-close` to cancel pending policy loading, and redact the registration bearer from registry diagnostics. - Reject incompatible stdin credential options, including flags placed on opposite sides of `forward`, and disallow ChatGPT token stdin authentication with `forward`. ## Testing Add coverage for account-claim validation, policy selection, policy-server sign-in requirements, shutdown during policy loading, credential flag conflicts, and bearer redaction. GitOrigin-RevId: b6b50565e1f150b96edf3cee796909a991f1b47f
463 lines
18 KiB
Rust
463 lines
18 KiB
Rust
//! Command-line startup shared by `codex exec-server` and `codex-exec-server`.
|
|
//!
|
|
//! Transport, configuration, authentication, and shutdown are kept together.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use clap::Parser;
|
|
use codex_arg0::Arg0DispatchPaths;
|
|
use codex_core::config::Config;
|
|
use codex_exec_server::ExecServerRuntimeOptions;
|
|
use codex_http_client::HttpClientFactory;
|
|
use codex_http_client::OutboundProxyPolicy;
|
|
use codex_utils_cli::CliConfigOverrides;
|
|
use codex_websocket_auth::WebsocketAuthArgs;
|
|
|
|
use crate::config::load_exec_server_config;
|
|
use crate::config::load_exec_server_remote_auth_provider;
|
|
use crate::config::read_chatgpt_stdin_auth;
|
|
use crate::exec_server_auth;
|
|
use crate::exec_server_credential::ExecutorCredentialPipe;
|
|
use crate::exec_server_telemetry;
|
|
use crate::exec_server_telemetry::ParentLifetime;
|
|
|
|
/// [EXPERIMENTAL] Run the standalone exec-server service.
|
|
#[derive(Debug, Parser)]
|
|
pub struct ExecServerCommand {
|
|
#[command(subcommand)]
|
|
command: Option<ExecServerSubcommand>,
|
|
|
|
/// Error out when config.toml contains fields that are not recognized by this version of Codex.
|
|
#[arg(
|
|
id = "exec_server_strict_config",
|
|
long = "strict-config",
|
|
default_value_t = false,
|
|
global = true
|
|
)]
|
|
pub strict_config: bool,
|
|
|
|
/// Linux PID namespace: isolate (default) or inherit. Inherit allows signals to
|
|
/// other same-UID processes; enable only when provisioning a dedicated environment.
|
|
#[arg(long, value_name = "MODE", default_value = "isolate", global = true)]
|
|
linux_sandbox_pid_namespace: codex_sandboxing::LinuxSandboxPidNamespace,
|
|
|
|
/// Allow permitted private IP destinations to use the configured upstream proxy.
|
|
/// If no valid upstream proxy applies to the request protocol, connect directly.
|
|
/// Loopback stays local. This flag does not require upstream routing.
|
|
#[arg(
|
|
long,
|
|
env = "CODEX_EXEC_SERVER_PROXY_PRIVATE_IPS_VIA_UPSTREAM",
|
|
global = true
|
|
)]
|
|
proxy_private_ips_via_upstream: bool,
|
|
|
|
/// Maximum number of requests to process concurrently on each connection.
|
|
#[arg(
|
|
long = "concurrent-requests",
|
|
value_name = "COUNT",
|
|
default_value = "1"
|
|
)]
|
|
request_dispatch_mode: codex_exec_server::RequestDispatchMode,
|
|
|
|
/// Transport endpoint URL. Supported values: `ws://IP:PORT` (default), `stdio`, `stdio://`.
|
|
#[arg(
|
|
long = "listen",
|
|
value_name = "URL",
|
|
conflicts_with = "exec_server_remote"
|
|
)]
|
|
listen: Option<String>,
|
|
|
|
#[command(flatten)]
|
|
websocket_auth: WebsocketAuthArgs,
|
|
|
|
/// Register this exec-server as a remote environment using the given base URL.
|
|
#[arg(
|
|
long = "remote",
|
|
id = "exec_server_remote",
|
|
value_name = "URL",
|
|
requires = "environment_id",
|
|
global = true
|
|
)]
|
|
remote: Option<String>,
|
|
|
|
/// Transport used for the remote executor connection.
|
|
#[arg(
|
|
long = "remote-transport",
|
|
value_enum,
|
|
default_value_t = ExecServerRemoteTransport::Noise,
|
|
requires = "exec_server_remote",
|
|
requires_if("direct", "aws_sigv4"),
|
|
global = true
|
|
)]
|
|
remote_transport: ExecServerRemoteTransport,
|
|
|
|
/// Environment id to attach to when registering remotely.
|
|
#[arg(long = "environment-id", value_name = "ID", global = true)]
|
|
environment_id: Option<String>,
|
|
|
|
/// Human-readable environment name.
|
|
#[arg(long = "name", value_name = "NAME", global = true)]
|
|
name: Option<String>,
|
|
|
|
/// Use Agent Identity auth from CODEX_ACCESS_TOKEN for remote registration.
|
|
#[arg(
|
|
long = "use-agent-identity-auth",
|
|
requires = "exec_server_remote",
|
|
conflicts_with = "aws_sigv4",
|
|
global = true
|
|
)]
|
|
use_agent_identity_auth: bool,
|
|
|
|
/// Read one registration bearer from stdin.
|
|
#[arg(
|
|
long,
|
|
requires = "exec_server_remote",
|
|
conflicts_with_all = ["use_agent_identity_auth", "executor_credential_stdin", "aws_sigv4"],
|
|
global = true
|
|
)]
|
|
registration_bearer_stdin: bool,
|
|
|
|
/// Read rotating executor credentials from stdin; exit when the pipe closes.
|
|
#[arg(
|
|
long = "executor-credential-stdin",
|
|
requires = "exec_server_remote",
|
|
conflicts_with_all = ["use_agent_identity_auth", "aws_sigv4"],
|
|
global = true
|
|
)]
|
|
executor_credential_stdin: bool,
|
|
|
|
/// Read a ChatGPT access token from stdin for HTTPS registration.
|
|
#[arg(
|
|
long,
|
|
requires = "exec_server_remote",
|
|
conflicts_with_all = ["use_agent_identity_auth", "aws_sigv4", "executor_credential_stdin", "registration_bearer_stdin"]
|
|
)]
|
|
chatgpt_token_stdin: bool,
|
|
|
|
/// ChatGPT account ID. Must match the token's account claim when present.
|
|
#[arg(long, value_name = "ID", requires = "chatgpt_token_stdin")]
|
|
chatgpt_account_id: Option<String>,
|
|
|
|
/// Sign Direct registration and WebSocket handshake requests with AWS SigV4.
|
|
#[arg(long = "aws-sigv4", requires = "exec_server_remote", global = true)]
|
|
aws_sigv4: bool,
|
|
|
|
/// AWS profile used for SigV4 authentication.
|
|
#[arg(
|
|
long = "aws-profile",
|
|
value_name = "PROFILE",
|
|
requires = "aws_sigv4",
|
|
global = true
|
|
)]
|
|
aws_profile: Option<String>,
|
|
|
|
/// AWS signing region. Uses the SDK region chain when omitted.
|
|
#[arg(
|
|
long = "aws-region",
|
|
value_name = "REGION",
|
|
requires = "aws_sigv4",
|
|
global = true
|
|
)]
|
|
aws_region: Option<String>,
|
|
|
|
/// AWS signing service.
|
|
#[arg(
|
|
long = "aws-service",
|
|
value_name = "SERVICE",
|
|
default_value = "execute-api",
|
|
requires = "aws_sigv4",
|
|
global = true
|
|
)]
|
|
aws_service: String,
|
|
|
|
/// Exit when the parent-owned standard-input pipe closes.
|
|
#[arg(
|
|
long = "exit-on-stdin-close",
|
|
env = codex_exec_server::CODEX_EXEC_SERVER_EXIT_ON_STDIN_CLOSE_ENV_VAR,
|
|
requires_if("true", "exec_server_remote"),
|
|
global = true
|
|
)]
|
|
exit_on_stdin_close: bool,
|
|
}
|
|
|
|
impl ExecServerCommand {
|
|
/// Starts the executor with the given runtime paths and configuration overrides.
|
|
pub async fn run(
|
|
mut self,
|
|
arg0_paths: &Arg0DispatchPaths,
|
|
root_config_overrides: &CliConfigOverrides,
|
|
) -> anyhow::Result<()> {
|
|
let strict_config = self.strict_config;
|
|
self.validate_remote_transport()?;
|
|
let enable_cloud_config = self.should_load_cloud_config();
|
|
let websocket_auth = self.websocket_auth.try_into_settings()?;
|
|
if websocket_auth.config.is_some() && (self.remote.is_some() || self.command.is_some()) {
|
|
anyhow::bail!("WebSocket listener auth cannot be used with --remote or forward");
|
|
}
|
|
let codex_self_exe = arg0_paths
|
|
.codex_self_exe
|
|
.clone()
|
|
.ok_or_else(|| anyhow::anyhow!("Codex executable path is not configured"))?;
|
|
let mut runtime_paths = ExecServerRuntimeOptions::new(
|
|
codex_self_exe,
|
|
arg0_paths.codex_linux_sandbox_exe.clone(),
|
|
)?
|
|
.with_linux_sandbox_pid_namespace(self.linux_sandbox_pid_namespace)
|
|
.with_proxy_private_ips_via_upstream(self.proxy_private_ips_via_upstream);
|
|
// Config loading below owns error handling (non-strict stdio can proceed
|
|
// without config). Never retain other startup flags in the executor.
|
|
if let Ok(overrides) = root_config_overrides.parse_overrides() {
|
|
let cli_flags = codex_config::build_cli_overrides_layer(&overrides);
|
|
runtime_paths.prefer_mxc = cli_flags
|
|
.get("features")
|
|
.and_then(|features| features.get("prefer_mxc"))
|
|
.and_then(toml::Value::as_bool);
|
|
}
|
|
if let Some(base_url) = self.remote.take() {
|
|
let environment_id = self.environment_id.take().ok_or_else(|| {
|
|
anyhow::anyhow!("--environment-id is required when --remote is set")
|
|
})?;
|
|
let registration_auth = self
|
|
.registration_bearer_stdin
|
|
.then(codex_exec_server::registration_auth_from_stdin)
|
|
.transpose()?;
|
|
let stdin_auth = if self.chatgpt_token_stdin {
|
|
codex_http_client::validate_https_endpoint_url(&base_url, "ChatGPT token registry")
|
|
.map_err(anyhow::Error::msg)?;
|
|
Some(read_chatgpt_stdin_auth(self.chatgpt_account_id.take())?)
|
|
} else {
|
|
None
|
|
};
|
|
let config = load_exec_server_config(
|
|
root_config_overrides,
|
|
strict_config,
|
|
enable_cloud_config,
|
|
stdin_auth.as_ref(),
|
|
);
|
|
let mut parent_lifetime = if self.exit_on_stdin_close && stdin_auth.is_some() {
|
|
ParentLifetime::MonitoredStdin(exec_server_telemetry::monitor_stdin_close())
|
|
} else if self.exit_on_stdin_close {
|
|
ParentLifetime::StdinPipe
|
|
} else {
|
|
ParentLifetime::Independent
|
|
};
|
|
let config = match &mut parent_lifetime {
|
|
ParentLifetime::MonitoredStdin(closed) => tokio::select! {
|
|
biased;
|
|
_ = closed => return Ok(()),
|
|
config = config => config?,
|
|
},
|
|
ParentLifetime::StdinPipe | ParentLifetime::Independent => config.await?,
|
|
};
|
|
let direct_transport = self.remote_transport == ExecServerRemoteTransport::Direct;
|
|
// Configured exporters may contain account-wide credentials or client keys.
|
|
// The scoped executor must not initialize those exporters.
|
|
let telemetry_config = (!self.executor_credential_stdin).then_some(&config);
|
|
let (_otel, telemetry) = exec_server_telemetry::init(telemetry_config);
|
|
let mut credential_provider = None;
|
|
let auth_provider: codex_api::SharedAuthProvider = if self.executor_credential_stdin {
|
|
let ExecutorCredentialPipe {
|
|
provider,
|
|
parent_closed,
|
|
} = ExecutorCredentialPipe::from_stdin(environment_id.trim().to_string());
|
|
provider.wait_for_initial().await?;
|
|
parent_lifetime = ParentLifetime::MonitoredStdin(parent_closed);
|
|
credential_provider = Some(Arc::clone(&provider));
|
|
provider
|
|
} else if let Some(auth) = stdin_auth {
|
|
anyhow::ensure!(
|
|
config.auth_config().allows_auth(&auth),
|
|
"ChatGPT authentication is not permitted for this account"
|
|
);
|
|
codex_model_provider::auth_provider_from_auth(&auth)
|
|
} else if self.aws_sigv4 {
|
|
exec_server_auth::aws_sigv4_auth_provider(
|
|
codex_aws_auth::AwsAuthConfig {
|
|
profile: self.aws_profile,
|
|
region: self.aws_region,
|
|
service: self.aws_service,
|
|
},
|
|
config.http_client_factory(),
|
|
)
|
|
.await?
|
|
} else if let Some(provider) = registration_auth {
|
|
provider
|
|
} else {
|
|
load_exec_server_remote_auth_provider(
|
|
&config,
|
|
&base_url,
|
|
self.use_agent_identity_auth,
|
|
)
|
|
.await?
|
|
};
|
|
let mut remote_config = codex_exec_server::RemoteEnvironmentConfig::new_with_transport(
|
|
base_url,
|
|
environment_id,
|
|
if direct_transport {
|
|
codex_exec_server::RemoteEnvironmentTransport::Direct
|
|
} else {
|
|
codex_exec_server::RemoteEnvironmentTransport::Noise
|
|
},
|
|
auth_provider,
|
|
config.http_client_factory(),
|
|
)?;
|
|
if self.executor_credential_stdin || self.registration_bearer_stdin {
|
|
remote_config = remote_config.with_executor_credential_auth()?;
|
|
} else if self.chatgpt_token_stdin {
|
|
remote_config = remote_config.with_registry_bearer_redaction();
|
|
}
|
|
if let Some(name) = self.name {
|
|
remote_config.name = name;
|
|
}
|
|
remote_config.request_dispatch_mode = self.request_dispatch_mode;
|
|
let remote_config = remote_config.with_telemetry(telemetry);
|
|
let (shutdown_sender, shutdown_receiver) = tokio::sync::oneshot::channel();
|
|
#[cfg(target_os = "macos")]
|
|
let runtime_paths = runtime_paths.with_allowed_symlinked_codex_home(
|
|
codex_config::allowed_symlinked_codex_home(
|
|
&config.config_layer_stack,
|
|
&config.codex_home,
|
|
),
|
|
);
|
|
let result = exec_server_telemetry::run_until_shutdown(
|
|
async move {
|
|
let shutdown = async move {
|
|
let _ = shutdown_receiver.await;
|
|
};
|
|
match self.command {
|
|
Some(ExecServerSubcommand::Forward { connect }) => {
|
|
codex_exec_server::run_remote_environment_forward_until_shutdown(
|
|
remote_config,
|
|
connect,
|
|
shutdown,
|
|
)
|
|
.await
|
|
}
|
|
None => {
|
|
codex_exec_server::run_remote_environment_until_shutdown(
|
|
remote_config,
|
|
runtime_paths,
|
|
shutdown,
|
|
)
|
|
.await
|
|
}
|
|
}
|
|
.map_err(anyhow::Error::new)
|
|
},
|
|
parent_lifetime,
|
|
exec_server_telemetry::ShutdownBehavior::Graceful(shutdown_sender),
|
|
)
|
|
.await;
|
|
if let Some(provider) = credential_provider {
|
|
provider.check_pipe_failure()?;
|
|
}
|
|
result
|
|
} else {
|
|
let config_result = load_exec_server_config(
|
|
root_config_overrides,
|
|
strict_config,
|
|
enable_cloud_config,
|
|
/*stdin_auth*/ None,
|
|
)
|
|
.await;
|
|
let config = if strict_config {
|
|
Some(config_result?)
|
|
} else {
|
|
config_result.ok()
|
|
};
|
|
let (_otel, telemetry) = exec_server_telemetry::init(config.as_ref());
|
|
#[cfg(target_os = "macos")]
|
|
let runtime_paths = runtime_paths.with_allowed_symlinked_codex_home(
|
|
config.as_ref().and_then(|config| {
|
|
codex_config::allowed_symlinked_codex_home(
|
|
&config.config_layer_stack,
|
|
&config.codex_home,
|
|
)
|
|
}),
|
|
);
|
|
let http_client_factory = config
|
|
.as_ref()
|
|
.map(Config::http_client_factory)
|
|
.unwrap_or_else(|| HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault));
|
|
let listen_url = self
|
|
.listen
|
|
.unwrap_or_else(|| codex_exec_server::DEFAULT_LISTEN_URL.to_string());
|
|
let run = exec_server_telemetry::run_until_shutdown(
|
|
codex_exec_server::run_main_with_telemetry(
|
|
&listen_url,
|
|
runtime_paths,
|
|
telemetry,
|
|
http_client_factory,
|
|
self.request_dispatch_mode,
|
|
websocket_auth,
|
|
),
|
|
ParentLifetime::Independent,
|
|
exec_server_telemetry::ShutdownBehavior::Immediate,
|
|
);
|
|
run.await.map_err(anyhow::Error::from_boxed)
|
|
}
|
|
}
|
|
|
|
fn should_load_cloud_config(&self) -> bool {
|
|
self.remote.is_some()
|
|
&& self.command.is_none()
|
|
&& !self.executor_credential_stdin
|
|
&& !self.registration_bearer_stdin
|
|
&& !self.aws_sigv4
|
|
&& !self.use_agent_identity_auth
|
|
}
|
|
|
|
fn validate_remote_transport(&self) -> anyhow::Result<()> {
|
|
// Clap checks global flag conflicts separately on either side of `forward`.
|
|
if self.registration_bearer_stdin && self.executor_credential_stdin {
|
|
anyhow::bail!(
|
|
"--registration-bearer-stdin cannot be used with --executor-credential-stdin"
|
|
);
|
|
}
|
|
anyhow::ensure!(
|
|
!self.chatgpt_token_stdin || self.command.is_none(),
|
|
"--chatgpt-token-stdin cannot be used with forward"
|
|
);
|
|
match (self.remote_transport, self.aws_sigv4) {
|
|
(ExecServerRemoteTransport::Noise, true) => {
|
|
anyhow::bail!("--aws-sigv4 requires --remote-transport direct");
|
|
}
|
|
(ExecServerRemoteTransport::Direct, false) => {
|
|
anyhow::bail!("--remote-transport direct requires --aws-sigv4");
|
|
}
|
|
(ExecServerRemoteTransport::Noise, false)
|
|
| (ExecServerRemoteTransport::Direct, true) => {}
|
|
}
|
|
if self.remote_transport == ExecServerRemoteTransport::Direct
|
|
&& matches!(
|
|
self.command.as_ref(),
|
|
Some(ExecServerSubcommand::Forward { .. })
|
|
)
|
|
{
|
|
anyhow::bail!("direct exec-server transport does not support forwarding");
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
|
|
enum ExecServerRemoteTransport {
|
|
#[default]
|
|
Noise,
|
|
Direct,
|
|
}
|
|
|
|
#[derive(Debug, clap::Subcommand)]
|
|
enum ExecServerSubcommand {
|
|
/// Register an existing WebSocket exec-server as a remote environment.
|
|
Forward {
|
|
/// Destination exec-server WebSocket URL.
|
|
#[arg(long, value_name = "URL", requires = "exec_server_remote")]
|
|
connect: String,
|
|
},
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "exec_server_args_tests.rs"]
|
|
mod tests;
|