sail/sailbox/types.rs
1//! Typed domain models for the sailbox lifecycle API.
2//!
3//! The core owns the wire schema: it deserializes API responses into these
4//! structs (applying the wire-schema field defaults) and serializes them back
5//! out for bindings. A binding maps a struct onto its own public type by field
6//! name without re-parsing the wire shape.
7
8use serde::{Deserialize, Serialize};
9
10/// A running-sailbox handle: the fields needed to exec/file/listener against a
11/// live box. Returned by create/resume/from_checkpoint.
12#[derive(Debug, Clone, Serialize)]
13pub struct SailboxHandle {
14 /// The sailbox's stable identifier.
15 pub sailbox_id: String,
16 /// The caller-supplied sailbox name.
17 pub name: String,
18 /// The sailbox's lifecycle status (for example `running`).
19 pub status: String,
20 /// Network address of the worker hosting the sailbox.
21 pub worker_address: String,
22 /// Endpoint used to open exec/file/listener streams to the box.
23 pub exec_endpoint: String,
24}
25
26/// Read-only snapshot from get/list. Deliberately omits worker_address /
27/// exec_endpoint (the list/get endpoints do not return them).
28#[derive(Debug, Clone, Serialize)]
29pub struct SailboxInfo {
30 /// The sailbox's stable identifier.
31 pub sailbox_id: String,
32 /// Identifier of the owning app.
33 pub app_id: String,
34 /// Name of the owning app.
35 pub app_name: String,
36 /// Identifier of the image the sailbox was created from.
37 pub image_id: String,
38 /// The caller-supplied sailbox name.
39 pub name: String,
40 /// The sailbox's lifecycle status (for example `running`).
41 pub status: String,
42 /// Configured memory size, in mebibytes.
43 pub memory_mib: i64,
44 /// Configured number of virtual CPUs.
45 pub vcpu_count: i64,
46 /// Configured state-disk size, in gibibytes.
47 pub state_disk_size_gib: i64,
48 /// Requested CPU allocation, in vCPUs.
49 pub cpu_requested_vcpu: i64,
50 /// Current CPU usage, in vCPUs.
51 pub cpu_used_vcpu: f64,
52 /// Requested memory allocation, in bytes.
53 pub memory_requested_bytes: i64,
54 /// Current memory usage, in bytes.
55 pub memory_used_bytes: i64,
56 /// Requested disk allocation, in bytes.
57 pub disk_requested_bytes: i64,
58 /// Current disk usage, in bytes.
59 pub disk_used_bytes: i64,
60 /// CPU architecture of the sailbox (for example `x86_64` or `arm64`).
61 pub architecture: String,
62 /// Guest schema version the box is running, when reported by the backend.
63 pub guest_schema_version: Option<i64>,
64 /// Human-readable error detail, present when the box is in an error state.
65 pub error_message: Option<String>,
66 /// Monotonic checkpoint generation counter for the sailbox.
67 pub checkpoint_generation: i64,
68 /// RFC 3339 timestamp of when the box last started, if it has started.
69 pub started_at: Option<String>,
70 /// RFC 3339 timestamp of the most recent checkpoint, if any.
71 pub last_checkpointed_at: Option<String>,
72 /// RFC 3339 timestamp of when the sailbox was created.
73 pub created_at: String,
74 /// RFC 3339 timestamp of the sailbox's last update.
75 pub updated_at: String,
76}
77
78/// Wire form: some backends omit the requested-resource fields, so they are
79/// optional and fall back to vcpu_count/memory_mib/state_disk_size_gib in [`From`].
80#[derive(Deserialize)]
81pub(crate) struct SailboxInfoWire {
82 sailbox_id: String,
83 app_id: String,
84 app_name: String,
85 #[serde(default)]
86 image_id: String,
87 name: String,
88 status: String,
89 memory_mib: i64,
90 vcpu_count: i64,
91 state_disk_size_gib: i64,
92 cpu_requested_vcpu: Option<i64>,
93 cpu_used_vcpu: Option<f64>,
94 memory_requested_bytes: Option<i64>,
95 memory_used_bytes: Option<i64>,
96 disk_requested_bytes: Option<i64>,
97 disk_used_bytes: Option<i64>,
98 architecture: String,
99 guest_schema_version: Option<i64>,
100 error_message: Option<String>,
101 checkpoint_generation: Option<i64>,
102 started_at: Option<String>,
103 last_checkpointed_at: Option<String>,
104 created_at: String,
105 updated_at: String,
106}
107
108impl From<SailboxInfoWire> for SailboxInfo {
109 fn from(w: SailboxInfoWire) -> SailboxInfo {
110 let memory_bytes = w.memory_mib.saturating_mul(1024 * 1024);
111 let disk_bytes = w.state_disk_size_gib.saturating_mul(1024 * 1024 * 1024);
112 SailboxInfo {
113 sailbox_id: w.sailbox_id,
114 app_id: w.app_id,
115 app_name: w.app_name,
116 image_id: w.image_id,
117 name: w.name,
118 status: w.status,
119 memory_mib: w.memory_mib,
120 vcpu_count: w.vcpu_count,
121 state_disk_size_gib: w.state_disk_size_gib,
122 cpu_requested_vcpu: w.cpu_requested_vcpu.unwrap_or(w.vcpu_count),
123 cpu_used_vcpu: w.cpu_used_vcpu.unwrap_or(0.0),
124 memory_requested_bytes: w.memory_requested_bytes.unwrap_or(memory_bytes),
125 memory_used_bytes: w.memory_used_bytes.unwrap_or(0),
126 disk_requested_bytes: w.disk_requested_bytes.unwrap_or(disk_bytes),
127 disk_used_bytes: w.disk_used_bytes.unwrap_or(0),
128 architecture: w.architecture,
129 guest_schema_version: w.guest_schema_version,
130 error_message: w.error_message,
131 checkpoint_generation: w.checkpoint_generation.unwrap_or(0),
132 started_at: w.started_at,
133 last_checkpointed_at: w.last_checkpointed_at,
134 created_at: w.created_at,
135 updated_at: w.updated_at,
136 }
137 }
138}
139
140/// One page of list results plus the pagination envelope.
141#[derive(Debug, Clone, Serialize)]
142pub struct SailboxPage {
143 /// The sailboxes in this page.
144 pub items: Vec<SailboxInfo>,
145 /// Maximum number of items requested for this page.
146 pub limit: i64,
147 /// Zero-based offset of the first item in this page.
148 pub offset: i64,
149 /// Total number of sailboxes matching the query across all pages.
150 pub total: i64,
151 /// True when further pages exist beyond this one.
152 pub has_more: bool,
153}
154
155/// A durable checkpoint handle. `status` echoes the source sailbox's lifecycle
156/// status after checkpointing (a running box is snapshotted; a paused/sleeping
157/// one returns its existing checkpoint), so the binding can sync its handle.
158#[derive(Debug, Clone, Serialize)]
159pub struct SailboxCheckpoint {
160 /// Stable identifier of the checkpoint.
161 pub checkpoint_id: String,
162 /// Identifier of the sailbox the checkpoint was taken from.
163 pub sailbox_id: String,
164 /// Checkpoint generation counter captured by this checkpoint.
165 pub checkpoint_generation: i64,
166 /// Source sailbox's lifecycle status after checkpointing.
167 pub status: String,
168}
169
170/// Filters for list/list_page.
171#[derive(Debug, Clone)]
172pub struct ListQuery {
173 /// Restrict results to sailboxes owned by this app (id or name).
174 pub app: Option<String>,
175 /// Restrict results to sailboxes in this lifecycle status.
176 pub status: Option<String>,
177 /// Free-text search filter applied by the backend.
178 pub search: Option<String>,
179 /// Exclude sailboxes whose guest schema version exceeds this value.
180 pub max_guest_schema_version: Option<i64>,
181 /// Maximum number of items to return.
182 pub limit: i64,
183 /// Zero-based offset of the first item to return.
184 pub offset: i64,
185}
186
187/// Default page size for listing, matching the sailbox API's own default. The
188/// API rejects `limit=0`, so the derived all-zero default cannot be used.
189pub const DEFAULT_LIST_LIMIT: i64 = 50;
190
191/// Resource-limit bounds enforced at create time. These mirror the scheduler's
192/// authoritative limits so every binding (CLI, Python, future SDKs) fails fast
193/// with the same error instead of round-tripping to a backend rejection.
194pub const MAX_SAILBOX_VCPUS: i64 = 4;
195/// Minimum requested memory in MiB (0 is allowed and means "use the default").
196pub const MIN_SAILBOX_MEMORY_MIB: i64 = 1024;
197/// Maximum requested memory in MiB.
198pub const MAX_SAILBOX_MEMORY_MIB: i64 = 8192;
199/// Maximum requested state-disk size in GiB.
200pub const MAX_SAILBOX_DISK_GIB: i64 = 32;
201
202impl Default for ListQuery {
203 fn default() -> ListQuery {
204 ListQuery {
205 app: None,
206 status: None,
207 search: None,
208 max_guest_schema_version: None,
209 limit: DEFAULT_LIST_LIMIT,
210 offset: 0,
211 }
212 }
213}
214
215/// One guest ingress port to reserve at create time.
216#[derive(Debug, Clone, Serialize)]
217pub struct IngressPort {
218 /// Port inside the guest to expose for ingress.
219 pub guest_port: u32,
220 /// Transport protocol for the port (for example `tcp`).
221 pub protocol: String,
222 /// Source addresses allowed to reach the port; empty means no restriction.
223 #[serde(skip_serializing_if = "Vec::is_empty")]
224 pub allowlist: Vec<String>,
225}
226
227/// One NFS volume mount.
228#[derive(Debug, Clone, Serialize)]
229pub struct VolumeMount {
230 /// Identifier of the NFS volume to mount.
231 pub volume_id: String,
232 /// Absolute path inside the guest where the volume is mounted.
233 pub mount_path: String,
234}
235
236/// A managed NFS volume as returned by the sailbox-volume API. (The local
237/// mount path is a binding-side concern and not part of this wire type.)
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct NfsVolume {
240 /// Stable identifier of the volume.
241 pub volume_id: String,
242 /// Caller-supplied volume name.
243 #[serde(default)]
244 pub name: String,
245 /// Storage backend serving the volume.
246 #[serde(default)]
247 pub backend: String,
248 /// Lifecycle status of the volume.
249 #[serde(default)]
250 pub status: String,
251 /// RFC 3339 timestamp of when the volume was created, if reported.
252 #[serde(default)]
253 pub created_at: Option<String>,
254 /// RFC 3339 timestamp of the volume's last update, if reported.
255 #[serde(default)]
256 pub updated_at: Option<String>,
257}
258
259/// Inputs to create a sailbox. The image is a binding-built spec passed through
260/// opaquely (the image DSL lives in the language wrapper), while the lifecycle
261/// envelope is owned by the core.
262#[derive(Debug, Clone)]
263pub struct CreateSailboxRequest {
264 /// Identifier of the app that will own the sailbox.
265 pub app_id: String,
266 /// Caller-supplied name for the sailbox.
267 pub name: String,
268 /// Guest ingress ports to reserve at create time.
269 pub ingress_ports: Vec<IngressPort>,
270 /// NFS volumes to mount into the guest.
271 pub volume_mounts: Vec<VolumeMount>,
272 /// The image spec as a JSON object built by the binding's image DSL.
273 pub image: serde_json::Value,
274 /// Requested CPU allocation in vCPUs; defaults server-side when absent.
275 pub cpu: Option<i64>,
276 /// Requested memory size in mebibytes; defaults server-side when absent.
277 pub memory_mib: Option<i64>,
278 /// Requested state-disk size in gibibytes; defaults server-side when absent.
279 pub state_disk_size_gib: Option<i64>,
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn list_query_default_uses_a_valid_nonzero_limit() {
288 // The sailbox API rejects limit=0, so the default must be the API's own
289 // page size, not the derived zero.
290 let query = ListQuery::default();
291 assert_eq!(query.limit, DEFAULT_LIST_LIMIT);
292 assert!(query.limit > 0);
293 assert_eq!(query.offset, 0);
294 }
295}