Files
codex/codex-rs/runtime-agent-instance/src/registration.rs
T
cooper-oai 091008729d Share runtime authentication across requests and credential refreshes (#55413)
## What changed

Add `RuntimeAuthManager` to share one registered runtime per credential source and return request-scoped `RuntimeAuth` values that pair the runtime with its credentials and HTTP client factory.

- Preserve registration keys and idempotency IDs across retryable failures and refreshes that retain the credential source.
- Replace the runtime when the credential source changes, preserve existing request snapshots, and reject late registration responses for a previous source.
- Use bounded credential recovery for registration `401` responses and cache `403` denials until the credential source changes.
- Expose authenticated actor and agent host information on `RuntimeAgentInstance`.

## Testing

Add tests for concurrent registration sharing, retry identity preservation, network policy recovery, credential refresh and replacement, bounded recovery, cached denials, and credential changes during registration.

GitOrigin-RevId: ea1e6e10a5939e8c3f7c0c317ccf8a83522c635f
2026-10-20 00:48:09 +00:00

158 lines
5.2 KiB
Rust

//! Direct composite bootstrap. Credentials are sent only in the body, never through redirects.
use crate::RuntimeRegistration;
use crate::RuntimeRegistrationResponse;
use codex_http_client::ClientRouteClass;
use codex_http_client::HttpClientFactory;
use codex_http_client::HttpError;
use codex_http_client::NetworkPolicyDenied;
use codex_http_client::RouteAwareClientPool;
use codex_http_client::RouteAwareRequestError;
use http::StatusCode;
use serde::Deserialize;
use std::time::Duration;
use url::Url;
pub const DEFAULT_REGISTRATION_URL: &str = "https://auth.openai.com/agent/register";
const MAX_BODY_BYTES: usize = 64 * 1024;
const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
/// Makes one bounded attempt. Callers reuse the pending registration when retrying.
pub struct RuntimeRegistrationClient {
http: RouteAwareClientPool,
url: Url,
}
impl RuntimeRegistrationClient {
pub fn new(factory: HttpClientFactory, url: Url) -> Result<Self, RegistrationError> {
let loopback = url.host_str().is_some_and(|host| {
host == "localhost"
|| host == "[::1]"
|| host
.parse::<std::net::IpAddr>()
.is_ok_and(|ip| ip.is_loopback())
});
if !(url.scheme() == "https" || (url.scheme() == "http" && loopback))
|| !url.username().is_empty()
|| url.password().is_some()
|| url.query().is_some()
|| url.fragment().is_some()
{
return Err(RegistrationError::InvalidEndpoint);
}
Ok(Self {
http: RouteAwareClientPool::with_chatgpt_cloudflare_cookies_without_redirects_or_request_logging(
factory,
ClientRouteClass::Auth,
),
url,
})
}
pub async fn register(
&self,
registration: &RuntimeRegistration,
) -> Result<RuntimeRegistrationResponse, RegistrationError> {
let body = serde_json::to_vec(&registration.request)
.map_err(|_| RegistrationError::InvalidRequest)?;
if body.len() > MAX_BODY_BYTES {
return Err(RegistrationError::InvalidRequest);
}
let mut response = self
.http
.post(self.url.as_str())
.header("Content-Type", "application/json")
.header(
"Idempotency-Key",
registration.initialization_id.to_string(),
)
.timeout(REQUEST_TIMEOUT)
.body(body)
.send()
.await
.map_err(|error| match error {
RouteAwareRequestError::Policy(denied) => RegistrationError::Policy(denied),
_ => RegistrationError::Transport,
})?;
let status = response.status();
let mut body = Vec::new();
while let Some(chunk) = response.chunk().await.map_err(|error| match error {
HttpError::Policy(denied) => RegistrationError::Policy(denied),
_ => RegistrationError::Transport,
})? {
if chunk.len() > MAX_BODY_BYTES - body.len() {
return Err(RegistrationError::InvalidResponse);
}
body.extend_from_slice(&chunk);
}
if status != StatusCode::OK {
let code = serde_json::from_slice::<ErrorEnvelope>(&body)
.ok()
.map(|e| e.error.code);
return Err(RegistrationError::Rejected { status, code });
}
let info: crate::RuntimeRegistrationResponse =
serde_json::from_slice(&body).map_err(|_| RegistrationError::InvalidResponse)?;
if info.runtime_agent_instance_id.is_empty() || info.agent_host_id.is_empty() {
return Err(RegistrationError::InvalidResponse);
}
Ok(info)
}
}
/// Errors intentionally omit raw transport details and server messages, which can contain secrets.
#[derive(Clone, Debug, thiserror::Error)]
pub enum RegistrationError {
#[error(
"runtime registration requires a secure endpoint without credentials or query parameters"
)]
InvalidEndpoint,
#[error("invalid runtime registration request")]
InvalidRequest,
#[error(transparent)]
Policy(#[from] NetworkPolicyDenied),
#[error("runtime registration transport failed")]
Transport,
#[error("invalid runtime registration response")]
InvalidResponse,
#[error("runtime registration rejected ({status}, {code:?})")]
Rejected {
status: StatusCode,
code: Option<RegistrationErrorCode>,
},
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RegistrationErrorCode {
TokenExpired,
RuntimeActorInvalid,
RuntimeInstanceRevoked,
RuntimeActorSourceInactive,
RuntimeInputInvalid,
RuntimeIdempotencyKeyInvalid,
RuntimeActorConflict,
RuntimeHostConflict,
RuntimeRegistrationConflict,
RuntimeInputTooLarge,
RuntimeRateLimited,
RuntimeVerificationUnavailable,
NotFound,
#[serde(other)]
Unknown,
}
#[derive(Deserialize)]
struct ErrorEnvelope {
error: ErrorDetail,
}
#[derive(Deserialize)]
struct ErrorDetail {
code: RegistrationErrorCode,
}
#[cfg(test)]
#[path = "registration_tests.rs"]
mod tests;