Rename ExecutionEnvironment to Sandbox across the codebase

Adopt cleaner terminology: Sandbox (resource providing disk + execution),
SandboxProvider (Local/Docker/Daytona), SandboxEvent (lifecycle events),
and Snapshot (pre-built environment images). Flatten DaytonaSandboxConfig
into DaytonaConfig, rename CLI flag to --sandbox, and update TOML config
sections from [execution] to [sandbox].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-02 11:33:21 -05:00
parent ffa9d1d505
commit d0c8bd3eed
48 changed files with 806 additions and 816 deletions

View file

@ -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<usize>, limit: Option<usize>) -> Result<String, String>;
async fn write_file(&self, path: &str, content: &str) -> Result<(), String>;
async fn exec_command(&self, command: &str, timeout_ms: u64, ...) -> Result<ExecResult, String>;
@ -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

View file

@ -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<dyn crate::ExecutionEnvironment> = Arc::new(LocalExecutionEnvironment::new(cwd));
let env: Arc<dyn crate::Sandbox> = Arc::new(LocalSandbox::new(cwd));
// Build tool approval callback
let is_interactive = std::io::stdin().is_terminal() && !args.auto_approve;

View file

@ -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<String>,
}
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<String>,
cached_platform: std::sync::OnceLock<String>,
cached_os_version: std::sync::OnceLock<String>,
rg_available: tokio::sync::OnceCell<bool>,
event_callback: Option<ExecEnvEventCallback>,
event_callback: Option<SandboxEventCallback>,
}
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<Self, String> {
pub fn new(config: DockerSandboxConfig) -> Result<Self, String> {
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<dyn ExecutionEnvironment> =
Arc::new(DockerExecutionEnvironment::new(config).unwrap());
let env: Arc<dyn Sandbox> =
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

View file

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

View file

@ -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<ExecEnvEventCallback>,
event_callback: Option<SandboxEventCallback>,
rg_available: std::sync::OnceLock<bool>,
}
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<Mutex<Vec<ExecutionEnvEvent>>> = Arc::new(Mutex::new(Vec::new()));
let events: Arc<Mutex<Vec<SandboxEvent>>> = 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<Mutex<Vec<ExecutionEnvEvent>>> = Arc::new(Mutex::new(Vec::new()));
let events: Arc<Mutex<Vec<SandboxEvent>>> = 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);

View file

@ -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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox::default());
let result = (tool.executor)(
serde_json::json!({"message": "test message"}),
ToolContext {

View file

@ -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("<environment>"));
@ -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"), &[]);

View file

@ -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("<environment>"));
assert!(prompt.contains("linux"));

View file

@ -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![
"<environment>".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("<environment>"));
assert!(block.contains("</environment>"));
@ -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,

View file

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

View file

@ -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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
files,
..Default::default()
});

View file

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

View file

@ -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<dyn ExecutionEnvironment>,
pub struct ReadBeforeWriteSandbox {
inner: Arc<dyn Sandbox>,
read_set: Mutex<HashSet<String>>,
}
impl ReadBeforeWriteEnvironment {
pub fn new(inner: Arc<dyn ExecutionEnvironment>) -> Self {
impl ReadBeforeWriteSandbox {
pub fn new(inner: Arc<dyn Sandbox>) -> 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();

View file

@ -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<dyn ExecutionEnvironment>`. The caller provides custom method
/// Generates an `#[async_trait] impl Sandbox` block for a decorator type
/// that wraps an `Arc<dyn Sandbox>`. 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<usize>, limit: Option<usize>) -> Result<String, String> {
@ -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<bool, String> {
@ -39,7 +39,7 @@ macro_rules! delegate_execution_env {
&self,
path: &str,
depth: Option<usize>,
) -> Result<Vec<$crate::execution_env::DirEntry>, String> {
) -> Result<Vec<$crate::sandbox::DirEntry>, 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<String, String>>,
cancel_token: Option<tokio_util::sync::CancellationToken>,
) -> 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<dyn Fn(ExecutionEnvEvent) + Send + Sync>;
/// Callback type for sandbox events.
pub type SandboxEventCallback = Arc<dyn Fn(SandboxEvent) + Send + Sync>;
/// 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
files,
..Default::default()
});
@ -318,7 +318,7 @@ mod tests {
#[tokio::test]
async fn mock_env_exec_command() {
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
let env: Arc<dyn Sandbox> = 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(),
});
}
}

