Sandbox

SDK Reference

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

npm install @tenkicloud/sandbox

Requires Python 3.10+.

pip install tenki
go get github.com/LuxorLabs/tenki-sdk-go/sandbox

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

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

import { TenkiSandbox } from "@tenkicloud/sandbox";

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

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

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

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.

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:

OptionDescriptionDefault
WithAuthToken(token)API authentication tokenTENKI_API_KEY env
WithBaseURL(url)Sandbox service endpointhttps://api.tenki.cloud
WithHTTPTimeout(d)HTTP timeout30s
WithHTTPClient(c)Custom *http.Clientauto-created
WithConnectClientOptions(...)Additional Connect client optionsnone

To create and drive a session, every SDK and the CLI accept the same set of options. See Create a session for the full list.

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

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.

const me = await sandbox.whoAmI();
console.log(`${me.ownerType}/${me.ownerId}`);
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})")
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

Sessions can run OpenCode 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 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

The SDK exposes structured Git operations on a session.

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" });
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")
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

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.

const deleted = await sandbox.deleteRegistryImageVersion(imageId, snapshotId);
console.log(deleted.imageId, deleted.snapshotId);
deleted = client.registry.delete_version(image_id, snapshot_id)
print(deleted.image_id, deleted.snapshot_id)
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

The default operation timeouts are the same across SDKs:

OperationDefault
Create180s (3m)
Exec30s
Snapshot create300s (5m)
Restore300s (5m)
Volume detach120s (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):

import { GiB } from "@tenkicloud/sandbox";

const tenGiB = 10 * GiB;
from tenki import GiB

ten_gib = 10 * GiB
tenGiB := 10 * tenkisandbox.GiB

Errors

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

MeaningGoTypeScriptPython
Session not foundErrSessionNotFoundSessionNotFoundErrorSessionNotFoundError
Session expiredErrSessionExpiredSessionExpiredErrorn/a
Session terminatedErrSessionTerminatedSessionTerminatedErrorSessionTerminatedError
Invalid stateErrInvalidStateInvalidStateErrorInvalidStateError
Command timeoutErrCommandTimeoutCommandTimeoutErrorCommandTimeoutError
UnauthorizedErrUnauthorizedUnauthorizedErrorUnauthorizedError
Permission deniedErrPermissionDeniedPermissionDeniedErrorPermissionDeniedError
Quota exceededErrQuotaExceededQuotaExceededErrorQuotaExceededError
Port limit exceededErrPortLimitExceededPortLimitExceededErrorPortLimitExceededError
Inbound disabledErrInboundDisabledInboundDisabledErrorInboundDisabledError
Rate limitedErrRateLimitedRateLimitedErrorRateLimitedError
Volume not foundErrVolumeNotFoundVolumeNotFoundErrorVolumeNotFoundError
Volume in useErrVolumeInUseVolumeInUseErrorVolumeInUseError
Snapshot not foundErrSnapshotNotFoundSnapshotNotFoundErrorSnapshotNotFoundError
Snapshot failedErrSnapshotFailedSnapshotFailedErrorn/a
Registry image not foundErrRegistryImageNotFoundRegistryImageNotFoundErrorRegistryImageNotFoundError

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

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");
  }
}
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.

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

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.