Solari

Sandboxes

solari (target solari) has four classes: Client, Sandbox, CommandHandle, and GitOps. A sandbox is a headless microVM for commands, files, code, and git. Blocking and synchronous; async output arrives on the control channel’s receive thread. See the C++ SDK hub for install and configuration.

#include <solari/solari.hpp>  // umbrella header
This is the core surface, not the full TypeScript one
The C++ binding covers the core: commands, files, code, git, plus session lifecycle (pause()/resume()) and create-time volume attachment. It does not bind the TypeScript SDK’s volume CRUD (client.volumes.create/list/get/delete— C++ can only attach a volume created elsewhere, by id), named snapshot management, PTYs, file watch/search, presigned transfer URLs, metrics(), env(), or previewUrl(). Reach those through client.http() or a TypeScript process.

Contents

Client

The sandbox entry point. Talks the gateway’s /sandboxes REST API and hands back Sandbox handles.

Constructors

Client()

explicit Client(const ClientOptions& options)

Creates a client. Opens no connection.

Parameters:

  • options.apiKey std::string: required.
  • options.baseUrl std::string: required, e.g. https://api.getsolari.com. There is no default.
  • options.callTimeoutMs long: per-RPC timeout for handles this client creates. Default 300000.

Throws: solari::Error if apiKey or baseUrl is empty.

Example:

solari::ClientOptions opts;
opts.apiKey = "slr_live_...";
opts.baseUrl = "https://api.getsolari.com";
solari::Client client(opts);

Methods

create()

std::shared_ptr<Sandbox> create(const CreateSandboxOptions& opts = {})

POST /sandboxes: creates a sandbox and returns a handle. Retry-safe: it sends a fresh idempotency key.

Parameters:

Returns: std::shared_ptr<Sandbox>, not connected; call connect() before any RPC.

Throws: AuthError, PlanError, ConcurrencyLimitError, NoCapacityError, or GatewayError.

Example:

solari::CreateSandboxOptions o;
o.template_ = "base";   // trailing underscore: "template" is a C++ keyword
o.cpu = 2;
o.memMb = 4096;

auto sbx = client.create(o);
sbx->connect();
create() always makes a sandbox
create() sends SandboxKind::Sandbox. For a GUI VM, call createDesktop(), which sends SandboxKind::Desktop and returns the same Sandbox handle plus streamUrl(). Driving the GUI is not part of the C++ surface. See VMs.

connect()

std::shared_ptr<Sandbox> connect(const std::string& sandboxId)

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

Parameters:

  • sandboxId std::string: the sandbox id.

Returns: std::shared_ptr<Sandbox>, also not connected; call connect() on the handle.

Throws: GatewayError if the id is unknown.

Example:

auto sbx = client.connect("sbx_abc123");
sbx->connect();  // Client::connect() re-attaches; Sandbox::connect() opens the socket
Two different connect()s
Client::connect(id) is a REST re-attach that builds a handle. Sandbox::connect() opens that handle’s control WebSocket. You need both. Unlike the TypeScript SDK, this one does not resume a paused session for you.

get()

SandboxView get(const std::string& sandboxId)

GET /sandboxes/:id: the current record. Returns a view, not a handle.

Parameters:

  • sandboxId std::string: the sandbox id.

Returns: SandboxView (sandboxId, kind, state, expiresAt, optional controlUrl).

Throws: GatewayError on a non-2xx response.

Example:

auto v = client.get("sbx_abc123");
std::cout << v.state << " " << v.expiresAt << "\n";

kill()

void kill(const std::string& sandboxId)

DELETE /sandboxes/:id: destroys a sandbox by id. Idempotent. Prefer Sandbox::kill() when you hold a handle.

Parameters:

  • sandboxId std::string: the sandbox id.

Example:

client.kill("sbx_abc123");

pause()

void pause(const std::string& sandboxId)

POST /sandboxes/:id/pause: snapshots the session’s RAM+disk and frees its host slot, by id (no handle needed). Prefer Sandbox::pause() when you hold one.

Parameters:

  • sandboxId std::string: the sandbox id.

Throws: GatewayError on a non-2xx response.

Example:

client.pause("sbx_abc123");

resume()

std::string resume(const std::string& sandboxId)

POST /sandboxes/:id/resume: brings a paused session back on a fresh host slot, by id.

Parameters:

  • sandboxId std::string: the sandbox id.

Returns: std::string, the control URL to re-attach to (the pre-pause one is stale — the session comes back on a different slot).

Throws: GatewayError on a non-2xx response.

Example:

std::string controlUrl = client.resume("sbx_abc123");

http()

HttpTransport& http()

The underlying transport, for endpoints this binding does not wrap (snapshots, volumes, preview URLs). Applies auth, retries, and timeouts.

Returns: HttpTransport&, whose request() returns parsed nlohmann::json and throws the mapped typed error on failure.

Example:

auto snaps = client.http().request("GET", "/snapshots");
std::cout << snaps.dump(2) << "\n";

Sandbox

A live session handle. Construct via Client::create() or Client::connect(); never directly. Not copyable.

Properties

  • id() const std::string&: the sandbox id.
  • controlUrl() const std::string&: the wss:// JSON-RPC control channel.
  • expiresAt() const std::string&: ISO 8601 expiry.
  • commands Commands, files Files, code Code, git GitOps: member objects, not methods.
  • channel() ControlChannel&: direct channel access, for the WS adapter and unit tests.

Channel

connect()

void connect()

Opens the control WebSocket. Idempotent. Every RPC below needs this first.

Throws: ConnectionError when built without SOLARI_WITH_WS, or if the socket cannot be opened.

Example:

auto sbx = client.create();
sbx->connect();

close()

void close()

Closes the control channel locally. Does not release the remote session. It keeps running until its timeout.

Example:

sbx->close();  // session survives; reattach later with client.connect(id)

connected()

bool connected() const

Whether the control channel is open.

Returns: bool

Example:

if (!sbx->connected()) sbx->connect();

kill()

void kill()

Destroys the remote session and closes the channel.

Throws: GatewayError if the delete fails.

Example:

sbx->kill();

pause()

void pause()

Snapshots this session’s RAM+disk, frees its host slot, and closes the control channel. The session keeps its id; bring it back with resume().

Throws: solari::Error if this handle was not created by a Client (e.g. hand-built in a test).

Example:

sbx->pause();  // channel closes; sbx->connected() is now false

resume()

void resume()

Resumes a paused session, adopts the control URL of the fresh slot it came back on, and reopens the control channel.

Throws: solari::Error if this handle was not created by a Client; whatever connect() throws if the reopen fails.

Example:

sbx->resume();  // controlUrl() is updated; sbx->connected() is true again
auto r = sbx->commands.run("echo", [] { solari::CommandOptions o; o.args = {"back"}; return o; }());
Not exposed on GitOps' CommandRunner — this is Sandbox-level
pause()/resume() exist on both Sandbox (needs a live handle) and Client (by id, no handle needed). Unlike the TypeScript SDK, Sandbox::connect() does not auto-resume a paused session — call resume() explicitly.

Commands

commands.run()

CommandResult run(const std::string& cmd, const CommandOptions& opts = {})

Runs a command to completion and returns its result. Equivalent to Sandbox::runCommand().

Parameters:

  • cmd std::string: the program. Not a shell line.
  • opts CommandOptions: args, cwd, env, user, timeoutMs, background, onStdout, onStderr.

Returns: CommandResult, with exitCode, stdout_, stderr_ (the trailing underscores dodge the <cstdio> macros).

Throws: ActionError, TimeoutError, or ConnectionError. A non-zero exit is not an error. Check exitCode.

Example:

solari::CommandOptions o;
o.args = {"-la", "/tmp"};
auto r = sbx->commands.run("ls", o);
if (r.exitCode != 0) std::cerr << r.stderr_;
std::cout << r.stdout_;
No shell by default
cmd is executed directly, so pipes, globs, and && do not work. For a shell line, run it explicitly:
o.args = {"-c", "ls /tmp | wc -l"}; sbx->commands.run("sh", o);

commands.start()

CommandHandle start(const std::string& cmd, const CommandOptions& opts = {})

Starts a command and returns a CommandHandle without waiting. Use it for long-running processes, streaming output, or stdin.

Parameters:

Returns: CommandHandle

Throws: ActionError or ConnectionError.

Example:

solari::CommandOptions o;
o.args = {"-u", "worker.py"};
auto proc = sbx->commands.start("python3", o);

proc.onData([](const std::string& stream, const std::string& data) {
  (stream == "stderr" ? std::cerr : std::cout) << data;
});
int code = proc.wait();

Code

code.run()

RunCodeResult run(const std::string& code, const RunCodeOptions& opts = {})

Executes a snippet in a stateful kernel and returns rich results. Equivalent to Sandbox::runCode().

Parameters:

  • code std::string: the source to run.
  • opts.language std::optional<std::string>: python, javascript, typescript, bash, or r.
  • opts.contextId std::optional<std::string>: reuse a kernel so variables persist across calls.
  • opts.onStdout / opts.onStderr std::function<void(const std::string&)>: called per text item as results are parsed.

Returns: RunCodeResult (results, charts, error). A runtime error in the snippet lands in error; it does not throw.

Throws: ActionError, TimeoutError, or ConnectionError.

Example:

solari::RunCodeOptions o;
o.language = "python";
auto r = sbx->code.run(R"(
import matplotlib.pyplot as plt
plt.plot([1, 2, 3], [4, 5, 6])
plt.title("demo")
plt.show()
)", o);

if (r.error) std::cerr << r.error->dump() << "\n";
for (const auto& c : r.charts)
  std::cout << c.type << " " << c.title.value_or("") << "\n";  // "line demo"
Charts are flattened for you
Every results[i].chart is lifted into the top-level charts array by flattenCodeResult(), so a matplotlib figure is structured data (type, title, axes, elements) rather than only a PNG. Chart::elements stays raw nlohmann::json so a new chart type never forces an SDK bump.

Files

files.read() / files.readText()

std::string read(const std::string& path)
std::string readText(const std::string& path)

Reads a file. The two are the same call. readText() exists for parity with the other SDKs; C++ has no separate bytes type here.

Parameters:

  • path std::string: absolute in-guest path.

Returns: std::string, the raw bytes.

Throws: ActionError if the path does not exist.

Example:

auto text = sbx->files.readText("/etc/hostname");

files.write()

void write(const std::string& path, const std::string& data,
           std::optional<int> mode = std::nullopt)

Writes a file, creating or truncating it.

Parameters:

  • path std::string: absolute in-guest path.
  • data std::string: the bytes.
  • mode std::optional<int>: unix permission bits, e.g. 0755.

Throws: ActionError if the parent directory is missing.

Example:

sbx->files.write("/app/run.sh", "#!/bin/sh\necho hi\n", 0755);

files.list()

std::vector<FsEntry> list(const std::string& path)

Lists a directory’s immediate children. Not recursive.

Parameters:

  • path std::string: absolute directory path.

Returns: std::vector<FsEntry>, each with name, dir, size.

Throws: ActionError if the path is not a directory.

Example:

for (const auto& e : sbx->files.list("/app"))
  std::cout << (e.dir ? "d " : "- ") << e.name << " " << e.size << "\n";

files.stat()

FsStat stat(const std::string& path)

Metadata for one path.

Parameters:

  • path std::string: absolute in-guest path.

Returns: FsStat (name, dir, size, mode, modTimeMs).

Throws: ActionError if the path does not exist.

Example:

auto st = sbx->files.stat("/app/run.sh");
std::cout << st.size << " bytes, mode " << std::oct << st.mode << "\n";

files.mkdir()

void mkdir(const std::string& path)

Creates a directory.

Parameters:

  • path std::string: absolute directory path.

Example:

sbx->files.mkdir("/app/data");

files.remove()

void remove(const std::string& path, bool recursive = false)

Deletes a file or directory. Named remove(), since delete is a C++ keyword.

Parameters:

  • path std::string: absolute in-guest path.
  • recursive bool: required to delete a non-empty directory. Default false.

Example:

sbx->files.remove("/tmp/build", true);

files.rename()

void rename(const std::string& from, const std::string& to)

Moves or renames a path.

Parameters:

  • from, to std::string: absolute paths.

Example:

sbx->files.rename("/tmp/a.txt", "/tmp/b.txt");

CommandHandle

A started command. Returned by commands.start(); not constructed directly.

Properties

  • cmdId() const std::string&: the server-side command id.

Methods

onData()

void onData(std::function<void(const std::string& stream,
                               const std::string& data)> cb)

Subscribes to output chunks. Replays anything buffered before you subscribed, so no output is lost to a late registration.

Parameters:

  • cb: called with stream ("stdout" or "stderr") and data. Runs on the channel’s receive thread.

Example:

proc.onData([](const std::string& stream, const std::string& data) {
  (stream == "stderr" ? std::cerr : std::cout) << data;
});

wait()

int wait()

Blocks until the command exits and returns its exit code.

Returns: int, the exit code.

Throws: if the channel drops before the command exits.

Example:

int code = proc.wait();

stdin_write()

void stdin_write(const std::string& data)

Writes bytes to the command’s stdin. Named stdin_write, since stdin is a <cstdio> macro.

Parameters:

  • data std::string: bytes to write. Include your own newline.

Example:

auto proc = sbx->commands.start("cat");
proc.stdin_write("hello\n");
proc.kill();

kill()

void kill(int signal = 0)

Signals the command.

Parameters:

  • signal int: signal number. 0 means the server default.

Example:

proc.kill();     // server default
proc.kill(9);    // SIGKILL

GitOps

sbx->git: safe, non-shell git invocations over the command RPC, plus client-side parsing. Arguments are passed as an argv vector, never interpolated into a shell line.

Methods

clone()

void clone(const std::string& url, const GitCloneOptions& opts = {})

Clones a repository into the guest.

Parameters:

  • url std::string: the remote.
  • opts GitCloneOptions: path, branch, depth, username, password, cwd.

Throws: ActionError if git exits non-zero.

Example:

solari::GitCloneOptions o;
o.path = "/app/repo";
o.branch = "main";
o.depth = 1;
sbx->git.clone("https://github.com/org/repo.git", o);
Credentials are spliced into the URL
username + password are woven into the https remote by GitOps::authUrl(), so a token never reaches a shell history or a process listing. Use a PAT as the password.

status()

GitStatus status(std::optional<std::string> cwd = std::nullopt)

Parses git status --porcelain=v1 --branch into a struct.

Parameters:

  • cwd std::optional<std::string>: the repo directory.

Returns: GitStatus (branch, detached, ahead, behind, staged, modified, untracked, clean).

Example:

auto st = sbx->git.status("/app/repo");
std::cout << st.branch << " ahead " << st.ahead << " clean " << st.clean << "\n";

add()

void add(const std::vector<std::string>& paths,
         std::optional<std::string> cwd = std::nullopt)

Stages paths.

Parameters:

  • paths std::vector<std::string>: pathspecs, e.g. {"."}.
  • cwd std::optional<std::string>: the repo directory.

Example:

sbx->git.add({"."}, "/app/repo");

commit()

GitCommitResult commit(const std::string& message, const GitCommitOptions& opts = {})

Commits the index.

Parameters:

  • message std::string: the commit message.
  • opts GitCommitOptions: cwd, author, email, all (stage tracked files first).

Returns: GitCommitResult (hash).

Throws: ActionError, e.g. nothing to commit.

Example:

solari::GitCommitOptions o;
o.cwd = "/app/repo";
o.author = "CI Bot";
o.email = "ci@example.com";
o.all = true;
auto c = sbx->git.commit("add feature", o);
std::cout << c.hash << "\n";

push() / pull()

void push(const GitRemoteOptions& opts = {})
void pull(const GitRemoteOptions& opts = {})

Publishes or fetches-and-merges against a remote.

Parameters:

  • opts GitRemoteOptions: cwd, remote, branch, username, password.

Throws: ActionError on a rejected push or a merge conflict.

Example:

solari::GitRemoteOptions o;
o.cwd = "/app/repo";
o.remote = "origin";
o.branch = "main";
o.username = "git";
o.password = std::getenv("GITHUB_TOKEN");
sbx->git.push(o);

checkout()

void checkout(const std::string& ref, const GitCheckoutOptions& opts = {})

Switches to a branch, tag, or commit.

Parameters:

  • ref std::string: the target ref.
  • opts GitCheckoutOptions: cwd, create (-b).

Example:

solari::GitCheckoutOptions o;
o.cwd = "/app/repo";
o.create = true;
sbx->git.checkout("feature/x", o);

branches()

std::vector<GitBranch> branches(std::optional<std::string> cwd = std::nullopt)

Lists local branches.

Parameters:

  • cwd std::optional<std::string>: the repo directory.

Returns: std::vector<GitBranch>, each with name, commit, current.

Example:

for (const auto& b : sbx->git.branches("/app/repo"))
  std::cout << (b.current ? "* " : "  ") << b.name << " " << b.commit << "\n";

log()

std::vector<GitCommit> log(const GitLogOptions& opts = {})

Reads commit history.

Parameters:

  • opts GitLogOptions: cwd, maxCount.

Returns: std::vector<GitCommit>, each with hash, author, email, date, message.

Example:

solari::GitLogOptions o;
o.cwd = "/app/repo";
o.maxCount = 10;
for (const auto& c : sbx->git.log(o))
  std::cout << c.hash.substr(0, 7) << " " << c.message << "\n";

Pure helpers

Builders and parsers are exported so you can assert wire shapes with no network. GitOps itself takes an injected CommandRunner, so the whole git surface is testable against canned CommandResults.

FunctionPurpose
buildCreateBody(opts, kind)The POST /sandboxes body. Unset fields are omitted.
flattenCodeResult(reply, onStdout, onStderr)A raw code.run reply → RunCodeResult, charts flattened out of results.
parseChart(json)One Chart, elements kept raw.
GitOps::authUrl(url, username, password)Splices basic-auth credentials into an https remote.
HttpTransport::prepare(...)The concrete request bytes (method, url, headers, body) without sending.
newIdempotencyKey() / encodeURIComponent()A fresh UUID v4, and path-segment escaping.
base64Encode() / base64Decode() / trim()solari/util.hpp. The codec behind files.read/write. base64Encode takes std::string or std::vector<std::uint8_t>; base64Decode tolerates missing padding.
using namespace solari;

// GitOps against a canned runner. No gateway, no socket.
std::vector<std::string> seen;
GitOps git([&](const std::string& cmd, const CommandOptions& o) -> CommandResult {
  seen = o.args;
  return {0, "## main...origin/main\n", ""};
});

auto st = git.status("/app/repo");
CHECK(st.branch == "main");
CHECK(seen == std::vector<std::string>{"status", "--porcelain=v1", "--branch"});

Types

ClientOptions

  • apiKey std::string: required.
  • baseUrl std::string: required, no default.
  • callTimeoutMs long: default 300000.

