Skip to main content

aios_protocol/
sandbox.rs

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
7use 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")]
17pub 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)]
31pub 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
41impl 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")]
54pub 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)]
68mod 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}