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 headercommands, 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: Client(), create(), connect(), get(), kill(), pause(), resume(), http()Sandbox: connect(), close(), connected(), kill(), pause(), resume(), commands.run(), commands.start(), code.run(), files.read(), files.write(), files.list(), files.stat(), files.mkdir(), files.remove(), files.rename()CommandHandle: onData(), wait(), stdin_write(), kill()GitOps: clone(), status(), add(), commit(), push() / pull(), checkout(), branches(), log()- Pure helpers:
buildCreateBody,flattenCodeResult,parseChart,GitOps::authUrl,HttpTransport::prepare - Types:
ClientOptions,CreateSandboxOptions,CreateSandboxResponse,SandboxView,CommandOptions,CommandResult,FsEntry,FsStat,RunCodeOptions,RunCodeResult,Chart, errors
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.apiKeystd::string: required.options.baseUrlstd::string: required, e.g.https://api.getsolari.com. There is no default.options.callTimeoutMslong: per-RPC timeout for handles this client creates. Default300000.
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:
optsCreateSandboxOptions: see CreateSandboxOptions.
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() 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:
sandboxIdstd::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 socketClient::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:
sandboxIdstd::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:
sandboxIdstd::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:
sandboxIdstd::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:
sandboxIdstd::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&: thewss://JSON-RPC control channel.expiresAt()const std::string&: ISO 8601 expiry.commandsCommands,filesFiles,codeCode,gitGitOps: 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() constWhether 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 falseresume()
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; }());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:
cmdstd::string: the program. Not a shell line.optsCommandOptions: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_;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:
cmdstd::string,optsCommandOptions: as commands.run().
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:
codestd::string: the source to run.opts.languagestd::optional<std::string>:python,javascript,typescript,bash, orr.opts.contextIdstd::optional<std::string>: reuse a kernel so variables persist across calls.opts.onStdout/opts.onStderrstd::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"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:
pathstd::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:
pathstd::string: absolute in-guest path.datastd::string: the bytes.modestd::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:
pathstd::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:
pathstd::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:
pathstd::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:
pathstd::string: absolute in-guest path.recursivebool: required to delete a non-empty directory. Defaultfalse.
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,tostd::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 withstream("stdout"or"stderr") anddata. 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:
datastd::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:
signalint: signal number.0means the server default.
Example:
proc.kill(); // server default
proc.kill(9); // SIGKILLGitOps
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:
urlstd::string: the remote.optsGitCloneOptions: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);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:
cwdstd::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:
pathsstd::vector<std::string>: pathspecs, e.g.{"."}.cwdstd::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:
messagestd::string: the commit message.optsGitCommitOptions: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:
optsGitRemoteOptions: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:
refstd::string: the target ref.optsGitCheckoutOptions: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:
cwdstd::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:
optsGitLogOptions: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.
| Function | Purpose |
|---|---|
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
apiKeystd::string: required.baseUrlstd::string: required, no default.callTimeoutMslong: default300000.
CreateSandboxOptions
template_std::optional<std::string>: e.g."base". Trailing underscore:templateis a C++ keyword.cpustd::optional<int>: vCPUs.memMbstd::optional<int>: RAM in MiB.diskGbstd::optional<int>: disk in GiB.envsstd::map<std::string, std::string>: environment variables baked in at create.metadatastd::map<std::string, std::string>: opaque labels.timeoutMsstd::optional<long>: rolling idle window.fromSnapshotstd::optional<std::string>: restore from a snapshot id.lifecyclestd::optional<Lifecycle>: idle policy —onTimeout("pause"or"kill") plus optionalautoResumebool."pause"is what resume() recovers from.resolutionstd::optional<std::string>: initial display resolution, e.g."1280x720". Desktops only — see VMs.recordstd::optional<bool>: record the session server-side; the create response carries a presigned playback URL. Desktops only — rejected with 400RecordingRequiresDesktopon a headless sandbox.volumesstd::vector<VolumeAttachment>: persistent volumes to mount before the session starts, each a{volumeId, path}pair (avol_…id and the absolute in-guest mount point). Volumes are created/listed/deleted viaclient.http()or a TypeScript process — C++ only attaches one that already exists.
CreateSandboxResponse / SandboxView
CreateSandboxResponse:sandboxId,kind(default"sandbox"),controlUrl,expiresAt,streamUrloptional: parsed but unused; see VMs.SandboxView:sandboxId,kind,state,expiresAt,controlUrloptional.
CommandOptions / CommandResult
argsstd::vector<std::string>: argv, not a shell line.cwd,userstd::optional<std::string>: working directory and run-as user.envstd::map<std::string, std::string>: per-command environment.timeoutMsstd::optional<long>: per-command cap.backgroundbool: detach instead of waiting.onStdout,onStderrstd::function<void(const std::string&)>: streaming callbacks.CommandResult:exitCodeint,stdout_,stderr_std::string.
FsEntry / FsStat
FsEntry:namestd::string,dirbool,sizestd::int64_t.FsStat: the same, plusmodeint andmodTimeMsstd::int64_t.
RunCodeResult / Chart
RunCodeResult:resultsstd::vector<CodeResultItem>,chartsstd::vector<Chart>,errorstd::optional<Json>.CodeResultItem:type(stdout|stderr|result), plus optionaltext,png,jpeg,svg,html,latex,markdown,json_,chart.Chart:type(line|scatter|bar|pie|box_and_whisker|composite|unknown),title,xLabel,yLabel,x/yChartAxis,elementsJson.ChartAxis:label,scale,ticksstd::vector<Json>,presentbool.
Errors
A typed hierarchy rooted at solari::Error (which extends std::runtime_error). Catch a specific type, or the base.
| Class | Status | Meaning |
|---|---|---|
GatewayError | any | Base for HTTP failures. Carries status, code, body. |
AuthError | 401 / 403 | Missing or rejected API key. |
PlanError | 402 | The plan does not allow this. |
ConcurrencyLimitError | 429 | At the session cap. Not retried. |
NoCapacityError | 503 | No host available. Retryable, and retried for you. |
ActionError | None | A control-WS RPC returned {ok: false}. Carries method, code. |
TimeoutError | None | An RPC or connect exceeded its timeout. Carries method, timeoutMs. |
ConnectionError | None | The 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";
}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.