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 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 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 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 {}