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