npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tangle-network/sandbox

v0.38.0

Published

Client SDK for the Tangle Sandbox platform - build AI agent applications with dev containers

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/sandbox

Set 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.tools

Choose 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

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-dist

License

MIT