botserver/src/basic/keywords/code_sandbox.rs
Rodrigo Rodriguez (Pragmatismo) e9dad539af Update dependencies and remove problematic crates
Drop image (with ravif/paste), sqlx, zitadel, and related dependencies
that were causing compilation issues. Replace image processing with
direct png crate usage. Update rcgen to 0.14 with new API changes.
Refactor CA certificate generation to use Issuer pattern.
2025-12-03 16:05:30 -03:00

1006 lines
34 KiB
Rust

//! Code Sandbox Module
//!
//! Provides sandboxed execution of Python and JavaScript code using LXC containers.
//! Supports the RUN keyword for executing arbitrary code safely.
//!
//! BASIC Keywords:
//! - RUN PYTHON "code"
//! - RUN JAVASCRIPT "code"
//! - RUN PYTHON WITH FILE "script.py"
//!
//! Config.csv properties:
//! ```csv
//! sandbox-enabled,true
//! sandbox-timeout,30
//! sandbox-memory-mb,256
//! sandbox-cpu-percent,50
//! sandbox-network,false
//! sandbox-runtime,lxc
//! sandbox-python-packages,numpy,pandas,requests
//! sandbox-allowed-paths,/data,/tmp
//! ```
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use diesel::prelude::*;
use log::{trace, warn};
use rhai::{Dynamic, Engine};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::process::Command;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::timeout;
use uuid::Uuid;
/// Supported sandbox runtimes
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SandboxRuntime {
/// LXC containers (recommended for security)
LXC,
/// Docker containers
Docker,
/// Firecracker microVMs (highest security)
Firecracker,
/// Direct process isolation (fallback, least secure)
Process,
}
impl Default for SandboxRuntime {
fn default() -> Self {
SandboxRuntime::Process
}
}
impl From<&str> for SandboxRuntime {
fn from(s: &str) -> Self {
match s.to_lowercase().as_str() {
"lxc" => SandboxRuntime::LXC,
"docker" => SandboxRuntime::Docker,
"firecracker" => SandboxRuntime::Firecracker,
_ => SandboxRuntime::Process,
}
}
}
/// Programming language for code execution
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum CodeLanguage {
Python,
JavaScript,
Bash,
}
impl From<&str> for CodeLanguage {
fn from(s: &str) -> Self {
match s.to_lowercase().as_str() {
"python" | "py" => CodeLanguage::Python,
"javascript" | "js" | "node" => CodeLanguage::JavaScript,
"bash" | "sh" | "shell" => CodeLanguage::Bash,
_ => CodeLanguage::Python,
}
}
}
impl CodeLanguage {
pub fn file_extension(&self) -> &str {
match self {
CodeLanguage::Python => "py",
CodeLanguage::JavaScript => "js",
CodeLanguage::Bash => "sh",
}
}
pub fn interpreter(&self) -> &str {
match self {
CodeLanguage::Python => "python3",
CodeLanguage::JavaScript => "node",
CodeLanguage::Bash => "bash",
}
}
pub fn lxc_image(&self) -> &str {
match self {
CodeLanguage::Python => "gb-sandbox-python",
CodeLanguage::JavaScript => "gb-sandbox-node",
CodeLanguage::Bash => "gb-sandbox-base",
}
}
}
/// Sandbox configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxConfig {
/// Whether sandbox execution is enabled
pub enabled: bool,
/// Sandbox runtime to use
pub runtime: SandboxRuntime,
/// Maximum execution time in seconds
pub timeout_seconds: u64,
/// Memory limit in MB
pub memory_limit_mb: u64,
/// CPU limit as percentage (0-100)
pub cpu_limit_percent: u32,
/// Whether network access is allowed
pub network_enabled: bool,
/// Working directory for execution
pub work_dir: String,
/// Additional environment variables
pub env_vars: HashMap<String, String>,
/// Allowed file paths for access
pub allowed_paths: Vec<String>,
/// Pre-installed Python packages
pub python_packages: Vec<String>,
}
impl Default for SandboxConfig {
fn default() -> Self {
Self {
enabled: true,
runtime: SandboxRuntime::Process,
timeout_seconds: 30,
memory_limit_mb: 256,
cpu_limit_percent: 50,
network_enabled: false,
work_dir: "/tmp/gb-sandbox".to_string(),
env_vars: HashMap::new(),
allowed_paths: vec!["/data".to_string(), "/tmp".to_string()],
python_packages: vec![],
}
}
}
impl SandboxConfig {
/// Load configuration from bot settings
pub fn from_bot_config(state: &AppState, bot_id: Uuid) -> Self {
let mut config = Self::default();
if let Ok(mut conn) = state.conn.get() {
#[derive(QueryableByName)]
struct ConfigRow {
#[diesel(sql_type = diesel::sql_types::Text)]
config_key: String,
#[diesel(sql_type = diesel::sql_types::Text)]
config_value: String,
}
let configs: Vec<ConfigRow> = diesel::sql_query(
"SELECT config_key, config_value FROM bot_configuration \
WHERE bot_id = $1 AND config_key LIKE 'sandbox-%'",
)
.bind::<diesel::sql_types::Uuid, _>(bot_id)
.load(&mut conn)
.unwrap_or_default();
for row in configs {
match row.config_key.as_str() {
"sandbox-enabled" => {
config.enabled = row.config_value.to_lowercase() == "true";
}
"sandbox-runtime" => {
config.runtime = SandboxRuntime::from(row.config_value.as_str());
}
"sandbox-timeout" => {
config.timeout_seconds = row.config_value.parse().unwrap_or(30);
}
// Support both old and new parameter names for backward compatibility
"sandbox-memory-mb" | "sandbox-memory-limit" => {
config.memory_limit_mb = row.config_value.parse().unwrap_or(256);
}
"sandbox-cpu-percent" | "sandbox-cpu-limit" => {
config.cpu_limit_percent = row.config_value.parse().unwrap_or(50);
}
"sandbox-network" | "sandbox-network-enabled" => {
config.network_enabled = row.config_value.to_lowercase() == "true";
}
"sandbox-python-packages" => {
config.python_packages = row
.config_value
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
"sandbox-allowed-paths" => {
config.allowed_paths = row
.config_value
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
_ => {}
}
}
}
config
}
}
/// Result of code execution
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionResult {
/// Standard output
pub stdout: String,
/// Standard error
pub stderr: String,
/// Exit code (0 = success)
pub exit_code: i32,
/// Execution time in milliseconds
pub execution_time_ms: u64,
/// Whether execution timed out
pub timed_out: bool,
/// Whether execution was killed due to resource limits
pub killed: bool,
/// Any error message
pub error: Option<String>,
}
impl ExecutionResult {
pub fn success(stdout: String, stderr: String, execution_time_ms: u64) -> Self {
Self {
stdout,
stderr,
exit_code: 0,
execution_time_ms,
timed_out: false,
killed: false,
error: None,
}
}
pub fn error(message: &str) -> Self {
Self {
stdout: String::new(),
stderr: String::new(),
exit_code: -1,
execution_time_ms: 0,
timed_out: false,
killed: false,
error: Some(message.to_string()),
}
}
pub fn timeout() -> Self {
Self {
stdout: String::new(),
stderr: "Execution timed out".to_string(),
exit_code: -1,
execution_time_ms: 0,
timed_out: true,
killed: true,
error: Some("Execution exceeded time limit".to_string()),
}
}
pub fn is_success(&self) -> bool {
self.exit_code == 0 && self.error.is_none() && !self.timed_out
}
pub fn output(&self) -> String {
if self.is_success() {
self.stdout.clone()
} else if let Some(err) = &self.error {
format!("Error: {}\n{}", err, self.stderr)
} else {
format!("Error (exit code {}): {}", self.exit_code, self.stderr)
}
}
}
/// Code Sandbox for safe execution
pub struct CodeSandbox {
config: SandboxConfig,
session_id: Uuid,
}
impl std::fmt::Debug for CodeSandbox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CodeSandbox")
.field("config", &self.config)
.field("session_id", &self.session_id)
.finish()
}
}
impl CodeSandbox {
pub fn new(config: SandboxConfig, session_id: Uuid) -> Self {
Self { config, session_id }
}
/// Execute code in the sandbox
pub async fn execute(&self, code: &str, language: CodeLanguage) -> ExecutionResult {
if !self.config.enabled {
return ExecutionResult::error("Sandbox execution is disabled");
}
let start_time = std::time::Instant::now();
let result = match self.config.runtime {
SandboxRuntime::LXC => self.execute_lxc(code, &language).await,
SandboxRuntime::Docker => self.execute_docker(code, &language).await,
SandboxRuntime::Firecracker => self.execute_firecracker(code, &language).await,
SandboxRuntime::Process => self.execute_process(code, &language).await,
};
let execution_time_ms = start_time.elapsed().as_millis() as u64;
match result {
Ok(mut exec_result) => {
exec_result.execution_time_ms = execution_time_ms;
exec_result
}
Err(e) => {
let mut err_result = ExecutionResult::error(&e);
err_result.execution_time_ms = execution_time_ms;
err_result
}
}
}
/// Execute code in LXC container
async fn execute_lxc(
&self,
code: &str,
language: &CodeLanguage,
) -> Result<ExecutionResult, String> {
// Create unique container name
let container_name = format!("gb-sandbox-{}", Uuid::new_v4());
// Ensure work directory exists
std::fs::create_dir_all(&self.config.work_dir)
.map_err(|e| format!("Failed to create work dir: {}", e))?;
// Write code to temp file
let code_file = format!(
"{}/{}.{}",
self.config.work_dir,
self.session_id,
language.file_extension()
);
std::fs::write(&code_file, code)
.map_err(|e| format!("Failed to write code file: {}", e))?;
// Build LXC command
let mut cmd = Command::new("lxc-execute");
cmd.arg("-n")
.arg(&container_name)
.arg("-f")
.arg(format!("/etc/lxc/{}.conf", language.lxc_image()))
.arg("--")
.arg(language.interpreter())
.arg(&code_file);
// Set resource limits via cgroups
cmd.env(
"LXC_CGROUP_MEMORY_LIMIT",
format!("{}M", self.config.memory_limit_mb),
)
.env(
"LXC_CGROUP_CPU_QUOTA",
format!("{}", self.config.cpu_limit_percent * 1000),
);
// Execute with timeout
let timeout_duration = Duration::from_secs(self.config.timeout_seconds);
let output = timeout(timeout_duration, async {
tokio::process::Command::new("lxc-execute")
.arg("-n")
.arg(&container_name)
.arg("-f")
.arg(format!("/etc/lxc/{}.conf", language.lxc_image()))
.arg("--")
.arg(language.interpreter())
.arg(&code_file)
.output()
.await
})
.await;
// Cleanup temp file
let _ = std::fs::remove_file(&code_file);
match output {
Ok(Ok(output)) => {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);
Ok(ExecutionResult {
stdout,
stderr,
exit_code,
execution_time_ms: 0,
timed_out: false,
killed: !output.status.success(),
error: None,
})
}
Ok(Err(e)) => Err(format!("LXC execution failed: {}", e)),
Err(_) => Ok(ExecutionResult::timeout()),
}
}
/// Execute code in Docker container
async fn execute_docker(
&self,
code: &str,
language: &CodeLanguage,
) -> Result<ExecutionResult, String> {
// Determine Docker image
let image = match language {
CodeLanguage::Python => "python:3.11-slim",
CodeLanguage::JavaScript => "node:20-slim",
CodeLanguage::Bash => "alpine:latest",
};
// Build Docker command
let args = vec![
"run".to_string(),
"--rm".to_string(),
"--network".to_string(),
if self.config.network_enabled {
"bridge"
} else {
"none"
}
.to_string(),
"--memory".to_string(),
format!("{}m", self.config.memory_limit_mb),
"--cpus".to_string(),
format!("{:.2}", self.config.cpu_limit_percent as f64 / 100.0),
"--read-only".to_string(),
"--security-opt".to_string(),
"no-new-privileges".to_string(),
image.to_string(),
language.interpreter().to_string(),
"-c".to_string(),
code.to_string(),
];
// Execute with timeout
let timeout_duration = Duration::from_secs(self.config.timeout_seconds);
let output = timeout(timeout_duration, async {
tokio::process::Command::new("docker")
.args(&args)
.output()
.await
})
.await;
match output {
Ok(Ok(output)) => {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);
Ok(ExecutionResult {
stdout,
stderr,
exit_code,
execution_time_ms: 0,
timed_out: false,
killed: !output.status.success(),
error: None,
})
}
Ok(Err(e)) => Err(format!("Docker execution failed: {}", e)),
Err(_) => Ok(ExecutionResult::timeout()),
}
}
/// Execute code in Firecracker microVM
async fn execute_firecracker(
&self,
code: &str,
language: &CodeLanguage,
) -> Result<ExecutionResult, String> {
// Firecracker requires more complex setup
// For now, fall back to process-based execution with a warning
warn!("Firecracker runtime not yet implemented, falling back to process isolation");
self.execute_process(code, language).await
}
/// Execute code in isolated process (fallback)
async fn execute_process(
&self,
code: &str,
language: &CodeLanguage,
) -> Result<ExecutionResult, String> {
// Create temp directory
let temp_dir = format!("{}/{}", self.config.work_dir, Uuid::new_v4());
std::fs::create_dir_all(&temp_dir)
.map_err(|e| format!("Failed to create temp dir: {}", e))?;
// Write code to temp file
let code_file = format!("{}/code.{}", temp_dir, language.file_extension());
std::fs::write(&code_file, code)
.map_err(|e| format!("Failed to write code file: {}", e))?;
// Build command based on language
let (cmd_name, cmd_args): (&str, Vec<&str>) = match language {
CodeLanguage::Python => ("python3", vec![&code_file]),
CodeLanguage::JavaScript => ("node", vec![&code_file]),
CodeLanguage::Bash => ("bash", vec![&code_file]),
};
// Execute with timeout
let timeout_duration = Duration::from_secs(self.config.timeout_seconds);
let output = timeout(timeout_duration, async {
tokio::process::Command::new(cmd_name)
.args(&cmd_args)
.current_dir(&temp_dir)
.env_clear()
.env("PATH", "/usr/local/bin:/usr/bin:/bin")
.env("HOME", &temp_dir)
.env("TMPDIR", &temp_dir)
.output()
.await
})
.await;
// Cleanup temp directory
let _ = std::fs::remove_dir_all(&temp_dir);
match output {
Ok(Ok(output)) => {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);
Ok(ExecutionResult {
stdout,
stderr,
exit_code,
execution_time_ms: 0,
timed_out: false,
killed: false,
error: None,
})
}
Ok(Err(e)) => Err(format!("Process execution failed: {}", e)),
Err(_) => Ok(ExecutionResult::timeout()),
}
}
/// Execute code from a file path
pub async fn execute_file(&self, file_path: &str, language: CodeLanguage) -> ExecutionResult {
match std::fs::read_to_string(file_path) {
Ok(code) => self.execute(&code, language).await,
Err(e) => ExecutionResult::error(&format!("Failed to read file: {}", e)),
}
}
}
/// Register code sandbox keywords with the engine
pub fn register_sandbox_keywords(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
run_python_keyword(state.clone(), user.clone(), engine);
run_javascript_keyword(state.clone(), user.clone(), engine);
run_bash_keyword(state.clone(), user.clone(), engine);
run_file_keyword(state.clone(), user.clone(), engine);
}
/// RUN PYTHON "code"
pub fn run_python_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine
.register_custom_syntax(
&["RUN", "PYTHON", "$expr$"],
false,
move |context, inputs| {
let code = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
trace!("RUN PYTHON for session: {}", user_clone.id);
let state_for_task = Arc::clone(&state_clone);
let session_id = user_clone.id;
let bot_id = user_clone.bot_id;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
let config = SandboxConfig::from_bot_config(&state_for_task, bot_id);
let sandbox = CodeSandbox::new(config, session_id);
sandbox.execute(&code, CodeLanguage::Python).await
});
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(60)) {
Ok(result) => Ok(Dynamic::from(result.output())),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"RUN PYTHON timed out".into(),
rhai::Position::NONE,
))),
}
},
)
.expect("Failed to register RUN PYTHON syntax");
}
/// RUN JAVASCRIPT "code"
pub fn run_javascript_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine
.register_custom_syntax(
&["RUN", "JAVASCRIPT", "$expr$"],
false,
move |context, inputs| {
let code = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
trace!("RUN JAVASCRIPT for session: {}", user_clone.id);
let state_for_task = Arc::clone(&state_clone);
let session_id = user_clone.id;
let bot_id = user_clone.bot_id;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
let config = SandboxConfig::from_bot_config(&state_for_task, bot_id);
let sandbox = CodeSandbox::new(config, session_id);
sandbox.execute(&code, CodeLanguage::JavaScript).await
});
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(60)) {
Ok(result) => Ok(Dynamic::from(result.output())),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"RUN JAVASCRIPT timed out".into(),
rhai::Position::NONE,
))),
}
},
)
.expect("Failed to register RUN JAVASCRIPT syntax");
// Also register JS as alias
let state_clone2 = Arc::clone(&state);
let user_clone2 = user.clone();
engine
.register_custom_syntax(&["RUN", "JS", "$expr$"], false, move |context, inputs| {
let code = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
let state_for_task = Arc::clone(&state_clone2);
let session_id = user_clone2.id;
let bot_id = user_clone2.bot_id;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
let config = SandboxConfig::from_bot_config(&state_for_task, bot_id);
let sandbox = CodeSandbox::new(config, session_id);
sandbox.execute(&code, CodeLanguage::JavaScript).await
});
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(60)) {
Ok(result) => Ok(Dynamic::from(result.output())),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"RUN JS timed out".into(),
rhai::Position::NONE,
))),
}
})
.expect("Failed to register RUN JS syntax");
}
/// RUN BASH "code"
pub fn run_bash_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine
.register_custom_syntax(&["RUN", "BASH", "$expr$"], false, move |context, inputs| {
let code = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
trace!("RUN BASH for session: {}", user_clone.id);
let state_for_task = Arc::clone(&state_clone);
let session_id = user_clone.id;
let bot_id = user_clone.bot_id;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
let config = SandboxConfig::from_bot_config(&state_for_task, bot_id);
let sandbox = CodeSandbox::new(config, session_id);
sandbox.execute(&code, CodeLanguage::Bash).await
});
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(60)) {
Ok(result) => Ok(Dynamic::from(result.output())),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"RUN BASH timed out".into(),
rhai::Position::NONE,
))),
}
})
.expect("Failed to register RUN BASH syntax");
}
/// RUN PYTHON WITH FILE "script.py"
pub fn run_file_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
let user_clone = user.clone();
engine
.register_custom_syntax(
&["RUN", "PYTHON", "WITH", "FILE", "$expr$"],
false,
move |context, inputs| {
let file_path = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
trace!(
"RUN PYTHON WITH FILE {} for session: {}",
file_path,
user_clone.id
);
let state_for_task = Arc::clone(&state_clone);
let session_id = user_clone.id;
let bot_id = user_clone.bot_id;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
let config = SandboxConfig::from_bot_config(&state_for_task, bot_id);
let sandbox = CodeSandbox::new(config, session_id);
sandbox.execute_file(&file_path, CodeLanguage::Python).await
});
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(60)) {
Ok(result) => Ok(Dynamic::from(result.output())),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"RUN PYTHON WITH FILE timed out".into(),
rhai::Position::NONE,
))),
}
},
)
.expect("Failed to register RUN PYTHON WITH FILE syntax");
// JavaScript file execution
let state_clone2 = Arc::clone(&state);
let user_clone2 = user.clone();
engine
.register_custom_syntax(
&["RUN", "JAVASCRIPT", "WITH", "FILE", "$expr$"],
false,
move |context, inputs| {
let file_path = context
.eval_expression_tree(&inputs[0])?
.to_string()
.trim_matches('"')
.to_string();
let state_for_task = Arc::clone(&state_clone2);
let session_id = user_clone2.id;
let bot_id = user_clone2.bot_id;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
let config = SandboxConfig::from_bot_config(&state_for_task, bot_id);
let sandbox = CodeSandbox::new(config, session_id);
sandbox
.execute_file(&file_path, CodeLanguage::JavaScript)
.await
});
let _ = tx.send(result);
});
match rx.recv_timeout(std::time::Duration::from_secs(60)) {
Ok(result) => Ok(Dynamic::from(result.output())),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"RUN JAVASCRIPT WITH FILE timed out".into(),
rhai::Position::NONE,
))),
}
},
)
.expect("Failed to register RUN JAVASCRIPT WITH FILE syntax");
}
// ============================================================================
// LXC Container Setup Templates
// ============================================================================
/// Generate LXC configuration for Python sandbox
pub fn generate_python_lxc_config() -> String {
r#"
# LXC configuration for Python sandbox
lxc.include = /usr/share/lxc/config/common.conf
lxc.arch = linux64
# Container name template
lxc.uts.name = gb-sandbox-python
# Root filesystem
lxc.rootfs.path = dir:/var/lib/lxc/gb-sandbox-python/rootfs
# Network - isolated by default
lxc.net.0.type = empty
# Resource limits
lxc.cgroup2.memory.max = 256M
lxc.cgroup2.cpu.max = 50000 100000
# Security
lxc.cap.drop = sys_admin sys_boot sys_module sys_time
lxc.apparmor.profile = generated
lxc.seccomp.profile = /usr/share/lxc/config/common.seccomp
# Mount points - minimal
lxc.mount.auto = proc:mixed sys:ro
lxc.mount.entry = /usr/bin/python3 usr/bin/python3 none ro,bind 0 0
lxc.mount.entry = /usr/lib/python3 usr/lib/python3 none ro,bind 0 0
lxc.mount.entry = tmpfs tmp tmpfs defaults 0 0
"#
.to_string()
}
/// Generate LXC configuration for Node.js sandbox
pub fn generate_node_lxc_config() -> String {
r#"
# LXC configuration for Node.js sandbox
lxc.include = /usr/share/lxc/config/common.conf
lxc.arch = linux64
# Container name template
lxc.uts.name = gb-sandbox-node
# Root filesystem
lxc.rootfs.path = dir:/var/lib/lxc/gb-sandbox-node/rootfs
# Network - isolated by default
lxc.net.0.type = empty
# Resource limits
lxc.cgroup2.memory.max = 256M
lxc.cgroup2.cpu.max = 50000 100000
# Security
lxc.cap.drop = sys_admin sys_boot sys_module sys_time
lxc.apparmor.profile = generated
lxc.seccomp.profile = /usr/share/lxc/config/common.seccomp
# Mount points - minimal
lxc.mount.auto = proc:mixed sys:ro
lxc.mount.entry = /usr/bin/node usr/bin/node none ro,bind 0 0
lxc.mount.entry = /usr/lib/node_modules usr/lib/node_modules none ro,bind 0 0
lxc.mount.entry = tmpfs tmp tmpfs defaults 0 0
"#
.to_string()
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sandbox_config_default() {
let config = SandboxConfig::default();
assert!(config.enabled);
assert_eq!(config.timeout_seconds, 30);
assert_eq!(config.memory_limit_mb, 256);
assert!(!config.network_enabled);
}
#[test]
fn test_execution_result_success() {
let result = ExecutionResult::success("Hello, World!".to_string(), String::new(), 100);
assert!(result.is_success());
assert_eq!(result.output(), "Hello, World!");
}
#[test]
fn test_execution_result_error() {
let result = ExecutionResult::error("Something went wrong");
assert!(!result.is_success());
assert!(result.output().contains("Error"));
}
#[test]
fn test_execution_result_timeout() {
let result = ExecutionResult::timeout();
assert!(!result.is_success());
assert!(result.timed_out);
}
#[test]
fn test_code_language_from_str() {
assert_eq!(CodeLanguage::from("python"), CodeLanguage::Python);
assert_eq!(CodeLanguage::from("PYTHON"), CodeLanguage::Python);
assert_eq!(CodeLanguage::from("py"), CodeLanguage::Python);
assert_eq!(CodeLanguage::from("javascript"), CodeLanguage::JavaScript);
assert_eq!(CodeLanguage::from("js"), CodeLanguage::JavaScript);
assert_eq!(CodeLanguage::from("node"), CodeLanguage::JavaScript);
assert_eq!(CodeLanguage::from("bash"), CodeLanguage::Bash);
}
#[test]
fn test_code_language_file_extension() {
assert_eq!(CodeLanguage::Python.file_extension(), "py");
assert_eq!(CodeLanguage::JavaScript.file_extension(), "js");
assert_eq!(CodeLanguage::Bash.file_extension(), "sh");
}
#[test]
fn test_code_language_interpreter() {
assert_eq!(CodeLanguage::Python.interpreter(), "python3");
assert_eq!(CodeLanguage::JavaScript.interpreter(), "node");
assert_eq!(CodeLanguage::Bash.interpreter(), "bash");
}
#[test]
fn test_sandbox_runtime_from_str() {
assert_eq!(SandboxRuntime::from("lxc"), SandboxRuntime::LXC);
assert_eq!(SandboxRuntime::from("docker"), SandboxRuntime::Docker);
assert_eq!(
SandboxRuntime::from("firecracker"),
SandboxRuntime::Firecracker
);
assert_eq!(SandboxRuntime::from("unknown"), SandboxRuntime::Process);
}
#[test]
fn test_lxc_config_generation() {
let python_config = generate_python_lxc_config();
assert!(python_config.contains("gb-sandbox-python"));
assert!(python_config.contains("memory.max"));
let node_config = generate_node_lxc_config();
assert!(node_config.contains("gb-sandbox-node"));
assert!(node_config.contains("/usr/bin/node"));
}
}