Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/sandbox-tensorlake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@ai-sdk/sandbox-tensorlake': patch
---

Add `@ai-sdk/sandbox-tensorlake`, a `HarnessV1SandboxProvider` backed by Tensorlake sandboxes. Supports running commands, spawning processes, file I/O, suspend-based stop with resume, and snapshot-backed session forking. The harness bridge is reached over an authenticated Tensorlake TCP tunnel (so `getPortUrl` works without exposing public ingress), and a dedicated bridge port is advertised automatically so harnesses like Claude Code work out of the box.

The provider sanitizes harness session ids (mixed-case) into valid Tensorlake sandbox names (lowercase letters, digits, and hyphens), deterministically so `resumeSession` still locates the sandbox by name. A `setup` option (e.g. `createTensorlakeSandbox({ setup: ['npm install -g pnpm@10'] })`) runs shell commands as root once after the sandbox is created and before any harness bootstrap, provisioning tools the default image lacks without building a custom image; with a snapshot recipe the setup runs on the template before the checkpoint, so it is baked into every forked session.
6 changes: 4 additions & 2 deletions content/docs/03-ai-sdk-harnesses/02-harness-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ Install the core harness package, a harness adapter, and a sandbox provider:
<InstallPackages packages="@ai-sdk/harness @ai-sdk/harness-claude-code @ai-sdk/sandbox-vercel" />

