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

# GMI Sandbox SDK Reference

> Python SDK reference for GMI Sandbox control-plane and data-plane APIs.

# GMI Sandbox SDK Reference

Python SDK for the GMI Sandbox control plane and sandbox data plane.

## Installation

```bash theme={null}
python -m pip install gmi-sandbox-sdk
```

The SDK requires Python 3.9 or newer.

## Configuration

The client reads these environment variables when explicit arguments are not provided:

| Variable | Default | Description |
| - | - | - |
| `GMI_SANDBOX_API_KEY` | none | Bearer API key |
| `GMI_SANDBOX_IDC_NAME` | none | Used when `idc_name` is omitted from `sandboxes.create()`, `templates.create()` and `product_specifications.list()`. If neither is set, sandbox and template creation fall back to the organization's default IDC (400 `idc_name is required` when it has none); `product_specifications.list()` has no such fallback and always needs an IDC. |

```bash theme={null}
export GMI_SANDBOX_API_KEY="your-api-key"
```

## Client

```python theme={null}
from sandbox_sdk import SandboxClient

client = SandboxClient()
```

Explicit configuration:

```python theme={null}
client = SandboxClient(
    api_key="your-api-key",
    timeout=30.0,
)
```

Constructor arguments:

| Argument | Type | Description |
| - | - | - |
| `api_key` | `str \| None` | API key. Falls back to `GMI_SANDBOX_API_KEY`. |
| `base_url` | `str \| None` | Optional control-plane URL for non-default API endpoints. Falls back to `GMI_SANDBOX_BASE_URL`. |
| `timeout` | `float` | Transport timeout in seconds. Default: `30.0`. |
| `transport` | callable \| None | Optional transport for tests or custom networking. |

`client.health()` returns local client information:

```python theme={null}
{
    "status": "ok",
    "baseUrl": "https://console.gmicloud.ai/api/v2",
    "authenticated": True,
}
```

### Async client

`AsyncSandboxClient` mirrors the synchronous client. All operations that can
perform network or file I/O are awaitable:

```python theme={null}
from sandbox_sdk import AsyncSandboxClient

async with AsyncSandboxClient() as client:
    sandbox = await client.sandboxes.create(template_id="template-id")
    await sandbox.wait_until_running()
    await sandbox.connect()
    execution = await sandbox.commands.run("echo hello", wait=True)
```

The async client accepts the same configuration arguments. Its optional
`transport` must be an async callable returning `RawResponse`. Call
`await client.aclose()` when the client is not managed by `async with`.

`aclose()` closes only the transport the SDK owns. A `transport` supplied by
the caller is never closed by the client; closing it is the caller's
responsibility. After `aclose()`, further requests raise `TransportError`
rather than silently opening a new connection pool.

Both clients accept `http` and `https` URLs only and re-validate that scheme on
every redirect hop. The check does not limit which hosts a configured
`base_url` may reach. A malformed URL in `base_url` or in a redirect
`Location`, such as a non-numeric port or an invalid host name, raises
`TransportError` and is not retried. Non-ASCII characters in the path and query
are sent percent-encoded as UTF-8.

## Basic Workflow

```python theme={null}
from sandbox_sdk import SandboxClient

client = SandboxClient()

sandbox = client.sandboxes.create(
    template_id="template-id",
    idempotency_key="create-request-1",
)

sandbox.wait_until_running(timeout=300)
sandbox.connect(timeout=300)
execution = sandbox.commands.run("echo hello", wait=False).wait(timeout=300)
print(execution.stdout)
sandbox.delete()
```

`idc_name` is optional for sandbox and template creation: when omitted, the service falls back to the organization's configured default IDC.

## Sandboxes

### `client.sandboxes.create()`

```python theme={null}
sandbox = client.sandboxes.create(
    idc_name="gmi-sandbox-us",
    template_id="template-id",  # or snapshot_id="snapshot-id"
    timeout=300,
    env_vars={"APP_ENV": "production"},
    metadata={"owner": "platform"},
    idempotency_key="sandbox-create-1",
)
```

Arguments:

