Compare commits

...

21 Commits

Author SHA1 Message Date
starr-openai
63d034844a Adapt runtime capabilities to local env API 2026-05-19 02:59:25 -07:00
starr-openai
d762993b67 Define runtime capabilities v1 2026-05-19 02:16:44 -07:00
starr-openai
49c3495b60 Fix MCP runtime argument comment lint 2026-05-18 21:51:34 -07:00
starr-openai
b63979021f Fix MCP runtime bool lint 2026-05-18 21:42:06 -07:00
starr-openai
b83f8fb0ea Fix MCP runtime lint failures 2026-05-18 21:41:17 -07:00
starr-openai
416c978757 Simplify MCP runtime placement failures 2026-05-18 21:31:39 -07:00
starr-openai
9b81714f50 Document optional local environment behavior 2026-05-18 21:16:58 -07:00
starr-openai
d265ea2cf3 Simplify MCP runtime environment selection 2026-05-18 19:06:37 -07:00
starr-openai
4fdb044248 Simplify optional local environment follow-up 2026-05-18 18:56:11 -07:00
starr-openai
4af256e946 codex: fix CI failure on PR #23369 2026-05-18 18:18:04 -07:00
starr-openai
85ec777c89 codex: fix CI failure on PR #23369 2026-05-18 18:15:40 -07:00
starr-openai
c082f8e90d codex: fix CI failure on PR #23369 2026-05-18 18:01:08 -07:00
starr-openai
51e12c7a48 Fix rebased MCP startup failure handling 2026-05-18 17:54:36 -07:00
starr-openai
e48b748c00 Preserve skipped MCP startup failures 2026-05-18 17:53:21 -07:00
starr-openai
03af15743a Simplify MCP local environment plumbing 2026-05-18 17:52:55 -07:00
starr-openai
6e47df9451 Simplify optional local env review findings 2026-05-18 17:52:55 -07:00
starr-openai
e9c7c88730 Cover no-local MCP filtering 2026-05-18 17:52:55 -07:00
starr-openai
3f11ea6503 Support no-local MCP runtime paths 2026-05-18 17:52:55 -07:00
starr-openai
2ac71d4e0b Reject local-only app-server APIs without local env 2026-05-18 17:52:55 -07:00
starr-openai
1643f20b47 Handle missing local environment fallbacks 2026-05-18 17:52:44 -07:00
starr-openai
0b06bcf47a Make local environment optional in EnvironmentManager 2026-05-18 17:52:44 -07:00
28 changed files with 996 additions and 294 deletions

View File

@@ -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,
);

View File

@@ -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)?;

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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;

View File

@@ -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<()> {

View File

@@ -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()?;

View File

@@ -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()?;

View File

@@ -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());

View File

@@ -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)
};

View File

@@ -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
);
}
}

View File

@@ -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,

View File

@@ -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),

View File

@@ -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;

View File

@@ -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()
}),

View File

@@ -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,

View 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}"),
}
}
}

View File

@@ -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(),
),

View File

@@ -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(),
),
};

View File

@@ -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"),

View File

@@ -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
});

View File

@@ -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())

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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)?,

View File

@@ -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(

View File

@@ -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;