Skip to main content

hara_native/runtime/
sandbox_model.rs

1use std::error::Error;
2
3pub const SANDBOX_SPEC_PROTOCOL: &str = "hara.sandbox/0-alpha";
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
6pub struct SandboxId(u64);
7
8impl SandboxId {
9    pub fn parse(value: u64) -> Result<Self, SandboxError> {
10        if value == 0 {
11            Err(SandboxError::invalid_spec("invalid sandbox identifier"))
12        } else {
13            Ok(Self(value))
14        }
15    }
16
17    pub const fn get(self) -> u64 {
18        self.0
19    }
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23pub struct EvaluationId(u64);
24
25impl EvaluationId {
26    pub(crate) const fn new(value: u64) -> Self {
27        Self(value)
28    }
29
30    /// Returns the Kernel-allocated provider-local evaluation identifier.
31    pub const fn get(self) -> u64 {
32        self.0
33    }
34}
35
36impl fmt::Display for SandboxId {
37    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38        self.0.fmt(formatter)
39    }
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum SandboxState {
44    Open,
45    Running,
46    Cancelling,
47    Cancelled,
48    Failed,
49    Closed,
50}
51
52impl SandboxState {
53    pub const fn as_str(self) -> &'static str {
54        match self {
55            Self::Open => "open",
56            Self::Running => "running",
57            Self::Cancelling => "cancelling",
58            Self::Cancelled => "cancelled",
59            Self::Failed => "failed",
60            Self::Closed => "closed",
61        }
62    }
63}
64
65#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct SandboxLimits {
67    pub source_bytes: usize,
68    pub result_bytes: usize,
69    pub output_bytes: usize,
70    pub evaluation_ms: u64,
71    pub memory_bytes: usize,
72    pub active_evaluations: usize,
73}
74
75#[derive(Clone, Debug, PartialEq, Eq)]
76pub struct SandboxBundleReference {
77    pub digest: String,
78    pub format: String,
79}
80
81impl SandboxBundleReference {
82    pub fn new(digest: impl Into<String>, format: impl Into<String>) -> Result<Self, SandboxError> {
83        let reference = Self {
84            digest: digest.into(),
85            format: format.into(),
86        };
87        let digest = reference.digest.strip_prefix("sha256:");
88        if !digest.is_some_and(|digest| {
89            digest.len() == 64
90                && digest
91                    .bytes()
92                    .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
93        }) || reference.format.is_empty()
94        {
95            Err(SandboxError::invalid_spec(
96                "invalid sandbox bundle reference",
97            ))
98        } else {
99            Ok(reference)
100        }
101    }
102}
103
104impl Default for SandboxLimits {
105    fn default() -> Self {
106        Self {
107            source_bytes: 64 * 1024,
108            result_bytes: 1024 * 1024,
109            output_bytes: 1024 * 1024,
110            evaluation_ms: 5_000,
111            memory_bytes: 64 * 1024 * 1024,
112            active_evaluations: 1,
113        }
114    }
115}
116
117#[derive(Clone, Debug, PartialEq, Eq)]
118pub struct SandboxSpec {
119    protocol: String,
120    provider: String,
121    runtime: String,
122    entry_namespace: String,
123    bundles: Vec<SandboxBundleReference>,
124    mount: Option<SessionMountId>,
125    provider_options_hta: Vec<u8>,
126    limits: SandboxLimits,
127}
128
129impl SandboxSpec {
130    pub fn new(
131        protocol: impl Into<String>,
132        provider: impl Into<String>,
133        runtime: impl Into<String>,
134        entry_namespace: impl Into<String>,
135        limits: SandboxLimits,
136    ) -> Result<Self, SandboxError> {
137        let spec = Self {
138            protocol: protocol.into(),
139            provider: provider.into(),
140            runtime: runtime.into(),
141            entry_namespace: entry_namespace.into(),
142            bundles: Vec::new(),
143            mount: None,
144            provider_options_hta: Vec::new(),
145            limits,
146        };
147        spec.validate()?;
148        Ok(spec)
149    }
150
151    pub fn with_inputs(
152        protocol: impl Into<String>,
153        provider: impl Into<String>,
154        runtime: impl Into<String>,
155        entry_namespace: impl Into<String>,
156        bundles: Vec<SandboxBundleReference>,
157        mount: Option<SessionMountId>,
158        provider_options_hta: Vec<u8>,
159        limits: SandboxLimits,
160    ) -> Result<Self, SandboxError> {
161        let spec = Self {
162            protocol: protocol.into(),
163            provider: provider.into(),
164            runtime: runtime.into(),
165            entry_namespace: entry_namespace.into(),
166            bundles,
167            mount,
168            provider_options_hta,
169            limits,
170        };
171        spec.validate()?;
172        Ok(spec)
173    }
174
175    pub fn in_process() -> Self {
176        Self {
177            protocol: SANDBOX_SPEC_PROTOCOL.into(),
178            provider: "in-process".into(),
179            runtime: "hara.standard/0-alpha".into(),
180            entry_namespace: "user".into(),
181            bundles: Vec::new(),
182            mount: None,
183            provider_options_hta: Vec::new(),
184            limits: SandboxLimits::default(),
185        }
186    }
187
188    pub fn validate(&self) -> Result<(), SandboxError> {
189        if self.protocol != SANDBOX_SPEC_PROTOCOL {
190            return Err(SandboxError::invalid_spec("unsupported sandbox protocol"));
191        }
192        if self.provider.is_empty() || self.runtime.is_empty() {
193            return Err(SandboxError::invalid_spec(
194                "provider and runtime are required",
195            ));
196        }
197        SessionId::parse(&self.entry_namespace)
198            .map_err(|_| SandboxError::invalid_spec("invalid entry namespace"))?;
199        if self.limits.source_bytes == 0
200            || self.limits.result_bytes == 0
201            || self.limits.output_bytes == 0
202            || self.limits.evaluation_ms == 0
203            || self.limits.memory_bytes == 0
204            || self.limits.active_evaluations != 1
205        {
206            return Err(SandboxError::invalid_spec("invalid sandbox limits"));
207        }
208        Ok(())
209    }
210
211    pub fn provider(&self) -> &str {
212        &self.provider
213    }
214
215    /// Returns the immutable runtime profile selected for the provider.
216    pub fn runtime(&self) -> &str {
217        &self.runtime
218    }
219
220    pub fn entry_namespace(&self) -> &str {
221        &self.entry_namespace
222    }
223
224    pub fn limits(&self) -> &SandboxLimits {
225        &self.limits
226    }
227
228    pub fn bundles(&self) -> &[SandboxBundleReference] {
229        &self.bundles
230    }
231
232    pub const fn mount(&self) -> Option<SessionMountId> {
233        self.mount
234    }
235
236    pub fn provider_options_hta(&self) -> &[u8] {
237        &self.provider_options_hta
238    }
239}
240
241#[derive(Clone, Debug, PartialEq, Eq)]
242pub struct SandboxStatus {
243    pub id: SandboxId,
244    pub provider: String,
245    pub state: SandboxState,
246    pub secure: bool,
247    pub evaluation_active: bool,
248    pub error: Option<SandboxError>,
249}
250
251#[derive(Clone, Copy, Debug, PartialEq, Eq)]
252pub enum SandboxErrorCode {
253    InvalidSpec,
254    ProviderNotFound,
255    ProviderUnavailable,
256    BundleNotFound,
257    BundleDigestMismatch,
258    MountNotFound,
259    NotFound,
260    Closed,
261    Busy,
262    Cancelled,
263    Timeout,
264    LimitExceeded,
265    EvaluationFailed,
266    ResultNotTransferable,
267    TransportFailed,
268    ProviderFailed,
269    Unsupported,
270}
271
272impl SandboxErrorCode {
273    pub const fn as_str(self) -> &'static str {
274        match self {
275            Self::InvalidSpec => "sandbox/invalid-spec",
276            Self::ProviderNotFound => "sandbox/provider-not-found",
277            Self::ProviderUnavailable => "sandbox/provider-unavailable",
278            Self::BundleNotFound => "sandbox/bundle-not-found",
279            Self::BundleDigestMismatch => "sandbox/bundle-digest-mismatch",
280            Self::MountNotFound => "sandbox/mount-not-found",
281            Self::NotFound => "sandbox/not-found",
282            Self::Closed => "sandbox/not-found",
283            Self::Busy => "sandbox/busy",
284            Self::Cancelled => "sandbox/cancelled",
285            Self::Timeout => "sandbox/timeout",
286            Self::LimitExceeded => "sandbox/limit-exceeded",
287            Self::EvaluationFailed => "sandbox/evaluation-failed",
288            Self::ResultNotTransferable => "sandbox/result-not-transferable",
289            Self::TransportFailed => "sandbox/transport-failed",
290            Self::ProviderFailed => "sandbox/provider-failed",
291            Self::Unsupported => "sandbox/provider-unavailable",
292        }
293    }
294}
295
296#[derive(Clone, Debug, PartialEq, Eq)]
297pub struct SandboxError {
298    pub code: SandboxErrorCode,
299    pub message: String,
300}
301
302impl SandboxError {
303    /// Creates a stable sandbox error at an external provider boundary.
304    pub fn new(code: SandboxErrorCode, message: impl Into<String>) -> Self {
305        Self {
306            code,
307            message: message.into(),
308        }
309    }
310
311    pub(crate) fn invalid_spec(message: impl Into<String>) -> Self {
312        Self::new(SandboxErrorCode::InvalidSpec, message)
313    }
314}
315
316impl fmt::Display for SandboxError {
317    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
318        write!(formatter, "{}: {}", self.code.as_str(), self.message)
319    }
320}
321
322impl Error for SandboxError {}