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"solari-sandbox is published to crates.io (source in the solari-sdk GitHub org).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: Client::new(), create(), connect(), get(), kill()Sandbox: accessors, connect(), close(), kill(), commands(), files(), code(), git()Commands: run(), start(), CommandHandleFiles: read(), read_text(), write(), list(), stat(), mkdir(), remove(), rename()Code: run()Git: clone(), status(), add(), commit(), push(), pull(), checkout(), branches(), log()- Types:
ClientOptions,CreateOptions,Lifecycle,SandboxView,RunOptions,CommandResult,RunCodeOptions,RunCodeResult,Chart,FsEntry,FsStat,GitStatus,GitBranch,GitCommit, SolariError
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:
optsClientOptions: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:
optsCreateOptions: 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 openChannel
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?;/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 timeoutkill()
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.optsRunOptions: 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?;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.optsRunOptions: 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;Nonemeans the guest default (SIGTERM).wait(): consumes the handle, accumulating stdout/stderr until exit. Errors withConnectionif 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.dataimpl AsRef<[u8]>: content;&strandVec<u8>both work.modeOption<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.recursivebool: 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.optsRunCodeOptions:language(defaults to python server-side) andcontext_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()); // base64RunCodeOptions::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.optsGitCloneOptions: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:
cwdOption<&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.cwdOption<&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.optsGitCommitOptions:cwd,author,email(passed as-coverrides, 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:
optsGitRemoteOptions: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.cwdOption<&str>: repository directory.createbool: 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:
cwdOption<&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:
optsGitLogOptions: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_keyString: required; empty is rejected.base_urlString: required; trailing slash stripped. The control-WS origin is derived from it (http→ws).call_timeout_msOption<u64>: per-RPC control-WS timeout.Noneuses the default of300_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.
templateOption<String>: e.g."base","code","python-kernel", or a promoted template id.cpuOption<u32>,mem_mbOption<u32>,disk_gbOption<u32>: resources; the host clamps to slot capacity.envsOption<HashMap<String, String>>: per-session environment.metadataOption<HashMap<String, String>>: opaque labels.timeout_msOption<u64>: idle window before auto-release.from_snapshotOption<String>: boot from a snapshot instead of the template.lifecycleOption<Lifecycle>:on_timeoutString ("pause"/"kill") +auto_resumeOption<bool>.
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_idString,kindString,stateString: e.g."running","paused".expires_atString: ISO 8601.control_urlOption<String>: some gateways include it; otherwise connect() derives it.
RunOptions
Struct + two builder shorthands (.args(), .cwd()). Everything else goes through struct literal syntax.
argsOption<Vec<String>>: argv tail; no shell expansion.cwd,userOption<String>,timeout_msOption<u64>.envOption<HashMap<String, String>>: per-command environment.backgroundbool: return immediately; output still streams via callbacks.on_stdout,on_stderrOption<OutputCallback>:Arc<dyn Fn(&str) + Send + Sync>.
/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_codei64,stdoutString,stderrString.
RunCodeOptions / RunCodeResult
RunCodeOptions:languageOption<String> (python server-side default),context_idOption<String>.RunCodeResult:resultsVec<CodeResultItem>,errorOption<Value>,chartsVec<Chart>: every chart acrossresults, flattened client-side.CodeResultItem:result_typeString (stdout/stderr/result) plus optionaltext,png,jpeg,svg,html,latex,json,markdown,chart.
Chart
chart_typeChartType:Line,Scatter,Bar,Pie,BoxAndWhisker,Composite,Unknown. Unrecognized wire values deserialize toUnknownrather than failing.title,x_label,y_labelOption<String>.x,yOption<ChartAxis>:label,ticks,scale.elementsOption<Value>: per-type data (points, bars, slices), kept as raw JSON so new chart types do not need an SDK bump.
FsEntry / FsStat
FsEntry:nameString,dirbool,sizei64.FsStat: the above plusmodei64 (permission bits) andmod_time_msi64 (unix millis).
GitStatus / GitBranch / GitCommit
GitStatus:branchString (empty on a detached HEAD or a repo with no commits),detachedbool,cleanbool,ahead/behindi64,staged/modified/untrackedVec<String>.GitBranch:name,commitString,currentbool.GitCommit:hash,author,email,date,messageString.
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.
| Variant | Status | Meaning |
|---|---|---|
Auth | 401 / 403 | API key missing, malformed, or rejected. |
Plan | 402 | The plan does not allow this. |
ConcurrencyLimit | 429 | Too many live sessions. Not retried. |
NoCapacity | 503 | No host available right now. Retryable. |
Gateway | other | Any other non-2xx response (404/405/409/501…). |
Action | None | An RPC replied ok: false. Carries method. |
Timeout | None | No reply within the call timeout. Carries method, timeout_ms. |
Connection | None | The control WebSocket is not open, or dropped. |
Git | None | A git subcommand exited non-zero. |
Other | None | Bad 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),
}