| Argument | Description |
| - | - |
| `idc_name` | Target IDC. Falls back to `GMI_SANDBOX_IDC_NAME`, then to the organization's default IDC. |
| `template_id` | Template source. Exactly one of `template_id` and `snapshot_id` is required. |
| `snapshot_id` | Launch from a `ready` snapshot instead of a template. Compute resources are inherited from the snapshot. |
| `timeout` | Sandbox lifetime in seconds. |
| `env_vars` | Runtime environment variables. |
| `metadata` | String metadata attached to the sandbox. |
| `default_user` | Optional default user for commands and files in this sandbox. |
| `idempotency_key` | Optional create idempotency key. |

Returns a `Sandbox`.

### `client.sandboxes.list()`

```python theme={null}
page = client.sandboxes.list(
    page=1,
    page_size=20,
    idc_name="gmi-sandbox-us",
    state=["running"],
    metadata={"owner": "platform"},
)
```

`state` accepts a string or a sequence of strings. Metadata is encoded as `metadata[key]` query parameters.

### `client.sandboxes.get()`

```python theme={null}
sandbox = client.sandboxes.get("sandbox-id")
```

### `client.sandboxes.connect()`

```python theme={null}
sandbox = client.sandboxes.connect("sandbox-id", timeout=300)
```

The returned sandbox contains fresh data-plane connection fields.

## `Sandbox`

Properties:

| Property | Description |
| - | - |
| `sandbox_id` | Sandbox identifier (control-plane UUID) |
| `id` | Alias for `sandbox_id` |
| `sandbox_key` | Data-plane addressing key, used to build `data_plane_url` |
| `domain` | Data-plane domain |
| `sandbox_access_token` | Data-plane access token |
| `state` | Lifecycle state |
| `data_plane_url` | Computed data-plane URL |
| `files` | `SandboxFiles` helper |
| `commands` | `SandboxCommands` helper |
| `executions` | Alias for `commands` |
| `data` | Raw response data dictionary |

Lifecycle states (`SandboxStatus`): `provisioning`, `running`, `pausing`, `paused`, `resuming`, `checkpointing`, `updating`, `failed`, `deleting`, and `deleted`.

Methods:

```python theme={null}
sandbox.refresh()
sandbox.wait_until_running(timeout=300)
sandbox.update_metadata({"team": "platform"})
sandbox.set_timeout(300)
sandbox.connect(timeout=300)
sandbox.pause()
sandbox.resume(timeout=300)            # timeout is optional
snapshot = sandbox.create_snapshot(name="base", metadata={"team": "platform"})
sandbox.delete()
```

`refresh()`, `wait_until_running()` and `update_metadata()` keep the data-plane
connection fields returned by `connect()` (`sandbox_access_token`,
`traffic_access_token`, `domain`); the sandbox detail and update responses do
not include the access tokens.
Data-plane calls (`files`, `commands`) raise `ValueError` if the sandbox has no
access token; call `connect()` first.

`pause()` keeps disk files except `/tmp`; memory and running processes are not kept.
`resume()` is a cold start. Call `connect()` again before data-plane calls.

`wait_until_running()` raises `TerminalStateError` (a `RuntimeError`) as soon as the sandbox is `failed`,
`deleting` or `deleted`, or is `paused` (a resume that did not complete); it
does not wait out the timeout for states that cannot become `running`.

`set_timeout()` resets the time-to-live and returns the sandbox with `end_at`
set to the new expiry. In some IDCs the access token is bound to the previous
`end_at`, so call `connect()` again after extending the lifetime; where the
token is not time-bound, `connect()` returns the same token.

## Snapshots

A snapshot is a durable copy of a running sandbox. Launch new sandboxes from a
`ready` snapshot with `client.sandboxes.create(snapshot_id=...)`.

```python theme={null}
snapshot = client.snapshots.create("sandbox-id", name="base", metadata={"team": "platform"},
                                   idempotency_key="snapshot-1")
snapshot.wait_until_ready(timeout=300)

page = client.snapshots.list(source_sandbox_id="sandbox-id", status="ready", page_size=50)
snapshot = client.snapshots.get("snapshot-id")
snapshot.update(name="renamed", metadata={"team": "infra"})
snapshot.delete()
```

`Snapshot` properties: `snapshot_id` (alias `id`), `name`, `idc_name`,
`source_sandbox_id`, `status`, `data`.

