Add ChatGPT token stdin authentication to exec-server (#59569)

## 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
This commit is contained in:
aagrawal-oai
2026-11-05 04:25:19 +00:00
committed by copyberry
parent 5c0b1ac71f
commit 529d965d80
17 changed files with 523 additions and 89 deletions
+69 -14
View File
@@ -15,9 +15,11 @@ 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)]
@@ -124,6 +126,18 @@ pub struct ExecServerCommand {
)]
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,
@@ -207,30 +221,55 @@ impl ExecServerCommand {
.registration_bearer_stdin
.then(codex_exec_server::registration_auth_from_stdin)
.transpose()?;
let config =
load_exec_server_config(root_config_overrides, strict_config, enable_cloud_config)
.await?;
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 mut parent_lifetime = if self.exit_on_stdin_close {
exec_server_telemetry::ParentLifetime::StdinPipe
} else {
exec_server_telemetry::ParentLifetime::Independent
};
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 =
exec_server_telemetry::ParentLifetime::MonitoredStdin(parent_closed);
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 {
@@ -264,6 +303,8 @@ impl ExecServerCommand {
)?;
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;
@@ -312,9 +353,13 @@ impl ExecServerCommand {
}
result
} else {
let config_result =
load_exec_server_config(root_config_overrides, strict_config, enable_cloud_config)
.await;
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 {
@@ -346,7 +391,7 @@ impl ExecServerCommand {
self.request_dispatch_mode,
websocket_auth,
),
exec_server_telemetry::ParentLifetime::Independent,
ParentLifetime::Independent,
exec_server_telemetry::ShutdownBehavior::Immediate,
);
run.await.map_err(anyhow::Error::from_boxed)
@@ -363,6 +408,16 @@ impl ExecServerCommand {
}
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");