> ## 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 Usage

> Install, configure, and use the GMI Sandbox Python SDK.

# GMI Sandbox SDK Usage

## Install

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

## Configure

Use environment variables or pass values directly.

To get an API key, log in to `https://console.gmicloud.ai/`, click **API keys**, select **compute**, and click **Create API key**.

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

Set `GMI_SANDBOX_IDC_NAME` (or pass `idc_name`) to choose the IDC for creating sandboxes and templates and for listing product specifications. Sandbox and template creation fall back to your organization's default IDC when neither is set (400 `idc_name is required` if it has none); listing product specifications always needs an IDC:

```bash theme={null}
export GMI_SANDBOX_IDC_NAME="gmi-sandbox-us"
```

## Create a client

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

client = SandboxClient()
```

You can also pass credentials explicitly:

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

### Async client

`AsyncSandboxClient` provides the same resources and operations with native
async HTTP I/O. Use it as an async context manager so pooled connections are
closed automatically:

```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()
    result = await sandbox.commands.run("echo hello", wait=True)
    print(result.stdout)
    await sandbox.files.write("/tmp/hello.txt", "hello")
    await sandbox.delete()
```

When not using an async context manager, call `await client.aclose()` after
the client is no longer needed. `aclose()` closes only the HTTP client the SDK
created for you; a `transport` you pass in stays open and remains yours to
close. Requests issued after `aclose()` raise `TransportError`, so create a new
client instead of reusing a closed one.

Both clients only send requests to `http` and `https` URLs, and they validate
every redirect hop against that rule. This is a scheme check: it does not
restrict which hosts a `base_url` you configure can 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.

## Create and use a sandbox

```python theme={null}
sandbox = client.sandboxes.create(
    template_id="template-id",
)

sandbox.wait_until_running()
sandbox.connect()
result = sandbox.commands.run("echo hello", wait=True)
print(result.stdout)

sandbox.files.write("/tmp/hello.txt", "hello")
print(sandbox.files.read("/tmp/hello.txt").decode())

sandbox.delete()
```

If you already have a sandbox ID:

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

## Files

```python theme={null}
sandbox.files.read("/tmp/a.txt")
sandbox.files.write("/tmp/a.txt", "hello")
sandbox.files.write("/tmp/a.bin", b"binary-data")
sandbox.files.upload("./local.txt", "/workspace/local.txt")
sandbox.files.download("/workspace/output.txt", "./output.txt")
```

`read()` and `download()` buffer the complete response in memory. `download()`
returns bytes and can also write to a destination path; use its `byte_range`
argument to retrieve large files in chunks.

## Commands

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

print(execution.status)
print(execution.exit_code)
print(execution.stdout)
print(execution.stderr)
```

`stdout` and `stderr` are each capped at 1 MiB. Check `execution.stdout_truncated`
(and `stderr_truncated`); for large output, write to a file and download it.

You can refresh or cancel a running execution:

```python theme={null}
execution.refresh()
execution.cancel()
```

## Snapshots

Prepare a sandbox once, save it as a snapshot, and launch new sandboxes from it:

```python theme={null}
base = client.sandboxes.create(template_id="template-id", timeout=900)
base.wait_until_running()
base.connect()
base.commands.run(
    "npm i -g @anthropic-ai/claude-code", execution_timeout_seconds=600
).wait(timeout=600)

snapshot = base.create_snapshot(name="agent-base")
snapshot.wait_until_ready()

worker = client.sandboxes.create(snapshot_id=snapshot.snapshot_id, timeout=900)
worker.wait_until_running()
worker.connect()
```

Sizing is inherited from the snapshot. Environment variables passed with
`env_vars` are not part of a snapshot; pass them again on each create. Clean up
caches and credentials before taking a snapshot.

```python theme={null}
client.snapshots.list(source_sandbox_id=base.sandbox_id)
snapshot.update(name="agent-base-v2")
snapshot.delete()   # delete sandboxes launched from it first, or this raises ConflictError
```

## Pause and resume

```python theme={null}
sandbox.pause()      # state becomes paused
sandbox.resume()     # cold start; pass timeout= to reset the lifetime
sandbox.wait_until_running()
sandbox.connect()    # reconnect before data-plane calls
```

A paused sandbox keeps its disk files except `/tmp`; memory and running
processes are not kept.

## Templates

```python theme={null}
template = client.templates.get("template-id")
template.update(name="new-name")
template.delete()

builds = template.builds()
build = template.build("build-id")
logs = template.build_logs("build-id", offset=0, limit=100)
```

Creating a template requires an idempotency key:

```python theme={null}
template = client.templates.create(
    name="demo",
    resources={"type": "preset", "product": "gmi.sandbox.small"},
    build={"source": {"type": "image", "image": "ubuntu:22.04"}},
    idempotency_key="template-1",
)

print(template.template_id)
template.update(name="new-name")
```

Create from a Dockerfile and local build context:

```python theme={null}
from pathlib import Path

template = client.templates.create(
    name="dockerfile-demo",
    resources={"type": "preset", "product": "gmi.sandbox.small"},
    dockerfile_content=Path("Dockerfile").read_text(),
    build_context_dir=".",
    idempotency_key="template-dockerfile-1",
)
```

The live API accepts one uploaded context archive. The SDK translates each
local `COPY`/`ADD` with one source inside `build_context_dir` (for example
`COPY . /app`, `COPY app.py /app/` or `COPY ["src", "/srv/src"]`) to the API's
Template-file reference syntax, uploads only those paths, and honors
`.dockerignore`. Patterns without `/` match only at the context root, as in
`docker build`; use `**/*.env` or `**/node_modules` to exclude them at any
depth. Flags such as `--chown` and heredocs are rejected before a request is
sent. `AsyncSandboxClient.templates.create()` accepts the same arguments.

Choose the runtime user of sandboxes launched from a Build with `default_user`
(for example `default_user={"username": "root"}`). It is applied after all build
steps, so it overrides the Dockerfile's final `USER`; it does not change the
working directory, which still comes from the Dockerfile's `WORKDIR`
(`/home/user` when omitted). `templates.create()` and `templates.builds.create()`
accept it with either `build=` or `dockerfile_content=`.

Rebuild an existing template without replacing it with
`client.templates.builds.create(template.template_id, dockerfile_content=..., build_context_dir=..., start_cmd=...)`; the async client exposes the same call and returns the newly created build, while `template.wait_until_ready()` remains the readiness helper. A rebuild is not idempotent: each call starts a new build and supersedes running ones.

## Product specifications

```python theme={null}
client.product_specifications.list(idc_name="gmi-sandbox-us")
```

## Errors

Errors from API calls, the HTTP transport and the wait helpers derive from
`SandboxSDKError`:

* HTTP errors, all subclasses of `APIError`: `BadRequestError` (400),
  `AuthenticationError` (401), `PermissionDeniedError` (403), `NotFoundError`
  (404), `ConflictError` (409), `RequestTooLargeError` (413),
  `UnsupportedMediaTypeError` (415), `RangeNotSatisfiableError` (416),
  `UnprocessableEntityError` (422), `RateLimitError` (429),
  `RequestCanceledError` (499), `ServerError` (5xx) and
  `InsufficientStorageError` (507). `SandboxQuotaExceeded` and
  `TemplateBuildCapacityExceeded` are `RateLimitError`s, and
  `StaleIdempotencyKey` is a `ConflictError`.
* `TransportError` when the request cannot reach the service or its URL is
  malformed.
* `WaitTimeoutError` (also a `TimeoutError`) and `TerminalStateError` (also a
  `RuntimeError`) from `wait_until_running()`, `wait_until_ready()`,
  `Execution.wait()` and `upload_tree()`.

Invalid arguments, and data-plane calls made before `connect()`, raise the
built-in `ValueError` or `TypeError` before any request is sent.

Example:

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

try:
    client.sandboxes.get("missing")
except NotFoundError:
    print("sandbox not found")
```


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