Files
codex/codex-rs/exec-server/src/environment.rs
2026-05-07 16:22:06 -07:00

688 lines
25 KiB
Rust

use std::collections::HashMap;
use std::sync::Arc;
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::remote_file_system::RemoteFileSystem;
use crate::remote_process::RemoteProcess;
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.
/// `from_codex_home` preserves the legacy `CODEX_EXEC_SERVER_URL` behavior when
/// no `environments.toml` file is present, while provider-based construction
/// accepts a provider-supplied snapshot.
///
/// Setting `CODEX_EXEC_SERVER_URL=none` disables environment access by leaving
/// the default environment unset while still keeping an explicit local
/// environment available through `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: HashMap<String, Arc<Environment>>,
local_environment: Arc<Environment>,
}
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: HashMap::from([(
LOCAL_ENVIRONMENT_ID.to_string(),
Arc::new(Environment::default_for_tests()),
)]),
local_environment: Arc::new(Environment::default_for_tests()),
}
}
/// Builds a test-only manager with environment access disabled.
pub fn disabled_for_tests(local_runtime_paths: ExecServerRuntimePaths) -> Self {
Self {
default_environment: None,
environments: HashMap::new(),
local_environment: Arc::new(Environment::local(local_runtime_paths)),
}
}
/// 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: 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. Callers that ignore user config
/// should use [`Self::from_env`] instead.
pub async fn from_codex_home(
codex_home: impl AsRef<std::path::Path>,
local_runtime_paths: ExecServerRuntimePaths,
) -> Result<Self, ExecServerError> {
let provider = environment_provider_from_codex_home(codex_home.as_ref())?;
Self::from_provider(provider.as_ref(), local_runtime_paths).await
}
/// 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: ExecServerRuntimePaths,
) -> Result<Self, ExecServerError> {
let provider = DefaultEnvironmentProvider::from_env();
Self::from_provider(&provider, local_runtime_paths).await
}
async fn from_default_provider_url(
exec_server_url: Option<String>,
local_runtime_paths: ExecServerRuntimePaths,
) -> Self {
let provider = DefaultEnvironmentProvider::new(exec_server_url);
match Self::from_provider(&provider, local_runtime_paths).await {
Ok(manager) => manager,
Err(err) => panic!("default provider should create valid environments: {err}"),
}
}
/// 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,
{
Self::from_provider_snapshot(
provider.snapshot(&local_runtime_paths).await?,
local_runtime_paths,
)
}
fn from_provider_snapshot(
snapshot: EnvironmentProviderSnapshot,
local_runtime_paths: ExecServerRuntimePaths,
) -> Result<Self, ExecServerError> {
let EnvironmentProviderSnapshot {
environments,
default,
} = snapshot;
for id in environments.keys() {
if id.is_empty() {
return Err(ExecServerError::Protocol(
"environment id cannot be empty".to_string(),
));
}
}
let default_environment = match default {
EnvironmentDefault::Disabled => None,
EnvironmentDefault::EnvironmentId(environment_id) => {
if !environments.contains_key(&environment_id) {
return Err(ExecServerError::Protocol(format!(
"default environment `{environment_id}` is not configured"
)));
}
Some(environment_id)
}
};
let local_environment = Arc::new(Environment::local(local_runtime_paths));
let environments = environments
.into_iter()
.map(|(id, environment)| (id, Arc::new(environment)))
.collect();
Ok(Self {
default_environment,
environments,
local_environment,
})
}
/// 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 local environment instance used for internal runtime work.
pub fn local_environment(&self) -> Arc<Environment> {
Arc::clone(&self.local_environment)
}
/// Returns a named environment instance.
pub fn get_environment(&self, environment_id: &str) -> Option<Arc<Environment>> {
self.environments.get(environment_id).cloned()
}
}
/// Concrete execution/filesystem environment selected for a session.
///
/// This bundles the selected backend metadata together with the local runtime
/// paths used by filesystem helpers.
#[derive(Clone)]
pub struct Environment {
remote_transport: Option<ExecServerTransportParams>,
exec_backend: Arc<dyn ExecBackend>,
filesystem: Arc<dyn ExecutorFileSystem>,
http_client: Arc<dyn HttpClient>,
local_runtime_paths: Option<ExecServerRuntimePaths>,
}
impl Environment {
/// Builds a test-only local environment without configured sandbox helper paths.
pub fn default_for_tests() -> Self {
Self {
remote_transport: None,
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("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_transport: None,
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),
}
}
pub(crate) fn remote_inner(
exec_server_url: String,
local_runtime_paths: Option<ExecServerRuntimePaths>,
) -> Self {
Self::remote_with_transport(
ExecServerTransportParams::WebSocketUrl(exec_server_url),
local_runtime_paths,
)
}
pub(crate) fn remote_with_transport(
transport_params: ExecServerTransportParams,
local_runtime_paths: Option<ExecServerRuntimePaths>,
) -> Self {
let client = LazyRemoteExecServerClient::new(transport_params.clone());
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_transport: Some(transport_params),
exec_backend,
filesystem,
http_client: Arc::new(client),
local_runtime_paths,
}
}
pub fn is_remote(&self) -> bool {
self.remote_transport.is_some()
}
/// Returns the remote exec-server URL when this environment is remote.
pub(crate) fn exec_server_url(&self) -> Option<&str> {
match self.remote_transport.as_ref() {
Some(ExecServerTransportParams::WebSocketUrl(url)) => Some(url.as_str()),
Some(ExecServerTransportParams::StdioCommand(_)) | None => None,
}
}
pub fn local_runtime_paths(&self) -> Option<&ExecServerRuntimePaths> {
self.local_runtime_paths.as_ref()
}
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)
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::sync::Arc;
use super::Environment;
use super::EnvironmentManager;
use super::LOCAL_ENVIRONMENT_ID;
use super::REMOTE_ENVIRONMENT_ID;
use crate::EnvironmentProvider;
use crate::ExecServerError;
use crate::ExecServerRuntimePaths;
use crate::ProcessId;
use crate::environment_provider::EnvironmentDefault;
use crate::environment_provider::EnvironmentProviderSnapshot;
use pretty_assertions::assert_eq;
struct TestEnvironmentProvider {
snapshot: EnvironmentProviderSnapshot,
}
#[async_trait::async_trait]
impl EnvironmentProvider for TestEnvironmentProvider {
async fn snapshot(
&self,
_local_runtime_paths: &ExecServerRuntimePaths,
) -> Result<EnvironmentProviderSnapshot, ExecServerError> {
Ok(self.snapshot.clone())
}
}
fn test_runtime_paths() -> ExecServerRuntimePaths {
ExecServerRuntimePaths::new(
std::env::current_exe().expect("current exe"),
/*codex_linux_sandbox_exe*/ None,
)
.expect("runtime paths")
}
#[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());
}
#[tokio::test]
async fn environment_manager_normalizes_empty_url() {
let manager =
EnvironmentManager::create_for_tests(Some(String::new()), test_runtime_paths()).await;
let environment = manager.default_environment().expect("default environment");
assert_eq!(manager.default_environment_id(), Some(LOCAL_ENVIRONMENT_ID));
assert!(!environment.is_remote());
assert!(
!manager
.get_environment(LOCAL_ENVIRONMENT_ID)
.expect("local environment")
.is_remote()
);
assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none());
}
#[tokio::test]
async fn disabled_environment_manager_has_no_default_but_keeps_explicit_local_environment() {
let manager = EnvironmentManager::disabled_for_tests(test_runtime_paths());
assert!(manager.default_environment().is_none());
assert_eq!(manager.default_environment_id(), None);
assert!(!manager.local_environment().is_remote());
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()),
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(), 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)
.expect("local environment")
.is_remote()
);
assert!(!manager.local_environment().is_remote());
}
#[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_provider() {
let provider = TestEnvironmentProvider {
snapshot: EnvironmentProviderSnapshot {
environments: HashMap::from([(
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()),
},
};
let manager = EnvironmentManager::from_provider(&provider, test_runtime_paths())
.await
.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!(!manager.local_environment().is_remote());
}
#[tokio::test]
async fn environment_manager_rejects_empty_environment_id() {
let provider = TestEnvironmentProvider {
snapshot: EnvironmentProviderSnapshot {
environments: HashMap::from([("".to_string(), Environment::default_for_tests())]),
default: EnvironmentDefault::Disabled,
},
};
let err = EnvironmentManager::from_provider(&provider, test_runtime_paths())
.await
.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_uses_explicit_provider_default() {
let provider = TestEnvironmentProvider {
snapshot: EnvironmentProviderSnapshot {
environments: HashMap::from([
(
LOCAL_ENVIRONMENT_ID.to_string(),
Environment::default_for_tests(),
),
(
"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()),
},
};
let manager = EnvironmentManager::from_provider(&provider, test_runtime_paths())
.await
.expect("manager");
assert_eq!(manager.default_environment_id(), Some("devbox"));
assert!(manager.default_environment().expect("default").is_remote());
}
#[tokio::test]
async fn environment_manager_disables_provider_default() {
let provider = TestEnvironmentProvider {
snapshot: EnvironmentProviderSnapshot {
environments: HashMap::from([(
LOCAL_ENVIRONMENT_ID.to_string(),
Environment::default_for_tests(),
)]),
default: EnvironmentDefault::Disabled,
},
};
let manager = EnvironmentManager::from_provider(&provider, test_runtime_paths())
.await
.expect("manager");
assert_eq!(manager.default_environment_id(), None);
assert!(manager.default_environment().is_none());
assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_some());
}
#[tokio::test]
async fn environment_manager_rejects_unknown_provider_default() {
let provider = TestEnvironmentProvider {
snapshot: EnvironmentProviderSnapshot {
environments: HashMap::from([(
LOCAL_ENVIRONMENT_ID.to_string(),
Environment::default_for_tests(),
)]),
default: EnvironmentDefault::EnvironmentId("missing".to_string()),
},
};
let err = EnvironmentManager::from_provider(&provider, test_runtime_paths())
.await
.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_uses_provider_supplied_local_environment() {
let manager = EnvironmentManager::create_for_tests(
/*exec_server_url*/ None,
test_runtime_paths(),
)
.await;
assert_eq!(manager.default_environment_id(), Some(LOCAL_ENVIRONMENT_ID));
let provider_local = manager
.get_environment(LOCAL_ENVIRONMENT_ID)
.expect("provider local environment");
assert!(!provider_local.is_remote());
assert!(!manager.local_environment().is_remote());
assert!(!Arc::ptr_eq(&provider_local, &manager.local_environment()));
}
#[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,
runtime_paths.clone(),
)
.await;
let environment = manager.default_environment().expect("default environment");
assert_eq!(environment.local_runtime_paths(), Some(&runtime_paths));
let manager = EnvironmentManager::create_for_tests(
environment.exec_server_url().map(str::to_owned),
environment
.local_runtime_paths()
.expect("local runtime paths")
.clone(),
)
.await;
let environment = manager.default_environment().expect("default environment");
assert_eq!(environment.local_runtime_paths(), Some(&runtime_paths));
}
#[tokio::test]
async fn disabled_environment_manager_has_no_default_environment() {
let manager = EnvironmentManager::disabled_for_tests(test_runtime_paths());
assert!(manager.default_environment().is_none());
assert_eq!(manager.default_environment_id(), None);
}
#[tokio::test]
async fn environment_manager_keeps_default_provider_local_lookup_when_default_disabled() {
let manager =
EnvironmentManager::create_for_tests(Some("none".to_string()), test_runtime_paths())
.await;
assert!(manager.default_environment().is_none());
assert_eq!(manager.default_environment_id(), None);
assert!(
!manager
.get_environment(LOCAL_ENVIRONMENT_ID)
.expect("local environment")
.is_remote()
);
assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none());
}
#[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 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 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"
);
}
}