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 cpus: Option<u8>,
30
31    /// Desired boot-time maximum possible vCPU count.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub max_cpus: 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#[serde(tag = "kind", rename_all = "snake_case")]
138pub enum SecretSource {
139    /// Read the value from a host environment variable at apply time.
140    Env {
141        /// Host environment variable name.
142        var: String,
143    },
144
145    /// Read the value from a host-side secret store reference.
146    Store {
147        /// Store-specific secret reference.
148        reference: String,
149    },
150}
151
152/// Serializable dry-run or apply plan for a sandbox modification.
153#[derive(Debug, Clone, Serialize, Deserialize)]
154#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
155pub struct SandboxModificationPlan {
156    /// Sandbox being modified.
157    pub sandbox: String,
158
159    /// Sandbox status used for classification.
160    pub status: String,
161
162    /// Whether the changes were applied.
163    pub applied: bool,
164
165    /// Modification policy used to produce the plan.
166    pub policy: ModificationPolicy,
167
168    /// Planned changes.
169    pub changes: Vec<PlannedChange>,
170
171    /// Conflicts that must be resolved before the patch can apply.
172    pub conflicts: Vec<ModificationConflict>,
173
174    /// Non-fatal warnings about the patch or current runtime capabilities.
175    pub warnings: Vec<ModificationWarning>,
176
177    /// Live resource resize outcomes, populated by apply when a live change ran.
178    #[serde(default, skip_serializing_if = "Vec::is_empty")]
179    pub resize_status: Vec<ResourceResizeStatus>,
180}
181
182/// One planned modification entry.
183#[derive(Debug, Clone, Serialize, Deserialize)]
184#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
185#[serde(tag = "kind", rename_all = "snake_case")]
186pub enum PlannedChange {
187    /// Ordinary config change.
188    Config(ConfigPlannedChange),
189
190    /// Secret change. Values are omitted by construction.
191    Secret(SecretPlannedChange),
192}
193
194/// Planned config change.
195#[derive(Debug, Clone, Serialize, Deserialize)]
196#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
197pub struct ConfigPlannedChange {
198    /// Config field being changed.
199    pub field: String,
200
201    /// Natural change type for table rendering.
202    pub change: ChangeKind,
203
204    /// Previous safe visible state.
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub before: Option<String>,
207
208    /// New safe visible state.
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub after: Option<String>,
211
212    /// When or whether the change can take effect.
213    pub disposition: ModificationDisposition,
214
215    /// Human-readable reason for this classification, when useful.
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub reason: Option<String>,
218}
219
220/// Planned secret change.
221#[derive(Debug, Clone, Serialize, Deserialize)]
222#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
223pub struct SecretPlannedChange {
224    /// Table field name. This is always `secret`.
225    pub field: String,
226
227    /// Stable secret identity, usually the environment variable name.
228    pub name: String,
229
230    /// Natural change type for table rendering.
231    pub change: SecretChangeKind,
232
233    /// Previous guest-visible reference or placeholder.
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub before_ref: Option<String>,
236
237    /// New guest-visible reference or placeholder.
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub after_ref: Option<String>,
240
241    /// When or whether the change can take effect.
242    pub disposition: ModificationDisposition,
243
244    /// Allowed hosts after the requested change.
245    #[serde(default, skip_serializing_if = "Vec::is_empty")]
246    pub allow_hosts: Vec<String>,
247
248    /// Human-readable reason for this classification, when useful.
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub reason: Option<String>,
251}
252
253/// Natural config change type for human output.
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
255#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
256#[serde(rename_all = "lowercase")]
257pub enum ChangeKind {
258    /// A field is being added.
259    Added,
260
261    /// A field is being updated.
262    Updated,
263
264    /// A field is being removed.
265    Removed,
266}
267
268/// Natural secret change type for human output.
269#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
270#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
271pub enum SecretChangeKind {
272    /// A secret placeholder is being added.
273    #[serde(rename = "added")]
274    Added,
275
276    /// A secret value is being rotated.
277    #[serde(rename = "rotated")]
278    Rotated,
279
280    /// A secret is being removed.
281    #[serde(rename = "removed")]
282    Removed,
283
284    /// A secret is being renamed.
285    #[serde(rename = "renamed")]
286    Renamed,
287
288    /// Allowed hosts are being updated.
289    #[serde(rename = "hosts updated")]
290    HostsUpdated,
291
292    /// The guest-visible placeholder is being updated.
293    #[serde(rename = "placeholder updated")]
294    PlaceholderUpdated,
295}
296
297/// When or whether a planned change can take effect.
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
299#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
300pub enum ModificationDisposition {
301    /// Applies to the running VM now.
302    #[serde(rename = "live")]
303    Live,
304
305    /// Persists to desired config and applies the next time the sandbox starts.
306    #[serde(rename = "next start")]
307    NextStart,
308
309    /// Needs a restart before it can take effect.
310    #[serde(rename = "requires restart")]
311    RequiresRestart,
312
313    /// Cannot be changed by `modify`.
314    #[serde(rename = "unsupported")]
315    Unsupported,
316}
317
318/// Conflict that blocks applying a modification.
319#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
320#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
321pub struct ModificationConflict {
322    /// Field with the conflict.
323    pub field: String,
324
325    /// Human-readable conflict description.
326    pub message: String,
327}
328
329/// Warning emitted while planning a modification.
330#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
331#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
332pub struct ModificationWarning {
333    /// Field associated with the warning.
334    pub field: String,
335
336    /// Human-readable warning description.
337    pub message: String,
338}
339
340/// Resource kind used by live resize convergence reporting.
341#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
342#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
343#[serde(rename_all = "snake_case")]
344pub enum ResourceKind {
345    /// vCPU count.
346    Cpus,
347
348    /// Guest memory.
349    Memory,
350}
351
352/// Runtime convergence state for an accepted resource resize.
353#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
354#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
355#[serde(rename_all = "kebab-case")]
356pub enum ResourceConvergenceState {
357    /// The runtime accepted the request.
358    Accepted,
359
360    /// The guest and VMM are still converging on the requested state.
361    Converging,
362
363    /// Desired, actual, and enforced state match.
364    Applied,
365
366    /// The guest refused or failed to cooperate.
367    GuestRefused,
368
369    /// The resize failed.
370    Failed,
371}
372
373/// Status for a live resource resize.
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
375#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
376pub struct ResourceResizeStatus {
377    /// Resource being resized.
378    pub resource: ResourceKind,
379
380    /// Requested value.
381    pub requested: String,
382
383    /// Actual value observed in the guest/runtime.
384    pub actual: String,
385
386    /// Host/VMM-enforced value.
387    pub enforced: String,
388
389    /// Convergence state.
390    pub state: ResourceConvergenceState,
391}
392
393//--------------------------------------------------------------------------------------------------
394// Trait Implementations
395//--------------------------------------------------------------------------------------------------
396
397impl std::fmt::Debug for SecretModificationPatch {
398    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399        f.debug_struct("SecretModificationPatch")
400            .field("name", &self.name)
401            .field("source", &self.source)
402            .field("value", &"[REDACTED]")
403            .field("placeholder", &self.placeholder)
404            .field("allowed_hosts", &self.allowed_hosts)
405            .finish()
406    }
407}