Skip to main content

solari/
types.rs

1//! Wire types for the Solari SDK core surface. serde field names match the JSON
2//! wire EXACTLY (camelCase where the gateway/control-WS is camelCase).
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::HashMap;
7
8// ---------------------------------------------------------------------------
9// REST — session lifecycle
10// ---------------------------------------------------------------------------
11
12/// Idle lifecycle policy (pause/kill on timeout; optional auto-resume).
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct Lifecycle {
16    pub on_timeout: String,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub auto_resume: Option<bool>,
19    /// Auto-park (deschedule) the guest after this many milliseconds with no
20    /// activity. Parking freezes vCPUs but keeps the slot, ports and control
21    /// channel resident, so the next request wakes it in milliseconds. Floored
22    /// by the gateway and capped at the session's idle window.
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub park_after_ms: Option<u64>,
25    /// Bounded park: the most a session may stay parked before the gateway
26    /// wakes it unconditionally, even with no incoming traffic. Floored at the
27    /// gateway's park floor and capped at the idle window.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub park_max_ms: Option<u64>,
30    /// Escalate a session parked this long to a full hibernate (durable
31    /// checkpoint, slot freed) rather than staying frozen indefinitely.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub hibernate_after_parked_ms: Option<u64>,
34    /// Take a live durable checkpoint at most this often while the session
35    /// runs. Floored server-side.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub checkpoint_every_ms: Option<u64>,
38}
39
40/// One attach-at-create instruction: a persistent volume and the absolute
41/// in-guest path to mount it at. The mount happens host-side before the guest
42/// starts, and survives pause/resume and recreate.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct VolumeAttachment {
46    /// A `vol_…` id belonging to your org.
47    pub volume_id: String,
48    /// Absolute in-guest mount point, e.g. `/data`.
49    pub path: String,
50}
51
52/// Wire body for `POST /sandboxes`. Null/unset fields are omitted.
53#[derive(Debug, Clone, Default, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub struct CreateSandboxRequest {
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub template: Option<String>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub kind: Option<String>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub cpu: Option<u32>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub mem_mb: Option<u32>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub disk_gb: Option<u32>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub envs: Option<HashMap<String, String>>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub metadata: Option<HashMap<String, String>>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub timeout_ms: Option<u64>,
72    /// Legacy, gateway-side FALLBACK idle window in seconds, only used when
73    /// `timeout_ms` is unset — prefer `timeout_ms`. Kept for parity with the
74    /// reference SDKs, which have always accepted it.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub ttl_seconds: Option<u64>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub from_snapshot: Option<String>,
79    /// VM isolation tier: `"shared"` (sandbox-only), `"dedicated"` (default)
80    /// or `"hardened"` (own VM, no outbound network — verified 2026-09-23 on
81    /// staging, not yet on prod; refused with 409 `HostCapabilityMissing`
82    /// where a host doesn't advertise it).
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub isolation: Option<String>,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub lifecycle: Option<Lifecycle>,
87    /// Initial display resolution, e.g. `"1280x720"`. Desktops only.
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub resolution: Option<String>,
90    /// Record the session server-side; the response carries a presigned playback
91    /// URL. Desktops only — `record` on a headless sandbox is rejected (400
92    /// `RecordingRequiresDesktop`).
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub record: Option<bool>,
95    /// Persistent volumes to mount before the session starts.
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub volumes: Option<Vec<VolumeAttachment>>,
98}
99
100/// `POST /sandboxes/:id/resume` reply. The gateway may hand back a fresh
101/// `controlUrl` (the session lands on a new slot, so the old one is stale);
102/// `Client::resume` derives one when it does not.
103#[derive(Debug, Clone, Default, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct ResumeResponse {
106    #[serde(default)]
107    pub state: String,
108    #[serde(default)]
109    pub control_url: Option<String>,
110}
111
112/// `POST /sandboxes/:id/start` reply. `control_url` is a fresh URL (the sandbox was
113/// cold-booted); `already_running` is set when it was already running.
114#[derive(Debug, Clone, Default, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct StartResponse {
117    #[serde(default)]
118    pub state: String,
119    #[serde(default)]
120    pub control_url: Option<String>,
121    #[serde(default)]
122    pub already_running: Option<bool>,
123}
124
125/// `201` response from `POST /sandboxes`.
126#[derive(Debug, Clone, Deserialize)]
127#[serde(rename_all = "camelCase")]
128pub struct CreateSandboxResponse {
129    pub sandbox_id: String,
130    #[serde(default)]
131    pub kind: String,
132    pub control_url: String,
133    #[serde(default)]
134    pub expires_at: String,
135    /// Present for desktops only; ignored on the sandbox core surface.
136    #[serde(default)]
137    pub stream_url: Option<String>,
138    /// The session recording link, when the create asked for a recording.
139    #[serde(default)]
140    pub recording_url: Option<String>,
141}
142
143/// `GET /sandboxes/:id` response (subset used by `connect`).
144#[derive(Debug, Clone, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct SandboxView {
147    pub sandbox_id: String,
148    #[serde(default)]
149    pub kind: String,
150    #[serde(default)]
151    pub state: String,
152    #[serde(default)]
153    pub expires_at: String,
154    /// Some gateways include a control URL directly; derived otherwise.
155    #[serde(default)]
156    pub control_url: Option<String>,
157}
158
159/// Shape gateway error bodies may take.
160#[derive(Debug, Clone, Default, Deserialize)]
161pub struct GatewayErrorBody {
162    #[serde(default)]
163    pub code: Option<String>,
164    #[serde(default)]
165    pub error: Option<String>,
166    #[serde(default)]
167    pub message: Option<String>,
168    #[serde(default)]
169    pub retryable: Option<bool>,
170}
171
172// ---------------------------------------------------------------------------
173// commands
174// ---------------------------------------------------------------------------
175
176/// Terminal result of `commands.run` (also the `/exec` fast-path response).
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(rename_all = "camelCase")]
179pub struct CommandResult {
180    #[serde(default)]
181    pub exit_code: i64,
182    #[serde(default)]
183    pub stdout: String,
184    #[serde(default)]
185    pub stderr: String,
186    /// True when the command hit its `timeout_ms` and was killed (`exit_code` is -1 then).
187    #[serde(default)]
188    pub timed_out: bool,
189}
190
191// ---------------------------------------------------------------------------
192// files
193// ---------------------------------------------------------------------------
194
195/// One directory entry (`fs.list`). Matches the reference `types.ts` FsEntry:
196/// the wire field is `dir` (bool), NOT `isDir`; there is no path/mode/modTime.
197#[derive(Debug, Clone, Deserialize)]
198pub struct FsEntry {
199    #[serde(default)]
200    pub name: String,
201    #[serde(default, rename = "dir")]
202    pub dir: bool,
203    #[serde(default)]
204    pub size: i64,
205}
206
207/// File metadata (`fs.stat`). Matches the reference `types.ts` FsStat: fields
208/// `dir` (bool), `mode` (int), and `modTimeMs` (unix-millis).
209#[derive(Debug, Clone, Deserialize)]
210pub struct FsStat {
211    #[serde(default)]
212    pub name: String,
213    #[serde(default, rename = "dir")]
214    pub dir: bool,
215    #[serde(default)]
216    pub size: i64,
217    #[serde(default)]
218    pub mode: i64,
219    #[serde(default, rename = "modTimeMs")]
220    pub mod_time_ms: i64,
221}
222
223// ---------------------------------------------------------------------------
224// code.run
225// ---------------------------------------------------------------------------
226
227/// Kind of a structured chart extracted from a matplotlib figure.
228#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229#[serde(rename_all = "snake_case")]
230pub enum ChartType {
231    Line,
232    Scatter,
233    Bar,
234    Pie,
235    BoxAndWhisker,
236    Composite,
237    #[serde(other)]
238    Unknown,
239}
240
241impl Default for ChartType {
242    fn default() -> Self {
243        ChartType::Unknown
244    }
245}
246
247/// One axis of a 2D chart.
248#[derive(Debug, Clone, Default, Serialize, Deserialize)]
249pub struct ChartAxis {
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub label: Option<String>,
252    #[serde(default, skip_serializing_if = "Option::is_none")]
253    pub ticks: Option<Vec<Value>>,
254    #[serde(default, skip_serializing_if = "Option::is_none")]
255    pub scale: Option<String>,
256}
257
258/// Structured representation of a matplotlib figure. `elements` is kept as raw
259/// JSON so new chart types don't require an SDK bump.
260#[derive(Debug, Clone, Default, Serialize, Deserialize)]
261#[serde(rename_all = "camelCase")]
262pub struct Chart {
263    #[serde(default, rename = "type")]
264    pub chart_type: ChartType,
265    #[serde(default, skip_serializing_if = "Option::is_none")]
266    pub title: Option<String>,
267    #[serde(default, skip_serializing_if = "Option::is_none")]
268    pub x_label: Option<String>,
269    #[serde(default, skip_serializing_if = "Option::is_none")]
270    pub y_label: Option<String>,
271    #[serde(default, skip_serializing_if = "Option::is_none")]
272    pub x: Option<ChartAxis>,
273    #[serde(default, skip_serializing_if = "Option::is_none")]
274    pub y: Option<ChartAxis>,
275    /// Loose per-type data (points/bars/slices). Raw decoded JSON.
276    #[serde(default, skip_serializing_if = "Option::is_none")]
277    pub elements: Option<Value>,
278}
279
280/// One rich result object from `code.run`.
281#[derive(Debug, Clone, Default, Deserialize)]
282pub struct CodeResultItem {
283    #[serde(default, rename = "type")]
284    pub result_type: String,
285    #[serde(default)]
286    pub text: Option<String>,
287    #[serde(default)]
288    pub png: Option<String>,
289    #[serde(default)]
290    pub jpeg: Option<String>,
291    #[serde(default)]
292    pub svg: Option<String>,
293    #[serde(default)]
294    pub html: Option<String>,
295    #[serde(default)]
296    pub latex: Option<String>,
297    #[serde(default)]
298    pub json: Option<Value>,
299    #[serde(default)]
300    pub markdown: Option<String>,
301    #[serde(default)]
302    pub chart: Option<Chart>,
303}
304
305/// Result of `code.run`, with the client-side `charts` convenience view.
306#[derive(Debug, Clone, Default)]
307pub struct RunCodeResult {
308    pub results: Vec<CodeResultItem>,
309    pub error: Option<Value>,
310    /// All structured charts across `results` (flattened convenience view).
311    pub charts: Vec<Chart>,
312}
313
314// ---------------------------------------------------------------------------
315// git
316// ---------------------------------------------------------------------------
317
318/// Working-tree status (`git.status`).
319#[derive(Debug, Clone, Default, PartialEq, Eq)]
320pub struct GitStatus {
321    pub branch: String,
322    pub detached: bool,
323    pub ahead: i64,
324    pub behind: i64,
325    pub staged: Vec<String>,
326    pub modified: Vec<String>,
327    pub untracked: Vec<String>,
328    pub clean: bool,
329}
330
331/// One branch entry (`git.branches`).
332#[derive(Debug, Clone, Default, PartialEq, Eq)]
333pub struct GitBranch {
334    pub name: String,
335    pub commit: String,
336    pub current: bool,
337}
338
339/// One commit record (`git.log`).
340#[derive(Debug, Clone, Default, PartialEq, Eq)]
341pub struct GitCommit {
342    pub hash: String,
343    pub author: String,
344    pub email: String,
345    pub date: String,
346    pub message: String,
347}