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}