Snapshot statuses (`SnapshotStatus`): `creating`, `ready`, `error`, `deleting`,
and `delete_failed`. A deleted snapshot is no longer returned: reading it raises
`NotFoundError`. `wait_until_ready()` raises `TerminalStateError` (a
`RuntimeError`) on `error`, `deleting` or `delete_failed`, including
`status_reason` when the service supplies one.

`snapshot.delete()` raises `ConflictError` while a sandbox launched from the
snapshot is still active; delete those sandboxes first. A paused sandbox cannot
be snapshotted (`ConflictError`); resume it first.

## Files

All file methods use the sandbox data plane and require `connect()` first.

```python theme={null}
sandbox.files.read("/tmp/input.txt")
sandbox.files.write("/tmp/output.txt", "hello")
sandbox.files.write("/tmp/output.bin", b"\x00\x01")
sandbox.files.upload("./local.txt", "/tmp/local.txt")
sandbox.files.upload_tree("./local-directory", "/workspace")
sandbox.files.download("/tmp/local.txt")
sandbox.files.download(
    "/tmp/local.txt",
    destination="./downloaded.txt",
    byte_range="bytes=10-",
)
```

Methods:

| Method | Description |
| - | - |
| `read(path, username=None)` | Read file bytes |
| `write(path, content, username=None, filename=None)` | Write text or bytes |
| `upload(source, path, username=None)` | Upload a local path or bytes |
| `upload_tree(local_dir, remote_dir, timeout=300)` | Tar, upload, and remotely unpack a directory tree |
| `download(path, destination=None, username=None, byte_range=None)` | Download bytes, optionally to a local path |

`download()` always returns `bytes`. `destination` is an additional local write.
Both `read()` and `download()` buffer the complete response in memory; use
`byte_range` with `download()` when retrieving large files in chunks.
`upload_tree()` reuses the 512 MiB packed-size limit and skips symlinks that
resolve outside the local directory. It uploads one archive, unpacks it with
the sandbox command API under `/bin/sh` (so images without bash work), and
removes the temporary archive. `timeout` bounds both the client wait and how
long the sandbox lets the unpack run (at most 3600 seconds). If the unpack
fails, it raises `TerminalStateError` (a `SandboxSDKError` and a
`RuntimeError`) with the exit code and stderr; `.state` holds the execution
status.

## Command Execution

```python theme={null}
execution = sandbox.commands.run(
    "python --version",
    envs={"PYTHONUNBUFFERED": "1"},
    cwd="/tmp",
    wait=True,
    wait_timeout_seconds=25,
)
```

Arguments:

| Argument | Description |
| - | - |
| `command` | Shell command |
| `envs` | Environment variables for the process |
| `cwd` | Working directory |
| `wait` | Wait for a result within the wait window |
| `wait_timeout_seconds` | Server wait window; the API currently accepts 1-25 seconds |
| `execution_timeout_seconds` | How long the sandbox lets the command run, 1-3600 seconds. The data plane uses 300 when omitted; past the limit the command is ended as `timed_out` with no exit code or output |
| `request_id` | Sent as the `X-Request-ID` header. The data plane ignores it and generates its own `request_id`, returned in responses and errors |

`Execution` properties:

```python theme={null}
execution.execution_id
execution.status
execution.exit_code
execution.stdout
execution.stderr
execution.stdout_truncated
execution.stderr_truncated
execution.data
```

`stdout` and `stderr` are each capped at 1 MiB. When a stream is cut,
`stdout_truncated` / `stderr_truncated` is `True`; redirect large output to a
file and read it with `sandbox.files.download(..., byte_range=...)`.

Execution statuses (`ExecutionStatus`): `pending`, `running`, `cancelling`, `succeeded`, `failed`, `cancelled`, and `timed_out`. `timed_out` is terminal: the command outlived `execution_timeout_seconds`.

Methods:

```python theme={null}
execution.refresh()
execution.cancel()
execution.wait(timeout=300, cancel_on_timeout=True)
```

