|
| 1 | +//! Canonical sandbox types for the Agent OS. |
| 2 | +//! |
| 3 | +//! These types define the shared vocabulary for sandbox isolation across |
| 4 | +//! all projects (Arcan, Lago, Praxis). Implementations live in their |
| 5 | +//! respective crates; this module provides only the contract. |
| 6 | +
|
| 7 | +use serde::{Deserialize, Serialize}; |
| 8 | + |
| 9 | +/// Sandbox isolation tiers, ordered from least to most isolated. |
| 10 | +/// |
| 11 | +/// Derives `PartialOrd`/`Ord` so comparisons like `tier >= SandboxTier::Process` |
| 12 | +/// work naturally for policy enforcement. |
| 13 | +#[derive( |
| 14 | + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default, |
| 15 | +)] |
| 16 | +#[serde(rename_all = "snake_case")] |
| 17 | +pub enum SandboxTier { |
| 18 | + /// No isolation — direct host access. |
| 19 | + #[default] |
| 20 | + None, |
| 21 | + /// Basic restrictions (e.g. seccomp, pledge). |
| 22 | + Basic, |
| 23 | + /// Process-level isolation (e.g. bubblewrap, firejail). |
| 24 | + Process, |
| 25 | + /// Full container isolation (e.g. Apple Containers, Docker). |
| 26 | + Container, |
| 27 | +} |
| 28 | + |
| 29 | +/// Resource limits for sandboxed command execution. |
| 30 | +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] |
| 31 | +pub struct SandboxLimits { |
| 32 | + /// Maximum wall-clock execution time in seconds. |
| 33 | + pub max_runtime_secs: u64, |
| 34 | + /// Maximum bytes for stdout/stderr output. |
| 35 | + pub max_output_bytes: usize, |
| 36 | + /// Maximum memory in megabytes (optional, not always enforced). |
| 37 | + #[serde(default, skip_serializing_if = "Option::is_none")] |
| 38 | + pub max_memory_mb: Option<u64>, |
| 39 | +} |
| 40 | + |
| 41 | +impl Default for SandboxLimits { |
| 42 | + fn default() -> Self { |
| 43 | + Self { |
| 44 | + max_runtime_secs: 30, |
| 45 | + max_output_bytes: 64 * 1024, |
| 46 | + max_memory_mb: None, |
| 47 | + } |
| 48 | + } |
| 49 | +} |
| 50 | + |
| 51 | +/// Network access policy for sandboxed execution. |
| 52 | +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] |
| 53 | +#[serde(tag = "policy", rename_all = "snake_case")] |
| 54 | +pub enum NetworkPolicy { |
| 55 | + /// No network access allowed. |
| 56 | + #[default] |
| 57 | + Disabled, |
| 58 | + /// Unrestricted network access. |
| 59 | + AllowAll, |
| 60 | + /// Network access limited to specific hosts. |
| 61 | + AllowList { |
| 62 | + #[serde(default)] |
| 63 | + hosts: Vec<String>, |
| 64 | + }, |
| 65 | +} |
| 66 | + |
| 67 | +#[cfg(test)] |
| 68 | +mod tests { |
| 69 | + use super::*; |
| 70 | + |
| 71 | + // ── SandboxTier tests ── |
| 72 | + |
| 73 | + #[test] |
| 74 | + fn tier_ordering() { |
| 75 | + assert!(SandboxTier::None < SandboxTier::Basic); |
| 76 | + assert!(SandboxTier::Basic < SandboxTier::Process); |
| 77 | + assert!(SandboxTier::Process < SandboxTier::Container); |
| 78 | + } |
| 79 | + |
| 80 | + #[test] |
| 81 | + fn tier_default_is_none() { |
| 82 | + assert_eq!(SandboxTier::default(), SandboxTier::None); |
| 83 | + } |
| 84 | + |
| 85 | + #[test] |
| 86 | + fn tier_serde_roundtrip() { |
| 87 | + for tier in [ |
| 88 | + SandboxTier::None, |
| 89 | + SandboxTier::Basic, |
| 90 | + SandboxTier::Process, |
| 91 | + SandboxTier::Container, |
| 92 | + ] { |
| 93 | + let json = serde_json::to_string(&tier).unwrap(); |
| 94 | + let back: SandboxTier = serde_json::from_str(&json).unwrap(); |
| 95 | + assert_eq!(back, tier); |
| 96 | + } |
| 97 | + assert_eq!( |
| 98 | + serde_json::to_string(&SandboxTier::None).unwrap(), |
| 99 | + "\"none\"" |
| 100 | + ); |
| 101 | + assert_eq!( |
| 102 | + serde_json::to_string(&SandboxTier::Container).unwrap(), |
| 103 | + "\"container\"" |
| 104 | + ); |
| 105 | + } |
| 106 | + |
| 107 | + #[test] |
| 108 | + fn tier_ge_comparison_for_policy() { |
| 109 | + let required = SandboxTier::Process; |
| 110 | + assert!(SandboxTier::Process >= required); |
| 111 | + assert!(SandboxTier::Container >= required); |
| 112 | + assert!(SandboxTier::Basic < required); |
| 113 | + assert!(SandboxTier::None < required); |
| 114 | + } |
| 115 | + |
| 116 | + // ── SandboxLimits tests ── |
| 117 | + |
| 118 | + #[test] |
| 119 | + fn limits_default() { |
| 120 | + let limits = SandboxLimits::default(); |
| 121 | + assert_eq!(limits.max_runtime_secs, 30); |
| 122 | + assert_eq!(limits.max_output_bytes, 64 * 1024); |
| 123 | + assert!(limits.max_memory_mb.is_none()); |
| 124 | + } |
| 125 | + |
| 126 | + #[test] |
| 127 | + fn limits_serde_roundtrip() { |
| 128 | + let limits = SandboxLimits { |
| 129 | + max_runtime_secs: 60, |
| 130 | + max_output_bytes: 128 * 1024, |
| 131 | + max_memory_mb: Some(512), |
| 132 | + }; |
| 133 | + let json = serde_json::to_string(&limits).unwrap(); |
| 134 | + let back: SandboxLimits = serde_json::from_str(&json).unwrap(); |
| 135 | + assert_eq!(limits, back); |
| 136 | + } |
| 137 | + |
| 138 | + #[test] |
| 139 | + fn limits_omits_none_memory() { |
| 140 | + let limits = SandboxLimits::default(); |
| 141 | + let json = serde_json::to_string(&limits).unwrap(); |
| 142 | + assert!(!json.contains("max_memory_mb")); |
| 143 | + } |
| 144 | + |
| 145 | + // ── NetworkPolicy tests ── |
| 146 | + |
| 147 | + #[test] |
| 148 | + fn network_policy_default_is_disabled() { |
| 149 | + assert_eq!(NetworkPolicy::default(), NetworkPolicy::Disabled); |
| 150 | + } |
| 151 | + |
| 152 | + #[test] |
| 153 | + fn network_policy_disabled_serde() { |
| 154 | + let policy = NetworkPolicy::Disabled; |
| 155 | + let json = serde_json::to_string(&policy).unwrap(); |
| 156 | + assert!(json.contains("\"policy\":\"disabled\"")); |
| 157 | + let back: NetworkPolicy = serde_json::from_str(&json).unwrap(); |
| 158 | + assert_eq!(policy, back); |
| 159 | + } |
| 160 | + |
| 161 | + #[test] |
| 162 | + fn network_policy_allow_all_serde() { |
| 163 | + let policy = NetworkPolicy::AllowAll; |
| 164 | + let json = serde_json::to_string(&policy).unwrap(); |
| 165 | + let back: NetworkPolicy = serde_json::from_str(&json).unwrap(); |
| 166 | + assert_eq!(policy, back); |
| 167 | + } |
| 168 | + |
| 169 | + #[test] |
| 170 | + fn network_policy_allow_list_serde() { |
| 171 | + let policy = NetworkPolicy::AllowList { |
| 172 | + hosts: vec!["api.anthropic.com".into(), "api.openai.com".into()], |
| 173 | + }; |
| 174 | + let json = serde_json::to_string(&policy).unwrap(); |
| 175 | + assert!(json.contains("api.anthropic.com")); |
| 176 | + let back: NetworkPolicy = serde_json::from_str(&json).unwrap(); |
| 177 | + assert_eq!(policy, back); |
| 178 | + } |
| 179 | + |
| 180 | + #[test] |
| 181 | + fn network_policy_allow_list_empty_hosts() { |
| 182 | + let json = r#"{"policy":"allow_list"}"#; |
| 183 | + let policy: NetworkPolicy = serde_json::from_str(json).unwrap(); |
| 184 | + match policy { |
| 185 | + NetworkPolicy::AllowList { hosts } => assert!(hosts.is_empty()), |
| 186 | + _ => panic!("expected AllowList"), |
| 187 | + } |
| 188 | + } |
| 189 | +} |
0 commit comments