Solari

Sandboxes

solari-sandbox (library solari) provides two types: Client and Sandbox. A sandbox is a headless microVM for commands, code, files, and git. See the Rust SDK hub for install and configuration.

solari-sandbox = "0.1"
Published on crates.io
solari-sandbox is published to crates.io (source in the solari-sdk GitHub org).
This is the core surface, not the full TypeScript one
The Rust crate covers session lifecycle plus commands, files, code.run, and git. It does not yet port PTYs, snapshots, volumes, metrics, pause/resume, preview URLs, signed transfer URLs, or files.search/watch. Use the HTTP API or the TypeScript SDK for those.

Contents

Client

The sandbox entry point. Talks the gateway REST API.

Constructors

Client::new()

fn new(opts: ClientOptions) -> Result<Client, SolariError>

Creates a client. Does not open any connection.

Parameters:

  • opts ClientOptions: api_key, base_url, call_timeout_ms. See ClientOptions.

Returns: Result<Client, SolariError>

Errors: SolariError::Other if api_key or base_url is empty.

Example:

use solari::{Client, ClientOptions};

let client = Client::new(ClientOptions::new(&api_key, "https://api.getsolari.com"))?;

Methods

create()

async fn create(&self, opts: CreateOptions) -> Result<Sandbox, SolariError>

Creates a sandbox and returns a live handle (POST /sandboxes). Retry-safe, since it sends an idempotency key.

Parameters:

  • opts CreateOptions: see CreateOptions. It is a plain struct, so use ..Default::default().

Returns: Result<Sandbox, SolariError>

Errors: Auth, Plan, ConcurrencyLimit, NoCapacity, or Gateway. See Errors.

Example:

use solari::CreateOptions;

let sbx = client
    .create(CreateOptions {
        template: Some("base".into()),
        cpu: Some(2),
        mem_mb: Some(4096),
        ..Default::default()
    })
    .await?;

connect()

async fn connect(&self, sandbox_id: &str) -> Result<Sandbox, SolariError>

Re-attaches to a running sandbox by id. Fetches its view, then derives the control URL from the gateway origin when the view omits one.

Parameters:

  • sandbox_id &str: the sandbox id.

Returns: Result<Sandbox, SolariError>

Errors: SolariError::Gateway if the id is unknown.

Example:

let sbx = client.connect("sbx_abc123").await?;
let out = sbx.commands().run("uptime", RunOptions::new()).await?;

get()

async fn get(&self, sandbox_id: &str) -> Result<SandboxView, SolariError>

Fetches a sandbox’s current record (state, kind, expiry). Returns the raw view, not a handle.

Parameters:

  • sandbox_id &str: the sandbox id.

Returns: Result<SandboxView, SolariError>

Errors: SolariError::Gateway on a non-2xx status.

Example:

let view = client.get("sbx_abc123").await?;
println!("{} {}", view.state, view.expires_at); // "running" …

kill()

async fn kill(&self, sandbox_id: &str) -> Result<(), SolariError>

Destroys a sandbox by id. Idempotent.

Parameters:

  • sandbox_id &str: the sandbox id.

Returns: Result<(), SolariError>

Example:

client.kill("sbx_abc123").await?;

Sandbox

A live headless session. Construct via Client::create() or Client::connect().

Accessors

fn id(&self) -> &str            // the session id
fn control_url(&self) -> &str   // wss:// JSON-RPC control channel
fn expires_at(&self) -> &str    // ISO 8601 expiry
fn connected(&self) -> bool     // whether the control channel is open

Channel

connect()

async fn connect(&self) -> Result<(), SolariError>

Opens the control WebSocket. Idempotent. Every namespace opens it on demand, so calling this is only a latency optimization.

Returns: Result<(), SolariError>

Errors: SolariError::Connection if the socket cannot be opened; SolariError::Timeout if the upgrade exceeds the call timeout.

Example:

sbx.connect().await?;
Leaving it closed enables a fast path
commands().run() takes a warm one-shot HTTP /exec route instead of the WebSocket when the channel is not already open and the call needs no streaming. For a single command, skipping connect() is faster.

close()

fn close(&self)

Closes the control channel locally. Does not release the remote session. Use kill() for that.

Returns: ()

Example:

sbx.close(); // the session keeps running until its timeout

kill()

async fn kill(&self) -> Result<(), SolariError>

