use crate::skills::error::SkillError;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::time::Duration;
use tracing::warn;
#[cfg(feature = "sandbox")]
use tracing::{debug, info};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxConfig {
pub timeout: Duration,
pub max_memory: Option<usize>,
pub max_fuel: Option<u64>,
pub allow_network: bool,
pub allow_filesystem: bool,
pub working_directory: Option<String>,
}
impl Default for SandboxConfig {
fn default() -> Self {
Self {
timeout: Duration::from_secs(30),
max_memory: Some(64 * 1024 * 1024), max_fuel: Some(1_000_000), allow_network: false,
allow_filesystem: false,
working_directory: None,
}
}
}
impl SandboxConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn with_max_memory(mut self, max_memory: usize) -> Self {
self.max_memory = Some(max_memory);
self
}
pub fn with_max_fuel(mut self, max_fuel: u64) -> Self {
self.max_fuel = Some(max_fuel);
self
}
pub fn with_network_access(mut self, allow: bool) -> Self {
self.allow_network = allow;
self
}
pub fn with_filesystem_access(mut self, allow: bool, working_dir: Option<String>) -> Self {
self.allow_filesystem = allow;
self.working_directory = working_dir;
self
}
pub fn restrictive() -> Self {
Self {
timeout: Duration::from_secs(10),
max_memory: Some(32 * 1024 * 1024), max_fuel: Some(500_000), allow_network: false,
allow_filesystem: false,
working_directory: None,
}
}
pub fn permissive() -> Self {
Self {
timeout: Duration::from_secs(300), max_memory: None, max_fuel: None, allow_network: true,
allow_filesystem: true,
working_directory: Some("/tmp".to_string()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxResult {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
pub execution_time_ms: u64,
pub timed_out: bool,
pub memory_used: Option<usize>,
pub fuel_consumed: Option<u64>,
}
impl SandboxResult {
pub fn is_success(&self) -> bool {
self.exit_code == 0 && !self.timed_out
}
pub fn error_message(&self) -> Option<String> {
if self.timed_out {
Some(format!(
"Execution timed out after {}ms (limit exceeded)",
self.execution_time_ms
))
} else if self.exit_code != 0 {
Some(format!(
"Script exited with error code {}{}{}",
self.exit_code,
if !self.stderr.is_empty() { ": " } else { "" },
self.stderr
))
} else {
None
}
}
}
#[cfg(feature = "sandbox")]
pub struct SandboxExecutor {
config: SandboxConfig,
}
#[cfg(feature = "sandbox")]
impl SandboxExecutor {
pub fn new(config: SandboxConfig) -> Self {
Self { config }
}
pub async fn execute(
&self,
script: &str,
args: Option<Vec<String>>,
) -> Result<SandboxResult, SkillError> {
let start_time = std::time::Instant::now();
info!(
"Executing script in sandbox with timeout={:?}, max_memory={:?}, max_fuel={:?}",
self.config.timeout, self.config.max_memory, self.config.max_fuel
);
let result = tokio::time::timeout(self.config.timeout, async {
self.execute_script(script, args).await
})
.await;
let execution_time = start_time.elapsed();
match result {
Ok(Ok(mut result)) => {
result.execution_time_ms = execution_time.as_millis() as u64;
Ok(result)
},
Ok(Err(e)) => Err(e),
Err(_) => {
warn!("Script execution timed out after {:?}", self.config.timeout);
Ok(SandboxResult {
stdout: String::new(),
stderr: format!("Execution timed out after {:?}", self.config.timeout),
exit_code: -1,
execution_time_ms: execution_time.as_millis() as u64,
timed_out: true,
memory_used: None,
fuel_consumed: None,
})
},
}
}
pub async fn execute_file<P: AsRef<Path>>(
&self,
path: P,
args: Option<Vec<String>>,
) -> Result<SandboxResult, SkillError> {
let path = path.as_ref();
if !path.exists() {
return Err(SkillError::Io(format!("Script file not found: {:?}", path)));
}
let script = std::fs::read_to_string(path)
.map_err(|e| SkillError::Io(format!("Failed to read script file: {}", e)))?;
self.execute(&script, args).await
}
async fn execute_script(
&self,
script: &str,
_args: Option<Vec<String>>,
) -> Result<SandboxResult, SkillError> {
debug!("Executing script ({} bytes)", script.len());
warn!(
"Sandbox feature is enabled but using safe fallback (WASM compilation not yet implemented)"
);
Ok(SandboxResult {
stdout: "Sandbox execution (safe fallback mode)".to_string(),
stderr: String::new(),
exit_code: 0,
execution_time_ms: 0,
timed_out: false,
memory_used: Some(0),
fuel_consumed: Some(0),
})
}
}
#[cfg(feature = "sandbox")]
impl Default for SandboxExecutor {
fn default() -> Self {
Self::new(SandboxConfig::default())
}
}
#[cfg(not(feature = "sandbox"))]
#[allow(dead_code)]
pub struct SandboxExecutor {
config: SandboxConfig,
}
#[cfg(not(feature = "sandbox"))]
impl SandboxExecutor {
pub fn new(config: SandboxConfig) -> Self {
warn!("Sandbox feature is disabled. Executor will run in fallback mode.");
Self { config }
}
pub async fn execute(
&self,
_script: &str,
_args: Option<Vec<String>>,
) -> Result<SandboxResult, SkillError> {
warn!("Attempting sandbox execution without 'sandbox' feature enabled");
Err(SkillError::Configuration(
"Sandbox feature is disabled. Enable with --features sandbox".to_string(),
))
}
pub async fn execute_file<P: AsRef<Path>>(
&self,
_path: P,
_args: Option<Vec<String>>,
) -> Result<SandboxResult, SkillError> {
Err(SkillError::Configuration(
"Sandbox feature is disabled. Enable with --features sandbox".to_string(),
))
}
}
#[cfg(not(feature = "sandbox"))]
impl Default for SandboxExecutor {
fn default() -> Self {
Self::new(SandboxConfig::default())
}
}
pub struct SandboxUtils;
impl SandboxUtils {
pub fn validate_script(script: &str) -> Result<(), SkillError> {
if script.is_empty() {
return Err(SkillError::Validation("Script is empty".to_string()));
}
if script.len() > 10 * 1024 * 1024 {
return Err(SkillError::Validation(
"Script is too large (>10 MB)".to_string(),
));
}
Ok(())
}
pub fn estimate_memory_requirement(script: &str) -> usize {
script.len() * 10
}
pub fn is_safe_config(config: &SandboxConfig) -> bool {
!config.allow_network && !config.allow_filesystem && config.max_memory.is_some()
}
pub fn recommended_config_for_script(script: &str) -> SandboxConfig {
let estimated_memory = Self::estimate_memory_requirement(script);
if estimated_memory < 1024 * 1024 {
SandboxConfig::restrictive()
} else {
SandboxConfig::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sandbox_config_default() {
let config = SandboxConfig::default();
assert_eq!(config.timeout, Duration::from_secs(30));
assert_eq!(config.max_memory, Some(64 * 1024 * 1024));
assert_eq!(config.max_fuel, Some(1_000_000));
assert!(!config.allow_network);
assert!(!config.allow_filesystem);
}
#[test]
fn test_sandbox_config_builder() {
let config = SandboxConfig::new()
.with_timeout(Duration::from_secs(60))
.with_max_memory(128 * 1024 * 1024)
.with_max_fuel(2_000_000)
.with_network_access(true)
.with_filesystem_access(true, Some("/tmp".to_string()));
assert_eq!(config.timeout, Duration::from_secs(60));
assert_eq!(config.max_memory, Some(128 * 1024 * 1024));
assert_eq!(config.max_fuel, Some(2_000_000));
assert!(config.allow_network);
assert!(config.allow_filesystem);
assert_eq!(config.working_directory, Some("/tmp".to_string()));
}
#[test]
fn test_sandbox_config_restrictive() {
let config = SandboxConfig::restrictive();
assert_eq!(config.timeout, Duration::from_secs(10));
assert_eq!(config.max_memory, Some(32 * 1024 * 1024));
assert_eq!(config.max_fuel, Some(500_000));
assert!(!config.allow_network);
assert!(!config.allow_filesystem);
}
#[test]
fn test_sandbox_config_permissive() {
let config = SandboxConfig::permissive();
assert_eq!(config.timeout, Duration::from_secs(300));
assert!(config.max_memory.is_none());
assert!(config.max_fuel.is_none());
assert!(config.allow_network);
assert!(config.allow_filesystem);
}
#[test]
fn test_sandbox_result_success() {
let result = SandboxResult {
stdout: "Hello".to_string(),
stderr: String::new(),
exit_code: 0,
execution_time_ms: 100,
timed_out: false,
memory_used: Some(1024),
fuel_consumed: Some(1000),
};
assert!(result.is_success());
assert!(result.error_message().is_none());
}
#[test]
fn test_sandbox_result_failure() {
let result = SandboxResult {
stdout: String::new(),
stderr: "Error".to_string(),
exit_code: 1,
execution_time_ms: 50,
timed_out: false,
memory_used: None,
fuel_consumed: None,
};
assert!(!result.is_success());
assert_eq!(
result.error_message(),
Some("Script exited with error code 1: Error".to_string())
);
}
#[test]
fn test_sandbox_result_timeout() {
let result = SandboxResult {
stdout: String::new(),
stderr: String::new(),
exit_code: -1,
execution_time_ms: 10000,
timed_out: true,
memory_used: None,
fuel_consumed: None,
};
assert!(!result.is_success());
assert_eq!(
result.error_message(),
Some("Execution timed out after 10000ms (limit exceeded)".to_string())
);
}
#[test]
fn test_validate_script_empty() {
let result = SandboxUtils::validate_script("");
assert!(result.is_err());
}
#[test]
fn test_validate_script_too_large() {
let large_script = "x".repeat(11 * 1024 * 1024);
let result = SandboxUtils::validate_script(&large_script);
assert!(result.is_err());
}
#[test]
fn test_validate_script_valid() {
let script = "print('Hello, World!')";
let result = SandboxUtils::validate_script(script);
assert!(result.is_ok());
}
#[test]
fn test_estimate_memory_requirement() {
let script = "x".repeat(1024);
let estimated = SandboxUtils::estimate_memory_requirement(&script);
assert_eq!(estimated, 10240);
}
#[test]
fn test_is_safe_config() {
let safe_config = SandboxConfig::restrictive();
assert!(SandboxUtils::is_safe_config(&safe_config));
let unsafe_config = SandboxConfig::permissive();
assert!(!SandboxUtils::is_safe_config(&unsafe_config));
}
#[test]
fn test_recommended_config_for_script() {
let small_script = "print('small')";
let config = SandboxUtils::recommended_config_for_script(small_script);
assert_eq!(config.timeout, Duration::from_secs(10));
let large_script = "x".repeat(2 * 1024 * 1024);
let config = SandboxUtils::recommended_config_for_script(&large_script);
assert_eq!(config.timeout, Duration::from_secs(30));
}
}