Sandboxes
github.com/solari-sdk/solari-sandbox-go, package solari. Types Client and Sandbox. A sandbox is a headless microVM for commands, code, files, and git. See the Go SDK hub for install and configuration.
go get github.com/solari-sdk/solari-sandbox-go@solarisdk/core surface: session lifecycle (including Pause/Resume) plus Commands, Files, Code, and Git. PTYs, full volume CRUD, snapshots, metrics, signed transfer URLs, and preview URLs are not ported. See what is not ported.Contents
Client: NewClient(), Create(), Get(), Connect(), Kill(), Pause(), Resume()Sandbox: Connect(), Reconnect(), Connected(), Close(), Pause(), Resume(), Kill()Commands: Run(), Start(), CommandHandleFiles: Read(), ReadText(), Write(), List(), Stat(), Mkdir(), Remove(), Rename()Code: Run(), CreateContext()Git: Clone(), Status(), Add(), Commit(), Push(), Pull(), Checkout(), Branches(), Log()- Types:
ClientOptions,CreateOptions,SandboxView,CommandOptions,CommandResult,FsEntry,FsStat,RunCodeOptions,RunCodeResult,Chart,GitStatus,GitBranch,GitCommit, errors
Client
The sandbox entry point. Talks the gateway REST API.
Constructor
NewClient()
func NewClient(opts ClientOptions) (*Client, error)Constructs a client. Opens no connection.
Parameters:
optsClientOptions: see ClientOptions.
Returns: (*Client, error)
Errors: *SolariError if APIKey or BaseURL is empty.
Example:
import solari "github.com/solari-sdk/solari-sandbox-go"
client, err := solari.NewClient(solari.ClientOptions{
APIKey: os.Getenv("SOLARI_API_KEY"),
BaseURL: "https://api.getsolari.com",
})Methods
Create()
func (c *Client) Create(ctx context.Context, opts CreateOptions) (*Sandbox, error)Provisions a sandbox (POST /sandboxes) and returns a handle. Retry-safe, since it sends an idempotency key. The control channel is not opened yet.
Parameters:
ctxcontext.Context.optsCreateOptions: see CreateOptions.Kinddefaults toKindSandbox.
Returns: (*Sandbox, error)
Errors: *AuthError, *PlanError, *ConcurrencyLimitError, *NoCapacityError, or *GatewayError.
Example:
sbx, err := client.Create(ctx, solari.CreateOptions{
Template: "base",
CPU: 2,
MemMb: 4096,
Envs: map[string]string{"NODE_ENV": "test"},
})
if err != nil {
log.Fatal(err)
}
defer sbx.Kill(ctx)Get()
func (c *Client) Get(ctx context.Context, sandboxID string) (*SandboxView, error)Fetches a sandbox’s current record (GET /sandboxes/:id). Returns the raw view, not a handle.
Parameters:
ctxcontext.Context.sandboxIDstring: the sandbox id.
Returns: (*SandboxView, error)
Errors: *GatewayError if the id is unknown.
Example:
view, _ := client.Get(ctx, "sbx_abc123")
fmt.Println(view.State, view.CPU, view.MemMb) // "running" 2 4096Connect()
func (c *Client) Connect(ctx context.Context, sandboxID string) (*Sandbox, error)Re-attaches to a running sandbox by id. When the view carries no ControlURL, one is derived by swapping the base URL scheme to ws/wss and appending /control/<id>.
Parameters:
ctxcontext.Context.sandboxIDstring: the sandbox id.
Returns: (*Sandbox, error)
Errors: *GatewayError if the id is unknown.
Example:
sbx, err := client.Connect(ctx, "sbx_abc123")
r, _ := sbx.Commands.Run(ctx, "uptime", solari.CommandOptions{})Kill()
func (c *Client) Kill(ctx context.Context, sandboxID string) errorDestroys a sandbox by id (DELETE /sandboxes/:id). Idempotent. Use Sandbox.Kill when you hold a handle.
Parameters:
ctxcontext.Context.sandboxIDstring: the sandbox id.
Returns: error
Example:
err := client.Kill(ctx, "sbx_abc123")Pause()
func (c *Client) Pause(ctx context.Context, sandboxID string) errorSnapshots a session’s RAM+disk and frees its host slot (POST /sandboxes/:id/pause). The session keeps its id and can be brought back with Resume; its control channel is dead until then. Prefer Sandbox.Pause when you hold a handle — it also closes the local channel.
Parameters:
ctxcontext.Context.sandboxIDstring: the sandbox id.
Returns: error
Example:
err := client.Pause(ctx, "sbx_abc123")Resume()
func (c *Client) Resume(ctx context.Context, sandboxID string) (string, error)Re-hydrates a paused session (POST /sandboxes/:id/resume) and returns the control URL to re-attach to. The session comes back on a fresh slot, so any control URL from before the pause is stale.
Parameters:
ctxcontext.Context.sandboxIDstring: the sandbox id.
Returns: (string, error), the fresh control URL.
Example:
controlURL, err := client.Resume(ctx, "sbx_abc123")Sandbox
A live session handle. Construct via Client.Create or Client.Connect.
Fields
IDstring: the session id.ControlURLstring: thewss://JSON-RPC control channel.ExpiresAtstring: ISO 8601 expiry.KindSandboxKind:KindSandboxorKindDesktop.Commands*Commands,Files*Files,Code*Code,Git*Git: the namespaces.
Channel
Connect()
func (s *Sandbox) Connect(ctx context.Context) errorOpens the control WebSocket. Idempotent. Only Commands.Run can avoid it. Every other namespace requires the channel, so connect first.
Parameters:
ctxcontext.Context: bounds the dial.
Returns: error
Errors: *ConnectionError if the dial fails; *TimeoutError with method "connect" after 15s.
Example:
if err := sbx.Connect(ctx); err != nil {
log.Fatal(err)
}Reconnect()
func (s *Sandbox) Reconnect(ctx context.Context) errorRe-opens the control channel after a drop.
Parameters:
ctxcontext.Context.
Returns: error
Example:
if !sbx.Connected() {
err := sbx.Reconnect(ctx)
}Connected()
func (s *Sandbox) Connected() boolReports whether the control channel is currently open.
Returns: bool
Example:
fmt.Println(sbx.Connected())Close()
func (s *Sandbox) Close()Closes the control channel locally. Does not release the remote session. Use Kill for that.
Returns: nothing.
Example:
sbx.Close() // session keeps running until its timeoutPause()
func (s *Sandbox) Pause(ctx context.Context) errorSnapshots this session’s RAM+disk, frees its host slot, and closes the control channel locally. The session keeps its id; bring it back with Resume.
Parameters:
ctxcontext.Context.
Returns: error
Example:
if err := sbx.Pause(ctx); err != nil {
log.Fatal(err)
}Resume()
func (s *Sandbox) Resume(ctx context.Context) errorRe-hydrates this paused session and re-points the control channel at the fresh slot it came back on: updates ControlURL and reconnects.
Parameters:
ctxcontext.Context.
Returns: error
Example:
if err := sbx.Resume(ctx); err != nil {
log.Fatal(err)
}
r, _ := sbx.Commands.Run(ctx, "echo", solari.CommandOptions{Args: []string{"back"}})Kill()
func (s *Sandbox) Kill(ctx context.Context) errorDestroys the remote session and closes the channel. Idempotent. The channel is closed even when the delete fails.
Parameters:
ctxcontext.Context.
Returns: error
Example:
sbx, err := client.Create(ctx, solari.CreateOptions{Template: "base"})
if err != nil {
log.Fatal(err)
}
defer sbx.Kill(ctx)Commands
The process-execution namespace. Reach it at sandbox.Commands.
Run()
func (c *Commands) Run(ctx context.Context, cmd string, opts CommandOptions) (*CommandResult, error)Executes a command to completion and returns its exit code and captured output. OnStdout/OnStderr, when set, receive output as it streams.
Parameters:
ctxcontext.Context.cmdstring: the program to run.optsCommandOptions: see CommandOptions.
Returns: (*CommandResult, error), carrying ExitCode, Stdout, Stderr. A non-zero exit returns a result, not an error.
Errors: *ActionError, *TimeoutError, *ConnectionError, or a gateway error from the fast path.
Example:
r, err := sbx.Commands.Run(ctx, "ls", solari.CommandOptions{
Args: []string{"-la", "/tmp"},
})
fmt.Println(r.ExitCode, r.Stdout)
// stream output as it arrives (needs the control channel)
_, err = sbx.Commands.Run(ctx, "npm", solari.CommandOptions{
Args: []string{"install"},
Cwd: "/app",
OnStdout: func(s string) { fmt.Print(s) },
})cmd runs directly with Args, not through a shell. Pipes, globs, and && will not expand. For shell syntax use Run(ctx, "sh", CommandOptions{Args: []string{"-c", "…"}}).POST /sandboxes/:id/exec, skipping the cold WS handshake. The fast path is used only when Background is false and OnStdout, OnStderr, Env, and User are all unset. It falls back to the control channel only when the route is unserved (404/405/501). A real command failure propagates, so nothing double-executes.Start()
func (c *Commands) Start(ctx context.Context, cmd string, opts CommandOptions) (*CommandHandle, error)Launches a command and returns a handle immediately, without waiting for exit. Requires the control channel.
Parameters:
ctxcontext.Context.cmdstring: the program to run.optsCommandOptions:TimeoutMsandBackgroundare ignored here.
Returns: (*CommandHandle, error). See CommandHandle.
Errors: *ConnectionError if the channel is not open; *ActionError if the guest rejects the start.
Example:
h, err := sbx.Commands.Start(ctx, "python3", solari.CommandOptions{
Args: []string{"-u", "worker.py"},
})
h.OnData(func(stream, data string) { fmt.Print(data) })
h.Stdin(ctx, []byte("input\n"))
code, err := h.Wait(ctx)CommandHandle
A started command from Start.
CmdIDstring: the command id.
func (h *CommandHandle) OnData(cb func(stream, data string))
func (h *CommandHandle) Wait(ctx context.Context) (int, error)
func (h *CommandHandle) Stdin(ctx context.Context, data []byte) error
func (h *CommandHandle) Kill(ctx context.Context, signal int) errorOnData: subscribes tostdout/stderrchunks. Output buffered before the first subscriber is replayed, so early output is never dropped.Wait: blocks until exit and returns the exit code. Also returns when the channel drops orctxis cancelled.Stdin: writes bytes to the command’s stdin.Kill: signals the process.signal≤ 0 means SIGTERM.
Files
The filesystem namespace (fs.* RPCs), moving bytes over the control channel. Reach it at sandbox.Files.
Read()
func (f *Files) Read(ctx context.Context, path string) ([]byte, error)Returns the raw bytes of an in-guest file.
Parameters:
ctxcontext.Context.pathstring: absolute in-guest path.
Returns: ([]byte, error)
Errors: *ActionError if the path does not exist; *ConnectionError if the channel is not open.
Example:
b, err := sbx.Files.Read(ctx, "/tmp/out.bin")ReadText()
func (f *Files) ReadText(ctx context.Context, path string) (string, error)Read decoded as UTF-8 text.
Parameters:
ctxcontext.Context.pathstring: absolute in-guest path.
Returns: (string, error)
Example:
host, err := sbx.Files.ReadText(ctx, "/etc/hostname")Write()
func (f *Files) Write(ctx context.Context, path string, data []byte, mode int) errorWrites bytes to an in-guest path, creating or truncating it.
Parameters:
ctxcontext.Context.pathstring: absolute in-guest path.data[]byte: content.modeint: unix permission bits.0means unset and is omitted from the wire.
Returns: error
Example:
err := sbx.Files.Write(ctx, "/app/run.sh", []byte("#!/bin/sh\necho hi\n"), 0o755)List()
func (f *Files) List(ctx context.Context, path string) ([]FsEntry, error)Returns a directory’s entries (non-recursive).
Parameters:
ctxcontext.Context.pathstring: directory path.
Returns: ([]FsEntry, error), each with Name, Dir, Size.
Example:
entries, _ := sbx.Files.List(ctx, "/app")
for _, e := range entries {
fmt.Println(e.Name, e.Dir, e.Size)
}Stat()
func (f *Files) Stat(ctx context.Context, path string) (*FsStat, error)Returns metadata for a single path.
Parameters:
ctxcontext.Context.pathstring: file or directory path.
Returns: (*FsStat, error), carrying Name, Dir, Size, Mode, ModTimeMs.
Example:
st, _ := sbx.Files.Stat(ctx, "/app/run.sh")
fmt.Println(st.Size, strconv.FormatInt(int64(st.Mode), 8), st.ModTimeMs)Mkdir()
func (f *Files) Mkdir(ctx context.Context, path string) errorCreates a directory, and any missing parents.
Parameters:
ctxcontext.Context.pathstring: directory to create.
Returns: error
Example:
err := sbx.Files.Mkdir(ctx, "/app/data")Remove()
func (f *Files) Remove(ctx context.Context, path string, recursive bool) errorDeletes a file or directory.
Parameters:
ctxcontext.Context.pathstring: path to delete.recursivebool: required for a non-empty directory.
Returns: error
Example:
err := sbx.Files.Remove(ctx, "/tmp/build", true)Rename()
func (f *Files) Rename(ctx context.Context, from, to string) errorRenames or moves a path.
Parameters:
ctxcontext.Context.from,tostring: source and destination.
Returns: error
Example:
err := sbx.Files.Rename(ctx, "/tmp/a.txt", "/tmp/b.txt")Code
The stateful-kernel namespace. Reach it at sandbox.Code.
Run()
func (c *Code) Run(ctx context.Context, code string, opts RunCodeOptions) (*RunCodeResult, error)Executes code in a stateful kernel. Rich outputs (PNG, HTML, JSON, structured charts) come back as Results items.
Parameters:
ctxcontext.Context.codestring: source to execute.optsRunCodeOptions:Language,ContextID,OnStdout,OnStderr.
Returns: (*RunCodeResult, error), carrying Results, Charts, Error. A runtime error in the executed code lands in Error; it is not returned as a Go error.
Errors: *ActionError, *TimeoutError, *ConnectionError.
Example:
r, err := sbx.Code.Run(ctx, `
import matplotlib.pyplot as plt
plt.plot([1, 2, 3], [4, 5, 6])
plt.title("demo")
plt.show()
`, solari.RunCodeOptions{})
fmt.Println(r.Charts[0].Type) // "line"
fmt.Println(r.Charts[0].Title) // "demo"CreateContext()
func (c *Code) CreateContext(ctx context.Context, language string) (string, error)Creates a fresh kernel context so state persists across Run calls, like a REPL.
Parameters:
ctxcontext.Context.languagestring: empty defaults to"python".
Returns: (string, error), the context id.
Example:
cid, _ := sbx.Code.CreateContext(ctx, "python")
sbx.Code.Run(ctx, "x = 41", solari.RunCodeOptions{ContextID: cid})
r, _ := sbx.Code.Run(ctx, "print(x + 1)", solari.RunCodeOptions{ContextID: cid}) // 42Git
Every method is a safe, non-shell git invocation over the command RPC, with client-side parsing and no injection surface. Reach it at sandbox.Git.
Clone()
func (g *Git) Clone(ctx context.Context, rawURL string, opts GitCloneOptions) errorClones a repository into the guest.
Parameters:
ctxcontext.Context.rawURLstring: remote URL.optsGitCloneOptions:Path,Branch,Depth,Username,Password,Cwd.
Returns: error
Errors: a plain error carrying the subcommand, exit code, and git’s stderr on a non-zero exit.
Example:
err := sbx.Git.Clone(ctx, "https://github.com/org/repo.git", solari.GitCloneOptions{
Path: "/app/repo",
Branch: "main",
Depth: 1,
})Username/Password are URL-encoded into the remote for this one command and never written to the repo config. A non-http(s) remote (ssh, scp-style) is passed through unchanged.Status()
func (g *Git) Status(ctx context.Context, cwd string) (*GitStatus, error)Parsed working-tree status.
Parameters:
ctxcontext.Context.cwdstring: repository directory; empty uses the session default.
Returns: (*GitStatus, error). See GitStatus.
Errors: a plain error on a non-zero exit.
Example:
st, _ := sbx.Git.Status(ctx, "/app/repo")
fmt.Println(st.Branch, st.Clean, st.Ahead, st.Modified)Add()
func (g *Git) Add(ctx context.Context, paths []string, cwd string) errorStages paths. An empty slice is a no-op; a -- separator guards paths that look like flags.
Parameters:
ctxcontext.Context.paths[]string: use[]string{"."}for everything.cwdstring: repository directory.
Returns: error
Example:
err := sbx.Git.Add(ctx, []string{"."}, "/app/repo")Commit()
func (g *Git) Commit(ctx context.Context, message string, opts GitCommitOptions) (string, error)Commits staged changes and returns the new hash. Set Author/Email, because an ephemeral session has no git identity.
Parameters:
ctxcontext.Context.messagestring: commit message.optsGitCommitOptions:Cwd,Author,Email(scoped to this commit only),All.
Returns: (string, error), the commit hash, read back with rev-parse HEAD.
Errors: a plain error on a non-zero exit.
Example:
hash, err := sbx.Git.Commit(ctx, "add feature", solari.GitCommitOptions{
Cwd: "/app/repo",
Author: "CI Bot",
Email: "ci@example.com",
All: true,
})Push()
func (g *Git) Push(ctx context.Context, opts GitRemoteOptions) errorPushes to a remote. Defaults to origin and the current branch.
Parameters:
ctxcontext.Context.optsGitRemoteOptions:Cwd,Remote,Branch,Username,Password.
Returns: error
Errors: a plain error on a non-zero exit.
Example:
err := sbx.Git.Push(ctx, solari.GitRemoteOptions{
Cwd: "/app/repo",
Username: "x-access-token",
Password: os.Getenv("GITHUB_TOKEN"),
})-c url.<authed>.insteadOf=<plain>. Nothing is persisted.Pull()
func (g *Git) Pull(ctx context.Context, opts GitRemoteOptions) errorPulls from a remote. Same options and credential handling as Push.
Parameters:
ctxcontext.Context.optsGitRemoteOptions:Cwd,Remote,Branch,Username,Password.
Returns: error
Example:
err := sbx.Git.Pull(ctx, solari.GitRemoteOptions{Cwd: "/app/repo"})Checkout()
func (g *Git) Checkout(ctx context.Context, ref string, cwd string, create bool) errorChecks out an existing ref, or creates a branch.
Parameters:
ctxcontext.Context.refstring: branch, tag, or commit.cwdstring: repository directory.createbool: create the branch (-b).
Returns: error
Example:
err := sbx.Git.Checkout(ctx, "feature/x", "/app/repo", true)Branches()
func (g *Git) Branches(ctx context.Context, cwd string) ([]GitBranch, error)Lists local branches.
Parameters:
ctxcontext.Context.cwdstring: repository directory.
Returns: ([]GitBranch, error), each with Name, Commit (short), Current.
Example:
branches, _ := sbx.Git.Branches(ctx, "/app/repo")
for _, b := range branches {
fmt.Println(b.Current, b.Name, b.Commit)
}Log()
func (g *Git) Log(ctx context.Context, opts GitLogOptions) ([]GitCommit, error)Recent commits, newest first.
Parameters:
ctxcontext.Context.optsGitLogOptions:Cwd,MaxCount.
Returns: ([]GitCommit, error), each with Hash, Author, Email, Date, Message.
Example:
commits, _ := sbx.Git.Log(ctx, solari.GitLogOptions{Cwd: "/app/repo", MaxCount: 10})Not ported to Go
These exist in the TypeScript SDK but have no Go equivalent. Reach them over the HTTP API.
| Missing | TypeScript equivalent |
|---|---|
| PTYs | pty.create() |
| Listing and filtering sessions | list(), listAll() |
| Snapshots | snapshot(), revert(), listSnapshots(), promoteSnapshot() |
| Volume create/list/delete | volumes.* (attach-at-create is ported — see CreateOptions.Volumes) |
| Timeout extension | setTimeout() |
| Signed transfer + preview URLs | downloadUrl(), uploadUrl(), previewUrl() |
| Metrics, session env, file search/watch | metrics(), env(), files.search(), files.watch() |
Types
ClientOptions
APIKeystring: required. Authenticates REST requests and the control-WS upgrade.BaseURLstring: required. The gateway origin.HTTPClient*http.Client: override for tests.CallTimeoutMsint: per-call control-WS RPC timeout. Default300000.MaxRetriesint: default5.RetryDelayMs*int: when non-nil, a fixed delay instead of exponential backoff.0disables the wait.
CreateOptions
Zero and nil fields are omitted from the wire body.
Templatestring: e.g."base","code", or a promoted template id.KindSandboxKind:KindSandbox(default) orKindDesktop.CPUint,MemMbint,DiskGbint: resources; the host clamps to slot capacity.Envsmap[string]string: per-session environment.Metadatamap[string]string: opaque labels.TimeoutMsint: idle window before auto-release.FromSnapshotstring: boot from a snapshot instead of the template.Lifecycle*SandboxLifecycle:OnTimeout("pause"/"kill") andAutoResume*bool.Resolutionstring: initial display resolution, e.g."1280x720". Desktops only (KindDesktop) — a headless sandbox has no display.Record*bool: ask the gateway to record the session server-side; the create response carries a presigned playback URL. Desktops only — rejected (400RecordingRequiresDesktop) on a headless sandbox. A pointer so an explicitfalseis distinguishable from unset.Volumes[]VolumeAttachment: persistent volumes to mount before the session starts, each withVolumeID(avol_…id) andPath. Attaching at create time is ported; creating, listing, and deleting volumes is not (see what is not ported).
SandboxView
SandboxIDstring,KindSandboxKind,Statestring.Metadatamap[string]string,ExpiresAtstring.ControlURLstring: may be empty; Connect derives one when it is.CPUint,MemMbint.
CommandOptions
Args[]string: the argv tail. No shell expansion.Cwdstring,Userstring.Envmap[string]string: per-command environment.TimeoutMsint: bounds the one-shot exec fast path, server-side.Backgroundbool: Run returns immediately with an empty result; output still streams to the callbacks.OnStdout,OnStderrfunc(string): streamed output.
CommandResult
ExitCodeint,Stdoutstring,Stderrstring.
FsEntry / FsStat
FsEntry:Namestring,Dirbool,Sizeint64.FsStat:Name,Dir,Size, plusModeint (permission bits) andModTimeMsint64 (unix millis).
RunCodeOptions / RunCodeResult
RunCodeOptions:Languagestring ("python","javascript","typescript","bash","r"),ContextIDstring,OnStdout,OnStderrfunc(string).RunCodeResult.Results[]CodeResultItem: each hasType(stdout/stderr/result) plus any ofText,PNG,JPEG,SVG,HTML,LaTeX,JSON,Markdown,Chart.RunCodeResult.Charts[]Chart: every present chart acrossResults, flattened client-side.RunCodeResult.Errorinterface{}: a*CodeError(Name,Message,Traceback) or a string; kept raw so both wire shapes round-trip.
Chart
TypeChartType:ChartLine,ChartScatter,ChartBar,ChartPie,ChartBoxAndWhisker,ChartComposite,ChartUnknown.Title,XLabel,YLabelstring.X,Y*ChartAxis:Label,Ticks,Scale.Elements[]interface{}: per-type data (points, bars, slices), kept loose so new chart types need no SDK bump.
GitStatus
Branchstring: empty on a detached HEAD.Detachedbool,Cleanbool.Ahead,Behindint: relative to the upstream, when set.Staged,Modified,Untracked[]string.
GitBranch / GitCommit
GitBranch:Namestring,Commitstring (short),Currentbool.GitCommit:Hash,Author,Email,Date(ISO 8601),Messagestring.
Errors
Every error embeds *SolariError directly or transitively. Match broadly on *SolariError or narrowly on a concrete type with errors.As. Gateway errors also carry Status, Code, and Body.
| Type | Status | Meaning |
|---|---|---|
*AuthError | 401, 403 | API key missing, malformed, or rejected. |
*PlanError | 402 | The plan does not allow this. |
*ConcurrencyLimitError | 429 | Too many live sessions. Not retryable. |
*NoCapacityError | 503 | No host available right now. Retryable. |
*GatewayError | other | Any other non-2xx response. |
*ActionError | None | An RPC returned ok: false. Carries Method, Code. |
*TimeoutError | None | No reply within the deadline. Carries Method (or "connect") and TimeoutMs. |
*ConnectionError | None | The control channel is not open, or the dial failed. |
_, err := client.Create(ctx, solari.CreateOptions{Template: "base"})
var capErr *solari.ConcurrencyLimitError
var noCap *solari.NoCapacityError
switch {
case errors.As(err, &noCap):
retryLater()
case errors.As(err, &capErr):
waitForSlot()
case err != nil:
log.Fatal(err)
}