Destroys the remote session and closes the channel. Idempotent.

Returns: Result<(), SolariError>

Errors: SolariError::Gateway on a non-2xx status.

Example:

let result = sbx.commands().run("./build.sh", RunOptions::new()).await;
sbx.kill().await?;
result?;

Commands

Run processes in the guest. Obtain with sbx.commands().

run()

async fn run(&self, cmd: &str, opts: RunOptions) -> Result<CommandResult, SolariError>

Runs a command to completion and returns its exit code and captured output. A non-zero exit is Ok, not an error.

Parameters:

  • cmd &str: the program to run.
  • opts RunOptions: see RunOptions.

Returns: Result<CommandResult, SolariError> with exit_code, stdout, stderr. With background: true it returns immediately with an empty result.

Errors: SolariError::Connection if the channel drops before the process exits; Action if the guest rejects the RPC.

Example:

use solari::RunOptions;

let r = sbx.commands().run("ls", RunOptions::new().args(["-la", "/tmp"])).await?;
println!("{} {}", r.exit_code, r.stdout);

// Stream output as it arrives.
let opts = RunOptions {
    args: Some(vec!["install".into()]),
    cwd: Some("/app".into()),
    on_stdout: Some(std::sync::Arc::new(|s: &str| print!("{s}"))),
    ..Default::default()
};
sbx.commands().run("npm", opts).await?;
No shell by default
cmd is executed directly with args, not through a shell. Pipes, globs, and && will not expand. For shell syntax use run("sh", RunOptions::new().args(["-c", "…"])).

start()

async fn start(&self, cmd: &str, opts: RunOptions) -> Result<CommandHandle, SolariError>

Starts a command and returns a handle immediately, without waiting for exit. Use it for interactive or long-running processes.

Parameters:

  • cmd &str: the program to run.
  • opts RunOptions: see RunOptions.

Returns: Result<CommandHandle, SolariError>

Errors: SolariError::Other if the guest reply carries no cmdId; Connection if the channel is not open.

Example:

let proc = sbx
    .commands()
    .start("python3", RunOptions::new().args(["-u", "worker.py"]))
    .await?;

proc.stdin("input\n").await?;
let out = proc.wait().await?;
println!("exit {}", out.exit_code);

CommandHandle

Returned by start(). Field cmd_id String identifies the process.

async fn stdin(&self, data: impl AsRef<[u8]>) -> Result<(), SolariError>
async fn kill(&self, signal: Option<i64>) -> Result<(), SolariError>
async fn wait(self) -> Result<CommandResult, SolariError>
  • stdin(data): writes bytes to the command’s stdin (base64-framed on the wire).
  • kill(signal): signals the process; None means the guest default (SIGTERM).
  • wait(): consumes the handle, accumulating stdout/stderr until exit. Errors with Connection if the channel closes first.

Example:

let proc = sbx.commands().start("sleep", RunOptions::new().args(["60"])).await?;
proc.kill(Some(9)).await?;      // SIGKILL
let out = proc.wait().await?;

Files

Guest filesystem access over the control channel. Obtain with sbx.files(). Bytes are base64-framed on the wire.

read()

async fn read(&self, path: &str) -> Result<Vec<u8>, SolariError>

Reads a file’s raw bytes.

Parameters:

  • path &str: absolute in-guest path.

Returns: Result<Vec<u8>, SolariError>

Errors: SolariError::Action if the guest cannot read it; Other on a bad base64 payload.

Example:

let bytes = sbx.files().read("/tmp/out.bin").await?;

read_text()

async fn read_text(&self, path: &str) -> Result<String, SolariError>

Reads a file as UTF-8 text. Invalid sequences are replaced, not rejected.

Parameters:

  • path &str: absolute in-guest path.

Returns: Result<String, SolariError>

Errors: as read().

Example:

let host = sbx.files().read_text("/etc/hostname").await?;

write()

async fn write(
    &self,
    path: &str,
    data: impl AsRef<[u8]>,
    mode: Option<i64>,
) -> Result<(), SolariError>

Writes a file, creating or truncating it.

Parameters:

  • path &str: absolute in-guest path.
  • data impl AsRef<[u8]>: content; &str and Vec<u8> both work.
  • mode Option<i64>: unix permission bits, e.g. Some(0o755).

Returns: Result<(), SolariError>

Errors: SolariError::Action on a guest-side failure.

Example:

