Skip to content

Commit 9721488

Browse files
ZhiXiao-Linclaude
andcommitted
feat(sandbox): add A3S Box sandbox integration as built-in tool
- New `sandbox` Cargo feature with optional `a3s-box-sdk` dependency - `SandboxConfig` + `BashSandbox` trait in src/sandbox.rs - `BoxSandboxHandle`: lazy MicroVM boot, workspace mount at /workspace, Drop cleanup - `BashTool`: delegates to sandbox when ctx.sandbox is set (transparent routing) - New `SandboxTool` built-in: LLM-callable, registered under `sandbox` feature - `ToolContext::with_sandbox()` + `ToolRegistry::set_sandbox()` for wiring - `SessionOptions::with_sandbox(SandboxConfig)` builder - `AgentSession::bash()` now uses execute_with_context (sandbox-aware) - 13 new tests covering config, trait, BashTool delegation, SessionOptions - Docs: sandbox section in sessions.mdx + README, test count 1166→1179 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fab8a09 commit 9721488

11 files changed

Lines changed: 2019 additions & 13 deletions

File tree

Cargo.lock

Lines changed: 1166 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ let result = session.send("Refactor auth to use JWT").await?;
1111
[![Crates.io](https://img.shields.io/crates/v/a3s-code-core.svg)](https://crates.io/crates/a3s-code-core)
1212
[![Documentation](https://docs.rs/a3s-code-core/badge.svg)](https://docs.rs/a3s-code-core)
1313
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
14-
[![Tests](https://img.shields.io/badge/tests-1166%20passing-brightgreen.svg)](./core/tests)
14+
[![Tests](https://img.shields.io/badge/tests-1179%20passing-brightgreen.svg)](./core/tests)
1515

1616
---
1717

@@ -123,13 +123,14 @@ print(result.text)
123123

124124
## Core Features
125125

126-
### 🛠️ Built-in Tools (11)
126+
### 🛠️ Built-in Tools (11 + 1 optional)
127127

128128
| Category | Tools | Description |
129129
|----------|-------|-------------|
130130
| **File Operations** | `read`, `write`, `edit`, `patch` | Read/write files, apply diffs |
131131
| **Search** | `grep`, `glob`, `ls` | Search content, find files, list directories |
132132
| **Execution** | `bash` | Execute shell commands |
133+
| **Sandbox** | `sandbox` | MicroVM execution via A3S Box (`sandbox` feature) |
133134
| **Web** | `web_fetch`, `web_search` | Fetch URLs, search the web |
134135
| **Subagents** | `task` | Delegate to specialized child agents |
135136

@@ -237,6 +238,33 @@ Advanced features: retry policies, rate limiting, priority boost, pressure monit
237238

238239
---
239240

241+
### 🔒 Sandbox Execution (A3S Box Integration)
242+
243+
Route `bash` commands through an A3S Box MicroVM for isolated execution. Requires the `sandbox` Cargo feature.
244+
245+
**Transparent routing** — configure once, bash tool uses sandbox automatically:
246+
247+
```rust
248+
use a3s_code_core::{SessionOptions, SandboxConfig};
249+
250+
SessionOptions::new().with_sandbox(SandboxConfig {
251+
image: "ubuntu:22.04".into(),
252+
memory_mb: 512,
253+
network: false,
254+
..SandboxConfig::default()
255+
})
256+
```
257+
258+
**Explicit `sandbox` tool** — with `sandbox` feature enabled, the LLM can call the `sandbox` tool directly. Workspace is mounted at `/workspace` inside the MicroVM.
259+
260+
Enable:
261+
262+
```toml
263+
a3s-code-core = { version = "0.7", features = ["sandbox"] }
264+
```
265+
266+
---
267+
240268
### 🌐 Multi-Machine Distribution
241269

242270
Offload tool execution to external workers via three handler modes:
@@ -476,7 +504,7 @@ cargo test # All tests
476504
cargo test --lib # Unit tests only
477505
```
478506

479-
**Test Coverage:** 1166 tests, 100% pass rate
507+
**Test Coverage:** 1179 tests, 100% pass rate
480508

481509
---
482510

core/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ path = "src/lib.rs"
1616
a3s-common = { version = "0.1", path = "../../common" }
1717
a3s-lane = { version = "0.4", path = "../../lane" }
1818
a3s-search = { version = "0.8", path = "../../search", default-features = false }
19+
# Sandbox integration (optional — requires `sandbox` feature)
20+
a3s-box-sdk = { version = "0.5", path = "../../box/src/sdk", optional = true }
1921

2022
# Async runtime
2123
tokio = { version = "1.35", features = [
@@ -89,6 +91,10 @@ chrono = { version = "0.4", features = ["serde"] }
8991

9092
[features]
9193
default = []
94+
# Enable A3S Box sandbox integration for the `bash` tool.
95+
# When active, `SessionOptions::with_sandbox()` routes bash commands through
96+
# a MicroVM sandbox instead of `std::process::Command`.
97+
sandbox = ["dep:a3s-box-sdk"]
9298

9399
[dev-dependencies]
94100
tempfile = "3.10"

core/src/agent_api.rs

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ pub struct SessionOptions {
9696
/// aborting in non-streaming mode (overrides default of 3).
9797
/// `None` uses the `AgentConfig` default.
9898
pub circuit_breaker_threshold: Option<u32>,
99+
/// Optional sandbox configuration.
100+
///
101+
/// When set, `bash` tool commands are routed through an A3S Box MicroVM
102+
/// sandbox instead of `std::process::Command`. Requires the `sandbox`
103+
/// Cargo feature to be enabled.
104+
pub sandbox_config: Option<crate::sandbox::SandboxConfig>,
99105
}
100106

101107
impl std::fmt::Debug for SessionOptions {
@@ -124,6 +130,7 @@ impl std::fmt::Debug for SessionOptions {
124130
.field("max_parse_retries", &self.max_parse_retries)
125131
.field("tool_timeout_ms", &self.tool_timeout_ms)
126132
.field("circuit_breaker_threshold", &self.circuit_breaker_threshold)
133+
.field("sandbox_config", &self.sandbox_config)
127134
.finish()
128135
}
129136
}
@@ -334,6 +341,29 @@ impl SessionOptions {
334341
.with_tool_timeout(120_000)
335342
.with_circuit_breaker(3)
336343
}
344+
345+
/// Route `bash` tool execution through an A3S Box MicroVM sandbox.
346+
///
347+
/// The workspace directory is mounted read-write at `/workspace` inside
348+
/// the sandbox. Requires the `sandbox` Cargo feature; without it a warning
349+
/// is logged and bash commands continue to run locally.
350+
///
351+
/// # Example
352+
///
353+
/// ```rust,no_run
354+
/// use a3s_code_core::{SessionOptions, SandboxConfig};
355+
///
356+
/// SessionOptions::new().with_sandbox(SandboxConfig {
357+
/// image: "ubuntu:22.04".into(),
358+
/// memory_mb: 512,
359+
/// network: false,
360+
/// ..SandboxConfig::default()
361+
/// });
362+
/// ```
363+
pub fn with_sandbox(mut self, config: crate::sandbox::SandboxConfig) -> Self {
364+
self.sandbox_config = Some(config);
365+
self
366+
}
337367
}
338368

339369
// ============================================================================
@@ -592,6 +622,28 @@ impl Agent {
592622
tool_context = tool_context.with_search_config(search_config.clone());
593623
}
594624

625+
// Wire sandbox when configured.
626+
#[cfg(feature = "sandbox")]
627+
if let Some(ref sandbox_cfg) = opts.sandbox_config {
628+
let handle: Arc<dyn crate::sandbox::BashSandbox> = Arc::new(
629+
crate::sandbox::BoxSandboxHandle::new(
630+
sandbox_cfg.clone(),
631+
canonical.display().to_string(),
632+
),
633+
);
634+
// Update the registry's default context so that direct
635+
// `AgentSession::bash()` calls also use the sandbox.
636+
tool_executor.registry().set_sandbox(Arc::clone(&handle));
637+
tool_context = tool_context.with_sandbox(handle);
638+
}
639+
#[cfg(not(feature = "sandbox"))]
640+
if opts.sandbox_config.is_some() {
641+
tracing::warn!(
642+
"sandbox_config is set but the `sandbox` Cargo feature is not enabled \
643+
— bash commands will run locally"
644+
);
645+
}
646+
595647
// Resolve memory store: explicit store takes priority, then file_memory_dir
596648
let memory = {
597649
let store = if let Some(ref store) = opts.memory_store {
@@ -837,9 +889,15 @@ impl AgentSession {
837889
}
838890

839891
/// Execute a bash command in the workspace.
892+
///
893+
/// When a sandbox is configured via [`SessionOptions::with_sandbox()`],
894+
/// the command is routed through the A3S Box sandbox.
840895
pub async fn bash(&self, command: &str) -> Result<String> {
841896
let args = serde_json::json!({ "command": command });
842-
let result = self.tool_executor.execute("bash", &args).await?;
897+
let result = self
898+
.tool_executor
899+
.execute_with_context("bash", &args, &self.tool_context)
900+
.await?;
843901
Ok(result.output)
844902
}
845903

@@ -1425,4 +1483,49 @@ mod tests {
14251483
assert_eq!(opts.session_id, Some("test-id".to_string()));
14261484
assert!(opts.auto_save);
14271485
}
1486+
1487+
// ========================================================================
1488+
// Sandbox Tests
1489+
// ========================================================================
1490+
1491+
#[test]
1492+
fn test_session_options_with_sandbox_sets_config() {
1493+
use crate::sandbox::SandboxConfig;
1494+
let cfg = SandboxConfig {
1495+
image: "ubuntu:22.04".into(),
1496+
memory_mb: 1024,
1497+
..SandboxConfig::default()
1498+
};
1499+
let opts = SessionOptions::new().with_sandbox(cfg);
1500+
assert!(opts.sandbox_config.is_some());
1501+
let sc = opts.sandbox_config.unwrap();
1502+
assert_eq!(sc.image, "ubuntu:22.04");
1503+
assert_eq!(sc.memory_mb, 1024);
1504+
}
1505+
1506+
#[test]
1507+
fn test_session_options_default_has_no_sandbox() {
1508+
let opts = SessionOptions::default();
1509+
assert!(opts.sandbox_config.is_none());
1510+
}
1511+
1512+
#[tokio::test]
1513+
async fn test_session_debug_includes_sandbox_config() {
1514+
use crate::sandbox::SandboxConfig;
1515+
let opts = SessionOptions::new().with_sandbox(SandboxConfig::default());
1516+
let debug = format!("{:?}", opts);
1517+
assert!(debug.contains("sandbox_config"));
1518+
}
1519+
1520+
#[tokio::test]
1521+
async fn test_session_build_with_sandbox_config_no_feature_warn() {
1522+
// When feature is not enabled, build_session should still succeed
1523+
// (it just logs a warning). With feature enabled, it creates a handle.
1524+
let agent = Agent::from_config(test_config()).await.unwrap();
1525+
let opts = SessionOptions::new()
1526+
.with_sandbox(crate::sandbox::SandboxConfig::default());
1527+
// build_session should not fail even if sandbox feature is off
1528+
let session = agent.session("/tmp/test-sandbox-session", Some(opts));
1529+
assert!(session.is_ok());
1530+
}
14281531
}

core/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
pub mod agent;
5757
pub mod agent_api;
5858
pub mod config;
59+
pub mod sandbox;
5960
pub mod context;
6061
pub mod error;
6162
pub mod file_history;
@@ -94,4 +95,5 @@ pub use queue::{
9495
pub use session::{SessionConfig, SessionManager, SessionState};
9596
pub use session_lane_queue::SessionLaneQueue;
9697
pub use skills::{builtin_skills, Skill, SkillKind};
98+
pub use sandbox::SandboxConfig;
9799
pub use tools::{ToolContext, ToolExecutor, ToolResult};

0 commit comments

Comments
 (0)