Skip to main content

What you can do

Everything here is a flat method directly on *Client or *Sandbox — there is no client.Sandboxes.Create(...)-style namespace object to navigate. client.CreateSandbox(ctx, template), client.GetSandbox(ctx, id), client.ListSandboxes(ctx), client.DeleteSandbox(ctx, id), client.ResumeSandbox(ctx, id) are the whole sandbox-lifecycle surface on *Client. Once you’re holding a *Sandbox, sandbox.CreateSession(...), sandbox.CreatePreview(...), sandbox.Snapshot(...), and the file methods below are flat methods on it directly.
*Client also has session/preview convenience methods that take a sandbox id directly instead of a *Sandbox — client.CreateSession(ctx, sandboxID, name, cmd), client.ListSessions(ctx, sandboxID), client.CreatePreview(ctx, sandboxID, port), and similar. Each does a GetSandbox first and then delegates, which is one extra round trip compared to already holding a *Sandbox handle. Prefer calling directly on sandbox when you have one in hand; reach for a client-level call only when all you have is an id.

Create sandboxes

CreateSandbox(ctx, template, opts...) returns a *Sandbox. WaitReady asks the service to return a running sandbox — LastObservedStatus == StatusRunning. WaitNone returns as soon as the service accepts the request — LastObservedStatus == StatusCreating. If create exhausts its deadline, the SDK returns a *SandboxCreationTimeoutError, carrying the IdempotencyKey used so you can retry the same creation rather than starting a second sandbox. Sandbox fields: sandbox.ID, .OperationID, .Template, .Version, .LastObservedStatus.

Get, list, and get by name

GetSandbox(ctx, sandboxID) requires a non-empty id and returns a usable *Sandbox handle. It does not explicitly resume the sandbox — exec and filesystem calls are what wake a suspended one.
Unlike the TypeScript and Python SDKs, Go’s ListSandboxes returns a plain []SandboxSummary slice, not a lazy iterator — Go has no generator protocol as natural as Python’s iterators or JS’s async iterables, so the Go SDK exposes a single “collect all matching, up to Limit” call and pages internally. ListOptions.Limit bounds how much work ListSandboxes does; Limit: 0 (the default) fetches every matching sandbox.
ListOptions.Name filters to sandboxes carrying an exact display name (names aren’t unique, so this can still match more than one). Valid States filters are StatusCreating, StatusRunning, StatusSuspending, StatusSuspended, StatusResuming, and StatusFailed — StatusDeleted is not a valid filter. A SandboxSummary has ID, OperationID, Template, Version, LastObservedStatus, FailureCode, FailureMessage — it cannot run Exec; call GetSandbox(ctx, summary.ID) first. GetSandboxByName(ctx, name) resolves a display name to a single sandbox. Names are not unique: if more than one sandbox carries the name, it returns an error rather than picking one (a later Delete could otherwise destroy the wrong sandbox) — use ListSandboxes with ListOptions{Name: ...} and decide yourself, which is what the bonya CLI does when it prompts.

Delete and cleanup

sandbox.Delete(ctx) returns *DeleteResult{SandboxID, AlreadyDeleted}. Calling it again on the same *Sandbox is local and idempotent — no second RPC.

Resume

sandbox.Resume(ctx, opts...) returns *ResumeResult and updates the sandbox’s private exec endpoint/capability, setting LastObservedStatus to StatusRunning. Resume on a failed sandbox returns a *SandboxFailedError locally, before any RPC. Explicit resume is different from automatic wake: GetSandbox never calls Resume, and ordinary exec/filesystem calls don’t make a public ResumeSandbox RPC — they hit the sandbox’s guest endpoint directly, and the service may wake it behind that route.

Exec vs ExecStream

Exec buffers output and returns once the process exits — good for short, bounded commands. ExecStream yields events as they happen — use it for large output, long-running commands, interactive stdin, or cancellation. Both are one-shot: the remote process ends when the call (or stream) ends. This is different from managed sessions, which keep running after you disconnect.

Buffered exec