Cancel only non-terminal executions. The service may reject cancellation after an execution has completed.
If `wait()` times out but the execution finishes before its cancel lands, it
returns the finished execution instead of raising. Otherwise it raises
`WaitTimeoutError`, even when the cancel itself fails; the cancel error is
available as `__cause__`.
The synchronous `POST /executions` wait window is capped at approximately 25
seconds. Run with `wait=False`, then call `Execution.wait()` for longer work, and
set `execution_timeout_seconds` for anything that may run longer than 300 seconds:
`Execution.wait()` only bounds how long the client waits, not how long the sandbox
lets the command run.

## Templates

### Create

```python theme={null}
template = client.templates.create(
    name="demo-template",
    idc_name="gmi-sandbox-us",
    resources={"type": "preset", "product": "gmi.sandbox.small"},
    build={
        "source": {"type": "image", "image": "ubuntu:22.04"},
        "commands": ["apt-get update"],
        "envs": {"BUILD_ENV": "production"},
        "start_cmd": "sleep infinity",
    },
    description="Example template",
    labels={"team": "platform"},
    idempotency_key="template-create-1",
)
```

`resources` selects a catalog preset by product SKU (see
`client.product_specifications.list()`):

```python theme={null}
{"type": "preset", "product": "gmi.sandbox.small"}
```

Custom resources (`"type": "custom"`) are currently rejected with 400
`Request.InvalidParameter`.

Build sources include image and template sources:

```python theme={null}
{"type": "image", "image": "ubuntu:22.04"}
{"type": "template", "template_id": "parent-template-id"}
```

`idempotency_key` is required for template creation: retrying with the same key
and arguments replays the same Template instead of creating a second one. A
retry after the Build already started is also safe; the SDK confirms the
Build is past `waiting` and does not start it again.

Rebuilds (`templates.builds.create()`) are not idempotent. Each call creates a
new Build and supersedes the Template's running Builds, so do not blindly retry
one. Its `idempotency_key` is optional and only lets a retry reuse an uploaded
`build_context_dir`; it is not sent with the rebuild itself.

Dockerfile builds accept `dockerfile_content`, `build_context_dir`, and
`start_cmd` in place of `build`:

```python theme={null}
template = client.templates.create(
    name="dockerfile-template",
    resources={"type": "preset", "product": "gmi.sandbox.small"},
    dockerfile_content="FROM ubuntu:22.04\nCOPY . /workspace\n",
    build_context_dir="./context",
    start_cmd="python /workspace/main.py",
    idempotency_key="template-dockerfile-1",
)
```

Build contexts are uploaded as gzip-compressed tar archives (`tgz`) and are
limited to 512 MiB before compression. Only the paths that `COPY`/`ADD` read
are packed, and a `.dockerignore` at the root of `build_context_dir` is honored
with Docker's rules (`*`, `**`, `?`, `!` exceptions, last match wins). A
`COPY` source excluded by `.dockerignore` raises `ValueError`. Symlinks that
resolve outside `build_context_dir` are excluded from the archive.

Unlike `.gitignore`, a pattern without `/` matches only at the root of
`build_context_dir`: `*.env` excludes `secret.env` but not `app/secret.env`.
Prefix `**/` to match at any depth, for example `**/*.env` or
`**/node_modules`. A trailing `/` is ignored, so `temp/` also excludes a file
named `temp`. This is the same as `docker build`.

When the Dockerfile uses the context, the SDK performs the live API's split
flow: create the waiting Template/Build, initialize and upload a tgz context,
poll until the file is ready, and start the existing Build. The API supports a
single context file, so the SDK rewrites each local `COPY`/`ADD` into steps
that unpack that file. Each instruction takes one source inside
`build_context_dir` (a file or a directory, `.` for all of it) and one
destination, in shell form or JSON form (`COPY ["app.py", "/app/"]`), and may
span lines with a trailing backslash. `$VAR` and `${VAR}` in the destination
expand to `ENV` values. `COPY`/`ADD` flags such as `--chown` are rejected, and
so are heredocs (`RUN <<EOF`), which the API does not support.

When `idc_name` is omitted, `templates.create()` uses `GMI_SANDBOX_IDC_NAME` for
every request it makes. `templates.builds.create()` targets an existing
Template, so without `idc_name` it lets the API use that Template's IDC.

