sail-rs 0.1.0

Official Rust SDK for Sail: create and drive sailboxes (sandboxed cloud VMs) with lifecycle, streaming exec, file transfer, and ingress.
Documentation
//! Typed domain models for the sailbox lifecycle API.
//!
//! The core owns the wire schema: it deserializes API responses into these
//! structs (applying the wire-schema field defaults) and serializes them back
//! out for bindings. A binding maps a struct onto its own public type by field
//! name without re-parsing the wire shape.

use serde::{Deserialize, Serialize};

/// A running-sailbox handle: the fields needed to exec/file/listener against a
/// live box. Returned by create/resume/from_checkpoint.
#[derive(Debug, Clone, Serialize)]
pub struct SailboxHandle {
    /// The sailbox's stable identifier.
    pub sailbox_id: String,
    /// The caller-supplied sailbox name.
    pub name: String,
    /// The sailbox's lifecycle status (for example `running`).
    pub status: String,
    /// Network address of the worker hosting the sailbox.
    pub worker_address: String,
    /// Endpoint used to open exec/file/listener streams to the box.
    pub exec_endpoint: String,
}

/// Read-only snapshot from get/list. Deliberately omits worker_address /
/// exec_endpoint (the list/get endpoints do not return them).
#[derive(Debug, Clone, Serialize)]
pub struct SailboxInfo {
    /// The sailbox's stable identifier.
    pub sailbox_id: String,
    /// Identifier of the owning app.
    pub app_id: String,
    /// Name of the owning app.
    pub app_name: String,
    /// Identifier of the image the sailbox was created from.
    pub image_id: String,
    /// The caller-supplied sailbox name.
    pub name: String,
    /// The sailbox's lifecycle status (for example `running`).
    pub status: String,
    /// Configured memory size, in mebibytes.
    pub memory_mib: i64,
    /// Configured number of virtual CPUs.
    pub vcpu_count: i64,
    /// Configured state-disk size, in gibibytes.
    pub state_disk_size_gib: i64,
    /// Requested CPU allocation, in vCPUs.
    pub cpu_requested_vcpu: i64,
    /// Current CPU usage, in vCPUs.
    pub cpu_used_vcpu: f64,
    /// Requested memory allocation, in bytes.
    pub memory_requested_bytes: i64,
    /// Current memory usage, in bytes.
    pub memory_used_bytes: i64,
    /// Requested disk allocation, in bytes.
    pub disk_requested_bytes: i64,
    /// Current disk usage, in bytes.
    pub disk_used_bytes: i64,
    /// CPU architecture of the sailbox (for example `x86_64` or `arm64`).
    pub architecture: String,
    /// Guest schema version the box is running, when reported by the backend.
    pub guest_schema_version: Option<i64>,
    /// Human-readable error detail, present when the box is in an error state.
    pub error_message: Option<String>,
    /// Monotonic checkpoint generation counter for the sailbox.
    pub checkpoint_generation: i64,
    /// RFC 3339 timestamp of when the box last started, if it has started.
    pub started_at: Option<String>,
    /// RFC 3339 timestamp of the most recent checkpoint, if any.
    pub last_checkpointed_at: Option<String>,
    /// RFC 3339 timestamp of when the sailbox was created.
    pub created_at: String,
    /// RFC 3339 timestamp of the sailbox's last update.
    pub updated_at: String,
}

/// Wire form: some backends omit the requested-resource fields, so they are
/// optional and fall back to vcpu_count/memory_mib/state_disk_size_gib in [`From`].
#[derive(Deserialize)]
pub(crate) struct SailboxInfoWire {
    sailbox_id: String,
    app_id: String,
    app_name: String,
    #[serde(default)]
    image_id: String,
    name: String,
    status: String,
    memory_mib: i64,
    vcpu_count: i64,
    state_disk_size_gib: i64,
    cpu_requested_vcpu: Option<i64>,
    cpu_used_vcpu: Option<f64>,
    memory_requested_bytes: Option<i64>,
    memory_used_bytes: Option<i64>,
    disk_requested_bytes: Option<i64>,
    disk_used_bytes: Option<i64>,
    architecture: String,
    guest_schema_version: Option<i64>,
    error_message: Option<String>,
    checkpoint_generation: Option<i64>,
    started_at: Option<String>,
    last_checkpointed_at: Option<String>,
    created_at: String,
    updated_at: String,
}

impl From<SailboxInfoWire> for SailboxInfo {
    fn from(w: SailboxInfoWire) -> SailboxInfo {
        let memory_bytes = w.memory_mib.saturating_mul(1024 * 1024);
        let disk_bytes = w.state_disk_size_gib.saturating_mul(1024 * 1024 * 1024);
        SailboxInfo {
            sailbox_id: w.sailbox_id,
            app_id: w.app_id,
            app_name: w.app_name,
            image_id: w.image_id,
            name: w.name,
            status: w.status,
            memory_mib: w.memory_mib,
            vcpu_count: w.vcpu_count,
            state_disk_size_gib: w.state_disk_size_gib,
            cpu_requested_vcpu: w.cpu_requested_vcpu.unwrap_or(w.vcpu_count),
            cpu_used_vcpu: w.cpu_used_vcpu.unwrap_or(0.0),
            memory_requested_bytes: w.memory_requested_bytes.unwrap_or(memory_bytes),
            memory_used_bytes: w.memory_used_bytes.unwrap_or(0),
            disk_requested_bytes: w.disk_requested_bytes.unwrap_or(disk_bytes),
            disk_used_bytes: w.disk_used_bytes.unwrap_or(0),
            architecture: w.architecture,
            guest_schema_version: w.guest_schema_version,
            error_message: w.error_message,
            checkpoint_generation: w.checkpoint_generation.unwrap_or(0),
            started_at: w.started_at,
            last_checkpointed_at: w.last_checkpointed_at,
            created_at: w.created_at,
            updated_at: w.updated_at,
        }
    }
}

/// One page of list results plus the pagination envelope.
#[derive(Debug, Clone, Serialize)]
pub struct SailboxPage {
    /// The sailboxes in this page.
    pub items: Vec<SailboxInfo>,
    /// Maximum number of items requested for this page.
    pub limit: i64,
    /// Zero-based offset of the first item in this page.
    pub offset: i64,
    /// Total number of sailboxes matching the query across all pages.
    pub total: i64,
    /// True when further pages exist beyond this one.
    pub has_more: bool,
}

/// A durable checkpoint handle. `status` echoes the source sailbox's lifecycle
/// status after checkpointing (a running box is snapshotted; a paused/sleeping
/// one returns its existing checkpoint), so the binding can sync its handle.
#[derive(Debug, Clone, Serialize)]
pub struct SailboxCheckpoint {
    /// Stable identifier of the checkpoint.
    pub checkpoint_id: String,
    /// Identifier of the sailbox the checkpoint was taken from.
    pub sailbox_id: String,
    /// Checkpoint generation counter captured by this checkpoint.
    pub checkpoint_generation: i64,
    /// Source sailbox's lifecycle status after checkpointing.
    pub status: String,
}

/// Filters for list/list_page.
#[derive(Debug, Clone)]
pub struct ListQuery {
    /// Restrict results to sailboxes owned by this app (id or name).
    pub app: Option<String>,
    /// Restrict results to sailboxes in this lifecycle status.
    pub status: Option<String>,
    /// Free-text search filter applied by the backend.
    pub search: Option<String>,
    /// Exclude sailboxes whose guest schema version exceeds this value.
    pub max_guest_schema_version: Option<i64>,
    /// Maximum number of items to return.
    pub limit: i64,
    /// Zero-based offset of the first item to return.
    pub offset: i64,
}

