@tangle-network/sandbox
v0.38.0
Published
Client SDK for the Tangle Sandbox platform - build AI agent applications with dev containers
Maintainers
Readme
@tangle-network/sandbox
TypeScript SDK for creating isolated development environments, running commands and agents, managing sessions, and coordinating fleets. The package is ESM-only and works in Node.js, edge runtimes, and browsers through separate entrypoints.
Install
npm install @tangle-network/sandboxSet the customer API key and Sandbox API URL in your server environment.
export TANGLE_API_KEY=sk-tan-...
export SANDBOX_BASE_URL=https://sandbox.tangle.toolsChoose an entrypoint
| Need | Import |
|---|---|
| Full client, sandboxes, fleets, images, sessions, and trace utilities | @tangle-network/sandbox |
| Edge-safe client without viem | @tangle-network/sandbox/core |
| Browser or Worker access with a short-lived scoped token | @tangle-network/sandbox/runtime |
| Browser status, claim, and release control for an interactive session | @tangle-network/sandbox/interactive-control |
| Direct Tangle chain operations | @tangle-network/sandbox/tangle |
| Server-side token issuance and validation | @tangle-network/sandbox/auth |
| Collaborative document clients and file bridging | @tangle-network/sandbox/collaboration |
| Browser WebSocket session streaming and replay | @tangle-network/sandbox/session-gateway |
| OpenAI-compatible request and event translation | @tangle-network/sandbox/openai |
| Multiple managed Agents, framework tool packs, and the local MCP bridge | @tangle-network/sandbox/agent |
| Browser-safe trace reads from Tangle Intelligence | @tangle-network/sandbox/intelligence |
Install an optional peer dependency only when the selected integration requires it.
Credential boundary
TANGLE_API_KEY is an account-level control-plane credential. Keep it in a
trusted server, CI secret store, or CLI configuration. Never ship it, a sandbox
connection token, or provider credentials to browser JavaScript.
Mint a short-lived delegated token on the server:
const scoped = await box.mintScopedToken({
scope: "session-runtime",
sessionId: ownedRuntimeSessionId,
ttlMinutes: 5,
});Give the browser only scoped.token, scoped.expiresAt, and
scoped.sidecarProxyUrl, then construct the browser-safe client:
import { createSandboxRuntimeClient } from "@tangle-network/sandbox/runtime";
const runtime = createSandboxRuntimeClient({
baseUrl: delegated.sidecarProxyUrl,
token: delegated.token,
});Use scope: "session" plus an authorized runtimeSessionId for
SessionGatewayClient reconnect and replay. Use scope: "read-only" for
runtime reads that need no workspace or terminal mutation. The server must
authorize every sandbox and session identifier before minting.
See the public /docs/authentication guide and
examples/browser-streaming-resume.ts.
Quick start
import { Sandbox } from "@tangle-network/sandbox";
const client = new Sandbox({
apiKey: process.env.TANGLE_API_KEY!,
baseUrl: process.env.SANDBOX_BASE_URL!,
});
const box = await client.create({
name: "sdk-quick-start",
environment: "universal",
resources: {
cpuCores: 2,
memoryMB: 4096,
diskGB: 20,
},
});
try {
await box.waitFor("running");
const result = await box.exec("node --version && npm --version");
if (result.exitCode !== 0) {
throw new Error(result.stderr);
}
console.log(result.stdout);
} finally {
await box.delete();
}apiKey and baseUrl are required.
Keep delete() in finally unless the sandbox must outlive the process.
Create a sandbox
client.create() accepts a named environment, container image, or published image ID.
The server chooses its default environment when environment is omitted.
const box = await client.create({
name: "repo-review",
environment: "node:22",
git: {
url: "https://github.com/acme/service.git",
ref: "main",
},
env: {
CI: "true",
},
resources: {
cpuCores: 4,
memoryMB: 8192,
diskGB: 40,
},
maxLifetimeSeconds: 3600,
idleTimeoutSeconds: 900,
// Delete the sandbox after one hour parked in the stopped state.
// Omit the field and the platform's default retention applies: the parked
// sweeps keep the sandbox at least 30 days parked and may reclaim it after
// that. An explicit delete, and platform policy, are unaffected.
deleteAfterStoppedSeconds: 3600,
metadata: {
project: "service",
},
});List server-supported environments instead of hard-coding an internal catalog.
const environments = await client.environments.list();
for (const environment of environments) {
console.log(environment.id, environment.description);
}Use bare: true for a minimal container with command execution and lifecycle controls.
Use agent: false for managed files, processes, and telemetry without configuring an agent backend.
Run several Agents in one Sandbox
Import the Agent entrypoint when one Sandbox should host several AgentProfiles.
It re-exports the main client and installs the typed box.agents namespace.
import type { AgentProfile } from "@tangle-network/agent-interface";
import { Sandbox } from "@tangle-network/sandbox/agent";
const client = new Sandbox({
apiKey: process.env.TANGLE_API_KEY!,
baseUrl: process.env.SANDBOX_BASE_URL!,
});
const plannerProfile = {
name: "planner",
harness: "opencode",
prompt: { appendSystemPrompt: "Plan before editing files." },
} satisfies AgentProfile;
const implementerProfile = {
name: "implementer",
harness: "claude-code",
prompt: { appendSystemPrompt: "Implement the request and run tests." },
} satisfies AgentProfile;
const box = await client.create({
environment: "universal",
agent: false,
});
try {
const [planner, implementer] = await box.agents.startMany([
{
id: "planner-v1",
backend: { type: "opencode", profile: plannerProfile },
},
{
id: "implementer-v1",
backend: { type: "claude-code", profile: implementerProfile },
},
]);
const [plan, implementation] = await Promise.all([
planner.prompt("Inspect the repository and produce a precise plan."),
implementer.prompt("Inspect the repository and implement the request."),
]);
console.log(plan.response);
console.log(implementation.response);
} finally {
await box.delete();
}Each returned Agent is the existing durable SandboxSession handle. Different
session ids may run in parallel; turns inside one session remain serialized.
Use a stable id and box.agents.get(id) to reattach from another process.
Shared workspace is the default. Use the existing task-session Git-worktree path when file changes or profile control files must remain isolated:
const reviewer = await box.agents.start({
id: "reviewer",
backend: { type: "codex", profile: reviewerProfile },
workspace: { mode: "isolated", parentSessionId: "implementer-v1" },
});
const changes = await reviewer.changes();
await reviewer.commit({ message: "Apply reviewer changes" });An isolated Agent requires a Git repository and returns
SandboxTaskSession. commit() applies the changes and removes the owned
worktree and task branch. box.agents.stop(id) discards an uncommitted isolated
generation and removes those owned Git resources before its durable session
record; cleanup failure leaves the record intact for a safe retry.
Agents in one Sandbox share a broad trust and resource boundary. Use separate Sandboxes for mutually untrusted Agents, separate tenants, or hard resource isolation.
Applications that already import from the root can install the namespace with:
import { Sandbox } from "@tangle-network/sandbox";
import "@tangle-network/sandbox/agent";See examples/multi-agent-sandbox.ts for a complete runnable example.
Lifecycle
The active states are pending, provisioning, running, stopped, failed, and expired.
Deletion is permanent and is not a retained status.
await box.waitFor("running", { timeoutMs: 120_000 });
await box.stop();
await box.waitFor("stopped");
await box.resume({ timeoutMs: 120_000 });
await box.waitFor("running");
await box.delete();Create a snapshot when later sandboxes need the same filesystem state.
const snapshot = await box.snapshot({
tags: ["baseline"],
});
const restored = await client.create({
fromSnapshot: snapshot.snapshotId,
});
try {
await restored.waitFor("running");
} finally {
await restored.delete();
}Use the same idempotencyKey only when retrying the same logical create request.
Commands and files
exec() returns the exit code, standard output, standard error, and timing data.
const test = await box.exec("pnpm test", {
cwd: "/workspace/service",
env: { CI: "true" },
timeoutMs: 10 * 60 * 1000,
});
if (test.exitCode !== 0) {
throw new Error(test.stderr);
}The direct file helpers cover common text operations.
The fs property exposes listing, metadata, upload, download, rename, and directory operations.
await box.write("/workspace/service/config.json", '{"enabled":true}\n');
const config = await box.read("/workspace/service/config.json");
console.log(config);File size contract
| Path | Limit | Constant | Use |
|---|---:|---|---|
| Decoded write() content | 5 MiB | FILE_DECODED_WRITE_MAX_BYTES | Common text and small binary writes |
| Public API request | 1 MiB | SANDBOX_PROXY_REQUEST_MAX_BYTES | Requests routed through the Sandbox API |
| Direct runtime file request | 32 MiB | SANDBOX_DIRECT_FILE_REQUEST_MAX_BYTES | Direct runtime requests |
| Multipart upload | 5 MiB | FILE_MULTIPART_UPLOAD_MAX_BYTES | One multipart upload |
| Chunked upload part | 768 KiB | FILE_UPLOAD_PART_MAX_BYTES | One fs.uploadData() part |
| Chunked upload session | 64 MiB | FILE_UPLOAD_SESSION_MAX_BYTES | One complete fs.uploadData() call |
Use fs.uploadData() when content is larger than the single-request limit.
The instance also exposes git, tools, process, terminals, network, egress, previewLinks, images, and backend capability clients.
Unsupported capabilities fail with a typed SDK error.
Prompts
Configure the agent backend when creating the sandbox or override it for one prompt. Provider credentials must come from your platform configuration or the backend model configuration.
const agentBox = await client.create({
environment: "universal",
backend: {
type: "opencode",
model: {
provider: process.env.MODEL_PROVIDER!,
model: process.env.MODEL_NAME!,
apiKey: process.env.MODEL_API_KEY!,
},
},
});
try {
const result = await agentBox.prompt(
"Run the tests, fix the failure, and explain the change.",
{
timeoutMs: 10 * 60 * 1000,
requireVisibleAssistantOutput: true,
},
);
if (!result.success) {
throw new Error(result.error ?? `Agent stopped with ${result.status}`);
}
console.log(result.response);
console.log(result.usage);
} finally {
await agentBox.delete();
}Use streamPrompt() when the current process will stay attached to the event stream.
Use dispatchPrompt() when the run must continue after the caller disconnects.
Resume a session
A stable session ID preserves conversation context across prompts.
const sessionId = "review-42";
await box.prompt("Inspect the authentication flow.", {
sessionId,
});
const resumed = await box
.session(sessionId)
.prompt("Now patch the highest-severity issue and run its tests.");
console.log(resumed.response);Dispatch returns after the session starts. A later process can retrieve the sandbox and await the stored result.
const dispatched = await box.dispatchPrompt("Run the full test suite.", {
sessionId: "ci-run-42",
});
const reloaded = await client.get(box.id);
if (!reloaded) {
throw new Error(`Sandbox ${box.id} no longer exists`);
}
if (!dispatched.executionId) {
throw new Error("Sandbox did not return the dispatched execution id");
}
const result = await reloaded
.session(dispatched.sessionId)
.result({ executionId: dispatched.executionId });
console.log(result.status, result.response);Reusing the same caller-owned session ID makes a repeated dispatch find the existing session instead of starting another run.
Keep the returned executionId with the session ID whenever another caller can add a turn to the same session.
session.result({ executionId }) replays only that run, and session.interrupt({ executionId }) cancels only that run, so neither operation can move to a newer turn between lookup and action.
prompt() also returns the execution ID that actually produced its result, including an idempotent replay that resolves to an earlier execution than the caller requested.
Use session events with an event ID when the consumer must replay a dropped stream.
Use the /session-gateway entrypoint for browser WebSocket reconnect and replay.
A dispatched run that dies before producing output reports its cause on session.status(): failureReason.message carries the runtime's own error, such as a backend binary that could not be spawned or an agent profile that could not be materialized.
Fleets
A fleet groups several sandboxes under one lifecycle and policy. Use it for parallel jobs, coordinated workers, or workloads that need stable machine IDs.
const fleet = await client.fleets.createWithCoordinator({
fleetId: "dependency-audit",
defaults: {
environment: "universal",
maxLifetimeSeconds: 1800,
},
policy: {
maxMachines: 3,
maxConcurrentCreates: 2,
maxTotalCpu: 6,
maxTotalMemoryMb: 12_288,
maxSpendUsd: 2,
allowAccelerators: false,
},
coordinator: {
resources: { cpuCores: 1, memoryMB: 1024 },
},
workers: [
{
machineId: "worker-1",
resources: { cpuCores: 2, memoryMB: 4096 },
},
{
machineId: "worker-2",
resources: { cpuCores: 2, memoryMB: 4096 },
},
],
});
try {
const results = await fleet.dispatchExec("pnpm test", {
machines: fleet.ids,
maxConcurrent: 2,
retry: { attempts: 2 },
timeoutMs: 10 * 60 * 1000,
});
for (const result of results) {
console.log(result.machineId, result.ok, result.error?.message);
}
} finally {
await fleet.delete({ continueOnError: true });
}Fleet policies are checked before provisioning and do not replace account quotas.
Batch jobs
Use runBatch() for parallel tasks that do not need stable fleet membership.
The default creates temporary sandboxes and removes them after the run.
const result = await client.runBatch({
tasks: [
{ id: "api", message: "Review the API package and fix its tests." },
{ id: "web", message: "Review the web package and fix its tests." },
],
backends: [{ id: "worker", type: "opencode" }],
});
console.log(result.totalSuccess, result.totalFailure);Set persistent: true when later runs must use the same workspace.
reuseKey selects that caller-owned workspace.
idempotencyKey identifies one exact batch request for join or replay.
const request = {
tasks: [{ id: "upgrade", message: "Upgrade dependencies and run tests." }],
backends: [{ id: "worker", type: "opencode" }],
persistent: true as const,
reuseKey: "service-maintenance",
git: { url: "https://github.com/acme/service.git" },
};
const result = await client.runBatch(request, {
idempotencyKey: "service-maintenance-run-42",
});Reuse an idempotency key only with the same request body.
A matching active run is joined and a completed run is replayed.
A different body with the same key is rejected.
Disconnecting an unkeyed stream cancels server work, while a keyed run continues and can be joined again with the same key and body.
streamBatch() accepts the same request and call options.
Temporary GPU leases
Attach a GPU only for the work that needs it.
Omit provider to request the cheapest eligible configured option.
Every lease requires a spend limit and lifetime.
let lease: Awaited<ReturnType<(typeof box)["gpu"]["attach"]>> | undefined;
try {
lease = await box.gpu.attach({
accelerator: {
kind: "nvidia-3090",
count: 1,
memoryMB: 24_000,
},
maxSpendUsd: 1,
maxLifetimeSeconds: 900,
idleTimeoutSeconds: 120,
});
const run = await box.gpu.exec(lease.id, {
command: "python eval.py",
timeoutMs: 15 * 60 * 1000,
});
process.stdout.write(run.result.stdout);
process.stderr.write(run.result.stderr);
} finally {
if (lease) {
await box.gpu.detach(lease.id);
}
}Always detach the lease in finally.
Deleting the base sandbox remains a separate cleanup step.
Public API map
The root entrypoint exports the primary client, instance, session, fleet, image, error, trace, and resource types.
| Surface | Start with |
|---|---|
| Account client | Sandbox |
| One environment | SandboxInstance |
| One or more managed Agents in a Sandbox | box.agents from the /agent entrypoint |
| Continued conversation | SandboxSession |
| Coordinated environments | SandboxFleetClient, SandboxFleet |
| Reusable images | Image, ImageBuilder |
| Failures | SandboxError and its specific subclasses |
The main client provides create, list, get, usage, subscription, and health.
Its managers cover environments, public templates, teams, secrets, SSH keys, fleets, and intelligence reports.
The published TypeScript declarations are the complete method and option reference.
Handle SDK failures through the exported error hierarchy.
import { SandboxError } from "@tangle-network/sandbox";
try {
await box.exec("pnpm test");
} catch (error) {
if (error instanceof SandboxError) {
console.error(error.code, error.status, error.endpoint, error.message);
throw error;
}
throw error;
}More documentation
- TypeScript SDK guide
- Platform quickstart
- GPU lease guide
- Runnable SDK examples
- Session and adapter integration notes
- CLI
- Python SDK
Package development
pnpm --filter @tangle-network/sandbox check-types
pnpm --filter @tangle-network/sandbox test
pnpm --filter @tangle-network/sandbox build
pnpm --filter @tangle-network/sandbox verify-distLicense
MIT