command accepts either a string (run as ["/bin/sh", "-c", command]) or a []string (run directly; must be non-empty with no empty entries).
Env overlays string env vars (keys can’t contain = or NUL). Cwd sets the remote working directory. *ExecResult exposes StdoutBytes/StderrBytes ([]byte), Stdout()/Stderr() (UTF-8 with replacement for invalid bytes), ExitCode, Signaled, Signal, OK() (ExitCode == 0 && !Signaled), and String() (same as Stdout()). ExecOptions.Check: true calls result.Check() before returning; on a non-ok result, Exec returns *ExecFailedError, with the original result at error.Result:
Exec buffers stdout and stderr in client memory. Use ExecStream for large output, long-running commands, interactive stdin, or cancellation.

Streaming exec

command, Env, Cwd, TTY, Cols, Rows, and Timeout follow the same rules as buffered Exec. Call session.Next() repeatedly to receive Stdout, Stderr, and Exit events until it returns (nil, nil). Write stdin with session.Write(data []byte) — returns *InvalidRequestError after the session or stdin is closed. session.CloseStdin() is idempotent. session.Cancel() is idempotent and sends a cancel frame when possible. session.Close() cancels an unfinished session — call it via defer. Internally the SDK uses a background reader goroutine feeding a buffered event channel per WebSocket connection. If Next reaches the session deadline before the next event, the SDK cancels the remote exec and returns *TimeoutError.

TTY exec

Set TTY: true for terminal semantics — stdout and stderr share the terminal stream, so the SDK returns terminal output as Stdout() and leaves Stderr() empty. Default dimensions are 80x24 (sent on the wire as cols=0, rows=0). Provide both Cols and Rows together to override — each must be an integer from 1 through 512, and both require TTY: true. Buffered Input isn’t allowed with TTY: true; use ExecStream and session.Write(...) instead. Resize an active TTY stream with session.Resize(cols, rows).

Managed console sessions

CreateSession, ListSessions, KillSession, and AttachSession are flat methods directly on *Sandbox for named, persistent command sessions that outlive the client connection. This is the key distinction from ExecStream: an exec process dies when its stream closes, but a managed session keeps running detached, and you can reattach later — even after the sandbox warm-suspends and resumes — and replay what it produced while nobody was watching. Use Exec/ExecStream for one-shot commands; use managed sessions for long-lived terminals like dev servers you want to detach from and come back to.
Create
name must match ^[a-z][a-z0-9-]{0,31}$. command is a non-empty slice of non-empty strings (no shell-string convenience like Exec). CreateSessionOptions has Env, Cwd, Cols, Rows, and Replace. Creating over an existing record returns *SessionExistsError unless Replace: true — and even then, only a terminal record (exited/killed/failed) is replaced; a running or attached session is never replaced. List — ListSessions(ctx) returns SessionList{Sessions []SessionInfo, SandboxSuspended bool}. Listing works on a suspended sandbox without waking it. Attach — AttachSession(ctx, name, opts...) with AttachOptions{Cols, Rows, MaxReplayBytes} returns *SessionStream. stream.Info, .ReplayedBytes, .HistoryDropped describe the bounded replay from while you were detached (the replay ring is 1 MiB). Attaching preempts any other attached client — the loser gets SessionEnded{Reason: SessionEndedReasonTakeover}. Attaching to a suspended sandbox’s session wakes it. stream.Next() yields: Stdout{Data []byte} (merged output — sessions are TTY-only, no separate stderr), Exit{ExitCode, Signaled, Signal}, SessionEnded{Reason} (SessionEndedReasonDetached or SessionEndedReasonTakeover), SessionOutputDropped{DroppedBytes} (client reading too slowly; doesn’t end the attach). stream.Write(data), stream.Resize(cols, rows) (1–512 per dimension), stream.Detach() (ends gracefully, process keeps running), stream.Close() (calls Detach if still open — call it via defer). Kill — KillSession(ctx, name, opts...) with KillOptions{Signal, GraceMS} signals the process group (Signal defaults to "TERM"), escalating to SIGKILL after GraceMS (defaults to 5000 when 0) if it hasn’t exited. Returns SessionInfo, but exit info isn’t guaranteed on that response — Kill doesn’t wait for the guest to reap the process, so a ListSessions shortly after is the reliable way to see the final exit code. Killing an unknown name returns *SessionNotFoundError. SessionInfo: Name, Command ([]string), WorkingDir, Status, Attached, StartedAt, LastActivityAt, EndedAt (zero value while running), Exit (non-nil only once terminal). SessionStatus values: SessionStatusUnspecified, SessionStatusStarting, SessionStatusIdle, SessionStatusAttached, SessionStatusExited, SessionStatusKilled, SessionStatusFailed. A session’s process never blocks idle suspend by itself — only an attached stream does. A quiet, detached session lets the sandbox warm-suspend and survives the resume with process and replay buffer intact. Session calls transparently reissue an expired capability and retry once, the same way ExecStream and the file methods below do. Call sandbox.ReissueCapability(ctx) directly only if you manage tokens yourself.

Files

File operations are flat methods directly on *Sandbox:
ReadFile buffers the whole file and returns *FilesystemLimitError before exceeding the configured filesystem read limit. WriteFile streams in 64 KiB chunks through a guest-side temp file and publishes atomically; a symlink at the final path is not followed. UploadFile/DownloadFile stream in 64 KiB chunks; DownloadFile uses a temp file, fsync, and atomic os.Rename. ListFiles returns immediate children sorted by name, or an error — no partial results. StatFile returns lstat-style metadata (a final symlink is reported as a symlink, not followed). MoveFile is same-filesystem, atomic, no-overwrite — cross-filesystem returns *CrossFilesystemMoveError, existing destination returns *RemoteFileExistsError. RemoveFile(ctx, path, true) recurses without following symlinks and is not atomic. FileInfo fields: Path, Name, Kind (FileKindFile / FileKindDirectory / FileKindSymlink / FileKindOther), Size, Mode, ModifiedAt (time.Time).

Preview URLs

A preview publishes one guest port at an HTTPS URL a browser can open. The server must bind a port in 1024–65535; privileged ports are never previewable.
A token-mode preview needs the sandbox’s capability, and a URL isn’t a safe place to leave one. sandbox.PreviewBrowserURL(preview) produces a single-use entry point: the gateway validates the token, trades it for a host-scoped HttpOnly cookie, and redirects without the token in the URL. It errors on a public preview, which has no token to exchange.
PreviewAuthPublic means anyone with the URL reaches the service, no credential — the only protection is the 26 random characters in the hostname. PreviewAuthToken is the default; an omitted Auth never yields a public URL. CreatePreview returns a fresh capability and the SDK stores it on the sandbox automatically (the preview scope is newer than a sandbox’s original token). If you’re holding a capability elsewhere, refresh it explicitly with sandbox.ReissueCapability(ctx). Traffic to a preview URL wakes a suspended sandbox; the first request after suspend pays resume latency and may return 503 with Retry-After.

Snapshots

sandbox.Snapshot(ctx, opts...) returns *Snapshot. If SnapshotOptions.IdempotencyKey is empty, the SDK generates and reuses one for transport retries; the same key against the same source sandbox returns the same snapshot identity on idempotent replay. Snapshot create requires a running source sandbox: a locally deleted or observed-deleted sandbox returns *SandboxDeletedError, a failed one returns *SandboxFailedError, a suspended one returns *SandboxSuspendedError. snapshot.Delete(ctx) returns nil and is idempotent on the same *Snapshot. A snapshot has its own identifier — you can delete it after deleting the sandbox that produced it.

Jobs

