use crate::env_helpers::default_true;
use serde::de::{self, MapAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
use std::fmt;
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
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, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NetworkPolicy {
#[default]
AllowlistOnly,
AllowAll,
BlockAll,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Serialize)]
pub struct NetworkConfig {
pub policy: NetworkPolicy,
#[serde(default)]
pub allowlist: Vec<NetworkAllowlistEntryConfig>,
}
impl Default for NetworkConfig {
fn default() -> Self {
Self {
policy: NetworkPolicy::AllowlistOnly,
allowlist: Vec::new(),
}
}
}
impl<'de> Deserialize<'de> for NetworkConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")]
enum Field {
Policy,
Allowlist,
AllowAll,
BlockAll,
}
struct NetworkConfigVisitor;
impl<'de> Visitor<'de> for NetworkConfigVisitor {
type Value = NetworkConfig;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("NetworkConfig struct")
}
fn visit_map<V>(self, mut map: V) -> Result<NetworkConfig, V::Error>
where
V: MapAccess<'de>,
{
let mut policy: Option<NetworkPolicy> = None;
let mut allowlist: Option<Vec<NetworkAllowlistEntryConfig>> = None;
let mut allow_all: Option<bool> = None;
let mut block_all: Option<bool> = None;
while let Some(key) = map.next_key()? {
match key {
Field::Policy => {
if policy.is_some() {
return Err(de::Error::duplicate_field("policy"));
}
policy = Some(map.next_value()?);
}
Field::Allowlist => {
if allowlist.is_some() {
return Err(de::Error::duplicate_field("allowlist"));
}
allowlist = Some(map.next_value()?);
}
Field::AllowAll => {
if allow_all.is_some() {
return Err(de::Error::duplicate_field("allow_all"));
}
allow_all = Some(map.next_value()?);
}
Field::BlockAll => {
if block_all.is_some() {
return Err(de::Error::duplicate_field("block_all"));
}
block_all = Some(map.next_value()?);
}
}
}
let resolved_policy = if block_all.unwrap_or(false) {
NetworkPolicy::BlockAll
} else if allow_all.unwrap_or(false) {
NetworkPolicy::AllowAll
} else {
policy.unwrap_or_default()
};
Ok(NetworkConfig {
policy: resolved_policy,
allowlist: allowlist.unwrap_or_default(),
})
}
}
deserializer.deserialize_map(NetworkConfigVisitor)
}
}
#[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,
}
fn default_docker_image() -> String {
"ubuntu:22.04".to_string()
}
impl Default for DockerSandboxConfig {
fn default() -> Self {
Self {
image: default_docker_image(),
memory_limit: String::new(),
cpu_limit: String::new(),
}
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum MicroVmProvider {
#[default]
#[serde(rename = "")]
None,
Firecracker,
CloudHypervisor,
#[serde(other)]
Unknown,
}
impl MicroVmProvider {
pub fn as_str(&self) -> &str {
match self {
Self::None => "",
Self::Firecracker => "firecracker",
Self::CloudHypervisor => "cloud-hypervisor",
Self::Unknown => "unknown",
}
}
}
impl fmt::Display for MicroVmProvider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MicroVMSandboxConfig {
#[serde(default)]
pub vmm: MicroVmProvider,
#[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: MicroVmProvider::None,
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_ignores_unknown_fields_for_forward_compatibility() {
let removed_field = format!("default_{}", "mode");
let input = format!(
r#"
enabled = true
{removed_field} = "workspace_write"
"#,
);
let config: SandboxConfig = toml::from_str(&input)
.expect("sandbox config should accept unknown fields for forward compatibility");
assert!(config.enabled);
}
#[test]
fn test_network_config_default() {
let config = NetworkConfig::default();
assert_eq!(config.policy, NetworkPolicy::AllowlistOnly);
assert!(config.allowlist.is_empty());
}
#[test]
fn test_network_config_policy_field() {
let config: NetworkConfig =
toml::from_str(r#"policy = "allow_all""#).expect("policy field should parse");
assert_eq!(config.policy, NetworkPolicy::AllowAll);
}
#[test]
fn test_network_config_legacy_allow_all() {
let config: NetworkConfig =
toml::from_str(r#"allow_all = true"#).expect("legacy allow_all should parse");
assert_eq!(config.policy, NetworkPolicy::AllowAll);
}
#[test]
fn test_network_config_legacy_block_all() {
let config: NetworkConfig =
toml::from_str(r#"block_all = true"#).expect("legacy block_all should parse");
assert_eq!(config.policy, NetworkPolicy::BlockAll);
}
#[test]
fn test_network_config_legacy_block_all_overrides_allow_all() {
let config: NetworkConfig = toml::from_str(
r#"
allow_all = true
block_all = true
"#,
)
.expect("legacy bool combination should parse");
assert_eq!(config.policy, NetworkPolicy::BlockAll);
}
#[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);
}
}