> ## Documentation Index
> Fetch the complete documentation index at: https://paper.brimble.io/llms.txt
> Use this file to discover all available pages before exploring further.

# SDKs

> Official Brimble sandbox SDKs for TypeScript, Python, and Go. Same surface, three runtimes.

Brimble ships **first-party SDKs** for the sandbox API in three runtimes. They wrap the REST endpoints in idiomatic clients with built-in retries, resume/reconnect polling (`getReady`, `waitUntilReady`), error hierarchies, and convenience helpers, so you don't have to hand-roll any of it.

The three SDKs are deliberately near-identical in surface: every method on one has a same-named counterpart on the others, every input shape matches, every default lines up. Pick the runtime that fits your stack and the rest of the page applies.

| Runtime                     | Package                                        |
| --------------------------- | ---------------------------------------------- |
| **TypeScript / JavaScript** | `@brimble/sandbox`                             |
| **Python**                  | `brimble-sandbox`                              |
| **Go**                      | `github.com/brimblehq/brimble-sdks/sandbox-go` |

## Install

<CodeGroup>
  ```bash npm theme={null}
  npm install @brimble/sandbox
  ```

  ```bash pip theme={null}
  pip install brimble-sandbox
  ```

  ```bash go theme={null}
  go get github.com/brimblehq/brimble-sdks/sandbox-go
  ```
</CodeGroup>

**Runtime requirements:** Node 20+, Python 3.10+, Go 1.22+.

