@tanstack/ai-sandbox 0.2.0 → 0.2.1
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 +1 -0
- package/dist/esm/contracts.d.ts +12 -0
- package/dist/esm/sandbox.js +3 -0
- package/dist/esm/sandbox.js.map +1 -1
- package/package.json +3 -3
- package/src/contracts.ts +12 -0
- package/src/sandbox.ts +3 -0
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ Pick a **provider** package for where the sandbox runs:
|
|
|
52
52
|
| `@tanstack/ai-sandbox-cloudflare` | Cloudflare Workers + Containers |
|
|
53
53
|
| `@tanstack/ai-sandbox-vercel` | Vercel Sandbox |
|
|
54
54
|
| `@tanstack/ai-sandbox-daytona` | Daytona dev environments |
|
|
55
|
+
| `@tanstack/ai-sandbox-sprites` | Sprites stateful sandboxes |
|
|
55
56
|
|
|
56
57
|
**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.
|
|
57
58
|
|
package/dist/esm/contracts.d.ts
CHANGED
|
@@ -170,6 +170,18 @@ export interface SandboxHandle {
|
|
|
170
170
|
}
|
|
171
171
|
/** Input passed to {@link SandboxProvider.create}. */
|
|
172
172
|
export interface SandboxCreateInput {
|
|
173
|
+
/**
|
|
174
|
+
* Deterministic instance id the caller wants the provider to use. `ensure()`
|
|
175
|
+
* passes the compound sandbox key here so the provider-assigned id is
|
|
176
|
+
* reconstructable from run context (thread/workspace/tenant/reuse) instead of
|
|
177
|
+
* being a random value only recoverable from the sandbox store. Providers
|
|
178
|
+
* whose native id is addressable by name (e.g. Cloudflare's DO id) SHOULD
|
|
179
|
+
* honor it (`input.id ?? <random>`); providers that mint their own opaque id
|
|
180
|
+
* MAY ignore it. Consumers that reconnect out-of-band — e.g. attaching a
|
|
181
|
+
* preview iframe to the exact sandbox an agent is editing — rely on this being
|
|
182
|
+
* honored to avoid addressing two different sandboxes.
|
|
183
|
+
*/
|
|
184
|
+
id?: string;
|
|
173
185
|
workspace?: WorkspaceDefinition;
|
|
174
186
|
policy?: SandboxPolicy;
|
|
175
187
|
env?: Record<string, string>;
|
package/dist/esm/sandbox.js
CHANGED
|
@@ -63,6 +63,9 @@ function defineSandbox(config) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
const created = await config.provider.create({
|
|
66
|
+
// Deterministic id so consumers can reconstruct the provider sandbox
|
|
67
|
+
// address from run context (not just from the store record).
|
|
68
|
+
id: key,
|
|
66
69
|
workspace: config.workspace,
|
|
67
70
|
policy: config.policy,
|
|
68
71
|
env: config.workspace?.secrets !== void 0 ? resolveAllSecrets(config.workspace.secrets) : void 0,
|
package/dist/esm/sandbox.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.js","sources":["../../src/sandbox.ts"],"sourcesContent":["/**\n * `defineSandbox()` returns a LAZY controller — it never creates a sandbox at\n * definition time. `withSandbox()` (and advanced users) call `ensure()` to\n * resume-or-create, following: provider.resume → provider.restoreSnapshot →\n * create + bootstrap. The controller folds provider/workspace/policy/lifecycle\n * into a stable instance key and coordinates through the (optional) lock +\n * sandbox stores.\n */\nimport { bootstrapWorkspace } from './bootstrap'\nimport { resolveAllSecrets } from './secrets'\nimport { computeSandboxKey } from './key'\nimport { InMemoryLockStore, InMemorySandboxStore } from './store'\nimport type { SandboxFileEvent } from '@tanstack/ai'\nimport type { SandboxHandle, SandboxProvider } from './contracts'\nimport type { SandboxKeyInput } from './key'\nimport type { LockStore, SandboxStore } from './store'\nimport type { SandboxPolicy } from './policy'\nimport type { WorkspaceDefinition } from './workspace'\n\n/**\n * Sandbox-scoped hooks declared on `defineSandbox`. File hooks fire for every\n * create/change/delete during a chat run; lifecycle hooks fire server-side.\n */\nexport interface SandboxHooks {\n onFile?: (e: SandboxFileEvent) => void | Promise<void>\n onFileCreate?: (e: SandboxFileEvent) => void | Promise<void>\n onFileChange?: (e: SandboxFileEvent) => void | Promise<void>\n onFileDelete?: (e: SandboxFileEvent) => void | Promise<void>\n onReady?: (handle: SandboxHandle) => void | Promise<void>\n onError?: (err: unknown) => void | Promise<void>\n onDestroy?: () => void | Promise<void>\n}\n\nexport type ReuseStrategy = 'thread' | 'none'\nexport type SnapshotStrategy = 'after-setup' | 'after-run' | 'none'\n\nexport interface SandboxLifecycle {\n /** `'thread'` resumes one sandbox per thread; `'none'` is fresh per run. */\n reuse?: ReuseStrategy\n /** When to snapshot (provider-permitting). */\n snapshot?: SnapshotStrategy\n /** Hint for how long a provider should keep the sandbox warm between runs. */\n keepAlive?: string\n /** Destroy the sandbox after the run completes. */\n destroyOnComplete?: boolean\n /**\n * Maximum age of a sandbox record before it is discarded and re-created\n * instead of resumed. Accepts `'<n>h'` (hours) or `'<n>m'` (minutes),\n * e.g. `'2h'` or `'30m'`.\n */\n snapshotMaxAge?: string\n}\n\nexport interface SandboxConfig {\n id: string\n provider: SandboxProvider\n workspace?: WorkspaceDefinition\n policy?: SandboxPolicy\n lifecycle?: SandboxLifecycle\n /** Sandbox-scoped file/lifecycle hooks. */\n hooks?: SandboxHooks\n /** Watch the workspace for file events (default true). Set false to disable. */\n fileEvents?: boolean\n}\n\n/** Context passed to `ensure()` by `withSandbox` (or advanced callers). */\nexport interface SandboxEnsureContext {\n threadId: string\n runId: string\n /** Persistence seam; falls back to an in-memory store when absent. */\n store?: SandboxStore\n /** Lock seam; falls back to an in-memory lock when absent. */\n locks?: LockStore\n tenant?: { userId?: string; orgId?: string }\n signal?: AbortSignal\n}\n\nexport interface SandboxDefinition {\n readonly id: string\n readonly provider: SandboxProvider\n readonly workspace?: WorkspaceDefinition\n readonly policy?: SandboxPolicy\n readonly lifecycle?: SandboxLifecycle\n /** Sandbox-scoped file/lifecycle hooks. */\n readonly hooks?: SandboxHooks\n /** Watch the workspace for file events (default true). Set false to disable. */\n readonly fileEvents?: boolean\n /** Compound instance key for a given run context. */\n key: (ctx: SandboxEnsureContext) => string\n /** Resume-or-create the sandbox for this thread/run. */\n ensure: (ctx: SandboxEnsureContext) => Promise<SandboxHandle>\n /** Tear down the sandbox recorded for this key. */\n destroy: (ctx: SandboxEnsureContext) => Promise<void>\n}\n\n/**\n * Parse a human-readable duration string into milliseconds.\n * Supports `'<n>h'` (hours) and `'<n>m'` (minutes).\n * Returns `undefined` when the input is undefined or the format is unrecognised.\n */\nfunction parseMaxAgeMs(value: string | undefined): number | undefined {\n if (value === undefined) return undefined\n const hourMatch = /^(\\d+)h$/.exec(value)\n if (hourMatch) return Number(hourMatch[1]) * 60 * 60 * 1000\n const minuteMatch = /^(\\d+)m$/.exec(value)\n if (minuteMatch) return Number(minuteMatch[1]) * 60 * 1000\n return undefined\n}\n\n// Process-lifetime fallbacks shared across all definitions so concurrent\n// ensures for the same key serialize even without an injected store/lock.\nconst fallbackStore = new InMemorySandboxStore()\nconst fallbackLocks = new InMemoryLockStore()\n\nexport function defineSandbox(config: SandboxConfig): SandboxDefinition {\n const keyInputFor = (ctx: SandboxEnsureContext): SandboxKeyInput => ({\n threadId:\n config.lifecycle?.reuse === 'none'\n ? `${ctx.threadId}:${ctx.runId}`\n : ctx.threadId,\n sandboxId: config.id,\n providerName: config.provider.name,\n workspace: config.workspace,\n tenant: ctx.tenant,\n })\n\n const ensure = async (ctx: SandboxEnsureContext): Promise<SandboxHandle> => {\n const store = ctx.store ?? fallbackStore\n const locks = ctx.locks ?? fallbackLocks\n const key = computeSandboxKey(keyInputFor(ctx))\n const caps = config.provider.capabilities()\n\n return locks.withLock(`sandbox:${key}`, async () => {\n const effectiveSnapshot: SnapshotStrategy =\n config.lifecycle?.snapshot ?? (caps.snapshots ? 'after-setup' : 'none')\n const maxAgeMs = parseMaxAgeMs(config.lifecycle?.snapshotMaxAge)\n\n const existing = await store.get(key)\n if (existing) {\n // Check whether the record has exceeded snapshotMaxAge; if so,\n // discard and fall through to a fresh create.\n const tooOld =\n maxAgeMs !== undefined && Date.now() - existing.updatedAt > maxAgeMs\n\n if (!tooOld) {\n // 1) Try to reconnect to the still-running sandbox.\n const resumed = await config.provider.resume({\n id: existing.providerSandboxId,\n signal: ctx.signal,\n })\n if (resumed) {\n await store.upsert({\n ...existing,\n latestRunId: ctx.runId,\n updatedAt: Date.now(),\n })\n return resumed\n }\n // 2) Else restore from the latest snapshot, if supported.\n if (\n existing.latestSnapshotId &&\n caps.snapshots &&\n config.provider.restoreSnapshot\n ) {\n const restored = await config.provider.restoreSnapshot({\n snapshotId: existing.latestSnapshotId,\n workspace: config.workspace,\n policy: config.policy,\n env:\n config.workspace?.secrets !== undefined\n ? resolveAllSecrets(config.workspace.secrets)\n : undefined,\n signal: ctx.signal,\n })\n await store.upsert({\n ...existing,\n providerSandboxId: restored.id,\n latestRunId: ctx.runId,\n updatedAt: Date.now(),\n })\n return restored\n }\n }\n // 3) Else fall through and re-create under the same identity\n // (capability-aware degradation for ephemeral-disk providers, or\n // snapshotMaxAge TTL exceeded).\n }\n\n const created = await config.provider.create({\n workspace: config.workspace,\n policy: config.policy,\n env:\n config.workspace?.secrets !== undefined\n ? resolveAllSecrets(config.workspace.secrets)\n : undefined,\n signal: ctx.signal,\n })\n\n if (config.workspace) {\n try {\n await bootstrapWorkspace(created, config.workspace, {\n signal: ctx.signal,\n })\n } catch (error) {\n // Bootstrap failed after the sandbox was created but before it was\n // recorded — destroy the orphan so a failed/retried run doesn't leak\n // a (billed) sandbox, then surface the original error.\n await created.destroy().catch(() => {})\n throw error\n }\n }\n\n let latestSnapshotId: string | undefined\n if (\n effectiveSnapshot === 'after-setup' &&\n caps.snapshots &&\n created.snapshot\n ) {\n latestSnapshotId = (await created.snapshot('after-setup')).id\n }\n\n await store.upsert({\n key,\n provider: config.provider.name,\n providerSandboxId: created.id,\n latestSnapshotId,\n threadId: ctx.threadId,\n latestRunId: ctx.runId,\n updatedAt: Date.now(),\n })\n return created\n })\n }\n\n const destroy = async (ctx: SandboxEnsureContext): Promise<void> => {\n const store = ctx.store ?? fallbackStore\n const key = computeSandboxKey(keyInputFor(ctx))\n const existing = await store.get(key)\n if (!existing) return\n await config.provider.destroy({\n id: existing.providerSandboxId,\n signal: ctx.signal,\n })\n await store.delete(key)\n }\n\n return {\n id: config.id,\n provider: config.provider,\n workspace: config.workspace,\n policy: config.policy,\n lifecycle: config.lifecycle,\n hooks: config.hooks,\n fileEvents: config.fileEvents,\n key: (ctx) => computeSandboxKey(keyInputFor(ctx)),\n ensure,\n destroy,\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sandbox.js","sources":["../../src/sandbox.ts"],"sourcesContent":["/**\n * `defineSandbox()` returns a LAZY controller — it never creates a sandbox at\n * definition time. `withSandbox()` (and advanced users) call `ensure()` to\n * resume-or-create, following: provider.resume → provider.restoreSnapshot →\n * create + bootstrap. The controller folds provider/workspace/policy/lifecycle\n * into a stable instance key and coordinates through the (optional) lock +\n * sandbox stores.\n */\nimport { bootstrapWorkspace } from './bootstrap'\nimport { resolveAllSecrets } from './secrets'\nimport { computeSandboxKey } from './key'\nimport { InMemoryLockStore, InMemorySandboxStore } from './store'\nimport type { SandboxFileEvent } from '@tanstack/ai'\nimport type { SandboxHandle, SandboxProvider } from './contracts'\nimport type { SandboxKeyInput } from './key'\nimport type { LockStore, SandboxStore } from './store'\nimport type { SandboxPolicy } from './policy'\nimport type { WorkspaceDefinition } from './workspace'\n\n/**\n * Sandbox-scoped hooks declared on `defineSandbox`. File hooks fire for every\n * create/change/delete during a chat run; lifecycle hooks fire server-side.\n */\nexport interface SandboxHooks {\n onFile?: (e: SandboxFileEvent) => void | Promise<void>\n onFileCreate?: (e: SandboxFileEvent) => void | Promise<void>\n onFileChange?: (e: SandboxFileEvent) => void | Promise<void>\n onFileDelete?: (e: SandboxFileEvent) => void | Promise<void>\n onReady?: (handle: SandboxHandle) => void | Promise<void>\n onError?: (err: unknown) => void | Promise<void>\n onDestroy?: () => void | Promise<void>\n}\n\nexport type ReuseStrategy = 'thread' | 'none'\nexport type SnapshotStrategy = 'after-setup' | 'after-run' | 'none'\n\nexport interface SandboxLifecycle {\n /** `'thread'` resumes one sandbox per thread; `'none'` is fresh per run. */\n reuse?: ReuseStrategy\n /** When to snapshot (provider-permitting). */\n snapshot?: SnapshotStrategy\n /** Hint for how long a provider should keep the sandbox warm between runs. */\n keepAlive?: string\n /** Destroy the sandbox after the run completes. */\n destroyOnComplete?: boolean\n /**\n * Maximum age of a sandbox record before it is discarded and re-created\n * instead of resumed. Accepts `'<n>h'` (hours) or `'<n>m'` (minutes),\n * e.g. `'2h'` or `'30m'`.\n */\n snapshotMaxAge?: string\n}\n\nexport interface SandboxConfig {\n id: string\n provider: SandboxProvider\n workspace?: WorkspaceDefinition\n policy?: SandboxPolicy\n lifecycle?: SandboxLifecycle\n /** Sandbox-scoped file/lifecycle hooks. */\n hooks?: SandboxHooks\n /** Watch the workspace for file events (default true). Set false to disable. */\n fileEvents?: boolean\n}\n\n/** Context passed to `ensure()` by `withSandbox` (or advanced callers). */\nexport interface SandboxEnsureContext {\n threadId: string\n runId: string\n /** Persistence seam; falls back to an in-memory store when absent. */\n store?: SandboxStore\n /** Lock seam; falls back to an in-memory lock when absent. */\n locks?: LockStore\n tenant?: { userId?: string; orgId?: string }\n signal?: AbortSignal\n}\n\nexport interface SandboxDefinition {\n readonly id: string\n readonly provider: SandboxProvider\n readonly workspace?: WorkspaceDefinition\n readonly policy?: SandboxPolicy\n readonly lifecycle?: SandboxLifecycle\n /** Sandbox-scoped file/lifecycle hooks. */\n readonly hooks?: SandboxHooks\n /** Watch the workspace for file events (default true). Set false to disable. */\n readonly fileEvents?: boolean\n /** Compound instance key for a given run context. */\n key: (ctx: SandboxEnsureContext) => string\n /** Resume-or-create the sandbox for this thread/run. */\n ensure: (ctx: SandboxEnsureContext) => Promise<SandboxHandle>\n /** Tear down the sandbox recorded for this key. */\n destroy: (ctx: SandboxEnsureContext) => Promise<void>\n}\n\n/**\n * Parse a human-readable duration string into milliseconds.\n * Supports `'<n>h'` (hours) and `'<n>m'` (minutes).\n * Returns `undefined` when the input is undefined or the format is unrecognised.\n */\nfunction parseMaxAgeMs(value: string | undefined): number | undefined {\n if (value === undefined) return undefined\n const hourMatch = /^(\\d+)h$/.exec(value)\n if (hourMatch) return Number(hourMatch[1]) * 60 * 60 * 1000\n const minuteMatch = /^(\\d+)m$/.exec(value)\n if (minuteMatch) return Number(minuteMatch[1]) * 60 * 1000\n return undefined\n}\n\n// Process-lifetime fallbacks shared across all definitions so concurrent\n// ensures for the same key serialize even without an injected store/lock.\nconst fallbackStore = new InMemorySandboxStore()\nconst fallbackLocks = new InMemoryLockStore()\n\nexport function defineSandbox(config: SandboxConfig): SandboxDefinition {\n const keyInputFor = (ctx: SandboxEnsureContext): SandboxKeyInput => ({\n threadId:\n config.lifecycle?.reuse === 'none'\n ? `${ctx.threadId}:${ctx.runId}`\n : ctx.threadId,\n sandboxId: config.id,\n providerName: config.provider.name,\n workspace: config.workspace,\n tenant: ctx.tenant,\n })\n\n const ensure = async (ctx: SandboxEnsureContext): Promise<SandboxHandle> => {\n const store = ctx.store ?? fallbackStore\n const locks = ctx.locks ?? fallbackLocks\n const key = computeSandboxKey(keyInputFor(ctx))\n const caps = config.provider.capabilities()\n\n return locks.withLock(`sandbox:${key}`, async () => {\n const effectiveSnapshot: SnapshotStrategy =\n config.lifecycle?.snapshot ?? (caps.snapshots ? 'after-setup' : 'none')\n const maxAgeMs = parseMaxAgeMs(config.lifecycle?.snapshotMaxAge)\n\n const existing = await store.get(key)\n if (existing) {\n // Check whether the record has exceeded snapshotMaxAge; if so,\n // discard and fall through to a fresh create.\n const tooOld =\n maxAgeMs !== undefined && Date.now() - existing.updatedAt > maxAgeMs\n\n if (!tooOld) {\n // 1) Try to reconnect to the still-running sandbox.\n const resumed = await config.provider.resume({\n id: existing.providerSandboxId,\n signal: ctx.signal,\n })\n if (resumed) {\n await store.upsert({\n ...existing,\n latestRunId: ctx.runId,\n updatedAt: Date.now(),\n })\n return resumed\n }\n // 2) Else restore from the latest snapshot, if supported.\n if (\n existing.latestSnapshotId &&\n caps.snapshots &&\n config.provider.restoreSnapshot\n ) {\n const restored = await config.provider.restoreSnapshot({\n snapshotId: existing.latestSnapshotId,\n workspace: config.workspace,\n policy: config.policy,\n env:\n config.workspace?.secrets !== undefined\n ? resolveAllSecrets(config.workspace.secrets)\n : undefined,\n signal: ctx.signal,\n })\n await store.upsert({\n ...existing,\n providerSandboxId: restored.id,\n latestRunId: ctx.runId,\n updatedAt: Date.now(),\n })\n return restored\n }\n }\n // 3) Else fall through and re-create under the same identity\n // (capability-aware degradation for ephemeral-disk providers, or\n // snapshotMaxAge TTL exceeded).\n }\n\n const created = await config.provider.create({\n // Deterministic id so consumers can reconstruct the provider sandbox\n // address from run context (not just from the store record).\n id: key,\n workspace: config.workspace,\n policy: config.policy,\n env:\n config.workspace?.secrets !== undefined\n ? resolveAllSecrets(config.workspace.secrets)\n : undefined,\n signal: ctx.signal,\n })\n\n if (config.workspace) {\n try {\n await bootstrapWorkspace(created, config.workspace, {\n signal: ctx.signal,\n })\n } catch (error) {\n // Bootstrap failed after the sandbox was created but before it was\n // recorded — destroy the orphan so a failed/retried run doesn't leak\n // a (billed) sandbox, then surface the original error.\n await created.destroy().catch(() => {})\n throw error\n }\n }\n\n let latestSnapshotId: string | undefined\n if (\n effectiveSnapshot === 'after-setup' &&\n caps.snapshots &&\n created.snapshot\n ) {\n latestSnapshotId = (await created.snapshot('after-setup')).id\n }\n\n await store.upsert({\n key,\n provider: config.provider.name,\n providerSandboxId: created.id,\n latestSnapshotId,\n threadId: ctx.threadId,\n latestRunId: ctx.runId,\n updatedAt: Date.now(),\n })\n return created\n })\n }\n\n const destroy = async (ctx: SandboxEnsureContext): Promise<void> => {\n const store = ctx.store ?? fallbackStore\n const key = computeSandboxKey(keyInputFor(ctx))\n const existing = await store.get(key)\n if (!existing) return\n await config.provider.destroy({\n id: existing.providerSandboxId,\n signal: ctx.signal,\n })\n await store.delete(key)\n }\n\n return {\n id: config.id,\n provider: config.provider,\n workspace: config.workspace,\n policy: config.policy,\n lifecycle: config.lifecycle,\n hooks: config.hooks,\n fileEvents: config.fileEvents,\n key: (ctx) => computeSandboxKey(keyInputFor(ctx)),\n ensure,\n destroy,\n }\n}\n"],"names":[],"mappings":";;;;AAoGA,SAAS,cAAc,OAA+C;AACpE,MAAI,UAAU,OAAW,QAAO;AAChC,QAAM,YAAY,WAAW,KAAK,KAAK;AACvC,MAAI,kBAAkB,OAAO,UAAU,CAAC,CAAC,IAAI,KAAK,KAAK;AACvD,QAAM,cAAc,WAAW,KAAK,KAAK;AACzC,MAAI,YAAa,QAAO,OAAO,YAAY,CAAC,CAAC,IAAI,KAAK;AACtD,SAAO;AACT;AAIA,MAAM,gBAAgB,IAAI,qBAAA;AAC1B,MAAM,gBAAgB,IAAI,kBAAA;AAEnB,SAAS,cAAc,QAA0C;AACtE,QAAM,cAAc,CAAC,SAAgD;AAAA,IACnE,UACE,OAAO,WAAW,UAAU,SACxB,GAAG,IAAI,QAAQ,IAAI,IAAI,KAAK,KAC5B,IAAI;AAAA,IACV,WAAW,OAAO;AAAA,IAClB,cAAc,OAAO,SAAS;AAAA,IAC9B,WAAW,OAAO;AAAA,IAClB,QAAQ,IAAI;AAAA,EAAA;AAGd,QAAM,SAAS,OAAO,QAAsD;AAC1E,UAAM,QAAQ,IAAI,SAAS;AAC3B,UAAM,QAAQ,IAAI,SAAS;AAC3B,UAAM,MAAM,kBAAkB,YAAY,GAAG,CAAC;AAC9C,UAAM,OAAO,OAAO,SAAS,aAAA;AAE7B,WAAO,MAAM,SAAS,WAAW,GAAG,IAAI,YAAY;AAClD,YAAM,oBACJ,OAAO,WAAW,aAAa,KAAK,YAAY,gBAAgB;AAClE,YAAM,WAAW,cAAc,OAAO,WAAW,cAAc;AAE/D,YAAM,WAAW,MAAM,MAAM,IAAI,GAAG;AACpC,UAAI,UAAU;AAGZ,cAAM,SACJ,aAAa,UAAa,KAAK,QAAQ,SAAS,YAAY;AAE9D,YAAI,CAAC,QAAQ;AAEX,gBAAM,UAAU,MAAM,OAAO,SAAS,OAAO;AAAA,YAC3C,IAAI,SAAS;AAAA,YACb,QAAQ,IAAI;AAAA,UAAA,CACb;AACD,cAAI,SAAS;AACX,kBAAM,MAAM,OAAO;AAAA,cACjB,GAAG;AAAA,cACH,aAAa,IAAI;AAAA,cACjB,WAAW,KAAK,IAAA;AAAA,YAAI,CACrB;AACD,mBAAO;AAAA,UACT;AAEA,cACE,SAAS,oBACT,KAAK,aACL,OAAO,SAAS,iBAChB;AACA,kBAAM,WAAW,MAAM,OAAO,SAAS,gBAAgB;AAAA,cACrD,YAAY,SAAS;AAAA,cACrB,WAAW,OAAO;AAAA,cAClB,QAAQ,OAAO;AAAA,cACf,KACE,OAAO,WAAW,YAAY,SAC1B,kBAAkB,OAAO,UAAU,OAAO,IAC1C;AAAA,cACN,QAAQ,IAAI;AAAA,YAAA,CACb;AACD,kBAAM,MAAM,OAAO;AAAA,cACjB,GAAG;AAAA,cACH,mBAAmB,SAAS;AAAA,cAC5B,aAAa,IAAI;AAAA,cACjB,WAAW,KAAK,IAAA;AAAA,YAAI,CACrB;AACD,mBAAO;AAAA,UACT;AAAA,QACF;AAAA,MAIF;AAEA,YAAM,UAAU,MAAM,OAAO,SAAS,OAAO;AAAA;AAAA;AAAA,QAG3C,IAAI;AAAA,QACJ,WAAW,OAAO;AAAA,QAClB,QAAQ,OAAO;AAAA,QACf,KACE,OAAO,WAAW,YAAY,SAC1B,kBAAkB,OAAO,UAAU,OAAO,IAC1C;AAAA,QACN,QAAQ,IAAI;AAAA,MAAA,CACb;AAED,UAAI,OAAO,WAAW;AACpB,YAAI;AACF,gBAAM,mBAAmB,SAAS,OAAO,WAAW;AAAA,YAClD,QAAQ,IAAI;AAAA,UAAA,CACb;AAAA,QACH,SAAS,OAAO;AAId,gBAAM,QAAQ,UAAU,MAAM,MAAM;AAAA,UAAC,CAAC;AACtC,gBAAM;AAAA,QACR;AAAA,MACF;AAEA,UAAI;AACJ,UACE,sBAAsB,iBACtB,KAAK,aACL,QAAQ,UACR;AACA,4BAAoB,MAAM,QAAQ,SAAS,aAAa,GAAG;AAAA,MAC7D;AAEA,YAAM,MAAM,OAAO;AAAA,QACjB;AAAA,QACA,UAAU,OAAO,SAAS;AAAA,QAC1B,mBAAmB,QAAQ;AAAA,QAC3B;AAAA,QACA,UAAU,IAAI;AAAA,QACd,aAAa,IAAI;AAAA,QACjB,WAAW,KAAK,IAAA;AAAA,MAAI,CACrB;AACD,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,QAAM,UAAU,OAAO,QAA6C;AAClE,UAAM,QAAQ,IAAI,SAAS;AAC3B,UAAM,MAAM,kBAAkB,YAAY,GAAG,CAAC;AAC9C,UAAM,WAAW,MAAM,MAAM,IAAI,GAAG;AACpC,QAAI,CAAC,SAAU;AACf,UAAM,OAAO,SAAS,QAAQ;AAAA,MAC5B,IAAI,SAAS;AAAA,MACb,QAAQ,IAAI;AAAA,IAAA,CACb;AACD,UAAM,MAAM,OAAO,GAAG;AAAA,EACxB;AAEA,SAAO;AAAA,IACL,IAAI,OAAO;AAAA,IACX,UAAU,OAAO;AAAA,IACjB,WAAW,OAAO;AAAA,IAClB,QAAQ,OAAO;AAAA,IACf,WAAW,OAAO;AAAA,IAClB,OAAO,OAAO;AAAA,IACd,YAAY,OAAO;AAAA,IACnB,KAAK,CAAC,QAAQ,kBAAkB,YAAY,GAAG,CAAC;AAAA,IAChD;AAAA,IACA;AAAA,EAAA;AAEJ;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-sandbox",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@ngrok/ngrok": "^1.0.0",
|
|
47
|
-
"@tanstack/ai": "^0.39.
|
|
47
|
+
"@tanstack/ai": "^0.39.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependenciesMeta": {
|
|
50
50
|
"@ngrok/ngrok": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@ngrok/ngrok": "^1.7.0",
|
|
56
56
|
"@vitest/coverage-v8": "4.0.14",
|
|
57
|
-
"@tanstack/ai": "0.39.
|
|
57
|
+
"@tanstack/ai": "0.39.1"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"build": "vite build",
|
package/src/contracts.ts
CHANGED
|
@@ -191,6 +191,18 @@ export interface SandboxHandle {
|
|
|
191
191
|
|
|
192
192
|
/** Input passed to {@link SandboxProvider.create}. */
|
|
193
193
|
export interface SandboxCreateInput {
|
|
194
|
+
/**
|
|
195
|
+
* Deterministic instance id the caller wants the provider to use. `ensure()`
|
|
196
|
+
* passes the compound sandbox key here so the provider-assigned id is
|
|
197
|
+
* reconstructable from run context (thread/workspace/tenant/reuse) instead of
|
|
198
|
+
* being a random value only recoverable from the sandbox store. Providers
|
|
199
|
+
* whose native id is addressable by name (e.g. Cloudflare's DO id) SHOULD
|
|
200
|
+
* honor it (`input.id ?? <random>`); providers that mint their own opaque id
|
|
201
|
+
* MAY ignore it. Consumers that reconnect out-of-band — e.g. attaching a
|
|
202
|
+
* preview iframe to the exact sandbox an agent is editing — rely on this being
|
|
203
|
+
* honored to avoid addressing two different sandboxes.
|
|
204
|
+
*/
|
|
205
|
+
id?: string
|
|
194
206
|
workspace?: WorkspaceDefinition
|
|
195
207
|
policy?: SandboxPolicy
|
|
196
208
|
env?: Record<string, string>
|
package/src/sandbox.ts
CHANGED
|
@@ -187,6 +187,9 @@ export function defineSandbox(config: SandboxConfig): SandboxDefinition {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
const created = await config.provider.create({
|
|
190
|
+
// Deterministic id so consumers can reconstruct the provider sandbox
|
|
191
|
+
// address from run context (not just from the store record).
|
|
192
|
+
id: key,
|
|
190
193
|
workspace: config.workspace,
|
|
191
194
|
policy: config.policy,
|
|
192
195
|
env:
|