Files
codex/codex-rs/exec-server/src/remote/auth.rs
T
aagrawal-oai 529d965d80 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
2026-11-05 04:25:19 +00:00

100 lines
3.8 KiB
Rust

//! Sends registry requests with current credentials and at most one retry after a 401.
//! Transport failures and other HTTP errors are never replayed here.
//! A deadline before the first transport send remains a retryable unsent request.
use codex_http_client::HttpResponse;
use codex_http_client::Request;
use http::HeaderValue;
use http::StatusCode;
use tokio::time::Instant;
use tokio::time::timeout_at;
use super::EnvironmentRegistryClient;
use super::RegistryAuthMode;
use super::current_trace_context_headers;
use crate::ExecServerError;
pub(super) fn registry_auth_error(error: codex_api::AuthError) -> ExecServerError {
match error {
codex_api::AuthError::Transient(_) => ExecServerError::RequestNotSent(
"environment registry credential resolution is temporarily unavailable".to_string(),
),
error => super::direct::direct_auth_error(error),
}
}
impl EnvironmentRegistryClient {
pub(super) async fn send_authenticated(
&self,
request: Request,
deadline: Option<Instant>,
) -> Result<(HttpResponse, Option<HeaderValue>), ExecServerError> {
let request = request
.into_prepared()
.map_err(ExecServerError::EnvironmentRegistryConfig)?;
let mut sent = false;
let send = async {
let mut retried = false;
loop {
let (authenticated, credential) = if self.auth_mode != RegistryAuthMode::Legacy {
let (headers, credential) = self.resolve_auth().await?;
let mut authenticated = request.clone();
authenticated.headers.extend(headers);
(authenticated, credential)
} else {
(
self.auth_provider
.apply_auth(request.clone())
.await
.map_err(registry_auth_error)?,
None,
)
};
let prepared = authenticated
.prepare_body_for_send()
.map_err(ExecServerError::EnvironmentRegistryConfig)?;
let rejected_headers = prepared.headers.clone();
let mut outgoing = self
.http_for_auth()
.request(authenticated.method, authenticated.url)
.headers(prepared.headers)
.headers(current_trace_context_headers())
.body(prepared.body.unwrap_or_default());
if let Some(timeout) = authenticated.timeout {
outgoing = outgoing.timeout(timeout);
}
sent = true;
let response = outgoing.send().await?;
if !retried
&& response.status() == StatusCode::UNAUTHORIZED
&& self
.auth_provider
.refresh_after_unauthorized(rejected_headers)
.await
{
retried = true;
continue;
}
return Ok((response, credential));
}
};
if let Some(deadline) = deadline {
match timeout_at(deadline, send).await {
Ok(result) => result,
Err(_) if !sent => Err(ExecServerError::RequestNotSent(
"environment registry credential resolution timed out".to_string(),
)),
Err(_) => Err(ExecServerError::EnvironmentRegistryRequest(
codex_http_client::RouteAwareRequestError::Timeout,
)),
}
} else {
send.await
}
}
}
#[cfg(test)]
#[path = "auth_tests.rs"]
mod tests;