Skip to main content

microsandbox_types/
modify.rs

1//! Sandbox modification contract shared by the SDKs, the CLI, and future backends.
2//!
3//! These are the serializable request/response types behind `sandbox.modify()`:
4//! the patch a caller submits, and the plan that classifies each change. The
5//! builder and classification logic live in the SDK; this module owns only the
6//! wire-shaped data so any backend (local today, cloud later) and any language
7//! binding can speak the same contract.
8
9use serde::{Deserialize, Serialize};
10use zeroize::Zeroizing;
11
12use crate::domain::EnvVar;
13
14//--------------------------------------------------------------------------------------------------
15// Types
16//--------------------------------------------------------------------------------------------------
17
18/// A requested sandbox modification.
19///
20/// This type is serializable so SDKs and the CLI can share one canonical
21/// contract. The only field that may carry raw secret material is the
22/// per-secret `value` inside [`SecretModificationPatch`]; plans derived from
23/// a patch are always value-free.
24#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
26pub struct SandboxModificationPatch {
27    /// Desired effective vCPU count.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub vcpus: Option<u8>,
30
31    /// Desired boot-time maximum possible vCPU count.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub max_vcpus: Option<u8>,
34
35    /// Desired effective guest memory in MiB.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub memory_mib: Option<u32>,
38
39    /// Desired boot-time maximum hotpluggable memory in MiB.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub max_memory_mib: Option<u32>,
42
43    /// Desired OCI writable overlay upper size in MiB. Grow-only: the upper is a real ext4 image, so shrinking risks data loss and is rejected in v1.
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub oci_upper_size_mib: Option<u32>,
46
47    /// Environment variables to set for future execs.
48    #[serde(default, skip_serializing_if = "Vec::is_empty")]
49    pub env: Vec<EnvVar>,
50
51    /// Environment variable keys to remove.
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub env_remove: Vec<String>,
54
55    /// Labels to set.
56    #[serde(default, skip_serializing_if = "Vec::is_empty")]
57    pub labels: Vec<(String, String)>,
58
59    /// Label keys to remove.
60    #[serde(default, skip_serializing_if = "Vec::is_empty")]
61    pub labels_remove: Vec<String>,
62
63    /// Desired working directory for future execs.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub workdir: Option<String>,
66
67    /// Desired secret specs, keyed by secret name. The planner diffs each
68    /// spec against the existing config to infer what changes.
69    #[serde(default, skip_serializing_if = "Vec::is_empty")]
70    pub secrets: Vec<SecretModificationPatch>,
71
72    /// Secret names to remove. Removal is explicit: absence of a name from
73    /// `secrets` never means removal.
74    #[serde(default, skip_serializing_if = "Vec::is_empty")]
75    pub secrets_remove: Vec<String>,
76}
77
78/// Policy selected for applying or planning a modification.
79#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
80#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
81#[serde(rename_all = "snake_case")]
82pub enum ModificationPolicy {
83    /// Apply only changes that can complete without restarting the running sandbox.
84    #[default]
85    NoRestart,
86
87    /// Persist the desired config for the next start and leave any running VM unchanged.
88    NextStart,
89
90    /// Persist the patch and restart the sandbox if restart-required changes are present.
91    Restart,
92}
93
94/// A desired secret spec inside a modification patch.
95///
96/// The spec is declarative: it states the target state for one secret (source
97/// or value, placeholder, allowed hosts) and the planner infers the concrete
98/// change — added, rotated, hosts updated, placeholder updated — by diffing
99/// the spec against the existing config. Removal is explicit through
100/// [`SandboxModificationPatch::secrets_remove`].
101///
102/// Only `value` may carry secret material, and only in-process: it is
103/// [`Zeroizing`]-wrapped, redacted from `Debug` output, skipped by serde when
104/// empty, and never copied into the plan.
105#[derive(Clone, Default, Serialize, Deserialize)]
106#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
107pub struct SecretModificationPatch {
108    /// Stable secret identity, usually the environment variable name.
109    pub name: String,
110
111    /// Host-side source reference to resolve the value from. Mutually
112    /// exclusive with `value`.
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub source: Option<SecretSource>,
115
116    /// Raw secret value supplied by the caller, for embedders that hold only
117    /// a value (e.g. from their own vault). Mutually exclusive with `source`.
118    /// A value-based apply persists the value into the durable config until a
119    /// later source-based rotate migrates the entry to a reference.
120    #[serde(default, skip_serializing_if = "String::is_empty")]
121    #[cfg_attr(feature = "ts", ts(type = "string"))]
122    pub value: Zeroizing<String>,
123
124    /// Guest-visible placeholder/reference, if explicitly requested.
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub placeholder: Option<String>,
127
128    /// Desired allowed host patterns. Empty means "leave unchanged" for an
129    /// existing secret; a new secret needs at least one.
130    #[serde(default, skip_serializing_if = "Vec::is_empty")]
131    pub allowed_hosts: Vec<String>,
132}
133
134/// Host-side source for secret material.
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
137#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
138#[serde(tag = "kind", rename_all = "snake_case")]
139pub enum SecretSource {
140    /// Read the value from a host environment variable at apply time.
141    Env {
142        /// Host environment variable name.
143        var: String,
144    },
145
146    /// Read the value from a host-side secret store reference.
147    Store {
148        /// Store-specific secret reference.
149        reference: String,
150    },
151}
152
153/// Serializable dry-run or apply plan for a sandbox modification.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
156pub struct SandboxModificationPlan {
157    /// Sandbox being modified.
158    pub sandbox: String,
159
160    /// Sandbox status used for classification.
161    pub status: String,
162
163    /// Whether the changes were applied.
164    pub applied: bool,
165
166    /// Modification policy used to produce the plan.
167    pub policy: ModificationPolicy,
168
169    /// Planned changes.
170    pub changes: Vec<PlannedChange>,
171
172    /// Conflicts that must be resolved before the patch can apply.
173    pub conflicts: Vec<ModificationConflict>,
174
175    /// Non-fatal warnings about the patch or current runtime capabilities.
176    pub warnings: Vec<ModificationWarning>,
177
178    /// Live resource resize outcomes, populated by apply when a live change ran.
179    #[serde(default, skip_serializing_if = "Vec::is_empty")]
180    pub resize_status: Vec<ResourceResizeStatus>,
181}
182
183/// One planned modification entry.
184#[derive(Debug, Clone, Serialize, Deserialize)]
185#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
186#[serde(tag = "kind", rename_all = "snake_case")]
187pub enum PlannedChange {
188    /// Ordinary config change.
189    Config(ConfigPlannedChange),
190
191    /// Secret change. Values are omitted by construction.
192    Secret(SecretPlannedChange),
193}
194
195/// Planned config change.
196#[derive(Debug, Clone, Serialize, Deserialize)]
197#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
198pub struct ConfigPlannedChange {
199    /// Config field being changed.
200    pub field: String,
201
202    /// Natural change type for table rendering.
203    pub change: ChangeKind,
204
205    /// Previous safe visible state.
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub before: Option<String>,
208
209    /// New safe visible state.
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub after: Option<String>,
212
213    /// When or whether the change can take effect.
214    pub disposition: ModificationDisposition,
215
216    /// Human-readable reason for this classification, when useful.
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub reason: Option<String>,
219}
220
221/// Planned secret change.
222#[derive(Debug, Clone, Serialize, Deserialize)]
223#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
224pub struct SecretPlannedChange {
225    /// Table field name. This is always `secret`.
226    pub field: String,
227
228    /// Stable secret identity, usually the environment variable name.
229    pub name: String,
230
231    /// Natural change type for table rendering.
232    pub change: SecretChangeKind,
233
234    /// Previous guest-visible reference or placeholder.
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub before_ref: Option<String>,
237
238    /// New guest-visible reference or placeholder.
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub after_ref: Option<String>,
241
242    /// When or whether the change can take effect.
243    pub disposition: ModificationDisposition,
244
245    /// Allowed hosts after the requested change.
246    #[serde(default, skip_serializing_if = "Vec::is_empty")]
247    pub allow_hosts: Vec<String>,
248
249    /// Human-readable reason for this classification, when useful.
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub reason: Option<String>,
252}
253
254/// Natural config change type for human output.
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
256#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
257#[serde(rename_all = "lowercase")]
258pub enum ChangeKind {
259    /// A field is being added.
260    Added,
261
262    /// A field is being updated.
263    Updated,
264
265    /// A field is being removed.
266    Removed,
267}
268
269/// Natural secret change type for human output.
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
271#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
272pub enum SecretChangeKind {
273    /// A secret placeholder is being added.
274    #[serde(rename = "added")]
275    Added,
276
277    /// A secret value is being rotated.
278    #[serde(rename = "rotated")]
279    Rotated,
280
281    /// A secret is being removed.
282    #[serde(rename = "removed")]
283    Removed,
284
285    /// A secret is being renamed.
286    #[serde(rename = "renamed")]
287    Renamed,
288
289    /// Allowed hosts are being updated.
290    #[serde(rename = "hosts updated")]
291    HostsUpdated,
292
293    /// The guest-visible placeholder is being updated.
294    #[serde(rename = "placeholder updated")]
295    PlaceholderUpdated,
296}
297
298/// When or whether a planned change can take effect.
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
300#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
301pub enum ModificationDisposition {
302    /// Applies to the running VM now.
303    #[serde(rename = "live")]
304    Live,
305
306    /// Persists to desired config and applies the next time the sandbox starts.
307    #[serde(rename = "next start")]
308    NextStart,
309
310    /// Needs a restart before it can take effect.
311    #[serde(rename = "requires restart")]
312    RequiresRestart,
313
314    /// Cannot be changed by `modify`.
315    #[serde(rename = "unsupported")]
316    Unsupported,
317}
318
319/// Conflict that blocks applying a modification.
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
321#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
322pub struct ModificationConflict {
323    /// Field with the conflict.
324    pub field: String,
325
326    /// Human-readable conflict description.
327    pub message: String,
328}
329
330/// Warning emitted while planning a modification.
331#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
332#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
333pub struct ModificationWarning {
334    /// Field associated with the warning.
335    pub field: String,
336
337    /// Human-readable warning description.
338    pub message: String,
339}
340
341/// Resource kind used by live resize convergence reporting.
342#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
343#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
344#[serde(rename_all = "snake_case")]
345pub enum ResourceKind {
346    /// vCPU count.
347    Cpus,
348
349    /// Guest memory.
350    Memory,
351}
352
353/// Runtime convergence state for an accepted resource resize.
354#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
355#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
356#[serde(rename_all = "kebab-case")]
357pub enum ResourceConvergenceState {
358    /// The runtime accepted the request.
359    Accepted,
360
361    /// The guest and VMM are still converging on the requested state.
362    Converging,
363
364    /// Desired, actual, and enforced state match.
365    Applied,
366
367    /// The guest refused or failed to cooperate.
368    GuestRefused,
369
370    /// The resize failed.
371    Failed,
372}
373
374/// Status for a live resource resize.
375#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
376#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
377pub struct ResourceResizeStatus {
378    /// Resource being resized.
379    pub resource: ResourceKind,
380
381    /// Requested value.
382    pub requested: String,
383
384    /// Actual value observed in the guest/runtime.
385    pub actual: String,
386
387    /// Host/VMM-enforced value.
388    pub enforced: String,
389
390    /// Convergence state.
391    pub state: ResourceConvergenceState,
392}
393
394//--------------------------------------------------------------------------------------------------
395// Trait Implementations
396//--------------------------------------------------------------------------------------------------
397
398impl std::fmt::Debug for SecretModificationPatch {
399    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
400        f.debug_struct("SecretModificationPatch")
401            .field("name", &self.name)
402            .field("source", &self.source)
403            .field("value", &"[REDACTED]")
404            .field("placeholder", &self.placeholder)
405            .field("allowed_hosts", &self.allowed_hosts)
406            .finish()
407    }
408}