Go API docs are hosted at [pkg.go.dev/github.com/brimblehq/brimble-sdks/sandbox-go](https://pkg.go.dev/github.com/brimblehq/brimble-sdks/sandbox-go).

## Authenticate

All three SDKs read the same environment variable, **`BRIMBLE_SANDBOX_KEY`**, set it to your account-level API key from the profile drawer (avatar → **API key**) in the dashboard. You can also pass the key explicitly to the client constructor. See [API keys](/security/api-keys) for how to generate, rotate, and rate-limit.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { Sandbox } from "@brimble/sandbox";

  // reads BRIMBLE_SANDBOX_KEY
  const client = new Sandbox();

  // or pass explicitly
  const client = new Sandbox({ apiKey: process.env.MY_KEY });
  ```

  ```python Python theme={null}
  from brimble_sandbox import Sandbox

  # reads BRIMBLE_SANDBOX_KEY
  client = Sandbox()

  # or pass explicitly
  client = Sandbox(api_key="...")
  ```

  ```go Go theme={null}
  import sandbox "github.com/brimblehq/brimble-sdks/sandbox-go"

  // reads BRIMBLE_SANDBOX_KEY
  client, err := sandbox.NewClient(sandbox.ClientConfig{})

  // or pass explicitly
  client, err := sandbox.NewClient(sandbox.ClientConfig{ APIKey: "..." })
  ```
</CodeGroup>

Other constructor options (all optional):

* **`baseUrl` / `base_url` / `BaseURL`** sets the API root. Defaults to `https://sandbox.brimble.io`.
* **`timeoutMs` / `timeout_ms` / `Timeout`** sets the per-request HTTP timeout. Defaults to 30 seconds.
* **`retry` / `retry` / `Retry`** lets you override the retry policy (see [Retries and idempotency](#retries-and-idempotency)).
* TypeScript-only: **`fetchImpl`** supplies a custom `fetch` for tests.
* Python-only: **`session`** supplies a `requests.Session` for connection pooling.
* Go-only: **`HTTPClient`** supplies a custom `*http.Client`.

The client errors out immediately if neither the constructor arg nor the env var is set.

## The client surface

Every client exposes three resource groups:

| Resource           | What it covers                                                                                                                                      |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client.sandboxes` | Lifecycle: create, list, get, destroy, pause, resume, updateEgress. Discovery: list templates, list regions. Plus `use(id)` for scoped runtime ops. |
| `client.snapshots` | Account-wide snapshot listing and deletion. (Per-sandbox snapshot ops live on the handle.)                                                          |
| `client.volumes`   | Volume lifecycle: create, list, get, delete.                                                                                                        |

Go also exposes `client.Ping(ctx)`, a one-shot connectivity and auth check.

## Quickstart presets

The shortest path to a running sandbox is a quickstart helper. They preconfigure the template, persistent disk, and ready-wait so you can get to work in a single call.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { Sandbox } from "@brimble/sandbox";

  const client = new Sandbox();

  // Node 22, 20 GB persistent disk, waits for ready
  const handle = await client.sandboxes.quickstartNode({ region: "auto" });

  const result = await handle.exec({ cmd: "node -e 'console.log(2 + 2)'" });
  console.log(result.stdout);

  await handle.destroy();
  ```

  ```python Python theme={null}
  from brimble_sandbox import Sandbox

  client = Sandbox()

  # Python 3.12, 20 GB persistent disk, waits for ready
  sandbox = client.sandboxes.quickstart_python(region="auto")

  result = sandbox.exec({"cmd": "python -c 'print(2 + 2)'"})
  print(result["stdout"])

  sandbox.destroy()
  ```

  ```go Go theme={null}
  package main

  import (
      "context"
      "fmt"
      sandbox "github.com/brimblehq/brimble-sdks/sandbox-go"
  )

  func main() {
      ctx := context.Background()
      client, _ := sandbox.NewClient(sandbox.ClientConfig{})

      // Node 22, 20 GB persistent disk, waits for ready (wait=true)
      handle, _ := client.Sandboxes.QuickstartNode(ctx, "auto", true)

      result, _ := handle.Exec(ctx, sandbox.ExecInput{
          Cmd: "node -e 'console.log(2 + 2)'",
      })
      fmt.Println(result.Stdout)

      _ = handle.Destroy(ctx)
  }
  ```
</CodeGroup>

The presets bake in a sensible default for getting started: persistent storage so your files survive a restart, and a 20 GB disk so you have headroom. For finer control, use `create` directly (next section).

## Create

`create` provisions a sandbox and **blocks until it is `ready`** (\~2–3s typical). The HTTP request does not return until the container is up — set a client timeout of at least **90 seconds**.

`region` is optional — omit it (or pass `"auto"`) to let the server pick. Pass a specific region ID from `listRegions()` to pin one.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const handle = await client.sandboxes.create({
    region: "auto",
    template: "python-3.12",
    persistent: true,
    persistentDiskGB: 20,
  });
  // handle is ready to use immediately
  ```

  ```python Python theme={null}
  sandbox = client.sandboxes.create({
      "region": "auto",
      "template": "python-3.12",
      "persistent": True,
      "persistentDiskGB": 20,
  })
  ```

  ```go Go theme={null}
  handle, err := client.Sandboxes.Create(ctx, sandbox.CreateSandboxRequest{
      Region:           "auto",
      Template:         "python-3.12",
      Persistent:       true,
      PersistentDiskGB: 20,
  })
  ```
</CodeGroup>

The matching `getReady(id)` fetches an existing sandbox and waits until it is `ready` — use after **resume** or when reconnecting to a sandbox whose state you do not know.

`waitUntilReady()` on a handle is for edge cases (resume, long-poll fallback). You do **not** need it after a fresh `create()`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Reconnect after resume — getReady waits for ready
  const handle = await client.sandboxes.getReady(sandboxId, {
    wait: { timeoutMs: 120_000 },
  });
  ```

  ```python Python theme={null}
  sandbox = client.sandboxes.get_ready(
      sandbox_id,
      wait_timeout_ms=120_000,
  )
  ```

  ```go Go theme={null}
  handle, _ := client.Sandboxes.GetReady(ctx, sandboxID, &sandbox.WaitOptions{
      Timeout: 120 * time.Second,
  })
  ```
</CodeGroup>

## Create a sandbox alongside a fresh volume

`withVolume` provisions both in a single call. Use it when you want a per-sandbox persistent workspace and don't already have a volume.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const handle = await client.sandboxes.withVolume({
    sandbox: {
      region: "auto",
      template: "node-22",
    },
    volume: {
      name: "alice-workspace",
      sizeGB: 20,
      region: "auto",
    },
  });
  ```

  ```python Python theme={null}
  sandbox = client.sandboxes.with_volume({
      "sandbox": {
          "region": "auto",
          "template": "node-22",
      },
      "volume": {
          "name": "alice-workspace",
          "sizeGB": 20,
          "region": "auto",
      },
  })
  ```

  ```go Go theme={null}
  handle, err := client.Sandboxes.WithVolume(ctx, sandbox.CreateSandboxWithVolumeInput{
      Sandbox: sandbox.CreateSandboxRequest{ Region: "auto", Template: "node-22" },
      Volume:  sandbox.CreateVolumeInput{ Name: "alice-workspace", SizeGB: 20, Region: "auto" },
  })
  ```
</CodeGroup>

The volume's `region` and the sandbox's `region` must resolve to the same region. Passing `"auto"` on both is the simplest path.

## The sandbox handle

`create()`, `get()`, `getReady()`, and `withVolume()` all return a **handle**, an object that bundles the sandbox ID with runtime helpers. Most of what you do with a sandbox happens through the handle, not the client.

You can also grab a handle for any sandbox by ID:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const handle = await client.sandboxes.get(sandboxId);

  // or a thinner scope (no cached state, no auto-wait)
  const scope = client.sandboxes.use(sandboxId);
  ```

  ```python Python theme={null}
  sandbox = client.sandboxes.get(sandbox_id)

  # or a thinner scope
  scope = client.sandboxes.use(sandbox_id)
  ```

  ```go Go theme={null}
  handle, _ := client.Sandboxes.Get(ctx, sandboxID)

  // or a thinner scope
  scope := client.Sandboxes.Use(sandboxID)
  ```
</CodeGroup>

The handle exposes the sandbox's identity, last-known state, and the full set of runtime methods:

| Surface             | TypeScript                              | Python                                  | Go                                         |
| ------------------- | --------------------------------------- | --------------------------------------- | ------------------------------------------ |
| Sandbox ID          | `handle.id`                             | `sandbox.id`                            | `handle.ID()`                              |
| Cached status       | `handle.status`                         | `sandbox.status`                        | `handle.Status()`                          |
| Full record         | `handle.data`                           | `sandbox.data`                          | `handle.Latest()`                          |
| Refresh from API    | `handle.refresh()`                      | `sandbox.refresh()`                     | `handle.Refresh(ctx)`                      |
| Destroy             | `handle.destroy()`                      | `sandbox.destroy()`                     | `handle.Destroy(ctx)`                      |
| Pause / resume      | `handle.pause()` / `handle.resume()`    | `sandbox.pause()` / `sandbox.resume()`  | `handle.Pause(ctx)` / `handle.Resume(ctx)` |
| Update egress       | `handle.updateEgress(input)`            | `sandbox.update_egress(input)`          | `handle.UpdateEgress(ctx, input)`          |
| Exec command        | `handle.exec({ cmd })`                  | `sandbox.exec({ cmd })`                 | `handle.Exec(ctx, input)`                  |
| Stream exec         | `handle.exec({ cmd, stream: true })`    | `sandbox.exec_stream({ cmd })`          | `handle.ExecStream(ctx, input)`            |
| Run code            | `handle.runCode({ language, code })`    | `sandbox.run_code({ language, code })`  | `handle.RunCode(ctx, input)`               |
| Stream code         | `handle.runCode({ ..., stream: true })` | `sandbox.run_code_stream({ ... })`      | `handle.RunCodeStream(ctx, input)`         |
| Wait until ready    | `handle.waitUntilReady()`               | `sandbox.wait_until_ready()`            | `handle.WaitUntilReady(ctx)`               |
| Snapshots namespace | `handle.snapshots.create/list`          | `sandbox.snapshots.create/list/iterate` | `handle.Snapshots.Create/List`             |

## Auto-wait on runtime ops

Every runtime method (`exec`, `runCode`, `putFile`, `getFile`, `stats`, `createSnapshot`, `listSnapshots`) accepts an optional **`waitUntilReady`** flag. When set, the SDK waits until the sandbox is `ready` before sending the actual call — useful after **resume** or when you fetched a handle with `get()` and aren't sure of its state.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const handle = await client.sandboxes.get(sandboxId);

  // waits if still resuming, then runs the command
  const result = await handle.exec(
    { cmd: "echo hello" },
    { waitUntilReady: true },
  );
  ```

  ```python Python theme={null}
  sandbox = client.sandboxes.get(sandbox_id)

  result = sandbox.exec(
      {"cmd": "echo hello"},
      wait_until_ready=True,
  )
  ```

  ```go Go theme={null}
  handle, _ := client.Sandboxes.Get(ctx, sandboxID)

  result, _ := handle.Exec(ctx, sandbox.ExecInput{Cmd: "echo hello"},
      sandbox.RuntimeOptions{WaitUntilReady: true})
  ```
</CodeGroup>

Without the flag, runtime methods return an error if the sandbox isn't `ready`. After `create()`, the handle is already ready — call `exec` directly. Use `waitUntilReady` when operating on a handle you fetched with `get()` and the sandbox may still be resuming.

## Sandboxes resource (full surface)

### Create

`region` is required (use `"auto"` to let the server pick); everything else is optional.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const handle = await client.sandboxes.create({
    region: "auto",                          // or a region ID
    template: "python-3.12",
    name: "scratch",
    teamId: "<team-id>",                     // omit for personal
    specs: { cpu: 500, memory: 512, disk: 2 },
    autoDestroy: true,
    destroyTimeout: "1h",                    // 30m, 1h, 3h, 6h, 12h, 18h
    oneShot: false,
    blockOutbound: false,                      // legacy; maps to deny_all
    egress: { mode: "restricted", allow: ["1.1.1.1"] },
    persistent: true,
    persistentDiskGB: 20,
    // volumeId: "<existing-volume-id>",
    // fromSnapshot: "<snapshot-id>",
    snapshotMode: "automatic",
    snapshotFrequency: "0 */2 * * *",
  });
  ```

  ```python Python theme={null}
  sandbox = client.sandboxes.create({
      "region": "auto",
      "template": "python-3.12",
      "name": "scratch",
      "specs": {"cpu": 500, "memory": 512, "disk": 2},
      "autoDestroy": True,
      "destroyTimeout": "1h",
      "persistent": True,
      "persistentDiskGB": 20,
  })
  ```

  ```go Go theme={null}
  handle, err := client.Sandboxes.Create(ctx, sandbox.CreateSandboxRequest{
      Region:           "auto",
      Template:         "python-3.12",
      Name:             "scratch",
      Specs:            &sandbox.SandboxSpecs{CPU: 500, Memory: 512, Disk: 2},
      AutoDestroy:      true,
      DestroyTimeout:   "1h",
      Persistent:       true,
      PersistentDiskGB: 20,
  })
  ```
</CodeGroup>

Specs ranges: `cpu` 1 to 2000 (MHz units), `memory` 1 to 2048 MB, `disk` 1 to 5 GB ephemeral.

### List, get, destroy

<CodeGroup>
  ```typescript TypeScript theme={null}
  const page = await client.sandboxes.list({ page: 1, limit: 15, teamId: "<team>" });
  const handle = await client.sandboxes.get(sandboxId);
  await client.sandboxes.destroy(sandboxId);
  ```

  ```python Python theme={null}
  page = client.sandboxes.list({"page": 1, "limit": 15})
  sandbox = client.sandboxes.get(sandbox_id)
  client.sandboxes.destroy(sandbox_id)
  ```

  ```go Go theme={null}
  page, _ := client.Sandboxes.List(ctx, sandbox.TeamScopedPagination{Page: 1, Limit: 15})
  handle, _ := client.Sandboxes.Get(ctx, sandboxID)
  err := client.Sandboxes.Destroy(ctx, sandboxID)
  ```
</CodeGroup>

`destroy` is idempotent: calling it on an already-destroyed sandbox is a no-op.

### Iterate across pages

For walking every sandbox without hand-rolling pagination:

<CodeGroup>
  ```typescript TypeScript theme={null}
  for await (const handle of client.sandboxes.iterate({ limit: 50 })) {
    console.log(handle.id, handle.status);
  }
  ```

  ```python Python theme={null}
  for sandbox in client.sandboxes.iterate({"limit": 50}):
      print(sandbox.id, sandbox.status)
  ```

  ```go Go theme={null}
  err := client.Sandboxes.Iterate(ctx, sandbox.TeamScopedPagination{Limit: 50},
      func(h *sandbox.SandboxHandle) error {
          fmt.Println(h.ID(), h.Status())
          return nil
      })
  ```
</CodeGroup>

### Pause and resume

<CodeGroup>
  ```typescript TypeScript theme={null}
  await handle.pause();
  await handle.resume();
  ```

  ```python Python theme={null}
  sandbox.pause()
  sandbox.resume()
  ```

  ```go Go theme={null}
  _, _ = handle.Pause(ctx)
  _, _ = handle.Resume(ctx)
  ```
</CodeGroup>

Both are async; the response acknowledges the request and the sandbox transitions a few seconds later. The handle auto-refreshes its cached state, so `handle.status` reflects the new value immediately.

### Update egress

Change outbound network policy on a running sandbox. Returns the updated sandbox record, including `network_updated` when the underlying network profile changed.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { SandboxEgressMode } from "@brimble/sandbox";

  const updated = await handle.updateEgress({
    mode: SandboxEgressMode.Restricted,
    allow: ["1.1.1.1", "api.example.com"],
  });

  if (updated.network_updated) {
    // allow a few seconds for the new policy to apply
  }
  ```

  ```python Python theme={null}
  from brimble_sandbox.enums import SandboxEgressMode

  updated = sandbox.update_egress({
      "mode": SandboxEgressMode.RESTRICTED,
      "allow": ["1.1.1.1", "api.example.com"],
  })
  ```

  ```go Go theme={null}
  updated, _ := handle.UpdateEgress(ctx, sandbox.UpdateSandboxEgressInput{
      Mode:  sandbox.SandboxEgressModeRestricted,
      Allow: []string{"1.1.1.1", "api.example.com"},
  })
  ```
</CodeGroup>

See [Network egress](#network-egress) for mode semantics and create-time configuration.

## Runtime operations

Once the sandbox is `ready` (or you've passed `waitUntilReady`):

### Exec

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await handle.exec({
    cmd: "echo hello && uname -a",
    timeout_seconds: 30,
    cwd: "/work",
  });
  console.log(result.stdout, result.stderr, result.exit_code, result.duration_ms);
  ```

  ```python Python theme={null}
  result = sandbox.exec({
      "cmd": "echo hello && uname -a",
      "timeout_seconds": 30,
  })
  print(result["stdout"], result["exit_code"])
  ```

  ```go Go theme={null}
  result, _ := handle.Exec(ctx, sandbox.ExecInput{
      Cmd: "echo hello && uname -a", TimeoutSeconds: 30,
  })
  ```
</CodeGroup>

By default `exec` buffers, you get `stdout`, `stderr`, `exit_code`, and `duration_ms` once the command finishes. For long-running commands where you want output as it arrives, see [Stream exec output](#stream-exec-output) below.

### Run code

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await handle.runCode({
    language: "python",       // or "node"
    code: "import sys; print(sys.version)",
  });
  ```

  ```python Python theme={null}
  result = sandbox.run_code({"language": "python", "code": "print('hi')"})
  ```

  ```go Go theme={null}
  result, _ := handle.RunCode(ctx, sandbox.CodeInput{
      Language: "python", Code: "print('hi')",
  })
  ```
</CodeGroup>

### Per-call environment variables

Both `exec` and `runCode` accept an **`env`** object that's layered on top of the sandbox's existing environment for a single call. Same-named keys override the sandbox-level value for that invocation only; the next call starts from the sandbox defaults again. Cleaner than shelling out `export FOO=bar &&` in front of every command, and stops secrets from showing up in argv / shell history.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await handle.exec({
    cmd: "node script.js",
    env: {
      NODE_ENV: "production",
      OPENAI_API_KEY: process.env.OPENAI_API_KEY!,
      REQUEST_ID: requestId,
    },
  });

  // Same shape on runCode
  await handle.runCode({
    language: "python",
    code: "import os; print(os.environ['MY_KEY'])",
    env: { MY_KEY: "value" },
  });
  ```

  ```python Python theme={null}
  result = sandbox.exec({
      "cmd": "node script.js",
      "env": {
          "NODE_ENV": "production",
          "OPENAI_API_KEY": os.environ["OPENAI_API_KEY"],
          "REQUEST_ID": request_id,
      },
  })

  # Same shape on run_code
  sandbox.run_code({
      "language": "python",
      "code": "import os; print(os.environ['MY_KEY'])",
      "env": {"MY_KEY": "value"},
  })
  ```

  ```go Go theme={null}
  result, _ := handle.Exec(ctx, sandbox.ExecInput{
      Cmd: "node script.js",
      Env: map[string]string{
          "NODE_ENV":       "production",
          "OPENAI_API_KEY": os.Getenv("OPENAI_API_KEY"),
          "REQUEST_ID":     requestID,
      },
  })

  // Same shape on RunCode
  _, _ = handle.RunCode(ctx, sandbox.CodeInput{
      Language: "python",
      Code:     "import os; print(os.environ['MY_KEY'])",
      Env:      map[string]string{"MY_KEY": "value"},
  })
  ```
</CodeGroup>

Values must be strings; numbers and booleans need to be stringified first. The map is scoped to the one call, sandbox-wide environment changes are not supported.

### Stream exec output

Both `exec` and `runCode` support three output modes:

| Mode            | When to use                                                 | TypeScript                                   | Python                                        | Go                                  |
| --------------- | ----------------------------------------------------------- | -------------------------------------------- | --------------------------------------------- | ----------------------------------- |
| **Buffered**    | Short commands                                              | `exec({ cmd })`                              | `exec({ cmd })`                               | `Exec(ctx, input)`                  |
| **Live stream** | Long output, agent logs, installs                           | `exec({ cmd, stream: true })` → `ExecStream` | `exec_stream({ cmd })` → `ExecStream`         | `ExecStream(ctx, input)`            |
| **Callbacks**   | Wire output to a UI/logger without managing a stream object | `exec({ cmd, onStdout, onStderr })`          | `exec({ cmd }, on_stdout=..., on_stderr=...)` | `Exec(ctx, input, &ExecHooks{...})` |

When streaming is active, the server responds with **`Content-Type: text/event-stream`**. Each event is a JSON object on a `data:` line; comment lines (`: open`, `: ping`) are keepalives and can be ignored. The stream always ends with a `done` event (or `error` on transport failure).

<Note>
  In Go, pass live output through **`ExecStream` / `RunCodeStream`**, not `Stream: true` on the input struct. Setting `stream: true` on `ExecInput` returns an error directing you to the stream methods.
</Note>

#### Live stream (`ExecStream`)

<CodeGroup>
  ```typescript TypeScript theme={null}
  const output = await handle.exec({
    cmd: "pip install -r requirements.txt",
    stream: true,
  });

  // Async-iterate stdout/stderr chunks as they arrive
  for await (const log of output) {
    if (log.stream === "stdout") process.stdout.write(log.data);
    if (log.stream === "stderr") process.stderr.write(log.data);
  }

  // Or iterate raw SSE frames (includes the terminal `done` frame)
  for await (const frame of output.frames()) {
    if (frame.type === "done") console.log("exit:", frame.exit_code);
  }

  const result = await output.result();
  ```

  ```python Python theme={null}
  output = sandbox.exec_stream({"cmd": "pip install -r requirements.txt"})

  for log in output:
      if log["stream"] == "stdout":
          print(log["data"], end="")

  # Or iterate raw frames
  for frame in output.frames():
      if frame["type"] == "done":
          print("exit:", frame["exit_code"])

  result = output.result()
  output.close()
  ```

  ```go Go theme={null}
  output, err := handle.ExecStream(ctx, sandbox.ExecInput{
      Cmd: "pip install -r requirements.txt",
  })
  if err != nil { panic(err) }
  defer output.Close()

  err = output.IterateLogs(ctx, func(log sandbox.ExecLog) error {
      if log.Stream == sandbox.LogStreamStdout {
          fmt.Print(log.Data)
      }
      return nil
  })

  result, err := output.Result(ctx)
  ```
</CodeGroup>

#### Callback streaming (buffered result, live chunks)

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await handle.exec({
    cmd: "npm install",
    onStdout: (chunk) => process.stdout.write(chunk),
    onStderr: (chunk) => process.stderr.write(chunk),
  });
  console.log(result.exit_code);
  ```

  ```python Python theme={null}
  result = sandbox.exec(
      {"cmd": "npm install"},
      on_stdout=lambda chunk: print(chunk, end=""),
      on_stderr=lambda chunk: print(chunk, end="", file=sys.stderr),
  )
  print(result["exit_code"])
  ```

  ```go Go theme={null}
  result, err := handle.Exec(ctx, sandbox.ExecInput{Cmd: "npm install"}, &sandbox.ExecHooks{
      OnStdout: func(chunk string) { fmt.Print(chunk) },
      OnStderr: func(chunk string) { fmt.Fprint(os.Stderr, chunk) },
  })
  ```
</CodeGroup>

#### Stream `runCode`

Same patterns on the language-aware variant:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const output = await handle.runCode({
    language: "node",
    code: "for (let i = 1; i <= 3; i++) console.log(i)",
    stream: true,
  });

  for await (const log of output) {
    process.stdout.write(log.data);
  }
  ```

  ```python Python theme={null}
  output = sandbox.run_code_stream({
      "language": "node",
      "code": "for i in range(1, 4): print(i)",
  })

  for log in output:
      print(log["data"], end="")
  ```

  ```go Go theme={null}
  output, _ := handle.RunCodeStream(ctx, sandbox.CodeInput{
      Language: "node",
      Code:     "for (let i = 1; i <= 3; i++) console.log(i)",
  })
  defer output.Close()
  _ = output.IterateLogs(ctx, func(log sandbox.ExecLog) error {
      fmt.Print(log.Data)
      return nil
  })
  ```
</CodeGroup>

#### SSE frame shape

Frame payloads (typed as `ExecStreamFrame` in TS, dict in Python, parsed in Go):

* `{ "type": "stdout", "data": "..." }`
* `{ "type": "stderr", "data": "..." }`
* `{ "type": "done", "exit_code": 0, "duration_ms": 142 }`
* `{ "type": "error", "message": "..." }`

TypeScript exports `ExecStream`, `parseSseFrames`, and `consumeExecStream`. Python exports `ExecStream`, `parse_sse_frames`, and `consume_exec_stream` for lower-level use.

### Upload and download files

<CodeGroup>
  ```typescript TypeScript theme={null}
  await handle.putFile("/app/index.js", "console.log('hello')");

  const stream = await handle.getFile("/app/index.js");
  // stream is a ReadableStream<Uint8Array>
  ```

  ```python Python theme={null}
  sandbox.put_file("/app/index.js", b"console.log('hello')")

  response = sandbox.get_file("/app/index.js")
  for chunk in response.iter_content(chunk_size=8192):
      ...
  ```

  ```go Go theme={null}
  f, _ := os.Open("./local.bin")
  defer f.Close()
  info, _ := f.Stat()
  _ = handle.PutFile(ctx, "/data/local.bin", f, info.Size())

  body, _ := handle.GetFile(ctx, "/data/local.bin")
  defer body.Close()
  io.Copy(os.Stdout, body)
  ```
</CodeGroup>

Parent directories must exist; uploads to a non-existent directory fail with a `400`.

### Batch file uploads

For seeding many small files in one round trip (a fresh repo checkout, a set of config files, a directory of fixtures), use the batch upload. It sends the files as base64-encoded JSON in a single POST and returns a per-file success/failure summary.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const summary = await handle.putFiles([
    { path: "/work/package.json", body: JSON.stringify({ name: "demo" }) },
    { path: "/work/index.js", body: "console.log('hello')" },
    { path: "/work/README.md", body: "# demo\n" },
  ]);

  console.log(summary.uploaded, "uploaded,", summary.failed, "failed");
  for (const r of summary.results) {
    if (!r.success) console.warn("failed:", r.path, r.error);
  }
  ```

  ```python Python theme={null}
  summary = sandbox.put_files([
      {"path": "/work/package.json", "body": '{"name": "demo"}'},
      {"path": "/work/index.js", "body": "console.log('hello')"},
      {"path": "/work/README.md", "body": "# demo\n"},
  ])

  print(summary["uploaded"], "uploaded,", summary["failed"], "failed")
  for r in summary["results"]:
      if not r["success"]:
          print("failed:", r["path"], r.get("error"))
  ```

  ```go Go theme={null}
  summary, _ := handle.PutFiles(ctx, []sandbox.BatchFileUploadItem{
      {Path: "/work/package.json", Content: []byte(`{"name":"demo"}`)},
      {Path: "/work/index.js", Content: []byte("console.log('hello')")},
      {Path: "/work/README.md", Content: []byte("# demo\n")},
  })

  fmt.Printf("%d uploaded, %d failed\n", summary.Uploaded, summary.Failed)
  for _, r := range summary.Results {
      if !r.Success {
          fmt.Println("failed:", r.Path, r.Error)
      }
  }
  ```
</CodeGroup>

Per-call cap is **100 files**, and the payload counts toward the same file-size limit as single uploads, the SDKs reject more than 100 files client-side. For larger transfers (single big files, or more than 100 files), loop `putFile` instead.

### Stats

<CodeGroup>
  ```typescript TypeScript theme={null}
  const stats = await handle.stats({ hoursAgo: 1 });
  ```

  ```python Python theme={null}
  stats = sandbox.stats({"hoursAgo": 1})
  ```

  ```go Go theme={null}
  stats, _ := handle.Stats(ctx, sandbox.StatsQuery{HoursAgo: 1})
  ```
</CodeGroup>

Returns averages and a time-series of CPU%, memory%, and network bytes/sec.

## Snapshots

Per-sandbox snapshot ops live on the handle. Account-wide ops live on `client.snapshots`.

<CodeGroup>
  ```typescript TypeScript theme={null}
  // per-sandbox
  const snap = await handle.snapshots.create({ name: "before-migration" });
  const list = await handle.snapshots.list({ page: 1, limit: 15 });

  // account-wide
  const all = await client.snapshots.listAll({ limit: 50 });
  await client.snapshots.delete(snapshotId);

  // iterate every snapshot you own
  for await (const snap of client.snapshots.iterateAll({ limit: 100 })) {
    console.log(snap.id, snap.status);
  }
  ```

  ```python Python theme={null}
  # per-sandbox
  snap = sandbox.snapshots.create({"name": "before-migration"})
  listing = sandbox.snapshots.list({"page": 1, "limit": 15})

  # account-wide
  all_snaps = client.snapshots.list_all({"limit": 50})
  client.snapshots.delete(snapshot_id)

  for snap in client.snapshots.iterate_all({"limit": 100}):
      print(snap["id"], snap["status"])
  ```

  ```go Go theme={null}
  snap, _ := handle.Snapshots.Create(ctx, sandbox.CreateSnapshotInput{Name: "before-migration"})
  list, _ := handle.Snapshots.List(ctx, sandbox.Pagination{Page: 1, Limit: 15})

  all, _ := client.Snapshots.ListAll(ctx, sandbox.Pagination{Limit: 50})
  err := client.Snapshots.Delete(ctx, snapshotID)

  _ = client.Snapshots.IterateAll(ctx, sandbox.Pagination{Limit: 100},
      func(s sandbox.Snapshot) error { fmt.Println(s.ID, s.Status); return nil })
  ```
</CodeGroup>

Snapshot names match `^[a-z0-9-]{1,40}$`. Creation is async: the response returns `status: "creating"` and the snapshot flips to `ready` (or `failed`) a few minutes later. Poll `list` to see the transition.

### Restore from a snapshot

Pass `fromSnapshot` at sandbox create time to seed the new sandbox with the snapshot's filesystem:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const handle = await client.sandboxes.create({
    region: "auto",
    fromSnapshot: "<snapshot-id>",
  });
  ```

  ```python Python theme={null}
  sandbox = client.sandboxes.create({
      "region": "auto",
      "fromSnapshot": "<snapshot-id>",
  })
  ```

  ```go Go theme={null}
  handle, _ := client.Sandboxes.Create(ctx, sandbox.CreateSandboxRequest{
      Region: "auto", FromSnapshot: "<snapshot-id>",
  })
  ```
</CodeGroup>

## Volumes

The SDKs only create **sandbox-type** volumes (`type: "sandbox"`); the `web` type is reserved for the dashboard's persistent-disk toggle on a project. All three SDKs enforce this client-side and reject any other value before the HTTP call.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const volume = await client.volumes.create({
    name: "node-cache",
    sizeGB: 20,                // min 10, max 50
    region: "auto",
    teamId: "<team-id>",       // optional
  });

  const list = await client.volumes.list({ page: 1, limit: 15 });
  const fetched = await client.volumes.get(volume.id);
  await client.volumes.delete(volume.id);
  ```

  ```python Python theme={null}
  volume = client.volumes.create({
      "name": "node-cache",
      "sizeGB": 20,
      "region": "auto",
  })
  listing = client.volumes.list({"page": 1, "limit": 15})
  v = client.volumes.get(volume["id"])
  client.volumes.delete(volume["id"])
  ```

  ```go Go theme={null}
  volume, err := client.Volumes.Create(ctx, sandbox.CreateVolumeInput{
      Name: "node-cache", SizeGB: 20, Region: "auto",
  })
  list,    _ := client.Volumes.List(ctx, sandbox.TeamScopedPagination{Page: 1, Limit: 15})
  v,       _ := client.Volumes.Get(ctx, volume.ID)
  err = client.Volumes.Delete(ctx, volume.ID)
  ```
</CodeGroup>

Attach a volume to a sandbox by passing `volumeId` on create. See the [Volumes](../volumes/overview) doc for the full attach model.

## Network egress

Control outbound network access when creating or updating a sandbox. Three modes:

| Mode         | SDK constant (TS)              | Behavior                          |
| ------------ | ------------------------------ | --------------------------------- |
| `open`       | `SandboxEgressMode.Open`       | Full outbound internet (default). |
| `deny_all`   | `SandboxEgressMode.DenyAll`    | All outbound blocked.             |
| `restricted` | `SandboxEgressMode.Restricted` | Default deny + allowlist.         |

**At create time**, pass `egress: { mode, allow? }`. **`allow`** is an array of IPv4 addresses, CIDR ranges, or hostnames (up to 50 entries). Required when updating to `restricted`; optional at create (defaults to an empty list).

**At runtime**, call `updateEgress` on the handle or sandboxes resource. The response includes `network_updated: true` when the sandbox's network profile was switched; allow a few seconds before testing connectivity from inside the sandbox.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { SandboxEgressMode } from "@brimble/sandbox";

  const handle = await client.sandboxes.create({
    template: "node-22",
    egress: {
      mode: SandboxEgressMode.Restricted,
      allow: ["1.1.1.1", "api.openai.com"],
    },
  });

  const updated = await handle.updateEgress({ mode: SandboxEgressMode.DenyAll });
  ```

  ```python Python theme={null}
  from brimble_sandbox.enums import SandboxEgressMode

  sandbox = client.sandboxes.create({
      "template": "node-22",
      "egress": {
          "mode": SandboxEgressMode.RESTRICTED,
          "allow": ["1.1.1.1", "api.openai.com"],
      },
  })

  updated = sandbox.update_egress({"mode": SandboxEgressMode.DENY_ALL})
  ```

  ```go Go theme={null}
  handle, _ := client.Sandboxes.Create(ctx, sandbox.CreateSandboxRequest{
      Template: "node-22",
      Egress: &sandbox.SandboxEgressConfig{
          Mode:  sandbox.SandboxEgressModeRestricted,
          Allow: []string{"1.1.1.1", "api.openai.com"},
      },
  })

  updated, _ := handle.UpdateEgress(ctx, sandbox.UpdateSandboxEgressInput{
      Mode: sandbox.SandboxEgressModeDenyAll,
  })
  ```
</CodeGroup>

**Legacy:** `blockOutbound: true` at create maps to `deny_all`. Do not combine `egress` and `blockOutbound` on the same request.

See [Sandboxes overview → Network egress](overview#network-egress) for the product-level model and [Update sandbox egress](/api-reference/sandboxes/egress-update) for the REST contract.

## Discovery

Both templates and regions are first-class on the client. No need to hit a separate API.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const templates = await client.sandboxes.listTemplates();
  const node = await client.sandboxes.getTemplate("node-22");

  const regions = await client.sandboxes.listRegions();
  ```

  ```python Python theme={null}
  templates = client.sandboxes.list_templates()
  node = client.sandboxes.get_template("node-22")

  regions = client.sandboxes.list_regions()
  ```

  ```go Go theme={null}
  templates, _ := client.Sandboxes.ListTemplates(ctx)
  node, _      := client.Sandboxes.GetTemplate(ctx, "node-22")

  regions, _ := client.Sandboxes.ListRegions(ctx)
  ```
</CodeGroup>

The Brimble team adds and retires templates without warning; calling `listTemplates()` is the authoritative way to see what's currently available.

## Pagination

All `list` endpoints accept `{ page, limit }` (plus `teamId` on team-scoped calls). Defaults are `page = 1`, `limit = 15`, max `limit = 100`. Responses include `totalCount`, `currentPage`, `totalPages`, `limit`, and a `data` array.

For walking every result, prefer the `iterate` / `iterate_all` / `Iterate` helpers shown above; they handle pagination internally.

## Errors

Every SDK exposes a typed error hierarchy. All subclasses extend the base type so a single `catch` / `except` / `errors.As` block can still handle "anything from the API," but you can narrow when you want to.

| Class                          | When                                                                                                        |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `SandboxApiError` / `APIError` | Base type. Any non-2xx response.                                                                            |
| `AuthError`                    | 401 / 403. Missing key, revoked key, insufficient permission.                                               |
| `ValidationError`              | 400 / 422. Bad input shape, invalid state transition.                                                       |
| `NotFoundError`                | 404. Sandbox / volume / snapshot doesn't exist or isn't yours.                                              |
| `RateLimitError`               | 429. Carries `retryAfterSeconds` / `retry_after_seconds` / `RetryAfterSeconds` when the server provides it. |

Every instance carries `status`, `message`, `endpoint`, `responseBody`, and `requestId`.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { RateLimitError, NotFoundError, SandboxApiError } from "@brimble/sandbox";

  try {
    await client.sandboxes.get("missing");
  } catch (err) {
    if (err instanceof RateLimitError) {
      await new Promise((r) => setTimeout(r, (err.retryAfterSeconds ?? 1) * 1000));
      // retry
    } else if (err instanceof NotFoundError) {
      console.warn("sandbox is gone");
    } else if (err instanceof SandboxApiError) {
      console.error(err.status, err.message, err.requestId);
    } else {
      throw err;
    }
  }
  ```

  ```python Python theme={null}
  import time
  from brimble_sandbox import RateLimitError, NotFoundError, SandboxApiError

  try:
      client.sandboxes.get("missing")
  except RateLimitError as err:
      time.sleep(err.retry_after_seconds or 1)
      # retry
  except NotFoundError:
      print("sandbox is gone")
  except SandboxApiError as err:
      print(err.status, err.message, err.request_id)
  ```

  ```go Go theme={null}
  import (
      "errors"
      "time"
  )

  _, err := client.Sandboxes.Get(ctx, "missing")
  if err != nil {
      var rate *sandbox.RateLimitError
      var notFound *sandbox.NotFoundError
      var apiErr *sandbox.APIError
      switch {
      case errors.As(err, &rate):
          if rate.RetryAfterSeconds != nil {
              time.Sleep(time.Duration(*rate.RetryAfterSeconds) * time.Second)
          }
          // retry
      case errors.As(err, &notFound):
          fmt.Println("sandbox is gone")
      case errors.As(err, &apiErr):
          fmt.Println(apiErr.Status, apiErr.Message, apiErr.RequestID)
      default:
          return err
      }
  }
  ```
</CodeGroup>

A few SDK-specific sentinels:

* **Python:** `wait_until_ready()` raises `TimeoutError` on deadline. `Sandbox(api_key=...)` raises `ValueError` if no key is found. `volumes.create()` raises `ValueError` for size or type violations before the HTTP call.
* **TypeScript:** the constructor throws a plain `Error` if no key is found.
* **Go:** `context.DeadlineExceeded` / `context.Canceled` for caller-driven cancellation; check with `errors.Is`.

## Retries and idempotency

Each SDK ships a built-in retry policy you can configure at the client or per call. Defaults are conservative: **one attempt** (no retries) by default, base delay 300 ms, max delay 3 s, retry on `408, 429, 500, 502, 503, 504`. Bump `maxAttempts` to opt in.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const client = new Sandbox({
    retry: {
      maxAttempts: 3,
      baseDelayMs: 300,
      maxDelayMs: 3_000,
      retryStatuses: [408, 429, 500, 502, 503, 504],
    },
  });

  // per-call override
  await client.sandboxes.create(input, {
    retry: { maxAttempts: 5 },
    idempotencyKey: crypto.randomUUID(),
  });

  // or disable retries on a single call
  await client.sandboxes.destroy(id, { retry: false });
  ```

  ```python Python theme={null}
  from brimble_sandbox import Sandbox, RetryOptions
  import uuid

  client = Sandbox(retry=RetryOptions(max_attempts=3, base_delay_ms=300, max_delay_ms=3_000))

  client.sandboxes.create(
      input,
      retry=RetryOptions(max_attempts=5),
      idempotency_key=str(uuid.uuid4()),
  )
  ```

  ```go Go theme={null}
  client, _ := sandbox.NewClient(sandbox.ClientConfig{
      Retry: &sandbox.RetryOptions{
          MaxAttempts: 3, BaseDelayMs: 300, MaxDelayMs: 3000,
      },
  })

  _, _ = client.Sandboxes.Create(ctx, input, sandbox.RequestOptions{
      Retry:          &sandbox.RetryOptions{MaxAttempts: 5},
      IdempotencyKey: "deploy-job-42",
  })
  ```
</CodeGroup>

**Idempotency keys** make lifecycle calls safe to retry. The server deduplicates retries that carry the same `idempotencyKey` within a short window: `create`, `destroy`, `pause`, `resume`, `createSnapshot`, `deleteSnapshot`, and volume `create` / `delete` all accept the option. Pass a stable, unique-per-operation value (a UUID, a job ID, the SHA of the request body) and you can retry network failures without spinning up a duplicate sandbox.

## Timeouts and cancellation

| Operation                | TypeScript             | Python                   | Go                                        |
| ------------------------ | ---------------------- | ------------------------ | ----------------------------------------- |
| Default HTTP timeout     | 30s                    | 30s                      | 30s                                       |
| Per-call override        | `timeoutMs` in options | `timeout_ms=<n>`         | `ctx` deadline / `RequestOptions.Timeout` |
| Cancel mid-flight        | `signal: AbortSignal`  | not directly cancellable | `ctx.Done()`                              |
| Wait-until-ready default | 60s timeout, 2s poll   | 60s timeout, 2s poll     | 60s timeout, 2s poll                      |
| Wait-until-ready cancel  | `signal: AbortSignal`  | raises `TimeoutError`    | `ctx.Done()`                              |

Best practice in Go: pass a `context.Context` with a deadline matched to the operation. The SDK respects `ctx.Done()` everywhere, including the wait-until-ready polling loop.

## Cleanup

**None of the SDKs auto-destroy sandboxes when your process exits.** Either set `autoDestroy: true` with a `destroyTimeout`, use `oneShot: true` so the sandbox terminates when its main process exits, or wrap your work in a cleanup block:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const handle = await client.sandboxes.create({ region: "auto", template: "node-22" });
  try {
    // ... do work
  } finally {
    await handle.destroy().catch(() => {});
  }
  ```

  ```python Python theme={null}
  sandbox = client.sandboxes.create({"region": "auto", "template": "python-3.12"})
  try:
      pass  # ... do work
  finally:
      try:
          sandbox.destroy()
      except Exception:
          pass
  ```

  ```go Go theme={null}
  handle, _ := client.Sandboxes.Create(ctx, sandbox.CreateSandboxRequest{
      Region: "auto", Template: "node-22",
  })
  defer func() {
      _ = handle.Destroy(context.Background())
  }()
  ```
</CodeGroup>

## Versioning and stability

All three SDKs are at **0.x** today; the public surface is shaped to stay stable, but minor bumps may break compatibility while we shake things out. Pin a specific version in production until we tag 1.0:

<CodeGroup>
  ```bash npm theme={null}
  npm install @brimble/sandbox@^0.1.2
  ```

  ```bash pip theme={null}
  pip install brimble-sandbox==0.1.0
  ```

  ```bash go theme={null}
  go get github.com/brimblehq/brimble-sdks/sandbox-go@v0.1.0
  ```
</CodeGroup>

## Next steps

* [Sandboxes overview](overview), the lifecycle and billing model.
* [Quickstart](quickstart), the five-minute end-to-end walkthrough with all three SDKs side by side.
* [Cookbook](cookbook/untrusted-code), recipes for the highest-traffic use cases.
* [Snapshots](snapshots), deep-dive on the snapshot lifecycle.
* [Volumes](../volumes/overview), persistent volumes that survive sandbox destruction.
* **Sandbox API** tab, the REST contract the SDKs wrap.
