use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SandboxSettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_allow_bash_if_sandboxed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub excluded_commands: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_unsandboxed_commands: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub network: Option<SandboxNetworkConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_violations: Option<SandboxIgnoreViolations>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_weaker_nested_sandbox: Option<bool>,
}
impl SandboxSettings {
pub fn new() -> Self {
Self::default()
}
pub fn enabled() -> Self {
Self {
enabled: Some(true),
..Default::default()
}
}
pub fn disabled() -> Self {
Self {
enabled: Some(false),
..Default::default()
}
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = Some(enabled);
self
}
pub fn with_auto_allow_bash_if_sandboxed(mut self, auto_allow: bool) -> Self {
self.auto_allow_bash_if_sandboxed = Some(auto_allow);
self
}
pub fn with_excluded_commands(mut self, commands: Vec<String>) -> Self {
self.excluded_commands = Some(commands);
self
}
pub fn add_excluded_command(mut self, command: impl Into<String>) -> Self {
self.excluded_commands
.get_or_insert_with(Vec::new)
.push(command.into());
self
}
pub fn with_allow_unsandboxed_commands(mut self, allow: bool) -> Self {
self.allow_unsandboxed_commands = Some(allow);
self
}
pub fn with_network(mut self, network: SandboxNetworkConfig) -> Self {
self.network = Some(network);
self
}
pub fn with_ignore_violations(mut self, ignore: SandboxIgnoreViolations) -> Self {
self.ignore_violations = Some(ignore);
self
}
pub fn with_enable_weaker_nested_sandbox(mut self, enable: bool) -> Self {
self.enable_weaker_nested_sandbox = Some(enable);
self
}
pub fn is_enabled(&self) -> bool {
self.enabled.unwrap_or(false)
}
pub fn is_command_excluded(&self, command: &str) -> bool {
self.excluded_commands
.as_ref()
.map(|cmds| cmds.iter().any(|c| command.contains(c)))
.unwrap_or(false)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SandboxNetworkConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_browser: Option<bool>,
}
impl SandboxNetworkConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_allow_browser(mut self, allow: bool) -> Self {
self.allow_browser = Some(allow);
self
}
pub fn is_browser_allowed(&self) -> bool {
self.allow_browser.unwrap_or(false)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SandboxIgnoreViolations {
#[serde(skip_serializing_if = "Option::is_none")]
pub filesystem: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub network: Option<bool>,
}
impl SandboxIgnoreViolations {
pub fn new() -> Self {
Self::default()
}
pub fn with_filesystem(mut self, ignore: bool) -> Self {
self.filesystem = Some(ignore);
self
}
pub fn with_network(mut self, ignore: bool) -> Self {
self.network = Some(ignore);
self
}
pub fn ignores_filesystem(&self) -> bool {
self.filesystem.unwrap_or(false)
}
pub fn ignores_network(&self) -> bool {
self.network.unwrap_or(false)
}
pub fn ignores_any(&self) -> bool {
self.ignores_filesystem() || self.ignores_network()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sandbox_settings_default() {
let settings = SandboxSettings::default();
assert!(settings.enabled.is_none());
assert!(!settings.is_enabled());
}
#[test]
fn test_sandbox_settings_enabled() {
let settings = SandboxSettings::enabled();
assert_eq!(settings.enabled, Some(true));
assert!(settings.is_enabled());
}
#[test]
fn test_sandbox_settings_disabled() {
let settings = SandboxSettings::disabled();
assert_eq!(settings.enabled, Some(false));
assert!(!settings.is_enabled());
}
#[test]
fn test_sandbox_settings_builder() {
let settings = SandboxSettings::new()
.with_enabled(true)
.with_auto_allow_bash_if_sandboxed(true)
.with_allow_unsandboxed_commands(false)
.with_enable_weaker_nested_sandbox(true);
assert!(settings.is_enabled());
assert_eq!(settings.auto_allow_bash_if_sandboxed, Some(true));
assert_eq!(settings.allow_unsandboxed_commands, Some(false));
assert_eq!(settings.enable_weaker_nested_sandbox, Some(true));
}
#[test]
fn test_sandbox_settings_excluded_commands() {
let settings = SandboxSettings::new()
.with_excluded_commands(vec!["rm -rf".to_string(), "dd".to_string()])
.add_excluded_command("mkfs");
assert!(settings.is_command_excluded("rm -rf /"));
assert!(settings.is_command_excluded("dd if=/dev/zero"));
assert!(settings.is_command_excluded("mkfs.ext4"));
assert!(!settings.is_command_excluded("ls"));
}
#[test]
fn test_sandbox_settings_serialization() {
let settings = SandboxSettings::new()
.with_enabled(true)
.with_auto_allow_bash_if_sandboxed(true);
let json = serde_json::to_string(&settings).unwrap();
assert!(json.contains("\"enabled\":true"));
assert!(json.contains("\"autoAllowBashIfSandboxed\":true"));
let deserialized: SandboxSettings = serde_json::from_str(&json).unwrap();
assert_eq!(settings, deserialized);
}
#[test]
fn test_sandbox_settings_skip_serializing_none() {
let settings = SandboxSettings::new().with_enabled(true);
let json = serde_json::to_string(&settings).unwrap();
assert!(json.contains("\"enabled\":true"));
assert!(!json.contains("autoAllowBashIfSandboxed"));
assert!(!json.contains("excludedCommands"));
}
#[test]
fn test_network_config_default() {
let config = SandboxNetworkConfig::default();
assert!(config.allow_browser.is_none());
assert!(!config.is_browser_allowed());
}
#[test]
fn test_network_config_builder() {
let config = SandboxNetworkConfig::new().with_allow_browser(true);
assert!(config.is_browser_allowed());
}
#[test]
fn test_network_config_serialization() {
let config = SandboxNetworkConfig::new().with_allow_browser(true);
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("\"allowBrowser\":true"));
let deserialized: SandboxNetworkConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config, deserialized);
}
#[test]
fn test_ignore_violations_default() {
let ignore = SandboxIgnoreViolations::default();
assert!(!ignore.ignores_filesystem());
assert!(!ignore.ignores_network());
assert!(!ignore.ignores_any());
}
#[test]
fn test_ignore_violations_builder() {
let ignore = SandboxIgnoreViolations::new()
.with_filesystem(true)
.with_network(true);
assert!(ignore.ignores_filesystem());
assert!(ignore.ignores_network());
assert!(ignore.ignores_any());
}
#[test]
fn test_ignore_violations_partial() {
let ignore = SandboxIgnoreViolations::new().with_filesystem(true);
assert!(ignore.ignores_filesystem());
assert!(!ignore.ignores_network());
assert!(ignore.ignores_any());
}
#[test]
fn test_ignore_violations_serialization() {
let ignore = SandboxIgnoreViolations::new()
.with_filesystem(true)
.with_network(false);
let json = serde_json::to_string(&ignore).unwrap();
assert!(json.contains("\"filesystem\":true"));
assert!(json.contains("\"network\":false"));
let deserialized: SandboxIgnoreViolations = serde_json::from_str(&json).unwrap();
assert_eq!(ignore, deserialized);
}
#[test]
fn test_full_sandbox_configuration() {
let settings = SandboxSettings::new()
.with_enabled(true)
.with_auto_allow_bash_if_sandboxed(true)
.with_excluded_commands(vec!["rm -rf".to_string()])
.with_network(SandboxNetworkConfig::new().with_allow_browser(true))
.with_ignore_violations(
SandboxIgnoreViolations::new()
.with_filesystem(false)
.with_network(true),
);
assert!(settings.is_enabled());
assert_eq!(settings.auto_allow_bash_if_sandboxed, Some(true));
assert!(settings.is_command_excluded("rm -rf /home"));
assert!(settings.network.as_ref().unwrap().is_browser_allowed());
assert!(!settings.ignore_violations.as_ref().unwrap().ignores_filesystem());
assert!(settings.ignore_violations.as_ref().unwrap().ignores_network());
}
#[test]
fn test_sandbox_configuration_round_trip() {
let settings = SandboxSettings::new()
.with_enabled(true)
.with_auto_allow_bash_if_sandboxed(true)
.with_excluded_commands(vec!["dd".to_string(), "mkfs".to_string()])
.with_allow_unsandboxed_commands(false)
.with_network(SandboxNetworkConfig::new().with_allow_browser(true))
.with_ignore_violations(SandboxIgnoreViolations::new().with_filesystem(true))
.with_enable_weaker_nested_sandbox(true);
let json = serde_json::to_string(&settings).unwrap();
let restored: SandboxSettings = serde_json::from_str(&json).unwrap();
assert_eq!(settings, restored);
}
#[test]
fn test_python_sdk_compatibility() {
let python_json = r#"{
"enabled": true,
"autoAllowBashIfSandboxed": true,
"excludedCommands": ["rm -rf", "dd"],
"allowUnsandboxedCommands": false,
"network": {"allowBrowser": true},
"ignoreViolations": {"filesystem": false, "network": true},
"enableWeakerNestedSandbox": false
}"#;
let settings: SandboxSettings = serde_json::from_str(python_json).unwrap();
assert!(settings.is_enabled());
assert_eq!(settings.auto_allow_bash_if_sandboxed, Some(true));
assert_eq!(
settings.excluded_commands,
Some(vec!["rm -rf".to_string(), "dd".to_string()])
);
assert_eq!(settings.allow_unsandboxed_commands, Some(false));
assert!(settings.network.as_ref().unwrap().is_browser_allowed());
assert!(!settings.ignore_violations.as_ref().unwrap().ignores_filesystem());
assert!(settings.ignore_violations.as_ref().unwrap().ignores_network());
assert_eq!(settings.enable_weaker_nested_sandbox, Some(false));
}
}