@tanstack/ai-sandbox-cloudflare 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tanner Linsley
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -19,7 +19,7 @@ class CloudflareProvider {
19
19
  return { transport: this.config.transport ?? "rpc" };
20
20
  }
21
21
  async create(input) {
22
- const id = crypto.randomUUID();
22
+ const id = input.id ?? crypto.randomUUID();
23
23
  const sandbox = getSandbox(this.config.binding, id, this.sandboxOptions);
24
24
  if (input.env && Object.keys(input.env).length > 0) {
25
25
  await sandbox.setEnvVars(input.env);
@@ -1 +1 @@
1
- {"version":3,"file":"provider.js","sources":["../../src/provider.ts"],"sourcesContent":["import { getSandbox } from '@cloudflare/sandbox'\nimport { CLOUDFLARE_CAPS, CloudflareHandle } from './handle'\nimport type { Sandbox, SandboxTransport } from '@cloudflare/sandbox'\nimport type {\n SandboxCapabilities,\n SandboxCreateInput,\n SandboxDestroyInput,\n SandboxHandle,\n SandboxProvider,\n SandboxResumeInput,\n} from '@tanstack/ai-sandbox'\n\nconst DEFAULT_WORKDIR = '/workspace'\n\nexport interface CloudflareSandboxConfig {\n /**\n * The Sandbox Durable Object namespace binding (e.g. `env.Sandbox`).\n * Available inside a Worker `fetch` handler.\n */\n binding: DurableObjectNamespace<Sandbox>\n /** Working directory inside the container. Defaults to `/workspace`. */\n workdir?: string\n /**\n * Your Worker's request hostname, required by `ports.connect` to expose a\n * preview URL (Cloudflare routes exposed ports by hostname).\n */\n previewHostname?: string\n /**\n * Container-control transport. Defaults to `'rpc'` (the SDK's primary path)\n * because `sandbox.tunnels` — used by `exposePreviewTool` to mint quick-tunnel\n * preview URLs — ONLY exists on the RPC transport; on `'http'`/`'websocket'` it\n * throws \"requires the RPC transport\". The transport must be the same for every\n * `getSandbox()` of a given id, so this provider applies it to create/resume/\n * destroy alike. Override to `'http'` only if you don't use preview tunnels.\n */\n transport?: SandboxTransport\n}\n\nclass CloudflareProvider implements SandboxProvider {\n readonly name = 'cloudflare'\n\n constructor(private readonly config: CloudflareSandboxConfig) {}\n\n capabilities(): SandboxCapabilities {\n return CLOUDFLARE_CAPS\n }\n\n private get workdir(): string {\n return this.config.workdir ?? DEFAULT_WORKDIR\n }\n\n // `transport: 'rpc'` by default so `sandbox.tunnels` (preview URLs) works; must be\n // identical across every `getSandbox()` for an id, so all three paths share this.\n private get sandboxOptions(): { transport: SandboxTransport } {\n return { transport: this.config.transport ?? 'rpc' }\n }\n\n async create(input: SandboxCreateInput): Promise<SandboxHandle> {\n const id = crypto.randomUUID()\n const sandbox = getSandbox(this.config.binding, id, this.sandboxOptions)\n if (input.env && Object.keys(input.env).length > 0) {\n await sandbox.setEnvVars(input.env)\n }\n await sandbox.mkdir(this.workdir, { recursive: true })\n return new CloudflareHandle(\n id,\n sandbox,\n this.workdir,\n this.config.previewHostname,\n )\n }\n\n resume(input: SandboxResumeInput): Promise<SandboxHandle | null> {\n // The Durable Object is durable, so the sandbox is always addressable by\n // id. (The container disk may have been wiped on cold start — withSandbox\n // re-bootstraps under the same identity when durableFilesystem is false.)\n const sandbox = getSandbox(\n this.config.binding,\n input.id,\n this.sandboxOptions,\n )\n return Promise.resolve(\n new CloudflareHandle(\n input.id,\n sandbox,\n this.workdir,\n this.config.previewHostname,\n ),\n )\n }\n\n async destroy(input: SandboxDestroyInput): Promise<void> {\n const sandbox = getSandbox(\n this.config.binding,\n input.id,\n this.sandboxOptions,\n )\n await sandbox.destroy()\n }\n}\n\n/**\n * Cloudflare sandbox provider — runs harness adapters inside Cloudflare\n * Containers at the edge. Construct it inside a Worker with the Sandbox Durable\n * Object namespace binding. See the stdin/snapshot limitations in `handle.ts`.\n */\nexport function cloudflareSandbox(\n config: CloudflareSandboxConfig,\n): SandboxProvider {\n return new CloudflareProvider(config)\n}\n"],"names":[],"mappings":";;AAYA,MAAM,kBAAkB;AA0BxB,MAAM,mBAA8C;AAAA,EAGlD,YAA6B,QAAiC;AAAjC,SAAA,SAAA;AAAA,EAAkC;AAAA,EAAlC;AAAA,EAFpB,OAAO;AAAA,EAIhB,eAAoC;AAClC,WAAO;AAAA,EACT;AAAA,EAEA,IAAY,UAAkB;AAC5B,WAAO,KAAK,OAAO,WAAW;AAAA,EAChC;AAAA;AAAA;AAAA,EAIA,IAAY,iBAAkD;AAC5D,WAAO,EAAE,WAAW,KAAK,OAAO,aAAa,MAAA;AAAA,EAC/C;AAAA,EAEA,MAAM,OAAO,OAAmD;AAC9D,UAAM,KAAK,OAAO,WAAA;AAClB,UAAM,UAAU,WAAW,KAAK,OAAO,SAAS,IAAI,KAAK,cAAc;AACvE,QAAI,MAAM,OAAO,OAAO,KAAK,MAAM,GAAG,EAAE,SAAS,GAAG;AAClD,YAAM,QAAQ,WAAW,MAAM,GAAG;AAAA,IACpC;AACA,UAAM,QAAQ,MAAM,KAAK,SAAS,EAAE,WAAW,MAAM;AACrD,WAAO,IAAI;AAAA,MACT;AAAA,MACA;AAAA,MACA,KAAK;AAAA,MACL,KAAK,OAAO;AAAA,IAAA;AAAA,EAEhB;AAAA,EAEA,OAAO,OAA0D;AAI/D,UAAM,UAAU;AAAA,MACd,KAAK,OAAO;AAAA,MACZ,MAAM;AAAA,MACN,KAAK;AAAA,IAAA;AAEP,WAAO,QAAQ;AAAA,MACb,IAAI;AAAA,QACF,MAAM;AAAA,QACN;AAAA,QACA,KAAK;AAAA,QACL,KAAK,OAAO;AAAA,MAAA;AAAA,IACd;AAAA,EAEJ;AAAA,EAEA,MAAM,QAAQ,OAA2C;AACvD,UAAM,UAAU;AAAA,MACd,KAAK,OAAO;AAAA,MACZ,MAAM;AAAA,MACN,KAAK;AAAA,IAAA;AAEP,UAAM,QAAQ,QAAA;AAAA,EAChB;AACF;AAOO,SAAS,kBACd,QACiB;AACjB,SAAO,IAAI,mBAAmB,MAAM;AACtC;"}
1
+ {"version":3,"file":"provider.js","sources":["../../src/provider.ts"],"sourcesContent":["import { getSandbox } from '@cloudflare/sandbox'\nimport { CLOUDFLARE_CAPS, CloudflareHandle } from './handle'\nimport type { Sandbox, SandboxTransport } from '@cloudflare/sandbox'\nimport type {\n SandboxCapabilities,\n SandboxCreateInput,\n SandboxDestroyInput,\n SandboxHandle,\n SandboxProvider,\n SandboxResumeInput,\n} from '@tanstack/ai-sandbox'\n\nconst DEFAULT_WORKDIR = '/workspace'\n\nexport interface CloudflareSandboxConfig {\n /**\n * The Sandbox Durable Object namespace binding (e.g. `env.Sandbox`).\n * Available inside a Worker `fetch` handler.\n */\n binding: DurableObjectNamespace<Sandbox>\n /** Working directory inside the container. Defaults to `/workspace`. */\n workdir?: string\n /**\n * Your Worker's request hostname, required by `ports.connect` to expose a\n * preview URL (Cloudflare routes exposed ports by hostname).\n */\n previewHostname?: string\n /**\n * Container-control transport. Defaults to `'rpc'` (the SDK's primary path)\n * because `sandbox.tunnels` — used by `exposePreviewTool` to mint quick-tunnel\n * preview URLs — ONLY exists on the RPC transport; on `'http'`/`'websocket'` it\n * throws \"requires the RPC transport\". The transport must be the same for every\n * `getSandbox()` of a given id, so this provider applies it to create/resume/\n * destroy alike. Override to `'http'` only if you don't use preview tunnels.\n */\n transport?: SandboxTransport\n}\n\nclass CloudflareProvider implements SandboxProvider {\n readonly name = 'cloudflare'\n\n constructor(private readonly config: CloudflareSandboxConfig) {}\n\n capabilities(): SandboxCapabilities {\n return CLOUDFLARE_CAPS\n }\n\n private get workdir(): string {\n return this.config.workdir ?? DEFAULT_WORKDIR\n }\n\n // `transport: 'rpc'` by default so `sandbox.tunnels` (preview URLs) works; must be\n // identical across every `getSandbox()` for an id, so all three paths share this.\n private get sandboxOptions(): { transport: SandboxTransport } {\n return { transport: this.config.transport ?? 'rpc' }\n }\n\n async create(input: SandboxCreateInput): Promise<SandboxHandle> {\n // Honor the deterministic id `ensure()` supplies so reconnecting consumers\n // (e.g. a preview iframe) address the same DO the agent edits. The DO id is\n // `idFromName(id)`, so any stable string works; fall back to a random id\n // when created outside `defineSandbox` (advanced direct use).\n const id = input.id ?? crypto.randomUUID()\n const sandbox = getSandbox(this.config.binding, id, this.sandboxOptions)\n if (input.env && Object.keys(input.env).length > 0) {\n await sandbox.setEnvVars(input.env)\n }\n await sandbox.mkdir(this.workdir, { recursive: true })\n return new CloudflareHandle(\n id,\n sandbox,\n this.workdir,\n this.config.previewHostname,\n )\n }\n\n resume(input: SandboxResumeInput): Promise<SandboxHandle | null> {\n // The Durable Object is durable, so the sandbox is always addressable by\n // id. (The container disk may have been wiped on cold start — withSandbox\n // re-bootstraps under the same identity when durableFilesystem is false.)\n const sandbox = getSandbox(\n this.config.binding,\n input.id,\n this.sandboxOptions,\n )\n return Promise.resolve(\n new CloudflareHandle(\n input.id,\n sandbox,\n this.workdir,\n this.config.previewHostname,\n ),\n )\n }\n\n async destroy(input: SandboxDestroyInput): Promise<void> {\n const sandbox = getSandbox(\n this.config.binding,\n input.id,\n this.sandboxOptions,\n )\n await sandbox.destroy()\n }\n}\n\n/**\n * Cloudflare sandbox provider — runs harness adapters inside Cloudflare\n * Containers at the edge. Construct it inside a Worker with the Sandbox Durable\n * Object namespace binding. See the stdin/snapshot limitations in `handle.ts`.\n */\nexport function cloudflareSandbox(\n config: CloudflareSandboxConfig,\n): SandboxProvider {\n return new CloudflareProvider(config)\n}\n"],"names":[],"mappings":";;AAYA,MAAM,kBAAkB;AA0BxB,MAAM,mBAA8C;AAAA,EAGlD,YAA6B,QAAiC;AAAjC,SAAA,SAAA;AAAA,EAAkC;AAAA,EAAlC;AAAA,EAFpB,OAAO;AAAA,EAIhB,eAAoC;AAClC,WAAO;AAAA,EACT;AAAA,EAEA,IAAY,UAAkB;AAC5B,WAAO,KAAK,OAAO,WAAW;AAAA,EAChC;AAAA;AAAA;AAAA,EAIA,IAAY,iBAAkD;AAC5D,WAAO,EAAE,WAAW,KAAK,OAAO,aAAa,MAAA;AAAA,EAC/C;AAAA,EAEA,MAAM,OAAO,OAAmD;AAK9D,UAAM,KAAK,MAAM,MAAM,OAAO,WAAA;AAC9B,UAAM,UAAU,WAAW,KAAK,OAAO,SAAS,IAAI,KAAK,cAAc;AACvE,QAAI,MAAM,OAAO,OAAO,KAAK,MAAM,GAAG,EAAE,SAAS,GAAG;AAClD,YAAM,QAAQ,WAAW,MAAM,GAAG;AAAA,IACpC;AACA,UAAM,QAAQ,MAAM,KAAK,SAAS,EAAE,WAAW,MAAM;AACrD,WAAO,IAAI;AAAA,MACT;AAAA,MACA;AAAA,MACA,KAAK;AAAA,MACL,KAAK,OAAO;AAAA,IAAA;AAAA,EAEhB;AAAA,EAEA,OAAO,OAA0D;AAI/D,UAAM,UAAU;AAAA,MACd,KAAK,OAAO;AAAA,MACZ,MAAM;AAAA,MACN,KAAK;AAAA,IAAA;AAEP,WAAO,QAAQ;AAAA,MACb,IAAI;AAAA,QACF,MAAM;AAAA,QACN;AAAA,QACA,KAAK;AAAA,QACL,KAAK,OAAO;AAAA,MAAA;AAAA,IACd;AAAA,EAEJ;AAAA,EAEA,MAAM,QAAQ,OAA2C;AACvD,UAAM,UAAU;AAAA,MACd,KAAK,OAAO;AAAA,MACZ,MAAM;AAAA,MACN,KAAK;AAAA,IAAA;AAEP,UAAM,QAAQ,QAAA;AAAA,EAChB;AACF;AAOO,SAAS,kBACd,QACiB;AACjB,SAAO,IAAI,mBAAmB,MAAM;AACtC;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-sandbox-cloudflare",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Cloudflare sandbox provider for TanStack AI — run harness adapters inside Cloudflare Containers (edge) through the uniform SandboxHandle.",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -40,35 +40,32 @@
40
40
  "dist",
41
41
  "src"
42
42
  ],
43
- "scripts": {
44
- "build": "vite build",
45
- "clean": "premove ./build ./dist",
46
- "lint:fix": "eslint ./src --fix",
47
- "test:build": "publint --strict",
48
- "test:eslint": "eslint ./src",
49
- "test:lib": "vitest",
50
- "test:lib:dev": "pnpm test:lib --watch",
51
- "test:types": "tsc"
52
- },
53
43
  "dependencies": {
54
44
  "@cloudflare/sandbox": "^0.12.1"
55
45
  },
56
46
  "peerDependencies": {
57
- "@tanstack/ai": "workspace:^",
58
- "@tanstack/ai-sandbox": "workspace:^",
59
- "@tanstack/ai-sandbox-local-process": "workspace:^",
60
- "zod": "^4.0.0"
47
+ "zod": "^4.0.0",
48
+ "@tanstack/ai": "^0.39.1",
49
+ "@tanstack/ai-sandbox": "^0.2.1",
50
+ "@tanstack/ai-sandbox-local-process": "^0.2.0"
61
51
  },
62
52
  "devDependencies": {
63
53
  "@cloudflare/workers-types": "^4.20260317.1",
64
- "@tanstack/ai": "workspace:*",
65
- "@tanstack/ai-sandbox": "workspace:*",
66
- "@tanstack/ai-sandbox-local-process": "workspace:*",
67
54
  "@types/node": "^24.10.1",
68
55
  "@vitest/coverage-v8": "4.0.14",
69
- "zod": "^4.2.0"
56
+ "zod": "^4.2.0",
57
+ "@tanstack/ai": "0.39.1",
58
+ "@tanstack/ai-sandbox": "0.2.1",
59
+ "@tanstack/ai-sandbox-local-process": "0.2.0"
70
60
  },
71
- "publishConfig": {
72
- "access": "public"
61
+ "scripts": {
62
+ "build": "vite build",
63
+ "clean": "premove ./build ./dist",
64
+ "lint:fix": "eslint ./src --fix",
65
+ "test:build": "publint --strict",
66
+ "test:eslint": "eslint ./src",
67
+ "test:lib": "vitest",
68
+ "test:lib:dev": "pnpm test:lib --watch",
69
+ "test:types": "tsc"
73
70
  }
74
- }
71
+ }
package/src/provider.ts CHANGED
@@ -56,7 +56,11 @@ class CloudflareProvider implements SandboxProvider {
56
56
  }
57
57
 
58
58
  async create(input: SandboxCreateInput): Promise<SandboxHandle> {
59
- const id = crypto.randomUUID()
59
+ // Honor the deterministic id `ensure()` supplies so reconnecting consumers
60
+ // (e.g. a preview iframe) address the same DO the agent edits. The DO id is
61
+ // `idFromName(id)`, so any stable string works; fall back to a random id
62
+ // when created outside `defineSandbox` (advanced direct use).
63
+ const id = input.id ?? crypto.randomUUID()
60
64
  const sandbox = getSandbox(this.config.binding, id, this.sandboxOptions)
61
65
  if (input.env && Object.keys(input.env).length > 0) {
62
66
  await sandbox.setEnvVars(input.env)