use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SandboxLevel {
Full,
NetworkOnly,
Minimal,
None,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SandboxConfig {
pub level: SandboxLevel,
#[serde(default = "default_memory_mb")]
pub memory_mb: u64,
#[serde(default = "default_cpus")]
pub cpus: f64,
#[serde(default = "default_timeout")]
pub timeout: u64,
#[serde(default)]
pub bind_mounts: Vec<BindMount>,
#[serde(default)]
pub env: Vec<EnvVar>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BindMount {
pub source: String,
pub target: String,
#[serde(default = "default_readonly")]
pub readonly: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EnvVar {
pub name: String,
pub value: String,
}
fn default_memory_mb() -> u64 {
2048
}
fn default_cpus() -> f64 {
4.0
}
fn default_timeout() -> u64 {
600
}
fn default_readonly() -> bool {
true
}
pub fn validate_config(config: &SandboxConfig) -> Vec<String> {
let mut errors = Vec::new();
if config.memory_mb == 0 {
errors.push("memory_mb must be > 0".to_string());
}
if config.cpus <= 0.0 {
errors.push("cpus must be > 0.0".to_string());
}
if config.timeout == 0 {
errors.push("timeout must be > 0".to_string());
}
if config.memory_mb > 1_048_576 {
errors.push("memory_mb exceeds 1 TiB maximum".to_string());
}
if config.cpus > 1024.0 {
errors.push("cpus exceeds 1024 core maximum".to_string());
}
for mount in &config.bind_mounts {
if mount.source.is_empty() {
errors.push("bind mount source cannot be empty".to_string());
}
if mount.target.is_empty() {
errors.push("bind mount target cannot be empty".to_string());
}
}
errors
}
pub fn preset_profile(name: &str) -> Option<SandboxConfig> {
match name {
"full" => Some(SandboxConfig {
level: SandboxLevel::Full,
memory_mb: 2048,
cpus: 4.0,
timeout: 600,
bind_mounts: Vec::new(),
env: Vec::new(),
}),
"network-only" => Some(SandboxConfig {
level: SandboxLevel::NetworkOnly,
memory_mb: 4096,
cpus: 8.0,
timeout: 1200,
bind_mounts: Vec::new(),
env: Vec::new(),
}),
"minimal" => Some(SandboxConfig {
level: SandboxLevel::Minimal,
memory_mb: 1024,
cpus: 2.0,
timeout: 300,
bind_mounts: Vec::new(),
env: Vec::new(),
}),
"gpu" => Some(SandboxConfig {
level: SandboxLevel::NetworkOnly,
memory_mb: 16384,
cpus: 8.0,
timeout: 3600,
bind_mounts: vec![BindMount {
source: "/dev/nvidia0".to_string(),
target: "/dev/nvidia0".to_string(),
readonly: false,
}],
env: vec![EnvVar {
name: "NVIDIA_VISIBLE_DEVICES".to_string(),
value: "all".to_string(),
}],
}),
_ => None,
}
}
pub fn parse_sandbox_config(yaml: &str) -> Result<SandboxConfig, String> {
serde_yaml_ng::from_str(yaml).map_err(|e| format!("invalid sandbox config: {e}"))
}
pub fn blocks_network(level: SandboxLevel) -> bool {
matches!(level, SandboxLevel::Full)
}
pub fn enforces_fs_isolation(level: SandboxLevel) -> bool {
matches!(
level,
SandboxLevel::Full | SandboxLevel::NetworkOnly | SandboxLevel::Minimal
)
}
pub fn cgroup_path(store_hash: &str) -> String {
let hash = store_hash.strip_prefix("blake3:").unwrap_or(store_hash);
format!(
"/sys/fs/cgroup/forjar-build-{}",
&hash[..16.min(hash.len())]
)
}