use crate::types::{Layer3Result, ToolRequest, ToolResponse};
use async_trait::async_trait;
use parking_lot::RwLock;
use sh_layer1::generate_short_id;
use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Command;
use std::time::{Duration, Instant};
#[async_trait]
pub trait SandboxRuntime: Send + Sync {
async fn create(&self, config: SandboxConfig) -> Layer3Result<SandboxId>;
async fn destroy(&self, id: &SandboxId) -> Layer3Result<bool>;
async fn execute(
&self,
id: &SandboxId,
code: &str,
language: &str,
) -> Layer3Result<ExecutionResult>;
async fn execute_tool(
&self,
id: &SandboxId,
request: ToolRequest,
) -> Layer3Result<ToolResponse>;
async fn status(&self, id: &SandboxId) -> Layer3Result<SandboxStatus>;
async fn info(&self, id: &SandboxId) -> Layer3Result<Option<SandboxInfo>>;
async fn list(&self) -> Layer3Result<Vec<SandboxInfo>>;
async fn reset(&self, id: &SandboxId) -> Layer3Result<bool>;
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SandboxId(pub String);
impl std::fmt::Display for SandboxId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct SandboxConfig {
pub base_image: String,
pub limits: SandboxLimits,
pub network: NetworkPolicy,
pub filesystem: FsPolicy,
pub env_vars: HashMap<String, String>,
pub working_dir: PathBuf,
pub timeout_secs: u64,
pub interactive: bool,
}
impl Default for SandboxConfig {
fn default() -> Self {
Self {
base_image: "default".to_string(),
limits: SandboxLimits::default(),
network: NetworkPolicy::Disabled,
filesystem: FsPolicy::ReadOnly,
env_vars: HashMap::new(),
working_dir: PathBuf::from("/sandbox"),
timeout_secs: 30,
interactive: false,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SandboxLimits {
pub max_memory: Option<u64>,
pub max_cpu_percent: Option<u32>,
pub max_file_size: Option<u64>,
pub max_processes: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NetworkPolicy {
Disabled,
OutboundOnly,
RestrictedPorts(Vec<u16>),
Full,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FsPolicy {
ReadOnly,
RestrictedDirs(Vec<PathBuf>),
TempWritable,
FullWritable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SandboxStatus {
Creating,
Ready,
Running,
Paused,
Error,
Destroyed,
}
#[derive(Debug, Clone)]
pub struct SandboxInfo {
pub id: SandboxId,
pub status: SandboxStatus,
pub created_at: chrono::DateTime<chrono::Utc>,
pub memory_used: u64,
pub cpu_used: f32,
pub executions: u32,
pub config: SandboxConfig,
}
#[derive(Debug, Clone)]
pub struct ExecutionResult {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
pub duration_ms: u64,
pub timed_out: bool,
pub killed: bool,
}
impl ExecutionResult {
pub fn success(stdout: String) -> Self {
Self {
stdout,
stderr: String::new(),
exit_code: 0,
duration_ms: 0,
timed_out: false,
killed: false,
}
}
pub fn failure(stderr: String, exit_code: i32) -> Self {
Self {
stdout: String::new(),
stderr,
exit_code,
duration_ms: 0,
timed_out: false,
killed: false,
}
}
pub fn timeout(stdout: String, stderr: String) -> Self {
Self {
stdout,
stderr,
exit_code: -1,
duration_ms: 0,
timed_out: true,
killed: false,
}
}
pub fn is_success(&self) -> bool {
self.exit_code == 0 && !self.timed_out && !self.killed
}
}
pub struct DefaultSandboxRuntime {
sandboxes: RwLock<HashMap<String, SandboxInfo>>,
temp_dir: PathBuf,
}
impl DefaultSandboxRuntime {
pub fn new() -> Layer3Result<Self> {
let temp_dir = std::env::temp_dir().join("continuum_sandboxes");
std::fs::create_dir_all(&temp_dir)?;
Ok(Self {
sandboxes: RwLock::new(HashMap::new()),
temp_dir,
})
}
fn get_language_command(language: &str) -> Option<(&'static str, &'static str)> {
match language.to_lowercase().as_str() {
"python" | "python3" | "py" => Some(("python", "-c")),
"javascript" | "js" | "node" => Some(("node", "-e")),
"ruby" | "rb" => Some(("ruby", "-e")),
"perl" | "pl" => Some(("perl", "-e")),
"bash" | "sh" | "shell" => Some(("bash", "-c")),
"lua" => Some(("lua", "-e")),
_ => None,
}
}
fn command_exists(cmd: &str) -> bool {
#[cfg(target_os = "windows")]
{
Command::new("where")
.arg(cmd)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[cfg(not(target_os = "windows"))]
{
Command::new("which")
.arg(cmd)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
}
fn execute_with_timeout(
&self,
cmd: &str,
args: &[&str],
input: Option<&str>,
timeout_secs: u64,
) -> ExecutionResult {
let start = Instant::now();
let mut command = Command::new(cmd);
command.args(args);
command.env_clear();
command.env("PATH", std::env::var("PATH").unwrap_or_default());
command.current_dir(&self.temp_dir);
command.stdout(std::process::Stdio::piped());
command.stderr(std::process::Stdio::piped());
if input.is_some() {
command.stdin(std::process::Stdio::piped());
}
let spawn_result = command.spawn();
match spawn_result {
Ok(mut child) => {
if let Some(input_data) = input {
use std::io::Write;
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(input_data.as_bytes());
}
}
let timeout = Duration::from_secs(timeout_secs);
let result = child.wait_timeout(timeout);
match result {
Ok(Some(status)) => {
let stdout = read_child_stdout(&mut child);
let stderr = read_child_stderr(&mut child);
let duration_ms = start.elapsed().as_millis() as u64;
ExecutionResult {
stdout,
stderr,
exit_code: status.code().unwrap_or(-1),
duration_ms,
timed_out: false,
killed: false,
}
}
Ok(None) => {
let _ = child.kill();
let _ = child.wait();
let stdout = read_child_stdout(&mut child);
let stderr = read_child_stderr(&mut child);
ExecutionResult::timeout(stdout, stderr)
}
Err(e) => ExecutionResult::failure(format!("Wait error: {}", e), -1),
}
}
Err(e) => ExecutionResult::failure(format!("Spawn error: {}", e), -1),
}
}
}
impl Default for DefaultSandboxRuntime {
fn default() -> Self {
Self::new().expect("Failed to create DefaultSandboxRuntime")
}
}
#[async_trait]
impl SandboxRuntime for DefaultSandboxRuntime {
async fn create(&self, config: SandboxConfig) -> Layer3Result<SandboxId> {
let id = SandboxId(generate_short_id());
let info = SandboxInfo {
id: id.clone(),
status: SandboxStatus::Ready,
created_at: chrono::Utc::now(),
memory_used: 0,
cpu_used: 0.0,
executions: 0,
config,
};
self.sandboxes.write().insert(id.0.clone(), info);
tracing::info!("Created sandbox: {}", id);
Ok(id)
}
async fn destroy(&self, id: &SandboxId) -> Layer3Result<bool> {
let mut sandboxes = self.sandboxes.write();
if let Some(mut info) = sandboxes.remove(&id.0) {
info.status = SandboxStatus::Destroyed;
tracing::info!("Destroyed sandbox: {}", id);
Ok(true)
} else {
Ok(false)
}
}
async fn execute(
&self,
id: &SandboxId,
code: &str,
language: &str,
) -> Layer3Result<ExecutionResult> {
{
let sandboxes = self.sandboxes.read();
let info = sandboxes
.get(&id.0)
.ok_or_else(|| anyhow::anyhow!("Sandbox not found: {}", id))?;
if info.status != SandboxStatus::Ready {
return Err(anyhow::anyhow!("Sandbox not ready: {:?}", info.status));
}
}
{
let mut sandboxes = self.sandboxes.write();
if let Some(info) = sandboxes.get_mut(&id.0) {
info.status = SandboxStatus::Running;
}
}
let (cmd, flag) = Self::get_language_command(language)
.ok_or_else(|| anyhow::anyhow!("Unsupported language: {}", language))?;
if !Self::command_exists(cmd) {
return Err(anyhow::anyhow!("Command not found: {}", cmd));
}
let timeout = {
let sandboxes = self.sandboxes.read();
sandboxes
.get(&id.0)
.map(|i| i.config.timeout_secs)
.unwrap_or(30)
};
let result = self.execute_with_timeout(cmd, &[flag, code], None, timeout);
{
let mut sandboxes = self.sandboxes.write();
if let Some(info) = sandboxes.get_mut(&id.0) {
info.status = SandboxStatus::Ready;
info.executions += 1;
}
}
Ok(result)
}
async fn execute_tool(
&self,
id: &SandboxId,
request: ToolRequest,
) -> Layer3Result<ToolResponse> {
Err(anyhow::anyhow!(
"[experimental] Tool execution in sandbox is not yet implemented (sandbox_id: {}, tool: {})",
id, request.name
))
}
async fn status(&self, id: &SandboxId) -> Layer3Result<SandboxStatus> {
let sandboxes = self.sandboxes.read();
let info = sandboxes
.get(&id.0)
.ok_or_else(|| anyhow::anyhow!("Sandbox not found: {}", id))?;
Ok(info.status)
}
async fn info(&self, id: &SandboxId) -> Layer3Result<Option<SandboxInfo>> {
let sandboxes = self.sandboxes.read();
Ok(sandboxes.get(&id.0).cloned())
}
async fn list(&self) -> Layer3Result<Vec<SandboxInfo>> {
Ok(self.sandboxes.read().values().cloned().collect())
}
async fn reset(&self, id: &SandboxId) -> Layer3Result<bool> {
let mut sandboxes = self.sandboxes.write();
if let Some(info) = sandboxes.get_mut(&id.0) {
info.status = SandboxStatus::Ready;
info.executions = 0;
info.memory_used = 0;
info.cpu_used = 0.0;
tracing::info!("Reset sandbox: {}", id);
Ok(true)
} else {
Ok(false)
}
}
}
fn read_child_stdout(child: &mut std::process::Child) -> String {
use std::io::Read;
if let Some(mut stdout) = child.stdout.take() {
let mut buf = String::new();
let _ = stdout.read_to_string(&mut buf);
buf
} else {
String::new()
}
}
fn read_child_stderr(child: &mut std::process::Child) -> String {
use std::io::Read;
if let Some(mut stderr) = child.stderr.take() {
let mut buf = String::new();
let _ = stderr.read_to_string(&mut buf);
buf
} else {
String::new()
}
}
trait ChildWaitTimeout {
fn wait_timeout(
&mut self,
timeout: Duration,
) -> std::io::Result<Option<std::process::ExitStatus>>;
}
impl ChildWaitTimeout for std::process::Child {
fn wait_timeout(
&mut self,
timeout: Duration,
) -> std::io::Result<Option<std::process::ExitStatus>> {
let start = Instant::now();
loop {
match self.try_wait()? {
Some(status) => return Ok(Some(status)),
None => {
if start.elapsed() >= timeout {
return Ok(None);
}
std::thread::sleep(Duration::from_millis(10));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sandbox_config_default() {
let config = SandboxConfig::default();
assert_eq!(config.timeout_secs, 30);
assert_eq!(config.network, NetworkPolicy::Disabled);
}
#[test]
fn test_execution_result_success() {
let result = ExecutionResult::success("hello".to_string());
assert!(result.is_success());
}
#[test]
fn test_execution_result_timeout() {
let result = ExecutionResult::timeout("out".to_string(), "err".to_string());
assert!(result.timed_out);
assert!(!result.is_success());
}
#[test]
fn test_sandbox_id_display() {
let id = SandboxId("abc123".to_string());
assert_eq!(format!("{}", id), "abc123");
}
#[test]
fn test_language_command_mapping() {
assert!(DefaultSandboxRuntime::get_language_command("python").is_some());
assert!(DefaultSandboxRuntime::get_language_command("javascript").is_some());
assert!(DefaultSandboxRuntime::get_language_command("bash").is_some());
assert!(DefaultSandboxRuntime::get_language_command("unknown").is_none());
}
#[tokio::test]
async fn test_sandbox_create_and_destroy() {
let runtime = DefaultSandboxRuntime::new().unwrap();
let config = SandboxConfig::default();
let id = runtime.create(config).await.unwrap();
assert!(!id.0.is_empty());
let status = runtime.status(&id).await.unwrap();
assert_eq!(status, SandboxStatus::Ready);
let destroyed = runtime.destroy(&id).await.unwrap();
assert!(destroyed);
let status = runtime.status(&id).await;
assert!(status.is_err());
}
#[tokio::test]
async fn test_sandbox_list() {
let runtime = DefaultSandboxRuntime::new().unwrap();
let id1 = runtime.create(SandboxConfig::default()).await.unwrap();
let id2 = runtime.create(SandboxConfig::default()).await.unwrap();
let list = runtime.list().await.unwrap();
assert_eq!(list.len(), 2);
runtime.destroy(&id1).await.unwrap();
runtime.destroy(&id2).await.unwrap();
let list = runtime.list().await.unwrap();
assert!(list.is_empty());
}
#[tokio::test]
async fn test_sandbox_reset() {
let runtime = DefaultSandboxRuntime::new().unwrap();
let id = runtime.create(SandboxConfig::default()).await.unwrap();
{
let mut sandboxes = runtime.sandboxes.write();
if let Some(info) = sandboxes.get_mut(&id.0) {
info.executions = 5;
}
}
let reset = runtime.reset(&id).await.unwrap();
assert!(reset);
let info = runtime.info(&id).await.unwrap().unwrap();
assert_eq!(info.executions, 0);
runtime.destroy(&id).await.unwrap();
}
#[test]
fn test_network_policy_equality() {
assert_eq!(NetworkPolicy::Disabled, NetworkPolicy::Disabled);
assert_ne!(NetworkPolicy::Disabled, NetworkPolicy::Full);
}
#[test]
fn test_fs_policy_equality() {
assert_eq!(FsPolicy::ReadOnly, FsPolicy::ReadOnly);
assert_ne!(FsPolicy::ReadOnly, FsPolicy::FullWritable);
}
#[test]
fn test_execution_result_failure() {
let result = ExecutionResult::failure("error".to_string(), 1);
assert!(!result.is_success());
assert_eq!(result.exit_code, 1);
}
#[tokio::test]
async fn test_execute_tool_returns_contextual_experimental_error() {
let runtime = DefaultSandboxRuntime::new().unwrap();
let sandbox_id = runtime.create(SandboxConfig::default()).await.unwrap();
let request = ToolRequest {
call_id: "call_1".to_string(),
name: "read_file".to_string(),
arguments: serde_json::json!({"path": "README.md"}),
};
let err = runtime
.execute_tool(&sandbox_id, request)
.await
.unwrap_err();
let message = err.to_string();
assert!(message.contains("[experimental]"));
assert!(message.contains(&sandbox_id.to_string()));
assert!(message.contains("read_file"));
}
}