Tenki
Run coding agents on full Tenki Linux VMs. Memory pause, disk and memory snapshots, exact sizes, network allow lists, and a hard lifetime.
tenki() runs each sandbox as a full Linux VM on Tenki through the official @tenkicloud/sandbox SDK.
Install
npm install @opencoredev/sandbox-sdk @tenkicloud/sandboxpnpm add @opencoredev/sandbox-sdk @tenkicloud/sandboxyarn add @opencoredev/sandbox-sdk @tenkicloud/sandboxbun add @opencoredev/sandbox-sdk @tenkicloud/sandboxThe adapter needs @tenkicloud/sandbox 1.4.1 or later. Set TENKI_API_KEY to a workspace API key on your server, and keep it out of browser bundles.
Quickstart
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { tenki } from "@opencoredev/sandbox-sdk/tenki";
await using box = await createSandbox(tenki());
const result = await box.run("uname -a");
console.log(result.stdout);
await using terminates the VM when the block ends.
What it supports
| Feature | Support | Notes |
|---|---|---|
| Commands and streaming | Yes | Output streams in bounded pages |
| Files | Yes | |
| Background processes | Yes | |
| Preview URLs | Yes | Public https://, with an expiry when Tenki reports one |
| Pause and resume | Yes | Disk and memory survive |
| Snapshots | Yes | Disk and memory, with optional expiry |
| Machine size | Yes | Presets or exact CPU, memory, and disk |
| Network policy | Yes | "none" or a host allow list, *. wildcards allowed |
Hard lifetime (timeoutMs) |
Yes | |
Idle timeout (idleTimeoutMs) |
No | Tenki removed idle auto-pause |
| Keys | Best-effort | Stored in session metadata |
| Terminal | No |
The capability matrix compares these with every other provider.
Run commands
Await box.run for the result, or iterate it to stream output as it arrives.
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { tenki } from "@opencoredev/sandbox-sdk/tenki";
await using box = await createSandbox(tenki());
const result = await box.run("ls -la", { cwd: "/workspace", timeoutMs: 30_000 });
console.log(result.exitCode, result.stdout);
for await (const chunk of box.run("for i in 1 2 3; do echo $i; sleep 1; done")) {
process.stdout.write(chunk.text);
}
A timeout or abort kills the command’s whole process group.
Files
Relative paths resolve against /workspace.
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { tenki } from "@opencoredev/sandbox-sdk/tenki";
await using box = await createSandbox(tenki());
await box.files.write("notes/todo.txt", "ship the docs\n");
console.log(await box.files.read("notes/todo.txt"));
console.log(await box.files.list("notes"));
await box.files.write("secrets/token", "abc123", { mode: 0o600 });
{ mode } is set when the file is written, by Tenki’s streaming file API under /home/tenki (where /workspace lives) and by the guest helper elsewhere, so the file never has a looser mode.
Background processes and previews
No ports to declare. The preview URL is public, so keep authentication in the app you expose.
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { tenki } from "@opencoredev/sandbox-sdk/tenki";
await using box = await createSandbox(tenki());
await box.start("python3 -m http.server 3000", { name: "web", ready: { port: 3000 } });
const preview = await box.preview(3000);
console.log(preview.url, preview.expiresAt);
Outbound network rules don’t make a preview private.
Pause, resume, and snapshots
Pause and resume keep disk and memory. Snapshots capture both, but not attached native volumes.
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { tenki } from "@opencoredev/sandbox-sdk/tenki";
await using box = await createSandbox(tenki(), { key: "user:123" });
await box.pause();
await box.resume();
const { id } = await box.snapshot();
await using copy = await createSandbox(tenki(), { from: id });
Pause closes active TCP, preview, and SSH connections; reconnect clients after resume. Snapshots stay in Tenki after the VM is gone, unless you give them an expiry:
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { tenki } from "@opencoredev/sandbox-sdk/tenki";
await using box = await createSandbox(tenki());
const { id } = await box.snapshot({ expiresInMs: 7 * 24 * 60 * 60_000 }); // Tenki deletes it in 7 days
expiresInMs: 0 or no option keeps the snapshot until you delete it. A paused sandbox is kept for Tenki’s pause retention; set pauseRetentionMs to change it.
Size, network, and lifetime
| Preset | CPUs | Memory | Disk |
|---|---|---|---|
small |
1 | 1024 MiB | 10 GiB |
medium |
2 | 4096 MiB | 20 GiB |
large |
4 | 8192 MiB | 40 GiB |
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { tenki } from "@opencoredev/sandbox-sdk/tenki";
await using box = await createSandbox(tenki(), {
size: { cpu: 4, memoryMb: 16_384, diskGb: 50 },
network: { allow: ["registry.npmjs.org", "*.github.com"] },
timeoutMs: 30 * 60_000,
});
- Exact sizes take 1 to 128 CPUs, an even
memoryMbfrom 128 to 524288, and 5 to 100 GiB of disk. Workspace quotas can be lower. - Without
size, the base image gets 2 CPUs, 4096 MB, and 5 GiB. - An empty allow list blocks outbound traffic. The adapter checks the returned policy and fails if it doesn’t match.
timeoutMssets Tenki’smaxDurationMs.idleTimeoutMsfails withunsupported.
Configuration
| Option | Type | Default | Env var |
|---|---|---|---|
apiKey |
string |
TENKI_AUTH_TOKEN, then TENKI_API_KEY |
|
baseUrl |
string |
https://api.tenki.cloud |
TENKI_API_ENDPOINT, then TENKI_API_URL |
image |
string |
Tenki’s default image | |
requestTimeoutMs |
number |
30_000 |
|
waitTimeoutMs |
number |
180_000; snapshots 300_000 |
|
resumeTimeoutMs |
number |
180_000, at most 900_000 |
|
pauseRetentionMs |
number |
Tenki’s retention | |
previewTtlMs |
number |
Tenki’s preview TTL | |
volumes |
{ volumeId, mountPath, readOnly? }[] |
||
workspaceId |
string |
Inferred from the API key |
authToken is a deprecated alias for apiKey. volumes mounts Tenki volumes you created with the native SDK. workspaceId is only needed for service credentials that span workspaces; it scopes both creation and listing. A custom image needs Python 3, sh, Git, tar, setsid, and nohup, and must allow privileged execution. box.native is a typed Tenki Session; its close() terminates the VM, so use box.close() to detach.
Limitations
- Keys are best-effort. Pass a
locatorfor exact get-or-create. - No idle timeout or terminals.
- Pause closes active connections.
- Snapshots have their own lifecycle; delete them through the native client.
How it works
- Tenki clears
/tmpon pause. The adapter keeps SDK state under/home/tenki/.sandbox-sdkand relinks/tmp/.sandbox-sdkon resume and connect. - Sticky mode is off, so it can’t override
timeoutMs. - Tenki 1.4.1 can’t cancel control-plane calls. A timed-out create may still finish; the adapter tries to close a late VM, but keep your process alive for that and check your sessions if it fails.
- Command output spools to a file in the VM, which uses guest disk until the run finishes.