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, and SSH. Pick your tool in any code block, and the selection syncs across the page.
Create a session
from tenki import Sandbox
sb = Sandbox.create(
name="demo",
cpu_cores=4,
memory_mb=8192,
allow_inbound=True,
allow_outbound=True,
env={"APP_ENV": "dev"},
metadata={"owner": "alice"},
)Sandbox.create waits by default via a single server-held request and returns an exec-ready session with data-plane access primed. Pass wait=False to return immediately in CREATING. Use Client().create(...) instead when you want to manage the client lifecycle yourself.
const session = await sandbox.create({
name: "demo",
cpuCores: 4,
memoryMb: 8192,
allowInbound: true,
allowOutbound: true,
env: { APP_ENV: "dev" },
metadata: { owner: "alice" },
});create() waits by default via a single server-held request and returns a run-ready session with data-plane access primed. Pass waitReady: false to return immediately in CREATING.
// Create waits by default and returns a RUNNING, exec-ready session.
session, err := client.Create(
ctx,
tenkisandbox.WithName("demo"),
tenkisandbox.WithCPUCores(4),
tenkisandbox.WithMemoryMB(8192),
tenkisandbox.WithAllowInbound(true),
tenkisandbox.WithAllowOutbound(true),
tenkisandbox.WithEnvs(map[string]string{"APP_ENV": "dev"}),
tenkisandbox.WithMetadata(map[string]string{"owner": "alice"}),
tenkisandbox.WithWaitTimeout(3*time.Minute),
)
// Opt out when you want to orchestrate readiness separately.
session, err := client.Create(ctx, tenkisandbox.WithName("demo"), tenkisandbox.WithWaitReady(false))
// Later: session.WaitReady(ctx, 3*time.Minute)tenki sandbox create \
--name my-session \
--cpu 4 \
--memory-mb 8192 \
--allow-inbound \
--allow-outbound \
--env APP_ENV=dev \
--metadata owner=alice \
--metadata purpose=reviewBy default, the CLI waits until the session is RUNNING and exec-ready before returning (a single server-held request). Pass --no-wait to return immediately.
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 |
| Idle timeout | idleTimeoutMinutes | idle_timeout_minutes | WithIdleTimeout | --idle-timeout |
| 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 (2 vCPU, 4096 MB). The CLI enables inbound and outbound networking by default; with the SDKs, set allowInbound / allowOutbound to enable them.
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.
await session.refresh();
await session.extend(600_000); // +10 minutes
await session.pause();
await session.resume();
await session.close(); // or session.closeIfOpen(), or Symbol.asyncDispose
// List and inspect
const sessions = await sandbox.list();
const s = await sandbox.get(sessionId);sb.wait_ready(180)
sb.refresh()
sb.extend(1800) # +30 minutes (seconds or a timedelta)
sb.pause()
sb.resume()
sb.close() # or sb.close_if_open(); the context manager closes on exit
# List and inspect
sessions = client.list()
sb = client.get(session_id)err = session.WaitReady(ctx, 3*time.Minute)
err = session.Refresh(ctx)
err = session.Extend(ctx, 30*time.Minute)
err = session.Pause(ctx)
err = session.Resume(ctx)
err = session.Close(ctx)
err = session.CloseIfOpen(ctx)
// List and inspect
sessions, err := client.List(ctx)
session, err := client.Get(ctx, sessionID)# List and inspect
tenki sandbox list
tenki sandbox list --json
tenki sandbox get --session <session-id>
# Pause, resume, terminate
tenki sandbox pause --session <session-id>
tenki sandbox resume --session <session-id>
tenki sandbox terminate --session <session-id>Command execution
# One-shot: collects stdout/stderr and returns a result
result = sb.exec("bash", "-lc", "echo $APP_ENV && make test", env={"APP_ENV": "ci"}, timeout=120)
if not result.ok:
raise RuntimeError(f"failed: exit={result.exit_code} stderr={result.stderr_text}")
# Or start a live process and stream output
proc = sb.start("npm", "test")
proc.close_stdin()
for chunk in proc.stdout:
print(chunk.decode(), end="")
proc.wait().check()exec accepts cwd, env, timeout, input, and check=True (raises CommandFailedError on a non-zero exit). Use sb.shell("...") for shell parsing.
Result helpers: result.stdout_text, result.stderr_text, result.ok, result.check().
// One-shot
const result = await session.exec("npm", {
args: ["test"],
timeoutMs: 60_000,
onOutput: (chunk) => process.stdout.write(chunk.data),
});
// Or stream explicitly
const stream = await session.stream("npm", { args: ["test"] });
for (;;) {
const chunk = await stream.next();
if (!chunk) break;
process.stdout.write(chunk.data);
}
await stream.wait();result, err := session.Exec(
ctx,
"bash",
tenkisandbox.WithArgs("-lc", "echo $APP_ENV && make test"),
tenkisandbox.WithEnv("APP_ENV", "ci"),
tenkisandbox.WithTimeout(2*time.Minute),
)
if err != nil {
log.Fatal(err)
}
if !result.Status.IsSuccess() {
log.Fatalf("failed: exit=%d stderr=%s", result.ExitCode, result.StderrString())
}Exec options: WithArgs, WithTimeout, WithEnv, WithEnvs.
Result helpers: result.StdoutString(), result.StderrString(), result.Status.IsSuccess(), IsFailed(), IsTimedOut().
tenki sandbox exec --session <session-id> -c 'go test ./...'
tenki sandbox exec --session <session-id> --timeout 2m -c 'npm ci && npm test'-c (short for --shell) runs the line through a shell so &&, pipes, redirects and globs work. Without it, exec runs the program directly with no shell; to pass flags straight to a program, put them after -- (for example tenki sandbox exec -- bash -lc '...').
CLI output includes:
- streamed stdout and stderr
- final status, exit code, and duration
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.
sb.fs.write_text("/home/tenki/config.json", '{"key": "value"}')
data = sb.fs.read_text("/home/tenki/config.json")
# Bytes, directory listing, and local <-> guest transfers
sb.fs.write_bytes("/home/tenki/blob.bin", b"\x00\x01")
entries = sb.fs.list("/home/tenki")
sb.fs.upload("./local.tar", "/home/tenki/local.tar")
sb.fs.download("/home/tenki/build.log", "./build.log")await session.writeFile("/home/tenki/config.json", '{"key": "value"}');
const data = await session.readFile("/home/tenki/config.json");err = session.WriteFile(ctx, "/home/tenki/hello.txt", []byte("hello"))
data, err := session.ReadFile(ctx, "/home/tenki/hello.txt")# inline
tenki sandbox write --session <session-id> --path /home/tenki/app.env --data 'PORT=3000'
# from a local file
tenki sandbox write --session <session-id> --path /home/tenki/config.json --data-file ./config.json
# from stdin
cat ./local-file.txt | tenki sandbox write --session <session-id> --path /home/tenki/input.txt
# read to stdout
tenki sandbox read --session <session-id> --path /home/tenki/config.json
# read into a local file
tenki sandbox read --session <session-id> --path /home/tenki/build.log --out ./build.logPort exposure and networking
Each session has independent inbound and outbound settings:
allow_outbound=truelets the guest make outbound network callsallow_inbound=trueenables inbound exposure workflows
preview = sb.expose_port(3000, ttl=3600) # ttl in seconds
print(preview.url)
ports = sb.list_exposed_ports()
sb.unexpose_port(3000)const port = await session.exposePort(3000, { ttlMs: 3600_000 });
console.log(port.previewUrl);port, err := session.ExposePort(ctx, 3000)
ports, err := session.ListExposedPorts(ctx)
err = session.UnexposePort(ctx, 3000)tenki sandbox expose --session <session-id> --port 3000
tenki sandbox ports --session <session-id>
tenki sandbox unexpose --session <session-id> --port 3000When you expose a long-running server you started with exec, background it and detach its streams (>/tmp/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.
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.)
Raw byte-stream transport for your own SSH tooling, plus replacing the authorized keys on a running session:
const conn = await session.ssh();
await conn.write(new TextEncoder().encode("ls -la\n"));
const chunk = await conn.read(); // Uint8Array | null
if (chunk) process.stdout.write(chunk);
conn.close();
// Replace the authorized keys
await session.updateSshAuthorizedKeys(["ssh-ed25519 AAAA..."]);Raw byte-stream transport (an io.RawIOBase), plus replacing the authorized keys on a running session:
conn = sb.ssh()
conn.write(b"ls -la\n")
print(conn.recv(4096).decode())
conn.close()
# Replace the authorized keys
sb.update_ssh_authorized_keys(["ssh-ed25519 AAAA..."])Raw byte-stream transport for your own SSH tooling, plus replacing the authorized keys on a running session:
conn, err := session.SSH(ctx)
defer conn.Close()
// Replace the authorized keys
err = session.UpdateSSHAuthorizedKeys(ctx, []string{"ssh-ed25519 AAAA..."})Open an interactive shell:
tenki sandbox ssh --session <session-id>Useful flags: --user (default tenki), --identity-file, --batch-mode, --connect-timeout, --strict-host-key-checking. Pass standard SSH arguments after the session ID:
tenki sandbox ssh <session-id> -L 8080:127.0.0.1:8080Install a managed entry in your local SSH config so you can use friendly aliases:
tenki sandbox ssh config install
tenki sandbox ssh config status
tenki sandbox ssh config uninstall
ssh sbx-<session-uuid>Replace the authorized keys on a running session:
tenki sandbox ssh-keys set --session <session-id> --keys-file ~/.ssh/authorized_keysSession 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.