mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user