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