`default_user` (optional, for example `{"username": "root"}` or
`{"username": "app", "uid": 9999}`) sets the runtime identity of sandboxes
launched from the Build. The API applies it after all build steps, so it
overrides the Dockerfile's final `USER`, but not its `WORKDIR`. It is accepted
with `build=` or `dockerfile_content=` on `templates.create()` and
`templates.builds.create()`; passing a different `default_user` inside `build`,
or passing it to `templates.create()` with neither `build=` nor
`dockerfile_content=`, raises `ValueError`. Username and UID rules are enforced
by the API.

Returns a `Template`. The returned object can be used with template instance methods such as `update()`, `delete()`, `builds()`, `build()`, and `build_logs()`.
Call `template.wait_until_ready(timeout=300)` to poll `latest_build_status`, the
status of the most recently created build. It refreshes before every check, so
it also waits for a rebuild started with `templates.builds.create()`.
It returns at `ready`, raises `TerminalStateError` for failed terminal states,
and raises `WaitTimeoutError` (a `TimeoutError`) while the build remains pending.

### List and Get

```python theme={null}
page = client.templates.list(idc_name="gmi-sandbox-us", name="demo")
template = client.templates.get("template-id", idc_name="gmi-sandbox-us")
```

### Update and Delete

```python theme={null}
template.update(name="renamed-template", description="Updated description")
template.delete()
```

The template's `idc_name` is reused automatically when present in the template response.

### Builds and Logs

```python theme={null}
builds = template.builds(page=1, page_size=20)
build = template.build("build-id")
logs = template.build_logs("build-id", offset=0, limit=100)
```

## Product Specifications

```python theme={null}
specifications = client.product_specifications.list(idc_name="gmi-sandbox-us")
everything = client.product_specifications.list(
    idc_name="gmi-sandbox-us", include_unavailable=True
)
```

An IDC is required: pass `idc_name` or set `GMI_SANDBOX_IDC_NAME`. Unlike
sandbox and template creation, the organization's default IDC does not apply,
so a request with neither returns 400. `include_unavailable=True` also returns
products whose status is `unavailable`.

## Pagination

List methods return a `Page` object:

```python theme={null}
page = client.sandboxes.list(page=1, page_size=20)

print(page.items)
print(page.total)
print(page.page)
print(page.page_size)
print(page.request_id)
```

`Page` fields:

| Field | Type | Description |
| - | - | - |
| `items` | `list` | Returned resources |
| `total` | `int` | Total matching resources |
| `page` | `int` | Current page |
| `page_size` | `int` | Requested page size |
| `request_id` | `str \| None` | Server request ID |

## Errors

Errors from API calls, the HTTP transport and the wait helpers
(`wait_until_running()`, `wait_until_ready()`, `Execution.wait()`) derive from
`SandboxSDKError`. Invalid arguments, and data-plane calls made before
`connect()`, raise the built-in `ValueError` or `TypeError` before any request
is sent. Local file errors, such as a missing upload source or an unreadable
build context, propagate unchanged as `OSError` (for example
`FileNotFoundError`).

| Exception | HTTP status |
| - | - |
| `BadRequestError` | 400 |
| `AuthenticationError` | 401; data-plane errors carry code `Auth.Unauthorized` (missing, expired, or wrong sandbox access token) |
| `PermissionDeniedError` | 403 |
| `NotFoundError` | 404 |
| `ConflictError` | 409 |
| `StaleIdempotencyKey` | 409 consumed idempotency key |
| `RequestTooLargeError` | 413 |
| `UnsupportedMediaTypeError` | 415 |
| `RangeNotSatisfiableError` | 416 |
| `UnprocessableEntityError` | 422 (not supported in the selected IDC) |
| `RateLimitError` | 429 |
| `SandboxQuotaExceeded` | 429 `Resource.QuotaExceeded` for sandbox quota |
| `TemplateBuildCapacityExceeded` | 429 build capacity |
| `RequestCanceledError` | 499 |
| `ServerError` | 500+ |
| `InsufficientStorageError` | 507 (a `ServerError`) |
| `TransportError` | Request could not reach the endpoint, or its URL is malformed |
| `WaitTimeoutError` | Wait helper timed out (also a `TimeoutError`) |
| `TerminalStateError` | Wait helper saw a failed terminal state; `.state` holds it (also a `RuntimeError`) |