/// Default page size for listing, matching the sailbox API's own default. The
/// API rejects `limit=0`, so the derived all-zero default cannot be used.
pub const DEFAULT_LIST_LIMIT: i64 = 50;

/// Resource-limit bounds enforced at create time. These mirror the scheduler's
/// authoritative limits so every binding (CLI, Python, future SDKs) fails fast
/// with the same error instead of round-tripping to a backend rejection.
pub const MAX_SAILBOX_VCPUS: i64 = 4;
/// Minimum requested memory in MiB (0 is allowed and means "use the default").
pub const MIN_SAILBOX_MEMORY_MIB: i64 = 1024;
/// Maximum requested memory in MiB.
pub const MAX_SAILBOX_MEMORY_MIB: i64 = 8192;
/// Maximum requested state-disk size in GiB.
pub const MAX_SAILBOX_DISK_GIB: i64 = 32;

impl Default for ListQuery {
    fn default() -> ListQuery {
        ListQuery {
            app: None,
            status: None,
            search: None,
            max_guest_schema_version: None,
            limit: DEFAULT_LIST_LIMIT,
            offset: 0,
        }
    }
}

/// One guest ingress port to reserve at create time.
#[derive(Debug, Clone, Serialize)]
pub struct IngressPort {
    /// Port inside the guest to expose for ingress.
    pub guest_port: u32,
    /// Transport protocol for the port (for example `tcp`).
    pub protocol: String,
    /// Source addresses allowed to reach the port; empty means no restriction.
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub allowlist: Vec<String>,
}

/// One NFS volume mount.
#[derive(Debug, Clone, Serialize)]
pub struct VolumeMount {
    /// Identifier of the NFS volume to mount.
    pub volume_id: String,
    /// Absolute path inside the guest where the volume is mounted.
    pub mount_path: String,
}

/// A managed NFS volume as returned by the sailbox-volume API. (The local
/// mount path is a binding-side concern and not part of this wire type.)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NfsVolume {
    /// Stable identifier of the volume.
    pub volume_id: String,
    /// Caller-supplied volume name.
    #[serde(default)]
    pub name: String,
    /// Storage backend serving the volume.
    #[serde(default)]
    pub backend: String,
    /// Lifecycle status of the volume.
    #[serde(default)]
    pub status: String,
    /// RFC 3339 timestamp of when the volume was created, if reported.
    #[serde(default)]
    pub created_at: Option<String>,
    /// RFC 3339 timestamp of the volume's last update, if reported.
    #[serde(default)]
    pub updated_at: Option<String>,
}

/// Inputs to create a sailbox. The image is a binding-built spec passed through
/// opaquely (the image DSL lives in the language wrapper), while the lifecycle
/// envelope is owned by the core.
#[derive(Debug, Clone)]
pub struct CreateSailboxRequest {
    /// Identifier of the app that will own the sailbox.
    pub app_id: String,
    /// Caller-supplied name for the sailbox.
    pub name: String,
    /// Guest ingress ports to reserve at create time.
    pub ingress_ports: Vec<IngressPort>,
    /// NFS volumes to mount into the guest.
    pub volume_mounts: Vec<VolumeMount>,
    /// The image spec as a JSON object built by the binding's image DSL.
    pub image: serde_json::Value,
    /// Requested CPU allocation in vCPUs; defaults server-side when absent.
    pub cpu: Option<i64>,
    /// Requested memory size in mebibytes; defaults server-side when absent.
    pub memory_mib: Option<i64>,
    /// Requested state-disk size in gibibytes; defaults server-side when absent.
    pub state_disk_size_gib: Option<i64>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn list_query_default_uses_a_valid_nonzero_limit() {
        // The sailbox API rejects limit=0, so the default must be the API's own
        // page size, not the derived zero.
        let query = ListQuery::default();
        assert_eq!(query.limit, DEFAULT_LIST_LIMIT);
        assert!(query.limit > 0);
        assert_eq!(query.offset, 0);
    }
}