Sessions
Create and drive Tenki Sandbox sessions covering lifecycle, command execution, file I/O, port exposure, and SSH access.
A session is a single running sandbox. This page covers the full session surface: lifecycle, command execution, file I/O, ports, socket bridging, and SSH. Pick your tool in any code block, and the selection syncs across the page.
Create a session
Every create call takes the same options, named per tool:
| Option | TypeScript | Python | Go | CLI |
|---|---|---|---|---|
| Name | name | name | WithName | --name |
| CPU / memory | cpuCores, memoryMb | cpu_cores, memory_mb | WithCPUCores, WithMemoryMB | --cpu, --memory-mb |
| Disk size | diskSizeGb | disk_size_gb | WithDiskSizeGB | --disk-size-gb |
| Network | allowInbound, allowOutbound | allow_inbound, allow_outbound | WithAllowInbound, WithAllowOutbound | --allow-inbound, --allow-outbound |
| Metadata / env | metadata, env | metadata, env | WithMetadata, WithEnvs | --metadata, --env |
| Tags | tags | tags | WithTags | --tags |
| SSH keys | sshAuthorizedKeys | ssh_authorized_keys | WithSSHKeys | --authorized-key, --authorized-keys-file |
| Volumes | volumes | volumes | WithVolume | --volume |
| Snapshot / image | snapshotId, image | snapshot_id, image | WithSnapshot, WithImage | --snapshot, --image |
| Max duration | maxDurationMs | max_duration | WithMaxDuration | --max-duration |
| Pause retention | pauseRetentionMs | pause_retention | WithPauseRetention | --pause-retention |
| Sticky | sticky | sticky | WithSticky | --sticky |
| OpenCode | enableOpenCode, openCodeProvider | enable_opencode | WithOpenCode, WithOpenCodeProvider | n/a |
| Clone repo | cloneRepoUrl, githubToken | clone_repo_url, github_token | WithCloneRepo, WithGitHubToken | n/a |
| Wait for ready | waitReady, waitTimeoutMs | wait, timeout | WithWaitReady, WithWaitTimeout | --no-wait, --wait-timeout |
Unset options fall back to service defaults of 2 vCPU, 4096 MB, and a 5 GiB root disk.
Set diskSizeGb, disk_size_gb, WithDiskSizeGB, or --disk-size-gb to 20 for coding agents that clone repositories and install dependencies.
Inbound and outbound networking are enabled by default in every SDK and the CLI, so the network options are only needed to opt out of inbound exposure.
Environment variables are plaintext configuration, not a secret-management channel.
Do not pass long-lived credentials through env.
Sticky sessions
sticky takes precedence over maxDurationMs, max_duration, WithMaxDuration, and --max-duration.
When both are set, Tenki discards the requested maximum duration and returns a create-session warning.
A sticky session runs until you terminate it.
A manual pause stops compute but does not clear sticky, so the session is still sticky after resume.
Sticky is not fixed at create time; see Updating a running session to toggle it, and to trade sticky for a bounded lifetime on a session that is already running.
Pausing and resuming
Pause and resume preserve the VM's memory and durable filesystem state, but active terminal, SSH, preview, and other TCP connections close when the session pauses.
Reconnect after the session returns to RUNNING.
/tmp is cleared across a pause, so write logs and durable state under /home/tenki or on an attached volume.
A process you started over exec survives a pause with the same PID, but it runs inside the guest-agent's cgroup and does not survive a guest-agent restart, so start anything load-bearing from a template start command.
Create and wait
A waiting create can fail after the service has admitted the sandbox (3 minutes by default).
Each SDK reports this as a dedicated error that carries the sandbox handle, so you can close it, inspect it, or keep waiting:
| Meaning | TypeScript | Python | Go |
|---|---|---|---|
| Admitted, not ready inside the wait budget | WaitReadyFailedError | WaitReadyFailedError | *WaitReadyFailedError |
| Template runtime readiness failed | TemplateRuntimeFailedError | TemplateRuntimeFailedError | *TemplateRuntimeFailedError |
TemplateRuntimeFailedError carries the handle on the same field, so the same branch covers both. In Go, errors.Is(err, tenkisandbox.ErrWaitReadyFailed) matches too when you only need to classify the failure.
Scope-based cleanup — await using, with, defer session.Close(ctx) — only runs once create has returned, so it cannot cover a create that throws. Handle these errors explicitly wherever you create with waiting enabled.
A sandbox that never leaves CREATING is reaped for you. The case worth closing yourself is the one that reaches RUNNING just after your wait expired: it keeps running, and billing, until its max duration.
Manage a session
WaitReady/wait_ready is only needed for sessions obtained via get/list or created with wait disabled; the create calls above already return ready sessions.
Filtering a session list
list returns every non-terminated session in the workspace unless you narrow it. Tag filtering is an AND: a session must carry every tag you pass to match. Tags are trimmed and lowercased on both write and read, so filter casing never has to match how the tags were set.
| Filter | TypeScript | Python | Go |
|---|---|---|---|
| Tags (AND) | tags | tags | WithTagFilter |
| Sticky only / non-sticky | sticky | sticky | WithStickyFilter |
| Include terminated | includeTerminated | include_terminated | WithIncludeTerminated |
| Explicit workspace scope | workspaceId | workspace_id | WithWorkspaceID |
Terminated sessions are omitted by default. Ask for them explicitly when you are reconciling against your own records, since a session you cannot find is otherwise indistinguishable from one that has been cleaned up.
Workspace API keys infer their workspace server-side; the explicit workspace scope is for service tokens that can see more than one.
Updating a running session
Name, tags, and sticky are mutable after create. Only the fields you pass change.
| Field | TypeScript | Python | Go |
|---|---|---|---|
| Name | name | name | WithName |
| Tags | tags (or updateTags) | tags (or update_tags) | WithTags (or UpdateTags) |
| Sticky | sticky | sticky | WithSetSticky |
| Max duration | maxDurationMs | max_duration | WithSetMaxDuration |
Tags replace rather than merge, so pass the full set you want; an empty list clears them.
Setting a max duration requires passing sticky in the same call, and every SDK rejects the call locally when you omit it. Sticky still wins when both are set, so setting sticky true alongside a duration discards the duration and returns a warning. Pass sticky: false when the point is to give the session a lifetime. Update returns warnings the same way create does, so read them instead of assuming a field took effect.
Command execution
There are three ways to run a command, from least to most control:
- One-shot (
exec): starts the process, buffers its output, and returns a single result when it exits. Use it for short commands where you only need the outcome. - Output streaming (
session.stream()in TypeScript,session.Stream()in Go, iteratingproc.stdoutin Python): incremental stdout/stderr chunks as the command produces them. Use it for output-heavy or long commands you want to follow live. - Process handle (
sb.start()in Python,session.run()in TypeScript,session.Command(...).Stream()in Go): the running process itself — pid, writable stdin, signals, and kill. Use it for interactive processes and anything you may need to stop early.
All three report the same command result when the process exits.
For git, prefer the structured helpers over shelling out: clone, checkout, diff, log, and fetchPR are available on the session as git in every SDK, and return the raw git output. See Git helpers.
Command results
Every execution path reports the same result object when the process exits — ExecResult in TypeScript, CommandResult in Python, Result in Go. Captured stdout / stderr are raw bytes in all three; use the decoding helpers for text.
| Field | TypeScript ExecResult | Python CommandResult | Go Result |
|---|---|---|---|
| Exit code | exitCode | exit_code | ExitCode |
| Output (bytes) | stdout, stderr (Uint8Array) | stdout, stderr (bytes) | Stdout, Stderr ([]byte) |
| Output (text) | stdoutText(result), stderrText(result) | result.stdout_text, result.stderr_text | result.StdoutString(), result.StderrString() |
| Success check | isSuccess(result.status) | result.ok, result.check() | result.Status.IsSuccess(), .IsFailed(), .IsTimedOut() |
| Status | status (SUCCEEDED, FAILED, TIMED_OUT, …) | derived from exit_code + signal | Status |
| Duration | durationMs | duration_ms | Duration |
| Timing | startedAt, endedAt | — | StartedAt, EndedAt |
| Failure detail | — | signal, reason, errno | — |
| Invocation | sessionId, command, args | argv | SessionID, Command, Args |
TypeScript's run() handle resolves to a slimmer ProcessRunResult instead: exitCode, stdout, stderr, plus signal, durationMs, reason, and errno (the raw errno on fork/exec failures such as ENOENT or EACCES).
File operations
File operations are rooted in the session's working directory, /home/tenki. Relative paths resolve from there; absolute paths outside it (including /tmp) are rejected with a permission error.
Every SDK covers the same surface: read and write, directory listing, metadata (stat), directory creation, and deletes. mkdir creates missing parent directories and remove deletes recursively. For large files, the streaming variants transfer data in chunks instead of buffering the whole file in memory.
Port exposure and networking
Each session has independent inbound and outbound settings. Every SDK and the CLI request both at create time, so both are enabled by default:
allow_outboundlets the guest make outbound network calls, so package installs andgit clonework out of the boxallow_inboundenables inbound exposure workflows; withallow_inbound=false, port exposure is rejected
Both are create-time settings; you cannot toggle them afterwards. Each tool reports what a session was created with:
| Setting | TypeScript | Python | Go | CLI |
|---|---|---|---|---|
inbound | session.inboundEnabled | sandbox.info.inbound_enabled | session.InboundEnabled | tenki sandbox get --session <session-id> |
outbound | session.outboundEnabled | sandbox.info.outbound_enabled | session.OutboundEnabled | tenki sandbox get --session <session-id> |
When you expose a long-running server you started with exec, background it and detach its streams (>/home/tenki/server.log 2>&1 </dev/null &).
exec streams the command's output until it closes, so a server that keeps the output stream open leaves exec waiting forever.
Use the previewUrl returned by each expose call rather than constructing hostnames yourself; the host pattern is not a stable contract.
Named exposures
By default an exposure gets a generated hostname that changes from session to session. Pass a slug to ask for a named exposure instead, which is what you want when the URL has to be written into something that cannot be updated per session: an OAuth callback, a webhook registration, a config file baked into an image.
A slug names the exposure; it does not by itself decide how the URL is served. Some exposures are served through a workspace wildcard domain, and the returned exposure reports that separately, along with the readiness of the backing certificate:
| Field | TypeScript | Python | Go |
|---|---|---|---|
| Requested slug | slug | slug | Slug |
| Wildcard scheme used | wildcard | wildcard | Wildcard |
| Readiness | wildcardStatus | wildcard_status | WildcardStatus |
| Failure reason | wildcardStatusReason | wildcard_status_reason | WildcardStatusReason |
Readiness is one of UNSPECIFIED, PENDING, READY, FAILED, or DISABLED, and is only meaningful when the wildcard field is true. A wildcard-served preview URL comes back from expose immediately, but it does not serve traffic until its workspace domain reaches READY. Check readiness before handing the URL to a caller that fails closed on an error response, and read the reason field when it is FAILED.
Even with a slug, use the returned preview URL rather than assembling a hostname from the slug yourself.
Workspace preview slug
Wildcard-served exposures live under a preview slug reserved at the workspace level. Team-plan workspaces can reserve one slug (one per workspace) from Settings, using the slug input in the first card. It is shared across every session in the workspace, which is what keeps named exposures stable from one session to the next.
Once reserved, named exposures are served under that slug. A workspace slug of acme-studio produces preview URLs of the form https://<exposure>--acme-studio.sb.tenki.sh/, for example https://landing-preview--acme-studio.sb.tenki.sh/. Reserving a slug is a Team-plan feature; Starter workspaces get generated per-session hostnames instead. As above, treat the example as illustrative and hand callers the returned preview URL rather than assembling one from the slug yourself.
Unix socket bridging (dial)
dial opens a byte-stream connection from your local code to a Unix socket inside the guest. It is the private counterpart to port exposure: nothing is published on the internet — the connection rides the session's authenticated data plane. Use it to talk to guest services that listen on a socket rather than a TCP port (a language server, an agent's control socket, docker.sock).
dial is SDK-only; there is no CLI equivalent. The optional connect timeout bounds how long the guest waits for the socket to accept.
SSH access
Connect to a session over SSH. The CLI gives you an interactive shell, managed local config, and key management; the SDKs expose a raw byte-stream transport for wiring into your own SSH tooling. (Set keys at create time with the sshAuthorizedKeys option in the table above.)
Session metadata
Metadata tags a session with arbitrary string key-value pairs (metadata in the SDKs, --metadata key=value on the CLI, both shown above). Use it to filter sessions in dashboards, attribute billing, or tie a session to an upstream job ID. Metadata is opaque to the service; it is for your bookkeeping.