feat(exec-server): add Noise rendezvous environment (#28774)

## Why

Codex can run a remote exec server through the Noise relay, but the
normal
environment-manager path could not establish an
environment-registry-backed
harness connection. Signed rendezvous URLs and harness authorizations
are
short-lived, so reconnects must fetch a fresh bundle instead of
retaining
stale connection credentials. A stalled registry request must also fail
within
the regular remote connection deadline, without exposing these
credentials in
debug logs.

Issue: N/A (internal environment-service integration).

## What Changed

- Add environment-manager configuration for a registry-backed Noise
rendezvous
  environment.
- Request a fresh bundle from
`/cloud/environment/{environment_id}/connect` for every physical harness
  connection, using the existing 10-second remote connection timeout.
- Share the Environment Registry register, connect, and validate wire
payloads
  through `codex-exec-server` and `codex-core-api`.
- Redact the signed rendezvous URL and harness authorization from the
public
  connect response's `Debug` output.
- Add focused coverage for registry bundle retrieval, stalled requests,
and
  credential redaction.
This commit is contained in:
Anton Panasenko
2026-06-17 17:20:53 -07:00
committed by GitHub
Unverified
parent e7b6e0d859
commit c274a83f8b
7 changed files with 433 additions and 0 deletions
+2
View File
@@ -48,6 +48,8 @@ pub use codex_core::resolve_installation_id;
pub use codex_core::skills::SkillsService;
pub use codex_core::thread_store_from_config;
pub use codex_exec_server::EnvironmentManager;
pub use codex_exec_server::EnvironmentRegistryConnectRequest;
pub use codex_exec_server::EnvironmentRegistryConnectResponse;
pub use codex_exec_server::EnvironmentRegistryHarnessKeyValidationRequest;
pub use codex_exec_server::EnvironmentRegistryHarnessKeyValidationResponse;
pub use codex_exec_server::EnvironmentRegistryRegistrationRequest;
+1
View File
@@ -28,6 +28,7 @@ codex-utils-path-uri = { workspace = true }
codex-utils-pty = { workspace = true }
codex-utils-rustls-provider = { workspace = true }
futures = { workspace = true }
http = { workspace = true }
reqwest = { workspace = true, features = ["json", "rustls-tls", "stream"] }
prost = "0.14.3"
serde = { workspace = true, features = ["derive"] }
+108
View File
@@ -25,11 +25,19 @@ use crate::local_process::LocalProcess;
use crate::process::ExecBackend;
use crate::protocol::EnvironmentInfo;
use crate::protocol::ShellInfo;
use crate::remote::NoiseRendezvousEnvironmentConfig;
use crate::remote_file_system::RemoteFileSystem;
use crate::remote_process::RemoteProcess;
use codex_shell_command::shell_detect::DetectedShell;
pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL";
pub const CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR: &str =
"CODEX_EXEC_SERVER_NOISE_REGISTRY_URL";
pub const CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR: &str =
"CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID";
pub const CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR: &str = "CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN";
pub const CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR: &str =
"CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID";
/// Owns the execution/filesystem environments available to the Codex runtime.
///
@@ -98,6 +106,9 @@ impl EnvironmentManager {
codex_home: impl AsRef<std::path::Path>,
local_runtime_paths: Option<ExecServerRuntimePaths>,
) -> Result<Self, ExecServerError> {
if let Some(config) = noise_environment_config_from_env()? {
return Self::from_noise_environment_config(config, local_runtime_paths);
}
let provider = environment_provider_from_codex_home(codex_home.as_ref())?;
Self::from_snapshot(provider.snapshot().await?, local_runtime_paths)
}
@@ -107,6 +118,9 @@ impl EnvironmentManager {
pub async fn from_env(
local_runtime_paths: Option<ExecServerRuntimePaths>,
) -> Result<Self, ExecServerError> {
if let Some(config) = noise_environment_config_from_env()? {
return Self::from_noise_environment_config(config, local_runtime_paths);
}
let provider = DefaultEnvironmentProvider::from_env();
Self::from_snapshot(provider.snapshot().await?, local_runtime_paths)
}
@@ -122,6 +136,23 @@ impl EnvironmentManager {
}
}
fn from_noise_environment_config(
config: NoiseRendezvousEnvironmentConfig,
local_runtime_paths: Option<ExecServerRuntimePaths>,
) -> Result<Self, ExecServerError> {
let manager = Self {
default_environment: Some(REMOTE_ENVIRONMENT_ID.to_string()),
environments: RwLock::new(HashMap::new()),
local_environment: None,
local_runtime_paths,
};
manager.upsert_noise_environment(
REMOTE_ENVIRONMENT_ID.to_string(),
config.connect_provider(),
)?;
Ok(manager)
}
/// Builds a test-only manager that keeps the provider default while also
/// allowing tests to select the local environment explicitly.
pub async fn create_for_tests_with_local(
@@ -317,6 +348,53 @@ impl EnvironmentManager {
}
}
fn noise_environment_config_from_env()
-> Result<Option<NoiseRendezvousEnvironmentConfig>, ExecServerError> {
noise_environment_config_from_values(
optional_environment_value(CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR),
optional_environment_value(CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR),
optional_environment_value(CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR),
optional_environment_value(CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR),
)
}
fn noise_environment_config_from_values(
registry_url: Option<String>,
environment_id: Option<String>,
auth_token: Option<String>,
chatgpt_account_id: Option<String>,
) -> Result<Option<NoiseRendezvousEnvironmentConfig>, ExecServerError> {
let (registry_url, environment_id, auth_token) =
match (registry_url, environment_id, auth_token) {
(None, None, None) => return Ok(None),
(Some(registry_url), Some(environment_id), Some(auth_token)) => {
(registry_url, environment_id, auth_token)
}
_ => {
return Err(ExecServerError::EnvironmentRegistryConfig(format!(
"Noise environment requires {CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR}, \
{CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR}, and \
{CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR}"
)));
}
};
let config = NoiseRendezvousEnvironmentConfig::new(
registry_url,
environment_id,
auth_token,
chatgpt_account_id,
)?;
Ok(Some(config))
}
fn optional_environment_value(name: &str) -> Option<String> {
std::env::var(name)
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
/// Concrete execution/filesystem environment selected for a session.
///
/// This bundles the selected backend metadata together with the local runtime
@@ -528,6 +606,7 @@ mod tests {
use super::EnvironmentManager;
use super::LOCAL_ENVIRONMENT_ID;
use super::REMOTE_ENVIRONMENT_ID;
use super::noise_environment_config_from_values;
use crate::ExecServerRuntimePaths;
use crate::ProcessId;
use crate::environment_provider::EnvironmentDefault;
@@ -547,6 +626,35 @@ mod tests {
assert!(manager.try_local_environment().is_none());
}
#[test]
fn noise_environment_config_selects_remote_as_default() {
let config = noise_environment_config_from_values(
Some("http://registry.example/api".to_string()),
Some("environment-requested".to_string()),
Some("registry-token".to_string()),
Some("workspace-123".to_string()),
)
.expect("parse noise environment configuration")
.expect("noise environment configuration");
let manager = EnvironmentManager::from_noise_environment_config(
config, /*local_runtime_paths*/ None,
)
.expect("build environment manager");
assert_eq!(
manager.default_environment_id(),
Some(REMOTE_ENVIRONMENT_ID)
);
assert!(
manager
.default_environment()
.expect("remote environment")
.is_remote()
);
assert_local_environment_unavailable(&manager);
}
#[tokio::test]
async fn create_local_environment_does_not_connect() {
let environment = Environment::create(/*exec_server_url*/ None, test_runtime_paths())
@@ -19,6 +19,36 @@ pub struct EnvironmentRegistryRegistrationResponse {
pub executor_registration_id: String,
}
/// Request body for connecting a harness key with the environment registry.
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
pub struct EnvironmentRegistryConnectRequest {
pub harness_public_key: NoiseChannelPublicKey,
}
/// Environment registry response returned after connecting a harness key.
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
pub struct EnvironmentRegistryConnectResponse {
pub environment_id: String,
pub url: String,
pub security_profile: String,
pub executor_registration_id: String,
pub executor_public_key: NoiseChannelPublicKey,
pub harness_key_authorization: String,
}
impl std::fmt::Debug for EnvironmentRegistryConnectResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EnvironmentRegistryConnectResponse")
.field("environment_id", &self.environment_id)
.field("url", &"<redacted>")
.field("security_profile", &self.security_profile)
.field("executor_registration_id", &self.executor_registration_id)
.field("executor_public_key", &self.executor_public_key)
.field("harness_key_authorization", &"<redacted>")
.finish()
}
}
/// Request body for authorizing a harness key with the environment registry.
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
pub struct EnvironmentRegistryHarnessKeyValidationRequest {
@@ -32,3 +62,7 @@ pub struct EnvironmentRegistryHarnessKeyValidationRequest {
pub struct EnvironmentRegistryHarnessKeyValidationResponse {
pub valid: bool,
}
#[cfg(test)]
#[path = "environment_registry_tests.rs"]
mod tests;
@@ -0,0 +1,22 @@
use crate::EnvironmentRegistryConnectResponse;
use crate::NoiseChannelIdentity;
#[test]
fn connect_response_debug_redacts_authorizations() {
let response = EnvironmentRegistryConnectResponse {
environment_id: "environment-1".to_string(),
url: "wss://rendezvous.test?sig=secret-url-authorization".to_string(),
security_profile: "noise_hybrid_ik_v1".to_string(),
executor_registration_id: "registration-1".to_string(),
executor_public_key: NoiseChannelIdentity::generate()
.expect("identity")
.public_key(),
harness_key_authorization: "secret-harness-authorization".to_string(),
};
let debug = format!("{response:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("secret-url-authorization"));
assert!(!debug.contains("secret-harness-authorization"));
}
+2
View File
@@ -57,6 +57,8 @@ pub use environment::REMOTE_ENVIRONMENT_ID;
pub use environment_provider::DefaultEnvironmentProvider;
pub use environment_provider::EnvironmentProvider;
pub use environment_provider::EnvironmentProviderFuture;
pub use environment_registry::EnvironmentRegistryConnectRequest;
pub use environment_registry::EnvironmentRegistryConnectResponse;
pub use environment_registry::EnvironmentRegistryHarnessKeyValidationRequest;
pub use environment_registry::EnvironmentRegistryHarnessKeyValidationResponse;
pub use environment_registry::EnvironmentRegistryRegistrationRequest;
+264
View File
@@ -1,6 +1,12 @@
use std::sync::Arc;
use std::time::Duration;
use codex_api::AuthProvider;
use codex_api::SharedAuthProvider;
use futures::FutureExt;
use http::HeaderMap;
use http::HeaderName;
use http::HeaderValue;
use reqwest::StatusCode;
use serde::Deserialize;
use tokio::time::sleep;
@@ -11,6 +17,8 @@ use tracing::warn;
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
use crate::EnvironmentRegistryConnectRequest;
use crate::EnvironmentRegistryConnectResponse;
use crate::EnvironmentRegistryHarnessKeyValidationRequest;
use crate::EnvironmentRegistryHarnessKeyValidationResponse;
use crate::EnvironmentRegistryRegistrationRequest;
@@ -19,6 +27,9 @@ use crate::ExecServerError;
use crate::ExecServerRuntimePaths;
use crate::NoiseChannelIdentity;
use crate::NoiseChannelPublicKey;
use crate::NoiseRendezvousConnectBundle;
use crate::NoiseRendezvousConnectProvider;
use crate::client_api::DEFAULT_REMOTE_EXEC_SERVER_CONNECT_TIMEOUT;
use crate::noise_relay::noise_relay_websocket_config;
use crate::relay::HarnessKeyValidator;
use crate::relay::run_multiplexed_environment;
@@ -32,6 +43,7 @@ struct EnvironmentRegistryClient {
base_url: String,
auth_provider: SharedAuthProvider,
http: reqwest::Client,
connect_timeout: Duration,
}
impl std::fmt::Debug for EnvironmentRegistryClient {
@@ -52,6 +64,7 @@ impl EnvironmentRegistryClient {
http: reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()?,
connect_timeout: DEFAULT_REMOTE_EXEC_SERVER_CONNECT_TIMEOUT,
})
}
@@ -102,6 +115,53 @@ impl EnvironmentRegistryClient {
Ok(response)
}
/// Authorize one Noise harness key and obtain the full rendezvous bundle.
async fn connect_environment(
&self,
environment_id: &str,
harness_public_key: NoiseChannelPublicKey,
) -> Result<NoiseRendezvousConnectBundle, ExecServerError> {
let response = self
.http
.post(endpoint_url(
&self.base_url,
&format!("/cloud/environment/{environment_id}/connect"),
))
.headers(self.auth_provider.to_auth_headers())
.json(&EnvironmentRegistryConnectRequest { harness_public_key })
.timeout(self.connect_timeout)
.send()
.await?;
let response: EnvironmentRegistryConnectResponse =
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
)));
}
if response.url.trim().is_empty()
|| response.executor_registration_id.trim().is_empty()
|| response.harness_key_authorization.trim().is_empty()
{
return Err(ExecServerError::Protocol(
"environment registry returned incomplete Noise connection data".to_string(),
));
}
Ok(NoiseRendezvousConnectBundle {
websocket_url: response.url,
environment_id: response.environment_id,
executor_registration_id: response.executor_registration_id,
executor_public_key: response.executor_public_key,
harness_key_authorization: response.harness_key_authorization,
})
}
async fn parse_json_response<R>(
&self,
response: reqwest::Response,
@@ -182,6 +242,130 @@ impl HarnessKeyValidator for RegistryHarnessKeyValidator {
}
}
/// Noise connection configuration for a Codex harness.
///
/// The provider holds the authenticated registry client so every reconnect
/// receives fresh URL and harness-key authorization material.
#[derive(Clone)]
pub(crate) struct NoiseRendezvousEnvironmentConfig {
provider: Arc<dyn NoiseRendezvousConnectProvider>,
}
impl std::fmt::Debug for NoiseRendezvousEnvironmentConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NoiseRendezvousEnvironmentConfig")
.field("provider", &"<redacted>")
.finish()
}
}
impl NoiseRendezvousEnvironmentConfig {
pub(crate) fn new(
base_url: String,
environment_id: String,
bearer_token: String,
chatgpt_account_id: Option<String>,
) -> Result<Self, ExecServerError> {
let environment_id = normalize_environment_id(environment_id)?;
let auth_provider = static_bearer_auth_provider(bearer_token, chatgpt_account_id)?;
let client = EnvironmentRegistryClient::new(base_url, auth_provider)?;
Ok(Self {
provider: Arc::new(EnvironmentRegistryNoiseConnectProvider {
client,
environment_id,
}),
})
}
pub(crate) fn connect_provider(&self) -> Arc<dyn NoiseRendezvousConnectProvider> {
Arc::clone(&self.provider)
}
}
#[derive(Clone, Debug)]
struct EnvironmentRegistryNoiseConnectProvider {
client: EnvironmentRegistryClient,
environment_id: String,
}
impl NoiseRendezvousConnectProvider for EnvironmentRegistryNoiseConnectProvider {
fn connect_bundle(
&self,
harness_public_key: NoiseChannelPublicKey,
) -> futures::future::BoxFuture<'_, Result<NoiseRendezvousConnectBundle, ExecServerError>> {
async move {
self.client
.connect_environment(&self.environment_id, harness_public_key)
.await
}
.boxed()
}
}
#[derive(Clone)]
struct StaticBearerAuthProvider {
authorization: HeaderValue,
chatgpt_account_id: Option<HeaderValue>,
}
impl std::fmt::Debug for StaticBearerAuthProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StaticBearerAuthProvider")
.field("authorization", &"<redacted>")
.field(
"chatgpt_account_id",
&self.chatgpt_account_id.as_ref().map(|_| "<redacted>"),
)
.finish()
}
}
impl AuthProvider for StaticBearerAuthProvider {
fn add_auth_headers(&self, headers: &mut HeaderMap) {
headers.insert(http::header::AUTHORIZATION, self.authorization.clone());
if let Some(chatgpt_account_id) = &self.chatgpt_account_id {
headers.insert(
HeaderName::from_static("chatgpt-account-id"),
chatgpt_account_id.clone(),
);
}
}
}
fn static_bearer_auth_provider(
bearer_token: String,
chatgpt_account_id: Option<String>,
) -> Result<SharedAuthProvider, ExecServerError> {
let bearer_token = bearer_token.trim();
if bearer_token.is_empty() {
return Err(ExecServerError::EnvironmentRegistryConfig(
"environment registry bearer token is required".to_string(),
));
}
let authorization =
HeaderValue::try_from(format!("Bearer {bearer_token}")).map_err(|error| {
ExecServerError::EnvironmentRegistryConfig(format!(
"environment registry bearer token is not a valid HTTP header: {error}"
))
})?;
let chatgpt_account_id = chatgpt_account_id
.as_deref()
.map(str::trim)
.filter(|account_id| !account_id.is_empty())
.map(|account_id| {
HeaderValue::try_from(account_id).map_err(|error| {
ExecServerError::EnvironmentRegistryConfig(format!(
"ChatGPT account id is not a valid HTTP header: {error}"
))
})
})
.transpose()?;
Ok(Arc::new(StaticBearerAuthProvider {
authorization,
chatgpt_account_id,
}))
}
/// Configuration for registering an exec-server for remote use.
#[derive(Clone)]
pub struct RemoteEnvironmentConfig {
@@ -459,6 +643,86 @@ mod tests {
);
}
#[tokio::test]
async fn noise_connect_provider_requests_and_validates_a_full_bundle() {
let server = MockServer::start().await;
let harness_public_key = NoiseChannelIdentity::generate()
.expect("identity")
.public_key();
let executor_public_key = NoiseChannelIdentity::generate()
.expect("identity")
.public_key();
Mock::given(method("POST"))
.and(path("/cloud/environment/environment-requested/connect"))
.and(header("authorization", "Bearer registry-token"))
.and(header("chatgpt-account-id", "workspace-123"))
.and(body_partial_json(serde_json::json!({
"harness_public_key": harness_public_key.clone(),
})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"environment_id": "environment-requested",
"url": "wss://rendezvous.test/cloud-agent/default/ws/environment/environment-requested?role=harness&sig=abc",
"security_profile": NOISE_RELAY_SECURITY_PROFILE,
"executor_registration_id": "registration-1",
"executor_public_key": executor_public_key.clone(),
"harness_key_authorization": "authorization-1",
})))
.mount(&server)
.await;
let config = NoiseRendezvousEnvironmentConfig::new(
server.uri(),
"environment-requested".to_string(),
"registry-token".to_string(),
Some("workspace-123".to_string()),
)
.expect("noise configuration");
let bundle = config
.connect_provider()
.connect_bundle(harness_public_key)
.await
.expect("Noise connect bundle");
assert_eq!(
bundle.websocket_url,
"wss://rendezvous.test/cloud-agent/default/ws/environment/environment-requested?role=harness&sig=abc"
);
assert_eq!(bundle.environment_id, "environment-requested");
assert_eq!(bundle.executor_registration_id, "registration-1");
assert_eq!(bundle.executor_public_key, executor_public_key);
assert_eq!(bundle.harness_key_authorization, "authorization-1");
}
#[tokio::test]
async fn connect_environment_times_out_when_registry_stalls() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/cloud/environment/environment-requested/connect"))
.respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(1)))
.mount(&server)
.await;
let mut client =
EnvironmentRegistryClient::new(server.uri(), static_registry_auth_provider())
.expect("client");
client.connect_timeout = Duration::from_millis(50);
let harness_public_key = NoiseChannelIdentity::generate()
.expect("identity")
.public_key();
let error = match client
.connect_environment("environment-requested", harness_public_key)
.await
{
Ok(_) => panic!("stalled connect response should time out"),
Err(error) => error,
};
assert!(matches!(
error,
ExecServerError::EnvironmentRegistryRequest(error) if error.is_timeout()
));
}
#[tokio::test]
async fn register_environment_does_not_follow_redirects_with_auth_headers() {
let server = MockServer::start().await;