use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub enum IsolationLevel {
#[default]
None,
CgroupTracking,
Namespace,
Seatbelt,
Container,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FilesystemConfig {
pub allow_write: Vec<String>,
pub deny_write: Vec<String>,
pub deny_read: Vec<String>,
pub inherit_readable: bool,
}
impl Default for FilesystemConfig {
fn default() -> Self {
Self {
allow_write: vec![".".into()],
deny_write: vec![],
deny_read: vec![],
inherit_readable: true,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NetworkConfig {
pub enabled: bool,
pub allowed_domains: Option<Vec<String>>,
pub denied_domains: Vec<String>,
pub allow_trusted_domains: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct EnvConfig {
pub inherit_parent: bool,
pub set: HashMap<String, String>,
pub unset: Vec<String>,
}
impl Default for EnvConfig {
fn default() -> Self {
Self {
inherit_parent: true,
set: HashMap::new(),
unset: vec![],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[derive(Default)]
pub struct ResourceLimits {
pub memory_bytes: Option<u64>,
pub cpu_quota: Option<f32>,
pub max_pids: Option<u32>,
pub exec_timeout_secs: Option<u64>,
}
impl ResourceLimits {
#[must_use]
pub fn none() -> Self {
Self::default()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ProcessTracking {
pub enabled: bool,
pub max_tracked: Option<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub enum SecurityPreset {
Privileged,
#[default]
Baseline,
Restricted,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SeccompProfile {
Unconfined,
#[default]
RuntimeDefault,
Localhost {
path: String,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CapabilityConfig {
pub drop: Vec<String>,
pub add: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SecurityProfile {
pub standard: SecurityPreset,
pub seccomp: SeccompProfile,
pub capabilities: CapabilityConfig,
pub no_new_privileges: bool,
pub run_as_user: Option<u32>,
pub run_as_group: Option<u32>,
}
impl Default for SecurityProfile {
fn default() -> Self {
Self {
standard: SecurityPreset::Baseline,
seccomp: SeccompProfile::RuntimeDefault,
capabilities: CapabilityConfig {
drop: vec!["NET_RAW".into(), "SYS_PTRACE".into(), "SYS_ADMIN".into()],
add: vec![],
},
no_new_privileges: true,
run_as_user: None,
run_as_group: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SandboxConfig {
pub enabled: bool,
pub isolation: IsolationLevel,
pub filesystem: Option<FilesystemConfig>,
pub network: Option<NetworkConfig>,
pub env: EnvConfig,
pub security: SecurityProfile,
pub resources: Option<ResourceLimits>,
pub process_tracking: ProcessTracking,
pub allowed_commands: Option<Vec<String>>,
pub denied_commands: Vec<String>,
}
impl Default for SandboxConfig {
fn default() -> Self {
Self {
enabled: false,
isolation: IsolationLevel::None,
filesystem: None,
network: None,
env: EnvConfig::default(),
security: SecurityProfile::default(),
resources: None,
process_tracking: ProcessTracking::default(),
allowed_commands: None,
denied_commands: vec![],
}
}
}
impl SandboxConfig {
#[must_use]
pub fn coding_agent() -> Self {
Self {
enabled: true,
isolation: IsolationLevel::CgroupTracking,
filesystem: Some(FilesystemConfig::default()),
network: Some(NetworkConfig::default()),
env: EnvConfig::default(),
security: SecurityProfile::default(),
resources: None,
process_tracking: ProcessTracking {
enabled: true,
max_tracked: Some(64),
},
allowed_commands: None,
denied_commands: vec![],
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn coding_agent_preset_has_expected_defaults() {
let cfg = SandboxConfig::coding_agent();
assert!(cfg.enabled);
assert_eq!(cfg.isolation, IsolationLevel::CgroupTracking);
assert!(cfg.process_tracking.enabled);
assert_eq!(cfg.process_tracking.max_tracked, Some(64));
assert!(cfg.env.inherit_parent);
let fs = cfg.filesystem.unwrap();
assert_eq!(fs.allow_write, vec!["."]);
assert!(fs.inherit_readable);
let net = cfg.network.unwrap();
assert!(!net.enabled);
}
#[test]
fn default_security_is_baseline() {
let sp = SecurityProfile::default();
assert_eq!(sp.standard, SecurityPreset::Baseline);
assert!(sp.no_new_privileges);
assert!(sp.capabilities.drop.contains(&"NET_RAW".to_string()));
assert!(sp.capabilities.drop.contains(&"SYS_PTRACE".to_string()));
assert!(sp.capabilities.drop.contains(&"SYS_ADMIN".to_string()));
}
#[test]
fn serde_round_trip() {
let cfg = SandboxConfig::coding_agent();
let json = serde_json::to_string(&cfg).unwrap();
let back: SandboxConfig = serde_json::from_str(&json).unwrap();
assert_eq!(back.isolation, cfg.isolation);
assert_eq!(
back.security.no_new_privileges,
cfg.security.no_new_privileges
);
}
}