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
SetTTY: 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.
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.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 aJobSpec 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 embedBaseError 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 usedefer for deterministic
cleanup:
Client.Close()closes cached channels and is idempotent.sandbox.Delete(ctx)affects the remote sandbox and updates the local handle toStatusDeleted; 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.
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’sexamples/ directory, a separate
module so go get on the SDK never pulls them in: