@tanstack/ai-sandbox 0.5.5 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/package.json +6 -6
- package/skills/ai-sandbox/SKILL.md +111 -31
package/README.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source
|
|
4
|
+
media="(prefers-color-scheme: dark)"
|
|
5
|
+
srcset="https://tanstack.com/api/readme/ai.png?theme=dark"
|
|
6
|
+
/>
|
|
7
|
+
<source
|
|
8
|
+
media="(prefers-color-scheme: light)"
|
|
9
|
+
srcset="https://tanstack.com/api/readme/ai.png"
|
|
10
|
+
/>
|
|
11
|
+
<img
|
|
12
|
+
src="https://tanstack.com/api/readme/ai.png"
|
|
13
|
+
alt="TanStack AI"
|
|
14
|
+
width="900"
|
|
15
|
+
/>
|
|
16
|
+
</picture>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<br />
|
|
20
|
+
|
|
1
21
|
# @tanstack/ai-sandbox
|
|
2
22
|
|
|
3
23
|
Provider-agnostic sandbox layer for [TanStack AI](https://tanstack.com/ai). Run coding-agent harness adapters (Grok Build, Claude Code, Codex, OpenCode, Gemini CLI) **inside** an isolated environment with a real filesystem, shell, and cloned repo — and stream their work back through `chat()`.
|
|
@@ -54,6 +74,13 @@ Pick a **provider** package for where the sandbox runs:
|
|
|
54
74
|
| `@tanstack/ai-sandbox-daytona` | Daytona cloud sandboxes, snapshots |
|
|
55
75
|
| `@tanstack/ai-sandbox-upstash-box` | Upstash Box cloud sandboxes, snapshots |
|
|
56
76
|
| `@tanstack/ai-sandbox-sprites` | Sprites stateful sandboxes |
|
|
77
|
+
| `@tanstack/ai-sandbox-blaxel` | Blaxel cloud sandboxes and previews |
|
|
78
|
+
|
|
79
|
+
Install the provider you select separately. For Blaxel:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm install @tanstack/ai-sandbox-blaxel
|
|
83
|
+
```
|
|
57
84
|
|
|
58
85
|
**Harness adapters** are separate packages. The default path is **Grok Build** (`@tanstack/ai-grok-build`); others include `@tanstack/ai-claude-code`, `@tanstack/ai-codex`, and `@tanstack/ai-opencode`. All require `withSandbox(...)` middleware — `chat()` fails fast without it.
|
|
59
86
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-sandbox",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "Provider-agnostic sandbox layer for TanStack AI — run harness adapters inside isolated sandboxes (defineSandbox, defineWorkspace, withSandbox) with a uniform SandboxHandle, workspace bootstrap, policy, and resumable lifecycle.",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,13 +56,13 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
59
|
-
"@tanstack/ai-skills": "^0.1.
|
|
59
|
+
"@tanstack/ai-skills": "^0.1.3"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"@ngrok/ngrok": "^1.0.0",
|
|
63
63
|
"vitest": "^4.1.10",
|
|
64
|
-
"@tanstack/ai": "^0.
|
|
65
|
-
"@tanstack/ai-persistence": "^0.5.
|
|
64
|
+
"@tanstack/ai": "^0.54.0",
|
|
65
|
+
"@tanstack/ai-persistence": "^0.5.7"
|
|
66
66
|
},
|
|
67
67
|
"peerDependenciesMeta": {
|
|
68
68
|
"@ngrok/ngrok": {
|
|
@@ -79,8 +79,8 @@
|
|
|
79
79
|
"@ngrok/ngrok": "^1.7.0",
|
|
80
80
|
"@vitest/coverage-v8": "4.1.10",
|
|
81
81
|
"vitest": "^4.1.10",
|
|
82
|
-
"@tanstack/ai": "0.
|
|
83
|
-
"@tanstack/ai-persistence": "0.5.
|
|
82
|
+
"@tanstack/ai": "0.54.0",
|
|
83
|
+
"@tanstack/ai-persistence": "0.5.7"
|
|
84
84
|
},
|
|
85
85
|
"scripts": {
|
|
86
86
|
"build": "vite build",
|
|
@@ -48,9 +48,10 @@ agent CLI **inside** the sandbox and streams its events back.
|
|
|
48
48
|
## Setup — Claude Code in a Docker sandbox
|
|
49
49
|
|
|
50
50
|
```typescript
|
|
51
|
-
import { chat } from '@tanstack/ai'
|
|
51
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
52
52
|
import { claudeCodeText } from '@tanstack/ai-claude-code'
|
|
53
53
|
import {
|
|
54
|
+
createSecrets,
|
|
54
55
|
defineSandbox,
|
|
55
56
|
defineWorkspace,
|
|
56
57
|
withSandbox,
|
|
@@ -65,17 +66,25 @@ const sandbox = defineSandbox({
|
|
|
65
66
|
packageManager: 'pnpm',
|
|
66
67
|
setup: ['corepack enable', 'pnpm install'],
|
|
67
68
|
scripts: { test: 'pnpm test' },
|
|
68
|
-
secrets: {
|
|
69
|
+
secrets: createSecrets({
|
|
70
|
+
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? '',
|
|
71
|
+
}),
|
|
69
72
|
}),
|
|
70
73
|
lifecycle: { reuse: 'thread', snapshot: 'after-setup', keepAlive: '30m' },
|
|
71
74
|
})
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
threadId,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
export async function POST(request: Request) {
|
|
77
|
+
const { threadId, messages } = await request.json()
|
|
78
|
+
|
|
79
|
+
const stream = chat({
|
|
80
|
+
threadId,
|
|
81
|
+
adapter: claudeCodeText('sonnet'),
|
|
82
|
+
messages,
|
|
83
|
+
middleware: [withSandbox(sandbox)],
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return toServerSentEventsResponse(stream)
|
|
87
|
+
}
|
|
79
88
|
```
|
|
80
89
|
|
|
81
90
|
## Type-safe secrets
|
|
@@ -165,6 +174,8 @@ serial and parallel groups over a **persistent shell** whose cwd/env carry over
|
|
|
165
174
|
between serial steps:
|
|
166
175
|
|
|
167
176
|
```typescript
|
|
177
|
+
import { githubRepo, defineWorkspace } from '@tanstack/ai-sandbox'
|
|
178
|
+
|
|
168
179
|
defineWorkspace({
|
|
169
180
|
source: githubRepo({ repo: 'owner/app' }),
|
|
170
181
|
setup: ({ serial, parallel }) => {
|
|
@@ -183,10 +194,17 @@ When the provider supports snapshots, bootstrap takes one automatically after
|
|
|
183
194
|
Override or add a TTL:
|
|
184
195
|
|
|
185
196
|
```typescript
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
import { defineSandbox } from '@tanstack/ai-sandbox'
|
|
198
|
+
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
|
|
199
|
+
|
|
200
|
+
const sandbox = defineSandbox({
|
|
201
|
+
id: 'repo-agent',
|
|
202
|
+
provider: dockerSandbox({ image: 'node:22' }),
|
|
203
|
+
lifecycle: {
|
|
204
|
+
snapshot: 'after-setup', // default when provider.capabilities().snapshots
|
|
205
|
+
snapshotMaxAge: '24h', // re-create when the snapshot is older than this
|
|
206
|
+
},
|
|
207
|
+
})
|
|
190
208
|
```
|
|
191
209
|
|
|
192
210
|
Providers without snapshot support skip the step silently.
|
|
@@ -199,8 +217,15 @@ middleware in this order, with the same persistence value in both places:
|
|
|
199
217
|
|
|
200
218
|
```typescript
|
|
201
219
|
import { withPersistence } from '@tanstack/ai-persistence'
|
|
202
|
-
import {
|
|
220
|
+
import {
|
|
221
|
+
InMemorySandboxInstanceStore,
|
|
222
|
+
memorySandboxSnapshots,
|
|
223
|
+
withSandbox,
|
|
224
|
+
} from '@tanstack/ai-sandbox'
|
|
225
|
+
// Your `defineSandbox(...)` result.
|
|
226
|
+
import { sandbox } from './sandbox'
|
|
203
227
|
|
|
228
|
+
const instances = new InMemorySandboxInstanceStore()
|
|
204
229
|
const snapshots = await memorySandboxSnapshots({ sandbox, instances })
|
|
205
230
|
|
|
206
231
|
const middleware = [
|
|
@@ -321,20 +346,30 @@ distributed lock: either `withLocks` from `@tanstack/ai/locks` (ordered
|
|
|
321
346
|
**before** `withSandbox`) or the `locks` option.
|
|
322
347
|
|
|
323
348
|
```typescript
|
|
324
|
-
import { chat } from '@tanstack/ai'
|
|
349
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
325
350
|
import { InMemoryLockStore, withLocks } from '@tanstack/ai/locks'
|
|
351
|
+
import { claudeCodeText } from '@tanstack/ai-claude-code'
|
|
326
352
|
import { withSandbox } from '@tanstack/ai-sandbox'
|
|
353
|
+
// Your `defineSandbox(...)` result.
|
|
354
|
+
import { sandbox } from './sandbox'
|
|
327
355
|
// Production: your BYO store — docs/sandbox/durability.md
|
|
328
356
|
import { instanceStore } from './sandbox-instance-store'
|
|
329
357
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
358
|
+
export async function POST(request: Request) {
|
|
359
|
+
const { threadId, messages } = await request.json()
|
|
360
|
+
|
|
361
|
+
const stream = chat({
|
|
362
|
+
threadId,
|
|
363
|
+
adapter: claudeCodeText('sonnet'),
|
|
364
|
+
messages,
|
|
365
|
+
middleware: [
|
|
366
|
+
withLocks(new InMemoryLockStore()), // multi-replica: distributed lock
|
|
367
|
+
withSandbox(sandbox, { instances: instanceStore }),
|
|
368
|
+
],
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
return toServerSentEventsResponse(stream)
|
|
372
|
+
}
|
|
338
373
|
```
|
|
339
374
|
|
|
340
375
|
The store option takes precedence over an ambient `SandboxInstanceStoreCapability`
|
|
@@ -360,8 +395,11 @@ middleware via the `sandbox` group (run-scoped):
|
|
|
360
395
|
import { defineSandbox, withSandbox } from '@tanstack/ai-sandbox'
|
|
361
396
|
// `defineChatMiddleware` is core's, not this package's — `@tanstack/ai-sandbox`
|
|
362
397
|
// consumes it too (see its own `src/middleware.ts`).
|
|
363
|
-
import { defineChatMiddleware } from '@tanstack/ai'
|
|
398
|
+
import { chat, defineChatMiddleware } from '@tanstack/ai'
|
|
399
|
+
import { claudeCodeText } from '@tanstack/ai-claude-code'
|
|
364
400
|
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
|
|
401
|
+
import { db } from './db'
|
|
402
|
+
import { metrics } from './metrics'
|
|
365
403
|
|
|
366
404
|
// Sandbox-scoped hooks (all optional):
|
|
367
405
|
const sandbox = defineSandbox({
|
|
@@ -392,6 +430,13 @@ const auditMiddleware = defineChatMiddleware({
|
|
|
392
430
|
|
|
393
431
|
// No extra middleware needed — sandbox.file CUSTOM events are emitted
|
|
394
432
|
// automatically. Read them from the stream:
|
|
433
|
+
const stream = chat({
|
|
434
|
+
threadId: 'thread-1',
|
|
435
|
+
adapter: claudeCodeText('sonnet'),
|
|
436
|
+
messages: [{ role: 'user', content: 'Add a README.' }],
|
|
437
|
+
middleware: [auditMiddleware, withSandbox(sandbox)],
|
|
438
|
+
})
|
|
439
|
+
|
|
395
440
|
for await (const chunk of stream) {
|
|
396
441
|
if (chunk.type === 'CUSTOM' && chunk.name === 'sandbox.file') {
|
|
397
442
|
const value = chunk.value
|
|
@@ -412,7 +457,10 @@ outside a `chat()` run:
|
|
|
412
457
|
|
|
413
458
|
```typescript
|
|
414
459
|
import { watchWorkspace } from '@tanstack/ai-sandbox'
|
|
460
|
+
// Your `defineSandbox(...)` result.
|
|
461
|
+
import { sandbox } from './sandbox'
|
|
415
462
|
|
|
463
|
+
const handle = await sandbox.ensure({ threadId: 'thread-1', runId: 'run-1' })
|
|
416
464
|
const watcher = await watchWorkspace(handle, {
|
|
417
465
|
onEvent: (e) => console.log(e.type, e.path),
|
|
418
466
|
ignore: ['.git', 'node_modules'], // default
|
|
@@ -424,8 +472,24 @@ Enable the `sandbox` debug category to log watcher start/stop, event dispatch,
|
|
|
424
472
|
and lifecycle transitions:
|
|
425
473
|
|
|
426
474
|
```typescript
|
|
427
|
-
|
|
428
|
-
|
|
475
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
476
|
+
import { claudeCodeText } from '@tanstack/ai-claude-code'
|
|
477
|
+
import { withSandbox } from '@tanstack/ai-sandbox'
|
|
478
|
+
import { sandbox } from './sandbox'
|
|
479
|
+
|
|
480
|
+
export async function POST(request: Request) {
|
|
481
|
+
const { threadId, messages } = await request.json()
|
|
482
|
+
|
|
483
|
+
const stream = chat({
|
|
484
|
+
threadId,
|
|
485
|
+
adapter: claudeCodeText('sonnet'),
|
|
486
|
+
messages,
|
|
487
|
+
middleware: [withSandbox(sandbox)],
|
|
488
|
+
debug: { sandbox: true }, // or debug: true to enable all categories
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
return toServerSentEventsResponse(stream)
|
|
492
|
+
}
|
|
429
493
|
```
|
|
430
494
|
|
|
431
495
|
## Edge / serverless execution
|
|
@@ -551,12 +615,17 @@ file from byte 0 at any point, including after the original host has died.
|
|
|
551
615
|
|
|
552
616
|
```typescript
|
|
553
617
|
import { spawnNdjson } from '@tanstack/ai-sandbox'
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
618
|
+
import type { SandboxHandle } from '@tanstack/ai-sandbox'
|
|
619
|
+
|
|
620
|
+
export async function runAgent(handle: SandboxHandle, runId: string) {
|
|
621
|
+
const agentCommand = 'claude -p --output-format stream-json'
|
|
622
|
+
for await (const event of spawnNdjson(handle, agentCommand, {
|
|
623
|
+
cwd: '/workspace',
|
|
624
|
+
journal: { runId }, // durability is opt-in: pass `journal` to route through it
|
|
625
|
+
})) {
|
|
626
|
+
// parsed NDJSON objects, translated by the harness adapter as usual
|
|
627
|
+
console.log(event)
|
|
628
|
+
}
|
|
560
629
|
}
|
|
561
630
|
```
|
|
562
631
|
|
|
@@ -1006,6 +1075,17 @@ import {
|
|
|
1006
1075
|
} from '@tanstack/ai-sandbox'
|
|
1007
1076
|
import type { RunRecord } from '@tanstack/ai'
|
|
1008
1077
|
import type { ReapResult, RunExitProbe } from '@tanstack/ai-sandbox'
|
|
1078
|
+
// Your distributed LockStore, the same one `withSandbox` gets.
|
|
1079
|
+
import { locks } from './locks'
|
|
1080
|
+
// Your persistence — the SAME RunStore the chat routes use.
|
|
1081
|
+
import { runs } from './persistence'
|
|
1082
|
+
// Your `defineSandbox(...)` result and the `SandboxInstanceStore` you passed to
|
|
1083
|
+
// `withSandbox(sandbox, { instances })`.
|
|
1084
|
+
import { instances, sandbox } from './sandbox'
|
|
1085
|
+
// The per-run log factory, resolving the SAME log the producing route wrote.
|
|
1086
|
+
import { durabilityFor } from './durability'
|
|
1087
|
+
// The same `drive` the attach route passes to `sandboxRunDriver`.
|
|
1088
|
+
import { driveRun } from './drive-run'
|
|
1009
1089
|
|
|
1010
1090
|
async function hasFinished(record: RunRecord): Promise<RunExitProbe> {
|
|
1011
1091
|
if (record.sandboxKey === undefined) return { state: 'unknown' }
|