sbx.files().write("/app/run.sh", "#!/bin/sh\necho hi\n", Some(0o755)).await?;

list()

async fn list(&self, path: &str) -> Result<Vec<FsEntry>, SolariError>

Lists a directory’s entries (non-recursive).

Parameters:

  • path &str: directory path.

Returns: Result<Vec<FsEntry>, SolariError>, each with name, dir, size.

Errors: SolariError::Other if the entries do not parse.

Example:

for e in sbx.files().list("/app").await? {
    println!("{} {} {}", if e.dir { "d" } else { "-" }, e.name, e.size);
}

stat()

async fn stat(&self, path: &str) -> Result<FsStat, SolariError>

Returns one entry’s metadata.

Parameters:

  • path &str: file or directory path.

Returns: Result<FsStat, SolariError> with name, dir, size, mode, mod_time_ms.

Errors: SolariError::Other if the reply does not parse.

Example:

let st = sbx.files().stat("/app/run.sh").await?;
println!("{} {:o} {}", st.size, st.mode, st.mod_time_ms);

mkdir()

async fn mkdir(&self, path: &str) -> Result<(), SolariError>

Creates a directory, including parents.

Parameters:

  • path &str: directory to create.

Returns: Result<(), SolariError>

Example:

sbx.files().mkdir("/app/data/nested").await?;

remove()

async fn remove(&self, path: &str, recursive: bool) -> Result<(), SolariError>

Deletes a file or directory.

Parameters:

  • path &str: path to delete.
  • recursive bool: required for non-empty directories.

Returns: Result<(), SolariError>

Example:

sbx.files().remove("/tmp/build", true).await?;

rename()

async fn rename(&self, from: &str, to: &str) -> Result<(), SolariError>

Renames or moves a path.

Parameters:

  • from, to &str: source and destination.

Returns: Result<(), SolariError>

Example:

sbx.files().rename("/tmp/a.txt", "/tmp/b.txt").await?;

Code

Stateful kernel execution. Obtain with sbx.code().

run()

async fn run(&self, code: &str, opts: RunCodeOptions) -> Result<RunCodeResult, SolariError>

Runs code in a stateful kernel. Rich outputs (PNG, HTML, JSON, structured charts) come back as results items.

Parameters:

  • code &str: source to execute.
  • opts RunCodeOptions: language (defaults to python server-side) and context_id.

Returns: Result<RunCodeResult, SolariError> with results, charts, error. A runtime error in the code lands in error; it is not a SolariError.

Errors: SolariError::Action or Connection. Transport failures only, never the code’s own exceptions.

Example:

use solari::RunCodeOptions;

let r = sbx
    .code()
    .run(
        "import matplotlib.pyplot as plt\nplt.plot([1,2,3],[4,5,6])\nplt.title('demo')\nplt.show()",
        RunCodeOptions::default(),
    )
    .await?;

println!("{:?}", r.charts[0].chart_type);          // ChartType::Line
println!("{:?}", r.charts[0].title);               // Some("demo")
let png = r.results.iter().find_map(|i| i.png.as_ref()); // base64
Contexts are pass-through only
RunCodeOptions::context_id reuses a kernel context so state persists across calls, like a REPL. The Rust crate has no create_code_context() to mint one. Obtain the id from the HTTP API or the TypeScript SDK, then pass it here.

Git

Obtain with sbx.git(). Every call is a non-shell git invocation in the guest, with no injection surface. Requires git on PATH, which the base template ships.

clone()

async fn clone(&self, url: &str, opts: GitCloneOptions) -> Result<(), SolariError>

Clones a repository into the guest.

Parameters:

  • url &str: remote URL.
  • opts GitCloneOptions: path, branch, depth, username, password, cwd.

Returns: Result<(), SolariError>

Errors: SolariError::Git carrying the subcommand, exit code, and git’s stderr.

Example:

use solari::GitCloneOptions;

sbx.git()
    .clone("https://github.com/org/repo.git", GitCloneOptions {
        path: Some("/app/repo".into()),
        branch: Some("main".into()),
        depth: Some(1),
        ..Default::default()
    })
    .await?;

status()

async fn status(&self, cwd: Option<&str>) -> Result<GitStatus, SolariError>

Parsed working-tree status.

Parameters:

  • cwd Option<&str>: repository directory.

Returns: Result<GitStatus, SolariError>. See GitStatus.

