pub mod docker;
pub mod k8s;
pub mod local;
pub mod manager;
pub mod policy;
use echo_core::error::Result;
use futures::future::BoxFuture;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::LazyLock;
use std::time::Duration;
pub use docker::{DockerConfig, DockerSandbox};
pub use k8s::{K8sConfig, K8sSandbox};
pub use local::{LocalConfig, LocalSandbox};
pub use manager::SandboxManager;
pub use policy::{SandboxPolicy, SecurityLevel};
pub static SENSITIVE_MOUNT_PATHS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
vec![
"/etc", "/proc", "/sys", "/dev", "/boot", "/run", "/var/run", "/var/log",
]
});
pub fn default_language_image(lang: &str) -> Option<&'static str> {
match lang {
"python" | "python3" => Some("python:3.12-slim"),
"node" | "javascript" | "js" | "typescript" | "ts" => Some("node:20-slim"),
"ruby" => Some("ruby:3.3-slim"),
"go" | "golang" => Some("golang:1.22-alpine"),
"rust" => Some("rust:1.77-slim"),
"perl" => Some("perl:5.38-slim"),
"php" => Some("php:8.3-cli"),
"lua" => Some("alpine:3.19"),
"r" => Some("rocker/r-base:latest"),
"julia" => Some("julia:1.10-slim"),
"swift" => Some("swift:5.9-slim"),
_ => None,
}
}
pub fn select_image_for_command(
command: &SandboxCommand,
language_images: &HashMap<String, String>,
default_image: &str,
) -> String {
match &command.kind {
CommandKind::Code { language, .. } => {
if let Some(img) = language_images.get(language) {
return img.clone();
}
if let Some(img) = default_language_image(language) {
return img.to_string();
}
default_image.to_string()
}
CommandKind::Shell(cmd) => {
let raw = cmd.split_whitespace().next().unwrap_or("");
if raw.contains('/') || raw.contains('\\') {
return default_image.to_string();
}
let base = std::path::Path::new(raw)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(raw);
if let Some(img) = language_images.get(base) {
return img.clone();
}
if let Some(img) = default_language_image(base) {
return img.to_string();
}
default_image.to_string()
}
CommandKind::Program { program, .. } => {
let name = std::path::Path::new(program)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(program);
if let Some(img) = language_images.get(name) {
return img.clone();
}
if let Some(img) = default_language_image(name) {
return img.to_string();
}
default_image.to_string()
}
}
}
pub trait SandboxExecutor: Send + Sync {
fn name(&self) -> &str;
fn isolation_level(&self) -> IsolationLevel;
fn is_available(&self) -> BoxFuture<'_, bool>;
fn execute(&self, command: SandboxCommand) -> BoxFuture<'_, Result<ExecutionResult>>;
fn execute_with_limits(
&self,
command: SandboxCommand,
limits: ResourceLimits,
) -> BoxFuture<'_, Result<ExecutionResult>> {
let _ = limits;
self.execute(command)
}
fn cleanup(&self) -> BoxFuture<'_, Result<()>> {
Box::pin(async { Ok(()) })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum IsolationLevel {
None = 0,
Process = 1,
OsSandbox = 2,
Container = 3,
Orchestrated = 4,
}
impl std::fmt::Display for IsolationLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IsolationLevel::None => write!(f, "none"),
IsolationLevel::Process => write!(f, "process"),
IsolationLevel::OsSandbox => write!(f, "os-sandbox"),
IsolationLevel::Container => write!(f, "container"),
IsolationLevel::Orchestrated => write!(f, "orchestrated"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxCommand {
pub kind: CommandKind,
pub working_dir: Option<PathBuf>,
pub env: HashMap<String, String>,
pub timeout: Duration,
pub stdin: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CommandKind {
Shell(String),
Program { program: String, args: Vec<String> },
Code { language: String, code: String },
}
impl SandboxCommand {
pub fn shell(cmd: impl Into<String>) -> Self {
Self {
kind: CommandKind::Shell(cmd.into()),
working_dir: None,
env: HashMap::new(),
timeout: Duration::from_secs(30),
stdin: None,
}
}
pub fn program(program: impl Into<String>, args: Vec<String>) -> Self {
Self {
kind: CommandKind::Program {
program: program.into(),
args,
},
working_dir: None,
env: HashMap::new(),
timeout: Duration::from_secs(30),
stdin: None,
}
}
pub fn code(language: impl Into<String>, code: impl Into<String>) -> Self {
Self {
kind: CommandKind::Code {
language: language.into(),
code: code.into(),
},
working_dir: None,
env: HashMap::new(),
timeout: Duration::from_secs(30),
stdin: None,
}
}
pub fn with_working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.working_dir = Some(dir.into());
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.insert(key.into(), value.into());
self
}
pub fn with_stdin(mut self, stdin: impl Into<String>) -> Self {
self.stdin = Some(stdin.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionResult {
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub duration: Duration,
pub sandbox_type: String,
pub timed_out: bool,
}
impl ExecutionResult {
pub fn success(&self) -> bool {
self.exit_code == 0 && !self.timed_out
}
pub fn combined_output(&self) -> String {
if self.stderr.is_empty() {
self.stdout.clone()
} else if self.stdout.is_empty() {
self.stderr.clone()
} else {
format!("{}\n{}", self.stdout, self.stderr)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceLimits {
pub cpu_time_secs: Option<u64>,
pub memory_bytes: Option<u64>,
pub max_output_bytes: Option<u64>,
pub max_processes: Option<u32>,
pub network: bool,
pub read_only_paths: Vec<PathBuf>,
pub writable_paths: Vec<PathBuf>,
}
impl Default for ResourceLimits {
fn default() -> Self {
Self {
cpu_time_secs: Some(30),
memory_bytes: Some(256 * 1024 * 1024), max_output_bytes: Some(1024 * 1024), max_processes: Some(64),
network: false,
read_only_paths: vec![],
writable_paths: vec![],
}
}
}
impl ResourceLimits {
pub fn unrestricted() -> Self {
Self {
cpu_time_secs: None,
memory_bytes: None,
max_output_bytes: None,
max_processes: None,
network: true,
read_only_paths: vec![],
writable_paths: vec![],
}
}
pub fn strict() -> Self {
Self {
cpu_time_secs: Some(10),
memory_bytes: Some(64 * 1024 * 1024), max_output_bytes: Some(256 * 1024), max_processes: Some(8),
network: false,
read_only_paths: vec![],
writable_paths: vec![],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_select_image_shell_path_uses_default_image() {
let mut language_images = HashMap::new();
language_images.insert("python3".to_string(), "python:3.12-slim".to_string());
let cmd = SandboxCommand::shell("/usr/bin/python3 script.py");
let image = select_image_for_command(&cmd, &language_images, "ubuntu:22.04");
assert_eq!(image, "ubuntu:22.04");
}
#[test]
fn test_select_image_shell_binary_name_mapping() {
let mut language_images = HashMap::new();
language_images.insert("python3".to_string(), "python:3.12-slim".to_string());
let cmd = SandboxCommand::shell("python3 script.py");
let image = select_image_for_command(&cmd, &language_images, "ubuntu:22.04");
assert_eq!(image, "python:3.12-slim");
}
}