Other 429 limits, such as the number of pending Template files, raise a plain
`RateLimitError`.

`TransportError.phase` is `connect`, `write`, or `read` when the transport can
identify the failing phase; otherwise it is `None`. The synchronous client
reports `read` for failures while waiting for or reading the response, and
`connect` only for refused connections and DNS failures; it cannot tell a
connect timeout from a write failure, so those have `phase` `None`.

A request that fails in transport is attempted up to three times in total:
`GET` and `HEAD` on any failure, other methods on `connect` failures, and
`DELETE` also on any failure except `read`. A `DELETE` whose response was lost
may already have run, so it raises `TransportError` instead of retrying into a
`NotFoundError`; check the resource before deleting again. Because each
attempt has its own `timeout`, a `GET` against a server that stops responding
can take about three times `timeout` (plus a short backoff) before
`TransportError` is raised.

`APIError` fields:

```python theme={null}
try:
    client.sandboxes.get("missing")
except NotFoundError as exc:
    print(exc.status_code)
    print(exc.message)
    print(exc.code)
    print(exc.request_id)
    print(exc.details)
```

Error handling example:

```python theme={null}
from sandbox_sdk import ConflictError, NotFoundError, SandboxSDKError

try:
    sandbox = client.sandboxes.get("sandbox-id")
    sandbox.connect()
except NotFoundError:
    print("sandbox no longer exists")
except ConflictError:
    print("sandbox is in a transitional state; retry later")
except SandboxSDKError as exc:
    print(f"sandbox request failed: {exc}")
```

## OpenAPI Coverage

The SDK maps the Sandbox-related OpenAPI operations as follows:

| OpenAPI operation | SDK |
| - | - |
| `createSandbox` | `client.sandboxes.create()` |
| `listSandboxes` | `client.sandboxes.list()` |
| `getSandbox` | `client.sandboxes.get()` |
| `updateSandbox` | `sandbox.update_metadata()` |
| `deleteSandbox` | `sandbox.delete()` |
| `connectSandbox` | `sandbox.connect()` |
| `setSandboxTimeout` | `sandbox.set_timeout()` |
| `pauseSandbox` | `sandbox.pause()` |
| `resumeSandbox` | `sandbox.resume()` |
| `createSandboxSnapshot` | `client.snapshots.create()` / `sandbox.create_snapshot()` |
| `listSandboxSnapshots` | `client.snapshots.list()` |
| `getSandboxSnapshot` | `client.snapshots.get()` / `snapshot.refresh()` |
| `updateSandboxSnapshot` | `snapshot.update()` |
| `deleteSandboxSnapshot` | `snapshot.delete()` |
| `createSandboxExecution` | `sandbox.commands.run()` |
| `getSandboxExecution` | `execution.refresh()` |
| `cancelSandboxExecution` | `execution.cancel()` |
| `downloadSandboxFile` | `sandbox.files.read()` / `download()` |
| `uploadSandboxFile` | `sandbox.files.write()` / `upload()` |
| `listProductCatalog` | `client.product_specifications.list()` |
| `createTemplate` | `client.templates.create()` |
| `createTemplateBuild` | `client.templates.builds.create()` |
| `createTemplateFile` | `templates.create()` / `templates.builds.create()` with `build_context_dir` |
| `getTemplateFile` | `templates.create()` / `templates.builds.create()` with `build_context_dir` |
| `startTemplateBuild` | `templates.create()` with `build_context_dir` |
| `listTemplates` | `client.templates.list()` |
| `getTemplate` | `client.templates.get()` |
| `updateTemplate` | `template.update()` |
| `deleteTemplate` | `template.delete()` |
| `listTemplateBuilds` | `template.builds()` |
| `getTemplateBuild` | `template.build()` |
| `getTemplateBuildLogs` | `template.build_logs()` |

Not available in the SDK yet: the interactive shell (`launchSandboxShell`,
`attachSandboxShell`, `controlSandboxShell`) and sandbox network actions
(`executeSandboxAction`). Call the HTTP API directly for these.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.