Skip to content

Find a component

Loading the directory…

↑ ↓ to select · Enter to open · Esc to closeBrowse all →
Contributor login

Tenki

Tenki Sandboxes for Bijection: cloud sandboxes for AI agents, with live state in your Bijection tables.

Bijection source
git clone https://github.com/bijectionhq/tenki.git
cd tenki
git checkout eacb1453ef98f3ed5b9e5c871ce1d27a1f867801

Bijection repository access is required. Request access.

Description

Tenki Sandboxes for Bijection: cloud sandboxes for AI agents, with live state in your Bijection tables.

Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.

Available to evaluate

Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.

Getting started

  1. Get the source at the revision above and read its package documentation.
  2. Check the required Bijection SDK version, dependencies, and external service credentials.
  3. Follow the package setup instructions and the guide to using components.
  4. Exercise the component in your deployment according to your project’s verification rules.

Open source

Maintained source: bijectionhq/tenki. Based on LuxorLabs/tenki-convex. License: MIT. Consult the repository for license terms, notices, and contributor attribution.

Compute runs with the external sandbox provider. Bijection connects application state and operations to that provider.

From the upstream publisher

Tenki Sandboxes for Convex: cloud sandboxes for AI agents, with live state in your Convex tables.

Explore the demo ↗

Upstream overview

From the upstream package documentation.

Give every user or AI agent in your Convex app its own full Linux VM that boots in about a second. This component creates and drives those VMs from your Convex actions and mirrors each sandbox's state into a component table, so your UI subscribes with a plain Convex query and updates live as a sandbox goes from provisioning to ready to paused, with no polling. Sandboxes are keyed by (ownerId, key), so one call gets a user their machine: run shell commands and background processes, read and write files, expose ports as public preview URLs, pause and resume with memory intact, and snapshot or fork environments. create is idempotent and crash-safe, maxActiveSandboxes caps spend, and your Tenki API key stays on the server as a Convex environment variable.

Use cases

From the upstream package documentation.

  • Coding agents with a real computer: give each chat or agent run its own VM to clone a repo, install dependencies and run tests, and expose exec, readFile and writeFile to the agent as tools called from Convex actions.
  • Per-user dev environments and live previews: start a dev server with spawn, publish it with exposePort, and render the preview URL and sandbox phase straight from a Convex query.
  • Code interpreters and playgrounds for untrusted code: run user- or LLM-generated code in an isolated VM with CPU and memory limits, a hard maxDurationMs, and an outbound allowlist (allowDomains).
  • Jobs longer than an action: spawn builds, test suites, data jobs or headless browsers in the background, then check processStatus from a scheduled function or cron. Processes keep running after the action returns and across pause/resume.
  • Pick up where users left off: pause a sandbox when a user goes idle (CPU and memory stop billing), and the next create resumes it with files, memory and running processes intact.
  • Checkpoint and branch: snapshot a prepared environment and start new sandboxes from it, or fork a live sandbox to try a risky change or run parallel agent attempts without touching the original.

How it works

From the upstream package documentation.

Run npm install @tenkicloud/convex, create an API key in the Tenki dashboard, store it with npx convex env set TENKI_API_KEY tk_..., and register the component in convex/convex.config.ts with app.use(tenki). The Tenki SDK needs Node.js, so construct new Tenki(components.tenki, { defaults, maxActiveSandboxes }) in a "use node" file and call it from actions. defaults applies to every create and accepts any @tenkicloud/sandbox option. Without it you get 2 vCPU, 4 GB RAM and a 5 GiB disk; coding agents that clone repos usually want diskSizeGb: 20. Sandboxes scale up to 16 vCPU and 64 GB of RAM, and new workspaces start on the free Starter plan with $100 in credits.

Every method takes the action ctx and { ownerId, key }. Derive ownerId from ctx.auth in your own function, never from client input, because it is the only tenant boundary. create returns the sandbox for that identity, creating it if needed and resuming it if paused; it is safe to retry and to call concurrently, and a create that dies mid-flight is adopted by the next call. Every operation writes its result back to the component's table, so components.tenki.sandboxes.get and .list work as ordinary reactive queries in the default runtime for phase, previews and lastError.

exec runs a command and waits: a string runs under bash -lc, an array runs as argv. It is capped at 9 minutes to fit inside Convex's action limit, with 1 MiB of output per stream by default, so use spawn and processStatus for anything longer. readFile and writeFile take text or bytes within Convex's limits (16 MiB returned, 5 MiB as arguments). Pausing keeps memory, disk and running processes but closes open connections and clears /tmp, so keep durable files under /home/tenki.

Sandboxes bill per second for vCPU and memory; a paused sandbox stops that billing, and its saved state counts toward workspace storage. Each run lasts at most maxDurationMs (activity doesn't extend it; call extend). At the deadline Tenki pauses the sandbox and keeps it for 7 days by default (pauseRetentionMs). Schedule reconcile from a cron so rows catch up with sandboxes that hit their deadline, and set maxActiveSandboxes (5 or fewer on Starter); it is checked in the same transaction that reserves the row, so concurrent creates, forks and resumes can't exceed it. Errors are ConvexErrors with a typed code such as capacity_exceeded, insufficient_credits or not_ready. allowDomains requires egress policies to be enabled on your workspace, so ask Tenki support first. The repo's example/ folder is a complete React app with anonymous auth, preview URLs, pause/resume and fork.

Upstream package FAQ

How does Tenki Sandboxes handle concurrent create calls for the same user?

Tenki Sandboxes makes create idempotent: calling tenki.create() concurrently for the same ownerId and key always results in a single sandbox. If a concurrent create fails mid-flight, the next call adopts the tagged session rather than creating a duplicate. This means it is safe to retry creates from Convex actions without risk of provisioning multiple sandboxes for the same identity.

How does multi-tenant isolation work in Tenki Sandboxes?

Each sandbox is keyed by a combination of ownerId and a string key. The README explicitly states that ownerId is the only thing separating tenants and must always be derived from ctx.auth, never from client input. Typically you pass the authenticated user ID returned by getAuthUserId as the ownerId.

How do I limit how many sandboxes can run simultaneously?

Tenki Sandboxes accepts a maxActiveSandboxes option when constructing the Tenki instance: new Tenki(components.tenki, { maxActiveSandboxes: 20 }). Once the limit is reached, any attempt to start or resume a sandbox throws a capacity_exceeded error. The check runs inside the same Convex transaction that reserves the sandbox row, so concurrent creates and forks cannot exceed the limit.

What happens when a Tenki sandbox reaches its time limit?

Tenki pauses the sandbox at its maxDurationMs deadline and retains it for the configured pause retention period, defaulting to 7 days. The Convex row keeps the last known phase until a refresh, reconcile, or the next create call syncs it. Calling tenki.create() on a paused sandbox resumes it and starts a new duration window of the same length, so maxDurationMs bounds each run rather than the sandbox's total lifetime.

Does Tenki Sandboxes require a specific Convex runtime for actions?

Yes. The Tenki SDK depends on Node.js, so the action file that calls Tenki methods must include the 'use node' directive at the top. Queries that read sandbox state from the Convex table do not require Node.js and run in the default Convex runtime, which means UI subscriptions work without any special configuration.

Use with agents and CLI

Give your coding agent the component’s source identity and setup guidance.

agent-prompt.md
Help me integrate Tenki with Bijection.

Read: https://bijection.com/components/tenki/SKILL.md

Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.

Upstream README.md

View repository ↗

Documentation from LuxorLabs/tenki-convex, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.

Tenki Sandboxes for Convex

Give every user or agent in your Convex app its own cloud machine. Tenki sandboxes are microVMs that boot in about a second. This component runs them from your Convex actions and keeps each sandbox's state in a Convex table, so your UI updates live as a sandbox goes from provisioning to ready to paused.

  • One sandbox per identity. Sandboxes are keyed by (ownerId, key). create is safe to retry and to call concurrently: every call ends up with the same sandbox.
  • Crash-safe. A create that dies mid-flight leaves a tagged session that the next call adopts. destroy also cleans up any such orphans.
  • Everything an agent needs: shell commands, background processes, files, public preview URLs, pause/resume with memory intact, snapshots and forks.
  • The API key stays on the server as a Convex environment variable.

Install

npm install @tenkicloud/convex
npx convex env set TENKI_API_KEY tk_...

Create the API key in the Tenki dashboard. Then add the component to your app:

// convex/convex.config.ts
import { defineApp } from "convex/server";
import tenki from "@tenkicloud/convex/convex.config.js";

const app = defineApp();
app.use(tenki);

export default app;

Usage

The Tenki SDK needs Node.js, so create and call Tenki from a "use node" file:

// convex/sandboxes.ts
"use node";
import { getAuthUserId } from "@convex-dev/auth/server";
import { v } from "convex/values";
import { Tenki } from "@tenkicloud/convex";
import { action, type ActionCtx } from "./_generated/server";
import { components } from "./_generated/api";

const tenki = new Tenki(components.tenki, {
  defaults: { cpuCores: 2, memoryMb: 4096, maxDurationMs: 30 * 60_000 },
});

async function ownerId(ctx: ActionCtx) {
  const userId = await getAuthUserId(ctx);
  if (!userId) throw new Error("Unauthenticated");
  return userId;
}

export const start = action({
  args: {},
  handler: async (ctx) =>
    await tenki.create(ctx, { ownerId: await ownerId(ctx), key: "main" }),
});

export const run = action({
  args: { command: v.string() },
  handler: async (ctx, { command }) =>
    await tenki.exec(ctx, {
      ownerId: await ownerId(ctx),
      key: "main",
      command,
    }),
});

Queries read the component directly and run in Convex's default runtime, so the UI can subscribe to a sandbox:

// convex/sandboxQueries.ts
import { getAuthUserId } from "@convex-dev/auth/server";
import { query } from "./_generated/server";
import { components } from "./_generated/api";

export const mine = query({
  args: {},
  handler: async (ctx) => {
    const ownerId = await getAuthUserId(ctx);
    if (!ownerId) return null;
    return await ctx.runQuery(components.tenki.sandboxes.get, {
      ownerId,
      key: "main",
    });
  },
});
const sandbox = useQuery(api.sandboxQueries.mine);
// sandbox?.phase: "provisioning" | "ready" | "pausing" | "paused" | "resuming" | "terminated" | "error"

ownerId is the only thing separating tenants. Always derive it from ctx.auth, never from client input.

API

Every method takes the action ctx and the sandbox's { ownerId, key }.

Lifecycle

  • create({ options?, resume? }) returns the sandbox, creating it if needed. A paused sandbox, including one Tenki paused at its deadline, is resumed first; pass resume: false to get it as it is. If a concurrent call for the same identity fails, this one throws the same error. options takes any @tenkicloud/sandbox create option: resources, image, template, env, allowDomains, maxDurationMs, snapshotId, ... With snapshotId, the size comes from the snapshot and the defaults for image, template and resources are skipped. Tags must be valid Tenki tags ([a-z0-9][a-z0-9_:.-]*), and the cvx: prefix is reserved.
  • pause({ wait? }) keeps memory and disk, so processes resume where they left off. It takes tens of seconds; with wait: false it returns pausing and a later refresh sees paused.
  • resume() returns once commands run again. It counts against maxActiveSandboxes.
  • extend({ additionalMs }) pushes the deadline out.
  • snapshot({ name?, expiresAt? }) captures the sandbox; listSnapshots() lists them. A snapshot without expiresAt is kept until you delete it in Tenki.
  • fork({ ownerId, from, to }) snapshots from and creates to from it. The two then run independently. It throws already_exists while to is live or being created, and checks maxActiveSandboxes, before taking the snapshot. The fork's snapshot expires after an hour, and Tenki deletes it once no sandbox uses it.
  • refresh() re-reads the sandbox from Tenki.
  • destroy() terminates it, and a create still in flight for it fails with terminated. The key can then be reused.
  • reconcile({ limit? }) refreshes the least recently updated live sandboxes across all owners. Run it from a cron so rows catch up with sandboxes that reached their deadline (see example/convex/crons.ts).

Commands and files

  • exec({ command, cwd?, env?, timeoutMs?, maxOutputBytes? }) runs a command and waits. A string runs under bash -lc; an array runs as argv, without shell parsing (both need bash, head and cat in the image). Output is capped inside the sandbox at maxOutputBytes per stream (1 MiB by default), and the timeout at 9 minutes, inside Convex's action limit.
  • spawn({ command, cwd?, env? }) starts a background command and returns a processId at once. It keeps running after the action ends and across pause/resume.
  • processStatus({ processId, tailBytes? }) returns running (also while children it started in the background run), exited (with exitCode), killed, or lost (ended without an exit, e.g. the sandbox restarted), plus the tail of its output.
  • kill({ processId, signal? }) signals the process and its children with TERM (the default), KILL, INT or HUP. INT and HUP need an env with --default-signal in the image (GNU coreutils 8.31 or later); without it they have no effect.
  • readFile({ path, encoding?, maxBytes? }) returns a string, or an ArrayBuffer with encoding: "bytes". Files over maxBytes (16 MiB by default, Convex's limit for a function's return value) throw file_too_large. writeFile({ path, data }) takes either; Convex caps action arguments at 5 MiB.
  • exposePort({ port, ttlMs?, slug? }) returns a public URL and records it in the row's previews.

Reads

  • get() / list({ ownerId }) from an action. In queries, use components.tenki.sandboxes.get / .list directly.

Errors

Errors are ConvexErrors with a code:

CodeMeaning
not_foundNo sandbox for this identity.
already_existsfork's target is live.
not_readyThe row's phase doesn't allow the call; phase is included.
terminatedThe sandbox is gone; the row is now terminated.
invalid_stateTenki refused the call in the sandbox's current state; the row was re-synced.
insufficient_creditsThe Tenki workspace's balance is empty.
capacity_exceededmaxActiveSandboxes is reached; try again later.
invalid_argumentTenki rejected an option, e.g. a malformed tag.
snapshot_not_foundsnapshotId doesn't exist.
file_too_largereadFile on a file over maxBytes; size is included.
spawn_failedspawn couldn't start the command.
unavailable, timeoutTenki was briefly unavailable or a wait ran out; retry.
pause_failed, resume_failedTenki couldn't pause or resume; the row was re-synced from Tenki.
provisioning_timeoutA concurrent create of the same identity is still provisioning.
file_not_foundreadFile on a missing path.
unauthenticatedTENKI_API_KEY is missing or invalid.
quota_exceeded, rate_limited, capacity_unavailableTenki limits; retry later.

A failed create also records the error on the row (phase: "error" and lastError), and the next create retries.

Limiting spend

new Tenki(components.tenki, { maxActiveSandboxes: 20 }) refuses to start or resume a sandbox once 20 are active (not paused) across all owners, counting creates in flight. The check runs in the same transaction that reserves the row, so concurrent creates, forks and resumes can't exceed it.

Lifetime

A Tenki sandbox runs for at most maxDurationMs at a time, set at create time; your workspace's limits set the default and the maximum. Activity does not extend it; call extend. At the deadline Tenki pauses the sandbox and keeps it for the pause retention (7 days unless you set pauseRetentionMs), then deletes it. The row keeps its last known phase until refresh, reconcile or the next create catches it up. create resumes a paused sandbox, and resuming one that reached its deadline starts a new lifetime of the same length, so maxDurationMs bounds each run, not the sandbox's total life. If sandboxes must not outlive a limit, enforce it yourself (see example/convex/demo.ts).

Example and demo

example/ is a working app: a React page where each visitor signs in anonymously, starts a sandbox, runs commands, serves a web page through a preview URL, and pauses, resumes or forks it. See docs/demo.md to run it.

Contributing

docs/architecture.md explains how the component works, and docs/testing.md covers the test suites.

Development needs Node 24 with npm 11.18 or later, but not npm 12. Node 24.20 and later bundle a matching npm, and nvm install installs Node 24 from .nvmrc. With any other npm, devEngines in package.json makes npm install, npm ci and npm run stop with an error. A dependency's install scripts run only if allowScripts in package.json allows them, and npm install-scripts ls lists any that still need a decision.

Keywords