diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index ee22adaa4c..0f61148d63 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -941,7 +941,7 @@ impl TurnContext { sandbox_policy: self.sandbox_policy.get(), windows_sandbox_level: self.windows_sandbox_level, }) - .with_has_attached_executor(self.environment.has_attached_executor()) + .with_attached_executor(self.environment.attached_executor()) .with_unified_exec_shell_mode(self.tools_config.unified_exec_shell_mode.clone()) .with_web_search_config(self.tools_config.web_search_config.clone()) .with_allow_login_shell(self.tools_config.allow_login_shell) @@ -1396,7 +1396,7 @@ impl Session { sandbox_policy: session_configuration.sandbox_policy.get(), windows_sandbox_level: session_configuration.windows_sandbox_level, }) - .with_has_attached_executor(environment.has_attached_executor()) + .with_attached_executor(environment.attached_executor()) .with_unified_exec_shell_mode_for_session( user_shell, shell_zsh_path, @@ -5474,7 +5474,7 @@ async fn spawn_review_thread( sess.services.shell_zsh_path.as_ref(), sess.services.main_execve_wrapper_exe.as_ref(), ) - .with_has_attached_executor(parent_turn_context.environment.has_attached_executor()) + .with_attached_executor(parent_turn_context.environment.attached_executor()) .with_web_search_config(/*web_search_config*/ None) .with_allow_login_shell(config.permissions.allow_login_shell) .with_agent_roles(config.agent_roles.clone()); diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index f5e5d4463e..99d4b65057 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -5259,7 +5259,12 @@ async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request() let turn_context = Arc::new(turn_context_raw); let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())); - let handler = UnifiedExecHandler; + let handler = UnifiedExecHandler::new( + turn_context + .environment + .attached_executor() + .expect("test turn context should have an attached executor"), + ); let resp = handler .handle(ToolInvocation { session: Arc::clone(&session), diff --git a/codex-rs/core/src/codex_tests_guardian.rs b/codex-rs/core/src/codex_tests_guardian.rs index f5698d58ff..3e4ef1e109 100644 --- a/codex-rs/core/src/codex_tests_guardian.rs +++ b/codex-rs/core/src/codex_tests_guardian.rs @@ -207,7 +207,12 @@ async fn guardian_allows_unified_exec_additional_permissions_requests_past_polic let turn_context = Arc::new(turn_context_raw); let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())); - let handler = UnifiedExecHandler; + let handler = UnifiedExecHandler::new( + turn_context + .environment + .attached_executor() + .expect("test turn context should have an attached executor"), + ); let resp = handler .handle(ToolInvocation { session: Arc::clone(&session), diff --git a/codex-rs/core/src/tools/handlers/unified_exec.rs b/codex-rs/core/src/tools/handlers/unified_exec.rs index b512a878b6..bebb7c78d2 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec.rs @@ -27,6 +27,7 @@ use crate::unified_exec::UnifiedExecContext; use crate::unified_exec::UnifiedExecProcessManager; use crate::unified_exec::WriteStdinRequest; use async_trait::async_trait; +use codex_exec_server::AttachedExecutor; use codex_features::Feature; use codex_otel::SessionTelemetry; use codex_otel::metrics::names::TOOL_CALL_UNIFIED_EXEC_METRIC; @@ -35,7 +36,15 @@ use serde::Deserialize; use std::path::PathBuf; use std::sync::Arc; -pub struct UnifiedExecHandler; +pub struct UnifiedExecHandler { + attached_executor: Arc, +} + +impl UnifiedExecHandler { + pub fn new(attached_executor: Arc) -> Self { + Self { attached_executor } + } +} #[derive(Debug, Deserialize)] pub(crate) struct ExecCommandArgs { @@ -179,7 +188,12 @@ impl ToolHandler for UnifiedExecHandler { }; let manager: &UnifiedExecProcessManager = &session.services.unified_exec_manager; - let context = UnifiedExecContext::new(session.clone(), turn.clone(), call_id.clone()); + let context = UnifiedExecContext::new( + session.clone(), + turn.clone(), + call_id.clone(), + Arc::clone(&self.attached_executor), + ); let response = match tool_name.as_str() { "exec_command" => { diff --git a/codex-rs/core/src/tools/handlers/unified_exec_tests.rs b/codex-rs/core/src/tools/handlers/unified_exec_tests.rs index 11b75b28d4..f044418104 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec_tests.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec_tests.rs @@ -19,6 +19,14 @@ use crate::tools::registry::ToolHandler; use crate::turn_diff_tracker::TurnDiffTracker; use tokio::sync::Mutex; +fn local_exec_handler() -> UnifiedExecHandler { + UnifiedExecHandler::new( + codex_exec_server::Environment::default() + .attached_executor() + .expect("default environment should have an attached executor"), + ) +} + #[test] fn test_get_command_uses_default_shell_when_unspecified() -> anyhow::Result<()> { let json = r#"{"cmd": "echo hello"}"#; @@ -202,7 +210,11 @@ async fn exec_command_pre_tool_use_payload_uses_raw_command() { arguments: serde_json::json!({ "cmd": "printf exec command" }).to_string(), }; let (session, turn) = make_session_and_context().await; - let handler = UnifiedExecHandler; + let handler = UnifiedExecHandler::new( + turn.environment + .attached_executor() + .expect("test turn context should have an attached executor"), + ); assert_eq!( handler.pre_tool_use_payload(&ToolInvocation { @@ -226,7 +238,11 @@ async fn exec_command_pre_tool_use_payload_skips_write_stdin() { arguments: serde_json::json!({ "chars": "echo hi" }).to_string(), }; let (session, turn) = make_session_and_context().await; - let handler = UnifiedExecHandler; + let handler = UnifiedExecHandler::new( + turn.environment + .attached_executor() + .expect("test turn context should have an attached executor"), + ); assert_eq!( handler.pre_tool_use_payload(&ToolInvocation { @@ -264,7 +280,7 @@ fn exec_command_post_tool_use_payload_uses_output_for_noninteractive_one_shot_co }; assert_eq!( - UnifiedExecHandler.post_tool_use_payload("call-43", &payload, &output), + local_exec_handler().post_tool_use_payload("call-43", &payload, &output), Some(crate::tools::registry::PostToolUsePayload { command: "echo three".to_string(), tool_response: serde_json::json!("three"), @@ -294,7 +310,7 @@ fn exec_command_post_tool_use_payload_skips_interactive_exec() { }; assert_eq!( - UnifiedExecHandler.post_tool_use_payload("call-44", &payload, &output), + local_exec_handler().post_tool_use_payload("call-44", &payload, &output), None ); } @@ -321,7 +337,7 @@ fn exec_command_post_tool_use_payload_skips_running_sessions() { }; assert_eq!( - UnifiedExecHandler.post_tool_use_payload("call-45", &payload, &output), + local_exec_handler().post_tool_use_payload("call-45", &payload, &output), None ); } diff --git a/codex-rs/core/src/tools/handlers/view_image.rs b/codex-rs/core/src/tools/handlers/view_image.rs index f4015a7624..28dccd3c9c 100644 --- a/codex-rs/core/src/tools/handlers/view_image.rs +++ b/codex-rs/core/src/tools/handlers/view_image.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use codex_exec_server::AttachedExecutor; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; @@ -20,8 +21,17 @@ use crate::tools::context::ToolPayload; use crate::tools::handlers::parse_arguments; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; +use std::sync::Arc; -pub struct ViewImageHandler; +pub struct ViewImageHandler { + attached_executor: Arc, +} + +impl ViewImageHandler { + pub fn new(attached_executor: Arc) -> Self { + Self { attached_executor } + } +} const VIEW_IMAGE_UNSUPPORTED_MESSAGE: &str = "view_image is not allowed because you do not support image inputs"; @@ -94,8 +104,8 @@ impl ToolHandler for ViewImageHandler { FunctionCallError::RespondToModel(format!("unable to resolve image path: {error}")) })?; - let metadata = turn - .environment + let metadata = self + .attached_executor .get_filesystem() .get_metadata(&abs_path) .await @@ -112,8 +122,8 @@ impl ToolHandler for ViewImageHandler { abs_path.display() ))); } - let file_bytes = turn - .environment + let file_bytes = self + .attached_executor .get_filesystem() .read_file(&abs_path) .await diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 9211fb08a7..383a72f69b 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -37,6 +37,7 @@ use crate::unified_exec::NoopSpawnLifecycle; use crate::unified_exec::UnifiedExecError; use crate::unified_exec::UnifiedExecProcess; use crate::unified_exec::UnifiedExecProcessManager; +use codex_exec_server::AttachedExecutor; use codex_network_proxy::NetworkProxy; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::ReviewDecision; @@ -44,6 +45,7 @@ use codex_sandboxing::SandboxablePreference; use futures::future::BoxFuture; use std::collections::HashMap; use std::path::PathBuf; +use std::sync::Arc; /// Request payload used by the unified-exec runtime after approvals and /// sandbox preferences have been resolved for the current turn. @@ -79,14 +81,20 @@ pub struct UnifiedExecApprovalKey { /// unified-exec side while delegating process startup to the manager. pub struct UnifiedExecRuntime<'a> { manager: &'a UnifiedExecProcessManager, + attached_executor: Arc, shell_mode: UnifiedExecShellMode, } impl<'a> UnifiedExecRuntime<'a> { /// Creates a runtime bound to the shared unified-exec process manager. - pub fn new(manager: &'a UnifiedExecProcessManager, shell_mode: UnifiedExecShellMode) -> Self { + pub fn new( + manager: &'a UnifiedExecProcessManager, + attached_executor: Arc, + shell_mode: UnifiedExecShellMode, + ) -> Self { Self { manager, + attached_executor, shell_mode, } } @@ -239,7 +247,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt .await? { Some(prepared) => { - if ctx.turn.environment.exec_server_url().is_some() { + if self.attached_executor.exec_server_url().is_some() { return Err(ToolError::Rejected( "unified_exec zsh-fork is not supported when exec_server_url is configured".to_string(), )); @@ -251,7 +259,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt &prepared.exec_request, req.tty, prepared.spawn_lifecycle, - ctx.turn.environment.as_ref(), + self.attached_executor.as_ref(), ) .await .map_err(|err| match err { @@ -287,7 +295,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt &exec_env, req.tty, Box::new(NoopSpawnLifecycle), - ctx.turn.environment.as_ref(), + self.attached_executor.as_ref(), ) .await .map_err(|err| match err { diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index f9ccc5d98f..b35ecc7417 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -21,6 +21,7 @@ use crate::tools::handlers::request_permissions_tool_description; use crate::tools::handlers::request_user_input_tool_description; use crate::tools::registry::ToolRegistryBuilder; use crate::tools::registry::tool_handler_key; +use codex_exec_server::AttachedExecutor; use codex_features::Feature; use codex_features::Features; use codex_protocol::config_types::WebSearchConfig; @@ -86,6 +87,7 @@ use serde::Serialize; use std::collections::BTreeMap; use std::collections::HashMap; use std::path::PathBuf; +use std::sync::Arc; pub type JsonSchema = codex_tools::JsonSchema; @@ -171,7 +173,7 @@ pub(crate) struct ToolsConfig { pub experimental_supported_tools: Vec, pub agent_jobs_tools: bool, pub agent_jobs_worker_tools: bool, - pub has_attached_executor: bool, + pub attached_executor: Option>, } pub(crate) struct ToolsConfigParams<'a> { @@ -306,12 +308,15 @@ impl ToolsConfig { experimental_supported_tools: model_info.experimental_supported_tools.clone(), agent_jobs_tools: include_agent_jobs, agent_jobs_worker_tools, - has_attached_executor: true, + attached_executor: codex_exec_server::Environment::default().attached_executor(), } } - pub fn with_has_attached_executor(mut self, has_attached_executor: bool) -> Self { - self.has_attached_executor = has_attached_executor; + pub fn with_attached_executor( + mut self, + attached_executor: Option>, + ) -> Self { + self.attached_executor = attached_executor; self } @@ -443,16 +448,12 @@ pub(crate) fn build_specs_with_discoverable_tools( use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2; use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2; use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2; - use std::sync::Arc; - let mut builder = ToolRegistryBuilder::new(); let shell_handler = Arc::new(ShellHandler); - let unified_exec_handler = Arc::new(UnifiedExecHandler); let plan_handler = Arc::new(PlanHandler); let apply_patch_handler = Arc::new(ApplyPatchHandler); let dynamic_tool_handler = Arc::new(DynamicToolHandler); - let view_image_handler = Arc::new(ViewImageHandler); let mcp_handler = Arc::new(McpHandler); let mcp_resource_handler = Arc::new(McpResourceHandler); let shell_command_handler = Arc::new(ShellCommandHandler::from(config.shell_command_backend)); @@ -500,7 +501,8 @@ pub(crate) fn build_specs_with_discoverable_tools( builder.register_handler(WAIT_TOOL_NAME, code_mode_wait_handler); } - if config.has_attached_executor { + if let Some(attached_executor) = config.attached_executor.as_ref() { + let unified_exec_handler = Arc::new(UnifiedExecHandler::new(Arc::clone(attached_executor))); match &config.shell_type { ConfigShellToolType::Default => { push_tool_spec( @@ -596,7 +598,7 @@ pub(crate) fn build_specs_with_discoverable_tools( ); builder.register_handler("update_plan", plan_handler); - if config.has_attached_executor && config.js_repl_enabled { + if config.attached_executor.is_some() && config.js_repl_enabled { push_tool_spec( &mut builder, create_js_repl_tool(), @@ -670,7 +672,7 @@ pub(crate) fn build_specs_with_discoverable_tools( builder.register_handler(TOOL_SUGGEST_TOOL_NAME, tool_suggest_handler); } - if config.has_attached_executor + if config.attached_executor.is_some() && let Some(apply_patch_tool_type) = &config.apply_patch_tool_type { match apply_patch_tool_type { @@ -694,7 +696,7 @@ pub(crate) fn build_specs_with_discoverable_tools( builder.register_handler("apply_patch", apply_patch_handler); } - if config.has_attached_executor + if config.attached_executor.is_some() && config .experimental_supported_tools .iter() @@ -775,7 +777,8 @@ pub(crate) fn build_specs_with_discoverable_tools( ); } - if config.has_attached_executor { + if let Some(attached_executor) = config.attached_executor.as_ref() { + let view_image_handler = Arc::new(ViewImageHandler::new(Arc::clone(attached_executor))); push_tool_spec( &mut builder, create_view_image_tool(ViewImageToolOptions { diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index eb450c5e1c..e2d45aba7a 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -1003,7 +1003,7 @@ fn no_attached_executor_hides_executor_backed_tools() { sandbox_policy: &SandboxPolicy::DangerFullAccess, windows_sandbox_level: WindowsSandboxLevel::Disabled, }) - .with_has_attached_executor(false); + .with_attached_executor(/*attached_executor*/ None); tools_config .experimental_supported_tools .push("list_dir".to_string()); diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index eb25f4ddc8..18a7fa4901 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -28,6 +28,7 @@ use std::path::PathBuf; use std::sync::Arc; use std::sync::Weak; +use codex_exec_server::AttachedExecutor; use codex_network_proxy::NetworkProxy; use codex_protocol::models::PermissionProfile; use rand::Rng; @@ -73,14 +74,21 @@ pub(crate) struct UnifiedExecContext { pub session: Arc, pub turn: Arc, pub call_id: String, + pub attached_executor: Arc, } impl UnifiedExecContext { - pub fn new(session: Arc, turn: Arc, call_id: String) -> Self { + pub fn new( + session: Arc, + turn: Arc, + call_id: String, + attached_executor: Arc, + ) -> Self { Self { session, turn, call_id, + attached_executor, } } } diff --git a/codex-rs/core/src/unified_exec/mod_tests.rs b/codex-rs/core/src/unified_exec/mod_tests.rs index 8348614f95..37dfafb2b9 100644 --- a/codex-rs/core/src/unified_exec/mod_tests.rs +++ b/codex-rs/core/src/unified_exec/mod_tests.rs @@ -26,6 +26,12 @@ async fn test_session_and_turn() -> (Arc, Arc) { (Arc::new(session), Arc::new(turn)) } +fn attached_executor(turn: &TurnContext) -> Arc { + turn.environment + .attached_executor() + .expect("test turn context should have an attached executor") +} + async fn exec_command( session: &Arc, turn: &Arc, @@ -91,6 +97,7 @@ async fn exec_command_with_tty( let command = vec!["bash".to_string(), "-lc".to_string(), cmd.to_string()]; let request = test_exec_request(turn, command.clone(), cwd.clone(), shell_env()); + let executor = attached_executor(turn.as_ref()); let process = Arc::new( manager .open_session_with_exec_env( @@ -98,12 +105,16 @@ async fn exec_command_with_tty( &request, tty, Box::new(NoopSpawnLifecycle), - turn.environment.as_ref(), + executor.as_ref(), ) .await?, ); - let context = - UnifiedExecContext::new(Arc::clone(session), Arc::clone(turn), "call".to_string()); + let context = UnifiedExecContext::new( + Arc::clone(session), + Arc::clone(turn), + "call".to_string(), + executor, + ); let started_at = Instant::now(); let process_started_alive = !process.has_exited() && process.exit_code().is_none(); if process_started_alive { @@ -507,13 +518,16 @@ async fn completed_pipe_commands_preserve_exit_code() -> anyhow::Result<()> { ); let environment = codex_exec_server::Environment::default(); + let attached_executor = environment + .attached_executor() + .expect("default environment should have an attached executor"); let process = UnifiedExecProcessManager::default() .open_session_with_exec_env( /*process_id*/ 1234, &request, /*tty*/ false, Box::new(NoopSpawnLifecycle), - &environment, + attached_executor.as_ref(), ) .await?; @@ -540,6 +554,10 @@ async fn unified_exec_uses_remote_exec_server_when_configured() -> anyhow::Resul }; let remote_test_env = remote_test_env().await?; + let attached_executor = remote_test_env + .environment() + .attached_executor() + .expect("remote test environment should have an attached executor"); let (_, turn) = make_session_and_context().await; let request = test_exec_request( &turn, @@ -555,7 +573,7 @@ async fn unified_exec_uses_remote_exec_server_when_configured() -> anyhow::Resul &request, /*tty*/ true, Box::new(NoopSpawnLifecycle), - remote_test_env.environment(), + attached_executor.as_ref(), ) .await?; @@ -603,6 +621,7 @@ async fn remote_exec_server_rejects_inherited_fd_launches() -> anyhow::Result<() ); let manager = UnifiedExecProcessManager::default(); + let attached_executor = attached_executor(&turn); let err = manager .open_session_with_exec_env( /*process_id*/ 1234, @@ -611,7 +630,7 @@ async fn remote_exec_server_rejects_inherited_fd_launches() -> anyhow::Result<() Box::new(TestSpawnLifecycle { inherited_fds: vec![42], }), - turn.environment.as_ref(), + attached_executor.as_ref(), ) .await .expect_err("expected inherited fd rejection"); diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 2d992507bd..3b568b76c3 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -585,7 +585,7 @@ impl UnifiedExecProcessManager { env: &ExecRequest, tty: bool, mut spawn_lifecycle: SpawnLifecycleHandle, - environment: &codex_exec_server::Environment, + attached_executor: &codex_exec_server::AttachedExecutor, ) -> Result { let (program, args) = env .command @@ -593,14 +593,14 @@ impl UnifiedExecProcessManager { .ok_or(UnifiedExecError::MissingCommandLine)?; let inherited_fds = spawn_lifecycle.inherited_fds(); - if environment.exec_server_url().is_some() { + if attached_executor.exec_server_url().is_some() { if !inherited_fds.is_empty() { return Err(UnifiedExecError::create_process( "remote exec-server does not support inherited file descriptors".to_string(), )); } - let started = environment + let started = attached_executor .get_exec_backend() .start(codex_exec_server::ExecParams { process_id: exec_server_process_id(process_id).into(), @@ -656,6 +656,7 @@ impl UnifiedExecProcessManager { let mut orchestrator = ToolOrchestrator::new(); let mut runtime = UnifiedExecRuntime::new( self, + Arc::clone(&context.attached_executor), context.turn.tools_config.unified_exec_shell_mode.clone(), ); let exec_approval_requirement = context diff --git a/codex-rs/exec-server/src/environment.rs b/codex-rs/exec-server/src/environment.rs index 4882e9d487..f2f2801163 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use async_trait::async_trait; use tokio::sync::OnceCell; use crate::ExecServerClient; @@ -10,8 +9,6 @@ use crate::file_system::ExecutorFileSystem; use crate::local_file_system::LocalFileSystem; use crate::local_process::LocalProcess; use crate::process::ExecBackend; -use crate::process::StartedExecProcess; -use crate::protocol::ExecParams; use crate::remote_file_system::RemoteFileSystem; use crate::remote_process::RemoteProcess; @@ -21,6 +18,57 @@ pub trait ExecutorEnvironment: Send + Sync { fn get_exec_backend(&self) -> Arc; } +#[derive(Clone)] +pub struct AttachedExecutor { + exec_server_url: Option, + exec_backend: Arc, + file_system: Arc, +} + +impl std::fmt::Debug for AttachedExecutor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AttachedExecutor") + .field("exec_server_url", &self.exec_server_url) + .finish_non_exhaustive() + } +} + +impl AttachedExecutor { + fn new_local(exec_backend: Arc) -> Self { + Self { + exec_server_url: None, + exec_backend, + file_system: Arc::new(LocalFileSystem), + } + } + + fn new_remote(exec_server_url: String, client: ExecServerClient) -> Self { + Self { + exec_server_url: Some(exec_server_url), + exec_backend: Arc::new(RemoteProcess::new(client.clone())), + file_system: Arc::new(RemoteFileSystem::new(client)), + } + } + + pub fn exec_server_url(&self) -> Option<&str> { + self.exec_server_url.as_deref() + } + + pub fn get_exec_backend(&self) -> Arc { + Arc::clone(&self.exec_backend) + } + + pub fn get_filesystem(&self) -> Arc { + Arc::clone(&self.file_system) + } +} + +impl ExecutorEnvironment for AttachedExecutor { + fn get_exec_backend(&self) -> Arc { + self.get_exec_backend() + } +} + #[derive(Debug, Default)] pub struct EnvironmentManager { executor_mode: ExecutorMode, @@ -88,8 +136,7 @@ impl ExecutorMode { #[derive(Clone)] pub struct Environment { executor_mode: ExecutorMode, - remote_exec_server_client: Option, - exec_backend: Arc, + attached_executor: Option>, } impl Default for Environment { @@ -104,8 +151,9 @@ impl Default for Environment { Self { executor_mode: ExecutorMode::LocalExecutor, - remote_exec_server_client: None, - exec_backend: Arc::new(local_process), + attached_executor: Some(Arc::new(AttachedExecutor::new_local(Arc::new( + local_process, + )))), } } } @@ -124,40 +172,36 @@ impl Environment { } async fn create_with_mode(executor_mode: ExecutorMode) -> Result { - let remote_exec_server_client = if let Some(url) = executor_mode.remote_exec_server_url() { - Some( - ExecServerClient::connect_websocket(RemoteExecServerConnectArgs { - websocket_url: url.to_string(), - client_name: "codex-environment".to_string(), - connect_timeout: std::time::Duration::from_secs(5), - initialize_timeout: std::time::Duration::from_secs(5), - }) - .await?, - ) - } else { + let attached_executor = if let Some(url) = executor_mode.remote_exec_server_url() { + let client = ExecServerClient::connect_websocket(RemoteExecServerConnectArgs { + websocket_url: url.to_string(), + client_name: "codex-environment".to_string(), + connect_timeout: std::time::Duration::from_secs(5), + initialize_timeout: std::time::Duration::from_secs(5), + }) + .await?; + Some(Arc::new(AttachedExecutor::new_remote( + url.to_string(), + client, + ))) + } else if matches!(executor_mode, ExecutorMode::NoExecutor) { None + } else { + let local_process = LocalProcess::default(); + local_process + .initialize() + .map_err(|err| ExecServerError::Protocol(err.message))?; + local_process + .initialized() + .map_err(ExecServerError::Protocol)?; + Some(Arc::new(AttachedExecutor::new_local(Arc::new( + local_process, + )))) }; - let exec_backend: Arc = - if let Some(client) = remote_exec_server_client.clone() { - Arc::new(RemoteProcess::new(client)) - } else if matches!(executor_mode, ExecutorMode::NoExecutor) { - Arc::new(NoAttachedExecutorBackend) - } else { - let local_process = LocalProcess::default(); - local_process - .initialize() - .map_err(|err| ExecServerError::Protocol(err.message))?; - local_process - .initialized() - .map_err(ExecServerError::Protocol)?; - Arc::new(local_process) - }; - Ok(Self { executor_mode, - remote_exec_server_client, - exec_backend, + attached_executor, }) } @@ -169,28 +213,22 @@ impl Environment { self.executor_mode.has_attached_executor() } + pub fn attached_executor(&self) -> Option> { + self.attached_executor.clone() + } + pub fn get_exec_backend(&self) -> Arc { - Arc::clone(&self.exec_backend) + self.attached_executor + .as_ref() + .expect("environment does not have an attached executor") + .get_exec_backend() } pub fn get_filesystem(&self) -> Arc { - if let Some(client) = self.remote_exec_server_client.clone() { - Arc::new(RemoteFileSystem::new(client)) - } else { - Arc::new(LocalFileSystem) - } - } -} - -#[derive(Clone, Default)] -struct NoAttachedExecutorBackend; - -#[async_trait] -impl ExecBackend for NoAttachedExecutorBackend { - async fn start(&self, _params: ExecParams) -> Result { - Err(ExecServerError::Protocol( - "no attached executor is configured for this session".to_string(), - )) + self.attached_executor + .as_ref() + .expect("environment does not have an attached executor") + .get_filesystem() } } @@ -206,7 +244,7 @@ fn parse_executor_mode(exec_server_url: Option) -> ExecutorMode { impl ExecutorEnvironment for Environment { fn get_exec_backend(&self) -> Arc { - Arc::clone(&self.exec_backend) + self.get_exec_backend() } } @@ -216,6 +254,8 @@ mod tests { use super::Environment; use super::EnvironmentManager; + use super::ExecutorMode; + use super::parse_executor_mode; use crate::ProcessId; use pretty_assertions::assert_eq; @@ -228,7 +268,7 @@ mod tests { assert_eq!(environment.exec_server_url(), None); assert!(environment.has_attached_executor()); assert_eq!(environment.executor_mode, ExecutorMode::LocalExecutor); - assert!(environment.remote_exec_server_client.is_none()); + assert!(environment.attached_executor().is_some()); } #[test] @@ -307,31 +347,15 @@ mod tests { assert_eq!(environment.exec_server_url(), None); assert!(!environment.has_attached_executor()); assert_eq!(environment.executor_mode, ExecutorMode::NoExecutor); - assert!(environment.remote_exec_server_client.is_none()); + assert!(environment.attached_executor().is_none()); } #[tokio::test] - async fn no_executor_environment_rejects_exec_start() { + async fn no_executor_environment_has_no_executor_capability() { let environment = Environment::create(Some("none".to_string())) .await .expect("create environment"); - let err = environment - .get_exec_backend() - .start(crate::ExecParams { - process_id: ProcessId::from("no-executor-proc"), - argv: vec!["true".to_string()], - cwd: std::env::current_dir().expect("read current dir"), - env: Default::default(), - tty: false, - arg0: None, - }) - .await - .expect_err("no-executor backend should reject starts"); - - assert_eq!( - err.to_string(), - "exec-server protocol error: no attached executor is configured for this session" - ); + assert!(environment.attached_executor().is_none()); } } diff --git a/codex-rs/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index 77834c6143..8eedbffe7e 100644 --- a/codex-rs/exec-server/src/lib.rs +++ b/codex-rs/exec-server/src/lib.rs @@ -31,6 +31,7 @@ pub use codex_app_server_protocol::FsRemoveParams; pub use codex_app_server_protocol::FsRemoveResponse; pub use codex_app_server_protocol::FsWriteFileParams; pub use codex_app_server_protocol::FsWriteFileResponse; +pub use environment::AttachedExecutor; pub use environment::CODEX_EXEC_SERVER_URL_ENV_VAR; pub use environment::Environment; pub use environment::EnvironmentManager;