mirror of
https://github.com/openai/codex.git
synced 2026-05-22 20:14:17 +00:00
Compare commits
21 Commits
cooper/cod
...
starr/cca-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63d034844a | ||
|
|
d762993b67 | ||
|
|
49c3495b60 | ||
|
|
b63979021f | ||
|
|
b83f8fb0ea | ||
|
|
416c978757 | ||
|
|
9b81714f50 | ||
|
|
d265ea2cf3 | ||
|
|
4fdb044248 | ||
|
|
4af256e946 | ||
|
|
85ec777c89 | ||
|
|
c082f8e90d | ||
|
|
51e12c7a48 | ||
|
|
e48b748c00 | ||
|
|
03af15743a | ||
|
|
6e47df9451 | ||
|
|
e9c7c88730 | ||
|
|
3f11ea6503 | ||
|
|
2ac71d4e0b | ||
|
|
1643f20b47 | ||
|
|
0b06bcf47a |
@@ -2178,11 +2178,13 @@ mod tests {
|
||||
let environment_manager = Arc::new(
|
||||
EnvironmentManager::create_for_tests(
|
||||
Some("ws://127.0.0.1:8765".to_string()),
|
||||
ExecServerRuntimePaths::new(
|
||||
std::env::current_exe().expect("current exe"),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("runtime paths"),
|
||||
Some(
|
||||
ExecServerRuntimePaths::new(
|
||||
std::env::current_exe().expect("current exe"),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("runtime paths"),
|
||||
),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
@@ -445,9 +445,9 @@ pub async fn run_main_with_transport_options(
|
||||
arg0_paths.codex_linux_sandbox_exe.clone(),
|
||||
)?;
|
||||
let environment_manager = if loader_overrides.ignore_user_config {
|
||||
EnvironmentManager::from_env(local_runtime_paths).await
|
||||
EnvironmentManager::from_env(Some(local_runtime_paths)).await
|
||||
} else {
|
||||
EnvironmentManager::from_codex_home(codex_home.clone(), local_runtime_paths).await
|
||||
EnvironmentManager::from_codex_home(codex_home.clone(), Some(local_runtime_paths)).await
|
||||
}
|
||||
.map(Arc::new)
|
||||
.map_err(std::io::Error::other)?;
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::sync::atomic::AtomicBool;
|
||||
use crate::attestation::app_server_attestation_provider;
|
||||
use crate::config_manager::ConfigManager;
|
||||
use crate::connection_rpc_gate::ConnectionRpcGate;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use crate::extensions::guardian_agent_spawner;
|
||||
use crate::extensions::thread_extensions;
|
||||
@@ -165,10 +166,11 @@ pub(crate) struct MessageProcessor {
|
||||
command_exec_processor: CommandExecRequestProcessor,
|
||||
process_exec_processor: ProcessExecRequestProcessor,
|
||||
config_processor: ConfigRequestProcessor,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
environment_processor: EnvironmentRequestProcessor,
|
||||
external_agent_config_processor: ExternalAgentConfigRequestProcessor,
|
||||
feedback_processor: FeedbackRequestProcessor,
|
||||
fs_processor: FsRequestProcessor,
|
||||
fs_processor: Option<FsRequestProcessor>,
|
||||
git_processor: GitRequestProcessor,
|
||||
initialize_processor: InitializeRequestProcessor,
|
||||
marketplace_processor: MarketplaceRequestProcessor,
|
||||
@@ -272,6 +274,22 @@ pub(crate) struct MessageProcessorArgs {
|
||||
}
|
||||
|
||||
impl MessageProcessor {
|
||||
fn fs_processor(&self) -> Result<&FsRequestProcessor, JSONRPCErrorError> {
|
||||
self.fs_processor
|
||||
.as_ref()
|
||||
.ok_or_else(|| internal_error("local filesystem is not configured"))
|
||||
}
|
||||
|
||||
fn require_local_environment(&self) -> Result<(), JSONRPCErrorError> {
|
||||
// CCA filters these local-only RPCs before they reach app-server, but
|
||||
// keep a Codex-side backstop so no-local app-server modes fail safely
|
||||
// if a client still invokes one directly.
|
||||
self.environment_manager
|
||||
.has_local_environment()
|
||||
.then_some(())
|
||||
.ok_or_else(|| internal_error("local environment is not configured"))
|
||||
}
|
||||
|
||||
/// Create a new `MessageProcessor`, retaining a handle to the outgoing
|
||||
/// `Sender` so handlers can enqueue messages to be written to stdout.
|
||||
pub(crate) fn new(args: MessageProcessorArgs) -> Self {
|
||||
@@ -301,6 +319,7 @@ impl MessageProcessor {
|
||||
// affect per-thread behavior, but they must not move newly started,
|
||||
// resumed, or forked threads to a different persistence backend/root.
|
||||
let thread_store = codex_core::thread_store_from_config(config.as_ref(), state_db.clone());
|
||||
let environment_manager_for_requests = Arc::clone(&environment_manager);
|
||||
let thread_manager = Arc::new_cyclic(|thread_manager| {
|
||||
ThreadManager::new(
|
||||
config.as_ref(),
|
||||
@@ -443,7 +462,6 @@ impl MessageProcessor {
|
||||
Some(on_effective_plugins_changed),
|
||||
);
|
||||
}
|
||||
let fs_watch_manager = FsWatchManager::new(outgoing.clone());
|
||||
let config_processor = ConfigRequestProcessor::new(
|
||||
outgoing.clone(),
|
||||
config_manager.clone(),
|
||||
@@ -461,13 +479,17 @@ impl MessageProcessor {
|
||||
);
|
||||
let environment_processor =
|
||||
EnvironmentRequestProcessor::new(thread_manager.environment_manager());
|
||||
let fs_processor = FsRequestProcessor::new(
|
||||
thread_manager
|
||||
.environment_manager()
|
||||
.local_environment()
|
||||
.get_filesystem(),
|
||||
fs_watch_manager,
|
||||
);
|
||||
// `fs/*` is a local-host filesystem surface. Do not construct it when
|
||||
// the manager intentionally has no local environment.
|
||||
let fs_processor = thread_manager
|
||||
.environment_manager()
|
||||
.try_local_environment()
|
||||
.map(|environment| {
|
||||
FsRequestProcessor::new(
|
||||
environment.get_filesystem(),
|
||||
FsWatchManager::new(outgoing.clone()),
|
||||
)
|
||||
});
|
||||
let windows_sandbox_processor = WindowsSandboxRequestProcessor::new(
|
||||
outgoing.clone(),
|
||||
Arc::clone(&config),
|
||||
@@ -482,6 +504,7 @@ impl MessageProcessor {
|
||||
command_exec_processor,
|
||||
process_exec_processor,
|
||||
config_processor,
|
||||
environment_manager: environment_manager_for_requests,
|
||||
environment_processor,
|
||||
external_agent_config_processor,
|
||||
feedback_processor,
|
||||
@@ -705,7 +728,9 @@ impl MessageProcessor {
|
||||
) {
|
||||
session_state.rpc_gate.shutdown().await;
|
||||
self.outgoing.connection_closed(connection_id).await;
|
||||
self.fs_processor.connection_closed(connection_id).await;
|
||||
if let Some(fs_processor) = &self.fs_processor {
|
||||
fs_processor.connection_closed(connection_id).await;
|
||||
}
|
||||
self.command_exec_processor
|
||||
.connection_closed(connection_id)
|
||||
.await;
|
||||
@@ -911,47 +936,47 @@ impl MessageProcessor {
|
||||
self.environment_processor.environment_add(params).await
|
||||
}
|
||||
ClientRequest::FsReadFile { params, .. } => self
|
||||
.fs_processor
|
||||
.fs_processor()?
|
||||
.read_file(params)
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::FsWriteFile { params, .. } => self
|
||||
.fs_processor
|
||||
.fs_processor()?
|
||||
.write_file(params)
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::FsCreateDirectory { params, .. } => self
|
||||
.fs_processor
|
||||
.fs_processor()?
|
||||
.create_directory(params)
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::FsGetMetadata { params, .. } => self
|
||||
.fs_processor
|
||||
.fs_processor()?
|
||||
.get_metadata(params)
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::FsReadDirectory { params, .. } => self
|
||||
.fs_processor
|
||||
.fs_processor()?
|
||||
.read_directory(params)
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::FsRemove { params, .. } => self
|
||||
.fs_processor
|
||||
.fs_processor()?
|
||||
.remove(params)
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::FsCopy { params, .. } => self
|
||||
.fs_processor
|
||||
.fs_processor()?
|
||||
.copy(params)
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::FsWatch { params, .. } => self
|
||||
.fs_processor
|
||||
.fs_processor()?
|
||||
.watch(connection_id, params)
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::FsUnwatch { params, .. } => self
|
||||
.fs_processor
|
||||
.fs_processor()?
|
||||
.unwatch(connection_id, params)
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
@@ -1280,6 +1305,7 @@ impl MessageProcessor {
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::OneOffCommandExec { params, .. } => {
|
||||
self.require_local_environment()?;
|
||||
self.command_exec_processor
|
||||
.one_off_command_exec(&request_id, params)
|
||||
.await
|
||||
@@ -1299,11 +1325,13 @@ impl MessageProcessor {
|
||||
.command_exec_terminate(request_id.clone(), params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ProcessSpawn { params, .. } => self
|
||||
.process_exec_processor
|
||||
.process_spawn(request_id.clone(), params)
|
||||
.await
|
||||
.map(|()| None),
|
||||
ClientRequest::ProcessSpawn { params, .. } => {
|
||||
self.require_local_environment()?;
|
||||
self.process_exec_processor
|
||||
.process_spawn(request_id.clone(), params)
|
||||
.await
|
||||
.map(|()| None)
|
||||
}
|
||||
ClientRequest::ProcessWriteStdin { params, .. } => {
|
||||
self.process_exec_processor
|
||||
.process_write_stdin(request_id.clone(), params)
|
||||
|
||||
@@ -205,17 +205,13 @@ impl McpRequestProcessor {
|
||||
.await;
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
let runtime_environment = match environment_manager.default_environment() {
|
||||
Some(environment) => {
|
||||
// Status listing has no turn cwd. This fallback is used only
|
||||
// by executor-backed stdio MCPs whose config omits `cwd`.
|
||||
McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf())
|
||||
}
|
||||
None => McpRuntimeEnvironment::new(
|
||||
environment_manager.local_environment(),
|
||||
config.cwd.to_path_buf(),
|
||||
),
|
||||
};
|
||||
// Status listing has no turn cwd. Prefer the configured default env,
|
||||
// then configured local if present; do not manufacture a hidden local
|
||||
// env in no-local modes.
|
||||
let runtime_environment = McpRuntimeEnvironment::new(
|
||||
environment_manager.default_or_local_environment(),
|
||||
config.cwd.to_path_buf(),
|
||||
);
|
||||
|
||||
tokio::spawn(async move {
|
||||
Self::list_mcp_server_status_task(
|
||||
@@ -369,15 +365,14 @@ impl McpRequestProcessor {
|
||||
.to_mcp_config(self.thread_manager.plugins_manager().as_ref())
|
||||
.await;
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let runtime_environment = {
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
let environment = environment_manager
|
||||
.default_environment()
|
||||
.unwrap_or_else(|| environment_manager.local_environment());
|
||||
// Resource reads without a thread have no turn cwd. This fallback
|
||||
// is used only by executor-backed stdio MCPs whose config omits `cwd`.
|
||||
McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf())
|
||||
};
|
||||
let environment_manager = self.thread_manager.environment_manager();
|
||||
// Resource reads without a thread have no turn cwd. Prefer the
|
||||
// configured default env, then configured local if present; do not
|
||||
// manufacture a hidden local env in no-local modes.
|
||||
let runtime_environment = McpRuntimeEnvironment::new(
|
||||
environment_manager.default_or_local_environment(),
|
||||
config.cwd.to_path_buf(),
|
||||
);
|
||||
let request_id = request_id.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
|
||||
@@ -1738,6 +1738,16 @@ impl ThreadRequestProcessor {
|
||||
if command.is_empty() {
|
||||
return Err(invalid_request("command must not be empty"));
|
||||
}
|
||||
// `thread/shellCommand` is app-server's local-host shell escape hatch,
|
||||
// not the normal turn-selected shell tool path.
|
||||
if self
|
||||
.thread_manager
|
||||
.environment_manager()
|
||||
.try_local_environment()
|
||||
.is_none()
|
||||
{
|
||||
return Err(internal_error("local environment is not configured"));
|
||||
}
|
||||
|
||||
let (_, thread) = self.load_thread(&thread_id).await?;
|
||||
self.submit_core_op(
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SandboxPolicy;
|
||||
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
@@ -400,6 +401,45 @@ async fn command_exec_permission_profile_project_roots_use_command_cwd() -> Resu
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_returns_error_when_local_environment_is_disabled() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let command_request_id = mcp
|
||||
.send_command_exec_request(CommandExecParams {
|
||||
command: vec!["sh".to_string(), "-lc".to_string(), "true".to_string()],
|
||||
process_id: None,
|
||||
tty: false,
|
||||
stream_stdin: false,
|
||||
stream_stdout_stderr: false,
|
||||
output_bytes_cap: None,
|
||||
disable_output_cap: false,
|
||||
disable_timeout: false,
|
||||
timeout_ms: None,
|
||||
cwd: None,
|
||||
env: None,
|
||||
size: None,
|
||||
sandbox_policy: None,
|
||||
permission_profile: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let error = mcp
|
||||
.read_stream_until_error_message(RequestId::Integer(command_request_id))
|
||||
.await?;
|
||||
assert_eq!(error.error.message, "local environment is not configured");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_exec_rejects_sandbox_policy_with_permission_profile() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
|
||||
@@ -14,6 +14,7 @@ use codex_app_server_protocol::FsWatchResponse;
|
||||
use codex_app_server_protocol::FsWriteFileParams;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
@@ -119,6 +120,28 @@ async fn fs_get_metadata_returns_only_used_fields() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_methods_return_error_when_local_environment_is_disabled() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let absolute_file = codex_home.path().join("absolute.txt");
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let read_id = mcp
|
||||
.send_fs_read_file_request(codex_app_server_protocol::FsReadFileParams {
|
||||
path: absolute_path(absolute_file),
|
||||
})
|
||||
.await?;
|
||||
expect_error_message(&mut mcp, read_id, "local filesystem is not configured").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fs_get_metadata_reports_symlink() -> Result<()> {
|
||||
|
||||
@@ -6,6 +6,7 @@ use codex_app_server_protocol::ProcessExitedNotification;
|
||||
use codex_app_server_protocol::ProcessKillParams;
|
||||
use codex_app_server_protocol::ProcessSpawnParams;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
@@ -102,6 +103,33 @@ async fn process_spawn_returns_before_exit_and_emits_exit_notification() -> Resu
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn process_spawn_returns_error_when_local_environment_is_disabled() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let process_request_id = mcp
|
||||
.send_process_spawn_request(process_spawn_params(
|
||||
"disabled-process".to_string(),
|
||||
codex_home.path(),
|
||||
vec!["sh".to_string(), "-lc".to_string(), "true".to_string()],
|
||||
)?)
|
||||
.await?;
|
||||
let error = mcp
|
||||
.read_stream_until_error_message(RequestId::Integer(process_request_id))
|
||||
.await?;
|
||||
assert_eq!(error.error.message, "local environment is not configured");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn process_spawn_reports_buffered_output_cap_reached() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -32,6 +32,7 @@ use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_core::shell::default_user_shell;
|
||||
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
use codex_features::FEATURES;
|
||||
use codex_features::Feature;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -182,6 +183,49 @@ async fn thread_shell_command_history_responses_exclude_persisted_command_execut
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_shell_command_returns_error_when_local_environment_is_disabled() -> Result<()> {
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().join("codex_home");
|
||||
std::fs::create_dir(&codex_home)?;
|
||||
let server = create_mock_responses_server_sequence(vec![]).await;
|
||||
create_config_toml(
|
||||
codex_home.as_path(),
|
||||
&server.uri(),
|
||||
"never",
|
||||
&BTreeMap::default(),
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.as_path(),
|
||||
&[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
.await?;
|
||||
let start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
|
||||
let shell_id = mcp
|
||||
.send_thread_shell_command_request(ThreadShellCommandParams {
|
||||
thread_id: thread.id,
|
||||
command: "pwd".to_string(),
|
||||
})
|
||||
.await?;
|
||||
let error = mcp
|
||||
.read_stream_until_error_message(RequestId::Integer(shell_id))
|
||||
.await?;
|
||||
assert_eq!(error.error.message, "local environment is not configured");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_shell_command_uses_existing_active_turn() -> Result<()> {
|
||||
let tmp = TempDir::new()?;
|
||||
|
||||
@@ -847,6 +847,108 @@ async fn list_all_tools_adds_server_metadata_to_cached_tools() {
|
||||
assert_eq!(tool.server_origin.as_deref(), Some("https://docs.example"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let (tx_event, rx_event) = async_channel::unbounded();
|
||||
drop(rx_event);
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let mcp_servers = HashMap::from([
|
||||
(
|
||||
"stdio".to_string(),
|
||||
EffectiveMcpServer::configured(McpServerConfig {
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "echo".to_string(),
|
||||
args: Vec::new(),
|
||||
env: None,
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth: None,
|
||||
oauth_resource: None,
|
||||
tools: HashMap::new(),
|
||||
}),
|
||||
),
|
||||
(
|
||||
"http".to_string(),
|
||||
EffectiveMcpServer::configured(McpServerConfig {
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "http://127.0.0.1:1".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth: None,
|
||||
oauth_resource: None,
|
||||
tools: HashMap::new(),
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
let (manager, cancel_token) = McpConnectionManager::new(
|
||||
&mcp_servers,
|
||||
OAuthCredentialsStoreMode::default(),
|
||||
HashMap::new(),
|
||||
&approval_policy,
|
||||
String::new(),
|
||||
tx_event,
|
||||
PermissionProfile::default(),
|
||||
McpRuntimeEnvironment::new(/*environment*/ None, PathBuf::from("/tmp")),
|
||||
codex_home.path().to_path_buf(),
|
||||
CodexAppsToolsCacheKey {
|
||||
account_id: None,
|
||||
chatgpt_user_id: None,
|
||||
is_workspace_account: false,
|
||||
},
|
||||
/*host_owned_codex_apps_enabled*/ false,
|
||||
ElicitationCapability::default(),
|
||||
ToolPluginProvenance::default(),
|
||||
/*auth*/ None,
|
||||
/*elicitation_reviewer*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(manager.clients.contains_key("stdio"));
|
||||
assert!(manager.clients.contains_key("http"));
|
||||
assert!(
|
||||
!manager
|
||||
.wait_for_server_ready("stdio", Duration::from_millis(10))
|
||||
.await
|
||||
);
|
||||
let failures = manager
|
||||
.required_startup_failures(&["stdio".to_string()])
|
||||
.await;
|
||||
assert_eq!(failures.len(), 1);
|
||||
assert_eq!(failures[0].server, "stdio");
|
||||
assert_eq!(
|
||||
failures[0].error,
|
||||
"local stdio MCP server `stdio` requires a local environment"
|
||||
);
|
||||
cancel_token.cancel();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elicitation_capability_uses_2025_06_18_shape_for_form_only_support() {
|
||||
let capability = Some(ElicitationCapability::default());
|
||||
|
||||
@@ -566,6 +566,9 @@ async fn make_rmcp_client(
|
||||
let config = match server.launch() {
|
||||
McpServerLaunch::Configured(config) => config.as_ref().clone(),
|
||||
};
|
||||
if let Some(reason) = runtime_environment.startup_unavailable_reason(server_name, &config) {
|
||||
return Err(StartupOutcomeError::from(anyhow!(reason)));
|
||||
}
|
||||
let McpServerConfig {
|
||||
transport,
|
||||
experimental_environment,
|
||||
@@ -573,14 +576,7 @@ async fn make_rmcp_client(
|
||||
} = config;
|
||||
let remote_environment = match experimental_environment.as_deref() {
|
||||
None | Some("local") => false,
|
||||
Some("remote") => {
|
||||
if !runtime_environment.environment().is_remote() {
|
||||
return Err(StartupOutcomeError::from(anyhow!(
|
||||
"remote MCP server `{server_name}` requires a remote environment"
|
||||
)));
|
||||
}
|
||||
true
|
||||
}
|
||||
Some("remote") => true,
|
||||
Some(environment) => {
|
||||
return Err(StartupOutcomeError::from(anyhow!(
|
||||
"unsupported experimental_environment `{environment}` for MCP server `{server_name}`"
|
||||
@@ -604,8 +600,15 @@ async fn make_rmcp_client(
|
||||
.collect::<HashMap<_, _>>()
|
||||
});
|
||||
let launcher = if remote_environment {
|
||||
// Preflight should reject this first, but keep client startup
|
||||
// defensive if optional runtime placement is mis-threaded.
|
||||
let Some(environment) = runtime_environment.environment() else {
|
||||
return Err(StartupOutcomeError::from(anyhow!(
|
||||
"remote MCP server requires a runtime environment"
|
||||
)));
|
||||
};
|
||||
Arc::new(ExecutorStdioServerLauncher::new(
|
||||
runtime_environment.environment().get_exec_backend(),
|
||||
environment.get_exec_backend(),
|
||||
runtime_environment.fallback_cwd(),
|
||||
))
|
||||
} else {
|
||||
@@ -628,7 +631,12 @@ async fn make_rmcp_client(
|
||||
bearer_token_env_var,
|
||||
} => {
|
||||
let http_client: Arc<dyn HttpClient> = if remote_environment {
|
||||
runtime_environment.environment().get_http_client()
|
||||
let Some(environment) = runtime_environment.environment() else {
|
||||
return Err(StartupOutcomeError::from(anyhow!(
|
||||
"remote MCP server requires a runtime environment"
|
||||
)));
|
||||
};
|
||||
environment.get_http_client()
|
||||
} else {
|
||||
Arc::new(ReqwestHttpClient)
|
||||
};
|
||||
|
||||
@@ -30,33 +30,71 @@ pub struct SandboxState {
|
||||
|
||||
/// Runtime placement information used when starting MCP server transports.
|
||||
///
|
||||
/// `McpConfig` describes what servers exist. This value describes where those
|
||||
/// servers should run for the current caller. Keep it explicit at manager
|
||||
/// construction time so status/snapshot paths and real sessions make the same
|
||||
/// local-vs-remote decision. `fallback_cwd` is not a per-server override; it is
|
||||
/// used when a stdio server omits `cwd` and the launcher needs a concrete
|
||||
/// process working directory.
|
||||
/// `McpConfig` describes what servers exist. This value describes which
|
||||
/// selected/default environment MCP servers should use for the current caller.
|
||||
/// Keep it explicit at manager construction time so status/snapshot paths and
|
||||
/// real sessions make the same placement decision. `fallback_cwd` is not a
|
||||
/// per-server override; it is used when a stdio server omits `cwd` and the
|
||||
/// launcher needs a concrete process working directory.
|
||||
#[derive(Clone)]
|
||||
pub struct McpRuntimeEnvironment {
|
||||
environment: Arc<Environment>,
|
||||
environment: Option<Arc<Environment>>,
|
||||
fallback_cwd: PathBuf,
|
||||
}
|
||||
|
||||
impl McpRuntimeEnvironment {
|
||||
pub fn new(environment: Arc<Environment>, fallback_cwd: PathBuf) -> Self {
|
||||
pub fn new(environment: Option<Arc<Environment>>, fallback_cwd: PathBuf) -> Self {
|
||||
Self {
|
||||
environment,
|
||||
fallback_cwd,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn environment(&self) -> Arc<Environment> {
|
||||
Arc::clone(&self.environment)
|
||||
pub(crate) fn environment(&self) -> Option<Arc<Environment>> {
|
||||
self.environment.as_ref().map(Arc::clone)
|
||||
}
|
||||
|
||||
pub(crate) fn fallback_cwd(&self) -> PathBuf {
|
||||
self.fallback_cwd.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn startup_unavailable_reason(
|
||||
&self,
|
||||
server_name: &str,
|
||||
config: &codex_config::McpServerConfig,
|
||||
) -> Option<String> {
|
||||
// This is intentionally narrower than "no env means no MCP": local
|
||||
// stdio needs a local process launcher, while local HTTP can still use
|
||||
// the ambient HTTP client with no local environment configured.
|
||||
match config.experimental_environment.as_deref() {
|
||||
None | Some("local") => {
|
||||
if self
|
||||
.environment
|
||||
.as_ref()
|
||||
.is_none_or(|environment| environment.is_remote())
|
||||
&& matches!(
|
||||
config.transport,
|
||||
codex_config::McpServerTransportConfig::Stdio { .. }
|
||||
)
|
||||
{
|
||||
Some(format!(
|
||||
"local stdio MCP server `{server_name}` requires a local environment"
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Some("remote") => match self.environment.as_ref() {
|
||||
Some(environment) if environment.is_remote() => None,
|
||||
_ => Some(format!(
|
||||
"remote MCP server `{server_name}` requires a remote environment"
|
||||
)),
|
||||
},
|
||||
Some(environment) => Some(format!(
|
||||
"unsupported experimental_environment `{environment}` for MCP server `{server_name}`"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn emit_duration(metric: &str, duration: Duration, tags: &[(&str, &str)]) {
|
||||
@@ -64,3 +102,120 @@ pub(crate) fn emit_duration(metric: &str, duration: Duration, tags: &[(&str, &st
|
||||
let _ = metrics.record_duration(metric, duration, tags);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn stdio_server(experimental_environment: Option<&str>) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "echo".to_string(),
|
||||
args: Vec::new(),
|
||||
env: None,
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: experimental_environment.map(str::to_string),
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth: None,
|
||||
oauth_resource: None,
|
||||
tools: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn http_server(experimental_environment: Option<&str>) -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: "http://127.0.0.1:1".to_string(),
|
||||
bearer_token_env_var: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
experimental_environment: experimental_environment.map(str::to_string),
|
||||
..stdio_server(/*experimental_environment*/ None)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_stdio_requires_local_stdio_availability() {
|
||||
let runtime_environment =
|
||||
McpRuntimeEnvironment::new(/*environment*/ None, PathBuf::from("/tmp"));
|
||||
|
||||
assert_eq!(
|
||||
runtime_environment.startup_unavailable_reason(
|
||||
"stdio",
|
||||
&stdio_server(/*experimental_environment*/ None)
|
||||
),
|
||||
Some("local stdio MCP server `stdio` requires a local environment".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_http_does_not_require_local_stdio_availability() {
|
||||
let runtime_environment =
|
||||
McpRuntimeEnvironment::new(/*environment*/ None, PathBuf::from("/tmp"));
|
||||
|
||||
assert_eq!(
|
||||
runtime_environment.startup_unavailable_reason(
|
||||
"http",
|
||||
&http_server(/*experimental_environment*/ None)
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_stdio_requires_remote_environment() {
|
||||
let runtime_environment =
|
||||
McpRuntimeEnvironment::new(/*environment*/ None, PathBuf::from("/tmp"));
|
||||
|
||||
assert_eq!(
|
||||
runtime_environment.startup_unavailable_reason(
|
||||
"stdio",
|
||||
&stdio_server(/*experimental_environment*/ Some("remote")),
|
||||
),
|
||||
Some("remote MCP server `stdio` requires a remote environment".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_stdio_and_http_accept_remote_environment() {
|
||||
let environment = Arc::new(
|
||||
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
|
||||
.expect("remote environment"),
|
||||
);
|
||||
let runtime_environment =
|
||||
McpRuntimeEnvironment::new(Some(environment), PathBuf::from("/tmp"));
|
||||
|
||||
assert_eq!(
|
||||
runtime_environment.startup_unavailable_reason(
|
||||
"stdio",
|
||||
&stdio_server(/*experimental_environment*/ Some("remote")),
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
runtime_environment.startup_unavailable_reason(
|
||||
"http",
|
||||
&http_server(/*experimental_environment*/ Some("remote")),
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,8 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
|
||||
config.codex_linux_sandbox_exe.clone(),
|
||||
)?;
|
||||
let environment_manager =
|
||||
EnvironmentManager::from_codex_home(config.codex_home.clone(), local_runtime_paths).await?;
|
||||
EnvironmentManager::from_codex_home(config.codex_home.clone(), Some(local_runtime_paths))
|
||||
.await?;
|
||||
list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
config,
|
||||
force_refetch,
|
||||
@@ -261,10 +262,6 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
let (tx_event, rx_event) = unbounded();
|
||||
drop(rx_event);
|
||||
|
||||
let environment = environment_manager
|
||||
.default_environment()
|
||||
.unwrap_or_else(|| environment_manager.local_environment());
|
||||
|
||||
let (mut mcp_connection_manager, cancel_token) = McpConnectionManager::new(
|
||||
&mcp_servers,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
@@ -273,7 +270,12 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
INITIAL_SUBMIT_ID.to_owned(),
|
||||
tx_event,
|
||||
PermissionProfile::default(),
|
||||
McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf()),
|
||||
// Connector discovery is threadless. Use an actually configured env if
|
||||
// one exists, but do not reintroduce the old hidden-local fallback.
|
||||
McpRuntimeEnvironment::new(
|
||||
environment_manager.default_or_local_environment(),
|
||||
config.cwd.to_path_buf(),
|
||||
),
|
||||
config.codex_home.to_path_buf(),
|
||||
codex_apps_tools_cache_key(auth.as_ref()),
|
||||
host_owned_codex_apps_enabled,
|
||||
|
||||
@@ -106,7 +106,7 @@ mod tests {
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
|
||||
let manager = EnvironmentManager::create_for_tests(
|
||||
Some("ws://127.0.0.1:8765".to_string()),
|
||||
test_runtime_paths(),
|
||||
Some(test_runtime_paths()),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -132,9 +132,10 @@ url = "ws://127.0.0.1:8765"
|
||||
)
|
||||
.expect("write environments.toml");
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
|
||||
let manager = EnvironmentManager::from_codex_home(temp_dir.path(), test_runtime_paths())
|
||||
.await
|
||||
.expect("environment manager");
|
||||
let manager =
|
||||
EnvironmentManager::from_codex_home(temp_dir.path(), Some(test_runtime_paths()))
|
||||
.await
|
||||
.expect("environment manager");
|
||||
|
||||
assert_eq!(
|
||||
default_thread_environment_selections(&manager, &cwd),
|
||||
@@ -154,7 +155,7 @@ url = "ws://127.0.0.1:8765"
|
||||
#[tokio::test]
|
||||
async fn default_thread_environment_selections_empty_when_default_disabled() {
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
|
||||
let manager = EnvironmentManager::disabled_for_tests(test_runtime_paths());
|
||||
let manager = EnvironmentManager::without_environments();
|
||||
|
||||
assert_eq!(
|
||||
default_thread_environment_selections(&manager, &cwd),
|
||||
|
||||
@@ -13,6 +13,7 @@ mod client_common;
|
||||
mod realtime_context;
|
||||
mod realtime_conversation;
|
||||
mod realtime_prompt;
|
||||
mod runtime_capabilities;
|
||||
pub(crate) mod session;
|
||||
pub use session::SteerInputError;
|
||||
mod codex_thread;
|
||||
@@ -192,6 +193,8 @@ pub use exec_policy::check_execpolicy_for_warnings;
|
||||
pub use exec_policy::format_exec_policy_error_with_source;
|
||||
pub use exec_policy::load_exec_policy;
|
||||
pub use installation_id::resolve_installation_id;
|
||||
pub use runtime_capabilities::RuntimeCapabilities;
|
||||
pub use runtime_capabilities::RuntimeMode;
|
||||
pub use turn_metadata::build_turn_metadata_header;
|
||||
pub mod compact;
|
||||
mod memory_usage;
|
||||
|
||||
@@ -1235,8 +1235,8 @@ async fn install_host_owned_codex_apps_manager(session: &Session, turn_context:
|
||||
let environment = session
|
||||
.services
|
||||
.environment_manager
|
||||
.default_environment()
|
||||
.unwrap_or_else(|| session.services.environment_manager.local_environment());
|
||||
.default_or_local_environment()
|
||||
.expect("test session should have an MCP runtime environment");
|
||||
let (manager, _cancel_token) = codex_mcp::McpConnectionManager::new(
|
||||
&HashMap::new(),
|
||||
turn_context.config.mcp_oauth_credentials_store_mode,
|
||||
@@ -1245,7 +1245,7 @@ async fn install_host_owned_codex_apps_manager(session: &Session, turn_context:
|
||||
turn_context.sub_id.clone(),
|
||||
session.get_tx_event(),
|
||||
turn_context.permission_profile(),
|
||||
codex_mcp::McpRuntimeEnvironment::new(environment, {
|
||||
codex_mcp::McpRuntimeEnvironment::new(Some(environment), {
|
||||
#[allow(deprecated)]
|
||||
turn_context.cwd.to_path_buf()
|
||||
}),
|
||||
|
||||
@@ -46,9 +46,12 @@ pub async fn build_prompt_input(
|
||||
Arc::clone(&auth_manager),
|
||||
SessionSource::Exec,
|
||||
Arc::new(
|
||||
EnvironmentManager::from_codex_home(config.codex_home.clone(), local_runtime_paths)
|
||||
.await
|
||||
.map_err(|err| CodexErr::Fatal(err.to_string()))?,
|
||||
EnvironmentManager::from_codex_home(
|
||||
config.codex_home.clone(),
|
||||
Some(local_runtime_paths),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| CodexErr::Fatal(err.to_string()))?,
|
||||
),
|
||||
empty_extension_registry(),
|
||||
/*analytics_events_client*/ None,
|
||||
|
||||
181
codex-rs/core/src/runtime_capabilities.rs
Normal file
181
codex-rs/core/src/runtime_capabilities.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_exec_server::Environment;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
|
||||
/// Describes whether a runtime may use ambient worker-local capabilities.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum RuntimeMode {
|
||||
LocalCodex,
|
||||
Isolated,
|
||||
}
|
||||
|
||||
/// Ambient worker-local capabilities available to a Codex runtime.
|
||||
///
|
||||
/// V1 intentionally models one worker-local `Environment` capability. Local
|
||||
/// filesystem and local exec access are derived views of that one capability
|
||||
/// rather than independently configurable powers.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RuntimeCapabilities {
|
||||
mode: RuntimeMode,
|
||||
local_environment: Option<Arc<Environment>>,
|
||||
}
|
||||
|
||||
impl RuntimeCapabilities {
|
||||
/// Builds capabilities for a local Codex runtime.
|
||||
pub fn local(environment_manager: &EnvironmentManager) -> Self {
|
||||
Self {
|
||||
mode: RuntimeMode::LocalCodex,
|
||||
local_environment: Some(environment_manager.require_local_environment()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds capabilities for a runtime isolated from worker-local powers.
|
||||
pub fn isolated() -> Self {
|
||||
Self {
|
||||
mode: RuntimeMode::Isolated,
|
||||
local_environment: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether this runtime is local Codex or isolated.
|
||||
pub fn mode(&self) -> RuntimeMode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
/// Returns the ambient worker-local environment when available.
|
||||
pub fn local_environment(&self) -> Option<Arc<Environment>> {
|
||||
self.local_environment.as_ref().map(Arc::clone)
|
||||
}
|
||||
|
||||
/// Returns the ambient worker-local environment or an unsupported error.
|
||||
pub fn require_local_environment(&self, operation: &str) -> CodexResult<Arc<Environment>> {
|
||||
self.local_environment()
|
||||
.ok_or_else(|| missing_local_capability(operation, "environment"))
|
||||
}
|
||||
|
||||
/// Returns the ambient worker-local filesystem when available.
|
||||
pub fn local_filesystem(&self) -> Option<Arc<dyn ExecutorFileSystem>> {
|
||||
self.local_environment()
|
||||
.map(|environment| environment.get_filesystem())
|
||||
}
|
||||
|
||||
/// Returns the ambient worker-local filesystem or an unsupported error.
|
||||
pub fn require_local_filesystem(
|
||||
&self,
|
||||
operation: &str,
|
||||
) -> CodexResult<Arc<dyn ExecutorFileSystem>> {
|
||||
self.local_filesystem()
|
||||
.ok_or_else(|| missing_local_capability(operation, "filesystem"))
|
||||
}
|
||||
|
||||
/// Returns whether ambient worker-local exec is available.
|
||||
pub fn local_exec_available(&self) -> bool {
|
||||
self.local_environment.is_some()
|
||||
}
|
||||
|
||||
/// Returns the ambient worker-local exec environment when available.
|
||||
pub fn local_exec(&self) -> Option<Arc<Environment>> {
|
||||
self.local_environment()
|
||||
}
|
||||
|
||||
/// Returns the ambient worker-local exec environment or an unsupported error.
|
||||
pub fn require_local_exec(&self, operation: &str) -> CodexResult<Arc<Environment>> {
|
||||
self.local_exec()
|
||||
.ok_or_else(|| missing_local_capability(operation, "exec"))
|
||||
}
|
||||
}
|
||||
|
||||
fn missing_local_capability(operation: &str, capability: &str) -> CodexErr {
|
||||
CodexErr::UnsupportedOperation(format!(
|
||||
"{operation} requires ambient worker-local {capability}"
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::RuntimeCapabilities;
|
||||
use super::RuntimeMode;
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_capabilities_derive_worker_local_views_from_environment() {
|
||||
let environment_manager = EnvironmentManager::default_for_tests();
|
||||
let expected_environment = environment_manager.require_local_environment();
|
||||
let expected_filesystem = expected_environment.get_filesystem();
|
||||
let capabilities = RuntimeCapabilities::local(&environment_manager);
|
||||
|
||||
assert_eq!(capabilities.mode(), RuntimeMode::LocalCodex);
|
||||
assert!(Arc::ptr_eq(
|
||||
&capabilities.local_environment().expect("local environment"),
|
||||
&expected_environment,
|
||||
));
|
||||
assert!(Arc::ptr_eq(
|
||||
&capabilities
|
||||
.require_local_environment("test operation")
|
||||
.expect("required local environment"),
|
||||
&expected_environment,
|
||||
));
|
||||
assert!(Arc::ptr_eq(
|
||||
&capabilities.local_filesystem().expect("local filesystem"),
|
||||
&expected_filesystem,
|
||||
));
|
||||
assert!(Arc::ptr_eq(
|
||||
&capabilities
|
||||
.require_local_filesystem("test operation")
|
||||
.expect("required local filesystem"),
|
||||
&expected_filesystem,
|
||||
));
|
||||
assert!(capabilities.local_exec_available());
|
||||
assert!(Arc::ptr_eq(
|
||||
&capabilities.local_exec().expect("local exec"),
|
||||
&expected_environment,
|
||||
));
|
||||
assert!(Arc::ptr_eq(
|
||||
&capabilities
|
||||
.require_local_exec("test operation")
|
||||
.expect("local exec"),
|
||||
&expected_environment,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolated_capabilities_do_not_expose_worker_local_views() {
|
||||
let capabilities = RuntimeCapabilities::isolated();
|
||||
|
||||
assert_eq!(capabilities.mode(), RuntimeMode::Isolated);
|
||||
assert!(capabilities.local_environment().is_none());
|
||||
assert!(capabilities.local_filesystem().is_none());
|
||||
assert!(!capabilities.local_exec_available());
|
||||
assert!(capabilities.local_exec().is_none());
|
||||
assert_eq!(
|
||||
unsupported_message(capabilities.require_local_environment("test operation")),
|
||||
"test operation requires ambient worker-local environment",
|
||||
);
|
||||
assert_eq!(
|
||||
unsupported_message(capabilities.require_local_filesystem("test operation")),
|
||||
"test operation requires ambient worker-local filesystem",
|
||||
);
|
||||
assert_eq!(
|
||||
unsupported_message(capabilities.require_local_exec("test operation")),
|
||||
"test operation requires ambient worker-local exec",
|
||||
);
|
||||
}
|
||||
|
||||
fn unsupported_message<T>(result: CodexResult<T>) -> String {
|
||||
match result {
|
||||
Ok(_) => panic!("expected unsupported operation"),
|
||||
Err(CodexErr::UnsupportedOperation(message)) => message,
|
||||
Err(err) => panic!("expected unsupported operation, got {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,14 +291,13 @@ impl Session {
|
||||
compute_auth_statuses(mcp_servers.iter(), store_mode, auth.as_ref()).await;
|
||||
let mcp_runtime_environment = match turn_context.environments.primary() {
|
||||
Some(turn_environment) => McpRuntimeEnvironment::new(
|
||||
Arc::clone(&turn_environment.environment),
|
||||
Some(Arc::clone(&turn_environment.environment)),
|
||||
turn_environment.cwd.to_path_buf(),
|
||||
),
|
||||
None => McpRuntimeEnvironment::new(
|
||||
self.services
|
||||
.environment_manager
|
||||
.default_environment()
|
||||
.unwrap_or_else(|| self.services.environment_manager.local_environment()),
|
||||
.default_or_local_environment(),
|
||||
#[allow(deprecated)]
|
||||
turn_context.cwd.to_path_buf(),
|
||||
),
|
||||
|
||||
@@ -1048,14 +1048,11 @@ impl Session {
|
||||
.cloned();
|
||||
let mcp_runtime_environment = match turn_environment {
|
||||
Some(turn_environment) => McpRuntimeEnvironment::new(
|
||||
Arc::clone(&turn_environment.environment),
|
||||
Some(Arc::clone(&turn_environment.environment)),
|
||||
turn_environment.cwd.to_path_buf(),
|
||||
),
|
||||
None => McpRuntimeEnvironment::new(
|
||||
sess.services
|
||||
.environment_manager
|
||||
.default_environment()
|
||||
.unwrap_or_else(|| sess.services.environment_manager.local_environment()),
|
||||
sess.services.environment_manager.default_or_local_environment(),
|
||||
session_configuration.cwd.to_path_buf(),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -309,7 +309,7 @@ async fn start_thread_rejects_explicit_local_environment_when_default_provider_i
|
||||
let environment_manager = Arc::new(
|
||||
codex_exec_server::EnvironmentManager::create_for_tests(
|
||||
Some("none".to_string()),
|
||||
runtime_paths,
|
||||
Some(runtime_paths),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
@@ -373,7 +373,7 @@ args = ["dev", "cd /tmp && true"]
|
||||
let environment_manager = Arc::new(
|
||||
codex_exec_server::EnvironmentManager::from_codex_home(
|
||||
config.codex_home.clone(),
|
||||
runtime_paths,
|
||||
Some(runtime_paths),
|
||||
)
|
||||
.await
|
||||
.expect("environment manager"),
|
||||
|
||||
@@ -425,7 +425,7 @@ impl TestCodexBuilder {
|
||||
} else {
|
||||
codex_exec_server::EnvironmentManager::create_for_tests(
|
||||
exec_server_url,
|
||||
local_runtime_paths,
|
||||
Some(local_runtime_paths),
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
||||
@@ -27,12 +27,11 @@ pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL";
|
||||
///
|
||||
/// `EnvironmentManager` is a shared registry for concrete environments. Its
|
||||
/// default constructor preserves the legacy `CODEX_EXEC_SERVER_URL` behavior
|
||||
/// while provider-based construction accepts a provider-supplied snapshot.
|
||||
/// while configured 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
|
||||
/// 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
|
||||
@@ -42,25 +41,13 @@ pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL";
|
||||
pub struct EnvironmentManager {
|
||||
default_environment: Option<String>,
|
||||
environments: RwLock<HashMap<String, Arc<Environment>>>,
|
||||
local_environment: 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";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EnvironmentManagerArgs {
|
||||
pub local_runtime_paths: ExecServerRuntimePaths,
|
||||
}
|
||||
|
||||
impl EnvironmentManagerArgs {
|
||||
pub fn new(local_runtime_paths: ExecServerRuntimePaths) -> Self {
|
||||
Self {
|
||||
local_runtime_paths,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EnvironmentManager {
|
||||
/// Builds a test-only manager without configured sandbox helper paths.
|
||||
pub fn default_for_tests() -> Self {
|
||||
@@ -70,37 +57,29 @@ impl EnvironmentManager {
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
Arc::new(Environment::default_for_tests()),
|
||||
)])),
|
||||
local_environment: Arc::new(Environment::default_for_tests()),
|
||||
local_environment: Some(Arc::new(Environment::default_for_tests())),
|
||||
local_runtime_paths: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a test-only manager with environment access disabled.
|
||||
pub fn disabled_for_tests(local_runtime_paths: ExecServerRuntimePaths) -> Self {
|
||||
/// Builds a manager with no configured execution environments.
|
||||
pub fn without_environments() -> Self {
|
||||
Self {
|
||||
default_environment: None,
|
||||
environments: RwLock::new(HashMap::new()),
|
||||
local_environment: Arc::new(Environment::local(local_runtime_paths)),
|
||||
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: ExecServerRuntimePaths,
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
) -> Self {
|
||||
Self::from_default_provider_url(exec_server_url, local_runtime_paths).await
|
||||
}
|
||||
|
||||
/// Builds a manager from `CODEX_EXEC_SERVER_URL` and local runtime paths
|
||||
/// used when creating local filesystem helpers.
|
||||
pub async fn new(args: EnvironmentManagerArgs) -> Self {
|
||||
let EnvironmentManagerArgs {
|
||||
local_runtime_paths,
|
||||
} = args;
|
||||
let exec_server_url = std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok();
|
||||
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.
|
||||
///
|
||||
@@ -109,27 +88,27 @@ impl EnvironmentManager {
|
||||
/// `CODEX_EXEC_SERVER_URL` behavior.
|
||||
pub async fn from_codex_home(
|
||||
codex_home: impl AsRef<std::path::Path>,
|
||||
local_runtime_paths: ExecServerRuntimePaths,
|
||||
local_runtime_paths: Option<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
|
||||
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: ExecServerRuntimePaths,
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
) -> Result<Self, ExecServerError> {
|
||||
let provider = DefaultEnvironmentProvider::from_env();
|
||||
Self::from_provider(&provider, local_runtime_paths).await
|
||||
Self::from_snapshot(provider.snapshot().await?, local_runtime_paths)
|
||||
}
|
||||
|
||||
async fn from_default_provider_url(
|
||||
exec_server_url: Option<String>,
|
||||
local_runtime_paths: ExecServerRuntimePaths,
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
) -> Self {
|
||||
let provider = DefaultEnvironmentProvider::new(exec_server_url);
|
||||
match Self::from_provider(&provider, local_runtime_paths).await {
|
||||
match Self::from_snapshot(provider.snapshot_inner(), local_runtime_paths) {
|
||||
Ok(manager) => manager,
|
||||
Err(err) => panic!("default provider should create valid environments: {err}"),
|
||||
}
|
||||
@@ -143,26 +122,15 @@ impl EnvironmentManager {
|
||||
) -> Self {
|
||||
let mut snapshot = DefaultEnvironmentProvider::new(exec_server_url).snapshot_inner();
|
||||
snapshot.include_local = true;
|
||||
match Self::from_provider_snapshot(snapshot, local_runtime_paths) {
|
||||
match Self::from_snapshot(snapshot, Some(local_runtime_paths)) {
|
||||
Ok(manager) => manager,
|
||||
Err(err) => panic!("test provider with local 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().await?, local_runtime_paths)
|
||||
}
|
||||
|
||||
fn from_provider_snapshot(
|
||||
fn from_snapshot(
|
||||
snapshot: EnvironmentProviderSnapshot,
|
||||
local_runtime_paths: ExecServerRuntimePaths,
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
) -> Result<Self, ExecServerError> {
|
||||
let EnvironmentProviderSnapshot {
|
||||
environments,
|
||||
@@ -171,13 +139,21 @@ impl EnvironmentManager {
|
||||
} = snapshot;
|
||||
let mut environment_map =
|
||||
HashMap::with_capacity(environments.len() + usize::from(include_local));
|
||||
let local_environment = Arc::new(Environment::local(local_runtime_paths));
|
||||
if 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(
|
||||
@@ -213,6 +189,7 @@ impl EnvironmentManager {
|
||||
default_environment,
|
||||
environments: RwLock::new(environment_map),
|
||||
local_environment,
|
||||
local_runtime_paths,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -249,8 +226,27 @@ impl EnvironmentManager {
|
||||
}
|
||||
|
||||
/// Returns the local environment instance used for internal runtime work.
|
||||
pub fn local_environment(&self) -> Arc<Environment> {
|
||||
Arc::clone(&self.local_environment)
|
||||
pub fn require_local_environment(&self) -> Arc<Environment> {
|
||||
match self.try_local_environment() {
|
||||
Some(environment) => environment,
|
||||
None => panic!("local environment is not configured"),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 whether a local environment is configured.
|
||||
pub fn has_local_environment(&self) -> bool {
|
||||
self.local_environment.is_some()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -285,10 +281,8 @@ impl EnvironmentManager {
|
||||
"remote environment requires an exec-server url".to_string(),
|
||||
));
|
||||
};
|
||||
let environment = Environment::remote_inner(
|
||||
exec_server_url,
|
||||
self.local_environment.local_runtime_paths.clone(),
|
||||
);
|
||||
let environment =
|
||||
Environment::remote_inner(exec_server_url, self.local_runtime_paths.clone());
|
||||
self.environments
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
@@ -452,25 +446,12 @@ mod tests {
|
||||
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) -> Result<EnvironmentProviderSnapshot, ExecServerError> {
|
||||
Ok(self.snapshot.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn test_runtime_paths() -> ExecServerRuntimePaths {
|
||||
ExecServerRuntimePaths::new(
|
||||
std::env::current_exe().expect("current exe"),
|
||||
@@ -479,6 +460,20 @@ mod tests {
|
||||
.expect("runtime paths")
|
||||
}
|
||||
|
||||
fn assert_local_environment_unavailable(manager: &EnvironmentManager) {
|
||||
assert!(manager.try_local_environment().is_none());
|
||||
let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
manager.require_local_environment()
|
||||
}))
|
||||
.expect_err("local environment should be unavailable");
|
||||
let message = panic
|
||||
.downcast_ref::<&'static str>()
|
||||
.copied()
|
||||
.or_else(|| panic.downcast_ref::<String>().map(String::as_str))
|
||||
.expect("panic message");
|
||||
assert_eq!(message, "local environment is not configured");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_local_environment_does_not_connect() {
|
||||
let environment = Environment::create(/*exec_server_url*/ None, test_runtime_paths())
|
||||
@@ -491,7 +486,8 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn environment_manager_normalizes_empty_url() {
|
||||
let manager =
|
||||
EnvironmentManager::create_for_tests(Some(String::new()), test_runtime_paths()).await;
|
||||
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));
|
||||
@@ -501,18 +497,22 @@ mod tests {
|
||||
.get_environment(LOCAL_ENVIRONMENT_ID)
|
||||
.expect("local environment")
|
||||
));
|
||||
assert!(Arc::ptr_eq(&environment, &manager.local_environment()));
|
||||
assert!(Arc::ptr_eq(
|
||||
&environment,
|
||||
&manager.require_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_but_keeps_explicit_local_environment() {
|
||||
let manager = EnvironmentManager::disabled_for_tests(test_runtime_paths());
|
||||
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!(!manager.local_environment().is_remote());
|
||||
assert_local_environment_unavailable(&manager);
|
||||
assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
|
||||
assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none());
|
||||
}
|
||||
@@ -521,7 +521,7 @@ mod tests {
|
||||
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(),
|
||||
Some(test_runtime_paths()),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -539,7 +539,7 @@ mod tests {
|
||||
.expect("remote environment")
|
||||
));
|
||||
assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
|
||||
assert!(!manager.local_environment().is_remote());
|
||||
assert_local_environment_unavailable(&manager);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -557,20 +557,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_builds_from_provider() {
|
||||
let provider = TestEnvironmentProvider {
|
||||
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,
|
||||
},
|
||||
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_provider(&provider, test_runtime_paths())
|
||||
.await
|
||||
let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
||||
.expect("environment manager");
|
||||
|
||||
assert_eq!(
|
||||
@@ -584,20 +581,17 @@ mod tests {
|
||||
.is_remote()
|
||||
);
|
||||
assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none());
|
||||
assert!(!manager.local_environment().is_remote());
|
||||
assert_local_environment_unavailable(&manager);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_rejects_empty_environment_id() {
|
||||
let provider = TestEnvironmentProvider {
|
||||
snapshot: EnvironmentProviderSnapshot {
|
||||
environments: vec![("".to_string(), Environment::default_for_tests())],
|
||||
default: EnvironmentDefault::Disabled,
|
||||
include_local: false,
|
||||
},
|
||||
let snapshot = EnvironmentProviderSnapshot {
|
||||
environments: vec![("".to_string(), Environment::default_for_tests())],
|
||||
default: EnvironmentDefault::Disabled,
|
||||
include_local: false,
|
||||
};
|
||||
let err = EnvironmentManager::from_provider(&provider, test_runtime_paths())
|
||||
.await
|
||||
let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
||||
.expect_err("empty id should fail");
|
||||
|
||||
assert_eq!(
|
||||
@@ -608,18 +602,15 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_rejects_provider_supplied_local_environment() {
|
||||
let provider = TestEnvironmentProvider {
|
||||
snapshot: EnvironmentProviderSnapshot {
|
||||
environments: vec![(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
Environment::default_for_tests(),
|
||||
)],
|
||||
default: EnvironmentDefault::Disabled,
|
||||
include_local: false,
|
||||
},
|
||||
let snapshot = EnvironmentProviderSnapshot {
|
||||
environments: vec![(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
Environment::default_for_tests(),
|
||||
)],
|
||||
default: EnvironmentDefault::Disabled,
|
||||
include_local: false,
|
||||
};
|
||||
let err = EnvironmentManager::from_provider(&provider, test_runtime_paths())
|
||||
.await
|
||||
let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
||||
.expect_err("local id should fail");
|
||||
|
||||
assert_eq!(
|
||||
@@ -630,19 +621,16 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_uses_explicit_provider_default() {
|
||||
let provider = TestEnvironmentProvider {
|
||||
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 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_provider(&provider, test_runtime_paths())
|
||||
.await
|
||||
let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
||||
.expect("manager");
|
||||
|
||||
assert_eq!(manager.default_environment_id(), Some("devbox"));
|
||||
@@ -655,19 +643,16 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_disables_provider_default() {
|
||||
let provider = TestEnvironmentProvider {
|
||||
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 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_provider(&provider, test_runtime_paths())
|
||||
.await
|
||||
let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
||||
.expect("manager");
|
||||
|
||||
assert_eq!(manager.default_environment_id(), None);
|
||||
@@ -676,25 +661,22 @@ mod tests {
|
||||
&manager
|
||||
.get_environment(LOCAL_ENVIRONMENT_ID)
|
||||
.expect("local environment"),
|
||||
&manager.local_environment()
|
||||
&manager.require_local_environment()
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_rejects_unknown_provider_default() {
|
||||
let provider = TestEnvironmentProvider {
|
||||
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 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_provider(&provider, test_runtime_paths())
|
||||
.await
|
||||
let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths()))
|
||||
.expect_err("unknown default should fail");
|
||||
|
||||
assert_eq!(
|
||||
@@ -707,7 +689,7 @@ mod tests {
|
||||
async fn environment_manager_includes_local_for_default_provider_without_url() {
|
||||
let manager = EnvironmentManager::create_for_tests(
|
||||
/*exec_server_url*/ None,
|
||||
test_runtime_paths(),
|
||||
Some(test_runtime_paths()),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -719,7 +701,10 @@ mod tests {
|
||||
.get_environment(LOCAL_ENVIRONMENT_ID)
|
||||
.expect("local environment")
|
||||
));
|
||||
assert!(Arc::ptr_eq(&environment, &manager.local_environment()));
|
||||
assert!(Arc::ptr_eq(
|
||||
&environment,
|
||||
&manager.require_local_environment()
|
||||
));
|
||||
assert!(!environment.is_remote());
|
||||
}
|
||||
|
||||
@@ -728,44 +713,59 @@ mod tests {
|
||||
let runtime_paths = test_runtime_paths();
|
||||
let manager = EnvironmentManager::create_for_tests(
|
||||
/*exec_server_url*/ None,
|
||||
runtime_paths.clone(),
|
||||
Some(runtime_paths.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
let environment = manager.local_environment();
|
||||
let environment = manager.require_local_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(),
|
||||
Some(
|
||||
environment
|
||||
.local_runtime_paths()
|
||||
.expect("local runtime paths")
|
||||
.clone(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let environment = manager.local_environment();
|
||||
let environment = manager.require_local_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_omits_default_provider_local_lookup_when_default_disabled() {
|
||||
let manager =
|
||||
EnvironmentManager::create_for_tests(Some("none".to_string()), test_runtime_paths())
|
||||
.await;
|
||||
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!(!manager.local_environment().is_remote());
|
||||
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]
|
||||
@@ -777,7 +777,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_upserts_named_remote_environment() {
|
||||
let manager = EnvironmentManager::disabled_for_tests(test_runtime_paths());
|
||||
let manager = EnvironmentManager::without_environments();
|
||||
|
||||
manager
|
||||
.upsert_environment("executor-a".to_string(), "ws://127.0.0.1:8765".to_string())
|
||||
@@ -802,7 +802,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn environment_manager_rejects_empty_remote_environment_url() {
|
||||
let manager = EnvironmentManager::disabled_for_tests(test_runtime_paths());
|
||||
let manager = EnvironmentManager::without_environments();
|
||||
|
||||
let err = manager
|
||||
.upsert_environment("executor-a".to_string(), String::new())
|
||||
|
||||
@@ -27,6 +27,7 @@ const MAX_ENVIRONMENT_ID_LEN: usize = 64;
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EnvironmentsToml {
|
||||
default: Option<String>,
|
||||
include_local: Option<bool>,
|
||||
|
||||
#[serde(default)]
|
||||
environments: Vec<EnvironmentToml>,
|
||||
@@ -50,6 +51,7 @@ struct EnvironmentToml {
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct TomlEnvironmentProvider {
|
||||
default: EnvironmentDefault,
|
||||
include_local: bool,
|
||||
environments: Vec<(String, ExecServerTransportParams)>,
|
||||
}
|
||||
|
||||
@@ -63,21 +65,31 @@ impl TomlEnvironmentProvider {
|
||||
config: EnvironmentsToml,
|
||||
config_dir: Option<&Path>,
|
||||
) -> Result<Self, ExecServerError> {
|
||||
let mut ids = HashSet::from([LOCAL_ENVIRONMENT_ID.to_string()]);
|
||||
let mut environments = Vec::with_capacity(config.environments.len());
|
||||
for item in config.environments {
|
||||
let EnvironmentsToml {
|
||||
default,
|
||||
include_local,
|
||||
environments,
|
||||
} = config;
|
||||
let include_local = include_local.unwrap_or(true);
|
||||
let mut ids = HashSet::new();
|
||||
if include_local {
|
||||
ids.insert(LOCAL_ENVIRONMENT_ID.to_string());
|
||||
}
|
||||
let mut parsed_environments = Vec::with_capacity(environments.len());
|
||||
for item in environments {
|
||||
let (id, transport) = parse_environment_toml(item, config_dir)?;
|
||||
if !ids.insert(id.clone()) {
|
||||
return Err(ExecServerError::Protocol(format!(
|
||||
"environment id `{id}` is duplicated"
|
||||
)));
|
||||
}
|
||||
environments.push((id, transport));
|
||||
parsed_environments.push((id, transport));
|
||||
}
|
||||
let default = normalize_default_environment_id(config.default.as_deref(), &ids)?;
|
||||
let default = normalize_default_environment_id(default.as_deref(), include_local, &ids)?;
|
||||
Ok(Self {
|
||||
default,
|
||||
environments,
|
||||
include_local,
|
||||
environments: parsed_environments,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -99,7 +111,7 @@ impl EnvironmentProvider for TomlEnvironmentProvider {
|
||||
Ok(EnvironmentProviderSnapshot {
|
||||
environments,
|
||||
default: self.default.clone(),
|
||||
include_local: true,
|
||||
include_local: self.include_local,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -212,12 +224,17 @@ pub(crate) fn environment_provider_from_codex_home(
|
||||
|
||||
fn normalize_default_environment_id(
|
||||
default: Option<&str>,
|
||||
include_local: bool,
|
||||
ids: &HashSet<String>,
|
||||
) -> Result<EnvironmentDefault, ExecServerError> {
|
||||
let Some(default) = default.map(str::trim) else {
|
||||
return Ok(EnvironmentDefault::EnvironmentId(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
));
|
||||
return if include_local {
|
||||
Ok(EnvironmentDefault::EnvironmentId(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
))
|
||||
} else {
|
||||
Ok(EnvironmentDefault::Disabled)
|
||||
};
|
||||
};
|
||||
if default.is_empty() {
|
||||
return Err(ExecServerError::Protocol(
|
||||
@@ -330,6 +347,7 @@ mod tests {
|
||||
async fn toml_provider_includes_local_and_adds_configured_environments() {
|
||||
let provider = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: Some("ssh-dev".to_string()),
|
||||
include_local: None,
|
||||
environments: vec![
|
||||
EnvironmentToml {
|
||||
id: "devbox".to_string(),
|
||||
@@ -396,6 +414,7 @@ mod tests {
|
||||
async fn toml_provider_default_none_disables_default() {
|
||||
let provider = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: Some("none".to_string()),
|
||||
include_local: None,
|
||||
environments: Vec::new(),
|
||||
})
|
||||
.expect("provider");
|
||||
@@ -405,6 +424,55 @@ mod tests {
|
||||
assert_eq!(snapshot.default, EnvironmentDefault::Disabled);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn toml_provider_can_disable_local_environment() {
|
||||
let provider = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: Some("ssh-dev".to_string()),
|
||||
include_local: Some(false),
|
||||
environments: vec![EnvironmentToml {
|
||||
id: "ssh-dev".to_string(),
|
||||
program: Some("ssh".to_string()),
|
||||
..Default::default()
|
||||
}],
|
||||
})
|
||||
.expect("provider");
|
||||
let snapshot = provider.snapshot().await.expect("environments");
|
||||
|
||||
assert!(!snapshot.include_local);
|
||||
assert_eq!(
|
||||
snapshot.default,
|
||||
EnvironmentDefault::EnvironmentId("ssh-dev".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn toml_provider_without_local_and_default_omitted_disables_default() {
|
||||
let provider = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
include_local: Some(false),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("provider");
|
||||
let snapshot = provider.snapshot().await.expect("environments");
|
||||
|
||||
assert!(!snapshot.include_local);
|
||||
assert_eq!(snapshot.default, EnvironmentDefault::Disabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toml_provider_rejects_local_default_when_local_is_disabled() {
|
||||
let err = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: Some(LOCAL_ENVIRONMENT_ID.to_string()),
|
||||
include_local: Some(false),
|
||||
environments: Vec::new(),
|
||||
})
|
||||
.expect_err("local default without local environment should fail");
|
||||
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"exec-server protocol error: default environment `local` is not configured"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toml_provider_rejects_invalid_environments() {
|
||||
let cases = [
|
||||
@@ -479,6 +547,7 @@ mod tests {
|
||||
for (item, expected) in cases {
|
||||
let err = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: None,
|
||||
include_local: None,
|
||||
environments: vec![item],
|
||||
})
|
||||
.expect_err("invalid item should fail");
|
||||
@@ -496,6 +565,7 @@ mod tests {
|
||||
let provider = TomlEnvironmentProvider::new_with_config_dir(
|
||||
EnvironmentsToml {
|
||||
default: None,
|
||||
include_local: None,
|
||||
environments: vec![EnvironmentToml {
|
||||
id: "ssh-dev".to_string(),
|
||||
program: Some("ssh".to_string()),
|
||||
@@ -525,6 +595,7 @@ mod tests {
|
||||
fn toml_provider_parses_configured_transport_timeouts() {
|
||||
let provider = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: None,
|
||||
include_local: None,
|
||||
environments: vec![
|
||||
EnvironmentToml {
|
||||
id: "devbox".to_string(),
|
||||
@@ -569,6 +640,7 @@ mod tests {
|
||||
fn toml_provider_rejects_relative_stdio_cwd_without_config_dir() {
|
||||
let err = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: None,
|
||||
include_local: None,
|
||||
environments: vec![EnvironmentToml {
|
||||
id: "ssh-dev".to_string(),
|
||||
program: Some("ssh".to_string()),
|
||||
@@ -588,6 +660,7 @@ mod tests {
|
||||
fn toml_provider_rejects_duplicate_ids() {
|
||||
let err = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: None,
|
||||
include_local: None,
|
||||
environments: vec![
|
||||
EnvironmentToml {
|
||||
id: "devbox".to_string(),
|
||||
@@ -614,6 +687,7 @@ mod tests {
|
||||
let id = "a".repeat(MAX_ENVIRONMENT_ID_LEN + 1);
|
||||
let err = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: None,
|
||||
include_local: None,
|
||||
environments: vec![EnvironmentToml {
|
||||
id: id.clone(),
|
||||
url: Some("ws://127.0.0.1:8765".to_string()),
|
||||
@@ -634,6 +708,7 @@ mod tests {
|
||||
fn toml_provider_rejects_unknown_default() {
|
||||
let err = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: Some("missing".to_string()),
|
||||
include_local: None,
|
||||
environments: Vec::new(),
|
||||
})
|
||||
.expect_err("unknown default should fail");
|
||||
@@ -652,6 +727,7 @@ mod tests {
|
||||
&path,
|
||||
r#"
|
||||
default = "ssh-dev"
|
||||
include_local = false
|
||||
|
||||
[[environments]]
|
||||
id = "devbox"
|
||||
@@ -673,6 +749,7 @@ CODEX_LOG = "debug"
|
||||
let environments = load_environments_toml(&path).expect("environments.toml");
|
||||
|
||||
assert_eq!(environments.default.as_deref(), Some("ssh-dev"));
|
||||
assert_eq!(environments.include_local, Some(false));
|
||||
assert_eq!(environments.environments.len(), 2);
|
||||
assert_eq!(
|
||||
environments.environments[0],
|
||||
@@ -736,6 +813,7 @@ unknown = true
|
||||
fn toml_provider_rejects_malformed_websocket_url() {
|
||||
let err = TomlEnvironmentProvider::new(EnvironmentsToml {
|
||||
default: None,
|
||||
include_local: None,
|
||||
environments: vec![EnvironmentToml {
|
||||
id: "devbox".to_string(),
|
||||
url: Some("ws://".to_string()),
|
||||
@@ -758,6 +836,7 @@ unknown = true
|
||||
codex_home.path().join(ENVIRONMENTS_TOML_FILE),
|
||||
r#"
|
||||
default = "none"
|
||||
include_local = false
|
||||
"#,
|
||||
)
|
||||
.expect("write environments.toml");
|
||||
@@ -772,7 +851,7 @@ default = "none"
|
||||
.map(|(id, _environment)| id)
|
||||
.collect();
|
||||
|
||||
assert!(snapshot.include_local);
|
||||
assert!(!snapshot.include_local);
|
||||
assert!(!environment_ids.contains(&LOCAL_ENVIRONMENT_ID.to_string()));
|
||||
assert_eq!(snapshot.default, EnvironmentDefault::Disabled);
|
||||
}
|
||||
|
||||
@@ -522,9 +522,10 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
)?;
|
||||
let state_db = codex_core::init_state_db(&config).await;
|
||||
let environment_manager = if run_loader_overrides.ignore_user_config {
|
||||
EnvironmentManager::from_env(local_runtime_paths).await?
|
||||
EnvironmentManager::from_env(Some(local_runtime_paths)).await?
|
||||
} else {
|
||||
EnvironmentManager::from_codex_home(config.codex_home.clone(), local_runtime_paths).await?
|
||||
EnvironmentManager::from_codex_home(config.codex_home.clone(), Some(local_runtime_paths))
|
||||
.await?
|
||||
};
|
||||
let in_process_start_args = InProcessClientStartArgs {
|
||||
arg0_paths,
|
||||
|
||||
@@ -96,10 +96,10 @@ pub async fn run_main(
|
||||
let environment_manager = Arc::new(
|
||||
EnvironmentManager::from_codex_home(
|
||||
config.codex_home.clone(),
|
||||
ExecServerRuntimePaths::from_optional_paths(
|
||||
Some(ExecServerRuntimePaths::from_optional_paths(
|
||||
arg0_paths.codex_self_exe.clone(),
|
||||
arg0_paths.codex_linux_sandbox_exe.clone(),
|
||||
)?,
|
||||
)?),
|
||||
)
|
||||
.await
|
||||
.map_err(std::io::Error::other)?,
|
||||
|
||||
@@ -115,7 +115,8 @@ async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
)?;
|
||||
let thread_store = thread_store_from_config(&config, state_db.clone());
|
||||
let environment_manager = Arc::new(
|
||||
EnvironmentManager::from_codex_home(config.codex_home.clone(), local_runtime_paths).await?,
|
||||
EnvironmentManager::from_codex_home(config.codex_home.clone(), Some(local_runtime_paths))
|
||||
.await?,
|
||||
);
|
||||
let installation_id = resolve_installation_id(&config.codex_home).await?;
|
||||
let thread_manager = ThreadManager::new(
|
||||
|
||||
@@ -852,9 +852,9 @@ pub async fn run_main(
|
||||
)?;
|
||||
let environment_manager =
|
||||
if should_load_configured_environments(&loader_overrides, &app_server_target) {
|
||||
EnvironmentManager::from_codex_home(codex_home.clone(), local_runtime_paths).await
|
||||
EnvironmentManager::from_codex_home(codex_home.clone(), Some(local_runtime_paths)).await
|
||||
} else {
|
||||
EnvironmentManager::from_env(local_runtime_paths).await
|
||||
EnvironmentManager::from_env(Some(local_runtime_paths)).await
|
||||
}
|
||||
.map(Arc::new)
|
||||
.map_err(std::io::Error::other)?;
|
||||
@@ -2291,10 +2291,10 @@ mod tests {
|
||||
let target = AppServerTarget::Embedded;
|
||||
let environment_manager = EnvironmentManager::create_for_tests(
|
||||
Some("ws://127.0.0.1:8765".to_string()),
|
||||
ExecServerRuntimePaths::new(
|
||||
Some(ExecServerRuntimePaths::new(
|
||||
std::env::current_exe().expect("current exe"),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)?,
|
||||
)?),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user