mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
1133 lines
42 KiB
Rust
1133 lines
42 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::sync::RwLock;
|
|
|
|
use futures::FutureExt;
|
|
use futures::future::BoxFuture;
|
|
|
|
use crate::ExecServerError;
|
|
use crate::ExecServerRuntimePaths;
|
|
use crate::ExecutorFileSystem;
|
|
use crate::HttpClient;
|
|
use crate::client::LazyRemoteExecServerClient;
|
|
use crate::client::http_client::ReqwestHttpClient;
|
|
use crate::client_api::ExecServerTransportParams;
|
|
use crate::environment_provider::DefaultEnvironmentProvider;
|
|
use crate::environment_provider::EnvironmentDefault;
|
|
use crate::environment_provider::EnvironmentProvider;
|
|
use crate::environment_provider::EnvironmentProviderSnapshot;
|
|
use crate::environment_provider::normalize_exec_server_url;
|
|
use crate::environment_toml::environment_provider_from_codex_home;
|
|
use crate::local_file_system::LocalFileSystem;
|
|
use crate::local_process::LocalProcess;
|
|
use crate::process::ExecBackend;
|
|
use crate::protocol::EnvironmentInfo;
|
|
use crate::protocol::ShellInfo;
|
|
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";
|
|
|
|
/// Owns the execution/filesystem environments available to the Codex runtime.
|
|
///
|
|
/// `EnvironmentManager` is a shared registry for environments. Its
|
|
/// default constructor preserves the legacy `CODEX_EXEC_SERVER_URL` behavior
|
|
/// while configured construction accepts a provider-supplied snapshot.
|
|
///
|
|
/// Setting `CODEX_EXEC_SERVER_URL=none` disables environment access by leaving
|
|
/// the default environment unset and omitting the local environment. Callers
|
|
/// use `default_environment().is_some()` as the signal for model-facing
|
|
/// shell/filesystem tool availability.
|
|
///
|
|
/// Remote environments create remote filesystem and execution backends that
|
|
/// lazy-connect to the configured exec-server on first use. The remote
|
|
/// transport is not opened when the manager or environment is constructed.
|
|
#[derive(Debug)]
|
|
pub struct EnvironmentManager {
|
|
default_environment: Option<String>,
|
|
environments: RwLock<HashMap<String, Arc<Environment>>>,
|
|
local_environment: Option<Arc<Environment>>,
|
|
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
|
}
|
|
|
|
pub const LOCAL_ENVIRONMENT_ID: &str = "local";
|
|
pub const REMOTE_ENVIRONMENT_ID: &str = "remote";
|
|
|
|
impl EnvironmentManager {
|
|
/// Builds a test-only manager without configured sandbox helper paths.
|
|
pub fn default_for_tests() -> Self {
|
|
Self {
|
|
default_environment: Some(LOCAL_ENVIRONMENT_ID.to_string()),
|
|
environments: RwLock::new(HashMap::from([(
|
|
LOCAL_ENVIRONMENT_ID.to_string(),
|
|
Arc::new(Environment::default_for_tests()),
|
|
)])),
|
|
local_environment: Some(Arc::new(Environment::default_for_tests())),
|
|
local_runtime_paths: None,
|
|
}
|
|
}
|
|
|
|
/// Builds a manager with no configured execution environments.
|
|
pub fn without_environments() -> Self {
|
|
Self {
|
|
default_environment: None,
|
|
environments: RwLock::new(HashMap::new()),
|
|
local_environment: None,
|
|
local_runtime_paths: None,
|
|
}
|
|
}
|
|
|
|
/// Builds a test-only manager from a raw exec-server URL value.
|
|
pub async fn create_for_tests(
|
|
exec_server_url: Option<String>,
|
|
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
|
) -> Self {
|
|
Self::from_default_provider_url(exec_server_url, local_runtime_paths).await
|
|
}
|
|
|
|
/// Builds a manager from `CODEX_HOME` and local runtime paths used when
|
|
/// creating local filesystem helpers.
|
|
///
|
|
/// If `CODEX_HOME/environments.toml` is present, it defines the configured
|
|
/// environments. Otherwise this preserves the legacy
|
|
/// `CODEX_EXEC_SERVER_URL` behavior.
|
|
pub async fn from_codex_home(
|
|
codex_home: impl AsRef<std::path::Path>,
|
|
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
|
) -> Result<Self, ExecServerError> {
|
|
let provider = environment_provider_from_codex_home(codex_home.as_ref())?;
|
|
Self::from_snapshot(provider.snapshot().await?, local_runtime_paths)
|
|
}
|
|
|
|
/// Builds a manager from the legacy environment-variable provider without
|
|
/// reading user config files from `CODEX_HOME`.
|
|
pub async fn from_env(
|
|
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
|
) -> Result<Self, ExecServerError> {
|
|
let provider = DefaultEnvironmentProvider::from_env();
|
|
Self::from_snapshot(provider.snapshot().await?, local_runtime_paths)
|
|
}
|
|
|
|
async fn from_default_provider_url(
|
|
exec_server_url: Option<String>,
|
|
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
|
) -> Self {
|
|
let provider = DefaultEnvironmentProvider::new(exec_server_url);
|
|
match Self::from_snapshot(provider.snapshot_inner(), local_runtime_paths) {
|
|
Ok(manager) => manager,
|
|
Err(err) => panic!("default provider should create valid environments: {err}"),
|
|
}
|
|
}
|
|
|
|
/// 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(
|
|
exec_server_url: Option<String>,
|
|
local_runtime_paths: ExecServerRuntimePaths,
|
|
) -> Self {
|
|
let mut snapshot = DefaultEnvironmentProvider::new(exec_server_url).snapshot_inner();
|
|
snapshot.include_local = true;
|
|
match Self::from_snapshot(snapshot, Some(local_runtime_paths)) {
|
|
Ok(manager) => manager,
|
|
Err(err) => panic!("test provider with local should create valid environments: {err}"),
|
|
}
|
|
}
|
|
|
|
fn from_snapshot(
|
|
snapshot: EnvironmentProviderSnapshot,
|
|
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
|
) -> Result<Self, ExecServerError> {
|
|
let EnvironmentProviderSnapshot {
|
|
environments,
|
|
default,
|
|
include_local,
|
|
} = snapshot;
|
|
let mut environment_map =
|
|
HashMap::with_capacity(environments.len() + usize::from(include_local));
|
|
let local_environment = if include_local {
|
|
let local_runtime_paths = local_runtime_paths.clone().ok_or_else(|| {
|
|
ExecServerError::Protocol(
|
|
"local environment requires configured runtime paths".to_string(),
|
|
)
|
|
})?;
|
|
let local_environment = Arc::new(Environment::local(local_runtime_paths));
|
|
environment_map.insert(
|
|
LOCAL_ENVIRONMENT_ID.to_string(),
|
|
Arc::clone(&local_environment),
|
|
);
|
|
Some(local_environment)
|
|
} else {
|
|
None
|
|
};
|
|
for (id, environment) in environments {
|
|
if id.is_empty() {
|
|
return Err(ExecServerError::Protocol(
|
|
"environment id cannot be empty".to_string(),
|
|
));
|
|
}
|
|
if id == LOCAL_ENVIRONMENT_ID {
|
|
return Err(ExecServerError::Protocol(format!(
|
|
"environment id `{LOCAL_ENVIRONMENT_ID}` is reserved for EnvironmentManager"
|
|
)));
|
|
}
|
|
if environment_map
|
|
.insert(id.clone(), Arc::new(environment))
|
|
.is_some()
|
|
{
|
|
return Err(ExecServerError::Protocol(format!(
|
|
"environment id `{id}` is duplicated"
|
|
)));
|
|
}
|
|
}
|
|
let default_environment = match default {
|
|
EnvironmentDefault::Disabled => None,
|
|
EnvironmentDefault::EnvironmentId(environment_id) => {
|
|
if !environment_map.contains_key(&environment_id) {
|
|
return Err(ExecServerError::Protocol(format!(
|
|
"default environment `{environment_id}` is not configured"
|
|
)));
|
|
}
|
|
Some(environment_id)
|
|
}
|
|
};
|
|
Ok(Self {
|
|
default_environment,
|
|
environments: RwLock::new(environment_map),
|
|
local_environment,
|
|
local_runtime_paths,
|
|
})
|
|
}
|
|
|
|
/// Returns the default environment instance.
|
|
pub fn default_environment(&self) -> Option<Arc<Environment>> {
|
|
self.default_environment
|
|
.as_deref()
|
|
.and_then(|environment_id| self.get_environment(environment_id))
|
|
}
|
|
|
|
/// Returns the id of the default environment.
|
|
pub fn default_environment_id(&self) -> Option<&str> {
|
|
self.default_environment.as_deref()
|
|
}
|
|
|
|
/// Returns the ordered environment ids used for new thread startup.
|
|
pub fn default_environment_ids(&self) -> Vec<String> {
|
|
let Some(default_environment_id) = self.default_environment.as_ref() else {
|
|
return Vec::new();
|
|
};
|
|
let environments = self
|
|
.environments
|
|
.read()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
let mut environment_ids = Vec::with_capacity(environments.len());
|
|
environment_ids.push(default_environment_id.clone());
|
|
environment_ids.extend(
|
|
environments
|
|
.keys()
|
|
.filter(|environment_id| *environment_id != default_environment_id)
|
|
.cloned(),
|
|
);
|
|
environment_ids
|
|
}
|
|
|
|
/// Returns the local environment instance when one is configured.
|
|
pub fn try_local_environment(&self) -> Option<Arc<Environment>> {
|
|
self.local_environment.as_ref().map(Arc::clone)
|
|
}
|
|
|
|
/// Returns the default environment or local environment when either exists.
|
|
pub fn default_or_local_environment(&self) -> Option<Arc<Environment>> {
|
|
self.default_environment()
|
|
.or_else(|| self.try_local_environment())
|
|
}
|
|
|
|
/// Returns a named environment instance.
|
|
pub fn get_environment(&self, environment_id: &str) -> Option<Arc<Environment>> {
|
|
self.environments
|
|
.read()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.get(environment_id)
|
|
.cloned()
|
|
}
|
|
|
|
/// Registers a named remote environment before its exec-server endpoint is available.
|
|
pub fn register_pending_environment(
|
|
&self,
|
|
environment_id: String,
|
|
) -> Result<Arc<Environment>, ExecServerError> {
|
|
validate_environment_id(&environment_id)?;
|
|
let mut environments = self
|
|
.environments
|
|
.write()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
// Reuse the same Arc so early turn and capability handles observe later updates.
|
|
Ok(Arc::clone(
|
|
environments.entry(environment_id).or_insert_with(|| {
|
|
Arc::new(Environment::pending_remote(
|
|
self.local_runtime_paths.clone(),
|
|
))
|
|
}),
|
|
))
|
|
}
|
|
|
|
/// Adds or replaces a named remote environment without changing the
|
|
/// manager's default environment selection.
|
|
pub fn upsert_environment(
|
|
&self,
|
|
environment_id: String,
|
|
exec_server_url: String,
|
|
) -> Result<(), ExecServerError> {
|
|
validate_environment_id(&environment_id)?;
|
|
let (exec_server_url, disabled) = normalize_exec_server_url(Some(exec_server_url));
|
|
if disabled {
|
|
return Err(ExecServerError::Protocol(
|
|
"remote environment cannot use disabled exec-server url".to_string(),
|
|
));
|
|
}
|
|
let Some(exec_server_url) = exec_server_url else {
|
|
return Err(ExecServerError::Protocol(
|
|
"remote environment requires an exec-server url".to_string(),
|
|
));
|
|
};
|
|
let mut environments = self
|
|
.environments
|
|
.write()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
match environments.get(&environment_id) {
|
|
// Update the shared client inside the stable Environment handle.
|
|
Some(existing) if existing.is_remote() => {
|
|
existing.set_remote_transport(ExecServerTransportParams::websocket_url(
|
|
exec_server_url,
|
|
));
|
|
existing.start_loading_info();
|
|
}
|
|
_ => {
|
|
let environment = Arc::new(Environment::remote_inner(
|
|
exec_server_url,
|
|
self.local_runtime_paths.clone(),
|
|
));
|
|
environment.start_loading_info();
|
|
environments.insert(environment_id, environment);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn validate_environment_id(environment_id: &str) -> Result<(), ExecServerError> {
|
|
if environment_id.is_empty() {
|
|
return Err(ExecServerError::Protocol(
|
|
"environment id cannot be empty".to_string(),
|
|
));
|
|
}
|
|
if environment_id == LOCAL_ENVIRONMENT_ID {
|
|
return Err(ExecServerError::Protocol(format!(
|
|
"environment id `{LOCAL_ENVIRONMENT_ID}` is reserved for EnvironmentManager"
|
|
)));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Execution/filesystem environment selected for a session.
|
|
///
|
|
/// The handle remains stable while its concrete backend is supplied or replaced.
|
|
#[derive(Clone)]
|
|
pub struct Environment {
|
|
// None is local; Some is remote and may still be waiting for its transport.
|
|
remote_client: Option<LazyRemoteExecServerClient>,
|
|
info_provider: Arc<dyn EnvironmentInfoProvider>,
|
|
exec_backend: Arc<dyn ExecBackend>,
|
|
filesystem: Arc<dyn ExecutorFileSystem>,
|
|
http_client: Arc<dyn HttpClient>,
|
|
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
|
}
|
|
|
|
/// Provides environment metadata from either a local environment or a remote exec-server.
|
|
trait EnvironmentInfoProvider: Send + Sync {
|
|
fn info(&self) -> BoxFuture<'_, Result<EnvironmentInfo, ExecServerError>>;
|
|
}
|
|
|
|
struct LocalEnvironmentInfoProvider;
|
|
|
|
impl EnvironmentInfoProvider for LocalEnvironmentInfoProvider {
|
|
fn info(&self) -> BoxFuture<'_, Result<EnvironmentInfo, ExecServerError>> {
|
|
std::future::ready(Ok(EnvironmentInfo::local())).boxed()
|
|
}
|
|
}
|
|
|
|
struct RemoteEnvironmentInfoProvider {
|
|
client: LazyRemoteExecServerClient,
|
|
}
|
|
|
|
impl RemoteEnvironmentInfoProvider {
|
|
fn new(client: LazyRemoteExecServerClient) -> Self {
|
|
Self { client }
|
|
}
|
|
}
|
|
|
|
impl EnvironmentInfoProvider for RemoteEnvironmentInfoProvider {
|
|
fn info(&self) -> BoxFuture<'_, Result<EnvironmentInfo, ExecServerError>> {
|
|
async move { self.client.environment_info().await }.boxed()
|
|
}
|
|
}
|
|
|
|
impl Environment {
|
|
/// Builds a test-only local environment without configured sandbox helper paths.
|
|
pub fn default_for_tests() -> Self {
|
|
Self {
|
|
remote_client: None,
|
|
info_provider: Arc::new(LocalEnvironmentInfoProvider),
|
|
exec_backend: Arc::new(LocalProcess::default()),
|
|
filesystem: Arc::new(LocalFileSystem::unsandboxed()),
|
|
http_client: Arc::new(ReqwestHttpClient),
|
|
local_runtime_paths: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for Environment {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("Environment")
|
|
.field("is_remote", &self.is_remote())
|
|
.field("is_ready", &self.is_ready())
|
|
.field("exec_server_url", &self.exec_server_url())
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
impl Environment {
|
|
/// Builds an environment from the raw `CODEX_EXEC_SERVER_URL` value.
|
|
pub fn create(
|
|
exec_server_url: Option<String>,
|
|
local_runtime_paths: ExecServerRuntimePaths,
|
|
) -> Result<Self, ExecServerError> {
|
|
Self::create_inner(exec_server_url, Some(local_runtime_paths))
|
|
}
|
|
|
|
/// Builds a test-only environment without configured sandbox helper paths.
|
|
pub fn create_for_tests(exec_server_url: Option<String>) -> Result<Self, ExecServerError> {
|
|
Self::create_inner(exec_server_url, /*local_runtime_paths*/ None)
|
|
}
|
|
|
|
/// Builds an environment from the raw `CODEX_EXEC_SERVER_URL` value and
|
|
/// local runtime paths used when creating local filesystem helpers.
|
|
fn create_inner(
|
|
exec_server_url: Option<String>,
|
|
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
|
) -> Result<Self, ExecServerError> {
|
|
let (exec_server_url, disabled) = normalize_exec_server_url(exec_server_url);
|
|
if disabled {
|
|
return Err(ExecServerError::Protocol(
|
|
"disabled mode does not create an Environment".to_string(),
|
|
));
|
|
}
|
|
|
|
Ok(match exec_server_url {
|
|
Some(exec_server_url) => Self::remote_inner(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(),
|
|
},
|
|
})
|
|
}
|
|
|
|
pub(crate) fn local(local_runtime_paths: ExecServerRuntimePaths) -> Self {
|
|
Self {
|
|
remote_client: None,
|
|
info_provider: Arc::new(LocalEnvironmentInfoProvider),
|
|
exec_backend: Arc::new(LocalProcess::default()),
|
|
filesystem: Arc::new(LocalFileSystem::with_runtime_paths(
|
|
local_runtime_paths.clone(),
|
|
)),
|
|
http_client: Arc::new(ReqwestHttpClient),
|
|
local_runtime_paths: Some(local_runtime_paths),
|
|
}
|
|
}
|
|
|
|
fn pending_remote(local_runtime_paths: Option<ExecServerRuntimePaths>) -> Self {
|
|
// Build capability wrappers now; their first operation waits inside the lazy client.
|
|
Self::remote_with_client(LazyRemoteExecServerClient::pending(), local_runtime_paths)
|
|
}
|
|
|
|
pub(crate) fn remote_inner(
|
|
exec_server_url: String,
|
|
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
|
) -> Self {
|
|
Self::remote_with_transport(
|
|
ExecServerTransportParams::websocket_url(exec_server_url),
|
|
local_runtime_paths,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn remote_with_transport(
|
|
remote_transport: ExecServerTransportParams,
|
|
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
|
) -> Self {
|
|
Self::remote_with_client(
|
|
LazyRemoteExecServerClient::new(remote_transport),
|
|
local_runtime_paths,
|
|
)
|
|
}
|
|
|
|
fn remote_with_client(
|
|
client: LazyRemoteExecServerClient,
|
|
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
|
) -> Self {
|
|
// These clones share one internal state, so every capability sees transport updates.
|
|
let exec_backend: Arc<dyn ExecBackend> = Arc::new(RemoteProcess::new(client.clone()));
|
|
let filesystem: Arc<dyn ExecutorFileSystem> =
|
|
Arc::new(RemoteFileSystem::new(client.clone()));
|
|
|
|
Self {
|
|
remote_client: Some(client.clone()),
|
|
info_provider: Arc::new(RemoteEnvironmentInfoProvider::new(client.clone())),
|
|
exec_backend,
|
|
filesystem,
|
|
http_client: Arc::new(client),
|
|
local_runtime_paths,
|
|
}
|
|
}
|
|
|
|
pub fn is_remote(&self) -> bool {
|
|
self.remote_client.is_some()
|
|
}
|
|
|
|
/// Ready means we know how to connect; the connection is opened only when needed.
|
|
pub fn is_ready(&self) -> bool {
|
|
self.remote_client
|
|
.as_ref()
|
|
.is_none_or(|client| client.transport_params().is_some())
|
|
}
|
|
|
|
/// Returns the currently configured remote exec-server URL.
|
|
pub fn exec_server_url(&self) -> Option<String> {
|
|
self.remote_client
|
|
.as_ref()?
|
|
.transport_params()
|
|
.and_then(|transport_params| match transport_params.as_ref() {
|
|
ExecServerTransportParams::WebSocketUrl { websocket_url, .. } => {
|
|
Some(websocket_url.clone())
|
|
}
|
|
ExecServerTransportParams::StdioCommand { .. } => None,
|
|
})
|
|
}
|
|
|
|
pub fn local_runtime_paths(&self) -> Option<&ExecServerRuntimePaths> {
|
|
self.local_runtime_paths.as_ref()
|
|
}
|
|
|
|
/// Returns environment information from the selected execution/filesystem environment.
|
|
pub async fn info(&self) -> Result<EnvironmentInfo, ExecServerError> {
|
|
self.info_provider.info().await
|
|
}
|
|
|
|
/// Returns already-loaded environment information without starting a connection.
|
|
pub fn current_info(&self) -> Option<EnvironmentInfo> {
|
|
self.remote_client.as_ref().map_or_else(
|
|
|| Some(EnvironmentInfo::local()),
|
|
LazyRemoteExecServerClient::current_environment_info,
|
|
)
|
|
}
|
|
|
|
/// Starts loading remote metadata without making the caller wait for it.
|
|
pub fn start_loading_info(self: &Arc<Self>) {
|
|
if self.current_info().is_some() || !self.is_ready() {
|
|
return;
|
|
}
|
|
let Ok(runtime) = tokio::runtime::Handle::try_current() else {
|
|
return;
|
|
};
|
|
let Some(remote_client) = self.remote_client.clone() else {
|
|
return;
|
|
};
|
|
let Some(load_token) = remote_client.try_begin_environment_info_load() else {
|
|
return;
|
|
};
|
|
let environment = Arc::clone(self);
|
|
runtime.spawn(async move {
|
|
let result = environment.info().await;
|
|
let transport_changed = remote_client.environment_info_load_token() != load_token;
|
|
let reload_requested = remote_client.finish_environment_info_load(load_token);
|
|
if let Err(err) = result {
|
|
tracing::warn!("failed to load environment metadata: {err}");
|
|
}
|
|
if transport_changed || reload_requested {
|
|
environment.start_loading_info();
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn get_exec_backend(&self) -> Arc<dyn ExecBackend> {
|
|
Arc::clone(&self.exec_backend)
|
|
}
|
|
|
|
pub fn get_http_client(&self) -> Arc<dyn HttpClient> {
|
|
Arc::clone(&self.http_client)
|
|
}
|
|
|
|
pub fn get_filesystem(&self) -> Arc<dyn ExecutorFileSystem> {
|
|
Arc::clone(&self.filesystem)
|
|
}
|
|
|
|
fn set_remote_transport(&self, remote_transport: ExecServerTransportParams) {
|
|
// Mutate the shared client rather than replacing the Environment held by existing turns.
|
|
if let Some(client) = &self.remote_client {
|
|
client.set_transport(remote_transport);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl EnvironmentInfo {
|
|
pub(crate) fn local() -> Self {
|
|
Self {
|
|
shell: codex_shell_command::shell_detect::default_user_shell().into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<DetectedShell> for ShellInfo {
|
|
fn from(shell: DetectedShell) -> Self {
|
|
Self {
|
|
name: shell.name().to_string(),
|
|
path: shell.shell_path.to_string_lossy().into_owned(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use super::Environment;
|
|
use super::EnvironmentManager;
|
|
use super::LOCAL_ENVIRONMENT_ID;
|
|
use super::REMOTE_ENVIRONMENT_ID;
|
|
use crate::ExecServerRuntimePaths;
|
|
use crate::ProcessId;
|
|
use crate::environment_provider::EnvironmentDefault;
|
|
use crate::environment_provider::EnvironmentProviderSnapshot;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
fn test_runtime_paths() -> ExecServerRuntimePaths {
|
|
ExecServerRuntimePaths::new(
|
|
std::env::current_exe().expect("current exe"),
|
|
/*codex_linux_sandbox_exe*/ None,
|
|
)
|
|
.expect("runtime paths")
|
|
}
|
|
|
|
fn assert_local_environment_unavailable(manager: &EnvironmentManager) {
|
|
assert!(manager.try_local_environment().is_none());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn create_local_environment_does_not_connect() {
|
|
let environment = Environment::create(/*exec_server_url*/ None, test_runtime_paths())
|
|
.expect("create environment");
|
|
|
|
assert_eq!(environment.exec_server_url(), None);
|
|
assert!(!environment.is_remote());
|
|
assert!(environment.info().await.is_ok());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn environment_manager_normalizes_empty_url() {
|
|
let manager =
|
|
EnvironmentManager::create_for_tests(Some(String::new()), Some(test_runtime_paths()))
|
|
.await;
|
|
|
|
let environment = manager.default_environment().expect("default environment");
|
|
assert_eq!(manager.default_environment_id(), Some(LOCAL_ENVIRONMENT_ID));
|
|
assert!(Arc::ptr_eq(
|
|
&environment,
|
|
&manager
|
|
.get_environment(LOCAL_ENVIRONMENT_ID)
|
|
.expect("local environment")
|
|
));
|
|
assert!(Arc::ptr_eq(
|
|
&environment,
|
|
&manager.try_local_environment().expect("local environment")
|
|
));
|
|
assert!(manager.try_local_environment().is_some());
|
|
assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none());
|
|
assert!(!environment.is_remote());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn disabled_environment_manager_has_no_default_or_local_environment() {
|
|
let manager = EnvironmentManager::without_environments();
|
|
|
|
assert!(manager.default_environment().is_none());
|
|
assert_eq!(manager.default_environment_id(), None);
|
|
assert_local_environment_unavailable(&manager);
|
|
assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
|
|
assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn environment_manager_reports_remote_url() {
|
|
let manager = EnvironmentManager::create_for_tests(
|
|
Some("ws://127.0.0.1:8765".to_string()),
|
|
Some(test_runtime_paths()),
|
|
)
|
|
.await;
|
|
|
|
let environment = manager.default_environment().expect("default environment");
|
|
assert_eq!(
|
|
manager.default_environment_id(),
|
|
Some(REMOTE_ENVIRONMENT_ID)
|
|
);
|
|
assert!(environment.is_remote());
|
|
assert_eq!(
|
|
environment.exec_server_url().as_deref(),
|
|
Some("ws://127.0.0.1:8765")
|
|
);
|
|
assert!(Arc::ptr_eq(
|
|
&environment,
|
|
&manager
|
|
.get_environment(REMOTE_ENVIRONMENT_ID)
|
|
.expect("remote environment")
|
|
));
|
|
assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
|
|
assert_local_environment_unavailable(&manager);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn environment_manager_default_environment_caches_environment() {
|
|
let manager = EnvironmentManager::default_for_tests();
|
|
|
|
let first = manager.default_environment().expect("default environment");
|
|
let second = manager.default_environment().expect("default environment");
|
|
|
|
assert!(Arc::ptr_eq(&first, &second));
|
|
assert!(Arc::ptr_eq(
|
|
&first.get_filesystem(),
|
|
&second.get_filesystem()
|
|
));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn environment_manager_builds_from_snapshot() {
|
|
let snapshot = EnvironmentProviderSnapshot {
|
|
environments: vec![(
|
|
REMOTE_ENVIRONMENT_ID.to_string(),
|
|
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
|
|
.expect("remote environment"),
|
|
)],
|
|
default: EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string()),
|
|
include_local: false,
|
|
};
|
|
let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
|
.expect("environment manager");
|
|
|
|
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).is_none());
|
|
assert_local_environment_unavailable(&manager);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn environment_manager_rejects_empty_environment_id() {
|
|
let snapshot = EnvironmentProviderSnapshot {
|
|
environments: vec![("".to_string(), Environment::default_for_tests())],
|
|
default: EnvironmentDefault::Disabled,
|
|
include_local: false,
|
|
};
|
|
let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
|
.expect_err("empty id should fail");
|
|
|
|
assert_eq!(
|
|
err.to_string(),
|
|
"exec-server protocol error: environment id cannot be empty"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn environment_manager_rejects_provider_supplied_local_environment() {
|
|
let snapshot = EnvironmentProviderSnapshot {
|
|
environments: vec![(
|
|
LOCAL_ENVIRONMENT_ID.to_string(),
|
|
Environment::default_for_tests(),
|
|
)],
|
|
default: EnvironmentDefault::Disabled,
|
|
include_local: false,
|
|
};
|
|
let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
|
.expect_err("local id should fail");
|
|
|
|
assert_eq!(
|
|
err.to_string(),
|
|
"exec-server protocol error: environment id `local` is reserved for EnvironmentManager"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn environment_manager_uses_explicit_provider_default() {
|
|
let snapshot = EnvironmentProviderSnapshot {
|
|
environments: vec![(
|
|
"devbox".to_string(),
|
|
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
|
|
.expect("remote environment"),
|
|
)],
|
|
default: EnvironmentDefault::EnvironmentId("devbox".to_string()),
|
|
include_local: true,
|
|
};
|
|
let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
|
.expect("manager");
|
|
|
|
assert_eq!(manager.default_environment_id(), Some("devbox"));
|
|
assert_eq!(
|
|
manager.default_environment_ids(),
|
|
vec!["devbox".to_string(), LOCAL_ENVIRONMENT_ID.to_string()]
|
|
);
|
|
assert!(manager.default_environment().expect("default").is_remote());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn environment_manager_disables_provider_default() {
|
|
let snapshot = EnvironmentProviderSnapshot {
|
|
environments: vec![(
|
|
"devbox".to_string(),
|
|
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
|
|
.expect("remote environment"),
|
|
)],
|
|
default: EnvironmentDefault::Disabled,
|
|
include_local: true,
|
|
};
|
|
let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
|
.expect("manager");
|
|
|
|
assert_eq!(manager.default_environment_id(), None);
|
|
assert!(manager.default_environment().is_none());
|
|
assert!(Arc::ptr_eq(
|
|
&manager
|
|
.get_environment(LOCAL_ENVIRONMENT_ID)
|
|
.expect("local environment"),
|
|
&manager.try_local_environment().expect("local environment")
|
|
));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn environment_manager_rejects_unknown_provider_default() {
|
|
let snapshot = EnvironmentProviderSnapshot {
|
|
environments: vec![(
|
|
"devbox".to_string(),
|
|
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
|
|
.expect("remote environment"),
|
|
)],
|
|
default: EnvironmentDefault::EnvironmentId("missing".to_string()),
|
|
include_local: true,
|
|
};
|
|
let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
|
.expect_err("unknown default should fail");
|
|
|
|
assert_eq!(
|
|
err.to_string(),
|
|
"exec-server protocol error: default environment `missing` is not configured"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn environment_manager_includes_local_for_default_provider_without_url() {
|
|
let manager = EnvironmentManager::create_for_tests(
|
|
/*exec_server_url*/ None,
|
|
Some(test_runtime_paths()),
|
|
)
|
|
.await;
|
|
|
|
let environment = manager.default_environment().expect("default environment");
|
|
assert_eq!(manager.default_environment_id(), Some(LOCAL_ENVIRONMENT_ID));
|
|
assert!(Arc::ptr_eq(
|
|
&environment,
|
|
&manager
|
|
.get_environment(LOCAL_ENVIRONMENT_ID)
|
|
.expect("local environment")
|
|
));
|
|
assert!(Arc::ptr_eq(
|
|
&environment,
|
|
&manager.try_local_environment().expect("local environment")
|
|
));
|
|
assert!(!environment.is_remote());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn environment_manager_carries_local_runtime_paths() {
|
|
let runtime_paths = test_runtime_paths();
|
|
let manager = EnvironmentManager::create_for_tests(
|
|
/*exec_server_url*/ None,
|
|
Some(runtime_paths.clone()),
|
|
)
|
|
.await;
|
|
|
|
let environment = manager.try_local_environment().expect("local environment");
|
|
|
|
assert_eq!(environment.local_runtime_paths(), Some(&runtime_paths));
|
|
let manager = EnvironmentManager::create_for_tests(
|
|
environment.exec_server_url(),
|
|
Some(
|
|
environment
|
|
.local_runtime_paths()
|
|
.expect("local runtime paths")
|
|
.clone(),
|
|
),
|
|
)
|
|
.await;
|
|
let environment = manager.try_local_environment().expect("local environment");
|
|
assert_eq!(environment.local_runtime_paths(), Some(&runtime_paths));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn environment_manager_omits_default_provider_local_lookup_when_default_disabled() {
|
|
let manager = EnvironmentManager::create_for_tests(
|
|
Some("none".to_string()),
|
|
Some(test_runtime_paths()),
|
|
)
|
|
.await;
|
|
|
|
assert!(manager.default_environment().is_none());
|
|
assert_eq!(manager.default_environment_id(), None);
|
|
assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
|
|
assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none());
|
|
assert_local_environment_unavailable(&manager);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn environment_manager_snapshot_without_local_environment_disables_local_default() {
|
|
let mut snapshot = EnvironmentProviderSnapshot {
|
|
environments: Vec::new(),
|
|
default: EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string()),
|
|
include_local: true,
|
|
};
|
|
snapshot.include_local = false;
|
|
snapshot.default = EnvironmentDefault::Disabled;
|
|
let manager =
|
|
EnvironmentManager::from_snapshot(snapshot, /*local_runtime_paths*/ None)
|
|
.expect("environment manager");
|
|
|
|
assert!(manager.default_environment().is_none());
|
|
assert_eq!(manager.default_environment_id(), None);
|
|
assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
|
|
assert_local_environment_unavailable(&manager);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn get_environment_returns_none_for_unknown_id() {
|
|
let manager = EnvironmentManager::default_for_tests();
|
|
|
|
assert!(manager.get_environment("does-not-exist").is_none());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn environment_manager_upserts_named_remote_environment() {
|
|
let reserved_listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
|
.await
|
|
.expect("test exec-server port should bind");
|
|
let exec_server_address = reserved_listener
|
|
.local_addr()
|
|
.expect("test exec-server should have an address");
|
|
drop(reserved_listener);
|
|
let exec_server_url = format!("ws://{exec_server_address}");
|
|
let exec_server_url_for_task = exec_server_url.clone();
|
|
let exec_server_task = tokio::spawn(async move {
|
|
crate::run_main(&exec_server_url_for_task, test_runtime_paths()).await
|
|
});
|
|
tokio::time::timeout(Duration::from_secs(1), async {
|
|
loop {
|
|
if tokio::net::TcpStream::connect(exec_server_address)
|
|
.await
|
|
.is_ok()
|
|
{
|
|
break;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("test exec-server should start");
|
|
|
|
let manager = EnvironmentManager::without_environments();
|
|
|
|
let pending = manager
|
|
.register_pending_environment("executor-a".to_string())
|
|
.expect("pending environment");
|
|
assert!(pending.is_remote() && !pending.is_ready());
|
|
let exec_backend = pending.get_exec_backend();
|
|
let http_client = pending.get_http_client();
|
|
let filesystem = pending.get_filesystem();
|
|
let waiting_exec_backend = Arc::clone(&exec_backend);
|
|
let exec_waiter = tokio::spawn(async move {
|
|
waiting_exec_backend
|
|
.start(crate::ExecParams {
|
|
process_id: ProcessId::from("pending-environment-proc"),
|
|
argv: vec!["true".to_string()],
|
|
cwd: std::env::current_dir().expect("read current dir"),
|
|
env_policy: None,
|
|
env: Default::default(),
|
|
tty: false,
|
|
pipe_stdin: false,
|
|
arg0: None,
|
|
})
|
|
.await
|
|
});
|
|
tokio::task::yield_now().await;
|
|
assert!(!exec_waiter.is_finished());
|
|
|
|
manager
|
|
.upsert_environment("executor-a".to_string(), exec_server_url.clone())
|
|
.expect("remote environment");
|
|
let started = tokio::time::timeout(Duration::from_secs(2), exec_waiter)
|
|
.await
|
|
.expect("pending capability should wake")
|
|
.expect("pending capability task should not panic")
|
|
.expect("pending capability should execute");
|
|
assert_eq!(
|
|
started.process.process_id().as_str(),
|
|
"pending-environment-proc"
|
|
);
|
|
let first = manager
|
|
.get_environment("executor-a")
|
|
.expect("first remote environment");
|
|
assert!(Arc::ptr_eq(&pending, &first) && first.is_ready());
|
|
assert!(first.is_remote());
|
|
assert_eq!(
|
|
first.exec_server_url().as_deref(),
|
|
Some(exec_server_url.as_str())
|
|
);
|
|
let loaded_info = tokio::time::timeout(Duration::from_secs(1), async {
|
|
loop {
|
|
if let Some(info) = first.current_info() {
|
|
break info;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("environment metadata should load in the background");
|
|
assert_eq!(
|
|
first.info().await.expect("cached environment info"),
|
|
loaded_info
|
|
);
|
|
assert_eq!(manager.default_environment_id(), None);
|
|
|
|
manager
|
|
.upsert_environment("executor-a".to_string(), "ws://127.0.0.1:9876".to_string())
|
|
.expect("updated remote environment");
|
|
let second = manager
|
|
.get_environment("executor-a")
|
|
.expect("second remote environment");
|
|
assert!(second.is_remote());
|
|
assert_eq!(
|
|
second.exec_server_url().as_deref(),
|
|
Some("ws://127.0.0.1:9876")
|
|
);
|
|
assert_eq!(second.current_info(), None);
|
|
assert!(Arc::ptr_eq(&first, &second));
|
|
assert!(Arc::ptr_eq(&exec_backend, &second.get_exec_backend()));
|
|
assert!(Arc::ptr_eq(&http_client, &second.get_http_client()));
|
|
assert!(Arc::ptr_eq(&filesystem, &second.get_filesystem()));
|
|
|
|
exec_server_task.abort();
|
|
let _ = exec_server_task.await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn environment_manager_rejects_reserved_dynamic_environment_id() {
|
|
let manager = EnvironmentManager::default_for_tests();
|
|
|
|
let errors = vec![
|
|
manager
|
|
.register_pending_environment(LOCAL_ENVIRONMENT_ID.to_string())
|
|
.expect_err("pending local environment should fail")
|
|
.to_string(),
|
|
manager
|
|
.upsert_environment(
|
|
LOCAL_ENVIRONMENT_ID.to_string(),
|
|
"ws://127.0.0.1:8765".to_string(),
|
|
)
|
|
.expect_err("remote local environment should fail")
|
|
.to_string(),
|
|
];
|
|
|
|
assert_eq!(
|
|
errors,
|
|
vec![
|
|
"exec-server protocol error: environment id `local` is reserved for EnvironmentManager",
|
|
"exec-server protocol error: environment id `local` is reserved for EnvironmentManager",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn environment_manager_rejects_empty_remote_environment_url() {
|
|
let manager = EnvironmentManager::without_environments();
|
|
|
|
let err = manager
|
|
.upsert_environment("executor-a".to_string(), String::new())
|
|
.expect_err("empty URL should fail");
|
|
|
|
assert_eq!(
|
|
err.to_string(),
|
|
"exec-server protocol error: remote environment requires an exec-server url"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn default_environment_has_ready_local_executor() {
|
|
let environment = Environment::default_for_tests();
|
|
|
|
let response = environment
|
|
.get_exec_backend()
|
|
.start(crate::ExecParams {
|
|
process_id: ProcessId::from("default-env-proc"),
|
|
argv: vec!["true".to_string()],
|
|
cwd: std::env::current_dir().expect("read current dir"),
|
|
env_policy: None,
|
|
env: Default::default(),
|
|
tty: false,
|
|
pipe_stdin: false,
|
|
arg0: None,
|
|
})
|
|
.await
|
|
.expect("start process");
|
|
|
|
assert_eq!(response.process.process_id().as_str(), "default-env-proc");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_environment_rejects_sandboxed_filesystem_without_runtime_paths() {
|
|
let environment = Environment::default_for_tests();
|
|
let path = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(
|
|
std::env::current_exe().expect("current exe").as_path(),
|
|
)
|
|
.expect("absolute current exe");
|
|
let path = codex_utils_path_uri::PathUri::from_abs_path(&path).expect("path URI");
|
|
let sandbox = crate::FileSystemSandboxContext::from_permission_profile(
|
|
codex_protocol::models::PermissionProfile::from_runtime_permissions(
|
|
&codex_protocol::permissions::FileSystemSandboxPolicy::restricted(Vec::new()),
|
|
codex_protocol::permissions::NetworkSandboxPolicy::Restricted,
|
|
),
|
|
);
|
|
|
|
let err = environment
|
|
.get_filesystem()
|
|
.read_file(&path, Some(&sandbox))
|
|
.await
|
|
.expect_err("sandboxed read should require runtime paths");
|
|
|
|
assert_eq!(
|
|
err.to_string(),
|
|
"sandboxed filesystem operations require configured runtime paths"
|
|
);
|
|
}
|
|
}
|