Sessions
Create and drive Tenki Sandbox sessions covering lifecycle, command execution, file I/O, port exposure, and SSH access.
A session is one running sandbox VM. This page covers the full session-level surface: lifecycle, command execution, file I/O, ports, and SSH. Pick your tool in any code block — the selection syncs across the page.
Create a session
tenki sandbox create \
--name my-session \
--cpu 4 \
--memory-mb 8192 \
--allow-inbound \
--allow-outbound \
--env APP_ENV=dev \
--metadata owner=alice \
--metadata purpose=reviewSupported flags on tenki sandbox create:
--name--cpu,--memory-mb--allow-inbound,--allow-outbound--max-duration--snapshot--metadata key=value(repeatable)--env key=value(repeatable)--authorized-key,--authorized-keys-file(repeatable)--volume <volume-id>:<mount-path>[:ro](repeatable)--no-wait,--wait-timeout
By default, the CLI waits for the session to become READY before returning. Pass --no-wait to return immediately.
const session = await sandbox.createAndWait({
name: "demo",
cpuCores: 4,
memoryMb: 8192,
allowInbound: true,
allowOutbound: true,
env: { APP_ENV: "dev" },
metadata: { owner: "alice" },
});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"}),
)
// Or create-and-wait in one call:
session, err := client.CreateAndWait(ctx, 3*time.Minute, tenkisandbox.WithName("demo"))Useful create options:
WithName,WithCPUCores,WithMemoryMB,WithMaxDurationWithAllowInbound,WithAllowOutboundWithMetadata,WithEnvsWithSSHKeysWithVolume(volumeID, mountPath, ...VolumeOption)WithSnapshot,WithImageWithOpenCode,WithOpenCodeProviderWithCloneRepo,WithGitHubToken
Defaults applied by Create: inbound=false, outbound=true, cpu=2, memory=4096 MB.
from tenki_sandbox 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 for the session to reach RUNNING by default — pass wait=False to return immediately. Use Client().create(...) instead when you want to manage the client lifecycle yourself.
Useful create arguments:
name,cpu_cores,memory_mb,max_durationallow_inbound,allow_outboundmetadata,env,tagsssh_authorized_keysvolumes(list of{"volume_id", "mount_path", "read_only"}dicts)snapshot_id,imageenable_opencodeclone_repo_url,github_token
Lifecycle
err = session.WaitReady(ctx, 3*time.Minute)
err = session.Refresh(ctx)
err = session.Extend(ctx, 30*time.Minute)
err = session.Close(ctx)
err = session.CloseIfOpen(ctx)In TypeScript:
await session.extend(600_000); // +10 minutes
await session.pause();
await session.resume();
await session.close(); // or use Symbol.asyncDisposeIn Python:
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 exitList and inspect
tenki sandbox list
tenki sandbox list --json
tenki sandbox get --session <session-id>sessions, err := client.List(ctx)
session, err := client.Get(ctx, sessionID)sessions = client.list()
sb = client.get(session_id)Command execution
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
// 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().
# 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().
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. /home/tenki/... paths only exist when a volume is mounted there.
# 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.logawait 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")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")Port 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
tenki sandbox expose --session <session-id> --port 3000
tenki sandbox ports --session <session-id>
tenki sandbox unexpose --session <session-id> --port 3000const 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)preview = sb.expose_port(3000, ttl=3600) # ttl in seconds
print(preview.url)
ports = sb.list_exposed_ports()
sb.unexpose_port(3000)Port numbers must be between 1 and 65535.
SSH access
Direct CLI SSH
tenki sandbox ssh --session <session-id>Useful flags: --user (default root), --identity-file, --batch-mode, --connect-timeout, --strict-host-key-checking.
You can pass standard SSH arguments after the session ID:
tenki sandbox ssh <session-id> -L 8080:127.0.0.1:8080Managed SSH config
Install a managed entry into your local SSH config so you can use friendly aliases:
tenki sandbox ssh config install
tenki sandbox ssh config status
tenki sandbox ssh config uninstallAfter install:
ssh sbx-<session-uuid>Authorized keys
Add keys at create time:
tenki sandbox create \
--authorized-key 'ssh-ed25519 AAAA...' \
--authorized-keys-file ~/.ssh/authorized_keysReplace the set later:
tenki sandbox ssh-keys set --session <session-id> --keys-file ~/.ssh/authorized_keyssession, err := client.Create(
ctx,
tenkisandbox.WithSSHKeys([]string{"ssh-ed25519 AAAA..."}),
)
err = session.UpdateSSHAuthorizedKeys(ctx, []string{"ssh-ed25519 AAAA..."})sb = Sandbox.create(ssh_authorized_keys=["ssh-ed25519 AAAA..."])
sb.update_ssh_authorized_keys(["ssh-ed25519 AAAA..."])Raw SSH transport
The Go SDK exposes the underlying tunnel for integration with your own SSH tooling:
conn, err := session.SSH(ctx)
defer conn.Close()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();In Python, sb.ssh() returns a byte-stream connection (an io.RawIOBase):
conn = sb.ssh()
conn.write(b"ls -la\n")
print(conn.recv(4096).decode())
conn.close()Session metadata
Use --metadata key=value (CLI) or WithMetadata (SDK) to tag sessions with arbitrary string pairs, useful for filtering in dashboards, billing attribution, or tying a session to an upstream job ID.
tenki sandbox create --metadata owner=alice --metadata job=ci-1234Metadata is opaque to the service; it is for your bookkeeping.