mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
e7b6e0d859
commit
c274a83f8b
@@ -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;
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user