Errors: SolariError::Git on a non-zero exit.

Example:

let st = sbx.git().status(Some("/app/repo")).await?;
println!("{} clean={} +{}/-{}", st.branch, st.clean, st.ahead, st.behind);

add()

async fn add(&self, paths: &[String], cwd: Option<&str>) -> Result<(), SolariError>

Stages paths. An empty slice is a no-op. Paths are passed after --, so flag-like names are safe.

Parameters:

  • paths &[String]: use ["."] for everything.
  • cwd Option<&str>: repository directory.

Returns: Result<(), SolariError>

Errors: SolariError::Git on a non-zero exit.

Example:

sbx.git().add(&[".".to_string()], Some("/app/repo")).await?;

commit()

async fn commit(&self, message: &str, opts: GitCommitOptions) -> Result<String, SolariError>

Commits staged changes and returns the new hash. Set author/email, because an ephemeral session has no git identity.

Parameters:

  • message &str: commit message.
  • opts GitCommitOptions: cwd, author, email (passed as -c overrides, scoped to this commit), all (stage tracked modifications first).

Returns: Result<String, SolariError> carrying the commit hash, read back with rev-parse HEAD.

Errors: SolariError::Git on a non-zero exit.

Example:

use solari::GitCommitOptions;

let hash = sbx
    .git()
    .commit("add feature", GitCommitOptions {
        cwd: Some("/app/repo".into()),
        author: Some("CI Bot".into()),
        email: Some("ci@example.com".into()),
        ..Default::default()
    })
    .await?;

push() / pull()

async fn push(&self, opts: GitRemoteOptions) -> Result<(), SolariError>
async fn pull(&self, opts: GitRemoteOptions) -> Result<(), SolariError>

Pushes to or pulls from a remote. Credentials are spliced in for this one invocation via an insteadOf override and never persisted to the repo config.

Parameters:

  • opts GitRemoteOptions: cwd, remote (default "origin"), branch (default: the current branch’s upstream), username, password.

Returns: Result<(), SolariError>

Errors: SolariError::Git on a non-zero exit.

Example:

use solari::GitRemoteOptions;

sbx.git()
    .push(GitRemoteOptions {
        cwd: Some("/app/repo".into()),
        username: Some("x-access-token".into()),
        password: Some(github_token),
        ..Default::default()
    })
    .await?;

checkout()

