Sandboxes
A sandbox is a machine for running code and automation, with no screen: run commands, execute code that keeps its state, watch files, and snapshot and fork. It works just like a VM, minus the graphical display.
Create and connect
npm install @solarisdk/sandboximport { SandboxClient } from "@solarisdk/sandbox"
const sandboxes = new SandboxClient({
apiKey: process.env.SOLARI_API_KEY!, // your Solari API key
baseUrl: "https://api.getsolari.com",
})
const sbx = await sandboxes.create({
template: "base",
cpu: 4, // number of CPUs, 1-16 (default 2)
memMb: 8192, // memory in MB, up to 65536 (default 2048)
timeoutMs: 15 * 60 * 1000, // pause after this much idle time
lifecycle: { onTimeout: "pause" }, // when idle: pause (default), or "kill"
})
await sbx.connect()Pick how many CPUs (cpu, from 1 to 16, default 2) and how much memory (memMb, from 2 GB up to 64 GB, default 2 GB) the sandbox has, same as a VM. The size sticks across snapshot, pause, and resume.
Run commands
commands.run runs a command to completion and returns { exitCode, stdout, stderr }; pass onStdout/onStderr to stream output live, and cwd/env to shape the environment.
const res = await sbx.commands.run("sh", {
args: ["-c", 'echo "$GREETING"'],
cwd: "/opt/app",
env: { GREETING: "hi" },
onStdout: (d) => process.stdout.write(d),
onStderr: (d) => process.stderr.write(d),
})
console.log("exit:", res.exitCode)For a long-running process, commands.start returns a handle with streamed output, stdin, and kill.
const proc = await sbx.commands.start("cat")
proc.onData((c) => process.stdout.write(`[${c.stream}] ${c.data}`))
await proc.stdin("echo me\n")
await proc.kill()Interactive PTY
Need a real terminal (colours, full-screen apps, a shell prompt)? pty.create opens one you can write to, resize, and read from. It's what powers the console's web terminal.
const term = await sbx.pty.create({ cols: 100, rows: 30 })
term.onData((bytes) => process.stdout.write(bytes))
await term.write("ls -la\n")
await term.resize(120, 40)Run code
runCode runs a snippet and remembers its state between calls, so variables and imports you set stick around. Output comes back in results: text from stdout and stderr, plus richer output like matplotlib figures as a base64 PNG on results[i].png.
await sbx.runCode("import numpy as np; x = np.arange(10)", { language: "python" })
const out = await sbx.runCode("print(x.sum())", { language: "python" })
console.log(out.results, "error:", out.error ?? "none")
// Matplotlib figures come back as base64 PNG result items.
const fig = await sbx.runCode("import matplotlib.pyplot as plt; plt.plot(x); plt.show()")
const png = fig.results.find((r) => r.png)?.png // base64-encoded PNGStructured charts
Alongside the PNG, a matplotlib figure also comes back as structured data on results[i].chart, and every chart in a run is collected on out.charts for convenience. Instead of an image you can't read, you get the chart's type (line, scatter, bar, pie, or composite), its title and axis labels, and the underlying series. That makes it easy for feeding a figure straight to an agent or re-rendering it yourself.
const r = await sbx.runCode(`
import matplotlib.pyplot as plt
plt.bar(["a", "b", "c"], [3, 7, 5])
plt.title("Scores"); plt.xlabel("group"); plt.ylabel("count")
plt.show()
`, { language: "python" })
const chart = r.charts[0]
console.log(chart.type) // "bar"
console.log(chart.title) // "Scores"
console.log(chart.x?.ticks) // ["a", "b", "c"]
console.log(chart.y?.label) // "count"
console.log(chart.elements) // the raw series valuesresults[i].png. The structured chart / out.charts view is extracted by the guest and may be empty on some templates; when it is, fall back to the PNG.Version control
The git namespace wraps common Git operations so you don't have to shell out and parse porcelain yourself: clone, status, add, commit, push/pull, checkout, branches, and log all return typed results. Credentials are passed per-call and never touch a shell, so there is nothing to escape or inject.
await sbx.git.clone("https://github.com/psf/requests", {
path: "/work/requests",
depth: 1,
})
const status = await sbx.git.status("/work/requests") // cwd
console.log(status.branch, status.clean) // "main" true
await sbx.files.write("/work/requests/NOTE.md", "hi")
await sbx.git.add(["NOTE.md"], "/work/requests") // paths, cwd
const { hash } = await sbx.git.commit("Add a note", {
cwd: "/work/requests",
author: "Agent", email: "agent@example.com",
})
const log = await sbx.git.log({ cwd: "/work/requests", maxCount: 1 })
console.log(log[0].hash === hash, log[0].message) // true "Add a note"Branch and publish the same way. Pass a username / password (a personal-access token) to push or pull and the credential is spliced into the remote URL for that one invocation only. It is never written to the repo or global Git config, so nothing is left behind on the machine.
await sbx.git.checkout("feature/notes", {
cwd: "/work/requests",
create: true, // -b: start a new branch
})
await sbx.git.push({
cwd: "/work/requests",
branch: "feature/notes",
username: "agent",
password: process.env.GITHUB_TOKEN, // one-off auth, not persisted
})Files
The files surface reads and writes guest files directly, plus list, search, watch, and upload/download of larger blobs.
await sbx.files.write("/tmp/hello.txt", "world")
const text = await sbx.files.readText("/tmp/hello.txt")
const entries = await sbx.files.list("/tmp")
const hits = await sbx.files.search("/tmp", "world")
// Stream filesystem events until you call the returned stop().
const stop = await sbx.files.watch("/tmp", (ev) =>
console.log("fs:", ev.type, ev.path),
)
await stop()Expose a port
previewUrl(port) gives you a public URL for something the sandbox is serving on that port: a dev server, an API, anything. Available where a preview domain is set up.
const preview = await sbx.previewUrl(3000)
console.log("preview:", preview.url)Snapshots
Save a prepared sandbox and start ready-to-go copies from it, or reset one back to that saved state between runs. See Snapshots for the full flow.
const snapId = await sbx.snapshot("after-setup")
const fork = await sandboxes.create({ template: "base", fromSnapshot: snapId })Idle timeout
Like VMs, a sandbox stays running as long as you're using it. Set timeoutMs to say how long it can sit idle; every action and open connection resets the clock. When that time passes with no activity, lifecycle decides what happens: onTimeout: "pause" (the default) saves its state and parks it, while onTimeout: "kill" shuts it down. If you don't set timeoutMs, it defaults to 30 minutes.
await sbx.setTimeout(15 * 60 * 1000) // change how long it can sit idle (ms)
await sbx.pause() // pause it, state is saved, resumes on connect()Tear down
await sbx.kill()