CreateSandboxOptions

  • template_ std::optional<std::string>: e.g. "base". Trailing underscore: template is a C++ keyword.
  • cpu std::optional<int>: vCPUs.
  • memMb std::optional<int>: RAM in MiB.
  • diskGb std::optional<int>: disk in GiB.
  • envs std::map<std::string, std::string>: environment variables baked in at create.
  • metadata std::map<std::string, std::string>: opaque labels.
  • timeoutMs std::optional<long>: rolling idle window.
  • fromSnapshot std::optional<std::string>: restore from a snapshot id.
  • lifecycle std::optional<Lifecycle>: idle policy — onTimeout ("pause" or "kill") plus optional autoResume bool. "pause" is what resume() recovers from.
  • resolution std::optional<std::string>: initial display resolution, e.g. "1280x720". Desktops only — see VMs.
  • record std::optional<bool>: record the session server-side; the create response carries a presigned playback URL. Desktops only — rejected with 400 RecordingRequiresDesktop on a headless sandbox.
  • volumes std::vector<VolumeAttachment>: persistent volumes to mount before the session starts, each a {volumeId, path} pair (a vol_… id and the absolute in-guest mount point). Volumes are created/listed/deleted via client.http() or a TypeScript process — C++ only attaches one that already exists.

CreateSandboxResponse / SandboxView

  • CreateSandboxResponse: sandboxId, kind (default "sandbox"), controlUrl, expiresAt, streamUrl optional: parsed but unused; see VMs.
  • SandboxView: sandboxId, kind, state, expiresAt, controlUrl optional.

CommandOptions / CommandResult

  • args std::vector<std::string>: argv, not a shell line.
  • cwd, user std::optional<std::string>: working directory and run-as user.
  • env std::map<std::string, std::string>: per-command environment.
  • timeoutMs std::optional<long>: per-command cap.
  • background bool: detach instead of waiting.
  • onStdout, onStderr std::function<void(const std::string&)>: streaming callbacks.
  • CommandResult: exitCode int, stdout_, stderr_ std::string.

FsEntry / FsStat

  • FsEntry: name std::string, dir bool, size std::int64_t.
  • FsStat: the same, plus mode int and modTimeMs std::int64_t.

RunCodeResult / Chart

  • RunCodeResult: results std::vector<CodeResultItem>, charts std::vector<Chart>, error std::optional<Json>.
  • CodeResultItem: type (stdout | stderr | result), plus optional text, png, jpeg, svg, html, latex, markdown, json_, chart.
  • Chart: type (line | scatter | bar | pie | box_and_whisker | composite | unknown), title, xLabel, yLabel, x / y ChartAxis, elements Json.
  • ChartAxis: label, scale, ticks std::vector<Json>, present bool.

Errors

A typed hierarchy rooted at solari::Error (which extends std::runtime_error). Catch a specific type, or the base.

ClassStatusMeaning
GatewayErroranyBase for HTTP failures. Carries status, code, body.
AuthError401 / 403Missing or rejected API key.
PlanError402The plan does not allow this.
ConcurrencyLimitError429At the session cap. Not retried.
NoCapacityError503No host available. Retryable, and retried for you.
ActionErrorNoneA control-WS RPC returned {ok: false}. Carries method, code.
TimeoutErrorNoneAn RPC or connect exceeded its timeout. Carries method, timeoutMs.
ConnectionErrorNoneThe control WebSocket is not open.
try {
  auto sbx = client.create();
  sbx->connect();
} catch (const solari::ConcurrencyLimitError& e) {
  // 429 is NOT retried by the transport. Back off yourself.
  std::this_thread::sleep_for(std::chrono::seconds(2));
} catch (const solari::GatewayError& e) {
  std::cerr << e.status << " " << e.what() << "\n";
} catch (const solari::Error& e) {
  std::cerr << e.what() << "\n";
}
429 needs caller-side backoff
The transport retries idempotent requests (GET, DELETE, or anything carrying an Idempotency-Key) on network errors, 5xx, and bodies flagged retryable, with backoff min(150 × 2^n, 8000) + rand(0..250) ms, 5 retries by default. ConcurrencyLimitError (429) is not retried. Handle it yourself.