async fn checkout(&self, r#ref: &str, cwd: Option<&str>, create: bool) -> Result<(), SolariError>

Checks out an existing ref, or creates a branch. Positional arguments here, not an options struct.

Parameters:

  • ref &str: branch, tag, or commit.
  • cwd Option<&str>: repository directory.
  • create bool: create the branch (-b).

Returns: Result<(), SolariError>

Errors: SolariError::Git on a non-zero exit.

Example:

sbx.git().checkout("feature/x", Some("/app/repo"), true).await?;

branches()

async fn branches(&self, cwd: Option<&str>) -> Result<Vec<GitBranch>, SolariError>

Lists local branches.

Parameters:

  • cwd Option<&str>: repository directory.

Returns: Result<Vec<GitBranch>, SolariError>, each with name, commit, current.

Errors: SolariError::Git on a non-zero exit.

Example:

for b in sbx.git().branches(Some("/app/repo")).await? {
    println!("{} {} {}", if b.current { "*" } else { " " }, b.name, b.commit);
}

log()

async fn log(&self, opts: GitLogOptions) -> Result<Vec<GitCommit>, SolariError>

Recent commits, newest first.

Parameters:

  • opts GitLogOptions: cwd, max_count (applied only when positive).

Returns: Result<Vec<GitCommit>, SolariError>, each with hash, author, email, date, message.

Errors: SolariError::Git on a non-zero exit.

Example:

use solari::GitLogOptions;

let commits = sbx
    .git()
    .log(GitLogOptions { cwd: Some("/app/repo".into()), max_count: Some(10) })
    .await?;

Types

ClientOptions

  • api_key String: required; empty is rejected.
  • base_url String: required; trailing slash stripped. The control-WS origin is derived from it (httpws).
  • call_timeout_ms Option<u64>: per-RPC control-WS timeout. None uses the default of 300_000.
ClientOptions::new(&api_key, "https://api.getsolari.com")

CreateOptions

A plain struct implementing Default, not a builder. Unset fields are omitted from the wire.

  • template Option<String>: e.g. "base", "code", "python-kernel", or a promoted template id.
  • cpu Option<u32>, mem_mb Option<u32>, disk_gb Option<u32>: resources; the host clamps to slot capacity.
  • envs Option<HashMap<String, String>>: per-session environment.
  • metadata Option<HashMap<String, String>>: opaque labels.
  • timeout_ms Option<u64>: idle window before auto-release.
  • from_snapshot Option<String>: boot from a snapshot instead of the template.
  • lifecycle Option<Lifecycle>: on_timeout String ("pause" / "kill") + auto_resume Option<bool>.
create() always sends kind: sandbox
CreateOptions has no kind field: the flavour is chosen by which method you call. create() sends kind: "sandbox" on the wire. For a GUI VM, call create_desktop(), which sends kind: "desktop" and returns the same Sandbox handle plus stream_url(). Driving the GUI is not part of the Rust surface. See VMs.

SandboxView

  • sandbox_id String, kind String, state String: e.g. "running", "paused".
  • expires_at String: ISO 8601.
  • control_url Option<String>: some gateways include it; otherwise connect() derives it.

RunOptions

Struct + two builder shorthands (.args(), .cwd()). Everything else goes through struct literal syntax.

  • args Option<Vec<String>>: argv tail; no shell expansion.
  • cwd, user Option<String>, timeout_ms Option<u64>.
  • env Option<HashMap<String, String>>: per-command environment.
  • background bool: return immediately; output still streams via callbacks.
  • on_stdout, on_stderr Option<OutputCallback>: Arc<dyn Fn(&str) + Send + Sync>.
The /exec fast path
run() uses a one-shot HTTP /exec route (skipping the WebSocket entirely) when the channel is closed and the call sets no on_stdout/on_stderr, no env, no user, and background: false. It falls back to the control channel only if the gateway does not serve /exec (404/405/501); any other failure propagates, so a command never double-executes.

CommandResult

  • exit_code i64, stdout String, stderr String.

RunCodeOptions / RunCodeResult

  • RunCodeOptions: language Option<String> (python server-side default), context_id Option<String>.
  • RunCodeResult: results Vec<CodeResultItem>, error Option<Value>, charts Vec<Chart>: every chart across results, flattened client-side.
  • CodeResultItem: result_type String (stdout/stderr/result) plus optional text, png, jpeg, svg, html, latex, json, markdown, chart.

Chart

  • chart_type ChartType: Line, Scatter, Bar, Pie, BoxAndWhisker, Composite, Unknown. Unrecognized wire values deserialize to Unknown rather than failing.
  • title, x_label, y_label Option<String>.
  • x, y Option<ChartAxis>: label, ticks, scale.
  • elements Option<Value>: per-type data (points, bars, slices), kept as raw JSON so new chart types do not need an SDK bump.

FsEntry / FsStat

  • FsEntry: name String, dir bool, size i64.
  • FsStat: the above plus mode i64 (permission bits) and mod_time_ms i64 (unix millis).

GitStatus / GitBranch / GitCommit

  • GitStatus: branch String (empty on a detached HEAD or a repo with no commits), detached bool, clean bool, ahead / behind i64, staged / modified / untracked Vec<String>.
  • GitBranch: name, commit String, current bool.
  • GitCommit: hash, author, email, date, message String.

Errors

One SolariError enum covers every failure, flattening the TypeScript error hierarchy so you can match broadly. .status() returns Option<u16> for the gateway variants.

VariantStatusMeaning
Auth401 / 403API key missing, malformed, or rejected.
Plan402The plan does not allow this.
ConcurrencyLimit429Too many live sessions. Not retried.
NoCapacity503No host available right now. Retryable.
GatewayotherAny other non-2xx response (404/405/409/501…).
ActionNoneAn RPC replied ok: false. Carries method.
TimeoutNoneNo reply within the call timeout. Carries method, timeout_ms.
ConnectionNoneThe control WebSocket is not open, or dropped.
GitNoneA git subcommand exited non-zero.
OtherNoneBad response body, or an internal invariant.
use solari::SolariError;

match client.create(opts).await {
    Ok(sbx) => { /* … */ }
    Err(SolariError::NoCapacity { .. }) => retry_later().await,
    Err(SolariError::ConcurrencyLimit { .. }) => wait_for_slot().await,
    Err(e) => return Err(e),
}