diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index 49a7a3b800..2f48936ef1 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -48,6 +48,7 @@ use codex_config::LoaderOverrides; use codex_config::NoopThreadConfigLoader; use codex_config::RemoteThreadConfigLoader; use codex_config::ThreadConfigLoader; +pub use codex_core::RuntimeCapabilities; use codex_core::config::Config; pub use codex_exec_server::EnvironmentManager; pub use codex_exec_server::ExecServerRuntimePaths; @@ -348,6 +349,8 @@ pub struct InProcessClientStartArgs { pub state_db: Option, /// Environment manager used by core execution and filesystem operations. pub environment_manager: Arc, + /// Ambient worker-local capabilities selected for this app-server runtime. + pub runtime_capabilities: Arc, /// Startup warnings emitted after initialize succeeds. pub config_warnings: Vec, /// Session source recorded in app-server thread metadata. @@ -411,6 +414,7 @@ impl InProcessClientStartArgs { log_db: self.log_db, state_db: self.state_db, environment_manager: self.environment_manager, + runtime_capabilities: self.runtime_capabilities, config_warnings: self.config_warnings, session_source: self.session_source, enable_codex_api_key_env: self.enable_codex_api_key_env, @@ -1028,6 +1032,7 @@ mod tests { let state_db = init_state_db(config.as_ref()) .await .expect("state db should initialize for in-process test"); + let environment_manager = Arc::new(EnvironmentManager::default_for_tests()); let client = InProcessAppServerClient::start(InProcessClientStartArgs { arg0_paths: Arg0DispatchPaths::default(), config, @@ -1038,7 +1043,10 @@ mod tests { feedback: CodexFeedback::new(), log_db: None, state_db: Some(state_db), - environment_manager: Arc::new(EnvironmentManager::default_for_tests()), + runtime_capabilities: Arc::new(RuntimeCapabilities::local( + environment_manager.as_ref(), + )), + environment_manager, config_warnings: Vec::new(), session_source, enable_codex_api_key_env: false, @@ -2197,6 +2205,9 @@ mod tests { feedback: CodexFeedback::new(), log_db: None, state_db: None, + runtime_capabilities: Arc::new(RuntimeCapabilities::local( + environment_manager.as_ref(), + )), environment_manager: environment_manager.clone(), config_warnings: Vec::new(), session_source: SessionSource::Exec, @@ -2228,6 +2239,7 @@ mod tests { let mut config = build_test_config().await; config.experimental_thread_config_endpoint = Some("not-a-valid-endpoint".to_string()); + let environment_manager = Arc::new(EnvironmentManager::default_for_tests()); let runtime_args = InProcessClientStartArgs { arg0_paths: Arg0DispatchPaths::default(), config: Arc::new(config), @@ -2238,7 +2250,10 @@ mod tests { feedback: CodexFeedback::new(), log_db: None, state_db: None, - environment_manager: Arc::new(EnvironmentManager::default_for_tests()), + runtime_capabilities: Arc::new(RuntimeCapabilities::local( + environment_manager.as_ref(), + )), + environment_manager, config_warnings: Vec::new(), session_source: SessionSource::Exec, enable_codex_api_key_env: false, diff --git a/codex-rs/app-server/src/config_manager.rs b/codex-rs/app-server/src/config_manager.rs index 25fdc5c0cb..087e4c26d6 100644 --- a/codex-rs/app-server/src/config_manager.rs +++ b/codex-rs/app-server/src/config_manager.rs @@ -5,9 +5,9 @@ use codex_config::ConfigLayerStack; use codex_config::LoaderOverrides; use codex_config::ThreadConfigLoader; use codex_config::loader::load_config_layers_state; +use codex_core::RuntimeCapabilities; use codex_core::config::Config; use codex_core::config::ConfigOverrides; -use codex_exec_server::LOCAL_FS; use codex_features::feature_for_key; use codex_login::AuthManager; use codex_login::default_client::set_default_client_residency_requirement; @@ -33,6 +33,7 @@ pub(crate) struct ConfigManager { strict_config: bool, cloud_requirements: Arc>, arg0_paths: Arg0DispatchPaths, + runtime_capabilities: Arc, thread_config_loader: Arc>>, } @@ -44,6 +45,7 @@ impl ConfigManager { strict_config: bool, cloud_requirements: CloudRequirementsLoader, arg0_paths: Arg0DispatchPaths, + runtime_capabilities: Arc, thread_config_loader: Arc, ) -> Self { Self { @@ -54,6 +56,7 @@ impl ConfigManager { strict_config, cloud_requirements: Arc::new(RwLock::new(cloud_requirements)), arg0_paths, + runtime_capabilities, thread_config_loader: Arc::new(RwLock::new(thread_config_loader)), } } @@ -258,8 +261,12 @@ impl ConfigManager { cwd: Option, ) -> std::io::Result { let thread_config_loader = self.current_thread_config_loader(); + let file_system = self + .runtime_capabilities + .require_local_filesystem("load app-server config layers") + .map_err(std::io::Error::other)?; load_config_layers_state( - LOCAL_FS.as_ref(), + file_system.as_ref(), &self.codex_home, cwd, &self.current_cli_overrides(), @@ -297,6 +304,7 @@ impl ConfigManager { loader_overrides: LoaderOverrides, cloud_requirements: CloudRequirementsLoader, ) -> Self { + let environment_manager = codex_exec_server::EnvironmentManager::default_for_tests(); Self::new( codex_home, cli_overrides, @@ -304,6 +312,7 @@ impl ConfigManager { /*strict_config*/ false, cloud_requirements, Arg0DispatchPaths::default(), + Arc::new(RuntimeCapabilities::local(&environment_manager)), Arc::new(codex_config::NoopThreadConfigLoader), ) } diff --git a/codex-rs/app-server/src/config_manager_service_tests.rs b/codex-rs/app-server/src/config_manager_service_tests.rs index c1a081e023..5d31d4fb2b 100644 --- a/codex-rs/app-server/src/config_manager_service_tests.rs +++ b/codex-rs/app-server/src/config_manager_service_tests.rs @@ -4,12 +4,15 @@ use codex_app_server_protocol::AppConfig; use codex_app_server_protocol::AppToolApproval; use codex_app_server_protocol::AppsConfig; use codex_app_server_protocol::AskForApproval; +use codex_arg0::Arg0DispatchPaths; use codex_config::CloudRequirementsLoader; use codex_config::FeatureRequirementsToml; use codex_config::LoaderOverrides; +use codex_core::RuntimeCapabilities; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::collections::BTreeMap; +use std::sync::Arc; use tempfile::tempdir; #[test] @@ -106,6 +109,31 @@ personality = true Ok(()) } +#[tokio::test] +async fn load_latest_config_rejects_isolated_runtime_without_local_filesystem() { + let tmp = tempdir().expect("tempdir"); + let service = ConfigManager::new( + tmp.path().to_path_buf(), + Vec::new(), + LoaderOverrides::without_managed_config_for_tests(), + /*strict_config*/ false, + CloudRequirementsLoader::default(), + Arg0DispatchPaths::default(), + Arc::new(RuntimeCapabilities::isolated()), + Arc::new(codex_config::NoopThreadConfigLoader), + ); + + let error = service + .load_latest_config(/*fallback_cwd*/ None) + .await + .expect_err("isolated config reload should be unsupported"); + + assert_eq!( + error.to_string(), + "load app-server config requires ambient worker-local filesystem" + ); +} + #[tokio::test] async fn clear_missing_nested_config_is_noop() -> Result<()> { let tmp = tempdir().expect("tempdir"); diff --git a/codex-rs/app-server/src/in_process.rs b/codex-rs/app-server/src/in_process.rs index c75c2d5ad1..096b81c418 100644 --- a/codex-rs/app-server/src/in_process.rs +++ b/codex-rs/app-server/src/in_process.rs @@ -80,6 +80,7 @@ use codex_arg0::Arg0DispatchPaths; use codex_config::CloudRequirementsLoader; use codex_config::LoaderOverrides; use codex_config::ThreadConfigLoader; +use codex_core::RuntimeCapabilities; use codex_core::config::Config; use codex_core::resolve_installation_id; use codex_exec_server::EnvironmentManager; @@ -133,6 +134,8 @@ pub struct InProcessStartArgs { pub state_db: Option, /// Environment manager used by core execution and filesystem operations. pub environment_manager: Arc, + /// Ambient worker-local capabilities selected for this app-server runtime. + pub runtime_capabilities: Arc, /// Startup warnings emitted after initialize succeeds. pub config_warnings: Vec, /// Session source stamped into thread/session metadata. @@ -414,6 +417,7 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult(channel_capacity); @@ -425,6 +429,7 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult IoResult InProcessClientHandle { + start_test_client_with_runtime_capabilities( + session_source, + channel_capacity, + RuntimeCapabilities::local, + ) + .await + } + + async fn start_test_client_with_runtime_capabilities( + session_source: SessionSource, + channel_capacity: usize, + runtime_capabilities: impl FnOnce(&EnvironmentManager) -> RuntimeCapabilities, ) -> InProcessClientHandle { let codex_home = TempDir::new().expect("temp dir"); let config = Arc::new(build_test_config(codex_home.path()).await); let state_db = codex_rollout::state_db::try_init(config.as_ref()) .await .expect("state db should initialize for in-process test"); + let environment_manager = Arc::new(EnvironmentManager::default_for_tests()); let args = InProcessStartArgs { arg0_paths: Arg0DispatchPaths::default(), config, @@ -774,7 +798,8 @@ mod tests { feedback: CodexFeedback::new(), log_db: None, state_db: Some(state_db), - environment_manager: Arc::new(EnvironmentManager::default_for_tests()), + runtime_capabilities: Arc::new(runtime_capabilities(environment_manager.as_ref())), + environment_manager, config_warnings: Vec::new(), session_source, enable_codex_api_key_env: false, @@ -797,6 +822,37 @@ mod tests { start_test_client_with_capacity(session_source, DEFAULT_IN_PROCESS_CHANNEL_CAPACITY).await } + async fn start_isolated_test_client() -> InProcessClientHandle { + start_test_client_with_runtime_capabilities( + SessionSource::Cli, + DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, + |_| RuntimeCapabilities::isolated(), + ) + .await + } + + async fn isolated_request_error( + client: &InProcessClientHandle, + request: ClientRequest, + ) -> JSONRPCErrorError { + client + .request(request) + .await + .expect("request transport should work") + .expect_err("isolated request should be unsupported") + } + + fn assert_method_not_found(error: JSONRPCErrorError, message: &str) { + assert_eq!( + error, + JSONRPCErrorError { + code: crate::error_code::METHOD_NOT_FOUND_ERROR_CODE, + message: message.to_string(), + data: None, + } + ); + } + #[tokio::test] async fn in_process_start_initializes_and_handles_typed_v2_request() { let client = start_test_client(SessionSource::Cli).await; @@ -873,6 +929,98 @@ mod tests { .expect("in-process runtime should shutdown cleanly"); } + #[tokio::test] + async fn isolated_in_process_runtime_rejects_ambient_local_requests() { + let client = start_isolated_test_client().await; + let cwd = AbsolutePathBuf::try_from( + client + ._test_codex_home + .as_ref() + .expect("test codex home") + .path() + .to_path_buf(), + ) + .expect("test codex home should be absolute"); + + assert_method_not_found( + isolated_request_error( + &client, + ClientRequest::FsReadFile { + request_id: RequestId::Integer(5), + params: FsReadFileParams { path: cwd.clone() }, + }, + ) + .await, + "fs requests require ambient worker-local filesystem", + ); + assert_method_not_found( + isolated_request_error( + &client, + ClientRequest::CommandExec { + request_id: RequestId::Integer(6), + params: CommandExecParams { + command: vec!["echo".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, + "command/exec requires ambient worker-local environment", + ); + assert_method_not_found( + isolated_request_error( + &client, + ClientRequest::ProcessSpawn { + request_id: RequestId::Integer(7), + params: ProcessSpawnParams { + command: vec!["echo".to_string()], + process_handle: "process-1".to_string(), + cwd: cwd.clone(), + tty: false, + stream_stdin: false, + stream_stdout_stderr: false, + output_bytes_cap: None, + timeout_ms: None, + env: None, + size: None, + }, + }, + ) + .await, + "process/spawn requires ambient worker-local environment", + ); + assert_method_not_found( + isolated_request_error( + &client, + ClientRequest::GitDiffToRemote { + request_id: RequestId::Integer(8), + params: GitDiffToRemoteParams { + cwd: cwd.into_path_buf(), + }, + }, + ) + .await, + "git diff to remote requires ambient worker-local environment", + ); + + client + .shutdown() + .await + .expect("in-process runtime should shutdown cleanly"); + } + #[test] fn guaranteed_delivery_helpers_cover_terminal_server_notifications() { assert!(server_notification_requires_delivery( diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 142df216b5..d4272804c3 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -47,6 +47,7 @@ use codex_app_server_protocol::TextRange as AppTextRange; use codex_config::ConfigLoadError; use codex_config::TextRange as CoreTextRange; use codex_core::ExecPolicyError; +use codex_core::RuntimeCapabilities; use codex_core::check_execpolicy_for_warnings; use codex_core::config::find_codex_home; use codex_exec_server::EnvironmentManager; @@ -451,6 +452,7 @@ pub async fn run_main_with_transport_options( } .map(Arc::new) .map_err(std::io::Error::other)?; + let runtime_capabilities = Arc::new(RuntimeCapabilities::local(environment_manager.as_ref())); let config_manager = ConfigManager::new( codex_home.to_path_buf(), cli_kv_overrides.clone(), @@ -458,6 +460,7 @@ pub async fn run_main_with_transport_options( strict_config, Default::default(), arg0_paths.clone(), + Arc::clone(&runtime_capabilities), Arc::new(NoopThreadConfigLoader), ); match config_manager @@ -791,6 +794,7 @@ pub async fn run_main_with_transport_options( config: Arc::new(config), config_manager, environment_manager, + runtime_capabilities, feedback: feedback.clone(), log_db, state_db: state_db.clone(), diff --git a/codex-rs/app-server/src/mcp_refresh.rs b/codex-rs/app-server/src/mcp_refresh.rs index f7d32b2ea8..7564372e19 100644 --- a/codex-rs/app-server/src/mcp_refresh.rs +++ b/codex-rs/app-server/src/mcp_refresh.rs @@ -110,6 +110,7 @@ mod tests { use codex_config::ThreadConfigLoadErrorCode; use codex_config::ThreadConfigLoader; use codex_config::ThreadConfigSource; + use codex_core::RuntimeCapabilities; use codex_core::config::ConfigOverrides; use codex_core::init_state_db; use codex_core::thread_store_from_config; @@ -210,6 +211,9 @@ mod tests { /*strict_config*/ false, CloudRequirementsLoader::default(), Arg0DispatchPaths::default(), + Arc::new(RuntimeCapabilities::local( + &EnvironmentManager::default_for_tests(), + )), loader.clone(), ); diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 4e2c6f38cc..7445485a5a 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -65,6 +65,7 @@ use codex_app_server_protocol::ServerRequestPayload; use codex_app_server_protocol::experimental_required_message; use codex_arg0::Arg0DispatchPaths; use codex_chatgpt::workspace_settings; +use codex_core::RuntimeCapabilities; use codex_core::ThreadManager; use codex_core::config::Config; use codex_exec_server::EnvironmentManager; @@ -259,6 +260,7 @@ pub(crate) struct MessageProcessorArgs { pub(crate) config: Arc, pub(crate) config_manager: ConfigManager, pub(crate) environment_manager: Arc, + pub(crate) runtime_capabilities: Arc, pub(crate) feedback: CodexFeedback, pub(crate) log_db: Option, pub(crate) state_db: Option, @@ -282,6 +284,7 @@ impl MessageProcessor { config, config_manager, environment_manager, + runtime_capabilities, feedback, log_db, state_db, @@ -355,8 +358,10 @@ impl MessageProcessor { Arc::clone(&config), outgoing.clone(), config_manager.clone(), + Arc::clone(&runtime_capabilities), ); - let process_exec_processor = ProcessExecRequestProcessor::new(outgoing.clone()); + let process_exec_processor = + ProcessExecRequestProcessor::new(outgoing.clone(), Arc::clone(&runtime_capabilities)); let feedback_processor = FeedbackRequestProcessor::new( auth_manager.clone(), Arc::clone(&thread_manager), @@ -365,7 +370,7 @@ impl MessageProcessor { log_db, state_db.clone(), ); - let git_processor = GitRequestProcessor::new(); + let git_processor = GitRequestProcessor::new(Arc::clone(&runtime_capabilities)); let initialize_processor = InitializeRequestProcessor::new( outgoing.clone(), analytics_events_client.clone(), @@ -461,13 +466,8 @@ 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, - ); + let fs_processor = + FsRequestProcessor::new(runtime_capabilities.local_filesystem(), fs_watch_manager); let windows_sandbox_processor = WindowsSandboxRequestProcessor::new( outgoing.clone(), Arc::clone(&config), diff --git a/codex-rs/app-server/src/message_processor_tracing_tests.rs b/codex-rs/app-server/src/message_processor_tracing_tests.rs index a9625d3086..6b22204c31 100644 --- a/codex-rs/app-server/src/message_processor_tracing_tests.rs +++ b/codex-rs/app-server/src/message_processor_tracing_tests.rs @@ -25,6 +25,7 @@ use codex_app_server_protocol::UserInput; use codex_arg0::Arg0DispatchPaths; use codex_config::CloudRequirementsLoader; use codex_config::LoaderOverrides; +use codex_core::RuntimeCapabilities; use codex_core::config::Config; use codex_core::config::ConfigBuilder; use codex_exec_server::EnvironmentManager; @@ -235,6 +236,8 @@ async fn build_test_processor( let (outgoing_tx, outgoing_rx) = mpsc::channel(16); let auth_manager = AuthManager::shared_from_config(config.as_ref(), /*enable_codex_api_key_env*/ false).await; + let environment_manager = Arc::new(EnvironmentManager::default_for_tests()); + let runtime_capabilities = Arc::new(RuntimeCapabilities::local(environment_manager.as_ref())); let config_manager = ConfigManager::new( config.codex_home.to_path_buf(), Vec::new(), @@ -242,6 +245,7 @@ async fn build_test_processor( /*strict_config*/ false, CloudRequirementsLoader::default(), Arg0DispatchPaths::default(), + Arc::clone(&runtime_capabilities), Arc::new(codex_config::NoopThreadConfigLoader), ); let analytics_events_client = @@ -256,7 +260,8 @@ async fn build_test_processor( arg0_paths: Arg0DispatchPaths::default(), config, config_manager, - environment_manager: Arc::new(EnvironmentManager::default_for_tests()), + environment_manager, + runtime_capabilities, feedback: CodexFeedback::new(), log_db: None, state_db: None, diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index 305e864ba0..bf286ec56d 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -264,6 +264,7 @@ use codex_core::ExternalGoalPreviousStatus; use codex_core::ExternalGoalSet; use codex_core::ForkSnapshot; use codex_core::NewThread; +use codex_core::RuntimeCapabilities; #[cfg(test)] use codex_core::SessionMeta; use codex_core::StartThreadOptions; @@ -480,6 +481,7 @@ pub(crate) use windows_sandbox_processor::WindowsSandboxRequestProcessor; use crate::error_code::internal_error; use crate::error_code::invalid_request; +use crate::error_code::method_not_found; use crate::filters::compute_source_filters; use crate::filters::source_kind_matches; use crate::thread_state::ConnectionCapabilities; diff --git a/codex-rs/app-server/src/request_processors/command_exec_processor.rs b/codex-rs/app-server/src/request_processors/command_exec_processor.rs index 1219eb17f9..c40d26bb95 100644 --- a/codex-rs/app-server/src/request_processors/command_exec_processor.rs +++ b/codex-rs/app-server/src/request_processors/command_exec_processor.rs @@ -6,6 +6,7 @@ pub(crate) struct CommandExecRequestProcessor { config: Arc, outgoing: Arc, config_manager: ConfigManager, + runtime_capabilities: Arc, command_exec_manager: CommandExecManager, } @@ -15,12 +16,14 @@ impl CommandExecRequestProcessor { config: Arc, outgoing: Arc, config_manager: ConfigManager, + runtime_capabilities: Arc, ) -> Self { Self { arg0_paths, config, outgoing, config_manager, + runtime_capabilities, command_exec_manager: CommandExecManager::default(), } } @@ -89,6 +92,9 @@ impl CommandExecRequestProcessor { params: CommandExecParams, ) -> Result<(), JSONRPCErrorError> { tracing::debug!("ExecOneOffCommand params: {params:?}"); + self.runtime_capabilities + .require_local_environment("command/exec") + .map_err(|err| method_not_found(err.to_string()))?; let request = request_id.clone(); diff --git a/codex-rs/app-server/src/request_processors/fs_processor.rs b/codex-rs/app-server/src/request_processors/fs_processor.rs index 01b9b20bfd..0a543effa2 100644 --- a/codex-rs/app-server/src/request_processors/fs_processor.rs +++ b/codex-rs/app-server/src/request_processors/fs_processor.rs @@ -1,5 +1,6 @@ use crate::error_code::internal_error; use crate::error_code::invalid_request; +use crate::error_code::method_not_found; use crate::fs_watch::FsWatchManager; use crate::outgoing_message::ConnectionId; use base64::Engine; @@ -33,13 +34,13 @@ use std::sync::Arc; #[derive(Clone)] pub(crate) struct FsRequestProcessor { - file_system: Arc, + file_system: Option>, fs_watch_manager: FsWatchManager, } impl FsRequestProcessor { pub(crate) fn new( - file_system: Arc, + file_system: Option>, fs_watch_manager: FsWatchManager, ) -> Self { Self { @@ -48,6 +49,12 @@ impl FsRequestProcessor { } } + fn file_system(&self) -> Result<&Arc, JSONRPCErrorError> { + self.file_system + .as_ref() + .ok_or_else(|| method_not_found("fs requests require ambient worker-local filesystem")) + } + pub(crate) async fn connection_closed(&self, connection_id: ConnectionId) { self.fs_watch_manager.connection_closed(connection_id).await; } @@ -57,7 +64,7 @@ impl FsRequestProcessor { params: FsReadFileParams, ) -> Result { let bytes = self - .file_system + .file_system()? .read_file(¶ms.path, /*sandbox*/ None) .await .map_err(map_fs_error)?; @@ -75,7 +82,7 @@ impl FsRequestProcessor { "fs/writeFile requires valid base64 dataBase64: {err}" )) })?; - self.file_system + self.file_system()? .write_file(¶ms.path, bytes, /*sandbox*/ None) .await .map_err(map_fs_error)?; @@ -86,7 +93,7 @@ impl FsRequestProcessor { &self, params: FsCreateDirectoryParams, ) -> Result { - self.file_system + self.file_system()? .create_directory( ¶ms.path, CreateDirectoryOptions { @@ -104,7 +111,7 @@ impl FsRequestProcessor { params: FsGetMetadataParams, ) -> Result { let metadata = self - .file_system + .file_system()? .get_metadata(¶ms.path, /*sandbox*/ None) .await .map_err(map_fs_error)?; @@ -122,7 +129,7 @@ impl FsRequestProcessor { params: FsReadDirectoryParams, ) -> Result { let entries = self - .file_system + .file_system()? .read_directory(¶ms.path, /*sandbox*/ None) .await .map_err(map_fs_error)?; @@ -142,7 +149,7 @@ impl FsRequestProcessor { &self, params: FsRemoveParams, ) -> Result { - self.file_system + self.file_system()? .remove( ¶ms.path, RemoveOptions { @@ -160,7 +167,7 @@ impl FsRequestProcessor { &self, params: FsCopyParams, ) -> Result { - self.file_system + self.file_system()? .copy( ¶ms.source_path, ¶ms.destination_path, @@ -179,6 +186,7 @@ impl FsRequestProcessor { connection_id: ConnectionId, params: FsWatchParams, ) -> Result { + self.file_system()?; self.fs_watch_manager.watch(connection_id, params).await } diff --git a/codex-rs/app-server/src/request_processors/git_processor.rs b/codex-rs/app-server/src/request_processors/git_processor.rs index b7c5fad610..70c00c4f8c 100644 --- a/codex-rs/app-server/src/request_processors/git_processor.rs +++ b/codex-rs/app-server/src/request_processors/git_processor.rs @@ -1,17 +1,24 @@ use super::*; #[derive(Clone)] -pub(crate) struct GitRequestProcessor; +pub(crate) struct GitRequestProcessor { + runtime_capabilities: Arc, +} impl GitRequestProcessor { - pub(crate) fn new() -> Self { - Self + pub(crate) fn new(runtime_capabilities: Arc) -> Self { + Self { + runtime_capabilities, + } } pub(crate) async fn git_diff_to_remote( &self, params: GitDiffToRemoteParams, ) -> Result, JSONRPCErrorError> { + self.runtime_capabilities + .require_local_environment("git diff to remote") + .map_err(|err| method_not_found(err.to_string()))?; self.git_diff_to_origin(params.cwd) .await .map(|response| Some(response.into())) diff --git a/codex-rs/app-server/src/request_processors/process_exec_processor.rs b/codex-rs/app-server/src/request_processors/process_exec_processor.rs index 5742d0e4d5..b5362b906d 100644 --- a/codex-rs/app-server/src/request_processors/process_exec_processor.rs +++ b/codex-rs/app-server/src/request_processors/process_exec_processor.rs @@ -20,6 +20,7 @@ use codex_app_server_protocol::ProcessTerminalSize; use codex_app_server_protocol::ProcessWriteStdinParams; use codex_app_server_protocol::ProcessWriteStdinResponse; use codex_app_server_protocol::ServerNotification; +use codex_core::RuntimeCapabilities; use codex_core::exec::ExecExpiration; use codex_core::exec::ExecExpirationOutcome; use codex_core::exec::IO_DRAIN_TIMEOUT_MS; @@ -38,6 +39,7 @@ use tokio_util::sync::CancellationToken; use crate::error_code::internal_error; use crate::error_code::invalid_params; use crate::error_code::invalid_request; +use crate::error_code::method_not_found; use crate::outgoing_message::ConnectionId; use crate::outgoing_message::ConnectionRequestId; use crate::outgoing_message::OutgoingMessageSender; @@ -48,13 +50,18 @@ const OUTPUT_CHUNK_SIZE_HINT: usize = 64 * 1024; #[derive(Clone)] pub(crate) struct ProcessExecRequestProcessor { outgoing: Arc, + runtime_capabilities: Arc, process_exec_manager: ProcessExecManager, } impl ProcessExecRequestProcessor { - pub(crate) fn new(outgoing: Arc) -> Self { + pub(crate) fn new( + outgoing: Arc, + runtime_capabilities: Arc, + ) -> Self { Self { outgoing, + runtime_capabilities, process_exec_manager: ProcessExecManager::default(), } } @@ -78,6 +85,9 @@ impl ProcessExecRequestProcessor { } = params; let method_name = "process/spawn"; tracing::debug!("{method_name} command: {command:?}"); + self.runtime_capabilities + .require_local_environment(method_name) + .map_err(|err| method_not_found(err.to_string()))?; if command.is_empty() { return Err(invalid_request("command must not be empty")); } diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index aa33fc623f..f62b52040e 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -594,6 +594,9 @@ mod thread_processor_behavior_tests { /*strict_config*/ false, CloudRequirementsLoader::default(), Arg0DispatchPaths::default(), + Arc::new(codex_core::RuntimeCapabilities::local( + &codex_exec_server::EnvironmentManager::default_for_tests(), + )), Arc::new(StaticThreadConfigLoader::new(vec![ ThreadConfigSource::Session(SessionThreadConfig { model_provider: Some("session".to_string()), diff --git a/codex-rs/app-server/tests/suite/conversation_summary.rs b/codex-rs/app-server/tests/suite/conversation_summary.rs index 5253850107..28325f5448 100644 --- a/codex-rs/app-server/tests/suite/conversation_summary.rs +++ b/codex-rs/app-server/tests/suite/conversation_summary.rs @@ -17,6 +17,7 @@ use codex_app_server_protocol::RequestId; use codex_arg0::Arg0DispatchPaths; use codex_config::CloudRequirementsLoader; use codex_config::LoaderOverrides; +use codex_core::RuntimeCapabilities; use codex_core::config::ConfigBuilder; use codex_exec_server::EnvironmentManager; use codex_feedback::CodexFeedback; @@ -144,6 +145,7 @@ async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() -> .loader_overrides(loader_overrides.clone()) .build() .await?; + let environment_manager = Arc::new(EnvironmentManager::default_for_tests()); let client = in_process::start(InProcessStartArgs { arg0_paths: Arg0DispatchPaths::default(), config: Arc::new(config), @@ -155,7 +157,8 @@ async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() -> feedback: CodexFeedback::new(), log_db: None, state_db: None, - environment_manager: Arc::new(EnvironmentManager::default_for_tests()), + runtime_capabilities: Arc::new(RuntimeCapabilities::local(environment_manager.as_ref())), + environment_manager, config_warnings: Vec::new(), session_source: SessionSource::Cli, enable_codex_api_key_env: false, diff --git a/codex-rs/app-server/tests/suite/v2/mcp_resource.rs b/codex-rs/app-server/tests/suite/v2/mcp_resource.rs index bddf57f666..319dc92642 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_resource.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_resource.rs @@ -23,6 +23,7 @@ use codex_arg0::Arg0DispatchPaths; use codex_config::CloudRequirementsLoader; use codex_config::LoaderOverrides; use codex_config::types::AuthCredentialsStoreMode; +use codex_core::RuntimeCapabilities; use codex_core::config::ConfigBuilder; use codex_exec_server::EnvironmentManager; use codex_feedback::CodexFeedback; @@ -195,6 +196,7 @@ async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()> { .await?; // This negative-path test does not need the stdio subprocess; keeping it // in-process avoids child-process teardown timing in nextest leak detection. + let environment_manager = Arc::new(EnvironmentManager::default_for_tests()); let client = in_process::start(InProcessStartArgs { arg0_paths: Arg0DispatchPaths::default(), config: Arc::new(config), @@ -206,7 +208,8 @@ async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()> { feedback: CodexFeedback::new(), log_db: None, state_db: None, - environment_manager: Arc::new(EnvironmentManager::default_for_tests()), + runtime_capabilities: Arc::new(RuntimeCapabilities::local(environment_manager.as_ref())), + environment_manager, config_warnings: Vec::new(), session_source: SessionSource::Cli, enable_codex_api_key_env: false, diff --git a/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs b/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs index 9787868f5c..1bf0d66eda 100644 --- a/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs +++ b/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs @@ -37,6 +37,7 @@ use codex_arg0::Arg0DispatchPaths; use codex_config::CloudRequirementsLoader; use codex_config::LoaderOverrides; use codex_config::NoopThreadConfigLoader; +use codex_core::RuntimeCapabilities; use codex_core::config::ConfigBuilder; use codex_exec_server::EnvironmentManager; use codex_feedback::CodexFeedback; @@ -70,6 +71,7 @@ async fn thread_start_with_non_local_thread_store_does_not_create_local_persiste let thread_store = InMemoryThreadStore::for_id(store_id.clone()); let _in_memory_store = InMemoryThreadStoreId { store_id }; + let environment_manager = Arc::new(EnvironmentManager::default_for_tests()); let mut client = in_process::start(InProcessStartArgs { arg0_paths: Arg0DispatchPaths::default(), config: Arc::new(config), @@ -81,7 +83,8 @@ async fn thread_start_with_non_local_thread_store_does_not_create_local_persiste feedback: CodexFeedback::new(), log_db: None, state_db: None, - environment_manager: Arc::new(EnvironmentManager::default_for_tests()), + runtime_capabilities: Arc::new(RuntimeCapabilities::local(environment_manager.as_ref())), + environment_manager, config_warnings: Vec::new(), session_source: SessionSource::Cli, enable_codex_api_key_env: false, diff --git a/codex-rs/app-server/tests/suite/v2/thread_read.rs b/codex-rs/app-server/tests/suite/v2/thread_read.rs index c8e3e179fb..9df4f9cd30 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_read.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_read.rs @@ -43,6 +43,7 @@ use codex_arg0::Arg0DispatchPaths; use codex_config::CloudRequirementsLoader; use codex_config::LoaderOverrides; use codex_core::ARCHIVED_SESSIONS_SUBDIR; +use codex_core::RuntimeCapabilities; use codex_core::config::ConfigBuilder; use codex_exec_server::EnvironmentManager; use codex_feedback::CodexFeedback; @@ -370,6 +371,7 @@ async fn thread_turns_list_reads_store_history_without_rollout_path() -> Result< .loader_overrides(loader_overrides.clone()) .build() .await?; + let environment_manager = Arc::new(EnvironmentManager::default_for_tests()); let client = in_process::start(InProcessStartArgs { arg0_paths: Arg0DispatchPaths::default(), config: Arc::new(config), @@ -381,7 +383,8 @@ async fn thread_turns_list_reads_store_history_without_rollout_path() -> Result< feedback: CodexFeedback::new(), log_db: None, state_db: None, - environment_manager: Arc::new(EnvironmentManager::default_for_tests()), + runtime_capabilities: Arc::new(RuntimeCapabilities::local(environment_manager.as_ref())), + environment_manager, config_warnings: Vec::new(), session_source: SessionSource::Cli.into(), enable_codex_api_key_env: false, @@ -436,6 +439,7 @@ async fn thread_read_loaded_include_turns_reads_store_history_without_rollout_pa .loader_overrides(loader_overrides.clone()) .build() .await?; + let environment_manager = Arc::new(EnvironmentManager::default_for_tests()); let client = in_process::start(InProcessStartArgs { arg0_paths: Arg0DispatchPaths::default(), config: Arc::new(config), @@ -447,7 +451,8 @@ async fn thread_read_loaded_include_turns_reads_store_history_without_rollout_pa feedback: CodexFeedback::new(), log_db: None, state_db: None, - environment_manager: Arc::new(EnvironmentManager::default_for_tests()), + runtime_capabilities: Arc::new(RuntimeCapabilities::local(environment_manager.as_ref())), + environment_manager, config_warnings: Vec::new(), session_source: SessionSource::Cli.into(), enable_codex_api_key_env: false, @@ -522,6 +527,7 @@ async fn thread_list_includes_store_thread_without_rollout_path() -> Result<()> .loader_overrides(loader_overrides.clone()) .build() .await?; + let environment_manager = Arc::new(EnvironmentManager::default_for_tests()); let client = in_process::start(InProcessStartArgs { arg0_paths: Arg0DispatchPaths::default(), config: Arc::new(config), @@ -533,7 +539,8 @@ async fn thread_list_includes_store_thread_without_rollout_path() -> Result<()> feedback: CodexFeedback::new(), log_db: None, state_db: None, - environment_manager: Arc::new(EnvironmentManager::default_for_tests()), + runtime_capabilities: Arc::new(RuntimeCapabilities::local(environment_manager.as_ref())), + environment_manager, config_warnings: Vec::new(), session_source: SessionSource::Cli.into(), enable_codex_api_key_env: false, diff --git a/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs b/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs index d2f8f268a2..221e771612 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs @@ -24,6 +24,7 @@ use codex_app_server_protocol::UserInput; use codex_arg0::Arg0DispatchPaths; use codex_config::CloudRequirementsLoader; use codex_config::LoaderOverrides; +use codex_core::RuntimeCapabilities; use codex_core::config::ConfigBuilder; use codex_core::find_archived_thread_path_by_id_str; use codex_core::find_thread_path_by_id_str; @@ -240,6 +241,7 @@ async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> { .loader_overrides(loader_overrides.clone()) .build() .await?; + let environment_manager = Arc::new(EnvironmentManager::default_for_tests()); let client = in_process::start(InProcessStartArgs { arg0_paths: Arg0DispatchPaths::default(), config: Arc::new(config), @@ -251,7 +253,8 @@ async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> { feedback: CodexFeedback::new(), log_db: None, state_db: None, - environment_manager: Arc::new(EnvironmentManager::default_for_tests()), + runtime_capabilities: Arc::new(RuntimeCapabilities::local(environment_manager.as_ref())), + environment_manager, config_warnings: Vec::new(), session_source: SessionSource::Cli, enable_codex_api_key_env: false, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 9e54a72abd..55e82bf764 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -56,6 +56,7 @@ use codex_config::ConfigLoadError; use codex_config::ConfigLoadOptions; use codex_config::LoaderOverrides; use codex_config::format_config_error_with_source; +use codex_core::RuntimeCapabilities; use codex_core::StateDbHandle; use codex_core::check_execpolicy_for_warnings; use codex_core::config::Config; @@ -536,6 +537,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result feedback: CodexFeedback::new(), log_db: None, state_db: state_db.clone(), + runtime_capabilities: std::sync::Arc::new(RuntimeCapabilities::local(&environment_manager)), environment_manager: std::sync::Arc::new(environment_manager), config_warnings, session_source: SessionSource::Exec, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 9ece5e3776..5fcbda5f05 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -27,6 +27,7 @@ use codex_app_server_client::InProcessClientStartArgs; use codex_app_server_client::RemoteAppServerClient; use codex_app_server_client::RemoteAppServerConnectArgs; pub use codex_app_server_client::RemoteAppServerEndpoint; +use codex_app_server_client::RuntimeCapabilities; use codex_app_server_protocol::Account as AppServerAccount; use codex_app_server_protocol::AskForApproval; use codex_app_server_protocol::AuthMode as AppServerAuthMode; @@ -575,6 +576,7 @@ where feedback, log_db, state_db, + runtime_capabilities: Arc::new(RuntimeCapabilities::local(environment_manager.as_ref())), environment_manager, config_warnings, session_source: serde_json::from_value(serde_json::json!("cli")) diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index 8486de32b4..c0650f621e 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -1035,6 +1035,7 @@ mod tests { use codex_app_server_client::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY; use codex_app_server_client::InProcessAppServerClient; use codex_app_server_client::InProcessClientStartArgs; + use codex_app_server_client::RuntimeCapabilities; use codex_arg0::Arg0DispatchPaths; use codex_cloud_requirements::cloud_requirements_loader_for_storage; use codex_config::types::AuthCredentialsStoreMode; @@ -1051,6 +1052,8 @@ mod tests { .build() .await .unwrap(); + let environment_manager = + Arc::new(codex_app_server_client::EnvironmentManager::default_for_tests()); let client = InProcessAppServerClient::start(InProcessClientStartArgs { arg0_paths: Arg0DispatchPaths::default(), config: Arc::new(config), @@ -1067,9 +1070,10 @@ mod tests { feedback: codex_feedback::CodexFeedback::new(), log_db: None, state_db: None, - environment_manager: Arc::new( - codex_app_server_client::EnvironmentManager::default_for_tests(), - ), + runtime_capabilities: Arc::new(RuntimeCapabilities::local( + environment_manager.as_ref(), + )), + environment_manager, config_warnings: Vec::new(), session_source: serde_json::from_value(serde_json::json!("cli")) .expect("cli session source should deserialize"),