A job is a managed run of a command or script — on a new sandbox (created and, by default, deleted for you) or an existing one. Jobs are how you run work without holding a *Sandbox handle open yourself for the duration.
RunJob blocks until the run finishes, bounded by the client’s own timeout — use it for short jobs. StartJob returns immediately with a run id instead:
JobSpec requires exactly one of ExistingSandboxID/NewSandbox, and exactly one of Cmd/Script. Disposition (bonya.DispositionDelete, the default, or bonya.DispositionKeep) controls what happens to a sandbox the job itself created once the run ends; it’s not meaningful for ExistingSandboxID. GetJobRun returns *JobRunDetail, which adds the stored Spec and an activity Timeline to the run summary ListJobRuns returns. There is no endpoint to edit a run in place: to “edit and rerun” one, fetch it with GetJobRun, change what you need on its Spec, and pass that to RunJob or StartJob as a new run.
CancelJobRun cancels rather than terminates, so the run’s own cleanup (e.g. deleting a sandbox it created) still executes.

Job schedules

A schedule wraps a JobSpec with timing — cron, interval, or a one-shot future time — so it runs without you calling RunJob yourself.
ScheduleSpec requires exactly one of CronExpressions, IntervalSeconds, and RunAtUnixNanos (a one-shot: a single future calendar time, refused if in the past). Overlap (default bonya.ScheduleOverlapSkip) controls what a fire does when the previous run from the same schedule is still going.
JobSchedule.RecentRunIDs holds the last 10 fires’ run ids (oldest first, including manual triggers), each a valid GetJobRun id — so you can follow a schedule straight to its recent runs without a separate ListJobRuns call.

Templates

ListTemplates lists every template_id/version CreateSandbox and RunJob will accept, with metadata (OS, installed language stacks, which AI agent CLIs are preinstalled) to help pick one. Not paginated: the catalog is the same for every caller. CreateSandbox’s template argument may be empty to use the deployment’s configured default template, if it has one.

Organizations

An API key belongs to a user, not a single organization, so one key works across every organization that user belongs to. Calls are scoped to whichever organization is current on the client.
ListOrganizations(ctx) returns every organization the caller belongs to, including their personal organization. Organization.Personal marks that one — it’s the deterministic tenant an omitted organization context resolves to, and every account has exactly one. SetOrganizationID(id) changes which organization subsequent calls run against, including calls on a client that is already connected — it takes effect immediately, not just on the next dial. An empty id is rejected as an *InvalidRequestError rather than silently falling back to the personal organization. To set it once at construction instead, use bonya.WithOrganizationID(id); OrganizationID() reads back whatever is currently in effect. See Configuration for the full precedence and CI guidance.

Error handling

All SDK errors embed BaseError and expose Message(), SandboxID, OperationID, IdempotencyKey. Use errors.As to match a specific type:
The SDK redacts API keys, capabilities, and selected operation identifiers from mapped service error messages, and replaces path-like internal details similarly.

Retries and idempotency

WithMaxRetries (default 2) controls SDK retries for retryable control-plane operations. The SDK retries transient failures for create, get, list, delete, snapshot create, and snapshot delete, while preserving the same request and idempotency key where one exists. Exec calls are not retried, except for one capability refresh when the SDK can prove an exec capability token is expired before responses start. Filesystem calls are not retried on transport unavailability; they may refresh a rejected filesystem capability once. Create and snapshot-create accept an idempotency key so a retried call joins the original attempt instead of creating a second resource — see Idempotency for what that key does and doesn’t protect against, and pass one explicitly whenever you might need to retry a create across process restarts.

Resource cleanup

Go has no context-manager protocol, so use defer for deterministic cleanup:
  • Client.Close() closes cached channels and is idempotent.
  • sandbox.Delete(ctx) affects the remote sandbox and updates the local handle to StatusDeleted; calling it again is a local no-op.
  • session.Close() (on an *ExecSession) cancels an unfinished session.
  • stream.Close() (on a *SessionStream) detaches an unfinished attach rather than killing the guest process, which keeps running.
  • snapshot.Delete(ctx) deletes the remote snapshot and is a local no-op when repeated on the same object.
For intentionally persistent sandboxes, don’t defer sandbox.Delete(ctx). Store sandbox.ID, close the client, and reconnect later with client.GetSandbox(ctx, id).

Examples

Runnable programs ship in the SDK’s examples/ directory, a separate module so go get on the SDK never pulls them in: