# SDK Reference (https://tenki.cloud/docs/sandbox/sdk)

> For the complete documentation index, see [llms.txt](https://tenki.cloud/llms.txt)

Programmatic reference for Tenki Sandbox covering installation, authentication, client options, identity discovery, OpenCode integration, and error types.



Tenki Sandbox has official SDKs for Go, TypeScript, and Python. All three wrap the same public service contract (`tenki.sandbox.v1`), so you get equivalent functionality whichever language you choose.

Install [#install]

**TypeScript**
```bash
npm install @tenkicloud/sandbox
```

**Python**
Requires Python 3.10+.

```bash
pip install tenki
```

**Go**
```bash
go get github.com/LuxorLabs/tenki-sdk-go/sandbox
```

Authenticate [#authenticate]

The public SDKs authenticate with Workspace API keys (`tk_...`), sent as `Authorization: Bearer <token>`. Each key
is bound to one workspace, and Sandbox requests infer that workspace automatically.

Resolution order [#resolution-order]

Every SDK resolves the auth token the same way: the token you pass explicitly (`WithAuthToken()` in Go, `authToken` in TypeScript, `auth_token=` in Python), then `TENKI_AUTH_TOKEN`, then `TENKI_API_KEY`.

The base URL resolves the same way: the explicit option (`WithBaseURL()` / `baseUrl` / `base_url=`), then `TENKI_API_ENDPOINT`, then the legacy `TENKI_API_URL`, then `https://api.tenki.cloud`.

Create a client [#create-a-client]

**TypeScript**
```typescript
import { TenkiSandbox } from "@tenkicloud/sandbox";

const sandbox = new TenkiSandbox(); // env-driven

// Or explicit:
const sandbox = new TenkiSandbox({
  authToken: "tk_...",
  baseUrl: "https://api.tenki.cloud",
});
```

**Python**
The Python SDK exposes a low-level `Client` for resource APIs (volumes, snapshots) and a high-level `Sandbox` for driving a single session.

```python
from tenki import Client, Sandbox

# Resource client, env-driven (reads TENKI_API_KEY and TENKI_API_ENDPOINT).
client = Client()

# Or explicit:
client = Client(
    auth_token="tk_your_api_key",
    base_url="https://api.tenki.cloud",
)

# Create and drive a session directly. `Sandbox.create` waits for RUNNING by
# default and doubles as a context manager that terminates on exit.
with Sandbox.create(name="demo", cpu_cores=2, memory_mb=4096) as sb:
    result = sb.exec("python3", "-c", "print('hello')")
    print(result.stdout_text)
```

`Sandbox.create` also accepts the client options (`auth_token`, `base_url`, `timeout`), since it constructs the client for you.

**Go**
```go
import tenkisandbox "github.com/LuxorLabs/tenki-sdk-go/sandbox"

// Zero-config: reads TENKI_API_KEY and TENKI_API_URL.
client, err := tenkisandbox.New()

// Explicit:
client, err := tenkisandbox.New(
  tenkisandbox.WithAuthToken("tk_your_api_key"),
  tenkisandbox.WithBaseURL("https://api.example.com"),
)
defer client.Close()
```

Useful client options:

| Option                          | Description                       | Default                   |
| ------------------------------- | --------------------------------- | ------------------------- |
| `WithAuthToken(token)`          | API authentication token          | `TENKI_API_KEY` env       |
| `WithBaseURL(url)`              | Sandbox service endpoint          | `https://api.tenki.cloud` |
| `WithHTTPTimeout(d)`            | HTTP timeout                      | `30s`                     |
| `WithHTTPClient(c)`             | Custom `*http.Client`             | auto-created              |
| `WithConnectClientOptions(...)` | Additional Connect client options | none                      |

To create and drive a session, every SDK and the CLI accept the same set of options. See [Create a session](/docs/sandbox/sessions#create-a-session) for the full list.

Defaults [#defaults]

Defaults applied by `Create` when you do not override them:

* `cpu`: `2`
* `memory`: `4096 MB`

Networking is off unless you set `allowInbound` / `allowOutbound`. The CLI enables both by default.

Validation: `cpu_cores` `1..16`, `memory_mb` `128..65536`, volume size `1 MiB` to `100 GiB`.

Identity [#identity]

Use `WhoAmI` to inspect the authenticated owner and its workspace. Normal resource calls do not need the
workspace ID; the API key already supplies that scope.

**TypeScript**
```typescript
const me = await sandbox.whoAmI();
console.log(`${me.ownerType}/${me.ownerId}`);
```

**Python**
```python
identity = client.who_am_i()
print(f"owner: {identity.owner_type}/{identity.owner_id}")
for ws in identity.workspaces:
    print(f"workspace: {ws.name} ({ws.id})")
```

**Go**
```go
identity, err := client.WhoAmI(ctx)
fmt.Printf("owner: %s/%s\n", identity.OwnerType, identity.OwnerID)
for _, ws := range identity.Workspaces {
  fmt.Printf("workspace: %s (%s)\n", ws.Name, ws.ID)
}
```

OpenCode integration [#opencode-integration]

Sessions can run [OpenCode](https://opencode.ai) inside the VM for AI-driven workflows. Enable it at create time with the `enableOpenCode` option (`WithOpenCode` in Go, `enable_opencode` in Python). In the TypeScript and Go SDKs you can also pass a provider option (`openCodeProvider` / `WithOpenCodeProvider`) to wire in your keys. See [Create a session](/docs/sandbox/sessions#create-a-session) for those options.

OpenCode then runs inside the session. The SDKs enable it at create time but do not expose an API for driving it programmatically once the session is running.

Git helpers [#git-helpers]

The SDK exposes structured Git operations on a session.

**TypeScript**
```typescript
await session.git.clone("https://github.com/org/repo", { depth: 1 });
await session.git.checkout("feature");
const diff = await session.git.diff({});
const log = await session.git.log({ maxCount: 10 });
await session.git.fetchPR(42, { remote: "origin" });
```

**Python**
```python
sb.git.clone("https://github.com/octocat/Hello-World.git", depth=1, directory="/home/tenki/repo")
sb.git.checkout("main")
diff = sb.git.diff()
log = sb.git.log(max_count=10)
sb.git.fetch_pr(123, directory="/home/tenki/repo")
```

**Go**
```go
out, err := session.Git.Clone(ctx, "https://github.com/octocat/Hello-World.git", tenkisandbox.GitCloneParams{
  Directory: "/home/tenki/repo",
  Depth:     1,
})
out, err = session.Git.Checkout(ctx, "main", tenkisandbox.GitCheckoutParams{})
out, err = session.Git.FetchPR(ctx, 123, tenkisandbox.GitFetchPRParams{
  Directory: "/home/tenki/repo",
})
```

You can also inject a GitHub token at session create time (`WithGitHubToken(token)` in Go, the `githubToken` option in TypeScript, or `github_token=` in Python) so private clones work without provisioning credentials inside the guest.

Registry version pruning [#registry-version-pruning]

Published registry versions pin their source snapshots. Delete an eligible historical version to release that pin; the version must be untagged, not the image's latest version, and unused by a share. The SDK methods take the registry image ID and snapshot ID, and return both IDs after deletion.

**TypeScript**
```typescript
const deleted = await sandbox.deleteRegistryImageVersion(imageId, snapshotId);
console.log(deleted.imageId, deleted.snapshotId);
```

**Python**
```python
deleted = client.registry.delete_version(image_id, snapshot_id)
print(deleted.image_id, deleted.snapshot_id)
```

**Go**
```go
deleted, err := client.DeleteRegistryImageVersion(ctx, imageID, snapshotID)
fmt.Println(deleted.ImageID, deleted.SnapshotID)
```

Deleting a latest, tagged, or shared version fails with a failed-precondition error, and the message names the rule that blocked it. Deleting the registry version does not delete the snapshot; call the snapshot deletion API separately after the pin is released.

Constants [#constants]

The default operation timeouts are the same across SDKs:

| Operation       | Default   |
| --------------- | --------- |
| Create          | 180s (3m) |
| Exec            | 30s       |
| Snapshot create | 300s (5m) |
| Restore         | 300s (5m) |
| Volume detach   | 120s (2m) |

Each SDK exports them as named constants: Go `DefaultSessionCreateTimeout` and friends, TypeScript `DEFAULT_SESSION_CREATE_TIMEOUT_MS` (milliseconds), and Python `DEFAULT_CREATE_TIMEOUT` (seconds, in `tenki_sandbox.constants`).

For volume sizes, the SDKs export byte-multiplier constants (`KB`, `MB`, `GB`, and the binary `KiB`, `MiB`, `GiB`, and so on):

**TypeScript**
```typescript
import { GiB } from "@tenkicloud/sandbox";

const tenGiB = 10 * GiB;
```

**Python**
```python
from tenki import GiB

ten_gib = 10 * GiB
```

**Go**
```go
tenGiB := 10 * tenkisandbox.GiB
```

Errors [#errors]

Every SDK surfaces service errors as typed values, sharing a common base (`SandboxError` in the TypeScript and Python SDKs). The common ones:

| Meaning                  | Go                         | TypeScript                   | Python                       |
| ------------------------ | -------------------------- | ---------------------------- | ---------------------------- |
| Session not found        | `ErrSessionNotFound`       | `SessionNotFoundError`       | `SessionNotFoundError`       |
| Session expired          | `ErrSessionExpired`        | `SessionExpiredError`        | n/a                          |
| Session terminated       | `ErrSessionTerminated`     | `SessionTerminatedError`     | `SessionTerminatedError`     |
| Invalid state            | `ErrInvalidState`          | `InvalidStateError`          | `InvalidStateError`          |
| Command timeout          | `ErrCommandTimeout`        | `CommandTimeoutError`        | `CommandTimeoutError`        |
| Unauthorized             | `ErrUnauthorized`          | `UnauthorizedError`          | `UnauthorizedError`          |
| Permission denied        | `ErrPermissionDenied`      | `PermissionDeniedError`      | `PermissionDeniedError`      |
| Quota exceeded           | `ErrQuotaExceeded`         | `QuotaExceededError`         | `QuotaExceededError`         |
| Port limit exceeded      | `ErrPortLimitExceeded`     | `PortLimitExceededError`     | `PortLimitExceededError`     |
| Inbound disabled         | `ErrInboundDisabled`       | `InboundDisabledError`       | `InboundDisabledError`       |
| Rate limited             | `ErrRateLimited`           | `RateLimitedError`           | `RateLimitedError`           |
| Volume not found         | `ErrVolumeNotFound`        | `VolumeNotFoundError`        | `VolumeNotFoundError`        |
| Volume in use            | `ErrVolumeInUse`           | `VolumeInUseError`           | `VolumeInUseError`           |
| Snapshot not found       | `ErrSnapshotNotFound`      | `SnapshotNotFoundError`      | `SnapshotNotFoundError`      |
| Snapshot failed          | `ErrSnapshotFailed`        | `SnapshotFailedError`        | n/a                          |
| Registry image not found | `ErrRegistryImageNotFound` | `RegistryImageNotFoundError` | `RegistryImageNotFoundError` |

The Go SDK also defines `ErrSSHUnavailable` and `ErrVolumeLimitExceeded`. Catch errors the idiomatic way in each tool:

**TypeScript**
```typescript
import { CommandTimeoutError } from "@tenkicloud/sandbox";

try {
  await session.exec("sleep", { args: ["999"], timeoutMs: 1000 });
} catch (err) {
  if (err instanceof CommandTimeoutError) {
    console.log("Command timed out");
  }
}
```

**Python**
```python
from tenki import CommandTimeoutError

try:
    sb.exec("sleep", "999", timeout=1)
except CommandTimeoutError:
    print("Command timed out")
```

`result.check()` raises `CommandFailedError` on a non-zero exit, and `RateLimitedError` carries `retryable = True`. Python also defines `CommandFailedError`, `VolumeSyncPendingError`, and `SnapshotNotDurableError`.

**Go**
```go
if errors.Is(err, tenkisandbox.ErrSnapshotFailed) {
  // inspect the snapshot record and recreate
}
```

Advanced API surface [#advanced-api-surface]

The public service contract also exposes lower-level RPCs that the convenience SDKs don't wrap:

* `PauseSession`, `ResumeSession`: present in the protobuf, not always wrapped, may not be available in every deployment

If you need them, integrate directly with the protocol:

* `proto/tenki/sandbox/v1/sandbox.proto`

> **Stick to the SDKs unless you need a raw RPC:** The Go, TypeScript, and Python SDKs are the officially supported surface for Tenki Sandbox. Only drop down to raw Connect/gRPC when an RPC isn't yet wrapped by an SDK, and keep in mind that a wrapper may be added in a future release.
