Skip to main content

sail/sailbox/
types.rs

1//! Typed domain models for the sailbox API: lifecycle, listing, ingress
2//! listeners, and volumes.
3//!
4//! The core owns the wire schema: it deserializes API responses into these
5//! structs (applying the wire-schema field defaults) and serializes them back
6//! out for bindings. A binding maps a struct onto its own public type by field
7//! name without re-parsing the wire shape.
8
9use std::time::Duration;
10
11use serde::{Deserialize, Serialize};
12use time::OffsetDateTime;
13
14/// Lifecycle status of a sailbox. An unrecognized server value is preserved in
15/// `Other` so a status a newer backend introduces never fails parsing. The
16/// default is `Other("")`: the status of a handle no server response has
17/// filled in yet.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum SailboxStatus {
20    /// Running and serving.
21    Running,
22    /// Paused in memory.
23    Paused,
24    /// Sleeping (checkpointed to disk).
25    Sleeping,
26    /// In a failed state.
27    Failed,
28    /// Terminated.
29    Terminated,
30    /// A status this SDK version does not recognize, kept verbatim.
31    Other(String),
32}
33
34impl Default for SailboxStatus {
35    fn default() -> Self {
36        SailboxStatus::Other(String::new())
37    }
38}
39
40impl SailboxStatus {
41    /// The wire string for this status.
42    pub fn as_str(&self) -> &str {
43        match self {
44            SailboxStatus::Running => "running",
45            SailboxStatus::Paused => "paused",
46            SailboxStatus::Sleeping => "sleeping",
47            SailboxStatus::Failed => "failed",
48            SailboxStatus::Terminated => "terminated",
49            SailboxStatus::Other(s) => s,
50        }
51    }
52}
53
54impl From<&str> for SailboxStatus {
55    fn from(s: &str) -> SailboxStatus {
56        match s {
57            "running" => SailboxStatus::Running,
58            "paused" => SailboxStatus::Paused,
59            "sleeping" => SailboxStatus::Sleeping,
60            "failed" => SailboxStatus::Failed,
61            "terminated" => SailboxStatus::Terminated,
62            other => SailboxStatus::Other(other.to_string()),
63        }
64    }
65}
66
67impl std::fmt::Display for SailboxStatus {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        f.write_str(self.as_str())
70    }
71}
72
73impl Serialize for SailboxStatus {
74    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
75        serializer.serialize_str(self.as_str())
76    }
77}
78
79impl<'de> Deserialize<'de> for SailboxStatus {
80    fn deserialize<D: serde::Deserializer<'de>>(
81        deserializer: D,
82    ) -> Result<SailboxStatus, D::Error> {
83        Ok(SailboxStatus::from(
84            String::deserialize(deserializer)?.as_str(),
85        ))
86    }
87}
88
89/// A lifecycle status a client can filter by in [`ListSailboxesQuery`]. A closed set: a
90/// client only filters by statuses the SDK knows. Server responses use the open
91/// [`SailboxStatus`], which tolerates a status a newer backend introduces.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum SailboxStatusFilter {
94    /// Running and serving.
95    Running,
96    /// Paused in memory.
97    Paused,
98    /// Sleeping (checkpointed to disk).
99    Sleeping,
100    /// In a failed state.
101    Failed,
102    /// Terminated.
103    Terminated,
104}
105
106/// Server-side ordering for list results.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
108pub enum SailboxListOrder {
109    /// Activity recency: active/restorable boxes first, then updated_at.
110    #[default]
111    Activity,
112    /// Newest-created boxes first.
113    CreatedAtDesc,
114}
115
116impl SailboxListOrder {
117    /// The wire string for this order.
118    pub fn as_str(&self) -> &'static str {
119        match self {
120            SailboxListOrder::Activity => "activity",
121            SailboxListOrder::CreatedAtDesc => "created_at_desc",
122        }
123    }
124
125    /// Parse a wire string.
126    pub fn parse(raw: &str) -> Option<SailboxListOrder> {
127        match raw.trim() {
128            "activity" => Some(SailboxListOrder::Activity),
129            "created_at_desc" => Some(SailboxListOrder::CreatedAtDesc),
130            _ => None,
131        }
132    }
133}
134
135impl SailboxStatusFilter {
136    /// The wire string for this status.
137    pub fn as_str(&self) -> &'static str {
138        match self {
139            SailboxStatusFilter::Running => "running",
140            SailboxStatusFilter::Paused => "paused",
141            SailboxStatusFilter::Sleeping => "sleeping",
142            SailboxStatusFilter::Failed => "failed",
143            SailboxStatusFilter::Terminated => "terminated",
144        }
145    }
146
147    /// Parse a wire string, returning `None` for a status the SDK does not know.
148    pub fn parse(s: &str) -> Option<SailboxStatusFilter> {
149        match s {
150            "running" => Some(SailboxStatusFilter::Running),
151            "paused" => Some(SailboxStatusFilter::Paused),
152            "sleeping" => Some(SailboxStatusFilter::Sleeping),
153            "failed" => Some(SailboxStatusFilter::Failed),
154            "terminated" => Some(SailboxStatusFilter::Terminated),
155            _ => None,
156        }
157    }
158}
159
160/// Wire protocol for an ingress port or listener. An unrecognized value is
161/// preserved in `Other` so a protocol a newer backend introduces never fails.
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub enum ListenerProtocol {
164    /// Raw TCP.
165    Tcp,
166    /// HTTP.
167    Http,
168    /// A protocol this SDK version does not recognize, kept verbatim.
169    Other(String),
170}
171
172impl ListenerProtocol {
173    /// The wire string for this protocol.
174    pub fn as_str(&self) -> &str {
175        match self {
176            ListenerProtocol::Tcp => "tcp",
177            ListenerProtocol::Http => "http",
178            ListenerProtocol::Other(s) => s,
179        }
180    }
181}
182
183impl From<&str> for ListenerProtocol {
184    fn from(s: &str) -> ListenerProtocol {
185        match s {
186            "tcp" => ListenerProtocol::Tcp,
187            "http" => ListenerProtocol::Http,
188            other => ListenerProtocol::Other(other.to_string()),
189        }
190    }
191}
192
193impl std::fmt::Display for ListenerProtocol {
194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195        f.write_str(self.as_str())
196    }
197}
198
199impl Serialize for ListenerProtocol {
200    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
201        serializer.serialize_str(self.as_str())
202    }
203}
204
205impl<'de> Deserialize<'de> for ListenerProtocol {
206    fn deserialize<D: serde::Deserializer<'de>>(
207        deserializer: D,
208    ) -> Result<ListenerProtocol, D::Error> {
209        Ok(ListenerProtocol::from(
210            String::deserialize(deserializer)?.as_str(),
211        ))
212    }
213}
214
215/// Status of a listener's ingress route. An unrecognized value is preserved in
216/// `Other` so a status a newer backend introduces never fails to parse.
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub enum ListenerRouteStatus {
219    /// The route state is not yet known.
220    Unspecified,
221    /// The route is being set up.
222    Pending,
223    /// The route is active and ready to carry traffic.
224    Active,
225    /// The route is being restored after a checkpoint or migration.
226    Restoring,
227    /// The route is temporarily unavailable.
228    Unavailable,
229    /// A status this SDK version does not recognize, kept verbatim.
230    Other(String),
231}
232
233impl ListenerRouteStatus {
234    /// The user-facing string for this status. The backend's proto spelling
235    /// (`LISTENER_ROUTE_STATUS_*`) is normalized here, once, so no wrapper
236    /// ever sees it.
237    pub fn as_str(&self) -> &str {
238        match self {
239            ListenerRouteStatus::Unspecified => "unspecified",
240            ListenerRouteStatus::Pending => "pending",
241            ListenerRouteStatus::Active => "active",
242            ListenerRouteStatus::Restoring => "restoring",
243            ListenerRouteStatus::Unavailable => "unavailable",
244            ListenerRouteStatus::Other(s) => s,
245        }
246    }
247}
248
249impl From<&str> for ListenerRouteStatus {
250    /// Parse either the backend's proto spelling or the friendly form (so a
251    /// serialized [`Listener`](crate::Listener) round-trips).
252    fn from(s: &str) -> ListenerRouteStatus {
253        match s {
254            "LISTENER_ROUTE_STATUS_UNSPECIFIED" | "unspecified" => ListenerRouteStatus::Unspecified,
255            "LISTENER_ROUTE_STATUS_PENDING" | "pending" => ListenerRouteStatus::Pending,
256            "LISTENER_ROUTE_STATUS_ACTIVE" | "active" => ListenerRouteStatus::Active,
257            "LISTENER_ROUTE_STATUS_RESTORING" | "restoring" => ListenerRouteStatus::Restoring,
258            "LISTENER_ROUTE_STATUS_UNAVAILABLE" | "unavailable" => ListenerRouteStatus::Unavailable,
259            other => ListenerRouteStatus::Other(other.to_string()),
260        }
261    }
262}
263
264impl std::fmt::Display for ListenerRouteStatus {
265    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266        f.write_str(self.as_str())
267    }
268}
269
270impl Serialize for ListenerRouteStatus {
271    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
272        serializer.serialize_str(self.as_str())
273    }
274}
275
276impl<'de> Deserialize<'de> for ListenerRouteStatus {
277    fn deserialize<D: serde::Deserializer<'de>>(
278        deserializer: D,
279    ) -> Result<ListenerRouteStatus, D::Error> {
280        Ok(ListenerRouteStatus::from(
281            String::deserialize(deserializer)?.as_str(),
282        ))
283    }
284}
285
286/// Transport protocol a client requests when exposing an ingress port. A closed
287/// set: a client can only request a protocol the SDK supports. Server responses
288/// use the open [`ListenerProtocol`], which tolerates a protocol a newer backend adds.
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290pub enum IngressProtocol {
291    /// Raw TCP.
292    Tcp,
293    /// HTTP.
294    Http,
295}
296
297impl IngressProtocol {
298    /// The wire string for this protocol.
299    pub fn as_str(&self) -> &'static str {
300        match self {
301            IngressProtocol::Tcp => "tcp",
302            IngressProtocol::Http => "http",
303        }
304    }
305
306    /// Parse a wire string, returning `None` for a protocol the SDK does not
307    /// support.
308    pub fn parse(s: &str) -> Option<IngressProtocol> {
309        match s {
310            "tcp" => Some(IngressProtocol::Tcp),
311            "http" => Some(IngressProtocol::Http),
312            _ => None,
313        }
314    }
315}
316
317impl std::fmt::Display for IngressProtocol {
318    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319        f.write_str(self.as_str())
320    }
321}
322
323impl Serialize for IngressProtocol {
324    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
325        serializer.serialize_str(self.as_str())
326    }
327}
328
329/// Options for [`Sailbox::checkpoint`](crate::Sailbox::checkpoint).
330#[derive(Debug, Clone, Default)]
331pub struct CheckpointOptions {
332    /// Human-readable label for the checkpoint.
333    pub name: Option<String>,
334    /// Retention override; must be positive when given.
335    pub ttl: Option<std::time::Duration>,
336}
337
338/// Backends report kernel spellings (`x86_64`, `aarch64`); the SDK speaks the
339/// create-side vocabulary (`amd64`, `arm64`).
340fn normalized_architecture<'de, D: serde::Deserializer<'de>>(
341    deserializer: D,
342) -> Result<String, D::Error> {
343    let raw = String::deserialize(deserializer)?;
344    Ok(match raw.as_str() {
345        "x86_64" | "amd64" => "amd64".to_string(),
346        "aarch64" | "arm64" => "arm64".to_string(),
347        _ => raw,
348    })
349}
350
351/// Named sailbox resource size. Sailboxes are offered in a small discrete CPU
352/// menu rather than free-form vCPU/memory/disk. Each size sets the vCPU count
353/// plus a default memory and disk ceiling. Billing is by observed usage, so a
354/// larger size is a higher ceiling, not a higher bill.
355#[derive(Debug, Clone, Copy, PartialEq, Eq)]
356pub enum SailboxSize {
357    /// Small (1 vCPU): fastest cold starts, forks, and resumes, with
358    /// lower ceilings that cap what a runaway workload can consume.
359    Small,
360    /// Medium (4 vCPU, default).
361    Medium,
362}
363
364impl SailboxSize {
365    /// The wire string for this size.
366    pub fn as_str(&self) -> &'static str {
367        match self {
368            SailboxSize::Small => "s",
369            SailboxSize::Medium => "m",
370        }
371    }
372
373    /// Parse a wire string, returning `None` for an unsupported size.
374    pub fn parse(s: &str) -> Option<SailboxSize> {
375        match s {
376            "s" => Some(SailboxSize::Small),
377            "m" => Some(SailboxSize::Medium),
378            _ => None,
379        }
380    }
381
382    /// The allowed size labels in ascending order, for error messages and help.
383    pub fn allowed() -> &'static [&'static str] {
384        &["s", "m"]
385    }
386}
387
388impl std::fmt::Display for SailboxSize {
389    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
390        f.write_str(self.as_str())
391    }
392}
393
394impl Serialize for SailboxSize {
395    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
396        serializer.serialize_str(self.as_str())
397    }
398}
399
400/// A running-sailbox handle: the fields needed to exec/file/listener against a
401/// live box. Returned by create/resume/from_checkpoint.
402#[derive(Debug, Clone, Default, Serialize)]
403pub struct SailboxHandle {
404    /// The sailbox's stable identifier.
405    pub sailbox_id: String,
406    /// The caller-supplied sailbox name.
407    pub name: String,
408    /// The sailbox's lifecycle status (for example `running`).
409    pub status: SailboxStatus,
410    /// Network address of the worker hosting the sailbox.
411    pub worker_address: String,
412    /// Endpoint used to open exec/file/listener streams to the box.
413    pub exec_endpoint: String,
414}
415
416/// Read-only snapshot from get/list. Deliberately omits worker_address /
417/// exec_endpoint (the list/get endpoints do not return them).
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct SailboxInfo {
420    /// The sailbox's stable identifier.
421    pub sailbox_id: String,
422    /// Identifier of the owning app.
423    pub app_id: String,
424    /// Name of the owning app.
425    pub app_name: String,
426    /// Identifier of the image the sailbox was created from.
427    pub image_id: String,
428    /// The caller-supplied sailbox name.
429    pub name: String,
430    /// The sailbox's lifecycle status (for example `running`).
431    pub status: SailboxStatus,
432    /// Configured memory size, in mebibytes.
433    pub memory_mib: i64,
434    /// Configured number of virtual CPUs.
435    pub vcpu_count: i64,
436    /// Configured state-disk size, in gibibytes.
437    pub state_disk_size_gib: i64,
438    /// Requested CPU allocation, in vCPUs.
439    pub cpu_requested_vcpu: i64,
440    /// Current CPU usage, in vCPUs.
441    pub cpu_used_vcpu: f64,
442    /// Requested memory allocation, in bytes.
443    pub memory_requested_bytes: i64,
444    /// Current memory usage, in bytes.
445    pub memory_used_bytes: i64,
446    /// Requested disk allocation, in bytes.
447    pub disk_requested_bytes: i64,
448    /// Current disk usage, in bytes.
449    pub disk_used_bytes: i64,
450    /// CPU architecture of the sailbox, in the create-side vocabulary
451    /// (`amd64` or `arm64`).
452    #[serde(deserialize_with = "normalized_architecture", default)]
453    pub architecture: String,
454    /// Guest schema version the box is running, when reported by the backend.
455    #[serde(default)]
456    pub guest_schema_version: Option<i64>,
457    /// Human-readable error detail, present when the box is in an error state.
458    #[serde(default)]
459    pub error_message: Option<String>,
460    /// Monotonic checkpoint generation counter for the sailbox.
461    pub checkpoint_generation: i64,
462    /// When the box last started, if it has started.
463    #[serde(with = "time::serde::rfc3339::option", default)]
464    pub started_at: Option<OffsetDateTime>,
465    /// When the most recent checkpoint was taken, if any.
466    #[serde(with = "time::serde::rfc3339::option", default)]
467    pub last_checkpointed_at: Option<OffsetDateTime>,
468    /// When the sailbox was created.
469    #[serde(with = "time::serde::rfc3339")]
470    pub created_at: OffsetDateTime,
471    /// When the sailbox was last updated.
472    #[serde(with = "time::serde::rfc3339")]
473    pub updated_at: OffsetDateTime,
474    /// The user whose credential created this box (for a fork or restore, the
475    /// user who ran it). `None` for service-key creates.
476    #[serde(default)]
477    pub created_by_user_id: Option<String>,
478    /// Access visibility: `Some("private")` restricts exec/files/SSH and
479    /// lifecycle to the creator; `None`/`Some("org")` is org-wide access.
480    #[serde(default)]
481    pub visibility: Option<String>,
482}
483
484/// One page of list results plus the pagination envelope.
485#[derive(Debug, Clone, Serialize)]
486pub struct SailboxPage {
487    /// The sailboxes in this page.
488    pub items: Vec<SailboxInfo>,
489    /// Maximum number of items requested for this page.
490    pub limit: i64,
491    /// Zero-based offset of the first item in this page.
492    pub offset: i64,
493    /// Total number of sailboxes matching the query across all pages.
494    pub total: i64,
495    /// True when further pages exist beyond this one.
496    pub has_more: bool,
497}
498
499/// Query parameters for [`Client::sailbox_spend`](crate::Client::sailbox_spend).
500#[derive(Debug, Clone, Default)]
501pub struct SailboxSpendQuery {
502    /// Restrict spend to sailboxes owned by this app id.
503    pub app_id: Option<String>,
504    /// Restrict spend to one sailbox id.
505    pub sailbox_id: Option<String>,
506    /// Inclusive lower bound for the spend window. The API defaults to the
507    /// current UTC month when omitted.
508    pub from: Option<OffsetDateTime>,
509    /// Exclusive upper bound for the spend window. The API defaults to now when
510    /// omitted.
511    pub to: Option<OffsetDateTime>,
512}
513
514/// Estimated sailbox spend over a requested time window.
515#[derive(Debug, Clone, Serialize, Deserialize)]
516pub struct SailboxSpendResponse {
517    /// Start of the spend window.
518    #[serde(with = "time::serde::rfc3339")]
519    pub start_at: OffsetDateTime,
520    /// End of the spend window.
521    #[serde(with = "time::serde::rfc3339")]
522    pub end_at: OffsetDateTime,
523    /// Cost from closed billing segments, in USD nanos.
524    pub finalized_cost_usd_nanos: i64,
525    /// Estimated cost from still-open billing segments, in USD nanos.
526    pub estimated_active_cost_usd_nanos: i64,
527    /// Total estimated cost, in USD nanos.
528    pub estimated_total_cost_usd_nanos: i64,
529    /// Total observed duration, in seconds.
530    pub duration_seconds: i64,
531    /// Total vCPU-seconds observed.
532    pub vcpu_seconds: f64,
533    /// Total GiB-seconds of memory observed.
534    pub memory_gib_seconds: f64,
535    /// Total GiB-seconds of state disk observed.
536    pub state_disk_gib_seconds: f64,
537    /// Whether non-zero pricing rates were configured by the backend.
538    pub pricing_configured: bool,
539    /// Pricing rates used for the local estimate.
540    pub rates: SailboxSpendRates,
541    /// Per-sailbox spend summaries.
542    pub sailboxes: Vec<SailboxSpendItem>,
543}
544
545/// Rate metadata returned with a sailbox spend estimate.
546#[derive(Debug, Clone, Default, Serialize, Deserialize)]
547pub struct SailboxSpendRates {
548    /// Nanodollars per vCPU-second.
549    pub vcpu_second_usd_nanos: i64,
550    /// Nanodollars per memory GiB-second.
551    pub memory_gib_second_usd_nanos: i64,
552    /// Nanodollars per arm64 state-disk GiB-second.
553    pub state_disk_gib_second_usd_nanos: i64,
554    /// Nanodollars per sailbox creation.
555    pub creation_usd_nanos: i64,
556    /// Nanodollars per amd64 state-disk GiB-second.
557    #[serde(default)]
558    pub amd64_state_disk_gib_second_usd_nanos: i64,
559}
560
561/// Estimated spend for one sailbox over a requested time window.
562#[derive(Debug, Clone, Serialize, Deserialize)]
563pub struct SailboxSpendItem {
564    /// Sailbox id.
565    pub sailbox_id: String,
566    /// Owning app id.
567    pub app_id: String,
568    /// Cost from closed billing segments, in USD nanos.
569    pub finalized_cost_usd_nanos: i64,
570    /// Estimated cost from still-open billing segments, in USD nanos.
571    pub estimated_active_cost_usd_nanos: i64,
572    /// Total estimated cost, in USD nanos.
573    pub estimated_total_cost_usd_nanos: i64,
574    /// Observed duration, in seconds.
575    pub duration_seconds: i64,
576    /// Observed vCPU-seconds.
577    pub vcpu_seconds: f64,
578    /// Observed GiB-seconds of memory.
579    pub memory_gib_seconds: f64,
580    /// Observed GiB-seconds of state disk.
581    pub state_disk_gib_seconds: f64,
582    /// Whether this row includes an active billing segment.
583    pub active: bool,
584}
585
586/// Query parameters for [`Client::sailbox_metrics`](crate::Client::sailbox_metrics).
587#[derive(Debug, Clone)]
588pub struct SailboxMetricsQuery {
589    /// Window name accepted by the API, for example `1h`, `6h`, `24h`, or `7d`.
590    pub range: String,
591}
592
593impl Default for SailboxMetricsQuery {
594    fn default() -> SailboxMetricsQuery {
595        SailboxMetricsQuery {
596            range: "24h".to_string(),
597        }
598    }
599}
600
601/// Resource-usage time series for one sailbox.
602#[derive(Debug, Clone, Serialize, Deserialize)]
603pub struct SailboxMetricsResponse {
604    /// Echoed range selected by the API.
605    pub range: String,
606    /// Time-ordered metric points.
607    pub data: Vec<SailboxMetricPoint>,
608}
609
610/// One bucket in a sailbox resource-usage time series.
611#[derive(Debug, Clone, Serialize, Deserialize)]
612pub struct SailboxMetricPoint {
613    /// Bucket timestamp.
614    #[serde(with = "time::serde::rfc3339")]
615    pub timestamp: OffsetDateTime,
616    /// Current CPU usage, in vCPUs.
617    pub cpu_used_vcpu: f64,
618    /// Requested CPU allocation, in vCPUs.
619    pub cpu_requested_vcpu: i64,
620    /// Current memory usage, in bytes.
621    pub memory_used_bytes: i64,
622    /// Requested memory allocation, in bytes.
623    pub memory_requested_bytes: i64,
624    /// Current disk usage, in bytes.
625    pub disk_used_bytes: i64,
626    /// Requested disk allocation, in bytes.
627    pub disk_requested_bytes: i64,
628}
629
630/// A durable checkpoint handle. `status` echoes the source sailbox's lifecycle
631/// status after checkpointing (a running box is snapshotted; a paused/sleeping
632/// one returns its existing checkpoint), so the binding can sync its handle.
633#[derive(Debug, Clone, Serialize)]
634pub struct SailboxCheckpoint {
635    /// Stable identifier of the checkpoint.
636    pub checkpoint_id: String,
637    /// Identifier of the sailbox the checkpoint was taken from.
638    pub sailbox_id: String,
639    /// Checkpoint generation counter captured by this checkpoint.
640    pub checkpoint_generation: i64,
641    /// When the handle expires and becomes eligible for garbage collection;
642    /// `None` for a handle that carries no fresh retention bound (a paused or
643    /// sleeping source).
644    #[serde(with = "time::serde::rfc3339::option")]
645    pub expires_at: Option<OffsetDateTime>,
646    /// Source sailbox's lifecycle status after checkpointing.
647    pub status: SailboxStatus,
648}
649
650/// Filters for list/list_page.
651#[derive(Debug, Clone)]
652pub struct ListSailboxesQuery {
653    /// Restrict results to sailboxes owned by the app with this id.
654    pub app_id: Option<String>,
655    /// Restrict results to sailboxes in this lifecycle status.
656    pub status: Option<SailboxStatusFilter>,
657    /// Free-text search filter applied by the backend.
658    pub search: Option<String>,
659    /// Exclude sailboxes whose guest schema version exceeds this value.
660    pub max_guest_schema_version: Option<i64>,
661    /// Ordering to apply before pagination.
662    pub order: SailboxListOrder,
663    /// Maximum number of items to return.
664    pub limit: i64,
665    /// Zero-based offset of the first item to return.
666    pub offset: i64,
667}
668
669/// Default page size for listing, matching the sailbox API's own default. The
670/// API rejects `limit=0`, so the derived all-zero default cannot be used.
671#[doc(hidden)]
672pub const DEFAULT_LIST_LIMIT: i64 = 50;
673
674impl Default for ListSailboxesQuery {
675    fn default() -> ListSailboxesQuery {
676        ListSailboxesQuery {
677            app_id: None,
678            status: None,
679            search: None,
680            max_guest_schema_version: None,
681            order: SailboxListOrder::Activity,
682            limit: DEFAULT_LIST_LIMIT,
683            offset: 0,
684        }
685    }
686}
687
688/// One guest ingress port to reserve at create time.
689#[derive(Debug, Clone, Serialize)]
690pub struct IngressPort {
691    /// Port inside the guest to expose for ingress.
692    pub guest_port: u32,
693    /// Transport protocol for the port (for example `tcp`).
694    pub protocol: IngressProtocol,
695    /// Source addresses allowed to reach the port; empty means all sources are
696    /// allowed. Always sent (even when empty) so the request states it
697    /// explicitly rather than relying on an omitted field.
698    pub allowlist: Vec<String>,
699}
700
701/// One NFS volume mount.
702#[derive(Debug, Clone, Serialize)]
703pub struct VolumeMount {
704    /// Identifier of the NFS volume to mount.
705    pub volume_id: String,
706    /// Absolute path inside the guest where the volume is mounted.
707    pub mount_path: String,
708}
709
710/// A managed NFS volume as returned by the sailbox-volume API. (The local
711/// mount path is a binding-side concern and not part of this wire type.)
712#[derive(Debug, Clone, Serialize, Deserialize)]
713pub struct VolumeInfo {
714    /// Stable identifier of the volume.
715    pub volume_id: String,
716    /// Caller-supplied volume name.
717    pub name: String,
718    /// Storage backend serving the volume.
719    pub backend: String,
720    /// Lifecycle status of the volume.
721    pub status: String,
722    /// When the volume was created, if reported.
723    #[serde(with = "time::serde::rfc3339::option", default)]
724    pub created_at: Option<OffsetDateTime>,
725    /// When the volume was last updated, if reported.
726    #[serde(with = "time::serde::rfc3339::option", default)]
727    pub updated_at: Option<OffsetDateTime>,
728}
729
730/// The wire shape of the add-listener response: the scheduler resolves the
731/// public endpoint in the response (so a caller renders it without a second
732/// lookup) but reports nothing about routing, so it converts to a [`Listener`]
733/// with an unspecified route status.
734#[derive(Debug, Clone, Deserialize)]
735pub(crate) struct AddListenerWire {
736    pub(crate) guest_port: u32,
737    pub(crate) protocol: ListenerProtocol,
738    #[serde(default)]
739    pub(crate) public_url: String,
740    #[serde(default, rename = "tcp_public_host")]
741    pub(crate) public_host: String,
742    #[serde(default, rename = "tcp_public_port")]
743    pub(crate) public_port: u32,
744}
745
746impl From<AddListenerWire> for crate::worker::Listener {
747    fn from(wire: AddListenerWire) -> crate::worker::Listener {
748        crate::worker::Listener {
749            guest_port: wire.guest_port,
750            protocol: wire.protocol,
751            // The expose response says nothing about reachability; confirm
752            // with wait_for_listener or re-fetch via get/list.
753            route_status: ListenerRouteStatus::Unspecified,
754            public_url: wire.public_url,
755            public_host: wire.public_host,
756            public_port: wire.public_port,
757        }
758    }
759}
760
761/// A CA-signed SSH user certificate plus the key id that identifies the signing
762/// org (`org=<id>;fp=...;iat=...`).
763#[derive(Debug, Clone)]
764pub struct IssuedUserCert {
765    /// The OpenSSH certificate (`<type>-cert-v01@openssh.com AAAA...`).
766    pub certificate: String,
767    /// The certificate key id.
768    pub key_id: String,
769}
770
771/// How to reach an exposed listener: a routable HTTPS URL for `http`
772/// listeners, or a host/port any TCP client can dial for `tcp` listeners.
773#[derive(Debug, Clone, PartialEq, Eq)]
774pub enum ListenerEndpoint {
775    /// The HTTPS URL to reach the guest service.
776    Http {
777        /// Routable URL.
778        url: String,
779    },
780    /// The address to dial for a raw-TCP listener.
781    Tcp {
782        /// Hostname to dial.
783        host: String,
784        /// Port to dial.
785        port: u32,
786    },
787}
788
789impl std::fmt::Display for ListenerEndpoint {
790    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
791        match self {
792            ListenerEndpoint::Http { url } => f.write_str(url),
793            ListenerEndpoint::Tcp { host, port } => write!(f, "{host}:{port}"),
794        }
795    }
796}
797
798/// Inputs to create a sailbox. The image is a typed [`ImageSpec`](crate::image::ImageSpec);
799/// build one fluently with [`ImageDefinition`](crate::imagebuild::ImageDefinition).
800#[derive(Debug, Clone)]
801pub struct CreateSailboxRequest {
802    /// Identifier of the app that will own the sailbox.
803    pub app_id: String,
804    /// Caller-supplied name for the sailbox.
805    pub name: String,
806    /// Guest ingress ports to reserve at create time.
807    pub ingress_ports: Vec<IngressPort>,
808    /// NFS volumes to mount into the guest.
809    pub volume_mounts: Vec<VolumeMount>,
810    /// The image to create the sailbox from. Serialized to canonical proto-JSON
811    /// and sent as the `image` field.
812    pub image: crate::image::ImageSpec,
813    /// Requested resource size; defaults server-side when absent.
814    pub size: Option<SailboxSize>,
815    /// Optional memory limit in whole GiB, within the size's range; the
816    /// size's default when absent.
817    pub memory_gib: Option<u32>,
818    /// Optional disk size in whole GiB, within the size's range; the size's
819    /// default when absent.
820    pub disk_gib: Option<u32>,
821    /// Enable SSH on the new sailbox after create: trust the org SSH CA, start
822    /// `sshd`, and expose guest port 22 as TCP once the CA-only daemon owns it.
823    /// An explicit port-22 ingress entry contributes just its allowlist.
824    pub ssh: bool,
825    /// By default a sailbox is org-wide: any credential in the org can exec,
826    /// copy files, SSH, or run lifecycle operations on it. `true` restricts all
827    /// of that to the creating user. An org admin can override that with a
828    /// recorded reason for exec, files, and pause/resume/terminate/upgrade. SSH,
829    /// exposing or removing listeners, and fork/checkpoint/restore stay
830    /// creator-only. Requires a user-scoped API key.
831    pub private: bool,
832    /// Budget for rebuilding the image if Sail needs to rebuild it before
833    /// the sailbox is created; the default build budget applies when absent.
834    pub image_build_timeout: Option<Duration>,
835}
836
837impl Default for CreateSailboxRequest {
838    /// An empty request for a plain Debian-base sailbox: fill in `app_id` and
839    /// `name`, override the rest as needed.
840    fn default() -> CreateSailboxRequest {
841        CreateSailboxRequest {
842            app_id: String::new(),
843            name: String::new(),
844            ingress_ports: Vec::new(),
845            volume_mounts: Vec::new(),
846            image: crate::image::ImageSpec {
847                base: Some(crate::image::BaseImage::Debian),
848                ..crate::image::ImageSpec::default()
849            },
850            size: None,
851            memory_gib: None,
852            disk_gib: None,
853            ssh: false,
854            private: false,
855            image_build_timeout: None,
856        }
857    }
858}
859
860#[cfg(test)]
861mod tests {
862    use super::*;
863
864    #[test]
865    fn list_query_default_uses_a_valid_nonzero_limit() {
866        // The sailbox API rejects limit=0, so the default must be the API's own
867        // page size, not the derived zero.
868        let query = ListSailboxesQuery::default();
869        assert_eq!(query.limit, DEFAULT_LIST_LIMIT);
870        assert!(query.limit > 0);
871        assert_eq!(query.offset, 0);
872    }
873}