Skip to main content

arcbox_computer_runtime/sandbox/
types.rs

1use super::*;
2use serde::{Deserialize, Serialize};
3
4pub type ComputerId = String;
5
6/// A sandbox's network as the boot flow carries it: the lease the guest
7/// network reserved, the NIC it returned when it activated that lease, and
8/// the addressing the guest is told to use over it.
9///
10/// The three travel together because all of them are needed at once and
11/// none derives the others — the crash journal records the lease, the VM
12/// spec boots the NIC, the kernel command line carries the identity, and
13/// only the network knows which interface it built for which address or
14/// what a guest on it sees.
15#[derive(Clone)]
16pub struct NetworkAttachment {
17    /// The reserved address, and the cleanup token its generation ends on.
18    pub lease: NetworkLease,
19    /// The interface the driver attaches the guest's `eth0` to.
20    pub nic: NicSpec,
21    /// What the guest is told over that interface, from
22    /// [`GuestNetwork::identity`](arcbox_vm_driver::net::GuestNetwork::identity)
23    /// under the mode it was activated in.
24    pub identity: NetworkIdentity,
25    /// Whether this boot bakes the fixed invariant identity (CORE-81) into
26    /// the guest's command line, which it does not when the caller supplied
27    /// their own `ip=`.
28    ///
29    /// Carried beside the mode rather than confused with it: the host side
30    /// of a cold boot is *always* activated
31    /// [`AttachMode::Invariant`](arcbox_vm_driver::net::AttachMode::Invariant),
32    /// so this flag says something about the guest, not about the datapath.
33    pub invariant_identity: bool,
34}
35
36impl NetworkAttachment {
37    /// This attachment as the crash journal records it.
38    pub(crate) fn journaled(&self) -> super::reconcile::JournaledLease<'_> {
39        super::reconcile::JournaledLease::cold_boot(&self.lease, self.invariant_identity)
40    }
41}
42
43// State
44
45/// Lifecycle state of a sandbox.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ComputerState {
48    /// VMM prepared; VM still booting.
49    Starting,
50    /// VM booted and ready to accept workloads (or last workload exited).
51    Ready,
52    /// A workload (cmd / Run) is currently executing inside the VM.
53    Running,
54    /// `Stop` called; draining workload and shutting down VM.
55    Stopping,
56    /// VM has shut down cleanly.
57    Stopped,
58    /// Unrecoverable error occurred.
59    Failed,
60    /// `Pause` called; checkpointing state, then releasing the VM.
61    Pausing,
62    /// Checkpointed to disk with runtime resources released; the record,
63    /// checkpoint, and disk overlay survive under the same id until
64    /// `Resume` or `Remove` (CORE-21).
65    Paused,
66}
67
68impl std::fmt::Display for ComputerState {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        match self {
71            Self::Starting => write!(f, "starting"),
72            Self::Ready => write!(f, "ready"),
73            Self::Running => write!(f, "running"),
74            Self::Stopping => write!(f, "stopping"),
75            Self::Stopped => write!(f, "stopped"),
76            Self::Failed => write!(f, "failed"),
77            Self::Pausing => write!(f, "pausing"),
78            Self::Paused => write!(f, "paused"),
79        }
80    }
81}
82
83// Spec types (input to ComputerManager methods)
84
85/// What happens when a sandbox's idle timeout expires (CORE-21).
86///
87/// Mirrors `arcbox.sandbox.v1.IdleAction`; `UNSPECIFIED` resolves to the
88/// daemon default ([`IdleAction::Kill`]) at the service boundary, so this
89/// type only carries effective policies.
90#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "snake_case")]
92pub enum IdleAction {
93    /// Destroy the sandbox and release all resources (Remove semantics).
94    #[default]
95    Kill,
96    /// Checkpoint to disk under the same id and release the VM.
97    Pause,
98}
99
100/// A partial lifecycle update applied by `SetLifecycle` (CORE-60).
101///
102/// `None` fields are left unchanged, so each knob can be adjusted
103/// independently.
104#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
105pub struct LifecycleUpdate {
106    /// Replace the hard maximum lifetime: expire this many seconds from
107    /// now (`Some(0)` removes the limit).
108    pub ttl_seconds: Option<u32>,
109    /// Replace the idle timeout (`Some(0)` disables idle detection).
110    pub idle_timeout_seconds: Option<u32>,
111    /// Replace the idle policy.
112    pub on_idle: Option<IdleAction>,
113}
114
115/// Network configuration supplied at sandbox creation time.
116#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
117pub struct ComputerNetworkSpec {
118    /// `"tap"` (default) or `"none"`.
119    pub mode: String,
120}
121
122/// A single bind-mount into the sandbox.
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct ComputerMountSpec {
125    pub source: String,
126    pub target: String,
127    pub readonly: bool,
128}
129
130/// Full sandbox creation parameters.
131///
132/// The initial workload fields (`cmd`, `env`, `working_dir`, `user`) are
133/// consumed by the boot task: a non-empty `cmd` is launched automatically
134/// once the sandbox is ready, through the same path as `Run`.
135/// `mounts`, `image`, and `ssh_public_key` are validated at the service
136/// boundary (see the guest agent's `SandboxService::create`).
137#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(default)]
139pub struct ComputerSpec {
140    /// Caller-supplied ID; auto-generated (UUID) when `None` or empty.
141    pub id: Option<String>,
142    /// Arbitrary key-value metadata (filtering, listing).
143    pub labels: HashMap<String, String>,
144    /// Kernel image path (empty = daemon default).
145    pub kernel: String,
146    /// Root filesystem image path (empty = daemon default).
147    pub rootfs: String,
148    /// Kernel command-line arguments (empty = daemon default).
149    pub boot_args: String,
150    /// Number of vCPUs (0 = daemon default).
151    pub vcpus: u32,
152    /// Memory in MiB (0 = daemon default).
153    pub memory_mib: u64,
154    /// Initial command launched automatically after boot (empty = none).
155    pub cmd: Vec<String>,
156    /// Environment variables for the initial command.
157    pub env: HashMap<String, String>,
158    /// Working directory for the initial command.
159    pub working_dir: String,
160    /// User to run the initial command as.
161    pub user: String,
162    /// Bind mounts into the sandbox.
163    pub mounts: Vec<ComputerMountSpec>,
164    /// Network configuration.
165    pub network: ComputerNetworkSpec,
166    /// Auto-destroy TTL in seconds (0 = no limit).
167    pub ttl_seconds: u32,
168    /// SSH public key injected via MMDS (None = no SSH setup).
169    pub ssh_public_key: Option<String>,
170    /// Apply [`Self::on_idle`] after this many seconds without a running
171    /// execution (0 = no idle detection). Re-armed on every `Ready` edge;
172    /// file activity does NOT re-arm (CORE-21).
173    pub idle_timeout_seconds: u32,
174    /// What to do when the idle timeout expires.
175    pub on_idle: IdleAction,
176    /// The catalog template's pre-warmed snapshot, when the create resolved
177    /// one (CORE-107). A boot-recipe input like `rootfs`: the create path
178    /// restores it instead of cold-booting when eligible, and it rides the
179    /// journaled effective spec so crash replay keeps working. `None` for
180    /// non-catalog templates and warm-less catalog entries.
181    pub template_warm: Option<TemplateWarmRef>,
182    /// The catalog template's readiness probe (CORE-107): READY is withheld
183    /// until it passes; expiry fails the boot. `None` = ready on
184    /// exec-acceptance. Filled from the resolved template, never the request.
185    pub ready_probe: Option<crate::template_catalog::ReadyProbeSpec>,
186}
187
188/// A template's pre-warmed boot-to-ready snapshot, threaded through
189/// [`ComputerSpec`] (CORE-107).
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191pub struct TemplateWarmRef {
192    /// Snapshot id in the snapshot catalog.
193    pub snapshot_id: String,
194    /// Capture-time geometry; restore is eligible only on an exact match.
195    pub vcpus: u32,
196    pub memory_mib: u64,
197}
198
199/// Parameters to restore a sandbox from a checkpoint.
200#[derive(Debug, Clone, Default)]
201pub struct RestoreComputerSpec {
202    /// Caller-supplied ID (None = auto-generate).
203    pub id: Option<String>,
204    /// Source checkpoint/snapshot ID.
205    pub snapshot_id: String,
206    /// Labels to assign to the restored sandbox.
207    pub labels: HashMap<String, String>,
208    /// Assign a fresh TAP + IP to the restored sandbox.
209    pub network_override: bool,
210    /// Auto-destroy TTL in seconds (0 = no limit).
211    pub ttl_seconds: u32,
212}
213
214// Public output types (returned to callers / gRPC layer)
215
216/// Lightweight summary for `List` operations.
217pub struct ComputerSummary {
218    pub id: ComputerId,
219    pub state: ComputerState,
220    pub labels: HashMap<String, String>,
221    /// Allocated IP address (empty when network mode is `"none"`).
222    pub ip_address: String,
223    pub created_at: DateTime<Utc>,
224    /// When the sandbox reached `Paused` (None otherwise).
225    pub paused_at: Option<DateTime<Utc>>,
226    /// On-disk footprint of retained state, in every lifecycle state: the
227    /// COW disk overlay while running, plus the pause checkpoint while
228    /// paused (CORE-146). Copy-mode sandboxes (no overlay) report 0 until
229    /// paused parks their rootfs copy.
230    pub storage_bytes: u64,
231}
232
233/// Detailed sandbox state for `Inspect`.
234pub struct ComputerInfo {
235    pub id: ComputerId,
236    pub state: ComputerState,
237    pub labels: HashMap<String, String>,
238    pub vcpus: u32,
239    pub memory_mib: u64,
240    pub network: Option<ComputerNetworkInfo>,
241    pub created_at: DateTime<Utc>,
242    pub ready_at: Option<DateTime<Utc>>,
243    pub last_exited_at: Option<DateTime<Utc>>,
244    pub last_exit_status: Option<ExitStatus>,
245    pub error: Option<String>,
246    /// When the sandbox reached `Paused` (None otherwise).
247    pub paused_at: Option<DateTime<Utc>>,
248    /// On-disk footprint of retained state, in every lifecycle state: the
249    /// COW disk overlay while running, plus the pause checkpoint while
250    /// paused (CORE-146). Copy-mode sandboxes (no overlay) report 0 until
251    /// paused parks their rootfs copy.
252    pub storage_bytes: u64,
253    /// When the hard maximum lifetime fires (None = no limit).
254    pub ttl_deadline: Option<DateTime<Utc>>,
255    /// Idle timeout in seconds (0 = no idle detection).
256    pub idle_timeout_seconds: u32,
257    /// Action applied when the idle timeout expires.
258    pub on_idle: IdleAction,
259}
260
261/// Network details within `ComputerInfo`.
262pub struct ComputerNetworkInfo {
263    pub ip_address: String,
264    pub gateway: String,
265}
266
267// Events
268
269/// The `action` values a [`ComputerEvent`] carries, in lifecycle order.
270///
271/// `action` stays a `String` on the event (it crosses the API as one), but
272/// every emit site and match in this crate goes through these constants, so
273/// renaming or adding an action is a change here — not a grep for string
274/// literals whose miss surfaces as silently skipped teardown handling.
275pub mod action {
276    pub const CREATED: &str = "created";
277    pub const READY: &str = "ready";
278    pub const RUNNING: &str = "running";
279    pub const IDLE: &str = "idle";
280    pub const STOPPING: &str = "stopping";
281    pub const STOPPED: &str = "stopped";
282    pub const FAILED: &str = "failed";
283    pub const REMOVED: &str = "removed";
284    pub const PAUSING: &str = "pausing";
285    pub const PAUSED: &str = "paused";
286    pub const RESUMED: &str = "resumed";
287}
288
289/// A sandbox lifecycle event broadcast to subscribers.
290#[derive(Debug, Clone)]
291pub struct ComputerEvent {
292    pub sandbox_id: ComputerId,
293    /// One of the [`action`] constants.
294    pub action: String,
295    /// Unix nanoseconds.
296    pub timestamp_ns: i64,
297    /// Extra context (e.g. `"exit_code"` on `"idle"`, `"error"` on `"failed"`).
298    pub attributes: HashMap<String, String>,
299    /// Monotonic sequence number, 1-based, global across all sandboxes of
300    /// one manager and stamped in the order subscribers receive events —
301    /// so a subscriber whose received sequences are not contiguous knows
302    /// it missed events (CORE-147). That gap test is conclusive only on
303    /// this unfiltered subscription: the stamp precedes any downstream
304    /// filtering, so a filtered view (the wire API's per-sandbox or
305    /// per-kind subscription) sees legitimate gaps for events its filter
306    /// dropped. Not persisted: a new manager numbers from 1 again. `0`
307    /// only on an event that never went through the bus (or, on the
308    /// wire, one from a daemon predating sequencing). The delivery
309    /// contract this makes checkable is on
310    /// [`ComputerManager::subscribe_events`](crate::ComputerManager::subscribe_events).
311    pub sequence: u64,
312}
313
314impl ComputerEvent {
315    pub fn new(sandbox_id: &str, action: &str) -> Self {
316        Self {
317            sandbox_id: sandbox_id.to_owned(),
318            action: action.to_owned(),
319            timestamp_ns: Utc::now().timestamp_nanos_opt().unwrap_or(0),
320            attributes: HashMap::new(),
321            sequence: 0,
322        }
323    }
324
325    pub fn with_attr(mut self, key: &str, value: &str) -> Self {
326        self.attributes.insert(key.to_owned(), value.to_owned());
327        self
328    }
329
330    /// Whether this event marks the sandbox's teardown — nothing can run in
331    /// it afterwards. A new terminal action must be added here, or torn-down
332    /// sandboxes silently stop purging their executions.
333    #[must_use]
334    pub fn is_terminal(&self) -> bool {
335        matches!(
336            self.action.as_str(),
337            action::STOPPED | action::FAILED | action::REMOVED
338        )
339    }
340}
341
342// Checkpoint / Restore output types
343
344/// Info returned after a successful checkpoint.
345#[derive(Debug, Clone)]
346pub struct CheckpointInfo {
347    pub snapshot_id: String,
348    pub snapshot_dir: String,
349    pub created_at: String,
350}
351
352/// Lightweight checkpoint summary for `ListSnapshots`.
353pub struct CheckpointSummary {
354    pub id: String,
355    /// ID of the sandbox that was checkpointed.
356    pub sandbox_id: String,
357    pub name: String,
358    pub labels: HashMap<String, String>,
359    pub snapshot_dir: String,
360    pub created_at: String,
361}
362
363// ComputerManager