Add ephemeral runtime registration and signed assertions (#55402)

## What changed

Extend `codex-runtime-agent-instance` with a registration client and an in-memory assertion signer. Pending registrations retain their signing key and idempotency key across caller-driven retries; registered runtimes generate fresh versioned assertions with a timestamp and unique ID.

Bound registration requests and responses, reject unsafe endpoints, disable redirects and request logging, and preserve network policy errors. Report server rejections using only the status and recognized error codes.

## Testing

Add HTTP-backed tests for retry stability, assertion signatures and freshness, policy revocation, endpoint and response validation, redirect refusal, and error redaction.

GitOrigin-RevId: a6dab0900459dfb75f72652a0a2ee1e4b65ca322
This commit is contained in:
cooper-oai
2026-10-19 23:01:54 +00:00
committed by copyberry
parent 431185b78b
commit 79a19cbb30
6 changed files with 564 additions and 3 deletions
@@ -0,0 +1,160 @@
//! Direct composite bootstrap. Credentials are sent only in the body, never through redirects.
use crate::RuntimeAgentInstance;
use crate::RuntimeRegistration;
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<RuntimeAgentInstance, 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(RuntimeAgentInstance {
info,
signing_key: registration.signing_key.clone(),
})
}
}
/// Errors intentionally omit raw transport details and server messages, which can contain secrets.
#[derive(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;