diff --git a/crates/arc-agent/README.md b/crates/arc-agent/README.md index 36a04a0da..a26bb5434 100644 --- a/crates/arc-agent/README.md +++ b/crates/arc-agent/README.md @@ -9,7 +9,7 @@ The crate is organized around a central `Session` that drives an agentic loop: 1. **User input** is appended to a conversation `History` 2. The session builds a `Request` with system prompt, history, and tools 3. An LLM generates a response (text and/or tool calls) via `unified-llm` -4. Tool calls are executed through a `ToolRegistry` against an `ExecutionEnvironment` +4. Tool calls are executed through a `ToolRegistry` against an `Sandbox` 5. Results are recorded and the loop continues until the LLM responds with text only (natural completion), a turn limit is reached, or the session is aborted ``` @@ -41,7 +41,7 @@ User Input - **`Session`** -- Manages the full agentic loop: LLM calls, tool execution, steering, follow-ups, abort handling, and event emission. - **`ProviderProfile`** (trait) -- Defines how to build system prompts, which tools to register, and what capabilities a provider supports. Ships with `AnthropicProfile`, `OpenAiProfile`, and `GeminiProfile`. -- **`ExecutionEnvironment`** (trait) -- Abstracts filesystem, shell, grep, and glob operations. `LocalExecutionEnvironment` provides a real implementation; the trait enables sandboxing and testing. +- **`Sandbox`** (trait) -- Abstracts filesystem, shell, grep, and glob operations. `LocalSandbox` provides a real implementation; the trait enables sandboxing and testing. - **`ToolRegistry`** -- Maps tool names to definitions and async executor functions. Tools are registered per-profile. - **`History`** -- Ordered list of `Turn` variants (`User`, `Assistant`, `ToolResults`, `System`, `Steering`) that converts to LLM messages. - **`EventEmitter`** -- Broadcasts `SessionEvent`s (tool calls, text, errors, warnings) over a `tokio::sync::broadcast` channel for UI or logging. @@ -52,7 +52,7 @@ User Input ### `Session` -The main entry point. Created with an LLM client, a provider profile, an execution environment, and a config. +The main entry point. Created with an LLM client, a provider profile, a sandbox, and a config. ### `ProviderProfile` @@ -63,7 +63,7 @@ pub trait ProviderProfile: Send + Sync { fn tool_registry(&self) -> &ToolRegistry; fn build_system_prompt( &self, - env: &dyn ExecutionEnvironment, + env: &dyn Sandbox, env_context: &EnvContext, project_docs: &[String], user_instructions: Option<&str>, @@ -79,10 +79,10 @@ Built-in profiles: - **`OpenAiProfile`** -- 128K context, reasoning effort support, tools: `read_file`, `write_file`, `shell`, `grep`, `glob`, `apply_patch` (v4a format) - **`GeminiProfile`** -- 1M context, safety settings, tools: all Anthropic tools plus `read_many_files`, `list_dir`, `web_search`, `web_fetch` -### `ExecutionEnvironment` +### `Sandbox` ```rust -pub trait ExecutionEnvironment: Send + Sync { +pub trait Sandbox: Send + Sync { async fn read_file(&self, path: &str, offset: Option, limit: Option) -> Result; async fn write_file(&self, path: &str, content: &str) -> Result<(), String>; async fn exec_command(&self, command: &str, timeout_ms: u64, ...) -> Result; @@ -92,7 +92,7 @@ pub trait ExecutionEnvironment: Send + Sync { } ``` -`LocalExecutionEnvironment` is the real implementation with env-var filtering (strips secrets), process group management, and ripgrep/grep fallback. +`LocalSandbox` is the real implementation with env-var filtering (strips secrets), process group management, and ripgrep/grep fallback. ### `SessionConfig` @@ -115,7 +115,7 @@ pub struct SessionConfig { ```rust use agent::{ - AnthropicProfile, LocalExecutionEnvironment, Session, SessionConfig, + AnthropicProfile, LocalSandbox, Session, SessionConfig, }; use std::path::PathBuf; use std::sync::Arc; @@ -127,8 +127,8 @@ let client: Client = /* configure unified-llm client */; // 2. Choose a provider profile let profile = Arc::new(AnthropicProfile::new("claude-sonnet-4-20250514")); -// 3. Create an execution environment -let env = Arc::new(LocalExecutionEnvironment::new( +// 3. Create a sandbox +let env = Arc::new(LocalSandbox::new( PathBuf::from("/path/to/project"), )); @@ -232,6 +232,6 @@ profile.register_subagent_tools(manager, factory, 0); - **Context window monitoring** -- Emits `ContextWindowWarning` events when estimated usage exceeds 80% - **Tool argument validation** -- Validates arguments against JSON Schema before execution - **Tool output truncation** -- Per-tool character and line limits with head/tail or tail-only truncation modes -- **Environment variable filtering** -- `LocalExecutionEnvironment` strips secrets (`*_API_KEY`, `*_SECRET`, `*_TOKEN`, `*_PASSWORD`, `*_CREDENTIAL`) from subprocess environments +- **Environment variable filtering** -- `LocalSandbox` strips secrets (`*_API_KEY`, `*_SECRET`, `*_TOKEN`, `*_PASSWORD`, `*_CREDENTIAL`) from subprocess environments - **Command timeouts** -- Configurable per-command with process group cleanup (SIGTERM then SIGKILL) - **Project doc discovery** -- Automatically discovers `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, or `.codex/instructions.md` based on provider, with a 32KB budget diff --git a/crates/arc-agent/src/cli.rs b/crates/arc-agent/src/cli.rs index 952ea37d0..25536d099 100644 --- a/crates/arc-agent/src/cli.rs +++ b/crates/arc-agent/src/cli.rs @@ -1,6 +1,6 @@ use crate::{ subagent::{SessionFactory, SubAgentManager}, - AgentEvent, AnthropicProfile, GeminiProfile, LocalExecutionEnvironment, OpenAiProfile, + AgentEvent, AnthropicProfile, GeminiProfile, LocalSandbox, OpenAiProfile, ProviderProfile, Session, SessionConfig, ToolApprovalFn, Turn, }; use arc_llm::client::Client; @@ -385,7 +385,7 @@ pub async fn run_with_args(args: AgentArgs) -> anyhow::Result<()> { // Build execution environment let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); let cwd_str = cwd.to_string_lossy().to_string(); - let env: Arc = Arc::new(LocalExecutionEnvironment::new(cwd)); + let env: Arc = Arc::new(LocalSandbox::new(cwd)); // Build tool approval callback let is_interactive = std::io::stdin().is_terminal() && !args.auto_approve; diff --git a/crates/arc-agent/src/docker_sandbox.rs b/crates/arc-agent/src/docker_sandbox.rs index b09c1830c..6db2285de 100644 --- a/crates/arc-agent/src/docker_sandbox.rs +++ b/crates/arc-agent/src/docker_sandbox.rs @@ -1,6 +1,6 @@ -use crate::execution_env::{ - format_lines_numbered, DirEntry, ExecEnvEventCallback, ExecResult, ExecutionEnvEvent, - ExecutionEnvironment, GrepOptions, +use crate::sandbox::{ + format_lines_numbered, DirEntry, SandboxEventCallback, ExecResult, SandboxEvent, + Sandbox, GrepOptions, }; use async_trait::async_trait; use bollard::container::{ @@ -16,7 +16,7 @@ use std::time::Instant; use tokio_util::sync::CancellationToken; /// Configuration for a Docker-based execution environment. -pub struct DockerConfig { +pub struct DockerSandboxConfig { /// Docker image to use. Default: `"arc-agent:latest"`. pub image: String, /// Host directory to bind-mount into the container. @@ -37,7 +37,7 @@ pub struct DockerConfig { pub env_vars: Vec, } -impl Default for DockerConfig { +impl Default for DockerSandboxConfig { fn default() -> Self { Self { image: "arc-agent:latest".to_string(), @@ -57,22 +57,22 @@ impl Default for DockerConfig { /// /// The host working directory is bind-mounted at `container_mount_point`. All file /// operations, commands, grep, and glob execute inside the container via `docker exec`. -pub struct DockerExecutionEnvironment { +pub struct DockerSandbox { docker: Docker, - config: DockerConfig, + config: DockerSandboxConfig, container_id: tokio::sync::OnceCell, cached_platform: std::sync::OnceLock, cached_os_version: std::sync::OnceLock, rg_available: tokio::sync::OnceCell, - event_callback: Option, + event_callback: Option, } -impl DockerExecutionEnvironment { - /// Creates a new `DockerExecutionEnvironment`. +impl DockerSandbox { + /// Creates a new `DockerSandbox`. /// /// Validates Docker daemon connectivity but does NOT create a container. /// Call `initialize()` to create and start the container. - pub fn new(config: DockerConfig) -> Result { + pub fn new(config: DockerSandboxConfig) -> Result { let docker = Docker::connect_with_local_defaults() .map_err(|e| format!("Failed to connect to Docker daemon: {e}"))?; Ok(Self { @@ -86,11 +86,11 @@ impl DockerExecutionEnvironment { }) } - pub fn set_event_callback(&mut self, cb: ExecEnvEventCallback) { + pub fn set_event_callback(&mut self, cb: SandboxEventCallback) { self.event_callback = Some(cb); } - fn emit(&self, event: ExecutionEnvEvent) { + fn emit(&self, event: SandboxEvent) { event.trace(); if let Some(ref cb) = self.event_callback { cb(event); @@ -269,29 +269,29 @@ impl DockerExecutionEnvironment { } #[async_trait] -impl ExecutionEnvironment for DockerExecutionEnvironment { +impl Sandbox for DockerSandbox { async fn initialize(&self) -> Result<(), String> { - self.emit(ExecutionEnvEvent::Initializing { - env_type: "docker".into(), + self.emit(SandboxEvent::Initializing { + provider: "docker".into(), }); let init_start = Instant::now(); - self.emit(ExecutionEnvEvent::ImagePulling { - image: self.config.image.clone(), + self.emit(SandboxEvent::SnapshotPulling { + name: self.config.image.clone(), }); let pull_start = Instant::now(); if let Err(e) = self.ensure_image().await { let duration_ms = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(ExecutionEnvEvent::InitializeFailed { - env_type: "docker".into(), + self.emit(SandboxEvent::InitializeFailed { + provider: "docker".into(), error: e.clone(), duration_ms, }); return Err(e); } let pull_duration = u64::try_from(pull_start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(ExecutionEnvEvent::ImagePulled { - image: self.config.image.clone(), + self.emit(SandboxEvent::SnapshotPulled { + name: self.config.image.clone(), duration_ms: pull_duration, }); @@ -361,8 +361,8 @@ impl ExecutionEnvironment for DockerExecutionEnvironment { .set(format!("linux {}", uname_output.trim())); let init_duration = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(ExecutionEnvEvent::Ready { - env_type: "docker".into(), + self.emit(SandboxEvent::Ready { + provider: "docker".into(), duration_ms: init_duration, }); @@ -370,8 +370,8 @@ impl ExecutionEnvironment for DockerExecutionEnvironment { } async fn cleanup(&self) -> Result<(), String> { - self.emit(ExecutionEnvEvent::CleanupStarted { - env_type: "docker".into(), + self.emit(SandboxEvent::CleanupStarted { + provider: "docker".into(), }); let start = Instant::now(); @@ -379,8 +379,8 @@ impl ExecutionEnvironment for DockerExecutionEnvironment { Some(id) => id.clone(), None => { let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(ExecutionEnvEvent::CleanupCompleted { - env_type: "docker".into(), + self.emit(SandboxEvent::CleanupCompleted { + provider: "docker".into(), duration_ms, }); return Ok(()); @@ -405,8 +405,8 @@ impl ExecutionEnvironment for DockerExecutionEnvironment { .await; let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(ExecutionEnvEvent::CleanupCompleted { - env_type: "docker".into(), + self.emit(SandboxEvent::CleanupCompleted { + provider: "docker".into(), duration_ms, }); @@ -720,8 +720,8 @@ mod tests { Docker::connect_with_local_defaults().expect("Docker not available — skipping") } - fn test_config(host_dir: &str) -> DockerConfig { - DockerConfig { + fn test_config(host_dir: &str) -> DockerSandboxConfig { + DockerSandboxConfig { host_working_directory: host_dir.to_string(), auto_pull: false, ..Default::default() @@ -737,8 +737,8 @@ mod tests { std::fs::create_dir_all(&host_dir).unwrap(); let config = test_config(host_dir.to_str().unwrap()); - let env: Arc = - Arc::new(DockerExecutionEnvironment::new(config).unwrap()); + let env: Arc = + Arc::new(DockerSandbox::new(config).unwrap()); // Initialize env.initialize().await.unwrap(); @@ -803,7 +803,7 @@ mod tests { std::fs::create_dir_all(&host_dir).unwrap(); let config = test_config(host_dir.to_str().unwrap()); - let env = DockerExecutionEnvironment::new(config).unwrap(); + let env = DockerSandbox::new(config).unwrap(); env.initialize().await.unwrap(); let result = env @@ -826,7 +826,7 @@ mod tests { std::fs::create_dir_all(&host_dir).unwrap(); let config = test_config(host_dir.to_str().unwrap()); - let env = DockerExecutionEnvironment::new(config).unwrap(); + let env = DockerSandbox::new(config).unwrap(); env.initialize().await.unwrap(); let content = "hello \"world\"\nit's a `test`\nprice: $100\nbackslash: \\\nnewline above"; @@ -852,7 +852,7 @@ mod tests { std::fs::create_dir_all(&host_dir).unwrap(); let config = test_config(host_dir.to_str().unwrap()); - let env = DockerExecutionEnvironment::new(config).unwrap(); + let env = DockerSandbox::new(config).unwrap(); env.initialize().await.unwrap(); // Relative path resolves to container_mount_point @@ -879,7 +879,7 @@ mod tests { std::fs::create_dir_all(&host_dir).unwrap(); let config = test_config(host_dir.to_str().unwrap()); - let env = DockerExecutionEnvironment::new(config).unwrap(); + let env = DockerSandbox::new(config).unwrap(); env.initialize().await.unwrap(); // First cleanup diff --git a/crates/arc-agent/src/lib.rs b/crates/arc-agent/src/lib.rs index 94258ae07..b092dc249 100644 --- a/crates/arc-agent/src/lib.rs +++ b/crates/arc-agent/src/lib.rs @@ -1,21 +1,21 @@ #[cfg(feature = "docker")] -pub mod docker_env; +pub mod docker_sandbox; pub mod cli; pub mod compaction; pub mod config; pub mod error; pub mod event; -pub mod execution_env; pub mod file_tracker; pub mod history; -pub mod local_env; +pub mod local_sandbox; pub mod loop_detection; pub mod mcp_integration; pub mod profiles; pub mod project_docs; pub mod provider_profile; -pub mod read_before_write_env; +pub mod read_before_write_sandbox; +pub mod sandbox; pub mod session; pub mod skills; pub mod subagent; @@ -29,20 +29,20 @@ pub mod v4a_patch; pub use arc_mcp::config::McpServerConfig; pub use config::{SessionConfig, ToolApprovalFn}; #[cfg(feature = "docker")] -pub use docker_env::{DockerConfig, DockerExecutionEnvironment}; +pub use docker_sandbox::{DockerSandboxConfig, DockerSandbox}; pub use error::AgentError; pub use event::EventEmitter; -pub use execution_env::{ - format_lines_numbered, DirEntry, ExecEnvEventCallback, ExecResult, ExecutionEnvEvent, - ExecutionEnvironment, GrepOptions, +pub use sandbox::{ + format_lines_numbered, DirEntry, SandboxEventCallback, ExecResult, SandboxEvent, + Sandbox, GrepOptions, }; pub use history::History; -pub use local_env::LocalExecutionEnvironment; +pub use local_sandbox::LocalSandbox; pub use loop_detection::detect_loop; pub use profiles::{AnthropicProfile, EnvContext, GeminiProfile, OpenAiProfile}; pub use project_docs::discover_project_docs; pub use provider_profile::{ProfileCapabilities, ProviderProfile}; -pub use read_before_write_env::ReadBeforeWriteEnvironment; +pub use read_before_write_sandbox::ReadBeforeWriteSandbox; pub use session::Session; pub use skills::Skill; pub use subagent::{SubAgent, SubAgentEventCallback, SubAgentManager, SubAgentResult}; diff --git a/crates/arc-agent/src/local_sandbox.rs b/crates/arc-agent/src/local_sandbox.rs index df61c38ff..caa47dd4e 100644 --- a/crates/arc-agent/src/local_sandbox.rs +++ b/crates/arc-agent/src/local_sandbox.rs @@ -1,6 +1,6 @@ -use crate::execution_env::{ - format_lines_numbered, DirEntry, ExecEnvEventCallback, ExecResult, ExecutionEnvEvent, - ExecutionEnvironment, GrepOptions, +use crate::sandbox::{ + format_lines_numbered, DirEntry, SandboxEventCallback, ExecResult, SandboxEvent, + Sandbox, GrepOptions, }; use async_trait::async_trait; use std::path::{Path, PathBuf}; @@ -9,13 +9,13 @@ use tokio::io::AsyncReadExt; use tokio::process::Command; use tokio_util::sync::CancellationToken; -pub struct LocalExecutionEnvironment { +pub struct LocalSandbox { working_directory: PathBuf, - event_callback: Option, + event_callback: Option, rg_available: std::sync::OnceLock, } -impl LocalExecutionEnvironment { +impl LocalSandbox { #[must_use] pub fn new(working_directory: PathBuf) -> Self { Self { @@ -25,11 +25,11 @@ impl LocalExecutionEnvironment { } } - pub fn set_event_callback(&mut self, cb: ExecEnvEventCallback) { + pub fn set_event_callback(&mut self, cb: SandboxEventCallback) { self.event_callback = Some(cb); } - fn emit(&self, event: ExecutionEnvEvent) { + fn emit(&self, event: SandboxEvent) { event.trace(); if let Some(ref cb) = self.event_callback { cb(event); @@ -72,7 +72,7 @@ impl LocalExecutionEnvironment { } #[async_trait] -impl ExecutionEnvironment for LocalExecutionEnvironment { +impl Sandbox for LocalSandbox { async fn read_file( &self, path: &str, @@ -346,8 +346,8 @@ impl ExecutionEnvironment for LocalExecutionEnvironment { } async fn initialize(&self) -> Result<(), String> { - self.emit(ExecutionEnvEvent::Initializing { - env_type: "local".into(), + self.emit(SandboxEvent::Initializing { + provider: "local".into(), }); let start = Instant::now(); let result = tokio::fs::create_dir_all(&self.working_directory) @@ -355,12 +355,12 @@ impl ExecutionEnvironment for LocalExecutionEnvironment { .map_err(|e| format!("Failed to create working directory: {e}")); let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); match &result { - Ok(()) => self.emit(ExecutionEnvEvent::Ready { - env_type: "local".into(), + Ok(()) => self.emit(SandboxEvent::Ready { + provider: "local".into(), duration_ms, }), - Err(e) => self.emit(ExecutionEnvEvent::InitializeFailed { - env_type: "local".into(), + Err(e) => self.emit(SandboxEvent::InitializeFailed { + provider: "local".into(), error: e.clone(), duration_ms, }), @@ -369,13 +369,13 @@ impl ExecutionEnvironment for LocalExecutionEnvironment { } async fn cleanup(&self) -> Result<(), String> { - self.emit(ExecutionEnvEvent::CleanupStarted { - env_type: "local".into(), + self.emit(SandboxEvent::CleanupStarted { + provider: "local".into(), }); let start = Instant::now(); let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(ExecutionEnvEvent::CleanupCompleted { - env_type: "local".into(), + self.emit(SandboxEvent::CleanupCompleted { + provider: "local".into(), duration_ms, }); Ok(()) @@ -457,7 +457,7 @@ mod tests { let dir = temp_dir(); std::fs::write(dir.join("test.txt"), "hello\nworld\nfoo").unwrap(); - let env = LocalExecutionEnvironment::new(dir.clone()); + let env = LocalSandbox::new(dir.clone()); let result = env.read_file("test.txt", None, None).await.unwrap(); assert_eq!(result, "1 | hello\n2 | world\n3 | foo\n"); @@ -470,7 +470,7 @@ mod tests { let content: String = (1..=12).map(|i| format!("line {i}\n")).collect(); std::fs::write(dir.join("padded.txt"), content.trim_end()).unwrap(); - let env = LocalExecutionEnvironment::new(dir.clone()); + let env = LocalSandbox::new(dir.clone()); let result = env.read_file("padded.txt", None, None).await.unwrap(); assert!(result.starts_with(" 1 | line 1\n")); @@ -481,7 +481,7 @@ mod tests { #[tokio::test] async fn read_file_not_found() { let dir = temp_dir(); - let env = LocalExecutionEnvironment::new(dir.clone()); + let env = LocalSandbox::new(dir.clone()); let result = env.read_file("nonexistent.txt", None, None).await; assert!(result.is_err()); std::fs::remove_dir_all(&dir).unwrap(); @@ -490,7 +490,7 @@ mod tests { #[tokio::test] async fn write_file_creates_parent_dirs() { let dir = temp_dir(); - let env = LocalExecutionEnvironment::new(dir.clone()); + let env = LocalSandbox::new(dir.clone()); env.write_file("sub/dir/test.txt", "content").await.unwrap(); let written = std::fs::read_to_string(dir.join("sub/dir/test.txt")).unwrap(); @@ -503,7 +503,7 @@ mod tests { let dir = temp_dir(); std::fs::write(dir.join("exists.txt"), "data").unwrap(); - let env = LocalExecutionEnvironment::new(dir.clone()); + let env = LocalSandbox::new(dir.clone()); assert!(env.file_exists("exists.txt").await.unwrap()); std::fs::remove_dir_all(&dir).unwrap(); } @@ -511,7 +511,7 @@ mod tests { #[tokio::test] async fn file_exists_false() { let dir = temp_dir(); - let env = LocalExecutionEnvironment::new(dir.clone()); + let env = LocalSandbox::new(dir.clone()); assert!(!env.file_exists("nope.txt").await.unwrap()); std::fs::remove_dir_all(&dir).unwrap(); } @@ -523,7 +523,7 @@ mod tests { std::fs::write(dir.join("a.txt"), "a").unwrap(); std::fs::create_dir(dir.join("c_dir")).unwrap(); - let env = LocalExecutionEnvironment::new(dir.clone()); + let env = LocalSandbox::new(dir.clone()); let entries = env.list_directory(".", None).await.unwrap(); assert_eq!(entries.len(), 3); @@ -540,7 +540,7 @@ mod tests { #[tokio::test] async fn exec_command_echo() { let dir = temp_dir(); - let env = LocalExecutionEnvironment::new(dir.clone()); + let env = LocalSandbox::new(dir.clone()); let result = env .exec_command("echo hello", 5000, None, None, None) .await @@ -556,7 +556,7 @@ mod tests { #[tokio::test] async fn exec_command_exit_code() { let dir = temp_dir(); - let env = LocalExecutionEnvironment::new(dir.clone()); + let env = LocalSandbox::new(dir.clone()); let result = env .exec_command("exit 42", 5000, None, None, None) .await @@ -570,7 +570,7 @@ mod tests { #[tokio::test] async fn exec_command_timeout() { let dir = temp_dir(); - let env = LocalExecutionEnvironment::new(dir.clone()); + let env = LocalSandbox::new(dir.clone()); let result = env .exec_command("sleep 10", 200, None, None, None) .await @@ -584,7 +584,7 @@ mod tests { #[tokio::test] async fn exec_command_stderr() { let dir = temp_dir(); - let env = LocalExecutionEnvironment::new(dir.clone()); + let env = LocalSandbox::new(dir.clone()); let result = env .exec_command("echo err >&2", 5000, None, None, None) .await @@ -596,43 +596,43 @@ mod tests { #[test] fn env_var_filtering() { - assert!(LocalExecutionEnvironment::should_filter_env_var( + assert!(LocalSandbox::should_filter_env_var( "OPENAI_API_KEY" )); - assert!(LocalExecutionEnvironment::should_filter_env_var( + assert!(LocalSandbox::should_filter_env_var( "ANTHROPIC_API_KEY" )); - assert!(LocalExecutionEnvironment::should_filter_env_var( + assert!(LocalSandbox::should_filter_env_var( "DB_PASSWORD" )); - assert!(LocalExecutionEnvironment::should_filter_env_var( + assert!(LocalSandbox::should_filter_env_var( "AWS_SECRET" )); - assert!(LocalExecutionEnvironment::should_filter_env_var( + assert!(LocalSandbox::should_filter_env_var( "AUTH_TOKEN" )); - assert!(LocalExecutionEnvironment::should_filter_env_var( + assert!(LocalSandbox::should_filter_env_var( "MY_CREDENTIAL" )); // Case insensitive - assert!(LocalExecutionEnvironment::should_filter_env_var( + assert!(LocalSandbox::should_filter_env_var( "my_api_key" )); - assert!(LocalExecutionEnvironment::should_filter_env_var( + assert!(LocalSandbox::should_filter_env_var( "Some_Secret" )); // Should not filter - assert!(!LocalExecutionEnvironment::should_filter_env_var("PATH")); - assert!(!LocalExecutionEnvironment::should_filter_env_var("HOME")); - assert!(!LocalExecutionEnvironment::should_filter_env_var("EDITOR")); - assert!(!LocalExecutionEnvironment::should_filter_env_var( + assert!(!LocalSandbox::should_filter_env_var("PATH")); + assert!(!LocalSandbox::should_filter_env_var("HOME")); + assert!(!LocalSandbox::should_filter_env_var("EDITOR")); + assert!(!LocalSandbox::should_filter_env_var( "SECRET_PATH" )); } #[test] fn platform_is_known() { - let env = LocalExecutionEnvironment::new(PathBuf::from("/tmp")); + let env = LocalSandbox::new(PathBuf::from("/tmp")); let platform = env.platform(); assert!( platform == "darwin" || platform == "linux" || platform == "windows", @@ -642,7 +642,7 @@ mod tests { #[test] fn os_version_contains_platform() { - let env = LocalExecutionEnvironment::new(PathBuf::from("/tmp")); + let env = LocalSandbox::new(PathBuf::from("/tmp")); let version = env.os_version(); assert!( version.contains(env.platform()), @@ -652,14 +652,14 @@ mod tests { #[test] fn working_directory_accessor() { - let env = LocalExecutionEnvironment::new(PathBuf::from("/tmp/test_dir")); + let env = LocalSandbox::new(PathBuf::from("/tmp/test_dir")); assert_eq!(env.working_directory(), "/tmp/test_dir"); } #[tokio::test] async fn initialize_creates_directory() { let dir = std::env::temp_dir().join(format!("init_test_{}", uuid::Uuid::new_v4())); - let env = LocalExecutionEnvironment::new(dir.clone()); + let env = LocalSandbox::new(dir.clone()); env.initialize().await.unwrap(); assert!(dir.exists()); std::fs::remove_dir_all(&dir).unwrap(); @@ -667,14 +667,14 @@ mod tests { #[tokio::test] async fn initialize_emits_events() { - use crate::execution_env::ExecutionEnvEvent; + use crate::sandbox::SandboxEvent; use std::sync::{Arc, Mutex}; let dir = std::env::temp_dir().join(format!("init_event_test_{}", uuid::Uuid::new_v4())); - let events: Arc>> = Arc::new(Mutex::new(Vec::new())); + let events: Arc>> = Arc::new(Mutex::new(Vec::new())); let events_clone = Arc::clone(&events); - let mut env = LocalExecutionEnvironment::new(dir.clone()); + let mut env = LocalSandbox::new(dir.clone()); env.set_event_callback(Arc::new(move |e| { events_clone.lock().unwrap().push(e); })); @@ -684,10 +684,10 @@ mod tests { let captured = events.lock().unwrap(); assert_eq!(captured.len(), 2); assert!( - matches!(&captured[0], ExecutionEnvEvent::Initializing { env_type } if env_type == "local") + matches!(&captured[0], SandboxEvent::Initializing { provider } if provider == "local") ); assert!( - matches!(&captured[1], ExecutionEnvEvent::Ready { env_type, .. } if env_type == "local") + matches!(&captured[1], SandboxEvent::Ready { provider, .. } if provider == "local") ); std::fs::remove_dir_all(&dir).unwrap(); @@ -695,14 +695,14 @@ mod tests { #[tokio::test] async fn cleanup_emits_events() { - use crate::execution_env::ExecutionEnvEvent; + use crate::sandbox::SandboxEvent; use std::sync::{Arc, Mutex}; let dir = temp_dir(); - let events: Arc>> = Arc::new(Mutex::new(Vec::new())); + let events: Arc>> = Arc::new(Mutex::new(Vec::new())); let events_clone = Arc::clone(&events); - let mut env = LocalExecutionEnvironment::new(dir.clone()); + let mut env = LocalSandbox::new(dir.clone()); env.set_event_callback(Arc::new(move |e| { events_clone.lock().unwrap().push(e); })); @@ -712,10 +712,10 @@ mod tests { let captured = events.lock().unwrap(); assert_eq!(captured.len(), 2); assert!( - matches!(&captured[0], ExecutionEnvEvent::CleanupStarted { env_type } if env_type == "local") + matches!(&captured[0], SandboxEvent::CleanupStarted { provider } if provider == "local") ); assert!( - matches!(&captured[1], ExecutionEnvEvent::CleanupCompleted { env_type, .. } if env_type == "local") + matches!(&captured[1], SandboxEvent::CleanupCompleted { provider, .. } if provider == "local") ); std::fs::remove_dir_all(&dir).unwrap(); @@ -730,7 +730,7 @@ mod tests { ) .unwrap(); - let env = LocalExecutionEnvironment::new(dir.clone()); + let env = LocalSandbox::new(dir.clone()); let results = env .grep("println", "test.rs", &GrepOptions::default()) .await @@ -746,7 +746,7 @@ mod tests { let dir = temp_dir(); std::fs::write(dir.join("test.txt"), "Hello\nhello\nHELLO\n").unwrap(); - let env = LocalExecutionEnvironment::new(dir.clone()); + let env = LocalSandbox::new(dir.clone()); let results = env .grep( "hello", @@ -768,7 +768,7 @@ mod tests { let dir = temp_dir(); std::fs::write(dir.join("test.txt"), "match1\nmatch2\nmatch3\nmatch4\n").unwrap(); - let env = LocalExecutionEnvironment::new(dir.clone()); + let env = LocalSandbox::new(dir.clone()); let results = env .grep( "match", @@ -792,7 +792,7 @@ mod tests { std::fs::write(dir.join("b.rs"), "").unwrap(); std::fs::write(dir.join("c.txt"), "").unwrap(); - let env = LocalExecutionEnvironment::new(dir.clone()); + let env = LocalSandbox::new(dir.clone()); let results = env.glob("*.rs", None).await.unwrap(); assert_eq!(results.len(), 2); diff --git a/crates/arc-agent/src/mcp_integration.rs b/crates/arc-agent/src/mcp_integration.rs index 48d1b0030..6740e7389 100644 --- a/crates/arc-agent/src/mcp_integration.rs +++ b/crates/arc-agent/src/mcp_integration.rs @@ -83,12 +83,12 @@ mod tests { let tools = make_mcp_tools(Arc::new(mgr)); let tool = &tools[0]; - use crate::execution_env::ExecutionEnvironment; - use crate::test_support::MockExecutionEnvironment; + use crate::sandbox::Sandbox; + use crate::test_support::MockSandbox; use crate::tool_registry::ToolContext; use tokio_util::sync::CancellationToken; - let env: Arc = Arc::new(MockExecutionEnvironment::default()); + let env: Arc = Arc::new(MockSandbox::default()); let result = (tool.executor)( serde_json::json!({"message": "test message"}), ToolContext { diff --git a/crates/arc-agent/src/profiles/anthropic.rs b/crates/arc-agent/src/profiles/anthropic.rs index 11c0f19dc..839d4f1e3 100644 --- a/crates/arc-agent/src/profiles/anthropic.rs +++ b/crates/arc-agent/src/profiles/anthropic.rs @@ -1,5 +1,5 @@ use crate::config::SessionConfig; -use crate::execution_env::ExecutionEnvironment; +use crate::sandbox::Sandbox; use crate::profiles::assemble_system_prompt; use crate::profiles::BaseProfile; use crate::provider_profile::{ProfileCapabilities, ProviderProfile}; @@ -71,7 +71,7 @@ impl ProviderProfile for AnthropicProfile { fn build_system_prompt( &self, - env: &dyn ExecutionEnvironment, + env: &dyn Sandbox, env_context: &EnvContext, project_docs: &[String], user_instructions: Option<&str>, @@ -209,7 +209,7 @@ in the project. Keep changes minimal and focused on the task."; #[cfg(test)] mod tests { use super::*; - use crate::test_support::MockExecutionEnvironment; + use crate::test_support::MockSandbox; #[test] fn anthropic_profile_identity() { @@ -236,7 +236,7 @@ mod tests { #[test] fn anthropic_system_prompt_contains_env_context() { let profile = AnthropicProfile::new("claude-sonnet-4-20250514"); - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]); assert!(prompt.contains("You are Claude, an AI coding assistant made by Anthropic")); assert!(prompt.contains("")); @@ -277,7 +277,7 @@ mod tests { #[test] fn anthropic_system_prompt_includes_project_docs() { let profile = AnthropicProfile::new("claude-sonnet-4-20250514"); - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let docs = vec!["# Project README".into(), "# CONTRIBUTING guide".into()]; let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &docs, None, &[]); assert!(prompt.contains("# Project README")); @@ -287,7 +287,7 @@ mod tests { #[test] fn anthropic_system_prompt_includes_env_context() { let profile = AnthropicProfile::new("claude-opus-4-6"); - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let ctx = EnvContext { git_branch: Some("feature-branch".into()), is_git_repo: true, @@ -308,7 +308,7 @@ mod tests { #[test] fn anthropic_system_prompt_includes_user_instructions() { let profile = AnthropicProfile::new("claude-opus-4-6"); - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let ctx = EnvContext::default(); let prompt = profile.build_system_prompt(&env, &ctx, &[], Some("Always write tests first"), &[]); diff --git a/crates/arc-agent/src/profiles/gemini.rs b/crates/arc-agent/src/profiles/gemini.rs index fb0c838ef..ba8864d7f 100644 --- a/crates/arc-agent/src/profiles/gemini.rs +++ b/crates/arc-agent/src/profiles/gemini.rs @@ -1,5 +1,5 @@ use crate::config::SessionConfig; -use crate::execution_env::ExecutionEnvironment; +use crate::sandbox::Sandbox; use crate::profiles::assemble_system_prompt; use crate::profiles::BaseProfile; use crate::provider_profile::{ProfileCapabilities, ProviderProfile}; @@ -65,7 +65,7 @@ impl ProviderProfile for GeminiProfile { fn build_system_prompt( &self, - env: &dyn ExecutionEnvironment, + env: &dyn Sandbox, env_context: &EnvContext, project_docs: &[String], user_instructions: Option<&str>, @@ -229,7 +229,7 @@ in the project."; #[cfg(test)] mod tests { use super::*; - use crate::test_support::MockExecutionEnvironment; + use crate::test_support::MockSandbox; use std::sync::Arc; #[test] @@ -251,7 +251,7 @@ mod tests { #[test] fn gemini_system_prompt_contains_identity() { let profile = GeminiProfile::new("gemini-2.0-flash"); - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]); assert!(prompt.contains("You are Gemini CLI")); assert!(prompt.contains("solving bugs")); @@ -263,7 +263,7 @@ mod tests { #[test] fn gemini_system_prompt_contains_tool_guidance() { let profile = GeminiProfile::new("gemini-2.0-flash"); - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]); assert!(prompt.contains("read_file")); assert!(prompt.contains("read_many_files")); @@ -281,7 +281,7 @@ mod tests { #[test] fn gemini_system_prompt_contains_project_docs_convention() { let profile = GeminiProfile::new("gemini-2.0-flash"); - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]); assert!(prompt.contains("GEMINI.md")); assert!(prompt.contains("AGENTS.md")); @@ -290,7 +290,7 @@ mod tests { #[test] fn gemini_system_prompt_contains_coding_best_practices() { let profile = GeminiProfile::new("gemini-2.0-flash"); - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]); assert!(prompt.contains("clean, maintainable code")); assert!(prompt.contains("Handle errors appropriately")); @@ -300,7 +300,7 @@ mod tests { #[test] fn gemini_system_prompt_contains_env_context() { let profile = GeminiProfile::new("gemini-2.0-flash"); - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]); assert!(prompt.contains("")); assert!(prompt.contains("linux")); diff --git a/crates/arc-agent/src/profiles/mod.rs b/crates/arc-agent/src/profiles/mod.rs index b763a3089..fc003df11 100644 --- a/crates/arc-agent/src/profiles/mod.rs +++ b/crates/arc-agent/src/profiles/mod.rs @@ -6,7 +6,7 @@ pub use anthropic::AnthropicProfile; pub use gemini::GeminiProfile; pub use openai::OpenAiProfile; -use crate::execution_env::ExecutionEnvironment; +use crate::sandbox::Sandbox; use crate::skills::{format_skills_prompt_section, Skill}; use crate::tool_registry::ToolRegistry; use arc_llm::provider::Provider; @@ -40,7 +40,7 @@ pub struct EnvContext { #[must_use] pub fn assemble_system_prompt( core_prompt: &str, - env: &dyn ExecutionEnvironment, + env: &dyn Sandbox, env_context: &EnvContext, project_docs: &[String], user_instructions: Option<&str>, @@ -71,12 +71,12 @@ pub fn assemble_system_prompt( #[cfg(test)] #[must_use] -pub fn build_env_context_block(env: &dyn ExecutionEnvironment) -> String { +pub fn build_env_context_block(env: &dyn Sandbox) -> String { build_env_context_block_with(env, &EnvContext::default()) } #[must_use] -pub fn build_env_context_block_with(env: &dyn ExecutionEnvironment, ctx: &EnvContext) -> String { +pub fn build_env_context_block_with(env: &dyn Sandbox, ctx: &EnvContext) -> String { let mut lines = vec![ "".to_string(), format!("Working directory: {}", env.working_directory()), @@ -114,11 +114,11 @@ pub fn build_env_context_block_with(env: &dyn ExecutionEnvironment, ctx: &EnvCon #[cfg(test)] mod tests { use super::*; - use crate::test_support::MockExecutionEnvironment; + use crate::test_support::MockSandbox; #[test] fn env_context_block_contains_platform() { - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let block = build_env_context_block(&env); assert!(block.contains("")); assert!(block.contains("")); @@ -129,7 +129,7 @@ mod tests { #[test] fn env_context_block_with_extra_context() { - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let ctx = EnvContext { git_branch: Some("main".into()), is_git_repo: true, diff --git a/crates/arc-agent/src/profiles/openai.rs b/crates/arc-agent/src/profiles/openai.rs index b5dbadeb6..489fed590 100644 --- a/crates/arc-agent/src/profiles/openai.rs +++ b/crates/arc-agent/src/profiles/openai.rs @@ -1,5 +1,5 @@ use crate::config::SessionConfig; -use crate::execution_env::ExecutionEnvironment; +use crate::sandbox::Sandbox; use crate::profiles::assemble_system_prompt; use crate::profiles::BaseProfile; use crate::provider_profile::{ProfileCapabilities, ProviderProfile}; @@ -75,7 +75,7 @@ impl ProviderProfile for OpenAiProfile { fn build_system_prompt( &self, - env: &dyn ExecutionEnvironment, + env: &dyn Sandbox, env_context: &EnvContext, project_docs: &[String], user_instructions: Option<&str>, @@ -210,7 +210,7 @@ in the project."; #[cfg(test)] mod tests { use super::*; - use crate::test_support::MockExecutionEnvironment; + use crate::test_support::MockSandbox; #[test] fn openai_profile_identity() { @@ -231,7 +231,7 @@ mod tests { #[test] fn openai_system_prompt_contains_env_context() { let profile = OpenAiProfile::new("o3-mini"); - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]); assert!(prompt.contains("You are a coding agent powered by OpenAI")); assert!(prompt.contains("")); @@ -243,7 +243,7 @@ mod tests { #[test] fn openai_system_prompt_contains_tool_guidance() { let profile = OpenAiProfile::new("o3-mini"); - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]); assert!(prompt.contains("read_file")); assert!(prompt.contains("apply_patch")); @@ -257,7 +257,7 @@ mod tests { #[test] fn openai_system_prompt_contains_coding_best_practices() { let profile = OpenAiProfile::new("o3-mini"); - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]); assert!(prompt.contains("clean, maintainable code")); assert!(prompt.contains("existing code conventions")); @@ -266,7 +266,7 @@ mod tests { #[test] fn openai_system_prompt_includes_project_docs() { let profile = OpenAiProfile::new("o3-mini"); - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let docs = vec!["# Project README".into(), "# CONTRIBUTING guide".into()]; let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &docs, None, &[]); assert!(prompt.contains("# Project README")); @@ -276,7 +276,7 @@ mod tests { #[test] fn openai_system_prompt_includes_user_instructions() { let profile = OpenAiProfile::new("o3-mini"); - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let prompt = profile.build_system_prompt( &env, &EnvContext::default(), diff --git a/crates/arc-agent/src/project_docs.rs b/crates/arc-agent/src/project_docs.rs index d10496902..951337a5c 100644 --- a/crates/arc-agent/src/project_docs.rs +++ b/crates/arc-agent/src/project_docs.rs @@ -1,10 +1,10 @@ -use crate::execution_env::ExecutionEnvironment; +use crate::sandbox::Sandbox; use arc_llm::provider::Provider; const BUDGET_BYTES: usize = 32768; pub async fn discover_project_docs( - env: &dyn ExecutionEnvironment, + env: &dyn Sandbox, git_root: &str, working_dir: &str, provider: Provider, @@ -92,8 +92,8 @@ fn truncate_to_budget(content: &str, budget: usize) -> String { #[cfg(test)] mod tests { use super::*; - use crate::execution_env::ExecutionEnvironment; - use crate::test_support::MockExecutionEnvironment; + use crate::sandbox::Sandbox; + use crate::test_support::MockSandbox; use std::collections::HashMap; use std::sync::Arc; @@ -101,7 +101,7 @@ mod tests { async fn discovers_agents_md() { let mut files = HashMap::new(); files.insert("/repo/AGENTS.md".into(), "Agent instructions".into()); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { files, ..Default::default() }); @@ -118,7 +118,7 @@ mod tests { files.insert("/repo/.codex/instructions.md".into(), "copilot".into()); files.insert("/repo/GEMINI.md".into(), "gemini".into()); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { files: files.clone(), ..Default::default() }); @@ -128,7 +128,7 @@ mod tests { assert_eq!(anthropic_docs[0], "agents"); assert_eq!(anthropic_docs[1], "claude"); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { files: files.clone(), ..Default::default() }); @@ -138,7 +138,7 @@ mod tests { assert_eq!(openai_docs[0], "agents"); assert_eq!(openai_docs[1], "copilot"); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { files, ..Default::default() }); @@ -158,7 +158,7 @@ mod tests { files.insert("/repo/AGENTS.md".into(), large_content.clone()); files.insert("/repo/CLAUDE.md".into(), second_content); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { files, ..Default::default() }); @@ -177,7 +177,7 @@ mod tests { files.insert("/repo/src/AGENTS.md".into(), "src agents".into()); files.insert("/repo/src/app/AGENTS.md".into(), "app agents".into()); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { files, ..Default::default() }); diff --git a/crates/arc-agent/src/provider_profile.rs b/crates/arc-agent/src/provider_profile.rs index 534f8761a..2b4f61619 100644 --- a/crates/arc-agent/src/provider_profile.rs +++ b/crates/arc-agent/src/provider_profile.rs @@ -1,4 +1,4 @@ -use crate::execution_env::ExecutionEnvironment; +use crate::sandbox::Sandbox; use crate::profiles::EnvContext; use crate::skills::Skill; use crate::subagent::{ @@ -25,7 +25,7 @@ pub trait ProviderProfile: Send + Sync { fn tool_registry_mut(&mut self) -> &mut ToolRegistry; fn build_system_prompt( &self, - env: &dyn ExecutionEnvironment, + env: &dyn Sandbox, env_context: &EnvContext, project_docs: &[String], user_instructions: Option<&str>, @@ -81,7 +81,7 @@ pub trait ProviderProfile: Send + Sync { #[cfg(test)] mod tests { use super::*; - use crate::test_support::{MockExecutionEnvironment, TestProfile}; + use crate::test_support::{MockSandbox, TestProfile}; use arc_llm::provider::Provider; #[test] @@ -103,7 +103,7 @@ mod tests { #[test] fn profile_build_system_prompt() { let profile = TestProfile::new(); - let env = MockExecutionEnvironment::linux(); + let env = MockSandbox::linux(); let ctx = EnvContext::default(); let docs = vec!["README.md contents".into()]; let prompt = profile.build_system_prompt(&env, &ctx, &docs, None, &[]); @@ -113,7 +113,7 @@ mod tests { #[test] fn profile_build_system_prompt_with_user_instructions() { let profile = TestProfile::new(); - let env = MockExecutionEnvironment::default(); + let env = MockSandbox::default(); let ctx = EnvContext::default(); let prompt = profile.build_system_prompt(&env, &ctx, &[], Some("Always use TDD"), &[]); assert!(prompt.contains("Always use TDD")); diff --git a/crates/arc-agent/src/read_before_write_sandbox.rs b/crates/arc-agent/src/read_before_write_sandbox.rs index 3407a6be4..0b2e928c4 100644 --- a/crates/arc-agent/src/read_before_write_sandbox.rs +++ b/crates/arc-agent/src/read_before_write_sandbox.rs @@ -1,4 +1,4 @@ -use crate::execution_env::*; +use crate::sandbox::*; use std::collections::HashSet; use std::path::{Component, PathBuf}; use std::sync::{Arc, Mutex}; @@ -8,13 +8,13 @@ use std::sync::{Arc, Mutex}; /// Tracks which file paths the agent has seen (via `read_file` or `grep`) and /// returns an error when `write_file` or `delete_file` targets an existing file /// that hasn't been read. Writing to new (non-existent) files is always allowed. -pub struct ReadBeforeWriteEnvironment { - inner: Arc, +pub struct ReadBeforeWriteSandbox { + inner: Arc, read_set: Mutex>, } -impl ReadBeforeWriteEnvironment { - pub fn new(inner: Arc) -> Self { +impl ReadBeforeWriteSandbox { + pub fn new(inner: Arc) -> Self { Self { inner, read_set: Mutex::new(HashSet::new()), @@ -71,8 +71,8 @@ impl ReadBeforeWriteEnvironment { } } -crate::delegate_execution_env! { - ReadBeforeWriteEnvironment => inner { +crate::delegate_sandbox! { + ReadBeforeWriteSandbox => inner { async fn read_file( &self, path: &str, @@ -116,17 +116,17 @@ crate::delegate_execution_env! { #[cfg(test)] mod tests { use super::*; - use crate::test_support::MockExecutionEnvironment; + use crate::test_support::MockSandbox; use std::collections::HashMap; // Cycle 1: write to existing unread file → error #[tokio::test] async fn write_to_existing_unread_file_returns_error() { - let mock = MockExecutionEnvironment { + let mock = MockSandbox { files: HashMap::from([("a.ts".into(), "content".into())]), ..Default::default() }; - let env = ReadBeforeWriteEnvironment::new(Arc::new(mock)); + let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); let result = env.write_file("a.ts", "new content").await; @@ -139,8 +139,8 @@ mod tests { // Cycle 2: write to non-existent file → success #[tokio::test] async fn write_to_nonexistent_file_succeeds() { - let mock = MockExecutionEnvironment::default(); - let env = ReadBeforeWriteEnvironment::new(Arc::new(mock)); + let mock = MockSandbox::default(); + let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); let result = env.write_file("new.ts", "content").await; @@ -150,11 +150,11 @@ mod tests { // Cycle 3: read then write → success #[tokio::test] async fn read_then_write_succeeds() { - let mock = MockExecutionEnvironment { + let mock = MockSandbox { files: HashMap::from([("a.ts".into(), "content".into())]), ..Default::default() }; - let env = ReadBeforeWriteEnvironment::new(Arc::new(mock)); + let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); env.read_file("a.ts", None, None).await.unwrap(); let result = env.write_file("a.ts", "new content").await; @@ -165,12 +165,12 @@ mod tests { // Cycle 4: grep results populate read set #[tokio::test] async fn grep_populates_read_set() { - let mock = MockExecutionEnvironment { + let mock = MockSandbox { files: HashMap::from([("b.ts".into(), "content".into())]), grep_results: vec!["b.ts:1:content".into()], ..Default::default() }; - let env = ReadBeforeWriteEnvironment::new(Arc::new(mock)); + let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); env.grep("pattern", ".", &GrepOptions::default()) .await @@ -183,12 +183,12 @@ mod tests { // Cycle 5: glob does NOT populate read set #[tokio::test] async fn glob_does_not_populate_read_set() { - let mock = MockExecutionEnvironment { + let mock = MockSandbox { files: HashMap::from([("c.ts".into(), "content".into())]), glob_results: vec!["c.ts".into()], ..Default::default() }; - let env = ReadBeforeWriteEnvironment::new(Arc::new(mock)); + let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); env.glob("*.ts", None).await.unwrap(); let result = env.write_file("c.ts", "new").await; @@ -199,7 +199,7 @@ mod tests { // Cycle 6: path normalization — relative vs absolute #[tokio::test] async fn path_normalization_relative_and_absolute() { - let mock = MockExecutionEnvironment { + let mock = MockSandbox { files: HashMap::from([ ("a.ts".into(), "content".into()), ("/work/a.ts".into(), "content".into()), @@ -207,7 +207,7 @@ mod tests { working_dir: "/work", ..Default::default() }; - let env = ReadBeforeWriteEnvironment::new(Arc::new(mock)); + let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); env.read_file("a.ts", None, None).await.unwrap(); let result = env.write_file("/work/a.ts", "new content").await; @@ -218,11 +218,11 @@ mod tests { // Cycle 7: delete unread file → error #[tokio::test] async fn delete_unread_file_returns_error() { - let mock = MockExecutionEnvironment { + let mock = MockSandbox { files: HashMap::from([("d.ts".into(), "content".into())]), ..Default::default() }; - let env = ReadBeforeWriteEnvironment::new(Arc::new(mock)); + let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); let result = env.delete_file("d.ts").await; @@ -232,11 +232,11 @@ mod tests { // Cycle 8: error message is actionable #[tokio::test] async fn error_message_is_actionable() { - let mock = MockExecutionEnvironment { + let mock = MockSandbox { files: HashMap::from([("main.rs".into(), "fn main() {}".into())]), ..Default::default() }; - let env = ReadBeforeWriteEnvironment::new(Arc::new(mock)); + let env = ReadBeforeWriteSandbox::new(Arc::new(mock)); let err = env.write_file("main.rs", "new").await.unwrap_err(); diff --git a/crates/arc-agent/src/sandbox.rs b/crates/arc-agent/src/sandbox.rs index 4671ea80a..67c0b5adb 100644 --- a/crates/arc-agent/src/sandbox.rs +++ b/crates/arc-agent/src/sandbox.rs @@ -4,14 +4,14 @@ use std::fmt::Write; use std::sync::Arc; use tokio_util::sync::CancellationToken; -/// Generates an `#[async_trait] impl ExecutionEnvironment` block for a decorator type -/// that wraps an `Arc`. The caller provides custom method +/// Generates an `#[async_trait] impl Sandbox` block for a decorator type +/// that wraps an `Arc`. The caller provides custom method /// implementations; all remaining trait methods delegate to the inner field. /// /// # Usage /// /// ```ignore -/// delegate_execution_env! { +/// delegate_sandbox! { /// MyDecorator => inner { /// // Only provide methods with custom logic — the rest delegate automatically. /// async fn read_file(&self, path: &str, offset: Option, limit: Option) -> Result { @@ -21,14 +21,14 @@ use tokio_util::sync::CancellationToken; /// } /// ``` #[macro_export] -macro_rules! delegate_execution_env { +macro_rules! delegate_sandbox { ( $type:ty => $field:ident { $($custom:item)* } ) => { #[async_trait::async_trait] - impl $crate::execution_env::ExecutionEnvironment for $type { + impl $crate::sandbox::Sandbox for $type { $($custom)* async fn file_exists(&self, path: &str) -> Result { @@ -39,7 +39,7 @@ macro_rules! delegate_execution_env { &self, path: &str, depth: Option, - ) -> Result, String> { + ) -> Result, String> { self.$field.list_directory(path, depth).await } @@ -50,7 +50,7 @@ macro_rules! delegate_execution_env { working_dir: Option<&str>, env_vars: Option<&std::collections::HashMap>, cancel_token: Option, - ) -> Result<$crate::execution_env::ExecResult, String> { + ) -> Result<$crate::sandbox::ExecResult, String> { self.$field .exec_command(command, timeout_ms, working_dir, env_vars, cancel_token) .await @@ -83,40 +83,40 @@ macro_rules! delegate_execution_env { }; } -/// Events emitted during execution environment lifecycle operations. +/// Events emitted during sandbox lifecycle operations. #[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ExecutionEnvEvent { +pub enum SandboxEvent { // -- Common lifecycle -- Initializing { - env_type: String, + provider: String, }, Ready { - env_type: String, + provider: String, duration_ms: u64, }, InitializeFailed { - env_type: String, + provider: String, error: String, duration_ms: u64, }, CleanupStarted { - env_type: String, + provider: String, }, CleanupCompleted { - env_type: String, + provider: String, duration_ms: u64, }, CleanupFailed { - env_type: String, + provider: String, error: String, }, // -- Docker -- - ImagePulling { - image: String, + SnapshotPulling { + name: String, }, - ImagePulled { - image: String, + SnapshotPulled { + name: String, duration_ms: u64, }, @@ -151,43 +151,43 @@ pub enum ExecutionEnvEvent { }, } -impl ExecutionEnvEvent { +impl SandboxEvent { pub fn trace(&self) { use tracing::{debug, error, info, warn}; match self { - Self::Initializing { env_type } => { - debug!(env_type, "Execution env initializing"); + Self::Initializing { provider } => { + debug!(provider, "Sandbox initializing"); } Self::Ready { - env_type, + provider, duration_ms, } => { - info!(env_type, duration_ms, "Execution env ready"); + info!(provider, duration_ms, "Sandbox ready"); } Self::InitializeFailed { - env_type, + provider, error, duration_ms, } => { - error!(env_type, error, duration_ms, "Execution env init failed"); + error!(provider, error, duration_ms, "Sandbox init failed"); } - Self::CleanupStarted { env_type } => { - debug!(env_type, "Execution env cleanup started"); + Self::CleanupStarted { provider } => { + debug!(provider, "Sandbox cleanup started"); } Self::CleanupCompleted { - env_type, + provider, duration_ms, } => { - debug!(env_type, duration_ms, "Execution env cleanup completed"); + debug!(provider, duration_ms, "Sandbox cleanup completed"); } - Self::CleanupFailed { env_type, error } => { - warn!(env_type, error, "Execution env cleanup failed"); + Self::CleanupFailed { provider, error } => { + warn!(provider, error, "Sandbox cleanup failed"); } - Self::ImagePulling { image } => { - debug!(image, "Docker image pulling"); + Self::SnapshotPulling { name } => { + debug!(name, "Snapshot pulling"); } - Self::ImagePulled { image, duration_ms } => { - debug!(image, duration_ms, "Docker image pulled"); + Self::SnapshotPulled { name, duration_ms } => { + debug!(name, duration_ms, "Snapshot pulled"); } Self::SnapshotEnsuring { name } => { debug!(name, "Snapshot ensuring"); @@ -214,8 +214,8 @@ impl ExecutionEnvEvent { } } -/// Callback type for execution environment events. -pub type ExecEnvEventCallback = Arc; +/// Callback type for sandbox events. +pub type SandboxEventCallback = Arc; /// Formats file content with line numbers for display. /// @@ -260,7 +260,7 @@ pub struct GrepOptions { } #[async_trait] -pub trait ExecutionEnvironment: Send + Sync { +pub trait Sandbox: Send + Sync { async fn read_file( &self, path: &str, @@ -300,7 +300,7 @@ pub trait ExecutionEnvironment: Send + Sync { #[cfg(test)] mod tests { use super::*; - use crate::test_support::MockExecutionEnvironment; + use crate::test_support::MockSandbox; use std::collections::HashMap; use std::sync::Arc; @@ -308,7 +308,7 @@ mod tests { async fn mock_env_read_file() { let mut files = HashMap::new(); files.insert("test.rs".into(), "hello".into()); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { files, ..Default::default() }); @@ -318,7 +318,7 @@ mod tests { #[tokio::test] async fn mock_env_exec_command() { - let env: Arc = Arc::new(MockExecutionEnvironment::default()); + let env: Arc = Arc::new(MockSandbox::default()); let result = env .exec_command("echo", 5000, None, None, None) .await @@ -329,7 +329,7 @@ mod tests { #[tokio::test] async fn mock_env_list_directory() { - let env: Arc = Arc::new(MockExecutionEnvironment::default()); + let env: Arc = Arc::new(MockSandbox::default()); let entries = env.list_directory("/tmp", None).await.unwrap(); assert_eq!(entries.len(), 0); } @@ -370,68 +370,68 @@ mod tests { #[test] fn mock_env_platform() { - let env = MockExecutionEnvironment::default(); + let env = MockSandbox::default(); assert_eq!(env.platform(), "darwin"); assert_eq!(env.working_directory(), "/tmp/test"); assert_eq!(env.os_version(), "Darwin 24.0.0"); } #[test] - fn execution_env_event_serialization_round_trip() { + fn sandbox_event_serialization_round_trip() { let events = vec![ - ExecutionEnvEvent::Initializing { - env_type: "local".into(), + SandboxEvent::Initializing { + provider: "local".into(), }, - ExecutionEnvEvent::Ready { - env_type: "local".into(), + SandboxEvent::Ready { + provider: "local".into(), duration_ms: 50, }, - ExecutionEnvEvent::InitializeFailed { - env_type: "docker".into(), + SandboxEvent::InitializeFailed { + provider: "docker".into(), error: "no daemon".into(), duration_ms: 100, }, - ExecutionEnvEvent::CleanupStarted { - env_type: "daytona".into(), + SandboxEvent::CleanupStarted { + provider: "daytona".into(), }, - ExecutionEnvEvent::CleanupCompleted { - env_type: "daytona".into(), + SandboxEvent::CleanupCompleted { + provider: "daytona".into(), duration_ms: 200, }, - ExecutionEnvEvent::CleanupFailed { - env_type: "docker".into(), + SandboxEvent::CleanupFailed { + provider: "docker".into(), error: "container gone".into(), }, - ExecutionEnvEvent::ImagePulling { - image: "ubuntu:22.04".into(), + SandboxEvent::SnapshotPulling { + name: "ubuntu:22.04".into(), }, - ExecutionEnvEvent::ImagePulled { - image: "ubuntu:22.04".into(), + SandboxEvent::SnapshotPulled { + name: "ubuntu:22.04".into(), duration_ms: 5000, }, - ExecutionEnvEvent::SnapshotEnsuring { + SandboxEvent::SnapshotEnsuring { name: "my-snap".into(), }, - ExecutionEnvEvent::SnapshotCreating { + SandboxEvent::SnapshotCreating { name: "my-snap".into(), }, - ExecutionEnvEvent::SnapshotReady { + SandboxEvent::SnapshotReady { name: "my-snap".into(), duration_ms: 30000, }, - ExecutionEnvEvent::SnapshotFailed { + SandboxEvent::SnapshotFailed { name: "my-snap".into(), error: "build failed".into(), }, - ExecutionEnvEvent::GitCloneStarted { + SandboxEvent::GitCloneStarted { url: "https://github.com/org/repo.git".into(), branch: Some("main".into()), }, - ExecutionEnvEvent::GitCloneCompleted { + SandboxEvent::GitCloneCompleted { url: "https://github.com/org/repo.git".into(), duration_ms: 8000, }, - ExecutionEnvEvent::GitCloneFailed { + SandboxEvent::GitCloneFailed { url: "https://github.com/org/repo.git".into(), error: "auth failed".into(), }, @@ -441,17 +441,17 @@ mod tests { for event in &events { let json = serde_json::to_string(event).unwrap(); - let deserialized: ExecutionEnvEvent = serde_json::from_str(&json).unwrap(); + let deserialized: SandboxEvent = serde_json::from_str(&json).unwrap(); let json2 = serde_json::to_string(&deserialized).unwrap(); assert_eq!(json, json2); } } #[test] - fn exec_env_event_callback_type_compiles() { - let cb: ExecEnvEventCallback = Arc::new(|_event| {}); - cb(ExecutionEnvEvent::Initializing { - env_type: "test".into(), + fn sandbox_event_callback_type_compiles() { + let cb: SandboxEventCallback = Arc::new(|_event| {}); + cb(SandboxEvent::Initializing { + provider: "test".into(), }); } } diff --git a/crates/arc-agent/src/session.rs b/crates/arc-agent/src/session.rs index a9a239dbc..a8229a41a 100644 --- a/crates/arc-agent/src/session.rs +++ b/crates/arc-agent/src/session.rs @@ -1,7 +1,7 @@ use crate::config::SessionConfig; use crate::error::AgentError; use crate::event::EventEmitter; -use crate::execution_env::ExecutionEnvironment; +use crate::sandbox::Sandbox; use crate::file_tracker::FileTracker; use crate::history::History; use crate::loop_detection::detect_loop; @@ -31,7 +31,7 @@ pub struct Session { state: SessionState, llm_client: Client, provider_profile: Arc, - execution_env: Arc, + sandbox: Arc, steering_queue: Arc>>, followup_queue: Arc>>, cancel_token: CancellationToken, @@ -47,7 +47,7 @@ impl Session { pub fn new( llm_client: Client, provider_profile: Arc, - execution_env: Arc, + sandbox: Arc, config: SessionConfig, ) -> Self { Self { @@ -58,7 +58,7 @@ impl Session { state: SessionState::Idle, llm_client, provider_profile, - execution_env, + sandbox, steering_queue: Arc::new(Mutex::new(VecDeque::new())), followup_queue: Arc::new(Mutex::new(VecDeque::new())), cancel_token: CancellationToken::new(), @@ -80,11 +80,11 @@ impl Session { .config .git_root .clone() - .unwrap_or_else(|| self.execution_env.working_directory().to_string()); + .unwrap_or_else(|| self.sandbox.working_directory().to_string()); self.project_docs = discover_project_docs( - self.execution_env.as_ref(), + self.sandbox.as_ref(), &doc_root, - self.execution_env.working_directory(), + self.sandbox.working_directory(), self.provider_profile.provider(), ) .await; @@ -98,7 +98,7 @@ impl Session { default_skill_dirs(home.as_deref(), self.config.git_root.as_deref()) } }; - self.skills = discover_skills(self.execution_env.as_ref(), &skill_dirs).await; + self.skills = discover_skills(self.sandbox.as_ref(), &skill_dirs).await; debug!(skill_count = self.skills.len(), "Skills discovered"); // Register use_skill tool when skills are available @@ -158,7 +158,7 @@ impl Session { // Build system prompt once (static for the session lifetime) self.system_prompt = self.provider_profile.build_system_prompt( - self.execution_env.as_ref(), + self.sandbox.as_ref(), &self.env_context, &self.project_docs, self.config.user_instructions.as_deref(), @@ -172,7 +172,7 @@ impl Session { // Detect git info via execution environment let git_branch = self - .execution_env + .sandbox .exec_command("git rev-parse --abbrev-ref HEAD", 5000, None, None, None) .await .ok() @@ -182,7 +182,7 @@ impl Session { let is_git_repo = git_branch.is_some(); let git_status_short = if is_git_repo { - self.execution_env + self.sandbox .exec_command("git status --short", 5000, None, None, None) .await .ok() @@ -194,7 +194,7 @@ impl Session { }; let git_recent_commits = if is_git_repo { - self.execution_env + self.sandbox .exec_command("git log --oneline -10", 5000, None, None, None) .await .ok() @@ -585,7 +585,7 @@ impl Session { &tool_calls, self.provider_profile.supports_parallel_tool_calls(), self.provider_profile.tool_registry(), - self.execution_env.clone(), + self.sandbox.clone(), self.config.tool_approval.as_ref(), &self.cancel_token, &self.config, @@ -1056,7 +1056,7 @@ mod tests { let provider = Arc::new(MockLlmProvider::new(responses)); let client = make_client(provider).await; let profile = Arc::new(TestProfile::with_tools(registry)); - let env = Arc::new(MockExecutionEnvironment::default()); + let env = Arc::new(MockSandbox::default()); let config = SessionConfig { enable_loop_detection: false, ..Default::default() @@ -1091,7 +1091,7 @@ mod tests { }); let client = make_client(error_provider).await; let profile = Arc::new(TestProfile::new()); - let env = Arc::new(MockExecutionEnvironment::default()); + let env = Arc::new(MockSandbox::default()); let mut session = Session::new(client, profile, env, SessionConfig::default()); let result = session.process_input("Hello").await; @@ -1170,7 +1170,7 @@ mod tests { let provider = Arc::new(MockLlmProvider::new(responses)); let client = make_client(provider).await; let profile = Arc::new(TestProfile::parallel(registry)); - let env = Arc::new(MockExecutionEnvironment::default()); + let env = Arc::new(MockSandbox::default()); let mut session = Session::new(client, profile, env, SessionConfig::default()); let mut rx = session.subscribe(); @@ -1221,7 +1221,7 @@ mod tests { let client = make_client(provider).await; let registry = ToolRegistry::new(); let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100)); - let env = Arc::new(MockExecutionEnvironment::default()); + let env = Arc::new(MockSandbox::default()); let mut session = Session::new(client, profile, env, SessionConfig::default()); let mut rx = session.subscribe(); @@ -1247,7 +1247,7 @@ mod tests { let provider_ref = provider.clone(); let client = make_client(provider as Arc).await; let profile = Arc::new(TestProfile::new()); - let env = Arc::new(MockExecutionEnvironment::default()); + let env = Arc::new(MockSandbox::default()); let mut session = Session::new(client, profile, env, SessionConfig::default()); // Default reasoning_effort is None @@ -1270,7 +1270,7 @@ mod tests { let registry = ToolRegistry::new(); // Large context window so short input stays well under 80% let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 200_000)); - let env = Arc::new(MockExecutionEnvironment::default()); + let env = Arc::new(MockSandbox::default()); let mut session = Session::new(client, profile, env, SessionConfig::default()); let mut rx = session.subscribe(); @@ -1399,7 +1399,7 @@ mod tests { let provider_ref = provider.clone(); let client = make_client(provider as Arc).await; let profile = Arc::new(TestProfile::new()); - let env = Arc::new(MockExecutionEnvironment::default()); + let env = Arc::new(MockSandbox::default()); let config = SessionConfig { user_instructions: Some("Always use TDD".into()), ..Default::default() @@ -1621,7 +1621,7 @@ mod tests { }); let client = make_client(provider as Arc).await; let profile = Arc::new(TestProfile::new()); - let env = Arc::new(MockExecutionEnvironment::default()); + let env = Arc::new(MockSandbox::default()); let mut session = Session::new(client, profile, env, SessionConfig::default()); let result = session.process_input("Hello").await; @@ -1647,7 +1647,7 @@ mod tests { let client = make_client(provider).await; let registry = ToolRegistry::new(); let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100)); - let env = Arc::new(MockExecutionEnvironment::default()); + let env = Arc::new(MockSandbox::default()); let config = SessionConfig { enable_context_compaction: true, compaction_preserve_turns: 1, @@ -1690,7 +1690,7 @@ mod tests { let client = make_client(provider).await; let registry = ToolRegistry::new(); let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100)); - let env = Arc::new(MockExecutionEnvironment::default()); + let env = Arc::new(MockSandbox::default()); let config = SessionConfig { enable_context_compaction: false, ..Default::default() @@ -1773,7 +1773,7 @@ mod tests { let client = make_client(provider as Arc).await; let registry = ToolRegistry::new(); let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100)); - let env = Arc::new(MockExecutionEnvironment::default()); + let env = Arc::new(MockSandbox::default()); let config = SessionConfig { enable_context_compaction: true, compaction_preserve_turns: 1, @@ -1876,7 +1876,7 @@ mod tests { let client = make_client(provider.clone() as Arc).await; // Tiny context window to force compaction let profile = Arc::new(TestProfile::parallel_with_context_window(registry, 100)); - let env = Arc::new(MockExecutionEnvironment::default()); + let env = Arc::new(MockSandbox::default()); let config = SessionConfig { enable_context_compaction: true, compaction_preserve_turns: 1, @@ -1977,8 +1977,8 @@ mod tests { let client = make_client(provider).await; let profile: Arc = Arc::new(TestProfile::new()); - let env: Arc = - Arc::new(MockExecutionEnvironment::default()); + let env: Arc = + Arc::new(MockSandbox::default()); let mut session = Session::new(client, profile, env, config); // Subscribe to events before initialize diff --git a/crates/arc-agent/src/skills.rs b/crates/arc-agent/src/skills.rs index eb5a51fb8..fb730f595 100644 --- a/crates/arc-agent/src/skills.rs +++ b/crates/arc-agent/src/skills.rs @@ -1,4 +1,4 @@ -use crate::execution_env::ExecutionEnvironment; +use crate::sandbox::Sandbox; use crate::tool_registry::RegisteredTool; use crate::tools::required_str; use arc_llm::types::ToolDefinition; @@ -220,7 +220,7 @@ pub fn default_skill_dirs(home_dir: Option<&str>, git_root: Option<&str>) -> Vec dirs } -pub async fn discover_skills(env: &dyn ExecutionEnvironment, dirs: &[String]) -> Vec { +pub async fn discover_skills(env: &dyn Sandbox, dirs: &[String]) -> Vec { let mut skills_by_name: std::collections::HashMap = std::collections::HashMap::new(); @@ -253,7 +253,7 @@ pub async fn discover_skills(env: &dyn ExecutionEnvironment, dirs: &[String]) -> #[cfg(test)] mod tests { use super::*; - use crate::test_support::MockExecutionEnvironment; + use crate::test_support::MockSandbox; use std::collections::HashMap; // --- parse_skill tests --- @@ -444,7 +444,7 @@ name: trimmed "/skills/commit/SKILL.md".into(), "---\nname: commit\ndescription: Make a commit\n---\nDo commit".into(), ); - let env = MockExecutionEnvironment { + let env = MockSandbox { files, glob_results: vec!["/skills/commit/SKILL.md".into()], ..Default::default() @@ -464,7 +464,7 @@ name: trimmed "---\nname: good\n---\nGood template".into(), ); files.insert("/skills/bad/SKILL.md".into(), "no frontmatter here".into()); - let env = MockExecutionEnvironment { + let env = MockSandbox { files, glob_results: vec![ "/skills/good/SKILL.md".into(), @@ -480,7 +480,7 @@ name: trimmed #[tokio::test] async fn discover_empty_dirs() { - let env = MockExecutionEnvironment::default(); + let env = MockSandbox::default(); let skills = discover_skills(&env, &[]).await; assert!(skills.is_empty()); } @@ -497,10 +497,10 @@ name: trimmed "---\nname: commit\ndescription: Project commit\n---\nProject template".into(), ); - // We need separate envs because MockExecutionEnvironment returns the same glob_results + // We need separate envs because MockSandbox returns the same glob_results // for all calls. Instead, we test with a single env that has both files // and glob returns both — the later dir overrides the earlier. - let env = MockExecutionEnvironment { + let env = MockSandbox { files, glob_results: vec![ "/global/commit/SKILL.md".into(), @@ -543,8 +543,8 @@ name: trimmed let skills = Arc::new(test_skills()); let tool = make_use_skill_tool(skills); - let env: Arc = - Arc::new(MockExecutionEnvironment::default()); + let env: Arc = + Arc::new(MockSandbox::default()); let args = serde_json::json!({"skill_name": "commit"}); let ctx = crate::tool_registry::ToolContext { env, @@ -562,8 +562,8 @@ name: trimmed let skills = Arc::new(test_skills()); let tool = make_use_skill_tool(skills); - let env: Arc = - Arc::new(MockExecutionEnvironment::default()); + let env: Arc = + Arc::new(MockSandbox::default()); let args = serde_json::json!({"skill_name": "nonexistent"}); let ctx = crate::tool_registry::ToolContext { env, @@ -579,8 +579,8 @@ name: trimmed let skills = Arc::new(test_skills()); let tool = make_use_skill_tool(skills); - let env: Arc = - Arc::new(MockExecutionEnvironment::default()); + let env: Arc = + Arc::new(MockSandbox::default()); let args = serde_json::json!({}); let ctx = crate::tool_registry::ToolContext { env, diff --git a/crates/arc-agent/src/test_support.rs b/crates/arc-agent/src/test_support.rs index 2bcc1b128..7fe0e9ceb 100644 --- a/crates/arc-agent/src/test_support.rs +++ b/crates/arc-agent/src/test_support.rs @@ -1,5 +1,5 @@ use crate::config::SessionConfig; -use crate::execution_env::*; +use crate::sandbox::*; use crate::profiles::EnvContext; use crate::provider_profile::{ProfileCapabilities, ProviderProfile}; use crate::session::Session; @@ -15,9 +15,9 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use tokio_util::sync::CancellationToken; -// --- MockExecutionEnvironment --- +// --- MockSandbox --- -pub struct MockExecutionEnvironment { +pub struct MockSandbox { pub files: HashMap, pub exec_result: ExecResult, pub grep_results: Vec, @@ -33,10 +33,10 @@ pub struct MockExecutionEnvironment { pub captured_timeout: Mutex>, /// Captures the `command` argument from `exec_command` calls. pub captured_command: Mutex>, - pub event_callback: Option, + pub event_callback: Option, } -impl MockExecutionEnvironment { +impl MockSandbox { pub fn linux() -> Self { Self { working_dir: "/home/test", @@ -47,8 +47,8 @@ impl MockExecutionEnvironment { } } -impl MockExecutionEnvironment { - fn emit(&self, event: crate::execution_env::ExecutionEnvEvent) { +impl MockSandbox { + fn emit(&self, event: crate::sandbox::SandboxEvent) { event.trace(); if let Some(ref cb) = self.event_callback { cb(event); @@ -56,7 +56,7 @@ impl MockExecutionEnvironment { } } -impl Default for MockExecutionEnvironment { +impl Default for MockSandbox { fn default() -> Self { Self { files: HashMap::new(), @@ -82,7 +82,7 @@ impl Default for MockExecutionEnvironment { } #[async_trait] -impl ExecutionEnvironment for MockExecutionEnvironment { +impl Sandbox for MockSandbox { async fn read_file( &self, path: &str, @@ -163,22 +163,22 @@ impl ExecutionEnvironment for MockExecutionEnvironment { } async fn initialize(&self) -> Result<(), String> { - self.emit(crate::execution_env::ExecutionEnvEvent::Initializing { - env_type: "mock".into(), + self.emit(crate::sandbox::SandboxEvent::Initializing { + provider: "mock".into(), }); - self.emit(crate::execution_env::ExecutionEnvEvent::Ready { - env_type: "mock".into(), + self.emit(crate::sandbox::SandboxEvent::Ready { + provider: "mock".into(), duration_ms: 0, }); Ok(()) } async fn cleanup(&self) -> Result<(), String> { - self.emit(crate::execution_env::ExecutionEnvEvent::CleanupStarted { - env_type: "mock".into(), + self.emit(crate::sandbox::SandboxEvent::CleanupStarted { + provider: "mock".into(), }); - self.emit(crate::execution_env::ExecutionEnvEvent::CleanupCompleted { - env_type: "mock".into(), + self.emit(crate::sandbox::SandboxEvent::CleanupCompleted { + provider: "mock".into(), duration_ms: 0, }); Ok(()) @@ -197,15 +197,15 @@ impl ExecutionEnvironment for MockExecutionEnvironment { } } -// --- MutableMockExecutionEnvironment --- +// --- MutableMockSandbox --- /// A mock execution environment with Mutex-protected files for tests that need /// write operations to be visible to subsequent reads (e.g., `apply_patch` tests). -pub struct MutableMockExecutionEnvironment { +pub struct MutableMockSandbox { pub files: Mutex>, } -impl MutableMockExecutionEnvironment { +impl MutableMockSandbox { pub fn new(files: HashMap) -> Self { Self { files: Mutex::new(files), @@ -214,7 +214,7 @@ impl MutableMockExecutionEnvironment { } #[async_trait] -impl ExecutionEnvironment for MutableMockExecutionEnvironment { +impl Sandbox for MutableMockSandbox { async fn read_file( &self, path: &str, @@ -370,7 +370,7 @@ impl ProviderProfile for TestProfile { fn build_system_prompt( &self, - _env: &dyn ExecutionEnvironment, + _env: &dyn Sandbox, _env_context: &EnvContext, _project_docs: &[String], user_instructions: Option<&str>, @@ -508,7 +508,7 @@ pub async fn make_session(responses: Vec) -> Session { let provider = Arc::new(MockLlmProvider::new(responses)); let client = make_client(provider).await; let profile = Arc::new(TestProfile::new()); - let env = Arc::new(MockExecutionEnvironment::default()); + let env = Arc::new(MockSandbox::default()); Session::new(client, profile, env, SessionConfig::default()) } @@ -516,7 +516,7 @@ pub async fn make_session_with_tools(responses: Vec, registry: ToolReg let provider = Arc::new(MockLlmProvider::new(responses)); let client = make_client(provider).await; let profile = Arc::new(TestProfile::with_tools(registry)); - let env = Arc::new(MockExecutionEnvironment::default()); + let env = Arc::new(MockSandbox::default()); Session::new(client, profile, env, SessionConfig::default()) } @@ -524,7 +524,7 @@ pub async fn make_session_with_config(responses: Vec, config: SessionC let provider = Arc::new(MockLlmProvider::new(responses)); let client = make_client(provider).await; let profile = Arc::new(TestProfile::new()); - let env = Arc::new(MockExecutionEnvironment::default()); + let env = Arc::new(MockSandbox::default()); Session::new(client, profile, env, config) } @@ -536,7 +536,7 @@ pub async fn make_session_with_tools_and_config( let provider = Arc::new(MockLlmProvider::new(responses)); let client = make_client(provider).await; let profile = Arc::new(TestProfile::with_tools(registry)); - let env = Arc::new(MockExecutionEnvironment::default()); + let env = Arc::new(MockSandbox::default()); Session::new(client, profile, env, config) } diff --git a/crates/arc-agent/src/tool_execution.rs b/crates/arc-agent/src/tool_execution.rs index 7dbf81e87..23153a7e6 100644 --- a/crates/arc-agent/src/tool_execution.rs +++ b/crates/arc-agent/src/tool_execution.rs @@ -1,6 +1,6 @@ use crate::config::{SessionConfig, ToolApprovalFn}; use crate::event::EventEmitter; -use crate::execution_env::ExecutionEnvironment; +use crate::sandbox::Sandbox; use crate::tool_registry::ToolRegistry; use crate::truncation::truncate_tool_output; use crate::types::AgentEvent; @@ -14,7 +14,7 @@ pub async fn execute_tool_calls( tool_calls: &[arc_llm::types::ToolCall], parallel: bool, registry: &ToolRegistry, - env: Arc, + env: Arc, tool_approval: Option<&ToolApprovalFn>, cancel_token: &CancellationToken, config: &SessionConfig, @@ -52,7 +52,7 @@ pub async fn execute_tool_calls( async fn execute_tool_calls_sequential( tool_calls: &[arc_llm::types::ToolCall], registry: &ToolRegistry, - env: Arc, + env: Arc, tool_approval: Option<&ToolApprovalFn>, cancel_token: &CancellationToken, config: &SessionConfig, @@ -86,7 +86,7 @@ async fn execute_tool_calls_sequential( async fn execute_tool_calls_parallel( tool_calls: &[arc_llm::types::ToolCall], registry: &ToolRegistry, - env: Arc, + env: Arc, tool_approval: Option<&ToolApprovalFn>, cancel_token: &CancellationToken, config: &SessionConfig, @@ -129,7 +129,7 @@ async fn execute_tool_calls_parallel( pub async fn execute_and_emit_one_tool( tc: &arc_llm::types::ToolCall, registry: &ToolRegistry, - env: Arc, + env: Arc, tool_approval: Option<&ToolApprovalFn>, cancel_token: CancellationToken, config: &SessionConfig, @@ -154,7 +154,7 @@ pub async fn execute_and_emit_one_tool( async fn execute_and_emit_one_tool_with_lookup( tc: &arc_llm::types::ToolCall, registered_tool: Option<&crate::tool_registry::RegisteredTool>, - env: Arc, + env: Arc, tool_approval: Option<&ToolApprovalFn>, cancel_token: CancellationToken, config: &SessionConfig, @@ -207,7 +207,7 @@ async fn execute_one_tool( tool_name: &str, arguments: &serde_json::Value, registered_tool: Option<&crate::tool_registry::RegisteredTool>, - env: Arc, + env: Arc, tool_approval: Option<&ToolApprovalFn>, cancel_token: CancellationToken, ) -> ToolResult { diff --git a/crates/arc-agent/src/tool_registry.rs b/crates/arc-agent/src/tool_registry.rs index 0769c3547..6aedd6d56 100644 --- a/crates/arc-agent/src/tool_registry.rs +++ b/crates/arc-agent/src/tool_registry.rs @@ -1,4 +1,4 @@ -use crate::execution_env::ExecutionEnvironment; +use crate::sandbox::Sandbox; use arc_llm::types::ToolDefinition; use std::collections::HashMap; use std::future::Future; @@ -7,7 +7,7 @@ use std::sync::Arc; use tokio_util::sync::CancellationToken; pub struct ToolContext { - pub env: Arc, + pub env: Arc, pub cancel: CancellationToken, } @@ -171,10 +171,10 @@ mod tests { let tool = registry.get("echo").unwrap(); use super::ToolContext; - use crate::execution_env::ExecutionEnvironment; - use crate::test_support::MockExecutionEnvironment; + use crate::sandbox::Sandbox; + use crate::test_support::MockSandbox; - let env: Arc = Arc::new(MockExecutionEnvironment::default()); + let env: Arc = Arc::new(MockSandbox::default()); let ctx = ToolContext { env, cancel: CancellationToken::new(), diff --git a/crates/arc-agent/src/tools.rs b/crates/arc-agent/src/tools.rs index c673d66b1..d46015504 100644 --- a/crates/arc-agent/src/tools.rs +++ b/crates/arc-agent/src/tools.rs @@ -1,5 +1,5 @@ use crate::config::SessionConfig; -use crate::execution_env::GrepOptions; +use crate::sandbox::GrepOptions; use crate::tool_registry::RegisteredTool; use arc_llm::client::Client; use arc_llm::provider::ModelId; @@ -590,8 +590,8 @@ pub(crate) fn make_web_fetch_tool(summarizer: Option) -> Reg #[cfg(test)] mod tests { use super::*; - use crate::execution_env::*; - use crate::test_support::MockExecutionEnvironment; + use crate::sandbox::*; + use crate::test_support::MockSandbox; use crate::tool_registry::ToolContext; use std::collections::HashMap; use tokio_util::sync::CancellationToken; @@ -601,7 +601,7 @@ mod tests { let tool = make_read_file_tool(); let mut files = HashMap::new(); files.insert("/test.txt".into(), " 1 | hello\n 2 | world".into()); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { files, apply_read_offset_limit: true, ..Default::default() @@ -625,7 +625,7 @@ mod tests { "/test.txt".into(), " 1 | line1\n 2 | line2\n 3 | line3\n 4 | line4".into(), ); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { files, apply_read_offset_limit: true, ..Default::default() @@ -644,8 +644,8 @@ mod tests { #[tokio::test] async fn write_file_calls_env() { let tool = make_write_file_tool(); - let env = Arc::new(MockExecutionEnvironment::default()); - let env_clone: Arc = env.clone(); + let env = Arc::new(MockSandbox::default()); + let env_clone: Arc = env.clone(); let result = (tool.executor)( serde_json::json!({"file_path": "/out.txt", "content": "hello"}), ToolContext { @@ -666,11 +666,11 @@ mod tests { let tool = make_edit_file_tool(); let mut files = HashMap::new(); files.insert("/f.txt".into(), " 1 | hello world".into()); - let env = Arc::new(MockExecutionEnvironment { + let env = Arc::new(MockSandbox { files, ..Default::default() }); - let env_clone: Arc = env.clone(); + let env_clone: Arc = env.clone(); let result = (tool.executor)( serde_json::json!({ "file_path": "/f.txt", @@ -694,7 +694,7 @@ mod tests { let tool = make_edit_file_tool(); let mut files = HashMap::new(); files.insert("/f.txt".into(), " 1 | hello world".into()); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { files, ..Default::default() }); @@ -718,7 +718,7 @@ mod tests { let tool = make_edit_file_tool(); let mut files = HashMap::new(); files.insert("/f.txt".into(), " 1 | aa bb aa".into()); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { files, ..Default::default() }); @@ -744,11 +744,11 @@ mod tests { let tool = make_edit_file_tool(); let mut files = HashMap::new(); files.insert("/f.txt".into(), " 1 | aa bb aa".into()); - let env = Arc::new(MockExecutionEnvironment { + let env = Arc::new(MockSandbox { files, ..Default::default() }); - let env_clone: Arc = env.clone(); + let env_clone: Arc = env.clone(); let result = (tool.executor)( serde_json::json!({ "file_path": "/f.txt", @@ -771,7 +771,7 @@ mod tests { #[tokio::test] async fn shell_basic_command() { let tool = make_shell_tool(); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { stdout: "hello".into(), stderr: String::new(), @@ -797,8 +797,8 @@ mod tests { #[tokio::test] async fn shell_with_timeout() { let tool = make_shell_tool(); - let env = Arc::new(MockExecutionEnvironment::default()); - let env_clone: Arc = env.clone(); + let env = Arc::new(MockSandbox::default()); + let env_clone: Arc = env.clone(); let _result = (tool.executor)( serde_json::json!({"command": "sleep 1", "timeout_ms": 5000}), ToolContext { @@ -813,7 +813,7 @@ mod tests { #[tokio::test] async fn shell_nonzero_exit_code() { let tool = make_shell_tool(); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { stdout: String::new(), stderr: "error".into(), @@ -839,7 +839,7 @@ mod tests { #[tokio::test] async fn shell_timeout_output() { let tool = make_shell_tool(); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { stdout: String::new(), stderr: String::new(), @@ -864,7 +864,7 @@ mod tests { #[tokio::test] async fn grep_basic() { let tool = make_grep_tool(); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { grep_results: vec![ "src/main.rs:10:fn main()".into(), "src/lib.rs:5:pub fn".into(), @@ -887,7 +887,7 @@ mod tests { #[tokio::test] async fn glob_basic() { let tool = make_glob_tool(); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { glob_results: vec!["src/main.rs".into(), "src/lib.rs".into()], ..Default::default() }); @@ -907,7 +907,7 @@ mod tests { #[tokio::test] async fn web_search_missing_api_key_returns_error() { let tool = make_web_search_tool_with_api_key(None); - let env: Arc = Arc::new(MockExecutionEnvironment::default()); + let env: Arc = Arc::new(MockSandbox::default()); let result = (tool.executor)( serde_json::json!({"query": "test"}), ToolContext { @@ -926,7 +926,7 @@ mod tests { #[tokio::test] async fn web_search_missing_query_returns_error() { let tool = make_web_search_tool_with_api_key(Some("fake-key".into())); - let env: Arc = Arc::new(MockExecutionEnvironment::default()); + let env: Arc = Arc::new(MockSandbox::default()); let result = (tool.executor)( serde_json::json!({}), ToolContext { @@ -968,7 +968,7 @@ mod tests { #[tokio::test] async fn web_fetch_builds_curl_command() { let tool = make_web_fetch_tool(None); - let env = Arc::new(MockExecutionEnvironment { + let env = Arc::new(MockSandbox { exec_result: ExecResult { stdout: "

hello

".into(), stderr: String::new(), @@ -978,7 +978,7 @@ mod tests { }, ..Default::default() }); - let env_clone: Arc = env.clone(); + let env_clone: Arc = env.clone(); let result = (tool.executor)( serde_json::json!({"url": "https://example.com"}), ToolContext { @@ -1014,7 +1014,7 @@ mod tests { #[tokio::test] async fn web_fetch_rejects_non_http_url() { let tool = make_web_fetch_tool(None); - let env: Arc = Arc::new(MockExecutionEnvironment::default()); + let env: Arc = Arc::new(MockSandbox::default()); let result = (tool.executor)( serde_json::json!({"url": "ftp://example.com/file"}), ToolContext { @@ -1033,8 +1033,8 @@ mod tests { #[tokio::test] async fn web_fetch_timeout_flows_through() { let tool = make_web_fetch_tool(None); - let env = Arc::new(MockExecutionEnvironment::default()); - let env_clone: Arc = env.clone(); + let env = Arc::new(MockSandbox::default()); + let env_clone: Arc = env.clone(); let _result = (tool.executor)( serde_json::json!({"url": "https://example.com", "timeout_ms": 15000}), ToolContext { @@ -1054,8 +1054,8 @@ mod tests { #[tokio::test] async fn web_fetch_timeout_capped_at_60s() { let tool = make_web_fetch_tool(None); - let env = Arc::new(MockExecutionEnvironment::default()); - let env_clone: Arc = env.clone(); + let env = Arc::new(MockSandbox::default()); + let env_clone: Arc = env.clone(); let _result = (tool.executor)( serde_json::json!({"url": "https://example.com", "timeout_ms": 120000}), ToolContext { @@ -1076,7 +1076,7 @@ mod tests { async fn web_fetch_truncates_large_output() { let large_content = "x".repeat(150 * 1024); let tool = make_web_fetch_tool(None); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { stdout: large_content, stderr: String::new(), @@ -1102,7 +1102,7 @@ mod tests { #[tokio::test] async fn web_fetch_returns_error_on_nonzero_exit() { let tool = make_web_fetch_tool(None); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { stdout: String::new(), stderr: "curl: (6) Could not resolve host".into(), @@ -1145,7 +1145,7 @@ mod tests { }; let tool = make_web_fetch_tool(Some(summarizer)); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { stdout: "

Lots of content about Rust...

".into(), stderr: String::new(), @@ -1173,7 +1173,7 @@ mod tests { #[tokio::test] async fn web_fetch_prompt_without_summarizer_returns_content_with_note() { let tool = make_web_fetch_tool(None); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { stdout: "

Rust is a systems programming language.

" .into(), @@ -1236,7 +1236,7 @@ mod tests { }; let tool = make_web_fetch_tool(Some(summarizer)); - let env: Arc = Arc::new(MockExecutionEnvironment { + let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { stdout: "

Page content

".into(), stderr: String::new(), @@ -1295,7 +1295,7 @@ mod tests { let api_key = std::env::var("BRAVE_SEARCH_API_KEY") .expect("BRAVE_SEARCH_API_KEY must be set to run this test"); let tool = make_web_search_tool_with_api_key(Some(api_key)); - let env: Arc = Arc::new(MockExecutionEnvironment::default()); + let env: Arc = Arc::new(MockSandbox::default()); let result = (tool.executor)( serde_json::json!({"query": "rust programming language"}), ToolContext { diff --git a/crates/arc-agent/src/v4a_patch.rs b/crates/arc-agent/src/v4a_patch.rs index 60b5fe37e..2d85ec2ff 100644 --- a/crates/arc-agent/src/v4a_patch.rs +++ b/crates/arc-agent/src/v4a_patch.rs @@ -1,4 +1,4 @@ -use crate::execution_env::ExecutionEnvironment; +use crate::sandbox::Sandbox; use crate::tool_registry::RegisteredTool; use arc_llm::types::ToolDefinition; use std::sync::Arc; @@ -130,7 +130,7 @@ pub fn parse_v4a_patch(text: &str) -> Result, String> { /// Returns an error if any file operation fails. pub async fn apply_patch_operations( ops: &[PatchOperation], - env: &dyn ExecutionEnvironment, + env: &dyn Sandbox, ) -> Result { let mut results = Vec::new(); @@ -244,7 +244,7 @@ pub fn make_apply_patch_tool() -> RegisteredTool { #[cfg(test)] mod tests { use super::*; - use crate::test_support::MutableMockExecutionEnvironment; + use crate::test_support::MutableMockSandbox; use std::collections::HashMap; #[test] @@ -338,7 +338,7 @@ mod tests { #[tokio::test] async fn apply_patch_add_file() { - let env = MutableMockExecutionEnvironment::new(HashMap::new()); + let env = MutableMockSandbox::new(HashMap::new()); let ops = vec![PatchOperation::Add { path: "src/new.rs".into(), content: "fn new() {}".into(), @@ -358,7 +358,7 @@ mod tests { "src/lib.rs".to_string(), "fn hello() {\n println!(\"old\");\n}".to_string(), ); - let env = MutableMockExecutionEnvironment::new(files); + let env = MutableMockSandbox::new(files); let ops = vec![PatchOperation::Update { path: "src/lib.rs".into(), diff --git a/crates/arc-agent/tests/parity_matrix.rs b/crates/arc-agent/tests/parity_matrix.rs index a294b9fe3..b53a74dc0 100644 --- a/crates/arc-agent/tests/parity_matrix.rs +++ b/crates/arc-agent/tests/parity_matrix.rs @@ -2,7 +2,7 @@ use std::path::Path; use std::sync::Arc; use arc_agent::{ - AnthropicProfile, GeminiProfile, LocalExecutionEnvironment, OpenAiProfile, ProviderProfile, + AnthropicProfile, GeminiProfile, LocalSandbox, OpenAiProfile, ProviderProfile, Session, SessionConfig, SubAgentManager, WebFetchSummarizer, }; use arc_llm::client::Client; @@ -43,7 +43,7 @@ async fn make_session(provider: Provider, model: &str, cwd: &Path) -> Session { dotenvy::dotenv().ok(); let client = Client::from_env().await.expect("Client::from_env failed"); let mut profile = build_profile(provider, model, &client); - let env = Arc::new(LocalExecutionEnvironment::new(cwd.to_path_buf())); + let env = Arc::new(LocalSandbox::new(cwd.to_path_buf())); // Register subagent tools so spawn_agent / wait / send_input / close_agent are available let manager = Arc::new(tokio::sync::Mutex::new(SubAgentManager::new(3))); @@ -72,7 +72,7 @@ async fn make_session(provider: Provider, model: &str, cwd: &Path) -> Session { } } }; - let sub_env = Arc::new(LocalExecutionEnvironment::new(factory_cwd.clone())); + let sub_env = Arc::new(LocalSandbox::new(factory_cwd.clone())); Session::new( factory_client.clone(), sub_profile, @@ -99,7 +99,7 @@ async fn make_session_with_config( dotenvy::dotenv().ok(); let client = Client::from_env().await.expect("Client::from_env failed"); let profile: Arc = Arc::from(build_profile(provider, model, &client)); - let env = Arc::new(LocalExecutionEnvironment::new(cwd.to_path_buf())); + let env = Arc::new(LocalSandbox::new(cwd.to_path_buf())); Session::new(client, profile, env, config) } diff --git a/crates/arc-api/src/serve.rs b/crates/arc-api/src/serve.rs index 630058243..f33298b05 100644 --- a/crates/arc-api/src/serve.rs +++ b/crates/arc-api/src/serve.rs @@ -9,7 +9,7 @@ use clap::Args; use crate::server::{build_router, create_app_state_with_options}; use arc_workflows::cli::backend::AgentApiBackend; -use arc_workflows::cli::ExecutionEnvKind; +use arc_workflows::cli::SandboxProvider; use arc_workflows::handler::default_registry; use arc_workflows::interviewer::Interviewer; @@ -37,7 +37,7 @@ pub struct ServeArgs { /// Execution environment for agent tools #[arg(long, value_enum)] - pub execution_env: Option, + pub sandbox: Option, /// Serve static demo data (disables auth, read-only) #[arg(long)] diff --git a/crates/arc-api/src/server.rs b/crates/arc-api/src/server.rs index bbcc0802b..6ce0cc4ef 100644 --- a/crates/arc-api/src/server.rs +++ b/crates/arc-api/src/server.rs @@ -14,7 +14,7 @@ use tokio_stream::StreamExt; use tracing::{error, info}; -use arc_agent::LocalExecutionEnvironment; +use arc_agent::LocalSandbox; use crate::jwt_auth::{AuthMode, AuthenticatedService}; use arc_workflows::checkpoint::Checkpoint; @@ -221,13 +221,13 @@ async fn start_run( let registry = (state.registry_factory)(Arc::clone(&interviewer) as Arc); let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); - let execution_env: Arc = - Arc::new(LocalExecutionEnvironment::new(cwd)); + let sandbox: Arc = + Arc::new(LocalSandbox::new(cwd)); let engine = WorkflowRunEngine::with_interviewer( registry, Arc::new(emitter), Arc::clone(&interviewer) as Arc, - execution_env, + sandbox, ); { diff --git a/crates/arc-workflows/src/artifact.rs b/crates/arc-workflows/src/artifact.rs index 34850eb3f..1ec755c28 100644 --- a/crates/arc-workflows/src/artifact.rs +++ b/crates/arc-workflows/src/artifact.rs @@ -6,7 +6,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use arc_agent::ExecutionEnvironment; +use arc_agent::Sandbox; use crate::error::{ArcError, Result}; @@ -251,7 +251,7 @@ pub fn format_artifact_reference(path: &str) -> String { format!("See: {path}") } -/// Sync artifact files to a remote execution environment. +/// Sync artifact files to a remote sandbox. /// /// For each `file://` pointer in `updates`, checks whether the file is accessible /// in `env`. If not, reads the local file and uploads it via `env.write_file`, @@ -263,7 +263,7 @@ pub fn format_artifact_reference(path: &str) -> String { /// Returns an error if reading a local artifact or writing to the remote env fails. pub async fn sync_artifacts_to_env( updates: &mut HashMap, - env: &dyn ExecutionEnvironment, + env: &dyn Sandbox, ) -> Result<()> { for value in updates.values_mut() { let local_path = match artifact_path(value) { @@ -512,7 +512,7 @@ mod tests { } #[async_trait::async_trait] - impl ExecutionEnvironment for TestSyncEnv { + impl Sandbox for TestSyncEnv { async fn read_file( &self, _path: &str, diff --git a/crates/arc-workflows/src/cli/backend.rs b/crates/arc-workflows/src/cli/backend.rs index 78be7c34a..43f8d0a06 100644 --- a/crates/arc-workflows/src/cli/backend.rs +++ b/crates/arc-workflows/src/cli/backend.rs @@ -5,7 +5,7 @@ use async_trait::async_trait; use arc_agent::{ subagent::{SessionFactory, SubAgentManager}, - AgentEvent, AnthropicProfile, ExecutionEnvironment, GeminiProfile, OpenAiProfile, + AgentEvent, AnthropicProfile, Sandbox, GeminiProfile, OpenAiProfile, ProviderProfile, Session, SessionConfig, Turn, }; use arc_llm::client::Client; @@ -45,7 +45,7 @@ impl AgentApiBackend { async fn create_session( &self, node: &Node, - execution_env: &Arc, + sandbox: &Arc, ) -> Result { let client = Client::from_env() .await @@ -68,7 +68,7 @@ impl AgentApiBackend { let factory_client = client.clone(); let factory_provider = self.provider; let factory_model = self.model.clone(); - let factory_env = Arc::clone(execution_env); + let factory_env = Arc::clone(sandbox); let factory: SessionFactory = Arc::new(move || { let child_profile: Arc = match factory_provider { Provider::OpenAi => Arc::new(OpenAiProfile::new(&factory_model)), @@ -89,7 +89,7 @@ impl AgentApiBackend { profile.register_subagent_tools(manager, factory, 0); let profile: Arc = Arc::from(profile); - let session = Session::new(client, profile, Arc::clone(execution_env), config); + let session = Session::new(client, profile, Arc::clone(sandbox), config); // Wire subagent event callback to parent session's emitter manager_for_callback @@ -196,7 +196,7 @@ impl CodergenBackend for AgentApiBackend { thread_id: Option<&str>, emitter: &Arc, stage_dir: &std::path::Path, - execution_env: &Arc, + sandbox: &Arc, ) -> Result { let fidelity = context.get_string("internal.fidelity", ""); let reuse_key = if fidelity == "full" { @@ -211,10 +211,10 @@ impl CodergenBackend for AgentApiBackend { if let Some(s) = existing { (s, true) } else { - (self.create_session(node, execution_env).await?, false) + (self.create_session(node, sandbox).await?, false) } } else { - (self.create_session(node, execution_env).await?, false) + (self.create_session(node, sandbox).await?, false) }; // File change tracking: shared between spawned task and main fn. diff --git a/crates/arc-workflows/src/cli/cli_backend.rs b/crates/arc-workflows/src/cli/cli_backend.rs index 79d34ea54..6325bc241 100644 --- a/crates/arc-workflows/src/cli/cli_backend.rs +++ b/crates/arc-workflows/src/cli/cli_backend.rs @@ -1,7 +1,7 @@ use std::path::Path; use std::sync::Arc; -use arc_agent::ExecutionEnvironment; +use arc_agent::Sandbox; use arc_llm::provider::Provider; use async_trait::async_trait; @@ -229,15 +229,15 @@ impl AgentCliBackend { /// Detect changed files by comparing git state before and after the CLI run. async fn detect_changed_files( &self, - execution_env: &Arc, + sandbox: &Arc, ) -> Vec { // Get unstaged changes - let diff_result = execution_env + let diff_result = sandbox .exec_command("git diff --name-only", 30_000, None, None, None) .await; // Get untracked files - let untracked_result = execution_env + let untracked_result = sandbox .exec_command( "git ls-files --others --exclude-standard", 30_000, @@ -289,14 +289,14 @@ impl CodergenBackend for AgentCliBackend { _thread_id: Option<&str>, _emitter: &Arc, stage_dir: &Path, - execution_env: &Arc, + sandbox: &Arc, ) -> Result { // 1. Snapshot git state before the CLI run - let files_before = self.detect_changed_files(execution_env).await; + let files_before = self.detect_changed_files(sandbox).await; // 2. Write prompt to temp file let prompt_path = "/tmp/arc_cli_prompt.txt"; - execution_env + sandbox .write_file(prompt_path, prompt) .await .map_err(|e| ArcError::Handler(format!("Failed to write prompt file: {e}")))?; @@ -320,7 +320,7 @@ impl CodergenBackend for AgentCliBackend { let _ = tokio::fs::write(stage_dir.join("provider_used.json"), json).await; } - let result = execution_env + let result = sandbox .exec_command(&command, 600_000, None, None, None) .await .map_err(|e| ArcError::Handler(format!("CLI command failed: {e}")))?; @@ -347,7 +347,7 @@ impl CodergenBackend for AgentCliBackend { .ok_or_else(|| ArcError::Handler("Failed to parse CLI output".to_string()))?; // 5. Detect changed files - let files_after = self.detect_changed_files(execution_env).await; + let files_after = self.detect_changed_files(sandbox).await; let files_touched: Vec = files_after .into_iter() .filter(|f| !files_before.contains(f)) @@ -415,7 +415,7 @@ impl CodergenBackend for BackendRouter { thread_id: Option<&str>, emitter: &Arc, stage_dir: &Path, - execution_env: &Arc, + sandbox: &Arc, ) -> Result { if self.should_use_cli(node) { self.cli_backend @@ -426,7 +426,7 @@ impl CodergenBackend for BackendRouter { thread_id, emitter, stage_dir, - execution_env, + sandbox, ) .await } else { @@ -438,7 +438,7 @@ impl CodergenBackend for BackendRouter { thread_id, emitter, stage_dir, - execution_env, + sandbox, ) .await } @@ -655,7 +655,7 @@ mod tests { _thread_id: Option<&str>, _emitter: &Arc, _stage_dir: &Path, - _execution_env: &Arc, + _sandbox: &Arc, ) -> Result { Ok(CodergenResult::Text { text: "stub".to_string(), diff --git a/crates/arc-workflows/src/cli/mod.rs b/crates/arc-workflows/src/cli/mod.rs index 2dc555481..5ec55131b 100644 --- a/crates/arc-workflows/src/cli/mod.rs +++ b/crates/arc-workflows/src/cli/mod.rs @@ -18,9 +18,9 @@ use crate::outcome::StageUsage; use crate::validation::{Diagnostic, Severity}; use arc_agent::AgentEvent; -/// Execution environment for agent tool operations. +/// Sandbox provider for agent tool operations. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)] -pub enum ExecutionEnvKind { +pub enum SandboxProvider { /// Run tools on the local host (default) #[default] Local, @@ -30,7 +30,7 @@ pub enum ExecutionEnvKind { Daytona, } -impl fmt::Display for ExecutionEnvKind { +impl fmt::Display for SandboxProvider { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Local => write!(f, "local"), @@ -40,7 +40,7 @@ impl fmt::Display for ExecutionEnvKind { } } -impl FromStr for ExecutionEnvKind { +impl FromStr for SandboxProvider { type Err = String; fn from_str(s: &str) -> Result { @@ -48,7 +48,7 @@ impl FromStr for ExecutionEnvKind { "local" => Ok(Self::Local), "docker" => Ok(Self::Docker), "daytona" => Ok(Self::Daytona), - other => Err(format!("unknown execution environment: {other}")), + other => Err(format!("unknown sandbox provider: {other}")), } } } @@ -112,7 +112,7 @@ pub struct RunArgs { /// Execution environment for agent tools #[arg(long, value_enum)] - pub execution_env: Option, + pub sandbox: Option, /// Attach a label to this run (repeatable, format: KEY=VALUE) #[arg(long = "label", value_name = "KEY=VALUE")] @@ -511,27 +511,27 @@ pub fn format_event_summary(event: &WorkflowRunEvent, styles: &Styles) -> String } => { format!("[SUBGRAPH_COMPLETED] node={node_id} steps={steps_executed} status={status} duration={duration_ms}ms") } - WorkflowRunEvent::ExecutionEnv { event } => { - use arc_agent::ExecutionEnvEvent; + WorkflowRunEvent::Sandbox { event } => { + use arc_agent::SandboxEvent; match event { - ExecutionEnvEvent::Initializing { env_type } => format!("[EXEC_ENV_INITIALIZING] env_type={env_type}"), - ExecutionEnvEvent::Ready { env_type, duration_ms } => format!("[EXEC_ENV_READY] env_type={env_type} duration={duration_ms}ms"), - ExecutionEnvEvent::InitializeFailed { env_type, error, duration_ms } => format!("[EXEC_ENV_INIT_FAILED] env_type={env_type} error=\"{error}\" duration={duration_ms}ms"), - ExecutionEnvEvent::CleanupStarted { env_type } => format!("[EXEC_ENV_CLEANUP_STARTED] env_type={env_type}"), - ExecutionEnvEvent::CleanupCompleted { env_type, duration_ms } => format!("[EXEC_ENV_CLEANUP_COMPLETED] env_type={env_type} duration={duration_ms}ms"), - ExecutionEnvEvent::CleanupFailed { env_type, error } => format!("[EXEC_ENV_CLEANUP_FAILED] env_type={env_type} error=\"{error}\""), - ExecutionEnvEvent::ImagePulling { image } => format!("[EXEC_ENV_IMAGE_PULLING] image={image}"), - ExecutionEnvEvent::ImagePulled { image, duration_ms } => format!("[EXEC_ENV_IMAGE_PULLED] image={image} duration={duration_ms}ms"), - ExecutionEnvEvent::SnapshotEnsuring { name } => format!("[EXEC_ENV_SNAPSHOT_ENSURING] name={name}"), - ExecutionEnvEvent::SnapshotCreating { name } => format!("[EXEC_ENV_SNAPSHOT_CREATING] name={name}"), - ExecutionEnvEvent::SnapshotReady { name, duration_ms } => format!("[EXEC_ENV_SNAPSHOT_READY] name={name} duration={duration_ms}ms"), - ExecutionEnvEvent::SnapshotFailed { name, error } => format!("[EXEC_ENV_SNAPSHOT_FAILED] name={name} error=\"{error}\""), - ExecutionEnvEvent::GitCloneStarted { url, branch } => { + SandboxEvent::Initializing { provider } => format!("[SANDBOX_INITIALIZING] provider={provider}"), + SandboxEvent::Ready { provider, duration_ms } => format!("[SANDBOX_READY] provider={provider} duration={duration_ms}ms"), + SandboxEvent::InitializeFailed { provider, error, duration_ms } => format!("[SANDBOX_INIT_FAILED] provider={provider} error=\"{error}\" duration={duration_ms}ms"), + SandboxEvent::CleanupStarted { provider } => format!("[SANDBOX_CLEANUP_STARTED] provider={provider}"), + SandboxEvent::CleanupCompleted { provider, duration_ms } => format!("[SANDBOX_CLEANUP_COMPLETED] provider={provider} duration={duration_ms}ms"), + SandboxEvent::CleanupFailed { provider, error } => format!("[SANDBOX_CLEANUP_FAILED] provider={provider} error=\"{error}\""), + SandboxEvent::SnapshotPulling { name } => format!("[SANDBOX_SNAPSHOT_PULLING] name={name}"), + SandboxEvent::SnapshotPulled { name, duration_ms } => format!("[SANDBOX_SNAPSHOT_PULLED] name={name} duration={duration_ms}ms"), + SandboxEvent::SnapshotEnsuring { name } => format!("[SANDBOX_SNAPSHOT_ENSURING] name={name}"), + SandboxEvent::SnapshotCreating { name } => format!("[SANDBOX_SNAPSHOT_CREATING] name={name}"), + SandboxEvent::SnapshotReady { name, duration_ms } => format!("[SANDBOX_SNAPSHOT_READY] name={name} duration={duration_ms}ms"), + SandboxEvent::SnapshotFailed { name, error } => format!("[SANDBOX_SNAPSHOT_FAILED] name={name} error=\"{error}\""), + SandboxEvent::GitCloneStarted { url, branch } => { let branch_str = branch.as_deref().unwrap_or("(default)"); - format!("[EXEC_ENV_GIT_CLONE_STARTED] url={url} branch={branch_str}") + format!("[SANDBOX_GIT_CLONE_STARTED] url={url} branch={branch_str}") } - ExecutionEnvEvent::GitCloneCompleted { url, duration_ms } => format!("[EXEC_ENV_GIT_CLONE_COMPLETED] url={url} duration={duration_ms}ms"), - ExecutionEnvEvent::GitCloneFailed { url, error } => format!("[EXEC_ENV_GIT_CLONE_FAILED] url={url} error=\"{error}\""), + SandboxEvent::GitCloneCompleted { url, duration_ms } => format!("[SANDBOX_GIT_CLONE_COMPLETED] url={url} duration={duration_ms}ms"), + SandboxEvent::GitCloneFailed { url, error } => format!("[SANDBOX_GIT_CLONE_FAILED] url={url} error=\"{error}\""), } } WorkflowRunEvent::SetupStarted { command_count } => { @@ -846,54 +846,54 @@ pub fn format_event_detail(event: &WorkflowRunEvent, styles: &Styles) -> String } => { format!("{d}── SUBGRAPH_COMPLETED ───────────────────────{r}\n {d}node_id:{r} {node_id}\n {d}steps_executed:{r} {steps_executed}\n {d}status:{r} {status}\n {d}duration_ms:{r} {duration_ms}\n") } - WorkflowRunEvent::ExecutionEnv { event } => { - use arc_agent::ExecutionEnvEvent; + WorkflowRunEvent::Sandbox { event } => { + use arc_agent::SandboxEvent; match event { - ExecutionEnvEvent::Initializing { env_type } => { - format!("{d}── EXEC_ENV_INITIALIZING ────────────────────{r}\n {d}env_type:{r} {env_type}\n") + SandboxEvent::Initializing { provider } => { + format!("{d}── SANDBOX_INITIALIZING ────────────────────{r}\n {d}provider:{r} {provider}\n") } - ExecutionEnvEvent::Ready { env_type, duration_ms } => { - format!("{d}── EXEC_ENV_READY ───────────────────────────{r}\n {d}env_type:{r} {env_type}\n {d}duration_ms:{r} {duration_ms}\n") + SandboxEvent::Ready { provider, duration_ms } => { + format!("{d}── SANDBOX_READY ───────────────────────────{r}\n {d}provider:{r} {provider}\n {d}duration_ms:{r} {duration_ms}\n") } - ExecutionEnvEvent::InitializeFailed { env_type, error, duration_ms } => { - format!("{d}── EXEC_ENV_INIT_FAILED ─────────────────────{r}\n {d}env_type:{r} {env_type}\n {d}error:{r} {error}\n {d}duration_ms:{r} {duration_ms}\n") + SandboxEvent::InitializeFailed { provider, error, duration_ms } => { + format!("{d}── SANDBOX_INIT_FAILED ─────────────────────{r}\n {d}provider:{r} {provider}\n {d}error:{r} {error}\n {d}duration_ms:{r} {duration_ms}\n") } - ExecutionEnvEvent::CleanupStarted { env_type } => { - format!("{d}── EXEC_ENV_CLEANUP_STARTED ─────────────────{r}\n {d}env_type:{r} {env_type}\n") + SandboxEvent::CleanupStarted { provider } => { + format!("{d}── SANDBOX_CLEANUP_STARTED ─────────────────{r}\n {d}provider:{r} {provider}\n") } - ExecutionEnvEvent::CleanupCompleted { env_type, duration_ms } => { - format!("{d}── EXEC_ENV_CLEANUP_COMPLETED ───────────────{r}\n {d}env_type:{r} {env_type}\n {d}duration_ms:{r} {duration_ms}\n") + SandboxEvent::CleanupCompleted { provider, duration_ms } => { + format!("{d}── SANDBOX_CLEANUP_COMPLETED ───────────────{r}\n {d}provider:{r} {provider}\n {d}duration_ms:{r} {duration_ms}\n") } - ExecutionEnvEvent::CleanupFailed { env_type, error } => { - format!("{d}── EXEC_ENV_CLEANUP_FAILED ──────────────────{r}\n {d}env_type:{r} {env_type}\n {d}error:{r} {error}\n") + SandboxEvent::CleanupFailed { provider, error } => { + format!("{d}── SANDBOX_CLEANUP_FAILED ──────────────────{r}\n {d}provider:{r} {provider}\n {d}error:{r} {error}\n") } - ExecutionEnvEvent::ImagePulling { image } => { - format!("{d}── EXEC_ENV_IMAGE_PULLING ───────────────────{r}\n {d}image:{r} {image}\n") + SandboxEvent::SnapshotPulling { name } => { + format!("{d}── SANDBOX_SNAPSHOT_PULLING ───────────────────{r}\n {d}image:{r} {name}\n") } - ExecutionEnvEvent::ImagePulled { image, duration_ms } => { - format!("{d}── EXEC_ENV_IMAGE_PULLED ────────────────────{r}\n {d}image:{r} {image}\n {d}duration_ms:{r} {duration_ms}\n") + SandboxEvent::SnapshotPulled { name, duration_ms } => { + format!("{d}── SANDBOX_SNAPSHOT_PULLED ────────────────────{r}\n {d}image:{r} {name}\n {d}duration_ms:{r} {duration_ms}\n") } - ExecutionEnvEvent::SnapshotEnsuring { name } => { - format!("{d}── EXEC_ENV_SNAPSHOT_ENSURING ───────────────{r}\n {d}name:{r} {name}\n") + SandboxEvent::SnapshotEnsuring { name } => { + format!("{d}── SANDBOX_SNAPSHOT_ENSURING ───────────────{r}\n {d}name:{r} {name}\n") } - ExecutionEnvEvent::SnapshotCreating { name } => { - format!("{d}── EXEC_ENV_SNAPSHOT_CREATING ───────────────{r}\n {d}name:{r} {name}\n") + SandboxEvent::SnapshotCreating { name } => { + format!("{d}── SANDBOX_SNAPSHOT_CREATING ───────────────{r}\n {d}name:{r} {name}\n") } - ExecutionEnvEvent::SnapshotReady { name, duration_ms } => { - format!("{d}── EXEC_ENV_SNAPSHOT_READY ──────────────────{r}\n {d}name:{r} {name}\n {d}duration_ms:{r} {duration_ms}\n") + SandboxEvent::SnapshotReady { name, duration_ms } => { + format!("{d}── SANDBOX_SNAPSHOT_READY ──────────────────{r}\n {d}name:{r} {name}\n {d}duration_ms:{r} {duration_ms}\n") } - ExecutionEnvEvent::SnapshotFailed { name, error } => { - format!("{d}── EXEC_ENV_SNAPSHOT_FAILED ─────────────────{r}\n {d}name:{r} {name}\n {d}error:{r} {error}\n") + SandboxEvent::SnapshotFailed { name, error } => { + format!("{d}── SANDBOX_SNAPSHOT_FAILED ─────────────────{r}\n {d}name:{r} {name}\n {d}error:{r} {error}\n") } - ExecutionEnvEvent::GitCloneStarted { url, branch } => { + SandboxEvent::GitCloneStarted { url, branch } => { let branch_str = branch.as_deref().unwrap_or("(default)"); - format!("{d}── EXEC_ENV_GIT_CLONE_STARTED ──────────────{r}\n {d}url:{r} {url}\n {d}branch:{r} {branch_str}\n") + format!("{d}── SANDBOX_GIT_CLONE_STARTED ──────────────{r}\n {d}url:{r} {url}\n {d}branch:{r} {branch_str}\n") } - ExecutionEnvEvent::GitCloneCompleted { url, duration_ms } => { - format!("{d}── EXEC_ENV_GIT_CLONE_COMPLETED ────────────{r}\n {d}url:{r} {url}\n {d}duration_ms:{r} {duration_ms}\n") + SandboxEvent::GitCloneCompleted { url, duration_ms } => { + format!("{d}── SANDBOX_GIT_CLONE_COMPLETED ────────────{r}\n {d}url:{r} {url}\n {d}duration_ms:{r} {duration_ms}\n") } - ExecutionEnvEvent::GitCloneFailed { url, error } => { - format!("{d}── EXEC_ENV_GIT_CLONE_FAILED ───────────────{r}\n {d}url:{r} {url}\n {d}error:{r} {error}\n") + SandboxEvent::GitCloneFailed { url, error } => { + format!("{d}── SANDBOX_GIT_CLONE_FAILED ───────────────{r}\n {d}url:{r} {url}\n {d}error:{r} {error}\n") } } } @@ -951,36 +951,36 @@ mod tests { use super::*; #[test] - fn execution_env_kind_default_is_local() { - assert_eq!(ExecutionEnvKind::default(), ExecutionEnvKind::Local); + fn sandbox_provider_default_is_local() { + assert_eq!(SandboxProvider::default(), SandboxProvider::Local); } #[test] - fn execution_env_kind_from_str() { + fn sandbox_provider_from_str() { assert_eq!( - "local".parse::().unwrap(), - ExecutionEnvKind::Local + "local".parse::().unwrap(), + SandboxProvider::Local ); assert_eq!( - "docker".parse::().unwrap(), - ExecutionEnvKind::Docker + "docker".parse::().unwrap(), + SandboxProvider::Docker ); assert_eq!( - "daytona".parse::().unwrap(), - ExecutionEnvKind::Daytona + "daytona".parse::().unwrap(), + SandboxProvider::Daytona ); assert_eq!( - "LOCAL".parse::().unwrap(), - ExecutionEnvKind::Local + "LOCAL".parse::().unwrap(), + SandboxProvider::Local ); - assert!("invalid".parse::().is_err()); + assert!("invalid".parse::().is_err()); } #[test] - fn execution_env_kind_display() { - assert_eq!(ExecutionEnvKind::Local.to_string(), "local"); - assert_eq!(ExecutionEnvKind::Docker.to_string(), "docker"); - assert_eq!(ExecutionEnvKind::Daytona.to_string(), "daytona"); + fn sandbox_provider_display() { + assert_eq!(SandboxProvider::Local.to_string(), "local"); + assert_eq!(SandboxProvider::Docker.to_string(), "docker"); + assert_eq!(SandboxProvider::Daytona.to_string(), "daytona"); } fn test_styles() -> &'static Styles { @@ -988,14 +988,14 @@ mod tests { } #[test] - fn format_summary_execution_env_initializing() { - let event = WorkflowRunEvent::ExecutionEnv { - event: arc_agent::ExecutionEnvEvent::Initializing { - env_type: "docker".into(), + fn format_summary_sandbox_initializing() { + let event = WorkflowRunEvent::Sandbox { + event: arc_agent::SandboxEvent::Initializing { + provider: "docker".into(), }, }; let s = format_event_summary(&event, test_styles()); - assert!(s.contains("[EXEC_ENV_INITIALIZING]")); + assert!(s.contains("[SANDBOX_INITIALIZING]")); assert!(s.contains("docker")); } @@ -1008,15 +1008,15 @@ mod tests { } #[test] - fn format_detail_execution_env_ready() { - let event = WorkflowRunEvent::ExecutionEnv { - event: arc_agent::ExecutionEnvEvent::Ready { - env_type: "local".into(), + fn format_detail_sandbox_ready() { + let event = WorkflowRunEvent::Sandbox { + event: arc_agent::SandboxEvent::Ready { + provider: "local".into(), duration_ms: 42, }, }; let s = format_event_detail(&event, test_styles()); - assert!(s.contains("EXEC_ENV_READY")); + assert!(s.contains("SANDBOX_READY")); assert!(s.contains("local")); assert!(s.contains("42")); } diff --git a/crates/arc-workflows/src/cli/run.rs b/crates/arc-workflows/src/cli/run.rs index 9fa2796ee..5e0fcc1ca 100644 --- a/crates/arc-workflows/src/cli/run.rs +++ b/crates/arc-workflows/src/cli/run.rs @@ -5,7 +5,7 @@ use std::time::Instant; use anyhow::bail; use arc_agent::{ - DockerConfig, DockerExecutionEnvironment, ExecutionEnvironment, LocalExecutionEnvironment, + DockerSandboxConfig, DockerSandbox, Sandbox, LocalSandbox, }; use arc_util::terminal::Styles; use chrono::{Local, Utc}; @@ -28,7 +28,7 @@ use super::cli_backend::{BackendRouter, AgentCliBackend}; use super::task_config; use super::{ compute_stage_cost, format_cost, format_duration_human, format_event_detail, - format_event_summary, format_tokens_human, print_diagnostics, read_dot_file, ExecutionEnvKind, + format_event_summary, format_tokens_human, print_diagnostics, read_dot_file, SandboxProvider, RunArgs, }; @@ -76,7 +76,7 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu } } - // Collect setup commands — they'll be run inside the execution environment + // Collect setup commands — they'll be run inside the sandbox let setup_commands: Vec = task_cfg .as_ref() .and_then(|c| c.setup.as_ref()) @@ -118,23 +118,23 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu // 2. Pre-flight: check git cleanliness before creating any files // (must happen before logs dir is created, which may be inside the repo) - let execution_env_kind_preview = { + let sandbox_provider_preview = { let toml_exec = task_cfg .as_ref() - .and_then(|c| c.execution.as_ref()) - .and_then(|e| e.environment.as_deref()) - .map(|s| s.parse::()) + .and_then(|c| c.sandbox.as_ref()) + .and_then(|e| e.provider.as_deref()) + .map(|s| s.parse::()) .transpose() .ok() .flatten(); - args.execution_env.or(toml_exec).unwrap_or_default() + args.sandbox.or(toml_exec).unwrap_or_default() }; let original_cwd = std::env::current_dir()?; - let git_clean = match execution_env_kind_preview { - ExecutionEnvKind::Local | ExecutionEnvKind::Docker => { + let git_clean = match sandbox_provider_preview { + SandboxProvider::Local | SandboxProvider::Docker => { crate::git::ensure_clean(&original_cwd).is_ok() } - ExecutionEnvKind::Daytona => false, + SandboxProvider::Daytona => false, }; // 3. Create logs directory @@ -284,17 +284,17 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu Arc::new(ConsoleInterviewer::new(styles)) }; - // 5. Resolve execution environment: CLI flag > TOML > default - let toml_execution_env = task_cfg + // 5. Resolve sandbox: CLI flag > TOML > default + let toml_sandbox = task_cfg .as_ref() - .and_then(|c| c.execution.as_ref()) - .and_then(|e| e.environment.as_deref()) - .map(|s| s.parse::()) + .and_then(|c| c.sandbox.as_ref()) + .and_then(|e| e.provider.as_deref()) + .map(|s| s.parse::()) .transpose() - .map_err(|e| anyhow::anyhow!("Invalid execution environment in TOML: {e}"))?; - let execution_env_kind = args - .execution_env - .or(toml_execution_env) + .map_err(|e| anyhow::anyhow!("Invalid sandbox in TOML: {e}"))?; + let sandbox_provider = args + .sandbox + .or(toml_sandbox) .unwrap_or_default(); // Set up git worktree for local execution (must happen before cwd is captured) @@ -319,75 +319,75 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); let daytona_config = task_cfg .as_ref() - .and_then(|c| c.execution.as_ref()) + .and_then(|c| c.sandbox.as_ref()) .and_then(|e| e.daytona.clone()); // Wrap emitter in Arc now so we can share it with exec env callbacks let emitter = Arc::new(emitter); - let execution_env: Arc = match execution_env_kind { - ExecutionEnvKind::Docker => { - let config = DockerConfig { + let sandbox: Arc = match sandbox_provider { + SandboxProvider::Docker => { + let config = DockerSandboxConfig { host_working_directory: cwd.to_string_lossy().to_string(), - ..DockerConfig::default() + ..DockerSandboxConfig::default() }; - let mut env = DockerExecutionEnvironment::new(config) + let mut env = DockerSandbox::new(config) .map_err(|e| anyhow::anyhow!("Failed to create Docker environment: {e}"))?; let emitter_cb = Arc::clone(&emitter); env.set_event_callback(Arc::new(move |event| { - emitter_cb.emit(&crate::event::WorkflowRunEvent::ExecutionEnv { event }); + emitter_cb.emit(&crate::event::WorkflowRunEvent::Sandbox { event }); })); Arc::new(env) } - ExecutionEnvKind::Daytona => { + SandboxProvider::Daytona => { let daytona_client = daytona_sdk::Client::new() .await .map_err(|e| anyhow::anyhow!("Failed to create Daytona client: {e}"))?; let config = daytona_config.clone().unwrap_or_default(); let mut env = - crate::daytona_env::DaytonaExecutionEnvironment::new(daytona_client, config); + crate::daytona_sandbox::DaytonaSandbox::new(daytona_client, config); let emitter_cb = Arc::clone(&emitter); env.set_event_callback(Arc::new(move |event| { - emitter_cb.emit(&crate::event::WorkflowRunEvent::ExecutionEnv { event }); + emitter_cb.emit(&crate::event::WorkflowRunEvent::Sandbox { event }); })); Arc::new(env) } - ExecutionEnvKind::Local => { - let mut env = LocalExecutionEnvironment::new(cwd); + SandboxProvider::Local => { + let mut env = LocalSandbox::new(cwd); let emitter_cb = Arc::clone(&emitter); env.set_event_callback(Arc::new(move |event| { - emitter_cb.emit(&crate::event::WorkflowRunEvent::ExecutionEnv { event }); + emitter_cb.emit(&crate::event::WorkflowRunEvent::Sandbox { event }); })); Arc::new(env) } }; - // Initialize execution environment (creates sandbox/container once for the whole run) - execution_env + // Initialize sandbox (creates sandbox/container once for the whole run) + sandbox .initialize() .await - .map_err(|e| anyhow::anyhow!("Failed to initialize execution environment: {e}"))?; + .map_err(|e| anyhow::anyhow!("Failed to initialize sandbox: {e}"))?; // Ensure cleanup runs even on error/panic - let exec_env_for_cleanup = Arc::clone(&execution_env); + let sandbox_for_cleanup = Arc::clone(&sandbox); let _cleanup_guard = scopeguard::guard((), move |()| { // Best-effort cleanup — fire and forget in a blocking context let rt = tokio::runtime::Handle::try_current(); if let Ok(handle) = rt { handle.spawn(async move { - if let Err(e) = exec_env_for_cleanup.cleanup().await { + if let Err(e) = sandbox_for_cleanup.cleanup().await { tracing::warn!(error = %e, "Execution environment cleanup failed"); - eprintln!("Warning: execution environment cleanup failed: {e}"); + eprintln!("Warning: sandbox cleanup failed: {e}"); } }); } }); // Set up git inside Daytona sandbox (if applicable) - let (daytona_run_id, daytona_base_sha, daytona_branch) = if execution_env_kind - == ExecutionEnvKind::Daytona + let (daytona_run_id, daytona_base_sha, daytona_branch) = if sandbox_provider + == SandboxProvider::Daytona { - match setup_daytona_git(&*execution_env).await { + match setup_daytona_git(&*sandbox).await { Ok((rid, base, branch)) => (Some(rid), Some(base), Some(branch)), Err(e) => { eprintln!( @@ -401,7 +401,7 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu (None, None, None) }; - // Run setup commands inside the execution environment (once, not per-stage) + // Run setup commands inside the sandbox (once, not per-stage) if !setup_commands.is_empty() { emitter.emit(&crate::event::WorkflowRunEvent::SetupStarted { command_count: setup_commands.len(), @@ -413,7 +413,7 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu index, }); let cmd_start = Instant::now(); - let result = execution_env + let result = sandbox .exec_command(cmd, 300_000, None, None, None) .await .map_err(|e| anyhow::anyhow!("Setup command failed: {e}"))?; @@ -533,7 +533,7 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu registry, Arc::clone(&emitter), interviewer, - Arc::clone(&execution_env), + Arc::clone(&sandbox), ); // 7. Execute @@ -551,11 +551,11 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu cancel_token: None, dry_run: dry_run_mode, run_id, - git_checkpoint: match execution_env_kind { - ExecutionEnvKind::Local | ExecutionEnvKind::Docker => { + git_checkpoint: match sandbox_provider { + SandboxProvider::Local | SandboxProvider::Docker => { worktree_work_dir.map(GitCheckpointMode::Host) } - ExecutionEnvKind::Daytona => daytona_base_sha + SandboxProvider::Daytona => daytona_base_sha .as_ref() .map(|_| GitCheckpointMode::Remote(original_cwd.clone())), }, @@ -622,7 +622,7 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu run_duration_ms, dry_run_mode, llm_client.as_ref(), - &execution_env, + &sandbox, provider_enum, &model, styles, @@ -738,10 +738,10 @@ fn setup_worktree( /// Set up git inside a Daytona sandbox for checkpoint commits. /// Returns (run_id, base_sha, branch_name) on success. async fn setup_daytona_git( - exec_env: &dyn arc_agent::ExecutionEnvironment, + sandbox: &dyn arc_agent::Sandbox, ) -> anyhow::Result<(String, String, String)> { // Get current HEAD as base SHA - let sha_result = exec_env + let sha_result = sandbox .exec_command("git rev-parse HEAD", 10_000, None, None, None) .await .map_err(|e| anyhow::anyhow!("git rev-parse HEAD failed: {e}"))?; @@ -759,7 +759,7 @@ async fn setup_daytona_git( // Create and checkout a run branch let checkout_cmd = format!("git checkout -b {branch_name}"); - let checkout_result = exec_env + let checkout_result = sandbox .exec_command(&checkout_cmd, 10_000, None, None, None) .await .map_err(|e| anyhow::anyhow!("git checkout failed: {e}"))?; @@ -854,13 +854,13 @@ async fn run_from_branch( let base_sha = crate::git::MetadataStore::read_manifest(&original_cwd, &run_id)? .and_then(|m| m.get("base_sha").and_then(|v| v.as_str()).map(String::from)); - // Build minimal execution environment (local only for now) + // Build minimal sandbox (local only for now) let emitter = Arc::new(EventEmitter::new()); - let execution_env: Arc = { - let mut env = arc_agent::LocalExecutionEnvironment::new(worktree_path.clone()); + let sandbox: Arc = { + let mut env = arc_agent::LocalSandbox::new(worktree_path.clone()); let emitter_cb = Arc::clone(&emitter); env.set_event_callback(Arc::new(move |event| { - emitter_cb.emit(&crate::event::WorkflowRunEvent::ExecutionEnv { event }); + emitter_cb.emit(&crate::event::WorkflowRunEvent::Sandbox { event }); })); Arc::new(env) }; @@ -901,7 +901,7 @@ async fn run_from_branch( registry, Arc::clone(&emitter), interviewer, - Arc::clone(&execution_env), + Arc::clone(&sandbox), ); let meta_branch = Some(crate::git::MetadataStore::branch_name(&run_id)); @@ -950,7 +950,7 @@ async fn run_from_branch( run_duration_ms, dry_run_mode, llm_client.as_ref(), - &execution_env, + &sandbox, provider_enum, &model, styles, @@ -1006,7 +1006,7 @@ async fn generate_retro( run_duration_ms: u64, dry_run_mode: bool, llm_client: Option<&arc_llm::client::Client>, - execution_env: &Arc, + sandbox: &Arc, provider_enum: Provider, model: &str, styles: &'static Styles, @@ -1050,7 +1050,7 @@ async fn generate_retro( let narrative_result = if dry_run_mode { Ok(crate::retro_agent::dry_run_narrative()) } else if let Some(client) = llm_client { - crate::retro_agent::run_retro_agent(execution_env, logs_dir, client, provider_enum, model) + crate::retro_agent::run_retro_agent(sandbox, logs_dir, client, provider_enum, model) .await } else { Err(anyhow::anyhow!("No LLM client available")) diff --git a/crates/arc-workflows/src/cli/task_config.rs b/crates/arc-workflows/src/cli/task_config.rs index 3f371a73c..b24901695 100644 --- a/crates/arc-workflows/src/cli/task_config.rs +++ b/crates/arc-workflows/src/cli/task_config.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use anyhow::{bail, Context}; use serde::Deserialize; -use crate::daytona_env::DaytonaConfig; +use crate::daytona_sandbox::DaytonaConfig; const SUPPORTED_VERSION: u32 = 1; @@ -17,7 +17,7 @@ pub struct TaskConfig { pub directory: Option, pub llm: Option, pub setup: Option, - pub execution: Option, + pub sandbox: Option, pub vars: Option>, } @@ -37,8 +37,8 @@ pub struct SetupConfig { #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] -pub struct ExecutionConfig { - pub environment: Option, +pub struct SandboxConfig { + pub provider: Option, pub daytona: Option, } @@ -220,19 +220,19 @@ language = "python" } #[test] - fn parse_toml_with_execution() { + fn parse_toml_with_sandbox() { let toml = r#" version = 1 task = "Run tests" graph = "workflow.dot" -[execution] -environment = "daytona" +[sandbox] +provider = "daytona" "#; let config = parse_task_config(toml).unwrap(); - let execution = config.execution.unwrap(); - assert_eq!(execution.environment.as_deref(), Some("daytona")); - assert!(execution.daytona.is_none()); + let sandbox = config.sandbox.unwrap(); + assert_eq!(sandbox.provider.as_deref(), Some("daytona")); + assert!(sandbox.daytona.is_none()); } #[test] @@ -242,16 +242,16 @@ version = 1 task = "Run tests" graph = "workflow.dot" -[execution] -environment = "daytona" +[sandbox] +provider = "daytona" -[execution.daytona.sandbox] +[sandbox.daytona] auto_stop_interval = 60 -[execution.daytona.sandbox.labels] +[sandbox.daytona.labels] project = "arc" -[execution.daytona.snapshot] +[sandbox.daytona.snapshot] name = "my-snapshot" cpu = 4 memory = 8 @@ -259,12 +259,12 @@ disk = 10 dockerfile = "FROM rust:1.85-slim-bookworm\nRUN apt-get update" "#; let config = parse_task_config(toml).unwrap(); - let execution = config.execution.unwrap(); - assert_eq!(execution.environment.as_deref(), Some("daytona")); + let sandbox = config.sandbox.unwrap(); + assert_eq!(sandbox.provider.as_deref(), Some("daytona")); - let daytona = execution.daytona.unwrap(); - assert_eq!(daytona.sandbox.auto_stop_interval, Some(60)); - let labels = daytona.sandbox.labels.unwrap(); + let daytona = sandbox.daytona.unwrap(); + assert_eq!(daytona.auto_stop_interval, Some(60)); + let labels = daytona.labels.unwrap(); assert_eq!(labels["project"], "arc"); let snapshot = daytona.snapshot.unwrap(); @@ -285,15 +285,15 @@ version = 1 task = "Run tests" graph = "workflow.dot" -[execution] -environment = "daytona" +[sandbox] +provider = "daytona" -[execution.daytona.sandbox] +[sandbox.daytona] auto_stop_interval = 30 "#; let config = parse_task_config(toml).unwrap(); - let daytona = config.execution.unwrap().daytona.unwrap(); - assert_eq!(daytona.sandbox.auto_stop_interval, Some(30)); + let daytona = config.sandbox.unwrap().daytona.unwrap(); + assert_eq!(daytona.auto_stop_interval, Some(30)); assert!(daytona.snapshot.is_none()); } diff --git a/crates/arc-workflows/src/daytona_sandbox.rs b/crates/arc-workflows/src/daytona_sandbox.rs index 5cc3d1143..eef3683d3 100644 --- a/crates/arc-workflows/src/daytona_sandbox.rs +++ b/crates/arc-workflows/src/daytona_sandbox.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; use std::path::Path; use std::time::Instant; -use arc_agent::execution_env::{ - format_lines_numbered, DirEntry, ExecEnvEventCallback, ExecResult, ExecutionEnvEvent, - ExecutionEnvironment, GrepOptions, +use arc_agent::sandbox::{ + format_lines_numbered, DirEntry, SandboxEventCallback, ExecResult, SandboxEvent, + Sandbox, GrepOptions, }; use async_trait::async_trait; use rand::Rng; @@ -13,23 +13,15 @@ use serde::Deserialize; const WORKING_DIRECTORY: &str = "/home/daytona/workspace"; const DEFAULT_IMAGE: &str = "ubuntu:22.04"; -/// Configuration for a Daytona cloud sandbox execution environment. +/// Configuration for a Daytona cloud sandbox. /// -/// Doubles as the TOML deserialization target for `[execution.daytona]`. +/// Doubles as the TOML deserialization target for `[sandbox.daytona]`. #[derive(Clone, Debug, Default, Deserialize)] #[serde(deny_unknown_fields)] pub struct DaytonaConfig { - #[serde(default)] - pub sandbox: DaytonaSandboxConfig, - pub snapshot: Option, -} - -/// Sandbox-level settings (labels, auto-stop). -#[derive(Clone, Debug, Default, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct DaytonaSandboxConfig { pub auto_stop_interval: Option, pub labels: Option>, + pub snapshot: Option, } /// Snapshot configuration: when present, the sandbox is created from a snapshot @@ -44,16 +36,16 @@ pub struct DaytonaSnapshotConfig { pub dockerfile: Option, } -/// Execution environment that runs all operations inside a Daytona cloud sandbox. -pub struct DaytonaExecutionEnvironment { +/// Sandbox that runs all operations inside a Daytona cloud sandbox. +pub struct DaytonaSandbox { config: DaytonaConfig, client: daytona_sdk::Client, sandbox: tokio::sync::OnceCell, rg_available: tokio::sync::OnceCell, - event_callback: Option, + event_callback: Option, } -impl DaytonaExecutionEnvironment { +impl DaytonaSandbox { #[must_use] pub fn new(client: daytona_sdk::Client, config: DaytonaConfig) -> Self { Self { @@ -65,11 +57,11 @@ impl DaytonaExecutionEnvironment { } } - pub fn set_event_callback(&mut self, cb: ExecEnvEventCallback) { + pub fn set_event_callback(&mut self, cb: SandboxEventCallback) { self.event_callback = Some(cb); } - fn emit(&self, event: ExecutionEnvEvent) { + fn emit(&self, event: SandboxEvent) { event.trace(); if let Some(ref cb) = self.event_callback { cb(event); @@ -101,8 +93,8 @@ impl DaytonaExecutionEnvironment { ); daytona_sdk::SandboxBaseParams { name: Some(name), - auto_stop_interval: self.config.sandbox.auto_stop_interval, - labels: self.config.sandbox.labels.clone(), + auto_stop_interval: self.config.auto_stop_interval, + labels: self.config.labels.clone(), ephemeral: Some(true), ..Default::default() } @@ -270,36 +262,36 @@ pub fn get_gh_token() -> Result { } #[async_trait] -impl ExecutionEnvironment for DaytonaExecutionEnvironment { +impl Sandbox for DaytonaSandbox { async fn initialize(&self) -> Result<(), String> { - self.emit(ExecutionEnvEvent::Initializing { - env_type: "daytona".into(), + self.emit(SandboxEvent::Initializing { + provider: "daytona".into(), }); let init_start = Instant::now(); let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); let params = if let Some(ref snap_cfg) = self.config.snapshot { - self.emit(ExecutionEnvEvent::SnapshotEnsuring { + self.emit(SandboxEvent::SnapshotEnsuring { name: snap_cfg.name.clone(), }); let snap_start = Instant::now(); if let Err(e) = self.ensure_snapshot(snap_cfg).await { - self.emit(ExecutionEnvEvent::SnapshotFailed { + self.emit(SandboxEvent::SnapshotFailed { name: snap_cfg.name.clone(), error: e.clone(), }); let duration_ms = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(ExecutionEnvEvent::InitializeFailed { - env_type: "daytona".into(), + self.emit(SandboxEvent::InitializeFailed { + provider: "daytona".into(), error: e.clone(), duration_ms, }); return Err(e); } let snap_duration = u64::try_from(snap_start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(ExecutionEnvEvent::SnapshotReady { + self.emit(SandboxEvent::SnapshotReady { name: snap_cfg.name.clone(), duration_ms: snap_duration, }); @@ -325,8 +317,8 @@ impl ExecutionEnvironment for DaytonaExecutionEnvironment { let err = format!("Failed to create Daytona sandbox: {e}"); let duration_ms = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(ExecutionEnvEvent::InitializeFailed { - env_type: "daytona".into(), + self.emit(SandboxEvent::InitializeFailed { + provider: "daytona".into(), error: err.clone(), duration_ms, }); @@ -338,7 +330,7 @@ impl ExecutionEnvironment for DaytonaExecutionEnvironment { Ok((detected_url, branch)) => { // Daytona clones over HTTPS with token auth, so rewrite SSH URLs. let url = ssh_url_to_https(&detected_url); - self.emit(ExecutionEnvEvent::GitCloneStarted { + self.emit(SandboxEvent::GitCloneStarted { url: url.clone(), branch: branch.clone(), }); @@ -349,14 +341,14 @@ impl ExecutionEnvironment for DaytonaExecutionEnvironment { let token = match token { Ok(t) => t, Err(e) => { - self.emit(ExecutionEnvEvent::GitCloneFailed { + self.emit(SandboxEvent::GitCloneFailed { url: url.clone(), error: e.clone(), }); let duration_ms = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(ExecutionEnvEvent::InitializeFailed { - env_type: "daytona".into(), + self.emit(SandboxEvent::InitializeFailed { + provider: "daytona".into(), error: e.clone(), duration_ms, }); @@ -371,14 +363,14 @@ impl ExecutionEnvironment for DaytonaExecutionEnvironment { let git_svc = match git_svc { Ok(g) => g, Err(e) => { - self.emit(ExecutionEnvEvent::GitCloneFailed { + self.emit(SandboxEvent::GitCloneFailed { url: url.clone(), error: e.clone(), }); let duration_ms = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(ExecutionEnvEvent::InitializeFailed { - env_type: "daytona".into(), + self.emit(SandboxEvent::InitializeFailed { + provider: "daytona".into(), error: e.clone(), duration_ms, }); @@ -402,21 +394,21 @@ impl ExecutionEnvironment for DaytonaExecutionEnvironment { Ok(()) => { let clone_duration = u64::try_from(clone_start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(ExecutionEnvEvent::GitCloneCompleted { + self.emit(SandboxEvent::GitCloneCompleted { url, duration_ms: clone_duration, }); } Err(e) => { let err = format!("Failed to clone repo into Daytona sandbox: {e}"); - self.emit(ExecutionEnvEvent::GitCloneFailed { + self.emit(SandboxEvent::GitCloneFailed { url, error: err.clone(), }); let duration_ms = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(ExecutionEnvEvent::InitializeFailed { - env_type: "daytona".into(), + self.emit(SandboxEvent::InitializeFailed { + provider: "daytona".into(), error: err.clone(), duration_ms, }); @@ -444,8 +436,8 @@ impl ExecutionEnvironment for DaytonaExecutionEnvironment { tracing::info!("Daytona sandbox ready"); let init_duration = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(ExecutionEnvEvent::Ready { - env_type: "daytona".into(), + self.emit(SandboxEvent::Ready { + provider: "daytona".into(), duration_ms: init_duration, }); @@ -453,24 +445,24 @@ impl ExecutionEnvironment for DaytonaExecutionEnvironment { } async fn cleanup(&self) -> Result<(), String> { - self.emit(ExecutionEnvEvent::CleanupStarted { - env_type: "daytona".into(), + self.emit(SandboxEvent::CleanupStarted { + provider: "daytona".into(), }); let start = Instant::now(); if let Some(sandbox) = self.sandbox.get() { tracing::info!("Destroying Daytona sandbox"); if let Err(e) = sandbox.delete().await { let err = format!("Failed to delete Daytona sandbox: {e}"); - self.emit(ExecutionEnvEvent::CleanupFailed { - env_type: "daytona".into(), + self.emit(SandboxEvent::CleanupFailed { + provider: "daytona".into(), error: err.clone(), }); return Err(err); } } let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(ExecutionEnvEvent::CleanupCompleted { - env_type: "daytona".into(), + self.emit(SandboxEvent::CleanupCompleted { + provider: "daytona".into(), duration_ms, }); Ok(()) @@ -775,8 +767,8 @@ mod tests { fn daytona_config_defaults() { let config = DaytonaConfig::default(); assert!(config.snapshot.is_none()); - assert!(config.sandbox.auto_stop_interval.is_none()); - assert!(config.sandbox.labels.is_none()); + assert!(config.auto_stop_interval.is_none()); + assert!(config.labels.is_none()); } #[test] diff --git a/crates/arc-workflows/src/engine.rs b/crates/arc-workflows/src/engine.rs index c68cc943c..7a4026981 100644 --- a/crates/arc-workflows/src/engine.rs +++ b/crates/arc-workflows/src/engine.rs @@ -5,7 +5,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Instant; -use arc_agent::ExecutionEnvironment; +use arc_agent::Sandbox; use chrono::Utc; use futures::FutureExt; use rand::Rng; @@ -531,7 +531,7 @@ pub struct GitState { pub enum GitCheckpointMode { /// Run git commands on the host filesystem (local & Docker bind-mount). Host(PathBuf), - /// Run git commands inside the remote execution environment via `exec_command`. + /// Run git commands inside the remote sandbox via `exec_command`. /// The `PathBuf` is the host repo path used for `MetadataStore` (shadow commits). Remote(PathBuf), } @@ -579,9 +579,9 @@ async fn git_diff_host(work_dir: PathBuf, base: String) -> Option { pub const GIT_REMOTE: &str = "git -c maintenance.auto=0 -c gc.auto=0"; -/// Run a git checkpoint commit inside a remote execution environment. +/// Run a git checkpoint commit inside a remote sandbox. pub async fn git_checkpoint_remote( - exec_env: &dyn ExecutionEnvironment, + sandbox: &dyn Sandbox, run_id: &str, node_id: &str, status: &str, @@ -590,7 +590,7 @@ pub async fn git_checkpoint_remote( ) -> Option { // Stage everything let add_cmd = format!("{GIT_REMOTE} add -A"); - let add_result = exec_env + let add_result = sandbox .exec_command(&add_cmd, 30_000, None, None, None) .await; if add_result.as_ref().map_or(true, |r| r.exit_code != 0) { @@ -620,7 +620,7 @@ pub async fn git_checkpoint_remote( let message = trailerlink::format_message(&subject, "", &trailers); // Write message to temp file in sandbox to avoid shell escaping issues - if exec_env + if sandbox .write_file("/tmp/arc-commit-msg", &message) .await .is_err() @@ -632,7 +632,7 @@ pub async fn git_checkpoint_remote( let commit_cmd = format!( "{GIT_REMOTE} -c user.name=arc -c user.email=arc@local commit --allow-empty -F /tmp/arc-commit-msg" ); - let commit_result = exec_env + let commit_result = sandbox .exec_command(&commit_cmd, 30_000, None, None, None) .await; if commit_result.as_ref().map_or(true, |r| r.exit_code != 0) { @@ -641,7 +641,7 @@ pub async fn git_checkpoint_remote( // Get the new HEAD SHA let sha_cmd = format!("{GIT_REMOTE} rev-parse HEAD"); - let sha_result = exec_env + let sha_result = sandbox .exec_command(&sha_cmd, 10_000, None, None, None) .await; match sha_result { @@ -650,10 +650,10 @@ pub async fn git_checkpoint_remote( } } -/// Run a git diff inside a remote execution environment. -async fn git_diff_remote(exec_env: &dyn ExecutionEnvironment, base: &str) -> Option { +/// Run a git diff inside a remote sandbox. +async fn git_diff_remote(sandbox: &dyn Sandbox, base: &str) -> Option { let cmd = format!("{GIT_REMOTE} diff {base} HEAD"); - match exec_env.exec_command(&cmd, 30_000, None, None, None).await { + match sandbox.exec_command(&cmd, 30_000, None, None, None).await { Ok(r) if r.exit_code == 0 => Some(r.stdout), _ => None, } @@ -661,54 +661,54 @@ async fn git_diff_remote(exec_env: &dyn ExecutionEnvironment, base: &str) -> Opt // --- Remote worktree helpers (for Daytona / sandbox environments) --- -/// Create a branch at a specific SHA inside a remote execution environment. +/// Create a branch at a specific SHA inside a remote sandbox. pub async fn git_create_branch_at_remote( - exec_env: &dyn ExecutionEnvironment, + sandbox: &dyn Sandbox, name: &str, sha: &str, ) -> bool { let cmd = format!("{GIT_REMOTE} branch --force {name} {sha}"); matches!( - exec_env.exec_command(&cmd, 30_000, None, None, None).await, + sandbox.exec_command(&cmd, 30_000, None, None, None).await, Ok(r) if r.exit_code == 0 ) } -/// Add a git worktree inside a remote execution environment. +/// Add a git worktree inside a remote sandbox. pub async fn git_add_worktree_remote( - exec_env: &dyn ExecutionEnvironment, + sandbox: &dyn Sandbox, path: &str, branch: &str, ) -> bool { let cmd = format!("{GIT_REMOTE} worktree add {path} {branch}"); matches!( - exec_env.exec_command(&cmd, 30_000, None, None, None).await, + sandbox.exec_command(&cmd, 30_000, None, None, None).await, Ok(r) if r.exit_code == 0 ) } -/// Remove a git worktree inside a remote execution environment. -pub async fn git_remove_worktree_remote(exec_env: &dyn ExecutionEnvironment, path: &str) -> bool { +/// Remove a git worktree inside a remote sandbox. +pub async fn git_remove_worktree_remote(sandbox: &dyn Sandbox, path: &str) -> bool { let cmd = format!("{GIT_REMOTE} worktree remove --force {path}"); matches!( - exec_env.exec_command(&cmd, 30_000, None, None, None).await, + sandbox.exec_command(&cmd, 30_000, None, None, None).await, Ok(r) if r.exit_code == 0 ) } -/// Fast-forward merge to a given SHA inside a remote execution environment. -pub async fn git_merge_ff_only_remote(exec_env: &dyn ExecutionEnvironment, sha: &str) -> bool { +/// Fast-forward merge to a given SHA inside a remote sandbox. +pub async fn git_merge_ff_only_remote(sandbox: &dyn Sandbox, sha: &str) -> bool { let cmd = format!("{GIT_REMOTE} merge --ff-only {sha}"); matches!( - exec_env.exec_command(&cmd, 30_000, None, None, None).await, + sandbox.exec_command(&cmd, 30_000, None, None, None).await, Ok(r) if r.exit_code == 0 ) } -/// Get the current HEAD SHA from a remote execution environment. -pub async fn git_head_sha_remote(exec_env: &dyn ExecutionEnvironment) -> Option { +/// Get the current HEAD SHA from a remote sandbox. +pub async fn git_head_sha_remote(sandbox: &dyn Sandbox) -> Option { let cmd = format!("{GIT_REMOTE} rev-parse HEAD"); - match exec_env.exec_command(&cmd, 10_000, None, None, None).await { + match sandbox.exec_command(&cmd, 10_000, None, None, None).await { Ok(r) if r.exit_code == 0 => Some(r.stdout.trim().to_string()), _ => None, } @@ -716,12 +716,12 @@ pub async fn git_head_sha_remote(exec_env: &dyn ExecutionEnvironment) -> Option< /// Remove any stale worktree at `path` (best-effort), then add a fresh one. pub async fn git_replace_worktree_remote( - exec_env: &dyn ExecutionEnvironment, + sandbox: &dyn Sandbox, path: &str, branch: &str, ) -> bool { - let _ = git_remove_worktree_remote(exec_env, path).await; - git_add_worktree_remote(exec_env, path, branch).await + let _ = git_remove_worktree_remote(sandbox, path).await; + git_add_worktree_remote(sandbox, path, branch).await } /// Configuration for a workflow run. @@ -754,13 +754,13 @@ impl WorkflowRunEngine { pub fn new( registry: HandlerRegistry, emitter: Arc, - execution_env: Arc, + sandbox: Arc, ) -> Self { Self { services: EngineServices { registry: Arc::new(registry), emitter, - execution_env, + sandbox, git_state: std::sync::RwLock::new(None), }, interviewer: None, @@ -774,7 +774,7 @@ impl WorkflowRunEngine { services: EngineServices { registry: Arc::clone(&services.registry), emitter: Arc::clone(&services.emitter), - execution_env: Arc::clone(&services.execution_env), + sandbox: Arc::clone(&services.sandbox), git_state: std::sync::RwLock::new(None), }, interviewer: None, @@ -787,13 +787,13 @@ impl WorkflowRunEngine { registry: HandlerRegistry, emitter: Arc, interviewer: Arc, - execution_env: Arc, + sandbox: Arc, ) -> Self { Self { services: EngineServices { registry: Arc::new(registry), emitter, - execution_env, + sandbox, git_state: std::sync::RwLock::new(None), }, interviewer: Some(interviewer), @@ -1400,9 +1400,9 @@ impl WorkflowRunEngine { context.append_log(format!("artifact offload failed: {e}")); } - // Sync artifact files to the execution environment (no-op for local envs) + // Sync artifact files to the sandbox (no-op for local envs) if let Err(e) = - sync_artifacts_to_env(&mut outcome.context_updates, &*self.services.execution_env) + sync_artifacts_to_env(&mut outcome.context_updates, &*self.services.sandbox) .await { context.append_log(format!("artifact sync failed: {e}")); @@ -1523,7 +1523,7 @@ impl WorkflowRunEngine { } GitCheckpointMode::Remote(_) => { git_checkpoint_remote( - &*self.services.execution_env, + &*self.services.sandbox, &run_id, &node.id, &outcome.status.to_string(), @@ -1559,7 +1559,7 @@ impl WorkflowRunEngine { git_diff_host(work_dir.clone(), diff_base).await } GitCheckpointMode::Remote(_) => { - git_diff_remote(&*self.services.execution_env, &diff_base).await + git_diff_remote(&*self.services.sandbox, &diff_base).await } }; if let Some(patch) = diff_result { @@ -1677,7 +1677,7 @@ impl WorkflowRunEngine { git_diff_host(work_dir.clone(), base.clone()).await } GitCheckpointMode::Remote(_) => { - git_diff_remote(&*self.services.execution_env, base).await + git_diff_remote(&*self.services.sandbox, base).await } }; if let Some(patch) = patch { @@ -1703,8 +1703,8 @@ mod tests { use async_trait::async_trait; use std::time::Duration; - fn local_env() -> Arc { - Arc::new(arc_agent::LocalExecutionEnvironment::new( + fn local_env() -> Arc { + Arc::new(arc_agent::LocalSandbox::new( std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), )) } diff --git a/crates/arc-workflows/src/event.rs b/crates/arc-workflows/src/event.rs index a0d246417..65db0a9a1 100644 --- a/crates/arc-workflows/src/event.rs +++ b/crates/arc-workflows/src/event.rs @@ -3,7 +3,7 @@ use std::sync::atomic::{AtomicI64, Ordering}; use serde::{Deserialize, Serialize}; use crate::outcome::StageUsage; -use arc_agent::{AgentEvent, ExecutionEnvEvent}; +use arc_agent::{AgentEvent, SandboxEvent}; /// Events emitted during workflow run execution for observability. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -147,9 +147,9 @@ pub enum WorkflowRunEvent { status: String, duration_ms: u64, }, - /// Forwarded from an execution environment lifecycle operation. - ExecutionEnv { - event: ExecutionEnvEvent, + /// Forwarded from a sandbox lifecycle operation. + Sandbox { + event: SandboxEvent, }, SetupStarted { command_count: usize, @@ -364,7 +364,7 @@ impl WorkflowRunEvent { debug!(stage, text_len = text.len(), "Prompt sent"); } Self::Agent { .. } => {} - Self::ExecutionEnv { .. } => {} + Self::Sandbox { .. } => {} Self::ParallelEarlyTermination { reason, completed_count, @@ -970,21 +970,21 @@ mod tests { } #[test] - fn execution_env_event_wrapper_serialization() { - use arc_agent::ExecutionEnvEvent; + fn sandbox_event_wrapper_serialization() { + use arc_agent::SandboxEvent; - let event = WorkflowRunEvent::ExecutionEnv { - event: ExecutionEnvEvent::Initializing { - env_type: "docker".into(), + let event = WorkflowRunEvent::Sandbox { + event: SandboxEvent::Initializing { + provider: "docker".into(), }, }; let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("ExecutionEnv")); + assert!(json.contains("Sandbox")); assert!(json.contains("Initializing")); assert!(json.contains("docker")); let deserialized: WorkflowRunEvent = serde_json::from_str(&json).unwrap(); - assert!(matches!(deserialized, WorkflowRunEvent::ExecutionEnv { .. })); + assert!(matches!(deserialized, WorkflowRunEvent::Sandbox { .. })); } #[test] diff --git a/crates/arc-workflows/src/handler/codergen.rs b/crates/arc-workflows/src/handler/codergen.rs index 78e9603d1..89e75cee0 100644 --- a/crates/arc-workflows/src/handler/codergen.rs +++ b/crates/arc-workflows/src/handler/codergen.rs @@ -1,7 +1,7 @@ use std::path::Path; use std::sync::Arc; -use arc_agent::ExecutionEnvironment; +use arc_agent::Sandbox; use async_trait::async_trait; use crate::context::Context; @@ -35,7 +35,7 @@ pub trait CodergenBackend: Send + Sync { thread_id: Option<&str>, emitter: &Arc, stage_dir: &Path, - execution_env: &Arc, + sandbox: &Arc, ) -> Result; /// Run a single LLM call with no tools (one_shot mode). @@ -249,7 +249,7 @@ impl Handler for CodergenHandler { thread_id.as_deref(), &services.emitter, &stage_dir, - &services.execution_env, + &services.sandbox, ) .await } @@ -333,7 +333,7 @@ mod tests { EngineServices { registry: std::sync::Arc::new(HandlerRegistry::new(Box::new(StartHandler))), emitter: std::sync::Arc::new(EventEmitter::new()), - execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new( + sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new( std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), )), git_state: std::sync::RwLock::new(None), @@ -582,7 +582,7 @@ mod tests { thread_id: Option<&str>, _emitter: &Arc, _stage_dir: &Path, - _execution_env: &Arc, + _sandbox: &Arc, ) -> Result { *self.captured_thread_id.lock().unwrap() = Some(thread_id.map(String::from)); Ok(CodergenResult::Text { @@ -633,7 +633,7 @@ mod tests { thread_id: Option<&str>, _emitter: &Arc, _stage_dir: &Path, - _execution_env: &Arc, + _sandbox: &Arc, ) -> Result { *self.captured_thread_id.lock().unwrap() = Some(thread_id.map(String::from)); Ok(CodergenResult::Text { @@ -679,7 +679,7 @@ mod tests { _thread_id: Option<&str>, _emitter: &Arc, _stage_dir: &Path, - _execution_env: &Arc, + _sandbox: &Arc, ) -> Result { Err(ArcError::Handler("Request timed out".to_string())) } @@ -795,7 +795,7 @@ Some text in between. _thread_id: Option<&str>, _emitter: &Arc, _stage_dir: &Path, - _execution_env: &Arc, + _sandbox: &Arc, ) -> Result { panic!("run() should not be called in one_shot mode"); } @@ -910,7 +910,7 @@ Some text in between. _thread_id: Option<&str>, _emitter: &Arc, _stage_dir: &Path, - _execution_env: &Arc, + _sandbox: &Arc, ) -> Result { Err(ArcError::Validation("bad config".to_string())) } @@ -948,7 +948,7 @@ Some text in between. _thread_id: Option<&str>, _emitter: &Arc, _stage_dir: &std::path::Path, - _execution_env: &Arc, + _sandbox: &Arc, ) -> Result { *self.captured_prompt.lock().unwrap() = Some(prompt.to_string()); Ok(CodergenResult::Text { @@ -1016,7 +1016,7 @@ Some text in between. _thread_id: Option<&str>, _emitter: &Arc, _stage_dir: &std::path::Path, - _execution_env: &Arc, + _sandbox: &Arc, ) -> Result { *self.captured_prompt.lock().unwrap() = Some(prompt.to_string()); Ok(CodergenResult::Text { @@ -1070,7 +1070,7 @@ Some text in between. _thread_id: Option<&str>, _emitter: &Arc, _stage_dir: &std::path::Path, - _execution_env: &Arc, + _sandbox: &Arc, ) -> Result { panic!("run() should not be called in one_shot mode"); } diff --git a/crates/arc-workflows/src/handler/conditional.rs b/crates/arc-workflows/src/handler/conditional.rs index cea0262b0..b227d5432 100644 --- a/crates/arc-workflows/src/handler/conditional.rs +++ b/crates/arc-workflows/src/handler/conditional.rs @@ -40,7 +40,7 @@ mod tests { EngineServices { registry: std::sync::Arc::new(HandlerRegistry::new(Box::new(StartHandler))), emitter: std::sync::Arc::new(EventEmitter::new()), - execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new( + sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new( std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), )), git_state: std::sync::RwLock::new(None), diff --git a/crates/arc-workflows/src/handler/exit.rs b/crates/arc-workflows/src/handler/exit.rs index bdbed8a9d..884eab073 100644 --- a/crates/arc-workflows/src/handler/exit.rs +++ b/crates/arc-workflows/src/handler/exit.rs @@ -37,7 +37,7 @@ mod tests { EngineServices { registry: std::sync::Arc::new(HandlerRegistry::new(Box::new(StartHandler))), emitter: std::sync::Arc::new(EventEmitter::new()), - execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new( + sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new( std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), )), git_state: std::sync::RwLock::new(None), diff --git a/crates/arc-workflows/src/handler/fan_in.rs b/crates/arc-workflows/src/handler/fan_in.rs index ed1ff2d64..8c5b79296 100644 --- a/crates/arc-workflows/src/handler/fan_in.rs +++ b/crates/arc-workflows/src/handler/fan_in.rs @@ -1,7 +1,7 @@ use std::path::Path; use std::sync::Arc; -use arc_agent::ExecutionEnvironment; +use arc_agent::Sandbox; use async_trait::async_trait; use crate::context::Context; @@ -51,7 +51,7 @@ impl Handler for FanInHandler { logs_root, &node.id, &services.emitter, - &services.execution_env, + &services.sandbox, ) .await? } else { @@ -90,7 +90,7 @@ impl Handler for FanInHandler { .await; } crate::engine::GitCheckpointMode::Remote(_) => { - crate::engine::git_merge_ff_only_remote(&*services.execution_env, sha).await; + crate::engine::git_merge_ff_only_remote(&*services.sandbox, sha).await; } } } @@ -196,7 +196,7 @@ async fn llm_evaluate( logs_root: &Path, node_id: &str, emitter: &Arc, - execution_env: &Arc, + sandbox: &Arc, ) -> Result { let results_text = serde_json::to_string_pretty(results).unwrap_or_else(|_| results.to_string()); @@ -224,7 +224,7 @@ async fn llm_evaluate( None, emitter, &stage_dir, - execution_env, + sandbox, ) .await { @@ -299,7 +299,7 @@ mod tests { EngineServices { registry: std::sync::Arc::new(HandlerRegistry::new(Box::new(StartHandler))), emitter: std::sync::Arc::new(EventEmitter::new()), - execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new( + sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new( std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), )), git_state: std::sync::RwLock::new(None), @@ -429,7 +429,7 @@ mod tests { _thread_id: Option<&str>, _emitter: &Arc, _stage_dir: &std::path::Path, - _execution_env: &Arc, + _sandbox: &Arc, ) -> Result { // Return text that contains the ID "branch_b" Ok(CodergenResult::Text { diff --git a/crates/arc-workflows/src/handler/manager_loop.rs b/crates/arc-workflows/src/handler/manager_loop.rs index 697c4667c..44e2c5926 100644 --- a/crates/arc-workflows/src/handler/manager_loop.rs +++ b/crates/arc-workflows/src/handler/manager_loop.rs @@ -234,7 +234,7 @@ mod tests { EngineServices { registry: std::sync::Arc::new(registry), emitter: std::sync::Arc::new(EventEmitter::new()), - execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new( + sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new( std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), )), git_state: std::sync::RwLock::new(None), @@ -364,7 +364,7 @@ mod tests { let services = EngineServices { registry: std::sync::Arc::new(registry), emitter: std::sync::Arc::new(EventEmitter::new()), - execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new( + sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new( std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), )), git_state: std::sync::RwLock::new(None), @@ -471,7 +471,7 @@ mod tests { let services = EngineServices { registry: std::sync::Arc::new(registry), emitter: std::sync::Arc::new(EventEmitter::new()), - execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new( + sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new( std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), )), git_state: std::sync::RwLock::new(None), @@ -534,7 +534,7 @@ mod tests { let services = EngineServices { registry: std::sync::Arc::new(registry), emitter: std::sync::Arc::new(EventEmitter::new()), - execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new( + sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new( std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), )), git_state: std::sync::RwLock::new(None), diff --git a/crates/arc-workflows/src/handler/mod.rs b/crates/arc-workflows/src/handler/mod.rs index 4ae28b71f..468205e51 100644 --- a/crates/arc-workflows/src/handler/mod.rs +++ b/crates/arc-workflows/src/handler/mod.rs @@ -12,7 +12,7 @@ use std::collections::HashMap; use std::path::Path; use std::sync::Arc; -use arc_agent::ExecutionEnvironment; +use arc_agent::Sandbox; use async_trait::async_trait; use crate::context::Context; @@ -27,7 +27,7 @@ use crate::outcome::Outcome; pub struct EngineServices { pub registry: Arc, pub emitter: Arc, - pub execution_env: Arc, + pub sandbox: Arc, /// Git state for the current run. Set via `set_git_state` at the start of /// `run_internal` and read by parallel/fan-in handlers. pub(crate) git_state: std::sync::RwLock>>, diff --git a/crates/arc-workflows/src/handler/parallel.rs b/crates/arc-workflows/src/handler/parallel.rs index 622504ec9..436896041 100644 --- a/crates/arc-workflows/src/handler/parallel.rs +++ b/crates/arc-workflows/src/handler/parallel.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Instant; -use arc_agent::ExecutionEnvironment; +use arc_agent::Sandbox; use async_trait::async_trait; use tokio::sync::Semaphore; @@ -16,18 +16,18 @@ use crate::outcome::{Outcome, StageStatus}; use super::{EngineServices, Handler}; // --------------------------------------------------------------------------- -// WorktreeEnv — decorates an ExecutionEnvironment with a custom working dir +// WorktreeSandbox — decorates an Sandbox with a custom working dir // --------------------------------------------------------------------------- -/// Wraps an existing `ExecutionEnvironment` so that all operations use a +/// Wraps an existing `Sandbox` so that all operations use a /// different working directory (the worktree path inside a remote sandbox). -struct WorktreeEnv { - inner: Arc, +struct WorktreeSandbox { + inner: Arc, worktree_dir: String, } #[async_trait] -impl ExecutionEnvironment for WorktreeEnv { +impl Sandbox for WorktreeSandbox { async fn read_file( &self, path: &str, @@ -49,7 +49,7 @@ impl ExecutionEnvironment for WorktreeEnv { &self, path: &str, depth: Option, - ) -> Result, String> { + ) -> Result, String> { self.inner.list_directory(path, depth).await } async fn exec_command( @@ -59,7 +59,7 @@ impl ExecutionEnvironment for WorktreeEnv { working_dir: Option<&str>, env_vars: Option<&std::collections::HashMap>, cancel_token: Option, - ) -> Result { + ) -> Result { // Default to worktree dir when no explicit working_dir is given let wd = working_dir.unwrap_or(&self.worktree_dir); self.inner @@ -70,7 +70,7 @@ impl ExecutionEnvironment for WorktreeEnv { &self, pattern: &str, path: &str, - options: &arc_agent::execution_env::GrepOptions, + options: &arc_agent::sandbox::GrepOptions, ) -> Result, String> { self.inner.grep(pattern, path, options).await } @@ -242,7 +242,7 @@ impl Handler for ParallelHandler { } GitCheckpointMode::Remote(_) => { crate::engine::git_checkpoint_remote( - &*services.execution_env, + &*services.sandbox, &gs.run_id, &node.id, "parallel_base", @@ -256,12 +256,12 @@ impl Handler for ParallelHandler { None }; - // Build per-branch execution environments (sequentially for git setup) + // Build per-branch sandboxs (sequentially for git setup) struct BranchSetup { target_id: String, branch_index: usize, branch_context: Context, - execution_env: Arc, + sandbox: Arc, worktree_path: Option, } @@ -270,7 +270,7 @@ impl Handler for ParallelHandler { let target_id = edge.to.clone(); let branch_context = context.clone_context(); - let (branch_exec_env, worktree_path): (Arc, Option) = + let (branch_sandbox, worktree_path): (Arc, Option) = if let (Some(ref gs), Some(ref bsha)) = (&git_state, &base_sha) { let branch_key = &target_id; let branch_name = format!( @@ -305,21 +305,21 @@ impl Handler for ParallelHandler { "internal.work_dir", serde_json::json!(wt_path.to_string_lossy().as_ref()), ); - let env: Arc = Arc::new( - arc_agent::LocalExecutionEnvironment::new(wt_path.clone()), + let env: Arc = Arc::new( + arc_agent::LocalSandbox::new(wt_path.clone()), ); (env, Some(wt_path)) } GitCheckpointMode::Remote(_) => { let wt_path_str = format!( "{}/.arc/logs/{}/parallel/{}/{}", - services.execution_env.working_directory(), + services.sandbox.working_directory(), gs.run_id, node.id, branch_key ); let ok = crate::engine::git_create_branch_at_remote( - &*services.execution_env, + &*services.sandbox, &branch_name, bsha, ) @@ -330,7 +330,7 @@ impl Handler for ParallelHandler { ))); } let ok = crate::engine::git_replace_worktree_remote( - &*services.execution_env, + &*services.sandbox, &wt_path_str, &branch_name, ) @@ -344,7 +344,7 @@ impl Handler for ParallelHandler { let reset_cmd = format!("{} reset --hard {bsha}", crate::engine::GIT_REMOTE); let reset_result = services - .execution_env + .sandbox .exec_command(&reset_cmd, 30_000, Some(&wt_path_str), None, None) .await; if !matches!(reset_result, Ok(ref r) if r.exit_code == 0) { @@ -354,22 +354,22 @@ impl Handler for ParallelHandler { } branch_context .set("internal.work_dir", serde_json::json!(&wt_path_str)); - let env: Arc = Arc::new(WorktreeEnv { - inner: Arc::clone(&services.execution_env), + let env: Arc = Arc::new(WorktreeSandbox { + inner: Arc::clone(&services.sandbox), worktree_dir: wt_path_str.clone(), }); (env, Some(PathBuf::from(wt_path_str))) } } } else { - (Arc::clone(&services.execution_env), None) + (Arc::clone(&services.sandbox), None) }; branch_setups.push(BranchSetup { target_id, branch_index, branch_context, - execution_env: branch_exec_env, + sandbox: branch_sandbox, worktree_path, }); } @@ -417,7 +417,7 @@ impl Handler for ParallelHandler { let branch_services = EngineServices { registry: Arc::clone(®istry), emitter: Arc::clone(&emitter), - execution_env: Arc::clone(&setup.execution_env), + sandbox: Arc::clone(&setup.sandbox), git_state: std::sync::RwLock::new(None), }; let handler = registry.resolve(target_node); @@ -440,7 +440,7 @@ impl Handler for ParallelHandler { let git_r = crate::engine::GIT_REMOTE; let add_cmd = format!("{git_r} add -A"); let add_result = setup - .execution_env + .sandbox .exec_command(&add_cmd, 30_000, None, None, None) .await; if add_result.as_ref().is_ok_and(|r| r.exit_code == 0) { @@ -449,13 +449,13 @@ impl Handler for ParallelHandler { "{git_r} -c user.name=arc -c user.email=arc@local commit --allow-empty -m '{msg}'" ); let _ = setup - .execution_env + .sandbox .exec_command(&commit_cmd, 30_000, None, None, None) .await; } let sha_cmd = format!("{git_r} rev-parse HEAD"); let sha_result = setup - .execution_env + .sandbox .exec_command(&sha_cmd, 10_000, None, None, None) .await; match sha_result { @@ -564,7 +564,7 @@ impl Handler for ParallelHandler { GitCheckpointMode::Remote(_) => { let wt_str = wt_path.to_string_lossy().to_string(); crate::engine::git_remove_worktree_remote( - &*services.execution_env, + &*services.sandbox, &wt_str, ) .await; @@ -592,7 +592,7 @@ impl Handler for ParallelHandler { .await; } GitCheckpointMode::Remote(_) => { - crate::engine::git_merge_ff_only_remote(&*services.execution_env, sha) + crate::engine::git_merge_ff_only_remote(&*services.sandbox, sha) .await; } } @@ -717,7 +717,7 @@ mod tests { EngineServices { registry: Arc::new(registry), emitter: Arc::new(EventEmitter::new()), - execution_env: Arc::new(arc_agent::LocalExecutionEnvironment::new( + sandbox: Arc::new(arc_agent::LocalSandbox::new( std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), )), git_state: std::sync::RwLock::new(None), diff --git a/crates/arc-workflows/src/handler/script.rs b/crates/arc-workflows/src/handler/script.rs index 18a5d5a7e..28a82accf 100644 --- a/crates/arc-workflows/src/handler/script.rs +++ b/crates/arc-workflows/src/handler/script.rs @@ -177,7 +177,7 @@ mod tests { EngineServices { registry: std::sync::Arc::new(HandlerRegistry::new(Box::new(StartHandler))), emitter: std::sync::Arc::new(EventEmitter::new()), - execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new( + sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new( std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), )), git_state: std::sync::RwLock::new(None), diff --git a/crates/arc-workflows/src/handler/start.rs b/crates/arc-workflows/src/handler/start.rs index 16180bfd8..c1a425a90 100644 --- a/crates/arc-workflows/src/handler/start.rs +++ b/crates/arc-workflows/src/handler/start.rs @@ -36,7 +36,7 @@ mod tests { EngineServices { registry: std::sync::Arc::new(HandlerRegistry::new(Box::new(StartHandler))), emitter: std::sync::Arc::new(EventEmitter::new()), - execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new( + sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new( std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), )), git_state: std::sync::RwLock::new(None), diff --git a/crates/arc-workflows/src/handler/wait_human.rs b/crates/arc-workflows/src/handler/wait_human.rs index d8cb1909d..0e693be2a 100644 --- a/crates/arc-workflows/src/handler/wait_human.rs +++ b/crates/arc-workflows/src/handler/wait_human.rs @@ -276,7 +276,7 @@ mod tests { EngineServices { registry: std::sync::Arc::new(HandlerRegistry::new(Box::new(StartHandler))), emitter: std::sync::Arc::new(EventEmitter::new()), - execution_env: std::sync::Arc::new(arc_agent::LocalExecutionEnvironment::new( + sandbox: std::sync::Arc::new(arc_agent::LocalSandbox::new( std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), )), git_state: std::sync::RwLock::new(None), diff --git a/crates/arc-workflows/src/lib.rs b/crates/arc-workflows/src/lib.rs index ba0525b01..1adabd793 100644 --- a/crates/arc-workflows/src/lib.rs +++ b/crates/arc-workflows/src/lib.rs @@ -3,7 +3,7 @@ pub mod checkpoint; pub mod cli; pub mod condition; pub mod context; -pub mod daytona_env; +pub mod daytona_sandbox; pub mod engine; pub mod error; pub mod event; diff --git a/crates/arc-workflows/src/retro_agent.rs b/crates/arc-workflows/src/retro_agent.rs index a5dcf86fb..6d87b1a62 100644 --- a/crates/arc-workflows/src/retro_agent.rs +++ b/crates/arc-workflows/src/retro_agent.rs @@ -3,7 +3,7 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; use arc_agent::{ - AnthropicProfile, ExecutionEnvironment, GeminiProfile, OpenAiProfile, ProviderProfile, Session, + AnthropicProfile, Sandbox, GeminiProfile, OpenAiProfile, ProviderProfile, Session, SessionConfig, }; use arc_llm::client::Client; @@ -112,16 +112,16 @@ const SUBMIT_RETRO_SCHEMA: &str = r#"{ /// a structured narrative. The agent explores `progress.ndjson` and other /// files via tool access, then calls `submit_retro` with its analysis. pub async fn run_retro_agent( - execution_env: &Arc, + sandbox: &Arc, logs_root: &Path, llm_client: &Client, provider: Provider, model: &str, ) -> anyhow::Result { - // Upload data files into execution env (needed for Daytona; no-op effect for local + // Upload data files into sandbox (needed for Daytona; no-op effect for local // since the agent can also read from the original paths via tools). let retro_data_dir = "/tmp/retro_data"; - upload_data_files(execution_env, logs_root, retro_data_dir).await?; + upload_data_files(sandbox, logs_root, retro_data_dir).await?; // Build provider profile with the submit_retro tool let captured: Arc>> = Arc::new(Mutex::new(None)); @@ -164,7 +164,7 @@ pub async fn run_retro_agent( let mut session = Session::new( llm_client.clone(), profile, - Arc::clone(execution_env), + Arc::clone(sandbox), config, ); @@ -218,12 +218,12 @@ fn build_profile(provider: Provider, model: &str) -> Box { } async fn upload_data_files( - execution_env: &Arc, + sandbox: &Arc, logs_root: &Path, target_dir: &str, ) -> anyhow::Result<()> { // Create target directory - execution_env + sandbox .exec_command(&format!("mkdir -p {target_dir}"), 10_000, None, None, None) .await .map_err(|e| anyhow::anyhow!("Failed to create retro data dir: {e}"))?; @@ -233,7 +233,7 @@ async fn upload_data_files( let source = logs_root.join(filename); if source.exists() { let content = std::fs::read_to_string(&source)?; - execution_env + sandbox .write_file(&format!("{target_dir}/{filename}"), &content) .await .map_err(|e| anyhow::anyhow!("Failed to upload {filename}: {e}"))?; diff --git a/crates/arc-workflows/tests/daytona_integration.rs b/crates/arc-workflows/tests/daytona_integration.rs index cefbbf0b0..79e4dc6ca 100644 --- a/crates/arc-workflows/tests/daytona_integration.rs +++ b/crates/arc-workflows/tests/daytona_integration.rs @@ -1,4 +1,4 @@ -//! Integration tests for `DaytonaExecutionEnvironment`. +//! Integration tests for `DaytonaSandbox`. //! //! These tests require a `DAYTONA_API_KEY` environment variable and network access. //! Run with: `cargo test --package arc-workflows -- --ignored daytona` @@ -7,12 +7,12 @@ use std::collections::HashMap; use std::path::Path; use std::sync::Arc; -use arc_agent::ExecutionEnvironment; +use arc_agent::Sandbox; use arc_llm::provider::Provider; use arc_workflows::artifact::sync_artifacts_to_env; use arc_workflows::checkpoint::Checkpoint; use arc_workflows::context::Context; -use arc_workflows::daytona_env::{DaytonaConfig, DaytonaExecutionEnvironment}; +use arc_workflows::daytona_sandbox::{DaytonaConfig, DaytonaSandbox}; use arc_workflows::engine::{WorkflowRunEngine, RunConfig}; use arc_workflows::error::ArcError; use arc_workflows::event::EventEmitter; @@ -22,12 +22,12 @@ use arc_workflows::handler::start::StartHandler; use arc_workflows::handler::{Handler, HandlerRegistry}; use arc_workflows::outcome::{Outcome, StageStatus}; -async fn create_env() -> DaytonaExecutionEnvironment { +async fn create_env() -> DaytonaSandbox { dotenvy::dotenv().ok(); let client = daytona_sdk::Client::new() .await .expect("Failed to create Daytona client — is DAYTONA_API_KEY set?"); - DaytonaExecutionEnvironment::new(client, DaytonaConfig::default()) + DaytonaSandbox::new(client, DaytonaConfig::default()) } #[tokio::test] @@ -117,7 +117,7 @@ async fn daytona_full_lifecycle() { #[tokio::test] #[ignore] async fn daytona_snapshot_sandbox() { - use arc_workflows::daytona_env::{DaytonaSandboxConfig, DaytonaSnapshotConfig}; + use arc_workflows::daytona_sandbox::DaytonaSnapshotConfig; dotenvy::dotenv().ok(); let client = daytona_sdk::Client::new() @@ -125,10 +125,8 @@ async fn daytona_snapshot_sandbox() { .expect("Failed to create Daytona client — is DAYTONA_API_KEY set?"); let config = DaytonaConfig { - sandbox: DaytonaSandboxConfig { - auto_stop_interval: Some(60), - ..Default::default() - }, + auto_stop_interval: Some(60), + labels: None, snapshot: Some(DaytonaSnapshotConfig { name: "arc-test-snapshot".to_string(), cpu: Some(2), @@ -140,7 +138,7 @@ async fn daytona_snapshot_sandbox() { }), }; - let env = DaytonaExecutionEnvironment::new(client, config); + let env = DaytonaSandbox::new(client, config); env.initialize().await.unwrap(); // Verify rg is available (installed by snapshot) @@ -236,7 +234,7 @@ impl Handler for LargeOutputHandler { async fn daytona_pipeline_artifact_offload_and_sync() { let env = create_env().await; env.initialize().await.unwrap(); - let env: Arc = Arc::new(env); + let env: Arc = Arc::new(env); // Pipeline: start -> big_output -> exit let mut graph = Graph::new("DaytonaArtifactPipeline"); @@ -350,7 +348,7 @@ impl Handler for FileWriterHandler { let content = format!("output from {}", node.id); let cmd = format!("echo '{content}' > {}.txt", node.id); let _ = services - .execution_env + .sandbox .exec_command(&cmd, 10_000, None, None, None) .await; Ok(Outcome::success()) @@ -359,9 +357,9 @@ impl Handler for FileWriterHandler { /// Set up git inside a Daytona sandbox for checkpoint commits. /// Returns (run_id, base_sha, branch_name) on success. -async fn setup_daytona_git(exec_env: &dyn ExecutionEnvironment) -> (String, String, String) { +async fn setup_daytona_git(sandbox: &dyn Sandbox) -> (String, String, String) { // Get current HEAD as base SHA - let sha_result = exec_env + let sha_result = sandbox .exec_command("git rev-parse HEAD", 10_000, None, None, None) .await .expect("git rev-parse HEAD should succeed"); @@ -376,7 +374,7 @@ async fn setup_daytona_git(exec_env: &dyn ExecutionEnvironment) -> (String, Stri let branch_name = format!("arc/run/{run_id}"); let checkout_cmd = format!("git checkout -b {branch_name}"); - let checkout_result = exec_env + let checkout_result = sandbox .exec_command(&checkout_cmd, 10_000, None, None, None) .await .expect("git checkout should succeed"); @@ -394,7 +392,7 @@ async fn setup_daytona_git(exec_env: &dyn ExecutionEnvironment) -> (String, Stri async fn daytona_git_checkpoint_remote_emits_events() { let env = create_env().await; env.initialize().await.unwrap(); - let env: Arc = Arc::new(env); + let env: Arc = Arc::new(env); // Install git if not available (the default ubuntu:22.04 image may not have it) let git_check = env @@ -553,7 +551,7 @@ use arc_workflows::handler::parallel::ParallelHandler; async fn daytona_parallel_git_branching_e2e() { let env = create_env().await; env.initialize().await.unwrap(); - let env: Arc = Arc::new(env); + let env: Arc = Arc::new(env); // Install git if not available let git_check = env @@ -781,7 +779,7 @@ use arc_workflows::handler::codergen::{CodergenBackend, CodergenResult}; async fn run_daytona_cli_test(provider: Provider, model: &str, install_command: &str) { let env = create_env().await; env.initialize().await.unwrap(); - let env: Arc = Arc::new(env); + let env: Arc = Arc::new(env); // Install the CLI tool inside the Daytona sandbox let install_result = env @@ -883,7 +881,7 @@ use arc_workflows::git::MetadataStore; async fn daytona_git_checkpoint_with_shadow_branch() { let env = create_env().await; env.initialize().await.unwrap(); - let env: Arc = Arc::new(env); + let env: Arc = Arc::new(env); // Install git if not available let git_check = env diff --git a/crates/arc-workflows/tests/integration.rs b/crates/arc-workflows/tests/integration.rs index 3bfa5b6f3..3bb1b682b 100644 --- a/crates/arc-workflows/tests/integration.rs +++ b/crates/arc-workflows/tests/integration.rs @@ -32,8 +32,8 @@ use arc_workflows::transform::{ }; use arc_workflows::validation::{validate, validate_or_raise, Severity}; -fn local_env() -> Arc { - Arc::new(arc_agent::LocalExecutionEnvironment::new( +fn local_env() -> Arc { + Arc::new(arc_agent::LocalSandbox::new( std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")), )) } @@ -1141,7 +1141,7 @@ impl CodergenBackend for MockCodergenBackend { _thread_id: Option<&str>, _emitter: &Arc, _stage_dir: &std::path::Path, - _execution_env: &Arc, + _sandbox: &Arc, ) -> Result { Ok(CodergenResult::Text { text: format!( @@ -5497,7 +5497,7 @@ mod real_llm { _thread_id: Option<&str>, _emitter: &Arc, _stage_dir: &std::path::Path, - _execution_env: &Arc, + _sandbox: &Arc, ) -> Result { let request = Request { model: self.model.clone(), @@ -7628,7 +7628,7 @@ impl RemoteMockEnv { } #[async_trait::async_trait] -impl arc_agent::ExecutionEnvironment for RemoteMockEnv { +impl arc_agent::Sandbox for RemoteMockEnv { async fn read_file( &self, _path: &str, @@ -7712,7 +7712,7 @@ impl arc_agent::ExecutionEnvironment for RemoteMockEnv { } #[tokio::test] -async fn artifact_pointers_rewritten_for_remote_execution_env() { +async fn artifact_pointers_rewritten_for_remote_sandbox() { // Pipeline: start -> big_output -> exit // big_output uses LargeOutputHandler which returns a >100KB context_update. // RemoteMockEnv simulates a container where local files don't exist. @@ -7966,7 +7966,7 @@ impl CliTestEnv { } #[async_trait::async_trait] -impl arc_agent::ExecutionEnvironment for CliTestEnv { +impl arc_agent::Sandbox for CliTestEnv { async fn read_file( &self, _path: &str, @@ -8074,13 +8074,13 @@ impl arc_agent::ExecutionEnvironment for CliTestEnv { } } -// -- Cycle 8: AgentCliBackend::run() e2e via mock ExecutionEnvironment -- +// -- Cycle 8: AgentCliBackend::run() e2e via mock Sandbox -- #[tokio::test] async fn cli_backend_run_writes_prompt_and_calls_exec() { let claude_output = r#"{"type":"result","result":"I fixed the bug.","usage":{"input_tokens":500,"output_tokens":200}}"#; let test_env = Arc::new(CliTestEnv::new(claude_output)); - let env: Arc = test_env.clone(); + let env: Arc = test_env.clone(); let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); let node = Node::new("fix_code"); @@ -8147,7 +8147,7 @@ async fn cli_backend_run_writes_prompt_and_calls_exec() { #[tokio::test] async fn cli_backend_run_detects_changed_files() { let claude_output = r#"{"type":"result","result":"Created new file.","usage":{"input_tokens":100,"output_tokens":50}}"#; - let env: Arc = + let env: Arc = Arc::new(CliTestEnv::new(claude_output).with_git_diff_after("src/main.rs\nsrc/lib.rs\n")); let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); @@ -8181,7 +8181,7 @@ async fn cli_backend_run_detects_changed_files() { async fn cli_backend_run_with_codex_provider() { let codex_output = "{\"type\":\"item.completed\",\"item\":{\"id\":\"item_0\",\"type\":\"agent_message\",\"text\":\"Implemented the feature.\"}}\n{\"type\":\"turn.completed\",\"usage\":{\"input_tokens\":300,\"output_tokens\":150}}"; let test_env = Arc::new(CliTestEnv::new(codex_output)); - let env: Arc = test_env.clone(); + let env: Arc = test_env.clone(); let backend = AgentCliBackend::new("gpt-5.3-codex".into(), Provider::OpenAi); let node = Node::new("implement"); @@ -8232,7 +8232,7 @@ async fn cli_backend_run_fails_on_nonzero_exit() { // Override exec_command to return non-zero for the CLI call struct FailingCliEnv; #[async_trait::async_trait] - impl arc_agent::ExecutionEnvironment for FailingCliEnv { + impl arc_agent::Sandbox for FailingCliEnv { async fn read_file( &self, _: &str, @@ -8310,7 +8310,7 @@ async fn cli_backend_run_fails_on_nonzero_exit() { } } - let failing_env: Arc = Arc::new(FailingCliEnv); + let failing_env: Arc = Arc::new(FailingCliEnv); let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); let node = Node::new("step"); let context = Context::new(); @@ -8348,7 +8348,7 @@ async fn cli_backend_run_fails_on_nonzero_exit() { #[tokio::test] async fn cli_backend_run_fails_on_unparseable_output() { - let env: Arc = + let env: Arc = Arc::new(CliTestEnv::new("this is not json at all")); let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); @@ -8385,7 +8385,7 @@ async fn cli_backend_run_uses_node_model_override() { let claude_output = r#"{"type":"result","result":"ok","usage":{"input_tokens":10,"output_tokens":5}}"#; let test_env = Arc::new(CliTestEnv::new(claude_output)); - let env: Arc = test_env.clone(); + let env: Arc = test_env.clone(); let backend = AgentCliBackend::new("default-model".into(), Provider::Anthropic); let mut node = Node::new("step"); @@ -8419,7 +8419,7 @@ async fn cli_backend_run_uses_node_model_override() { async fn cli_backend_run_uses_node_provider_override() { let codex_output = "{\"type\":\"item.completed\",\"item\":{\"id\":\"item_0\",\"type\":\"agent_message\",\"text\":\"ok\"}}\n{\"type\":\"turn.completed\",\"usage\":{\"input_tokens\":10,\"output_tokens\":5}}"; let test_env = Arc::new(CliTestEnv::new(codex_output)); - let env: Arc = test_env.clone(); + let env: Arc = test_env.clone(); let backend = AgentCliBackend::new("default-model".into(), Provider::Anthropic); let mut node = Node::new("step"); @@ -8453,7 +8453,7 @@ async fn cli_backend_run_uses_node_provider_override() { async fn cli_backend_run_writes_provider_used_json() { let claude_output = r#"{"type":"result","result":"done","usage":{"input_tokens":10,"output_tokens":5}}"#; - let env: Arc = Arc::new(CliTestEnv::new(claude_output)); + let env: Arc = Arc::new(CliTestEnv::new(claude_output)); let backend = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); let node = Node::new("step"); @@ -8484,7 +8484,7 @@ async fn cli_backend_run_writes_provider_used_json() { #[tokio::test] async fn backend_router_delegates_to_cli_for_cli_node() { let claude_output = r#"{"type":"result","result":"CLI response","usage":{"input_tokens":10,"output_tokens":5}}"#; - let env: Arc = Arc::new(CliTestEnv::new(claude_output)); + let env: Arc = Arc::new(CliTestEnv::new(claude_output)); let api_backend = Box::new(MockCodergenBackend); // would return "Response for ..." let cli = AgentCliBackend::new("claude-opus-4-6".into(), Provider::Anthropic); @@ -8571,7 +8571,7 @@ async fn backend_router_delegates_to_api_for_normal_node() { #[tokio::test] async fn backend_router_delegates_to_cli_for_backend_attr() { let codex_output = "{\"type\":\"item.completed\",\"item\":{\"id\":\"item_0\",\"type\":\"agent_message\",\"text\":\"Codex did it\"}}\n{\"type\":\"turn.completed\",\"usage\":{\"input_tokens\":10,\"output_tokens\":5}}"; - let env: Arc = Arc::new(CliTestEnv::new(codex_output)); + let env: Arc = Arc::new(CliTestEnv::new(codex_output)); let api_backend = Box::new(MockCodergenBackend); let cli = AgentCliBackend::new("gpt-5.3-codex".into(), Provider::OpenAi); @@ -8620,7 +8620,7 @@ async fn full_pipeline_with_cli_backend_node() { // Pipeline: start -> api_work -> cli_work -> exit // api_work uses MockCodergenBackend (API), cli_work has backend="cli" let claude_output = r#"{"type":"result","result":"CLI completed the task.","usage":{"input_tokens":100,"output_tokens":50}}"#; - let env: Arc = Arc::new(CliTestEnv::new(claude_output)); + let env: Arc = Arc::new(CliTestEnv::new(claude_output)); let mut graph = Graph::new("CliPipelineTest"); @@ -8749,7 +8749,7 @@ async fn full_pipeline_with_cli_backend_node() { #[tokio::test] async fn stylesheet_backend_property_routes_to_cli() { let claude_output = r#"{"type":"result","result":"Styled CLI response.","usage":{"input_tokens":10,"output_tokens":5}}"#; - let env: Arc = Arc::new(CliTestEnv::new(claude_output)); + let env: Arc = Arc::new(CliTestEnv::new(claude_output)); let mut graph = Graph::new("StylesheetTest"); graph.attrs.insert( @@ -8845,7 +8845,7 @@ async fn stylesheet_backend_property_routes_to_cli() { use arc_workflows::cli::cli_backend::parse_cli_response; -/// Run a real CLI tool via LocalExecutionEnvironment and verify the full flow. +/// Run a real CLI tool via LocalSandbox and verify the full flow. async fn run_real_cli_test(provider: Provider, model: &str) { let env = local_env(); let backend = AgentCliBackend::new(model.to_string(), provider); @@ -8980,10 +8980,10 @@ impl Handler for FileWriterHandler { _logs_root: &Path, services: &arc_workflows::handler::EngineServices, ) -> Result { - let work_dir = services.execution_env.working_directory().to_string(); + let work_dir = services.sandbox.working_directory().to_string(); let file_path = format!("{}/{}.txt", work_dir, node.id); services - .execution_env + .sandbox .write_file(&file_path, &format!("written by {}", node.id)) .await .map_err(|e| ArcError::Handler(format!("write_file failed: {e}")))?; @@ -9073,8 +9073,8 @@ async fn git_checkpoint_host_emits_events_and_diff_patch() { let mut emitter = EventEmitter::new(); let events = collect_events(&mut emitter); - let env: Arc = Arc::new( - arc_agent::LocalExecutionEnvironment::new(worktree_path.clone()), + let env: Arc = Arc::new( + arc_agent::LocalSandbox::new(worktree_path.clone()), ); let mut registry = HandlerRegistry::new(Box::new(ContextSetterHandler)); registry.register("start", Box::new(StartHandler)); @@ -9253,8 +9253,8 @@ async fn git_checkpoint_host_writes_shadow_branch() { std::fs::write(logs_dir.path().join("graph.dot"), "digraph {}").unwrap(); let emitter = EventEmitter::new(); - let env: Arc = Arc::new( - arc_agent::LocalExecutionEnvironment::new(worktree_path.clone()), + let env: Arc = Arc::new( + arc_agent::LocalSandbox::new(worktree_path.clone()), ); let mut registry = HandlerRegistry::new(Box::new(ContextSetterHandler)); registry.register("start", Box::new(StartHandler)); @@ -9440,8 +9440,8 @@ async fn parallel_git_branching_host_e2e() { let mut emitter = EventEmitter::new(); let events = collect_events(&mut emitter); - let env: Arc = Arc::new( - arc_agent::LocalExecutionEnvironment::new(worktree_path.clone()), + let env: Arc = Arc::new( + arc_agent::LocalSandbox::new(worktree_path.clone()), ); let mut registry = HandlerRegistry::new(Box::new(FileWriterHandler)); diff --git a/pipelines/daytona-check/task.toml b/pipelines/daytona-check/task.toml index f51f280f6..824c38b36 100644 --- a/pipelines/daytona-check/task.toml +++ b/pipelines/daytona-check/task.toml @@ -5,16 +5,16 @@ graph = "check.dot" [llm] model = "claude-sonnet" -[execution] -environment = "daytona" +[sandbox] +provider = "daytona" -[execution.daytona.sandbox] +[sandbox.daytona] auto_stop_interval = 60 -[execution.daytona.sandbox.labels] +[sandbox.daytona.labels] project = "attractor-rust" -[execution.daytona.snapshot] +[sandbox.daytona.snapshot] name = "attractor-rust-check-dev" cpu = 4 memory = 8