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}
139
140/// `GET /sandboxes/:id` response (subset used by `connect`).
141#[derive(Debug, Clone, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct SandboxView {
144    pub sandbox_id: String,
145    #[serde(default)]
146    pub kind: String,
147    #[serde(default)]
148    pub state: String,
149    #[serde(default)]
150    pub expires_at: String,
151    /// Some gateways include a control URL directly; derived otherwise.
152    #[serde(default)]
153    pub control_url: Option<String>,
154}
155
156/// Shape gateway error bodies may take.
157#[derive(Debug, Clone, Default, Deserialize)]
158pub struct GatewayErrorBody {
159    #[serde(default)]
160    pub code: Option<String>,
161    #[serde(default)]
162    pub error: Option<String>,
163    #[serde(default)]
164    pub message: Option<String>,
165    #[serde(default)]
166    pub retryable: Option<bool>,
167}
168
169// ---------------------------------------------------------------------------
170// commands
171// ---------------------------------------------------------------------------
172
173/// Terminal result of `commands.run` (also the `/exec` fast-path response).
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct CommandResult {
177    #[serde(default)]
178    pub exit_code: i64,
179    #[serde(default)]
180    pub stdout: String,
181    #[serde(default)]
182    pub stderr: String,
183}
184
185// ---------------------------------------------------------------------------
186// files
187// ---------------------------------------------------------------------------
188
189/// One directory entry (`fs.list`). Matches the reference `types.ts` FsEntry:
190/// the wire field is `dir` (bool), NOT `isDir`; there is no path/mode/modTime.
191#[derive(Debug, Clone, Deserialize)]
192pub struct FsEntry {
193    #[serde(default)]
194    pub name: String,
195    #[serde(default, rename = "dir")]
196    pub dir: bool,
197    #[serde(default)]
198    pub size: i64,
199}
200
201/// File metadata (`fs.stat`). Matches the reference `types.ts` FsStat: fields
202/// `dir` (bool), `mode` (int), and `modTimeMs` (unix-millis).
203#[derive(Debug, Clone, Deserialize)]
204pub struct FsStat {
205    #[serde(default)]
206    pub name: String,
207    #[serde(default, rename = "dir")]
208    pub dir: bool,
209    #[serde(default)]
210    pub size: i64,
211    #[serde(default)]
212    pub mode: i64,
213    #[serde(default, rename = "modTimeMs")]
214    pub mod_time_ms: i64,
215}
216
217// ---------------------------------------------------------------------------
218// code.run
219// ---------------------------------------------------------------------------
220
221/// Kind of a structured chart extracted from a matplotlib figure.
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(rename_all = "snake_case")]
224pub enum ChartType {
225    Line,
226    Scatter,
227    Bar,
228    Pie,
229    BoxAndWhisker,
230    Composite,
231    #[serde(other)]
232    Unknown,
233}
234
235impl Default for ChartType {
236    fn default() -> Self {
237        ChartType::Unknown
238    }
239}
240
241/// One axis of a 2D chart.
242#[derive(Debug, Clone, Default, Serialize, Deserialize)]
243pub struct ChartAxis {
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub label: Option<String>,
246    #[serde(default, skip_serializing_if = "Option::is_none")]
247    pub ticks: Option<Vec<Value>>,
248    #[serde(default, skip_serializing_if = "Option::is_none")]
249    pub scale: Option<String>,
250}
251
252/// Structured representation of a matplotlib figure. `elements` is kept as raw
253/// JSON so new chart types don't require an SDK bump.
254#[derive(Debug, Clone, Default, Serialize, Deserialize)]
255#[serde(rename_all = "camelCase")]
256pub struct Chart {
257    #[serde(default, rename = "type")]
258    pub chart_type: ChartType,
259    #[serde(default, skip_serializing_if = "Option::is_none")]
260    pub title: Option<String>,
261    #[serde(default, skip_serializing_if = "Option::is_none")]
262    pub x_label: Option<String>,
263    #[serde(default, skip_serializing_if = "Option::is_none")]
264    pub y_label: Option<String>,
265    #[serde(default, skip_serializing_if = "Option::is_none")]
266    pub x: Option<ChartAxis>,
267    #[serde(default, skip_serializing_if = "Option::is_none")]
268    pub y: Option<ChartAxis>,
269    /// Loose per-type data (points/bars/slices). Raw decoded JSON.
270    #[serde(default, skip_serializing_if = "Option::is_none")]
271    pub elements: Option<Value>,
272}
273
274/// One rich result object from `code.run`.
275#[derive(Debug, Clone, Default, Deserialize)]
276pub struct CodeResultItem {
277    #[serde(default, rename = "type")]
278    pub result_type: String,
279    #[serde(default)]
280    pub text: Option<String>,
281    #[serde(default)]
282    pub png: Option<String>,
283    #[serde(default)]
284    pub jpeg: Option<String>,
285    #[serde(default)]
286    pub svg: Option<String>,
287    #[serde(default)]
288    pub html: Option<String>,
289    #[serde(default)]
290    pub latex: Option<String>,
291    #[serde(default)]
292    pub json: Option<Value>,
293    #[serde(default)]
294    pub markdown: Option<String>,
295    #[serde(default)]
296    pub chart: Option<Chart>,
297}
298
299/// Result of `code.run`, with the client-side `charts` convenience view.
300#[derive(Debug, Clone, Default)]
301pub struct RunCodeResult {
302    pub results: Vec<CodeResultItem>,
303    pub error: Option<Value>,
304    /// All structured charts across `results` (flattened convenience view).
305    pub charts: Vec<Chart>,
306}
307
308// ---------------------------------------------------------------------------
309// git
310// ---------------------------------------------------------------------------
311
312/// Working-tree status (`git.status`).
313#[derive(Debug, Clone, Default, PartialEq, Eq)]
314pub struct GitStatus {
315    pub branch: String,
316    pub detached: bool,
317    pub ahead: i64,
318    pub behind: i64,
319    pub staged: Vec<String>,
320    pub modified: Vec<String>,
321    pub untracked: Vec<String>,
322    pub clean: bool,
323}
324
325/// One branch entry (`git.branches`).
326#[derive(Debug, Clone, Default, PartialEq, Eq)]
327pub struct GitBranch {
328    pub name: String,
329    pub commit: String,
330    pub current: bool,
331}
332
333/// One commit record (`git.log`).
334#[derive(Debug, Clone, Default, PartialEq, Eq)]
335pub struct GitCommit {
336    pub hash: String,
337    pub author: String,
338    pub email: String,
339    pub date: String,
340    pub message: String,
341}