View file

@ -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<dyn ProviderProfile>,
execution_env: Arc<dyn ExecutionEnvironment>,
sandbox: Arc<dyn Sandbox>,
steering_queue: Arc<Mutex<VecDeque<String>>>,
followup_queue: Arc<Mutex<VecDeque<String>>>,
cancel_token: CancellationToken,
@ -47,7 +47,7 @@ impl Session {
pub fn new(
llm_client: Client,
provider_profile: Arc<dyn ProviderProfile>,
execution_env: Arc<dyn ExecutionEnvironment>,
sandbox: Arc<dyn Sandbox>,
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<dyn ProviderAdapter>).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<dyn ProviderAdapter>).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<dyn ProviderAdapter>).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<dyn ProviderAdapter>).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<dyn ProviderAdapter>).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<dyn crate::provider_profile::ProviderProfile> =
Arc::new(TestProfile::new());
let env: Arc<dyn crate::execution_env::ExecutionEnvironment> =
Arc::new(MockExecutionEnvironment::default());
let env: Arc<dyn crate::sandbox::Sandbox> =
Arc::new(MockSandbox::default());
let mut session = Session::new(client, profile, env, config);
// Subscribe to events before initialize

View file

@ -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<Skill> {
pub async fn discover_skills(env: &dyn Sandbox, dirs: &[String]) -> Vec<Skill> {
let mut skills_by_name: std::collections::HashMap<String, Skill> =
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<dyn crate::execution_env::ExecutionEnvironment> =
Arc::new(MockExecutionEnvironment::default());
let env: Arc<dyn crate::sandbox::Sandbox> =
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<dyn crate::execution_env::ExecutionEnvironment> =
Arc::new(MockExecutionEnvironment::default());
let env: Arc<dyn crate::sandbox::Sandbox> =
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<dyn crate::execution_env::ExecutionEnvironment> =
Arc::new(MockExecutionEnvironment::default());
let env: Arc<dyn crate::sandbox::Sandbox> =
Arc::new(MockSandbox::default());
let args = serde_json::json!({});
let ctx = crate::tool_registry::ToolContext {
env,

View file

@ -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<String, String>,
pub exec_result: ExecResult,
pub grep_results: Vec<String>,
@ -33,10 +33,10 @@ pub struct MockExecutionEnvironment {
pub captured_timeout: Mutex<Option<u64>>,
/// Captures the `command` argument from `exec_command` calls.
pub captured_command: Mutex<Option<String>>,
pub event_callback: Option<crate::execution_env::ExecEnvEventCallback>,
pub event_callback: Option<crate::sandbox::SandboxEventCallback>,
}
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<HashMap<String, String>>,
}
impl MutableMockExecutionEnvironment {
impl MutableMockSandbox {
pub fn new(files: HashMap<String, String>) -> 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<Response>) -> 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<Response>, 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<Response>, 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)
}

View file

@ -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<dyn ExecutionEnvironment>,
env: Arc<dyn Sandbox>,
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<dyn ExecutionEnvironment>,
env: Arc<dyn Sandbox>,
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<dyn ExecutionEnvironment>,
env: Arc<dyn Sandbox>,
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<dyn ExecutionEnvironment>,
env: Arc<dyn Sandbox>,
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<dyn ExecutionEnvironment>,
env: Arc<dyn Sandbox>,
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<dyn ExecutionEnvironment>,
env: Arc<dyn Sandbox>,
tool_approval: Option<&ToolApprovalFn>,
cancel_token: CancellationToken,
) -> ToolResult {

View file

@ -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<dyn ExecutionEnvironment>,
pub env: Arc<dyn Sandbox>,
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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox::default());
let ctx = ToolContext {
env,
cancel: CancellationToken::new(),

View file

@ -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<WebFetchSummarizer>) -> 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = env.clone();
let env = Arc::new(MockSandbox::default());
let env_clone: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = env.clone();
let env_clone: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = env.clone();
let env_clone: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = env.clone();
let env = Arc::new(MockSandbox::default());
let env_clone: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
let env: Arc<dyn Sandbox> = 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: "<html><body><h1>hello</h1></body></html>".into(),
stderr: String::new(),
@ -978,7 +978,7 @@ mod tests {
},
..Default::default()
});
let env_clone: Arc<dyn ExecutionEnvironment> = env.clone();
let env_clone: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = env.clone();
let env = Arc::new(MockSandbox::default());
let env_clone: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = env.clone();
let env = Arc::new(MockSandbox::default());
let env_clone: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
exec_result: ExecResult {
stdout: "<html><body><p>Lots of content about Rust...</p></body></html>".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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
exec_result: ExecResult {
stdout: "<html><body><p>Rust is a systems programming language.</p></body></html>"
.into(),
@ -1236,7 +1236,7 @@ mod tests {
};
let tool = make_web_fetch_tool(Some(summarizer));
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox {
exec_result: ExecResult {
stdout: "<html><body><p>Page content</p></body></html>".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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox::default());
let result = (tool.executor)(
serde_json::json!({"query": "rust programming language"}),
ToolContext {

View file

@ -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<Vec<PatchOperation>, String> {
/// Returns an error if any file operation fails.
pub async fn apply_patch_operations(
ops: &[PatchOperation],
env: &dyn ExecutionEnvironment,
env: &dyn Sandbox,
) -> Result<String, String> {
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(),

View file

@ -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<dyn ProviderProfile> = 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)
}

View file

@ -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<ExecutionEnvKind>,
pub sandbox: Option<SandboxProvider>,
/// Serve static demo data (disables auth, read-only)
#[arg(long)]

View file

@ -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<dyn Interviewer>);
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let execution_env: Arc<dyn arc_agent::ExecutionEnvironment> =
Arc::new(LocalExecutionEnvironment::new(cwd));
let sandbox: Arc<dyn arc_agent::Sandbox> =
Arc::new(LocalSandbox::new(cwd));
let engine = WorkflowRunEngine::with_interviewer(
registry,
Arc::new(emitter),
Arc::clone(&interviewer) as Arc<dyn Interviewer>,
execution_env,
sandbox,
);
{

View file

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

View file

@ -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<dyn ExecutionEnvironment>,
sandbox: &Arc<dyn Sandbox>,
) -> Result<Session, ArcError> {
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<dyn ProviderProfile> = 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<dyn ProviderProfile> = 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<crate::event::EventEmitter>,
stage_dir: &std::path::Path,
execution_env: &Arc<dyn ExecutionEnvironment>,
sandbox: &Arc<dyn Sandbox>,
) -> Result<CodergenResult, ArcError> {
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.

View file

@ -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<dyn ExecutionEnvironment>,
sandbox: &Arc<dyn Sandbox>,
) -> Vec<String> {
// 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<EventEmitter>,
stage_dir: &Path,
execution_env: &Arc<dyn ExecutionEnvironment>,
sandbox: &Arc<dyn Sandbox>,
) -> Result<CodergenResult, ArcError> {
// 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<String> = files_after
.into_iter()
.filter(|f| !files_before.contains(f))
@ -415,7 +415,7 @@ impl CodergenBackend for BackendRouter {
thread_id: Option<&str>,
emitter: &Arc<EventEmitter>,
stage_dir: &Path,
execution_env: &Arc<dyn ExecutionEnvironment>,
sandbox: &Arc<dyn Sandbox>,
) -> Result<CodergenResult, ArcError> {
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<EventEmitter>,
_stage_dir: &Path,
_execution_env: &Arc<dyn ExecutionEnvironment>,
_sandbox: &Arc<dyn Sandbox>,
) -> Result<CodergenResult, ArcError> {
Ok(CodergenResult::Text {
text: "stub".to_string(),

View file

@ -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<Self, Self::Err> {
@ -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<ExecutionEnvKind>,
pub sandbox: Option<SandboxProvider>,
/// 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::<ExecutionEnvKind>().unwrap(),
ExecutionEnvKind::Local
"local".parse::<SandboxProvider>().unwrap(),
SandboxProvider::Local
);
assert_eq!(
"docker".parse::<ExecutionEnvKind>().unwrap(),
ExecutionEnvKind::Docker
"docker".parse::<SandboxProvider>().unwrap(),
SandboxProvider::Docker
);
assert_eq!(
"daytona".parse::<ExecutionEnvKind>().unwrap(),
ExecutionEnvKind::Daytona
"daytona".parse::<SandboxProvider>().unwrap(),
SandboxProvider::Daytona
);
assert_eq!(
"LOCAL".parse::<ExecutionEnvKind>().unwrap(),
ExecutionEnvKind::Local
"LOCAL".parse::<SandboxProvider>().unwrap(),
SandboxProvider::Local
);
assert!("invalid".parse::<ExecutionEnvKind>().is_err());
assert!("invalid".parse::<SandboxProvider>().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"));
}

View file

@ -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<String> = 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::<ExecutionEnvKind>())
.and_then(|c| c.sandbox.as_ref())
.and_then(|e| e.provider.as_deref())
.map(|s| s.parse::<SandboxProvider>())
.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::<ExecutionEnvKind>())
.and_then(|c| c.sandbox.as_ref())
.and_then(|e| e.provider.as_deref())
.map(|s| s.parse::<SandboxProvider>())
.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<dyn ExecutionEnvironment> = match execution_env_kind {
ExecutionEnvKind::Docker => {
let config = DockerConfig {
let sandbox: Arc<dyn Sandbox> = 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<dyn arc_agent::ExecutionEnvironment> = {
let mut env = arc_agent::LocalExecutionEnvironment::new(worktree_path.clone());
let sandbox: Arc<dyn arc_agent::Sandbox> = {
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<dyn arc_agent::ExecutionEnvironment>,
sandbox: &Arc<dyn arc_agent::Sandbox>,
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"))

View file

@ -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<String>,
pub llm: Option<LlmConfig>,
pub setup: Option<SetupConfig>,
pub execution: Option<ExecutionConfig>,
pub sandbox: Option<SandboxConfig>,
pub vars: Option<HashMap<String, String>>,
}
@ -37,8 +37,8 @@ pub struct SetupConfig {
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExecutionConfig {
pub environment: Option<String>,
pub struct SandboxConfig {
pub provider: Option<String>,
pub daytona: Option<DaytonaConfig>,
}
@ -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());
}

View file

@ -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<DaytonaSnapshotConfig>,
}
/// Sandbox-level settings (labels, auto-stop).
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DaytonaSandboxConfig {
pub auto_stop_interval: Option<i32>,
pub labels: Option<HashMap<String, String>>,
pub snapshot: Option<DaytonaSnapshotConfig>,
}
/// Snapshot configuration: when present, the sandbox is created from a snapshot
@ -44,16 +36,16 @@ pub struct DaytonaSnapshotConfig {
pub dockerfile: Option<String>,
}
/// 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<daytona_sdk::Sandbox>,
rg_available: tokio::sync::OnceCell<bool>,
event_callback: Option<ExecEnvEventCallback>,
event_callback: Option<SandboxEventCallback>,
}
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<String, String> {
}
#[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]

View file

@ -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<String> {
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<String> {
// 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<String> {
/// Run a git diff inside a remote sandbox.
async fn git_diff_remote(sandbox: &dyn Sandbox, base: &str) -> Option<String> {
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<String> {
/// Get the current HEAD SHA from a remote sandbox.
pub async fn git_head_sha_remote(sandbox: &dyn Sandbox) -> Option<String> {
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<EventEmitter>,
execution_env: Arc<dyn ExecutionEnvironment>,
sandbox: Arc<dyn Sandbox>,
) -> 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<EventEmitter>,
interviewer: Arc<dyn Interviewer>,
execution_env: Arc<dyn ExecutionEnvironment>,
sandbox: Arc<dyn Sandbox>,
) -> 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<dyn ExecutionEnvironment> {
Arc::new(arc_agent::LocalExecutionEnvironment::new(
fn local_env() -> Arc<dyn Sandbox> {
Arc::new(arc_agent::LocalSandbox::new(
std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
))
}

View file

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

View file

@ -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<EventEmitter>,
stage_dir: &Path,
execution_env: &Arc<dyn ExecutionEnvironment>,
sandbox: &Arc<dyn Sandbox>,
) -> Result<CodergenResult, ArcError>;
/// 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<EventEmitter>,
_stage_dir: &Path,
_execution_env: &Arc<dyn ExecutionEnvironment>,
_sandbox: &Arc<dyn Sandbox>,
) -> Result<CodergenResult, ArcError> {
*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<EventEmitter>,
_stage_dir: &Path,
_execution_env: &Arc<dyn ExecutionEnvironment>,
_sandbox: &Arc<dyn Sandbox>,
) -> Result<CodergenResult, ArcError> {
*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<EventEmitter>,
_stage_dir: &Path,
_execution_env: &Arc<dyn ExecutionEnvironment>,
_sandbox: &Arc<dyn Sandbox>,
) -> Result<CodergenResult, ArcError> {
Err(ArcError::Handler("Request timed out".to_string()))
}
@ -795,7 +795,7 @@ Some text in between.
_thread_id: Option<&str>,
_emitter: &Arc<EventEmitter>,
_stage_dir: &Path,
_execution_env: &Arc<dyn ExecutionEnvironment>,
_sandbox: &Arc<dyn Sandbox>,
) -> Result<CodergenResult, ArcError> {
panic!("run() should not be called in one_shot mode");
}
@ -910,7 +910,7 @@ Some text in between.
_thread_id: Option<&str>,
_emitter: &Arc<EventEmitter>,
_stage_dir: &Path,
_execution_env: &Arc<dyn ExecutionEnvironment>,
_sandbox: &Arc<dyn Sandbox>,
) -> Result<CodergenResult, ArcError> {
Err(ArcError::Validation("bad config".to_string()))
}
@ -948,7 +948,7 @@ Some text in between.
_thread_id: Option<&str>,
_emitter: &Arc<EventEmitter>,
_stage_dir: &std::path::Path,
_execution_env: &Arc<dyn ExecutionEnvironment>,
_sandbox: &Arc<dyn Sandbox>,
) -> Result<CodergenResult, ArcError> {
*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<EventEmitter>,
_stage_dir: &std::path::Path,
_execution_env: &Arc<dyn ExecutionEnvironment>,
_sandbox: &Arc<dyn Sandbox>,
) -> Result<CodergenResult, ArcError> {
*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<EventEmitter>,
_stage_dir: &std::path::Path,
_execution_env: &Arc<dyn ExecutionEnvironment>,
_sandbox: &Arc<dyn Sandbox>,
) -> Result<CodergenResult, ArcError> {
panic!("run() should not be called in one_shot mode");
}

View file

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

View file

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

View file

@ -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<EventEmitter>,
execution_env: &Arc<dyn ExecutionEnvironment>,
sandbox: &Arc<dyn Sandbox>,
) -> Result<Candidate, ArcError> {
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<EventEmitter>,
_stage_dir: &std::path::Path,
_execution_env: &Arc<dyn ExecutionEnvironment>,
_sandbox: &Arc<dyn Sandbox>,
) -> Result<CodergenResult, ArcError> {
// Return text that contains the ID "branch_b"
Ok(CodergenResult::Text {

View file

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

View file

@ -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<HandlerRegistry>,
pub emitter: Arc<EventEmitter>,
pub execution_env: Arc<dyn ExecutionEnvironment>,
pub sandbox: Arc<dyn Sandbox>,
/// 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<Option<Arc<GitState>>>,

View file

@ -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<dyn ExecutionEnvironment>,
struct WorktreeSandbox {
inner: Arc<dyn Sandbox>,
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<usize>,
) -> Result<Vec<arc_agent::execution_env::DirEntry>, String> {
) -> Result<Vec<arc_agent::sandbox::DirEntry>, 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<String, String>>,
cancel_token: Option<tokio_util::sync::CancellationToken>,
) -> Result<arc_agent::execution_env::ExecResult, String> {
) -> Result<arc_agent::sandbox::ExecResult, String> {
// 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<Vec<String>, 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<dyn ExecutionEnvironment>,
sandbox: Arc<dyn Sandbox>,
worktree_path: Option<PathBuf>,
}
@ -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<dyn ExecutionEnvironment>, Option<PathBuf>) =
let (branch_sandbox, worktree_path): (Arc<dyn Sandbox>, Option<PathBuf>) =
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<dyn ExecutionEnvironment> = Arc::new(
arc_agent::LocalExecutionEnvironment::new(wt_path.clone()),
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(WorktreeEnv {
inner: Arc::clone(&services.execution_env),
let env: Arc<dyn Sandbox> = 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(&registry),
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),

View file

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

View file

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

View file

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

View file

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

View file

@ -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<dyn ExecutionEnvironment>,
sandbox: &Arc<dyn Sandbox>,
logs_root: &Path,
llm_client: &Client,
provider: Provider,
model: &str,
) -> anyhow::Result<RetroNarrative> {
// 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<Mutex<Option<RetroNarrative>>> = 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<dyn ProviderProfile> {
}
async fn upload_data_files(
execution_env: &Arc<dyn ExecutionEnvironment>,
sandbox: &Arc<dyn Sandbox>,
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}"))?;

View file

@ -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<dyn ExecutionEnvironment> = Arc::new(env);
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(env);
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(env);
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(env);
let env: Arc<dyn Sandbox> = 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<dyn ExecutionEnvironment> = Arc::new(env);
let env: Arc<dyn Sandbox> = Arc::new(env);
// Install git if not available
let git_check = env

View file

@ -32,8 +32,8 @@ use arc_workflows::transform::{
};
use arc_workflows::validation::{validate, validate_or_raise, Severity};
fn local_env() -> Arc<dyn arc_agent::ExecutionEnvironment> {
Arc::new(arc_agent::LocalExecutionEnvironment::new(
fn local_env() -> Arc<dyn arc_agent::Sandbox> {
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<EventEmitter>,
_stage_dir: &std::path::Path,
_execution_env: &Arc<dyn arc_agent::ExecutionEnvironment>,
_sandbox: &Arc<dyn arc_agent::Sandbox>,
) -> Result<CodergenResult, ArcError> {
Ok(CodergenResult::Text {
text: format!(
@ -5497,7 +5497,7 @@ mod real_llm {
_thread_id: Option<&str>,
_emitter: &Arc<EventEmitter>,
_stage_dir: &std::path::Path,
_execution_env: &Arc<dyn arc_agent::ExecutionEnvironment>,
_sandbox: &Arc<dyn arc_agent::Sandbox>,
) -> Result<CodergenResult, ArcError> {
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<dyn arc_agent::ExecutionEnvironment> = test_env.clone();
let env: Arc<dyn arc_agent::Sandbox> = 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<dyn arc_agent::ExecutionEnvironment> =
let env: Arc<dyn arc_agent::Sandbox> =
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<dyn arc_agent::ExecutionEnvironment> = test_env.clone();
let env: Arc<dyn arc_agent::Sandbox> = 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<dyn arc_agent::ExecutionEnvironment> = Arc::new(FailingCliEnv);
let failing_env: Arc<dyn arc_agent::Sandbox> = 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<dyn arc_agent::ExecutionEnvironment> =
let env: Arc<dyn arc_agent::Sandbox> =
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<dyn arc_agent::ExecutionEnvironment> = test_env.clone();
let env: Arc<dyn arc_agent::Sandbox> = 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<dyn arc_agent::ExecutionEnvironment> = test_env.clone();
let env: Arc<dyn arc_agent::Sandbox> = 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<dyn arc_agent::ExecutionEnvironment> = Arc::new(CliTestEnv::new(claude_output));
let env: Arc<dyn arc_agent::Sandbox> = 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<dyn arc_agent::ExecutionEnvironment> = Arc::new(CliTestEnv::new(claude_output));
let env: Arc<dyn arc_agent::Sandbox> = 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<dyn arc_agent::ExecutionEnvironment> = Arc::new(CliTestEnv::new(codex_output));
let env: Arc<dyn arc_agent::Sandbox> = 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<dyn arc_agent::ExecutionEnvironment> = Arc::new(CliTestEnv::new(claude_output));
let env: Arc<dyn arc_agent::Sandbox> = 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<dyn arc_agent::ExecutionEnvironment> = Arc::new(CliTestEnv::new(claude_output));
let env: Arc<dyn arc_agent::Sandbox> = 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<Outcome, ArcError> {
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<dyn arc_agent::ExecutionEnvironment> = Arc::new(
arc_agent::LocalExecutionEnvironment::new(worktree_path.clone()),
let env: Arc<dyn arc_agent::Sandbox> = 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<dyn arc_agent::ExecutionEnvironment> = Arc::new(
arc_agent::LocalExecutionEnvironment::new(worktree_path.clone()),
let env: Arc<dyn arc_agent::Sandbox> = 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<dyn arc_agent::ExecutionEnvironment> = Arc::new(
arc_agent::LocalExecutionEnvironment::new(worktree_path.clone()),
let env: Arc<dyn arc_agent::Sandbox> = Arc::new(
arc_agent::LocalSandbox::new(worktree_path.clone()),
);
let mut registry = HandlerRegistry::new(Box::new(FileWriterHandler));

View file

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