exec-server: default remote transport to Noise (#26245)

## Why

The transport in
[openai/codex#26242](https://github.com/openai/codex/pull/26242) needs
to be used by every remote orchestrator-to-executor connection before
JSON-RPC traffic starts.

## Changes

- Generates one executor Noise identity when remote exec-server starts
and registers its public key.
- Creates a harness identity for each physical remote environment
connection.
- Fetches a fresh registry bundle before connecting and validates the
authenticated harness key before completing the executor handshake.
- Multiplexes encrypted logical streams over the existing executor
WebSocket.
- Adds bounded stream, handshake-failure, and reassembly state.
- Adds safe lifecycle diagnostics without logging keys, authorizations,
plaintext, or ciphertext.
- Covers reconnects, replay rejection, validation failure, framing
limits, and encrypted JSON-RPC tool traffic.

## Stack

1. [openai/codex#26242](https://github.com/openai/codex/pull/26242):
Noise channel and relay transport
2. **[openai/codex#26245](https://github.com/openai/codex/pull/26245)**:
remote registration and runtime activation

## Verification

- `just test -p codex-exec-server`
- `just fix -p codex-exec-server`
- `just bazel-lock-check`
- `cargo shear`

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
viyatb-oai
2026-06-15 17:39:00 -07:00
committed by GitHub
co-authored by Codex
parent 1fe89de576
commit 6e50b22e55
14 changed files with 1902 additions and 491 deletions
+201 -26
View File
@@ -3,18 +3,26 @@ use std::time::Duration;
use codex_api::SharedAuthProvider;
use reqwest::StatusCode;
use serde::Deserialize;
use serde::Serialize;
use tokio::time::sleep;
use tokio_tungstenite::connect_async;
use tokio_tungstenite::connect_async_with_config;
use tracing::debug;
use tracing::info;
use tracing::warn;
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
use crate::ExecServerError;
use crate::ExecServerRuntimePaths;
use crate::NoiseChannelIdentity;
use crate::NoiseChannelPublicKey;
use crate::noise_relay::noise_relay_websocket_config;
use crate::relay::HarnessKeyValidator;
use crate::relay::run_multiplexed_environment;
use crate::server::ConnectionProcessor;
const ERROR_BODY_PREVIEW_BYTES: usize = 4096;
const NOISE_RELAY_SECURITY_PROFILE: &str = "noise_hybrid_ik_v1";
#[derive(Clone)]
struct EnvironmentRegistryClient {
@@ -44,9 +52,12 @@ impl EnvironmentRegistryClient {
})
}
/// Register the executor public key and obtain the rendezvous allocation.
/// The returned registration ID is included in each stream's Noise prologue.
async fn register_environment(
&self,
environment_id: &str,
executor_public_key: &NoiseChannelPublicKey,
) -> Result<EnvironmentRegistryRegistrationResponse, ExecServerError> {
let response = self
.http
@@ -55,9 +66,37 @@ impl EnvironmentRegistryClient {
&format!("/cloud/environment/{environment_id}/register"),
))
.headers(self.auth_provider.to_auth_headers())
.json(&EnvironmentRegistryRegistrationRequest {
security_profile: NOISE_RELAY_SECURITY_PROFILE,
executor_public_key,
})
.send()
.await?;
self.parse_json_response(response).await
let response: EnvironmentRegistryRegistrationResponse =
self.parse_json_response(response).await?;
if response.environment_id != environment_id {
return Err(ExecServerError::Protocol(
"environment registry returned a different environment id".to_string(),
));
}
if response.security_profile != NOISE_RELAY_SECURITY_PROFILE {
return Err(ExecServerError::Protocol(format!(
"environment registry returned unsupported security profile `{}`",
response.security_profile
)));
}
info!(
noise_event = "registration",
noise_outcome = "ok",
security_profile = NOISE_RELAY_SECURITY_PROFILE,
"Noise executor registration completed"
);
debug!(
environment_id = response.environment_id,
executor_registration_id = response.executor_registration_id,
"Noise executor registration details"
);
Ok(response)
}
async fn parse_json_response<R>(
@@ -81,10 +120,89 @@ impl EnvironmentRegistryClient {
}
}
#[derive(Serialize)]
struct EnvironmentRegistryRegistrationRequest<'a> {
security_profile: &'static str,
executor_public_key: &'a NoiseChannelPublicKey,
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
struct EnvironmentRegistryRegistrationResponse {
environment_id: String,
url: String,
security_profile: String,
executor_registration_id: String,
}
#[derive(Serialize)]
struct EnvironmentRegistryHarnessKeyValidationRequest<'a> {
executor_registration_id: &'a str,
harness_public_key: &'a NoiseChannelPublicKey,
harness_key_authorization: &'a str,
}
#[derive(Deserialize)]
struct EnvironmentRegistryHarnessKeyValidationResponse {
valid: bool,
}
#[derive(Clone)]
struct RegistryHarnessKeyValidator {
client: EnvironmentRegistryClient,
environment_id: String,
executor_registration_id: String,
}
impl HarnessKeyValidator for RegistryHarnessKeyValidator {
/// Authorize the harness key recovered from the first IK message.
/// Noise proves key possession; the registry decides whether that key may use
/// this executor. The authorization token and public key are checked together.
async fn validate_harness_key(
&self,
harness_public_key: &NoiseChannelPublicKey,
authorization: &str,
) -> Result<(), ExecServerError> {
let environment_id = &self.environment_id;
let response = self
.client
.http
.post(endpoint_url(
&self.client.base_url,
&format!("/cloud/environment/{environment_id}/validate"),
))
.headers(self.client.auth_provider.to_auth_headers())
.json(&EnvironmentRegistryHarnessKeyValidationRequest {
executor_registration_id: &self.executor_registration_id,
harness_public_key,
harness_key_authorization: authorization,
})
.send()
.await?;
let status = response.status();
if !status.is_success() {
// The request contains the short-lived authorization. Do not include
// a response body that might echo it in logs or error chains.
if matches!(status, StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN) {
return Err(ExecServerError::EnvironmentRegistryAuth(format!(
"environment registry harness key validation authentication failed ({status})"
)));
}
return Err(ExecServerError::EnvironmentRegistryHttp {
status,
code: None,
message: "environment registry harness key validation failed".to_string(),
});
}
let response = response
.json::<EnvironmentRegistryHarnessKeyValidationResponse>()
.await?;
if !response.valid {
return Err(ExecServerError::Protocol(
"environment registry rejected Noise relay harness key".to_string(),
));
}
Ok(())
}
}
/// Configuration for registering an exec-server for remote use.
@@ -123,8 +241,12 @@ impl RemoteEnvironmentConfig {
}
}
/// Register an exec-server for remote use and serve requests over the returned
/// rendezvous websocket.
/// Register an exec-server for remote use and serve requests over Noise.
///
/// The executor identity is generated once per process and reused across
/// reconnects. The registration and rendezvous URL are also reused until
/// rendezvous rejects the URL, at which point the next attempt registers again.
/// The websocket carries cleartext routing metadata and encrypted payloads.
pub async fn run_remote_environment(
config: RemoteEnvironmentConfig,
runtime_paths: ExecServerRuntimePaths,
@@ -133,22 +255,62 @@ pub async fn run_remote_environment(
let client =
EnvironmentRegistryClient::new(config.base_url.clone(), config.auth_provider.clone())?;
let processor = ConnectionProcessor::new(runtime_paths);
let identity = NoiseChannelIdentity::generate().map_err(|error| {
ExecServerError::Protocol(format!("failed to generate Noise relay identity: {error}"))
})?;
let mut backoff = Duration::from_secs(1);
let mut response = client
.register_environment(&config.environment_id, &identity.public_key())
.await?;
loop {
let response = client.register_environment(&config.environment_id).await?;
eprintln!(
"codex exec-server remote environment registered with environment_id {}",
response.environment_id
);
match connect_async(response.url.as_str()).await {
match connect_async_with_config(
response.url.as_str(),
Some(noise_relay_websocket_config()),
/*disable_nagle*/ false,
)
.await
{
Ok((websocket, _)) => {
backoff = Duration::from_secs(1);
run_multiplexed_environment(websocket, processor.clone()).await;
let executor_registration_id = response.executor_registration_id.clone();
info!(
noise_event = "rendezvous_connection",
noise_outcome = "ok",
"Noise executor connected to rendezvous"
);
run_multiplexed_environment(
websocket,
processor.clone(),
response.environment_id.clone(),
executor_registration_id.clone(),
identity.clone(),
RegistryHarnessKeyValidator {
client: client.clone(),
environment_id: config.environment_id.clone(),
executor_registration_id,
},
)
.await;
}
Err(err) => {
warn!("failed to connect remote exec-server websocket: {err}");
Err(error) => {
let registration_rejected = matches!(
&error,
tokio_tungstenite::tungstenite::Error::Http(response)
if response.status().is_client_error()
);
warn!(
noise_event = "rendezvous_connection",
noise_outcome = "error",
noise_reason = "websocket_error",
"Noise executor failed to connect to rendezvous"
);
debug!(error = %error, "Noise executor rendezvous connection error");
if registration_rejected {
response = client
.register_environment(&config.environment_id, &identity.public_key())
.await?;
}
}
}
@@ -252,6 +414,7 @@ mod tests {
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::body_partial_json;
use wiremock::matchers::header;
use wiremock::matchers::method;
use wiremock::matchers::path;
@@ -281,19 +444,22 @@ mod tests {
#[tokio::test]
async fn register_environment_posts_with_auth_provider_headers() {
let server = MockServer::start().await;
let config = RemoteEnvironmentConfig::new(
server.uri(),
"environment-requested".to_string(),
static_registry_auth_provider(),
)
.expect("config");
let executor_public_key = NoiseChannelIdentity::generate()
.expect("identity")
.public_key();
Mock::given(method("POST"))
.and(path("/cloud/environment/environment-requested/register"))
.and(header("authorization", "Bearer registry-token"))
.and(header("chatgpt-account-id", "workspace-123"))
.and(body_partial_json(serde_json::json!({
"security_profile": NOISE_RELAY_SECURITY_PROFILE,
"executor_public_key": executor_public_key.clone(),
})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"environment_id": "env-1",
"url": "wss://rendezvous.test/cloud-agent/default/ws/environment/env-1?role=environment&sig=abc"
"environment_id": "environment-requested",
"url": "wss://rendezvous.test/cloud-agent/default/ws/environment/environment-requested?role=environment&sig=abc",
"security_profile": NOISE_RELAY_SECURITY_PROFILE,
"executor_registration_id": "registration-1",
})))
.mount(&server)
.await;
@@ -301,15 +467,17 @@ mod tests {
.expect("client");
let response = client
.register_environment(&config.environment_id)
.register_environment("environment-requested", &executor_public_key)
.await
.expect("register environment");
assert_eq!(
response,
EnvironmentRegistryRegistrationResponse {
environment_id: "env-1".to_string(),
url: "wss://rendezvous.test/cloud-agent/default/ws/environment/env-1?role=environment&sig=abc".to_string(),
environment_id: "environment-requested".to_string(),
url: "wss://rendezvous.test/cloud-agent/default/ws/environment/environment-requested?role=environment&sig=abc".to_string(),
security_profile: NOISE_RELAY_SECURITY_PROFILE.to_string(),
executor_registration_id: "registration-1".to_string(),
}
);
}
@@ -317,6 +485,9 @@ mod tests {
#[tokio::test]
async fn register_environment_does_not_follow_redirects_with_auth_headers() {
let server = MockServer::start().await;
let executor_public_key = NoiseChannelIdentity::generate()
.expect("identity")
.public_key();
Mock::given(method("POST"))
.and(path("/cloud/environment/environment-requested/register"))
.and(header("authorization", "Bearer registry-token"))
@@ -336,7 +507,7 @@ mod tests {
.expect("client");
let error = client
.register_environment("environment-requested")
.register_environment("environment-requested", &executor_public_key)
.await
.expect_err("redirect response should not be followed");
@@ -364,3 +535,7 @@ mod tests {
assert!(!debug.contains("workspace-123"));
}
}
#[cfg(test)]
#[path = "remote/noise_tests.rs"]
mod noise_tests;