use crate::env_helpers::default_true;
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SandboxConfig {
#[serde(default = "default_false")]
pub enabled: bool,
#[serde(default)]
pub default_policy: SandboxPolicy,
#[serde(default)]
pub network: NetworkConfig,
#[serde(default)]
pub sensitive_paths: SensitivePathsConfig,
#[serde(default)]
pub resource_limits: ResourceLimitsConfig,
#[serde(default)]
pub seccomp: SeccompConfig,
#[serde(default)]
pub external: ExternalSandboxConfig,
}
impl Default for SandboxConfig {
fn default() -> Self {
Self {
enabled: default_false(),
default_policy: SandboxPolicy::default(),
network: NetworkConfig::default(),
sensitive_paths: SensitivePathsConfig::default(),
resource_limits: ResourceLimitsConfig::default(),
seccomp: SeccompConfig::default(),
external: ExternalSandboxConfig::default(),
}
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SandboxPolicy {
#[default]
ReadOnly,
WorkspaceWrite,
DangerFullAccess,
External,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct NetworkConfig {
#[serde(default)]
pub allow_all: bool,
#[serde(default)]
pub allowlist: Vec<NetworkAllowlistEntryConfig>,
#[serde(default)]
pub block_all: bool,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NetworkAllowlistEntryConfig {
pub domain: String,
#[serde(default = "default_https_port")]
pub port: u16,
}
fn default_https_port() -> u16 {
443
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SensitivePathsConfig {
#[serde(default = "default_true")]
pub use_defaults: bool,
#[serde(default)]
pub additional: Vec<String>,
#[serde(default)]
pub exceptions: Vec<String>,
}
impl Default for SensitivePathsConfig {
fn default() -> Self {
Self {
use_defaults: default_true(),
additional: Vec::new(),
exceptions: Vec::new(),
}
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ResourceLimitsConfig {
#[serde(default)]
pub preset: ResourceLimitsPreset,
#[serde(default)]
pub max_memory_mb: u64,
#[serde(default)]
pub max_pids: u32,
#[serde(default)]
pub max_disk_mb: u64,
#[serde(default)]
pub cpu_time_secs: u64,
#[serde(default)]
pub timeout_secs: u64,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ResourceLimitsPreset {
Unlimited,
Conservative,
#[default]
Moderate,
Generous,
Custom,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SeccompConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub profile: SeccompProfilePreset,
#[serde(default)]
pub additional_blocked: Vec<String>,
#[serde(default)]
pub log_only: bool,
}
impl Default for SeccompConfig {
fn default() -> Self {
Self {
enabled: default_true(),
profile: SeccompProfilePreset::default(),
additional_blocked: Vec::new(),
log_only: false,
}
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SeccompProfilePreset {
#[default]
Strict,
Permissive,
Disabled,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ExternalSandboxConfig {
#[serde(default)]
pub sandbox_type: ExternalSandboxType,
#[serde(default)]
pub docker: DockerSandboxConfig,
#[serde(default)]
pub microvm: MicroVMSandboxConfig,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ExternalSandboxType {
#[default]
None,
Docker,
MicroVM,
GVisor,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DockerSandboxConfig {
#[serde(default = "default_docker_image")]
pub image: String,
#[serde(default)]
pub memory_limit: String,
#[serde(default)]
pub cpu_limit: String,
#[serde(default = "default_network_mode")]
pub network_mode: String,
}
fn default_docker_image() -> String {
"ubuntu:22.04".to_string()
}
fn default_network_mode() -> String {
"none".to_string()
}
impl Default for DockerSandboxConfig {
fn default() -> Self {
Self {
image: default_docker_image(),
memory_limit: String::new(),
cpu_limit: String::new(),
network_mode: default_network_mode(),
}
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MicroVMSandboxConfig {
#[serde(default)]
pub vmm: String,
#[serde(default)]
pub kernel_path: String,
#[serde(default)]
pub rootfs_path: String,
#[serde(default = "default_microvm_memory")]
pub memory_mb: u64,
#[serde(default = "default_vcpus")]
pub vcpus: u32,
}
fn default_microvm_memory() -> u64 {
512
}
fn default_vcpus() -> u32 {
1
}
impl Default for MicroVMSandboxConfig {
fn default() -> Self {
Self {
vmm: String::new(),
kernel_path: String::new(),
rootfs_path: String::new(),
memory_mb: default_microvm_memory(),
vcpus: default_vcpus(),
}
}
}
#[inline]
const fn default_false() -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sandbox_config_default() {
let config = SandboxConfig::default();
assert!(!config.enabled);
assert_eq!(config.default_policy, SandboxPolicy::ReadOnly);
}
#[test]
fn test_sandbox_config_parses_default_policy() {
let config: SandboxConfig = toml::from_str(
r#"
enabled = true
default_policy = "workspace_write"
"#,
)
.expect("sandbox config with default_policy should parse");
assert!(config.enabled);
assert_eq!(config.default_policy, SandboxPolicy::WorkspaceWrite);
}
#[test]
fn test_sandbox_config_serializes_default_policy() {
let config = SandboxConfig {
default_policy: SandboxPolicy::DangerFullAccess,
..SandboxConfig::default()
};
let toml = toml::to_string(&config).expect("sandbox config should serialize");
assert!(toml.contains("default_policy = \"danger_full_access\""));
let removed_field = format!("default_{}", "mode");
assert!(!toml.contains(&removed_field));
}
#[test]
fn test_sandbox_config_rejects_removed_default_field() {
let removed_field = format!("default_{}", "mode");
let input = format!(
r#"
enabled = true
{removed_field} = "workspace_write"
"#,
);
let err = toml::from_str::<SandboxConfig>(&input)
.expect_err("sandbox config should reject removed default field");
assert!(
err.to_string()
.contains(&format!("unknown field `{removed_field}`"))
);
}
#[test]
fn test_network_config_default() {
let config = NetworkConfig::default();
assert!(!config.allow_all);
assert!(!config.block_all);
assert!(config.allowlist.is_empty());
}
#[test]
fn test_resource_limits_config_default() {
let config = ResourceLimitsConfig::default();
assert_eq!(config.preset, ResourceLimitsPreset::Moderate);
}
#[test]
fn test_seccomp_config_default() {
let config = SeccompConfig::default();
assert!(config.enabled);
assert_eq!(config.profile, SeccompProfilePreset::Strict);
}
}