mirror of
https://github.com/openai/codex.git
synced 2026-05-05 05:42:33 +03:00
Compare commits
4 Commits
automation
...
starr/envi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7933a9adf3 | ||
|
|
a66bb6303d | ||
|
|
900ad27d28 | ||
|
|
430fde2ab1 |
@@ -25,6 +25,8 @@ use crate::client_api::ExecServerClientConnectOptions;
|
||||
use crate::client_api::HttpClient;
|
||||
use crate::client_api::RemoteExecServerConnectArgs;
|
||||
use crate::connection::JsonRpcConnection;
|
||||
use crate::environment_provider::EnvironmentResolver;
|
||||
use crate::environment_provider::normalize_remote_exec_server_url;
|
||||
use crate::process::ExecProcessEvent;
|
||||
use crate::process::ExecProcessEventLog;
|
||||
use crate::process::ExecProcessEventReceiver;
|
||||
@@ -180,14 +182,16 @@ pub struct ExecServerClient {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct LazyRemoteExecServerClient {
|
||||
websocket_url: String,
|
||||
environment_id: String,
|
||||
resolver: Arc<dyn EnvironmentResolver>,
|
||||
client: Arc<OnceCell<ExecServerClient>>,
|
||||
}
|
||||
|
||||
impl LazyRemoteExecServerClient {
|
||||
pub(crate) fn new(websocket_url: String) -> Self {
|
||||
pub(crate) fn new(environment_id: String, resolver: Arc<dyn EnvironmentResolver>) -> Self {
|
||||
Self {
|
||||
websocket_url,
|
||||
environment_id,
|
||||
resolver,
|
||||
client: Arc::new(OnceCell::new()),
|
||||
}
|
||||
}
|
||||
@@ -195,8 +199,13 @@ impl LazyRemoteExecServerClient {
|
||||
pub(crate) async fn get(&self) -> Result<ExecServerClient, ExecServerError> {
|
||||
self.client
|
||||
.get_or_try_init(|| async {
|
||||
let resolved = self.resolver.resolve().await?;
|
||||
let websocket_url = normalize_remote_exec_server_url(
|
||||
self.environment_id.as_str(),
|
||||
resolved.exec_server_url,
|
||||
)?;
|
||||
ExecServerClient::connect_websocket(RemoteExecServerConnectArgs {
|
||||
websocket_url: self.websocket_url.clone(),
|
||||
websocket_url,
|
||||
client_name: "codex-environment".to_string(),
|
||||
connect_timeout: Duration::from_secs(5),
|
||||
initialize_timeout: Duration::from_secs(5),
|
||||
|
||||
@@ -7,6 +7,11 @@ use crate::ExecutorFileSystem;
|
||||
use crate::HttpClient;
|
||||
use crate::client::LazyRemoteExecServerClient;
|
||||
use crate::client::http_client::ReqwestHttpClient;
|
||||
use crate::environment_provider::DefaultEnvironmentProvider;
|
||||
use crate::environment_provider::EnvironmentConfiguration;
|
||||
use crate::environment_provider::EnvironmentConfigurations;
|
||||
use crate::environment_provider::EnvironmentProvider;
|
||||
use crate::environment_provider::normalize_exec_server_url;
|
||||
use crate::local_file_system::LocalFileSystem;
|
||||
use crate::local_process::LocalProcess;
|
||||
use crate::process::ExecBackend;
|
||||
@@ -17,11 +22,9 @@ pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL";
|
||||
|
||||
/// Owns the execution/filesystem environments available to the Codex runtime.
|
||||
///
|
||||
/// `EnvironmentManager` is a shared registry for concrete environments. It
|
||||
/// always creates a local environment under [`LOCAL_ENVIRONMENT_ID`]. When
|
||||
/// `CODEX_EXEC_SERVER_URL` is set to a websocket URL, it also creates a remote
|
||||
/// environment under [`REMOTE_ENVIRONMENT_ID`] and makes that the default
|
||||
/// environment. Otherwise the local environment is the default.
|
||||
/// `EnvironmentManager` is a shared registry for concrete environments. Its
|
||||
/// default constructor preserves the `CODEX_EXEC_SERVER_URL` behavior by using
|
||||
/// [`DefaultEnvironmentProvider`] to produce the environment configurations.
|
||||
///
|
||||
/// Setting `CODEX_EXEC_SERVER_URL=none` disables environment access by leaving
|
||||
/// the default environment unset while still keeping the local environment
|
||||
@@ -82,25 +85,43 @@ impl EnvironmentManager {
|
||||
exec_server_url,
|
||||
local_runtime_paths,
|
||||
} = args;
|
||||
let (exec_server_url, environment_disabled) = normalize_exec_server_url(exec_server_url);
|
||||
let provider = DefaultEnvironmentProvider::new(exec_server_url);
|
||||
Self::from_configurations(provider.environment_configurations(), local_runtime_paths)
|
||||
}
|
||||
|
||||
/// Builds a manager from a provider-supplied startup snapshot.
|
||||
pub async fn from_provider<P>(
|
||||
provider: &P,
|
||||
local_runtime_paths: ExecServerRuntimePaths,
|
||||
) -> Result<Self, ExecServerError>
|
||||
where
|
||||
P: EnvironmentProvider + ?Sized,
|
||||
{
|
||||
Ok(Self::from_configurations(
|
||||
provider.get_environments().await?,
|
||||
local_runtime_paths,
|
||||
))
|
||||
}
|
||||
|
||||
fn from_configurations(
|
||||
environment_configurations: EnvironmentConfigurations,
|
||||
local_runtime_paths: ExecServerRuntimePaths,
|
||||
) -> Self {
|
||||
let default_environment = environment_configurations
|
||||
.default_environment_id()
|
||||
.map(str::to_string);
|
||||
let mut environments = HashMap::from([(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
Arc::new(Environment::local(local_runtime_paths.clone())),
|
||||
)]);
|
||||
let default_environment = if environment_disabled {
|
||||
None
|
||||
} else {
|
||||
match exec_server_url {
|
||||
Some(exec_server_url) => {
|
||||
environments.insert(
|
||||
REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
Arc::new(Environment::remote(exec_server_url, local_runtime_paths)),
|
||||
);
|
||||
Some(REMOTE_ENVIRONMENT_ID.to_string())
|
||||
}
|
||||
None => Some(LOCAL_ENVIRONMENT_ID.to_string()),
|
||||
}
|
||||
};
|
||||
for (id, configuration) in environment_configurations.into_environments() {
|
||||
let environment = Environment::remote_inner(
|
||||
id.clone(),
|
||||
configuration,
|
||||
Some(local_runtime_paths.clone()),
|
||||
);
|
||||
environments.insert(id, Arc::new(environment));
|
||||
}
|
||||
|
||||
Self {
|
||||
default_environment,
|
||||
@@ -140,6 +161,7 @@ impl EnvironmentManager {
|
||||
/// paths used by filesystem helpers.
|
||||
#[derive(Clone)]
|
||||
pub struct Environment {
|
||||
is_remote: bool,
|
||||
exec_server_url: Option<String>,
|
||||
exec_backend: Arc<dyn ExecBackend>,
|
||||
filesystem: Arc<dyn ExecutorFileSystem>,
|
||||
@@ -151,6 +173,7 @@ impl Environment {
|
||||
/// Builds a test-only local environment without configured sandbox helper paths.
|
||||
pub fn default_for_tests() -> Self {
|
||||
Self {
|
||||
is_remote: false,
|
||||
exec_server_url: None,
|
||||
exec_backend: Arc::new(LocalProcess::default()),
|
||||
filesystem: Arc::new(LocalFileSystem::unsandboxed()),
|
||||
@@ -196,7 +219,11 @@ impl Environment {
|
||||
}
|
||||
|
||||
Ok(match exec_server_url {
|
||||
Some(exec_server_url) => Self::remote_inner(exec_server_url, local_runtime_paths),
|
||||
Some(exec_server_url) => Self::remote_inner(
|
||||
REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
EnvironmentConfiguration::normalized_static_url(exec_server_url),
|
||||
local_runtime_paths,
|
||||
),
|
||||
None => match local_runtime_paths {
|
||||
Some(local_runtime_paths) => Self::local(local_runtime_paths),
|
||||
None => Self::default_for_tests(),
|
||||
@@ -206,6 +233,7 @@ impl Environment {
|
||||
|
||||
fn local(local_runtime_paths: ExecServerRuntimePaths) -> Self {
|
||||
Self {
|
||||
is_remote: false,
|
||||
exec_server_url: None,
|
||||
exec_backend: Arc::new(LocalProcess::default()),
|
||||
filesystem: Arc::new(LocalFileSystem::with_runtime_paths(
|
||||
@@ -216,21 +244,20 @@ impl Environment {
|
||||
}
|
||||
}
|
||||
|
||||
fn remote(exec_server_url: String, local_runtime_paths: ExecServerRuntimePaths) -> Self {
|
||||
Self::remote_inner(exec_server_url, Some(local_runtime_paths))
|
||||
}
|
||||
|
||||
fn remote_inner(
|
||||
exec_server_url: String,
|
||||
environment_id: String,
|
||||
configuration: EnvironmentConfiguration,
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
) -> Self {
|
||||
let client = LazyRemoteExecServerClient::new(exec_server_url.clone());
|
||||
let exec_server_url = configuration.static_exec_server_url().map(str::to_string);
|
||||
let client = LazyRemoteExecServerClient::new(environment_id, configuration.resolver());
|
||||
let exec_backend: Arc<dyn ExecBackend> = Arc::new(RemoteProcess::new(client.clone()));
|
||||
let filesystem: Arc<dyn ExecutorFileSystem> =
|
||||
Arc::new(RemoteFileSystem::new(client.clone()));
|
||||
|
||||
Self {
|
||||
exec_server_url: Some(exec_server_url),
|
||||
is_remote: true,
|
||||
exec_server_url,
|
||||
exec_backend,
|
||||
filesystem,
|
||||
http_client: Arc::new(client),
|
||||
@@ -239,10 +266,13 @@ impl Environment {
|
||||
}
|
||||
|
||||
pub fn is_remote(&self) -> bool {
|
||||
self.exec_server_url.is_some()
|
||||
self.is_remote
|
||||
}
|
||||
|
||||
/// Returns the remote exec-server URL when this environment is remote.
|
||||
/// Returns the statically configured remote exec-server URL when known.
|
||||
///
|
||||
/// Dynamically resolved remote environments return `None` here and resolve
|
||||
/// their URL on first remote client use.
|
||||
pub fn exec_server_url(&self) -> Option<&str> {
|
||||
self.exec_server_url.as_deref()
|
||||
}
|
||||
@@ -264,24 +294,23 @@ impl Environment {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_exec_server_url(exec_server_url: Option<String>) -> (Option<String>, bool) {
|
||||
match exec_server_url.as_deref().map(str::trim) {
|
||||
None | Some("") => (None, false),
|
||||
Some(url) if url.eq_ignore_ascii_case("none") => (None, true),
|
||||
Some(url) => (Some(url.to_string()), false),
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use super::Environment;
|
||||
use super::EnvironmentManager;
|
||||
use super::EnvironmentManagerArgs;
|
||||
use super::LOCAL_ENVIRONMENT_ID;
|
||||
use super::REMOTE_ENVIRONMENT_ID;
|
||||
use crate::EnvironmentConfiguration;
|
||||
use crate::EnvironmentConfigurations;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::ProcessId;
|
||||
use futures::FutureExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn test_runtime_paths() -> ExecServerRuntimePaths {
|
||||
@@ -380,6 +409,91 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_builds_from_provider_configurations() {
|
||||
let manager = EnvironmentManager::from_configurations(
|
||||
EnvironmentConfigurations::new(
|
||||
Some(REMOTE_ENVIRONMENT_ID.to_string()),
|
||||
HashMap::from([(
|
||||
REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
EnvironmentConfiguration::static_url("ws://127.0.0.1:8765".to_string())
|
||||
.expect("static environment configuration"),
|
||||
)]),
|
||||
)
|
||||
.expect("environment configurations"),
|
||||
test_runtime_paths(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
manager.default_environment_id(),
|
||||
Some(REMOTE_ENVIRONMENT_ID)
|
||||
);
|
||||
assert!(
|
||||
manager
|
||||
.get_environment(REMOTE_ENVIRONMENT_ID)
|
||||
.expect("remote environment")
|
||||
.is_remote()
|
||||
);
|
||||
assert!(
|
||||
!manager
|
||||
.get_environment(LOCAL_ENVIRONMENT_ID)
|
||||
.expect("local environment")
|
||||
.is_remote()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_does_not_resolve_dynamic_environment_when_building_cache() {
|
||||
let resolve_count = Arc::new(AtomicUsize::new(0));
|
||||
let configuration = EnvironmentConfiguration::from_resolver_fn({
|
||||
let resolve_count = Arc::clone(&resolve_count);
|
||||
move || {
|
||||
let resolve_count = Arc::clone(&resolve_count);
|
||||
async move {
|
||||
resolve_count.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(crate::ResolvedEnvironment {
|
||||
exec_server_url: "ws://127.0.0.1:8765".to_string(),
|
||||
})
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
});
|
||||
|
||||
let manager = EnvironmentManager::from_configurations(
|
||||
EnvironmentConfigurations::new(
|
||||
Some(REMOTE_ENVIRONMENT_ID.to_string()),
|
||||
HashMap::from([(REMOTE_ENVIRONMENT_ID.to_string(), configuration)]),
|
||||
)
|
||||
.expect("environment configurations"),
|
||||
test_runtime_paths(),
|
||||
);
|
||||
|
||||
assert_eq!(resolve_count.load(Ordering::SeqCst), 0);
|
||||
assert!(
|
||||
manager
|
||||
.get_environment(REMOTE_ENVIRONMENT_ID)
|
||||
.expect("remote environment")
|
||||
.is_remote()
|
||||
);
|
||||
assert_eq!(resolve_count.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_inserts_local_environment() {
|
||||
let manager = EnvironmentManager::from_configurations(
|
||||
EnvironmentConfigurations::disabled(),
|
||||
test_runtime_paths(),
|
||||
);
|
||||
|
||||
assert_eq!(manager.default_environment_id(), None);
|
||||
assert!(
|
||||
!manager
|
||||
.get_environment(LOCAL_ENVIRONMENT_ID)
|
||||
.expect("local environment")
|
||||
.is_remote()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_carries_local_runtime_paths() {
|
||||
let runtime_paths = test_runtime_paths();
|
||||
|
||||
425
codex-rs/exec-server/src/environment_provider.rs
Normal file
425
codex-rs/exec-server/src/environment_provider.rs
Normal file
@@ -0,0 +1,425 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::future::BoxFuture;
|
||||
|
||||
use crate::ExecServerError;
|
||||
use crate::environment::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
use crate::environment::LOCAL_ENVIRONMENT_ID;
|
||||
use crate::environment::REMOTE_ENVIRONMENT_ID;
|
||||
|
||||
/// Resolved connection details for a provider-supplied environment.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResolvedEnvironment {
|
||||
pub exec_server_url: String,
|
||||
}
|
||||
|
||||
/// Resolves provider-supplied environment connection details on demand.
|
||||
#[async_trait]
|
||||
pub trait EnvironmentResolver: Send + Sync + fmt::Debug {
|
||||
/// Returns the static exec-server URL for resolvers that do not need
|
||||
/// asynchronous lookup.
|
||||
fn static_exec_server_url(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn resolve(&self) -> Result<ResolvedEnvironment, ExecServerError>;
|
||||
}
|
||||
|
||||
/// Provider-supplied environment definition consumed by `EnvironmentManager`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EnvironmentConfiguration {
|
||||
resolver: Arc<dyn EnvironmentResolver>,
|
||||
}
|
||||
|
||||
impl EnvironmentConfiguration {
|
||||
pub fn static_url(exec_server_url: String) -> Result<Self, ExecServerError> {
|
||||
let exec_server_url = normalize_remote_exec_server_url("<static>", exec_server_url)?;
|
||||
Ok(Self::normalized_static_url(exec_server_url))
|
||||
}
|
||||
|
||||
pub(crate) fn normalized_static_url(exec_server_url: String) -> Self {
|
||||
debug_assert!(matches!(
|
||||
normalize_remote_exec_server_url("<static>", exec_server_url.clone()),
|
||||
Ok(normalized) if normalized == exec_server_url
|
||||
));
|
||||
Self {
|
||||
resolver: Arc::new(StaticEnvironmentResolver { exec_server_url }),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_resolver<R>(resolver: R) -> Self
|
||||
where
|
||||
R: EnvironmentResolver + 'static,
|
||||
{
|
||||
Self {
|
||||
resolver: Arc::new(resolver),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_resolver_fn<F>(resolver: F) -> Self
|
||||
where
|
||||
F: Fn() -> BoxFuture<'static, Result<ResolvedEnvironment, ExecServerError>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
Self::with_resolver(FnEnvironmentResolver {
|
||||
resolver: Arc::new(resolver),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn resolve(&self) -> Result<ResolvedEnvironment, ExecServerError> {
|
||||
self.resolver.resolve().await
|
||||
}
|
||||
|
||||
pub(crate) fn static_exec_server_url(&self) -> Option<&str> {
|
||||
self.resolver.static_exec_server_url()
|
||||
}
|
||||
|
||||
pub(crate) fn resolver(&self) -> Arc<dyn EnvironmentResolver> {
|
||||
Arc::clone(&self.resolver)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct StaticEnvironmentResolver {
|
||||
exec_server_url: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EnvironmentResolver for StaticEnvironmentResolver {
|
||||
fn static_exec_server_url(&self) -> Option<&str> {
|
||||
Some(&self.exec_server_url)
|
||||
}
|
||||
|
||||
async fn resolve(&self) -> Result<ResolvedEnvironment, ExecServerError> {
|
||||
Ok(ResolvedEnvironment {
|
||||
exec_server_url: self.exec_server_url.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct FnEnvironmentResolver {
|
||||
resolver: Arc<
|
||||
dyn Fn() -> BoxFuture<'static, Result<ResolvedEnvironment, ExecServerError>> + Send + Sync,
|
||||
>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for FnEnvironmentResolver {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("FnEnvironmentResolver")
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EnvironmentResolver for FnEnvironmentResolver {
|
||||
async fn resolve(&self) -> Result<ResolvedEnvironment, ExecServerError> {
|
||||
(self.resolver)().await
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider-supplied environment snapshot consumed by `EnvironmentManager`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EnvironmentConfigurations {
|
||||
default_environment_id: Option<String>,
|
||||
environments: HashMap<String, EnvironmentConfiguration>,
|
||||
}
|
||||
|
||||
impl EnvironmentConfigurations {
|
||||
pub fn new(
|
||||
default_environment_id: Option<String>,
|
||||
environments: HashMap<String, EnvironmentConfiguration>,
|
||||
) -> Result<Self, ExecServerError> {
|
||||
for id in environments.keys() {
|
||||
if id.is_empty() {
|
||||
return Err(ExecServerError::Protocol(
|
||||
"environment configuration id cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
if id == LOCAL_ENVIRONMENT_ID {
|
||||
return Err(ExecServerError::Protocol(format!(
|
||||
"provider environment configurations must not include `{LOCAL_ENVIRONMENT_ID}`"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(default_environment_id) = default_environment_id.as_deref()
|
||||
&& default_environment_id != LOCAL_ENVIRONMENT_ID
|
||||
&& !environments.contains_key(default_environment_id)
|
||||
{
|
||||
return Err(ExecServerError::Protocol(format!(
|
||||
"default environment id `{default_environment_id}` has no environment configuration"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
default_environment_id,
|
||||
environments,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn disabled() -> Self {
|
||||
Self {
|
||||
default_environment_id: None,
|
||||
environments: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_default() -> Self {
|
||||
Self {
|
||||
default_environment_id: Some(LOCAL_ENVIRONMENT_ID.to_string()),
|
||||
environments: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_default(exec_server_url: String) -> Self {
|
||||
Self {
|
||||
default_environment_id: Some(REMOTE_ENVIRONMENT_ID.to_string()),
|
||||
environments: HashMap::from([(
|
||||
REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
EnvironmentConfiguration::normalized_static_url(exec_server_url),
|
||||
)]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_environment_id(&self) -> Option<&str> {
|
||||
self.default_environment_id.as_deref()
|
||||
}
|
||||
|
||||
pub fn into_environments(self) -> HashMap<String, EnvironmentConfiguration> {
|
||||
self.environments
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists the concrete environment configurations available to Codex.
|
||||
///
|
||||
/// Implementations should return the provider-owned portion of the startup
|
||||
/// snapshot that `EnvironmentManager` will cache. The local environment is
|
||||
/// always supplied by `EnvironmentManager`; providers only need to set
|
||||
/// `local` as the default when they want local to be selected by default.
|
||||
/// Remote configurations carry their own resolver so providers can choose
|
||||
/// between static URLs and dynamic, on-demand endpoint lookup.
|
||||
#[async_trait]
|
||||
pub trait EnvironmentProvider: Send + Sync {
|
||||
/// Returns the environment configurations available for a new manager.
|
||||
async fn get_environments(&self) -> Result<EnvironmentConfigurations, ExecServerError>;
|
||||
}
|
||||
|
||||
/// Default provider backed by `CODEX_EXEC_SERVER_URL`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DefaultEnvironmentProvider {
|
||||
exec_server_url: Option<String>,
|
||||
}
|
||||
|
||||
impl DefaultEnvironmentProvider {
|
||||
/// Builds a provider from an already-read raw `CODEX_EXEC_SERVER_URL` value.
|
||||
pub fn new(exec_server_url: Option<String>) -> Self {
|
||||
Self { exec_server_url }
|
||||
}
|
||||
|
||||
/// Builds a provider by reading `CODEX_EXEC_SERVER_URL`.
|
||||
pub fn from_env() -> Self {
|
||||
Self::new(std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok())
|
||||
}
|
||||
|
||||
pub(crate) fn environment_configurations(&self) -> EnvironmentConfigurations {
|
||||
let (exec_server_url, environment_disabled) =
|
||||
normalize_exec_server_url(self.exec_server_url.clone());
|
||||
|
||||
if let Some(exec_server_url) = exec_server_url {
|
||||
EnvironmentConfigurations::remote_default(exec_server_url)
|
||||
} else if !environment_disabled {
|
||||
EnvironmentConfigurations::local_default()
|
||||
} else {
|
||||
EnvironmentConfigurations::disabled()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EnvironmentProvider for DefaultEnvironmentProvider {
|
||||
async fn get_environments(&self) -> Result<EnvironmentConfigurations, ExecServerError> {
|
||||
Ok(self.environment_configurations())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_exec_server_url(exec_server_url: Option<String>) -> (Option<String>, bool) {
|
||||
match exec_server_url.as_deref().map(str::trim) {
|
||||
None | Some("") => (None, false),
|
||||
Some(url) if url.eq_ignore_ascii_case("none") => (None, true),
|
||||
Some(url) => (Some(url.to_string()), false),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_remote_exec_server_url(
|
||||
environment_id: &str,
|
||||
exec_server_url: String,
|
||||
) -> Result<String, ExecServerError> {
|
||||
match normalize_exec_server_url(Some(exec_server_url)) {
|
||||
(Some(exec_server_url), false) => Ok(exec_server_url),
|
||||
(None, false) | (None, true) | (Some(_), true) => Err(ExecServerError::Protocol(format!(
|
||||
"environment configuration `{environment_id}` must resolve to a remote exec-server URL"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use futures::FutureExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn assert_local_default(configurations: EnvironmentConfigurations) {
|
||||
assert_eq!(
|
||||
configurations.default_environment_id(),
|
||||
Some(LOCAL_ENVIRONMENT_ID)
|
||||
);
|
||||
assert!(configurations.environments.is_empty());
|
||||
}
|
||||
|
||||
fn assert_disabled(configurations: EnvironmentConfigurations) {
|
||||
assert_eq!(configurations.default_environment_id(), None);
|
||||
assert!(configurations.environments.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_provider_uses_local_environment_when_url_is_missing() {
|
||||
let provider = DefaultEnvironmentProvider::new(/*exec_server_url*/ None);
|
||||
|
||||
assert_local_default(provider.get_environments().await.expect("environments"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_provider_uses_local_environment_when_url_is_empty() {
|
||||
let provider = DefaultEnvironmentProvider::new(Some(String::new()));
|
||||
|
||||
assert_local_default(provider.get_environments().await.expect("environments"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_provider_disables_default_environment_for_none_value() {
|
||||
let provider = DefaultEnvironmentProvider::new(Some("none".to_string()));
|
||||
|
||||
assert_disabled(provider.get_environments().await.expect("environments"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_provider_adds_remote_environment_for_websocket_url() {
|
||||
let provider = DefaultEnvironmentProvider::new(Some("ws://127.0.0.1:8765".to_string()));
|
||||
|
||||
let configurations = provider.get_environments().await.expect("environments");
|
||||
assert_eq!(
|
||||
configurations.default_environment_id(),
|
||||
Some(REMOTE_ENVIRONMENT_ID)
|
||||
);
|
||||
let environment = configurations
|
||||
.environments
|
||||
.get(REMOTE_ENVIRONMENT_ID)
|
||||
.expect("remote configuration");
|
||||
assert_eq!(
|
||||
environment.static_exec_server_url(),
|
||||
Some("ws://127.0.0.1:8765")
|
||||
);
|
||||
assert_eq!(
|
||||
environment.resolve().await.expect("resolved environment"),
|
||||
ResolvedEnvironment {
|
||||
exec_server_url: "ws://127.0.0.1:8765".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_configurations_rejects_local_provider_entry() {
|
||||
let err = EnvironmentConfigurations::new(
|
||||
Some(LOCAL_ENVIRONMENT_ID.to_string()),
|
||||
HashMap::from([(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
EnvironmentConfiguration::static_url("ws://127.0.0.1:8765".to_string())
|
||||
.expect("static environment configuration"),
|
||||
)]),
|
||||
)
|
||||
.expect_err("local provider entry should fail");
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"exec-server protocol error: provider environment configurations must not include `local`"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_configurations_rejects_missing_default() {
|
||||
let err =
|
||||
EnvironmentConfigurations::new(Some(REMOTE_ENVIRONMENT_ID.to_string()), HashMap::new())
|
||||
.expect_err("missing default should fail");
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"exec-server protocol error: default environment id `remote` has no environment configuration"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_environment_configuration_rejects_empty_exec_server_url() {
|
||||
let err =
|
||||
EnvironmentConfiguration::static_url(String::new()).expect_err("empty URL should fail");
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"exec-server protocol error: environment configuration `<static>` must resolve to a remote exec-server URL"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_environment_configuration_rejects_disabled_exec_server_url() {
|
||||
let err = EnvironmentConfiguration::static_url("none".to_string())
|
||||
.expect_err("disabled URL should fail");
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"exec-server protocol error: environment configuration `<static>` must resolve to a remote exec-server URL"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn static_environment_configuration_normalizes_exec_server_url() {
|
||||
let configuration =
|
||||
EnvironmentConfiguration::static_url(" ws://127.0.0.1:8765 ".to_string())
|
||||
.expect("environment configurations");
|
||||
|
||||
assert_eq!(
|
||||
configuration.static_exec_server_url(),
|
||||
Some("ws://127.0.0.1:8765")
|
||||
);
|
||||
assert_eq!(
|
||||
configuration.resolve().await.expect("resolved environment"),
|
||||
ResolvedEnvironment {
|
||||
exec_server_url: "ws://127.0.0.1:8765".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_configuration_can_resolve_from_closure() {
|
||||
let configuration = EnvironmentConfiguration::from_resolver_fn(|| {
|
||||
async {
|
||||
Ok(ResolvedEnvironment {
|
||||
exec_server_url: "ws://127.0.0.1:8765".to_string(),
|
||||
})
|
||||
}
|
||||
.boxed()
|
||||
});
|
||||
|
||||
assert_eq!(configuration.static_exec_server_url(), None);
|
||||
assert_eq!(
|
||||
configuration.resolve().await.expect("resolved environment"),
|
||||
ResolvedEnvironment {
|
||||
exec_server_url: "ws://127.0.0.1:8765".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ mod client;
|
||||
mod client_api;
|
||||
mod connection;
|
||||
mod environment;
|
||||
mod environment_provider;
|
||||
mod fs_helper;
|
||||
mod fs_helper_main;
|
||||
mod fs_sandbox;
|
||||
@@ -38,6 +39,12 @@ pub use environment::EnvironmentManager;
|
||||
pub use environment::EnvironmentManagerArgs;
|
||||
pub use environment::LOCAL_ENVIRONMENT_ID;
|
||||
pub use environment::REMOTE_ENVIRONMENT_ID;
|
||||
pub use environment_provider::DefaultEnvironmentProvider;
|
||||
pub use environment_provider::EnvironmentConfiguration;
|
||||
pub use environment_provider::EnvironmentConfigurations;
|
||||
pub use environment_provider::EnvironmentProvider;
|
||||
pub use environment_provider::EnvironmentResolver;
|
||||
pub use environment_provider::ResolvedEnvironment;
|
||||
pub use fs_helper::CODEX_FS_HELPER_ARG1;
|
||||
pub use fs_helper_main::main as run_fs_helper_main;
|
||||
pub use local_file_system::LOCAL_FS;
|
||||
|
||||
Reference in New Issue
Block a user