Bridge-backed harnesses such as Claude Code and Codex require using real network sandbox
like `@ai-sdk/sandbox-vercel`. Host-runtime harnesses such as Pi can also run with
`@ai-sdk/sandbox-just-bash` because they do not need a sandbox-exposed port.
like `@ai-sdk/sandbox-vercel` or
[`@ai-sdk/sandbox-tensorlake`](https://docs.tensorlake.ai/sandboxes/sdk-reference).
Host-runtime harnesses such as Pi can also run with `@ai-sdk/sandbox-just-bash`
because they do not need a sandbox-exposed port.

## Create an Agent

Expand Down
6 changes: 6 additions & 0 deletions examples/ai-functions/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ ANTHROPIC_API_KEY=""
# Override the default API base URL (optional).
ANTHROPIC_BASE_URL=""

# -----------------------------------------------------------------------------
# Tensorlake Sandbox (@ai-sdk/sandbox-tensorlake)
# -----------------------------------------------------------------------------
# Get a key at https://cloud.tensorlake.ai
TENSORLAKE_API_KEY=""


# -----------------------------------------------------------------------------
# AssemblyAI (@ai-sdk/assemblyai)
Expand Down
1 change: 1 addition & 0 deletions examples/ai-functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@ai-sdk/replicate": "workspace:*",
"@ai-sdk/revai": "workspace:*",
"@ai-sdk/sandbox-just-bash": "workspace:*",
"@ai-sdk/sandbox-tensorlake": "workspace:*",
"@ai-sdk/sandbox-vercel": "workspace:*",
"@ai-sdk/togetherai": "workspace:*",
"@ai-sdk/tui": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { HarnessAgent } from '@ai-sdk/harness/agent';
import { claudeCode } from '@ai-sdk/harness-claude-code';
import { createTensorlakeSandbox } from '@ai-sdk/sandbox-tensorlake';
import { run } from '../../lib/run';

// A minimal, non-streaming Claude Code + Tensorlake sandbox demo.
//
// Requires in the environment (or examples/ai-functions/.env):
// TENSORLAKE_API_KEY — Tensorlake sandbox credential
// ANTHROPIC_API_KEY — (or AI_GATEWAY_API_KEY) Claude credential
//
// No custom image needed: the Claude Code harness bootstraps with `pnpm`, which
// the default Tensorlake image lacks, so we install it via the `setup` option.
// Setup commands run as root once after the sandbox is created, landing `pnpm`
// in /usr/bin where the non-root run user can execute it.
run(async () => {
const agent = new HarnessAgent({
harness: claudeCode,
sandbox: createTensorlakeSandbox({
setup: ['npm install -g pnpm@10'],
cpus: 2,
memoryMb: 4096,
}),
});

const session = await agent.createSession();
try {
const { text } = await agent.generate({
session,
prompt:
'Write bubble-sort.js that sorts [5, 2, 9, 1, 7] and prints the result, then run it.',
});
console.log(text);
} finally {
await session.destroy();
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { HarnessAgent } from '@ai-sdk/harness/agent';
import { claudeCode } from '@ai-sdk/harness-claude-code';
import { createTensorlakeSandbox } from '@ai-sdk/sandbox-tensorlake';
import { printFullStream } from '../../lib/print-full-stream';
import { run } from '../../lib/run';

run(async () => {
// Requires TENSORLAKE_API_KEY (sandbox) and a Claude credential
// (ANTHROPIC_API_KEY or AI_GATEWAY_API_KEY) in the environment.
//
// The Claude Code harness bootstraps itself inside the sandbox with
// `pnpm install`, but Tensorlake's default image does not ship `pnpm`. Rather
// than build a custom image, install it at runtime via `setup`: the commands
// run as root once after the sandbox is created, so `pnpm` lands in /usr/bin
// where the non-root run user can execute it.
const sandbox = createTensorlakeSandbox({
setup: ['npm install -g pnpm@10'],
cpus: 2,
memoryMb: 4096,
timeoutSecs: 10 * 60,
});
const agent = new HarnessAgent({
harness: claudeCode,
sandbox,
});

let exitCode = 0;
const session = await agent.createSession();
try {
const result = await agent.stream({
session,
prompt: 'Recite the first sentence of "A Tale of Two Cities".',
});

await printFullStream({ result });

console.log('finishReason:', await result.finishReason);
console.log('usage:', await result.usage);
} catch (err) {
exitCode = 1;
console.error('[example] failed:', err);
} finally {
await session.destroy();
process.exit(exitCode);
}
});
3 changes: 3 additions & 0 deletions examples/ai-functions/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@
{
"path": "../../packages/sandbox-just-bash"
},
{
"path": "../../packages/sandbox-tensorlake"
},
{
"path": "../../packages/sandbox-vercel"
},
Expand Down
1 change: 1 addition & 0 deletions packages/sandbox-tensorlake/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @ai-sdk/sandbox-tensorlake
158 changes: 158 additions & 0 deletions packages/sandbox-tensorlake/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# AI SDK - Tensorlake Sandbox

_This package is **experimental**._

`HarnessV1SandboxProvider` implementation for [Tensorlake Sandboxes](https://docs.tensorlake.ai/sandboxes/sdk-reference) — stateful execution environments for AI agents with suspend/resume and snapshots.

## Setup

```bash
npm i @ai-sdk/sandbox-tensorlake
```

Authenticate via the `TENSORLAKE_API_KEY` environment variable (get a key at [cloud.tensorlake.ai](https://cloud.tensorlake.ai)) or pass `apiKey` in the settings.

## Usage with a harness

```ts
import { HarnessAgent } from '@ai-sdk/harness/agent';
import { claudeCode } from '@ai-sdk/harness-claude-code';
import { createTensorlakeSandbox } from '@ai-sdk/sandbox-tensorlake';

const agent = new HarnessAgent({
harness: claudeCode,
sandbox: createTensorlakeSandbox({
setup: ['npm install -g pnpm@10'], // provision pnpm for the harness
cpus: 2,
memoryMb: 4096,
ports: [4000],
}),
});

const session = await agent.createSession();
try {
const result = await agent.stream({
session,
prompt: 'Check the test failures and fix the production code.',
});
for await (const part of result.fullStream) {
if (part.type === 'text-delta') process.stdout.write(part.text);
}
} finally {
await session.destroy();
}
```

### Provisioning the harness's `pnpm` (the `setup` option)

The Claude Code harness bootstraps itself inside the sandbox with `pnpm install`. Tensorlake's default image ships `node`, `npm`, and `git` but **not `pnpm`**. The simplest fix is the `setup` option — shell commands run **as root** once, right after the sandbox is created:

```ts
createTensorlakeSandbox({
setup: ['npm install -g pnpm@10'], // runs as root → lands in /usr/bin
cpus: 2,
memoryMb: 4096,
});
```

Setup runs as root because the default run user (`tl-user`) cannot write to system `PATH` directories; installing there as root makes `pnpm` executable by that user. A non-zero exit aborts session creation. With a snapshot recipe (`identity` + `onFirstCreate`), setup runs on the template before the checkpoint, so the provisioned tools are baked into every forked session and don't re-run per session.

Prefer a prebuilt image instead? Build one once with the `tensorlake` `Image` API and pass `image` (no `setup` needed):

```ts
import { Image } from 'tensorlake';

await new Image({
name: 'claude-harness',
baseImage: 'tensorlake/ubuntu-minimal',
})
.run('apt-get update && apt-get install -y curl git ca-certificates')
.run(
'curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && apt-get install -y nodejs',
)
.run('npm install -g pnpm@10')
.build({ registeredName: 'claude-harness' });

createTensorlakeSandbox({ image: 'claude-harness', cpus: 2, memoryMb: 4096 });
```

## Direct usage

The factory is synchronous. The returned provider is stable; the actual `tensorlake` `Sandbox` is created on demand inside `provider.createSession()`.

```ts
import { createTensorlakeSandbox } from '@ai-sdk/sandbox-tensorlake';

const tensorlakeSandbox = createTensorlakeSandbox({
cpus: 2,
memoryMb: 4096,
ports: [3000],
});

const networkSandboxSession = await tensorlakeSandbox.createSession();
const sandboxSession = networkSandboxSession.restricted();

await sandboxSession.writeTextFile({
path: '/home/tl-user/hello.txt',
content: 'hi',
});

const { stdout } = await sandboxSession.run({
command: 'cat /home/tl-user/hello.txt',
});
console.log(stdout); // "hi"
await networkSandboxSession.stop();
```

The default sandbox image runs as the non-root user `tl-user`, so the harness composes session paths under its home, `/home/tl-user` (the default working directory). `/root` and a not-yet-created `/workspace` are not writable by that user. If you supply a custom `image` whose default user/home differ, set `workingDirectory` to a path that user can write:

```ts
const sandbox = createTensorlakeSandbox({
image: myCustomImage, // runs as root
workingDirectory: '/workspace',
});
```

`networkSandboxSession.restricted()` is typed as `Experimental_SandboxSession`, so it's safe to pass to AI SDK tools that accept `experimental_sandbox`. The network sandbox session itself carries the infra surface (`ports`, `getPortUrl`, `setPorts`, `stop`) that only the harness should reach for.

The create-time settings are forwarded directly to `tensorlake`'s `Sandbox.create`, so every option Tensorlake supports is available (`image`, `cpus`, `memoryMb`, `gpus`, `timeoutSecs`, `snapshotId`, …), plus one adapter-specific field:

- `ports` — extra in-sandbox ports to advertise on the session, reachable from the host via `getPortUrl`. The harness bridge port is advertised automatically, so this is only needed to reach additional services the agent starts.

### How the harness reaches the sandbox (tunnels)

The harness runs a WebSocket bridge inside the sandbox and connects to it from the host using `getPortUrl`. Tensorlake's public ingress (`https://<port>-<id>.sandbox.tensorlake.ai`) requires a Tensorlake credential on every request, which the harness cannot supply. So `getPortUrl` instead opens a `tensorlake` **TCP tunnel** — an authenticated WebSocket the SDK secures with your API key — and returns a `127.0.0.1` URL. The harness connects to localhost; the tunnel forwards to the in-sandbox port. Tunnels are cached per port and closed when the session stops.

This means `getPortUrl` returns a host-local URL (great for the harness and local access), not a shareable public URL. For a public URL, read `sandbox.info().sandboxUrl` after exposing the port yourself.

### Outbound network policy

Tensorlake fixes egress at create time and cannot change it on a running sandbox, so this adapter does not implement mid-session `setNetworkPolicy`. Configure egress when constructing the provider:

```ts
const sandbox = createTensorlakeSandbox({
allowInternetAccess: false,
allowOut: ['api.example.com'],
denyOut: ['169.254.169.254'],
});
```

### Wrapping an existing sandbox

To wrap an already-created `tensorlake` `Sandbox` — e.g. to share one sandbox across multiple harness sessions or to construct it with options outside the factory — pass it via `sandbox`. The session's `stop()` and `destroy()` become no-ops; the caller owns the lifecycle.

```ts
import { createTensorlakeSandbox } from '@ai-sdk/sandbox-tensorlake';
import { Sandbox } from 'tensorlake';

const tensorlakeSandbox = createTensorlakeSandbox({
sandbox: await Sandbox.create({ cpus: 2, name: 'shared-env' }),
ports: [3000],
});
```

## Sessions and resume

When the harness supplies a `sessionId`, the provider names the sandbox deterministically and `stop()` suspends it (named sandboxes are resumable). A later `resumeSession({ sessionId })` locates the sandbox by name via `Sandbox.list()`, reconnects, and resumes it.

When the harness supplies a snapshot recipe (`identity` + `onFirstCreate`), the provider runs the one-time setup once per identity, `checkpoint()`s a snapshot, and forks an ephemeral session sandbox from that snapshot on each `createSession`.
Loading