From 167d0523421134596ce44fbd1c3df3e452d4e09e Mon Sep 17 00:00:00 2001 From: Sriket Komali Date: Sat, 2 May 2026 20:15:25 -0400 Subject: [PATCH 1/5] fix(ai-isolate-cloudflare): port worker from unsafe_eval to worker_loader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cloudflare gates the `unsafe_eval` binding for all customer prod accounts (no public entitlement); the previous driver was unusable in production and broken in `wrangler dev` on current Wrangler 4.x. Swap `env.UNSAFE_EVAL.eval(code)` for the supported `worker_loader` (Dynamic Workers) binding — load the wrapped code as an ES module into a fresh child Worker isolate via `env.LOADER.load({...}).getEntrypoint() .fetch(...)` and read the structured result back as JSON. The HTTP tool-callback protocol, driver, and public API are unchanged. ~120 LOC change in worker; tests + wrangler.toml + README updated. Workers Paid plan is required for any edge usage (deploy or `wrangler dev --remote`); local `wrangler dev` works on the Free plan. The custom Miniflare `dev-server.mjs` is removed since `wrangler dev` now binds `worker_loader` natively. Closes #522. --- .changeset/worker-loader-port.md | 24 ++++ .../ai-isolate-cloudflare/README.md | 24 ++-- .../ai-isolate-cloudflare/dev-server.mjs | 49 ------- .../ai-isolate-cloudflare/package.json | 2 +- .../ai-isolate-cloudflare/src/worker/index.ts | 131 +++++++++++++----- .../src/worker/wrap-code.ts | 2 +- .../tests/escape-attempts.test.ts | 6 +- .../tests/isolate-driver.test.ts | 8 +- .../tests/worker.test.ts | 25 +++- .../ai-isolate-cloudflare/wrangler.toml | 24 ++-- 10 files changed, 173 insertions(+), 122 deletions(-) create mode 100644 .changeset/worker-loader-port.md delete mode 100644 packages/typescript/ai-isolate-cloudflare/dev-server.mjs diff --git a/.changeset/worker-loader-port.md b/.changeset/worker-loader-port.md new file mode 100644 index 000000000..6f7339a5f --- /dev/null +++ b/.changeset/worker-loader-port.md @@ -0,0 +1,24 @@ +--- +'@tanstack/ai-isolate-cloudflare': minor +--- + +Port the Cloudflare worker driver from `unsafe_eval` to `worker_loader` (Dynamic Workers). + +Cloudflare gates the `unsafe_eval` binding for all customer prod accounts; the previous driver was unusable in production and broken in `wrangler dev` on current Wrangler 4.x. The supported replacement is the `worker_loader` binding (GA-beta'd 2026-03-24). + +**Breaking:** the worker now requires the `LOADER` binding instead of `UNSAFE_EVAL`. Update your `wrangler.toml`: + +```toml +# before +[[unsafe.bindings]] +name = "UNSAFE_EVAL" +type = "unsafe_eval" + +# after +[[worker_loaders]] +binding = "LOADER" +``` + +The HTTP tool-callback protocol and public driver API are unchanged. Workers Paid plan is required for any edge usage (deploy or `wrangler dev --remote`); local `wrangler dev` works on the Free plan. + +Closes #522. diff --git a/packages/typescript/ai-isolate-cloudflare/README.md b/packages/typescript/ai-isolate-cloudflare/README.md index aeb922a7a..7fbb8da3f 100644 --- a/packages/typescript/ai-isolate-cloudflare/README.md +++ b/packages/typescript/ai-isolate-cloudflare/README.md @@ -12,9 +12,9 @@ pnpm add @tanstack/ai-isolate-cloudflare ## Environment Guidance -- **Local development:** supported with the package's Miniflare dev server (`pnpm dev:worker`) -- **Remote dev:** supported with `wrangler dev --remote` -- **Production:** supported on Cloudflare accounts with the `unsafe_eval` binding enabled. Before rollout, put the Worker behind authentication (e.g. Cloudflare Access or the `authorization` driver option), rate limiting, and CORS restrictions — running LLM-authored code is a high-trust operation. +- **Local development:** supported with `wrangler dev` (the `worker_loader` binding works in local workerd on the Workers Free plan). +- **Remote dev:** supported with `wrangler dev --remote` on a Workers Paid plan. +- **Production:** supported on Cloudflare accounts on the Workers Paid plan ($5/mo). The Free plan rejects `worker_loader` deploys at the API level. Before rollout, put the Worker behind authentication (e.g. Cloudflare Access or the `authorization` driver option), rate limiting, and CORS restrictions — running LLM-authored code is a high-trust operation. If you want a self-contained host without Cloudflare infrastructure, prefer `@tanstack/ai-isolate-node` or `@tanstack/ai-isolate-quickjs`. @@ -60,31 +60,31 @@ const result = await chat({ ## Worker Setup -### Option 1: Local Miniflare server +### Option 1: Local dev with `wrangler dev` From this package directory: ```bash -pnpm dev:worker +wrangler dev ``` -This starts a local Worker endpoint (default `http://localhost:8787`) with the `UNSAFE_EVAL` binding configured in `wrangler.toml`. +This starts a local Worker endpoint (default `http://localhost:8787`) with the `worker_loader` binding from `wrangler.toml`. Local workerd accepts the binding on the Workers Free plan, so no upgrade is needed for inner-loop iteration. -### Option 3: Production deployment +### Option 2: Wrangler remote dev ```bash -wrangler deploy +wrangler dev --remote ``` -The same `wrangler.toml` `[[unsafe.bindings]]` configuration applies in production. Deploying requires that your Cloudflare account has `unsafe_eval` enabled; without it, the Worker returns an `UnsafeEvalNotAvailable` error. Because this Worker executes LLM-generated code, only deploy it behind authentication, rate limiting, and an allow-listed origin. +Runs through Cloudflare's network for validation against the hosted runtime. Requires a Workers Paid plan because `worker_loader` is gated to Paid for any edge usage. -### Option 2: Wrangler remote dev +### Option 3: Production deployment ```bash -wrangler dev --remote +wrangler deploy ``` -This runs through Cloudflare's network and can be useful when validating behavior against the hosted runtime. +Requires a Workers Paid plan. The Free plan rejects deploys with `worker_loader` (`code: 10195` — "In order to use Dynamic Workers, you must switch to a paid plan."). Without the binding, the Worker returns a `WorkerLoaderNotAvailable` error. Because this Worker executes LLM-generated code, only deploy it behind authentication, rate limiting, and an allow-listed origin. ## API diff --git a/packages/typescript/ai-isolate-cloudflare/dev-server.mjs b/packages/typescript/ai-isolate-cloudflare/dev-server.mjs deleted file mode 100644 index eec0a4463..000000000 --- a/packages/typescript/ai-isolate-cloudflare/dev-server.mjs +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Local dev server using miniflare directly. - * - * wrangler dev does NOT translate [[unsafe.bindings]] type = "unsafe_eval" - * into miniflare's unsafeEvalBinding option, so the binding is always - * undefined in local dev. This script bundles the Worker with esbuild - * and runs it via miniflare with unsafeEvalBinding configured correctly. - * - * Usage: node dev-server.mjs [--port 8787] - */ - -import { Miniflare } from 'miniflare' -import { build } from 'esbuild' -import { resolve, dirname } from 'node:path' -import { fileURLToPath } from 'node:url' - -const __dirname = dirname(fileURLToPath(import.meta.url)) -const ENTRY = resolve(__dirname, 'src/worker/index.ts') -const PORT = Number(process.env.PORT) || 8787 - -const result = await build({ - entryPoints: [ENTRY], - bundle: true, - format: 'esm', - target: 'esnext', - write: false, -}) - -const mf = new Miniflare({ - modules: [ - { - type: 'ESModule', - path: 'worker.js', - contents: result.outputFiles[0].text, - }, - ], - unsafeEvalBinding: 'UNSAFE_EVAL', - compatibilityDate: '2024-12-01', - compatibilityFlags: ['nodejs_compat'], - port: PORT, -}) - -const url = await mf.ready -console.log(`Worker ready on ${url}`) - -process.on('SIGINT', async () => { - await mf.dispose() - process.exit(0) -}) diff --git a/packages/typescript/ai-isolate-cloudflare/package.json b/packages/typescript/ai-isolate-cloudflare/package.json index 55b619181..90cbc57d0 100644 --- a/packages/typescript/ai-isolate-cloudflare/package.json +++ b/packages/typescript/ai-isolate-cloudflare/package.json @@ -35,7 +35,7 @@ "scripts": { "build": "vite build", "clean": "premove ./build ./dist", - "dev:worker": "node dev-server.mjs", + "dev:worker": "wrangler dev", "deploy:worker": "wrangler deploy", "lint:fix": "eslint ./src --fix", "test:build": "publint --strict", diff --git a/packages/typescript/ai-isolate-cloudflare/src/worker/index.ts b/packages/typescript/ai-isolate-cloudflare/src/worker/index.ts index 2e6d4ff79..ffe543cf6 100644 --- a/packages/typescript/ai-isolate-cloudflare/src/worker/index.ts +++ b/packages/typescript/ai-isolate-cloudflare/src/worker/index.ts @@ -1,41 +1,88 @@ /** * Cloudflare Worker for Code Mode execution * - * This Worker executes JavaScript code in a V8 isolate on Cloudflare's edge network. - * Tool calls are handled via a request/response loop with the driver. + * Executes JavaScript code in a fresh V8 isolate on Cloudflare's edge network + * using the `worker_loader` (Dynamic Workers) binding. Tool calls round-trip + * to the driver via the same request/response protocol as before. * * Flow: * 1. Receive code + tool schemas - * 2. Execute code, collecting any tool calls - * 3. If tool calls are needed, return them to the driver - * 4. Driver executes tools locally, sends results back - * 5. Re-execute with tool results injected - * 6. Return final result + * 2. Wrap user code in an ES module exporting a `fetch` handler that returns + * the IIFE result as JSON + * 3. Load the module into a child Worker via `env.LOADER.load(...)` and + * invoke its entrypoint + * 4. If tool calls are needed, return them to the driver + * 5. Driver executes tools locally, sends results back + * 6. Re-execute with tool results injected + * 7. Return final result + * + * `worker_loader` replaces the previous `unsafe_eval` binding, which is gated + * by Cloudflare for all customer accounts and unusable in production. See + * https://developers.cloudflare.com/dynamic-workers/ for the supported API. */ import { wrapCode } from './wrap-code' import type { ExecuteRequest, ExecuteResponse, ToolCallRequest } from '../types' /** - * UnsafeEval binding type. + * Compatibility date for the loaded child Worker. Pinned at this layer so + * sandbox semantics don't drift with the parent Worker's compat date. + */ +const SANDBOX_COMPAT_DATE = '2026-05-01' + +/** + * Worker Loader binding type. * - * Provides dynamic-code execution against the Worker's V8 isolate. Available - * locally (via wrangler dev) and in production deployments where the - * `unsafe_eval` binding has been enabled on the Cloudflare account. + * Provides dynamic-code execution by loading a module into a fresh V8 + * isolate. Configure in wrangler.toml under `[[worker_loaders]]`. Requires a + * Workers Paid plan; see https://developers.cloudflare.com/dynamic-workers/. */ -interface UnsafeEval { - eval: (code: string) => unknown +interface WorkerLoaderEntrypoint { + fetch: (request: Request) => Promise +} + +interface LoadedWorker { + getEntrypoint: (name?: string) => WorkerLoaderEntrypoint +} + +interface WorkerLoader { + load: (options: { + compatibilityDate: string + mainModule: string + modules: Record + globalOutbound?: unknown + env?: Record + }) => LoadedWorker } interface Env { /** - * UnsafeEval binding. Configured in wrangler.toml as an unsafe binding. + * worker_loader (Dynamic Workers) binding. Configured in wrangler.toml + * under `[[worker_loaders]] binding = "LOADER"`. */ - UNSAFE_EVAL?: UnsafeEval + LOADER?: WorkerLoader } /** - * Execute code in the Worker's V8 isolate + * Wrap the existing IIFE-returning string in an ES module that exposes a + * `fetch` handler. The child Worker's entrypoint runs the IIFE on each + * invocation and returns the structured result as JSON. + */ +function wrapAsSandboxModule(wrappedCode: string): string { + return ` +export default { + async fetch() { + const __result = await ${wrappedCode}; + return new Response(JSON.stringify(__result), { + headers: { 'Content-Type': 'application/json' }, + }); + } +}; +` +} + +/** + * Execute code in a freshly loaded child Worker isolate. */ async function executeCode( request: ExecuteRequest, @@ -43,41 +90,55 @@ async function executeCode( ): Promise { const { code, tools, toolResults, timeout = 30000 } = request - // Check if UNSAFE_EVAL binding is available - if (!env.UNSAFE_EVAL) { + if (!env.LOADER) { return { status: 'error', error: { - name: 'UnsafeEvalNotAvailable', + name: 'WorkerLoaderNotAvailable', message: - 'UNSAFE_EVAL binding is not available. ' + - 'This Worker requires the unsafe_eval binding. ' + - 'Declare it in wrangler.toml under [[unsafe.bindings]] ' + - '(works for local development and production where the ' + - 'account has unsafe_eval enabled).', + 'LOADER binding is not available. ' + + 'This Worker requires the worker_loader (Dynamic Workers) binding. ' + + 'Declare it in wrangler.toml under [[worker_loaders]] with ' + + 'binding = "LOADER" (Workers Paid plan required).', }, } } try { const wrappedCode = wrapCode(code, tools, toolResults) + const moduleSource = wrapAsSandboxModule(wrappedCode) - // Execute with timeout - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), timeout) + let timeoutId: ReturnType | undefined + const TIMEOUT_SENTINEL = '__SANDBOX_TIMEOUT__' + const timeoutPromise = new Promise((_resolve, reject) => { + timeoutId = setTimeout(() => { + reject(new Error(TIMEOUT_SENTINEL)) + }, timeout) + }) try { - // Execute the wrapped code through the UNSAFE_EVAL binding. - const result = (await env.UNSAFE_EVAL.eval(wrappedCode)) as { + const loaded = env.LOADER.load({ + compatibilityDate: SANDBOX_COMPAT_DATE, + mainModule: 'main.js', + modules: { 'main.js': moduleSource }, + globalOutbound: null, + env: {}, + }) + const entrypoint = loaded.getEntrypoint() + const fetchPromise = entrypoint.fetch( + new Request('https://sandbox.invalid/'), + ) + const response = await Promise.race([fetchPromise, timeoutPromise]) + if (timeoutId) clearTimeout(timeoutId) + + const result: { status: string success?: boolean value?: unknown error?: { name: string; message: string; stack?: string } logs: Array toolCalls?: Array - } - - clearTimeout(timeoutId) + } = await response.json() if (result.status === 'need_tools') { return { @@ -96,9 +157,10 @@ async function executeCode( logs: result.logs, } } catch (evalError: unknown) { - clearTimeout(timeoutId) + if (timeoutId) clearTimeout(timeoutId) + const error = evalError as Error - if (controller.signal.aborted) { + if (error.message === TIMEOUT_SENTINEL) { return { status: 'error', error: { @@ -108,7 +170,6 @@ async function executeCode( } } - const error = evalError as Error return { status: 'done', success: false, diff --git a/packages/typescript/ai-isolate-cloudflare/src/worker/wrap-code.ts b/packages/typescript/ai-isolate-cloudflare/src/worker/wrap-code.ts index 72aafb6c8..3a711058e 100644 --- a/packages/typescript/ai-isolate-cloudflare/src/worker/wrap-code.ts +++ b/packages/typescript/ai-isolate-cloudflare/src/worker/wrap-code.ts @@ -1,6 +1,6 @@ /** * Code wrapping utilities for the Cloudflare Worker. - * Extracted for testability without UNSAFE_EVAL. + * Extracted for testability without a live worker_loader binding. */ import type { ToolResultPayload, ToolSchema } from '../types' diff --git a/packages/typescript/ai-isolate-cloudflare/tests/escape-attempts.test.ts b/packages/typescript/ai-isolate-cloudflare/tests/escape-attempts.test.ts index f68ab8109..7cfcfcabe 100644 --- a/packages/typescript/ai-isolate-cloudflare/tests/escape-attempts.test.ts +++ b/packages/typescript/ai-isolate-cloudflare/tests/escape-attempts.test.ts @@ -4,9 +4,9 @@ import type { ToolResultPayload, ToolSchema } from '../src/types' /** * The CF Worker delegates actual sandboxing to Workers' V8 isolate via the - * UNSAFE_EVAL binding, so we can't perform a real escape attempt in Node. What - * we verify here instead is structural — the wrapper must not let user inputs - * break out of their intended quoting/scoping. + * worker_loader binding, so we can't perform a real escape attempt in Node. + * What we verify here instead is structural — the wrapper must not let user + * inputs break out of their intended quoting/scoping. */ const benignTool: ToolSchema = { diff --git a/packages/typescript/ai-isolate-cloudflare/tests/isolate-driver.test.ts b/packages/typescript/ai-isolate-cloudflare/tests/isolate-driver.test.ts index 42e2dc2d5..848f0244d 100644 --- a/packages/typescript/ai-isolate-cloudflare/tests/isolate-driver.test.ts +++ b/packages/typescript/ai-isolate-cloudflare/tests/isolate-driver.test.ts @@ -424,8 +424,8 @@ describe('createCloudflareIsolateDriver', () => { ({ status: 'error', error: { - name: 'UnsafeEvalNotAvailable', - message: 'UNSAFE_EVAL binding is not available', + name: 'WorkerLoaderNotAvailable', + message: 'LOADER binding is not available', }, }) as ExecuteResponse, }) @@ -436,8 +436,8 @@ describe('createCloudflareIsolateDriver', () => { const result = await context.execute('return 1') expect(result.success).toBe(false) - expect(result.error?.name).toBe('UnsafeEvalNotAvailable') - expect(result.error?.message).toContain('UNSAFE_EVAL') + expect(result.error?.name).toBe('WorkerLoaderNotAvailable') + expect(result.error?.message).toContain('LOADER') }) it('returns error when Worker returns status: done with success: false', async () => { diff --git a/packages/typescript/ai-isolate-cloudflare/tests/worker.test.ts b/packages/typescript/ai-isolate-cloudflare/tests/worker.test.ts index 27534e1b9..99e18ed26 100644 --- a/packages/typescript/ai-isolate-cloudflare/tests/worker.test.ts +++ b/packages/typescript/ai-isolate-cloudflare/tests/worker.test.ts @@ -6,7 +6,21 @@ import workerModule from '../src/worker/index' const worker = workerModule as { fetch: ( request: Request, - env: { UNSAFE_EVAL?: { eval: (code: string) => unknown } }, + env: { + LOADER?: { + load: (options: { + compatibilityDate: string + mainModule: string + modules: Record + globalOutbound?: unknown + env?: Record + }) => { + getEntrypoint: (name?: string) => { + fetch: (request: Request) => Promise + } + } + } + }, ctx: ExecutionContext, ) => Promise } @@ -174,7 +188,7 @@ describe('Worker fetch handler', () => { expect(json).toHaveProperty('error', 'Code is required') }) - it('returns 200 with UnsafeEvalNotAvailable when env has no UNSAFE_EVAL', async () => { + it('returns 200 with WorkerLoaderNotAvailable when env has no LOADER', async () => { const request = new Request('https://worker.test/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -188,11 +202,10 @@ describe('Worker fetch handler', () => { expect(response.status).toBe(200) const json = await response.json() expect(json.status).toBe('error') - expect(json.error.name).toBe('UnsafeEvalNotAvailable') - expect(json.error.message).toContain('UNSAFE_EVAL') + expect(json.error.name).toBe('WorkerLoaderNotAvailable') + expect(json.error.message).toContain('LOADER') + expect(json.error.message).toContain('worker_loaders') expect(json.error.message).toContain('wrangler.toml') - // No longer steers users to Workers for Platforms - expect(json.error.message).not.toContain('Workers for Platforms') }) it('returns 500 with RequestError when body is invalid JSON', async () => { diff --git a/packages/typescript/ai-isolate-cloudflare/wrangler.toml b/packages/typescript/ai-isolate-cloudflare/wrangler.toml index 39dab3696..87752bad8 100644 --- a/packages/typescript/ai-isolate-cloudflare/wrangler.toml +++ b/packages/typescript/ai-isolate-cloudflare/wrangler.toml @@ -1,22 +1,24 @@ #:schema node_modules/wrangler/config-schema.json # Cloudflare Worker configuration for Code Mode execution -# Run locally: pnpm dev:worker (or wrangler dev) -# Deploy: pnpm deploy:worker (or wrangler deploy) +# Run locally: wrangler dev (Dynamic Workers binding works in local workerd +# on the Free plan; deploy/--remote require Paid) +# Deploy: wrangler deploy name = "tanstack-ai-code-mode" main = "src/worker/index.ts" -compatibility_date = "2024-12-01" +compatibility_date = "2026-05-01" compatibility_flags = ["nodejs_compat"] -# UnsafeEval binding - enables dynamic code execution inside the Worker's V8 isolate. -# Works in both local dev (wrangler dev) and production deployments where the -# Cloudflare account has the unsafe_eval binding enabled. Because this lets the -# Worker evaluate arbitrary JavaScript, protect the Worker's public endpoint -# with authentication and rate limiting before deploying. -[[unsafe.bindings]] -name = "UNSAFE_EVAL" -type = "unsafe_eval" +# worker_loader (Dynamic Workers) binding — loads LLM-generated code into a +# fresh V8 isolate via env.LOADER.load(...). Replaces the previous +# `unsafe_eval` binding, which Cloudflare gates for all customer prod +# deploys. Workers Paid plan required to deploy or run --remote; local +# wrangler dev works on the Free plan. Because this Worker evaluates +# arbitrary JavaScript, protect the public endpoint with authentication and +# rate limiting before deploying. +[[worker_loaders]] +binding = "LOADER" # Local development settings [dev] From 669a2aac3d6bd160bc0481b977d2c1ea44113f99 Mon Sep 17 00:00:00 2001 From: Sriket Komali Date: Sat, 2 May 2026 20:29:50 -0400 Subject: [PATCH 2/5] fix(ai-isolate-cloudflare): cancel in-flight fetch on timeout + happy-path tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address CodeRabbit review on #523: 1. Promise.race timeout left `entrypoint.fetch` running, leaking the loaded child Worker isolate. Add an AbortController whose signal flows into the Request passed to entrypoint.fetch — the timeout now actually cancels the in-flight request. Promise.race remains as a belt-and-suspenders guard. 2. Add three integration tests against a mocked LOADER binding: - happy path: full load → getEntrypoint → fetch chain, asserts the load() arguments (mainModule, modules, globalOutbound) and that the Request carries an AbortSignal - need_tools: forwards toolCalls + continuationId from sandbox - TimeoutError: AbortSignal-driven cancellation triggers the right error shape Tests: 39/39 pass. --- .../ai-isolate-cloudflare/src/worker/index.ts | 8 +- .../tests/worker.test.ts | 127 ++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/packages/typescript/ai-isolate-cloudflare/src/worker/index.ts b/packages/typescript/ai-isolate-cloudflare/src/worker/index.ts index ffe543cf6..fa795923f 100644 --- a/packages/typescript/ai-isolate-cloudflare/src/worker/index.ts +++ b/packages/typescript/ai-isolate-cloudflare/src/worker/index.ts @@ -108,10 +108,16 @@ async function executeCode( const wrappedCode = wrapCode(code, tools, toolResults) const moduleSource = wrapAsSandboxModule(wrappedCode) + // AbortController propagates into the loaded Worker via Request.signal so + // a timeout actually cancels the in-flight fetch instead of leaking the + // child isolate. The Promise.race remains as a belt-and-suspenders guard + // for runtimes that ignore the signal. + const controller = new AbortController() let timeoutId: ReturnType | undefined const TIMEOUT_SENTINEL = '__SANDBOX_TIMEOUT__' const timeoutPromise = new Promise((_resolve, reject) => { timeoutId = setTimeout(() => { + controller.abort() reject(new Error(TIMEOUT_SENTINEL)) }, timeout) }) @@ -126,7 +132,7 @@ async function executeCode( }) const entrypoint = loaded.getEntrypoint() const fetchPromise = entrypoint.fetch( - new Request('https://sandbox.invalid/'), + new Request('https://sandbox.invalid/', { signal: controller.signal }), ) const response = await Promise.race([fetchPromise, timeoutPromise]) if (timeoutId) clearTimeout(timeoutId) diff --git a/packages/typescript/ai-isolate-cloudflare/tests/worker.test.ts b/packages/typescript/ai-isolate-cloudflare/tests/worker.test.ts index 99e18ed26..1974eaf29 100644 --- a/packages/typescript/ai-isolate-cloudflare/tests/worker.test.ts +++ b/packages/typescript/ai-isolate-cloudflare/tests/worker.test.ts @@ -208,6 +208,133 @@ describe('Worker fetch handler', () => { expect(json.error.message).toContain('wrangler.toml') }) + it('exercises the LOADER.load → getEntrypoint → fetch chain on the happy path', async () => { + let loadCalled = false + let receivedSignal: AbortSignal | null = null + const env = { + LOADER: { + load: (options: { + compatibilityDate: string + mainModule: string + modules: Record + globalOutbound?: unknown + env?: Record + }) => { + loadCalled = true + // Sanity-check the load() arguments the worker passes. + expect(options.mainModule).toBe('main.js') + expect(options.modules).toHaveProperty('main.js') + expect(options.modules['main.js']).toContain('export default') + expect(options.globalOutbound).toBeNull() + return { + getEntrypoint: () => ({ + fetch: async (req: Request) => { + receivedSignal = req.signal + return new Response( + JSON.stringify({ + status: 'done', + success: true, + value: 42, + logs: ['hello from sandbox'], + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }, + }), + } + }, + }, + } + + const request = new Request('https://worker.test/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: 'return 42', tools: [], timeout: 5000 }), + }) + const response = await worker.fetch(request, env, mockExecutionContext) + + expect(loadCalled).toBe(true) + expect(receivedSignal).not.toBeNull() + expect(response.status).toBe(200) + const json = await response.json() + expect(json.status).toBe('done') + expect(json.success).toBe(true) + expect(json.value).toBe(42) + expect(json.logs).toEqual(['hello from sandbox']) + }) + + it('forwards need_tools status from the loaded Worker back to the driver', async () => { + const env = { + LOADER: { + load: () => ({ + getEntrypoint: () => ({ + fetch: async () => + new Response( + JSON.stringify({ + status: 'need_tools', + toolCalls: [{ id: 'tc_0', name: 'fetchData', args: {} }], + logs: [], + }), + { headers: { 'Content-Type': 'application/json' } }, + ), + }), + }), + }, + } + + const request = new Request('https://worker.test/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + code: 'return await fetchData({})', + tools: [{ name: 'fetchData', description: 'd', inputSchema: {} }], + }), + }) + const response = await worker.fetch(request, env, mockExecutionContext) + + expect(response.status).toBe(200) + const json = await response.json() + expect(json.status).toBe('need_tools') + expect(json.toolCalls).toHaveLength(1) + expect(json.toolCalls[0].name).toBe('fetchData') + expect(typeof json.continuationId).toBe('string') + }) + + it('returns TimeoutError when entrypoint.fetch exceeds timeout', async () => { + const env = { + LOADER: { + load: () => ({ + getEntrypoint: () => ({ + fetch: (req: Request) => + new Promise((_resolve, reject) => { + req.signal.addEventListener('abort', () => { + reject(new Error('aborted')) + }) + // Never resolves on its own; relies on AbortSignal. + }), + }), + }), + }, + } + + const request = new Request('https://worker.test/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + code: 'while(true){}', + tools: [], + timeout: 50, + }), + }) + const response = await worker.fetch(request, env, mockExecutionContext) + + expect(response.status).toBe(200) + const json = await response.json() + expect(json.status).toBe('error') + expect(json.error.name).toBe('TimeoutError') + expect(json.error.message).toContain('50ms') + }) + it('returns 500 with RequestError when body is invalid JSON', async () => { const request = new Request('https://worker.test/', { method: 'POST', From b2ce85fe432f898afdae4e1f473701aa81d8631c Mon Sep 17 00:00:00 2001 From: Sriket Komali Date: Sat, 2 May 2026 20:52:40 -0400 Subject: [PATCH 3/5] fix(ai-isolate-cloudflare): tighten timeout test + happy-path assertions Address CodeRabbit second-pass review: 1. happy-path test: hoist load() argument assertions out of the synchronous mock. Inside load() they get swallowed by the worker's outer try/catch and surface as a generic 500. Capture options into a local + assert after worker.fetch() resolves. 2. timeout test: `expect(receivedSignal).not.toBeNull()` is trivially true per the Fetch spec (Request.signal is always present). Drop it from the happy-path test and instead assert `signal.aborted === true` in the timeout test, which actually proves the outer worker's AbortController fired. 3. worker fix: when the AbortController fires first, fetchPromise rejects before timeoutPromise. Detect the timeout via either TIMEOUT_SENTINEL or `controller.signal.aborted` so the right error surfaces regardless of which branch of the race wins. Tests: 39/39 pass. --- .../ai-isolate-cloudflare/src/worker/index.ts | 5 +- .../tests/worker.test.ts | 64 +++++++++++-------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/packages/typescript/ai-isolate-cloudflare/src/worker/index.ts b/packages/typescript/ai-isolate-cloudflare/src/worker/index.ts index fa795923f..073b89252 100644 --- a/packages/typescript/ai-isolate-cloudflare/src/worker/index.ts +++ b/packages/typescript/ai-isolate-cloudflare/src/worker/index.ts @@ -166,7 +166,10 @@ async function executeCode( if (timeoutId) clearTimeout(timeoutId) const error = evalError as Error - if (error.message === TIMEOUT_SENTINEL) { + // Either branch of the Promise.race may win on timeout: timeoutPromise + // rejects with TIMEOUT_SENTINEL, while the AbortController.abort() call + // can race-reject the in-flight fetch first. Treat both as a timeout. + if (error.message === TIMEOUT_SENTINEL || controller.signal.aborted) { return { status: 'error', error: { diff --git a/packages/typescript/ai-isolate-cloudflare/tests/worker.test.ts b/packages/typescript/ai-isolate-cloudflare/tests/worker.test.ts index 1974eaf29..1b692ef63 100644 --- a/packages/typescript/ai-isolate-cloudflare/tests/worker.test.ts +++ b/packages/typescript/ai-isolate-cloudflare/tests/worker.test.ts @@ -209,37 +209,37 @@ describe('Worker fetch handler', () => { }) it('exercises the LOADER.load → getEntrypoint → fetch chain on the happy path', async () => { + // Capture mock state for post-fetch assertions. Asserting inside the + // synchronous `load()` mock would be swallowed by the outer worker's + // try/catch and surface as a generic 500, masking the real failure. let loadCalled = false - let receivedSignal: AbortSignal | null = null + type LoadOptions = { + compatibilityDate: string + mainModule: string + modules: Record + globalOutbound?: unknown + env?: Record + } + let capturedOptions: LoadOptions | null = null const env = { LOADER: { - load: (options: { - compatibilityDate: string - mainModule: string - modules: Record - globalOutbound?: unknown - env?: Record - }) => { + load: (options: LoadOptions) => { loadCalled = true - // Sanity-check the load() arguments the worker passes. - expect(options.mainModule).toBe('main.js') - expect(options.modules).toHaveProperty('main.js') - expect(options.modules['main.js']).toContain('export default') - expect(options.globalOutbound).toBeNull() + capturedOptions = options return { getEntrypoint: () => ({ - fetch: async (req: Request) => { - receivedSignal = req.signal - return new Response( - JSON.stringify({ - status: 'done', - success: true, - value: 42, - logs: ['hello from sandbox'], - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }, + fetch: (_req: Request) => + Promise.resolve( + new Response( + JSON.stringify({ + status: 'done', + success: true, + value: 42, + logs: ['hello from sandbox'], + }), + { headers: { 'Content-Type': 'application/json' } }, + ), + ), }), } }, @@ -254,7 +254,11 @@ describe('Worker fetch handler', () => { const response = await worker.fetch(request, env, mockExecutionContext) expect(loadCalled).toBe(true) - expect(receivedSignal).not.toBeNull() + expect(capturedOptions).not.toBeNull() + expect(capturedOptions!.mainModule).toBe('main.js') + expect(capturedOptions!.modules).toHaveProperty('main.js') + expect(capturedOptions!.modules['main.js']).toContain('export default') + expect(capturedOptions!.globalOutbound).toBeNull() expect(response.status).toBe(200) const json = await response.json() expect(json.status).toBe('done') @@ -301,12 +305,18 @@ describe('Worker fetch handler', () => { }) it('returns TimeoutError when entrypoint.fetch exceeds timeout', async () => { + // Capture the AbortSignal seen by the loaded Worker so we can assert that + // the outer worker's AbortController actually fires `abort` on timeout. + // (Request.signal is always non-null per spec, so `not.toBeNull()` would + // be trivially true and prove nothing.) + let receivedSignal: AbortSignal | null = null const env = { LOADER: { load: () => ({ getEntrypoint: () => ({ fetch: (req: Request) => new Promise((_resolve, reject) => { + receivedSignal = req.signal req.signal.addEventListener('abort', () => { reject(new Error('aborted')) }) @@ -327,6 +337,8 @@ describe('Worker fetch handler', () => { }), }) const response = await worker.fetch(request, env, mockExecutionContext) + expect(receivedSignal).not.toBeNull() + expect(receivedSignal!.aborted).toBe(true) expect(response.status).toBe(200) const json = await response.json() From a266c4172210e74a81ad86b2428322f98c16ebae Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Thu, 7 May 2026 16:58:38 +1000 Subject: [PATCH 4/5] Updated wrangler --- .gitignore | 1 + .../ai-isolate-cloudflare/package.json | 2 +- pnpm-lock.yaml | 167 ++++++++++++++---- 3 files changed, 131 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index 4c8ee6364..6c359c884 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ stats.html .tsup .vinxi temp +.wrangler vite.config.js.timestamp-* vite.config.ts.timestamp-* diff --git a/packages/typescript/ai-isolate-cloudflare/package.json b/packages/typescript/ai-isolate-cloudflare/package.json index 90cbc57d0..ecf9c5ddf 100644 --- a/packages/typescript/ai-isolate-cloudflare/package.json +++ b/packages/typescript/ai-isolate-cloudflare/package.json @@ -61,6 +61,6 @@ "@vitest/coverage-v8": "4.0.14", "esbuild": "^0.25.12", "miniflare": "^4.20260305.0", - "wrangler": "^4.19.1" + "wrangler": "^4.88.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb41b0817..db28f82c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -589,7 +589,7 @@ importers: version: 0.561.0(react@19.2.3) nitro: specifier: latest - version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@17.2.3)(giget@2.0.0)(jiti@2.6.1)(rollup@4.60.1)(vite@7.3.1(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.0.260429-beta(chokidar@5.0.0)(dotenv@17.2.3)(giget@2.0.0)(jiti@2.6.1)(miniflare@4.20260504.0)(rollup@4.60.1)(vite@7.3.1(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -1249,8 +1249,8 @@ importers: specifier: ^4.20260305.0 version: 4.20260317.0 wrangler: - specifier: ^4.19.1 - version: 4.75.0(@cloudflare/workers-types@4.20260317.1) + specifier: ^4.88.0 + version: 4.88.0(@cloudflare/workers-types@4.20260317.1) packages/typescript/ai-isolate-node: dependencies: @@ -1671,7 +1671,7 @@ importers: dependencies: '@copilotkit/aimock': specifier: latest - version: 1.14.0 + version: 1.19.0(vitest@4.1.4(@types/node@24.10.3)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.9))(vite@7.3.1(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))) '@tailwindcss/vite': specifier: ^4.1.18 version: 4.1.18(vite@7.3.1(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) @@ -2139,19 +2139,19 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@cloudflare/kv-asset-handler@0.4.1': - resolution: {integrity: sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==} - engines: {node: '>=18.0.0'} - '@cloudflare/kv-asset-handler@0.4.2': resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} engines: {node: '>=18.0.0'} - '@cloudflare/unenv-preset@2.15.0': - resolution: {integrity: sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw==} + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} peerDependencies: unenv: 2.0.0-rc.24 - workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0 + workerd: '>1.20260305.0 <2.0.0-0' peerDependenciesMeta: workerd: optional: true @@ -2162,37 +2162,75 @@ packages: cpu: [x64] os: [darwin] + '@cloudflare/workerd-darwin-64@1.20260504.1': + resolution: {integrity: sha512-IOMjYoftNRXabFt+QzY2Bo2mR2TNl8xsGvE0HnQ+K0S2c61VOUGUkr9gpJjnwrJ65yA9Qed4xfg0RRqXHO+nfA==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20260317.1': resolution: {integrity: sha512-M/MnNyvO5HMgoIdr3QHjdCj2T1ki9gt0vIUnxYxBu9ISXS/jgtMl6chUVPJ7zHYBn9MyYr8ByeN6frjYxj0MGg==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20260504.1': + resolution: {integrity: sha512-7iMXxIU0N5KklZpQm2kuwTm0XtrpHXNqhejJyGquky8gSTnm31zBdutjMekH8VRr6ckbvZIl6lvqXzXdfOEojg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + '@cloudflare/workerd-linux-64@1.20260317.1': resolution: {integrity: sha512-1ltuEjkRcS3fsVF7CxsKlWiRmzq2ZqMfqDN0qUOgbUwkpXsLVJsXmoblaLf5OP00ELlcgF0QsN0p2xPEua4Uug==} engines: {node: '>=16'} cpu: [x64] os: [linux] + '@cloudflare/workerd-linux-64@1.20260504.1': + resolution: {integrity: sha512-YLB0EH5FQV++oWlalFgPF3p2Bp3dn/D6RWNMw0ukEC8gKnNX6o61A+dlFUl8hRD35ja1zKRxGFUojs4U2+MoJA==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + '@cloudflare/workerd-linux-arm64@1.20260317.1': resolution: {integrity: sha512-3QrNnPF1xlaNwkHpasvRvAMidOvQs2NhXQmALJrEfpIJ/IDL2la8g499yXp3eqhG3hVMCB07XVY149GTs42Xtw==} engines: {node: '>=16'} cpu: [arm64] os: [linux] + '@cloudflare/workerd-linux-arm64@1.20260504.1': + resolution: {integrity: sha512-FAh/82jDXDArfn9xDih6f/IJfF2SHXBb4nFeQAyHyvXrn18zM6Q3yl2Vj0U7LybbNbmu7TNGghwaM2NoSQS+0A==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + '@cloudflare/workerd-windows-64@1.20260317.1': resolution: {integrity: sha512-MfZTz+7LfuIpMGTa3RLXHX8Z/pnycZLItn94WRdHr8LPVet+C5/1Nzei399w/jr3+kzT4pDKk26JF/tlI5elpQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] + '@cloudflare/workerd-windows-64@1.20260504.1': + resolution: {integrity: sha512-QUg/B3dfrK/KHHHhiJzdkLkTg5mG7lA3t8iplbBoUa3XKCLOHOOXhbU4WSYlLqg8YnsQ6XLZ1HVA99fmZhJh7A==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + '@cloudflare/workers-types@4.20260317.1': resolution: {integrity: sha512-+G4eVwyCpm8Au1ex8vQBCuA9wnwqetz4tPNRoB/53qvktERWBRMQnrtvC1k584yRE3emMThtuY0gWshvSJ++PQ==} - '@copilotkit/aimock@1.14.0': - resolution: {integrity: sha512-1NqwWEameArC7HWT7UHBlkq3pNlCA0eHBocaeL6mS5CULolT9XFL27tC9jJ+OSmREzLwkKbFYaAl2SssaXexVA==} - engines: {node: '>=20.15.0'} + '@copilotkit/aimock@1.19.0': + resolution: {integrity: sha512-sbrWDTkJOlK1Y/ARxvVgUvqESQPkBac7Dd7A4Bg/46R6t+D3jGqvWeClpIo380vymGCDrhZGRb1HeQTb9Fg0nA==} + engines: {node: '>=24.0.0'} hasBin: true + peerDependencies: + jest: '>=29' + vitest: '>=3' + peerDependenciesMeta: + jest: + optional: true + vitest: + optional: true '@crazydos/vue-markdown@1.1.4': resolution: {integrity: sha512-0I1QMP59LJ3aEjE7bolgvPU4JAFt+pykdDo5674CbsCwFo7OVFos50+MPhGdWflCz1mac5t152lB1qvV/tR/rw==} @@ -9368,6 +9406,11 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + miniflare@4.20260504.0: + resolution: {integrity: sha512-HeI/HLx+rbeo/UB4qb6NsNcFdUVD7xDzyCexZJTVtFMlfpfexUKEDmdeTRRpzeHrJseZFGua+v9JO1kfPublUw==} + engines: {node: '>=22.0.0'} + hasBin: true + minimatch@10.1.1: resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} @@ -9486,16 +9529,16 @@ packages: xml2js: optional: true - nitro@3.0.260415-beta: - resolution: {integrity: sha512-J0ntJERWtIdvweZdmkCiF8eOFvP9fIAJR2gpeIDrHbAlYavK41WQfADo/YoZ/LF7RMTZBiPaH/pt2s/nPru9Iw==} + nitro@3.0.260429-beta: + resolution: {integrity: sha512-KweLVCUN5X9v9g+4yxAyRcz3FcOlnjmt9FyrAIWDxJETJmNT7I0JV0clgsONjo2nI0U5gwedXYA3RaNtF5XWzg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - '@vercel/queue': ^0.1.4 + '@vercel/queue': ^0.1.6 dotenv: '*' giget: '*' jiti: ^2.6.1 - rollup: ^4.60.1 + rollup: ^4.60.2 vite: ^7 || ^8 xml2js: ^0.6.2 zephyr-agent: ^0.2.0 @@ -11128,6 +11171,10 @@ packages: resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} engines: {node: '>=20.18.1'} + undici@7.24.8: + resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} + engines: {node: '>=20.18.1'} + unenv@1.10.0: resolution: {integrity: sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==} @@ -11865,12 +11912,17 @@ packages: engines: {node: '>=16'} hasBin: true - wrangler@4.75.0: - resolution: {integrity: sha512-Efk1tcnm4eduBYpH1sSjMYydXMnIFPns/qABI3+fsbDrUk5GksNYX8nYGVP4sFygvGPO7kJc36YJKB5ooA7JAg==} - engines: {node: '>=20.0.0'} + workerd@1.20260504.1: + resolution: {integrity: sha512-AQTXSHbYNP9tLPgJNn0TmizyE4aDh2VuZZXlTAL0uu4fbCY436NAnQSJIzZbaFHM3DnAtVs9G8tkiJztSdYqDg==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.88.0: + resolution: {integrity: sha512-f470QwbeT/JM1S0duq+sLtkss7UBxIFDtYHgujv9tdQUyA/dLGDq51am0rqrsuFtCi97lTM1P5sqtt8xra1AlA==} + engines: {node: '>=22.0.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20260317.1 + '@cloudflare/workers-types': ^4.20260504.1 peerDependenciesMeta: '@cloudflare/workers-types': optional: true @@ -12408,36 +12460,51 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 - '@cloudflare/kv-asset-handler@0.4.1': - dependencies: - mime: 3.0.0 - '@cloudflare/kv-asset-handler@0.4.2': {} - '@cloudflare/unenv-preset@2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1)': + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260504.1)': dependencies: unenv: 2.0.0-rc.24 optionalDependencies: - workerd: 1.20260317.1 + workerd: 1.20260504.1 '@cloudflare/workerd-darwin-64@1.20260317.1': optional: true + '@cloudflare/workerd-darwin-64@1.20260504.1': + optional: true + '@cloudflare/workerd-darwin-arm64@1.20260317.1': optional: true + '@cloudflare/workerd-darwin-arm64@1.20260504.1': + optional: true + '@cloudflare/workerd-linux-64@1.20260317.1': optional: true + '@cloudflare/workerd-linux-64@1.20260504.1': + optional: true + '@cloudflare/workerd-linux-arm64@1.20260317.1': optional: true + '@cloudflare/workerd-linux-arm64@1.20260504.1': + optional: true + '@cloudflare/workerd-windows-64@1.20260317.1': optional: true + '@cloudflare/workerd-windows-64@1.20260504.1': + optional: true + '@cloudflare/workers-types@4.20260317.1': {} - '@copilotkit/aimock@1.14.0': {} + '@copilotkit/aimock@1.19.0(vitest@4.1.4(@types/node@24.10.3)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.9))(vite@7.3.1(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))': + optionalDependencies: + vitest: 4.1.4(@types/node@24.10.3)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.9))(vite@7.3.1(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@crazydos/vue-markdown@1.1.4(vue@3.5.25(typescript@5.9.3))': dependencies: @@ -18409,12 +18476,14 @@ snapshots: env-paths@2.2.1: {} - env-runner@0.1.7: + env-runner@0.1.7(miniflare@4.20260504.0): dependencies: crossws: 0.4.5(srvx@0.11.15) exsolve: 1.0.8 httpxy: 0.5.1 srvx: 0.11.15 + optionalDependencies: + miniflare: 4.20260504.0 error-ex@1.3.4: dependencies: @@ -20555,6 +20624,18 @@ snapshots: - bufferutil - utf-8-validate + miniflare@4.20260504.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.24.8 + workerd: 1.20260504.1 + ws: 8.18.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + minimatch@10.1.1: dependencies: '@isaacs/brace-expansion': 5.0.0 @@ -20689,12 +20770,12 @@ snapshots: - sqlite3 - uploadthing - nitro@3.0.260415-beta(chokidar@5.0.0)(dotenv@17.2.3)(giget@2.0.0)(jiti@2.6.1)(rollup@4.60.1)(vite@7.3.1(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + nitro@3.0.260429-beta(chokidar@5.0.0)(dotenv@17.2.3)(giget@2.0.0)(jiti@2.6.1)(miniflare@4.20260504.0)(rollup@4.60.1)(vite@7.3.1(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: consola: 3.4.2 crossws: 0.4.5(srvx@0.11.15) db0: 0.3.4 - env-runner: 0.1.7 + env-runner: 0.1.7(miniflare@4.20260504.0) h3: 2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)) hookable: 6.1.1 nf3: 0.3.16 @@ -20744,7 +20825,7 @@ snapshots: nitropack@2.12.9(rolldown@1.0.0-rc.17): dependencies: - '@cloudflare/kv-asset-handler': 0.4.1 + '@cloudflare/kv-asset-handler': 0.4.2 '@rollup/plugin-alias': 5.1.1(rollup@4.57.1) '@rollup/plugin-commonjs': 28.0.9(rollup@4.57.1) '@rollup/plugin-inject': 5.0.5(rollup@4.57.1) @@ -22917,6 +22998,8 @@ snapshots: undici@7.24.4: {} + undici@7.24.8: {} + unenv@1.10.0: dependencies: consola: 3.4.2 @@ -23705,16 +23788,24 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260317.1 '@cloudflare/workerd-windows-64': 1.20260317.1 - wrangler@4.75.0(@cloudflare/workers-types@4.20260317.1): + workerd@1.20260504.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260504.1 + '@cloudflare/workerd-darwin-arm64': 1.20260504.1 + '@cloudflare/workerd-linux-64': 1.20260504.1 + '@cloudflare/workerd-linux-arm64': 1.20260504.1 + '@cloudflare/workerd-windows-64': 1.20260504.1 + + wrangler@4.88.0(@cloudflare/workers-types@4.20260317.1): dependencies: - '@cloudflare/kv-asset-handler': 0.4.2 - '@cloudflare/unenv-preset': 2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1) + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260504.1) blake3-wasm: 2.1.5 esbuild: 0.27.3 - miniflare: 4.20260317.0 + miniflare: 4.20260504.0 path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 - workerd: 1.20260317.1 + workerd: 1.20260504.1 optionalDependencies: '@cloudflare/workers-types': 4.20260317.1 fsevents: 2.3.3 From 4d4fb1978f0bc69151239adf5ff7842b5855f02b Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Thu, 7 May 2026 17:33:31 +1000 Subject: [PATCH 5/5] chore(ai-isolate-cloudflare): drop unused esbuild and miniflare devDeps These were only used by the deleted dev-server.mjs, which existed to work around wrangler dev not surfacing the old unsafe_eval binding. With the worker_loader port, wrangler dev handles dev natively. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ai-isolate-cloudflare/package.json | 2 - pnpm-lock.yaml | 87 ------------------- 2 files changed, 89 deletions(-) diff --git a/packages/typescript/ai-isolate-cloudflare/package.json b/packages/typescript/ai-isolate-cloudflare/package.json index ecf9c5ddf..df1b5d02c 100644 --- a/packages/typescript/ai-isolate-cloudflare/package.json +++ b/packages/typescript/ai-isolate-cloudflare/package.json @@ -59,8 +59,6 @@ "devDependencies": { "@cloudflare/workers-types": "^4.20241230.0", "@vitest/coverage-v8": "4.0.14", - "esbuild": "^0.25.12", - "miniflare": "^4.20260305.0", "wrangler": "^4.88.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e12b8399..30003271c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1245,12 +1245,6 @@ importers: '@vitest/coverage-v8': specifier: 4.0.14 version: 4.0.14(vitest@4.1.4) - esbuild: - specifier: ^0.25.12 - version: 0.25.12 - miniflare: - specifier: ^4.20260305.0 - version: 4.20260317.0 wrangler: specifier: ^4.88.0 version: 4.88.0(@cloudflare/workers-types@4.20260317.1) @@ -2158,60 +2152,30 @@ packages: workerd: optional: true - '@cloudflare/workerd-darwin-64@1.20260317.1': - resolution: {integrity: sha512-8hjh3sPMwY8M/zedq3/sXoA2Q4BedlGufn3KOOleIG+5a4ReQKLlUah140D7J6zlKmYZAFMJ4tWC7hCuI/s79g==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - '@cloudflare/workerd-darwin-64@1.20260504.1': resolution: {integrity: sha512-IOMjYoftNRXabFt+QzY2Bo2mR2TNl8xsGvE0HnQ+K0S2c61VOUGUkr9gpJjnwrJ65yA9Qed4xfg0RRqXHO+nfA==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260317.1': - resolution: {integrity: sha512-M/MnNyvO5HMgoIdr3QHjdCj2T1ki9gt0vIUnxYxBu9ISXS/jgtMl6chUVPJ7zHYBn9MyYr8ByeN6frjYxj0MGg==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260504.1': resolution: {integrity: sha512-7iMXxIU0N5KklZpQm2kuwTm0XtrpHXNqhejJyGquky8gSTnm31zBdutjMekH8VRr6ckbvZIl6lvqXzXdfOEojg==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20260317.1': - resolution: {integrity: sha512-1ltuEjkRcS3fsVF7CxsKlWiRmzq2ZqMfqDN0qUOgbUwkpXsLVJsXmoblaLf5OP00ELlcgF0QsN0p2xPEua4Uug==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - '@cloudflare/workerd-linux-64@1.20260504.1': resolution: {integrity: sha512-YLB0EH5FQV++oWlalFgPF3p2Bp3dn/D6RWNMw0ukEC8gKnNX6o61A+dlFUl8hRD35ja1zKRxGFUojs4U2+MoJA==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260317.1': - resolution: {integrity: sha512-3QrNnPF1xlaNwkHpasvRvAMidOvQs2NhXQmALJrEfpIJ/IDL2la8g499yXp3eqhG3hVMCB07XVY149GTs42Xtw==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260504.1': resolution: {integrity: sha512-FAh/82jDXDArfn9xDih6f/IJfF2SHXBb4nFeQAyHyvXrn18zM6Q3yl2Vj0U7LybbNbmu7TNGghwaM2NoSQS+0A==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20260317.1': - resolution: {integrity: sha512-MfZTz+7LfuIpMGTa3RLXHX8Z/pnycZLItn94WRdHr8LPVet+C5/1Nzei399w/jr3+kzT4pDKk26JF/tlI5elpQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] - '@cloudflare/workerd-windows-64@1.20260504.1': resolution: {integrity: sha512-QUg/B3dfrK/KHHHhiJzdkLkTg5mG7lA3t8iplbBoUa3XKCLOHOOXhbU4WSYlLqg8YnsQ6XLZ1HVA99fmZhJh7A==} engines: {node: '>=16'} @@ -9416,11 +9380,6 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} - miniflare@4.20260317.0: - resolution: {integrity: sha512-xuwk5Kjv+shi5iUBAdCrRl9IaWSGnTU8WuTQzsUS2GlSDIMCJuu8DiF/d9ExjMXYiQG5ml+k9SVKnMj8cRkq0w==} - engines: {node: '>=18.0.0'} - hasBin: true - miniflare@4.20260504.0: resolution: {integrity: sha512-HeI/HLx+rbeo/UB4qb6NsNcFdUVD7xDzyCexZJTVtFMlfpfexUKEDmdeTRRpzeHrJseZFGua+v9JO1kfPublUw==} engines: {node: '>=22.0.0'} @@ -11182,10 +11141,6 @@ packages: resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==} engines: {node: '>=20.18.1'} - undici@7.24.4: - resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} - engines: {node: '>=20.18.1'} - undici@7.24.8: resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} engines: {node: '>=20.18.1'} @@ -11922,11 +11877,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - workerd@1.20260317.1: - resolution: {integrity: sha512-ZuEq1OdrJBS+NV+L5HMYPCzVn49a2O60slQiiLpG44jqtlOo+S167fWC76kEXteXLLLydeuRrluRel7WdOUa4g==} - engines: {node: '>=16'} - hasBin: true - workerd@1.20260504.1: resolution: {integrity: sha512-AQTXSHbYNP9tLPgJNn0TmizyE4aDh2VuZZXlTAL0uu4fbCY436NAnQSJIzZbaFHM3DnAtVs9G8tkiJztSdYqDg==} engines: {node: '>=16'} @@ -12479,33 +12429,18 @@ snapshots: optionalDependencies: workerd: 1.20260504.1 - '@cloudflare/workerd-darwin-64@1.20260317.1': - optional: true - '@cloudflare/workerd-darwin-64@1.20260504.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260317.1': - optional: true - '@cloudflare/workerd-darwin-arm64@1.20260504.1': optional: true - '@cloudflare/workerd-linux-64@1.20260317.1': - optional: true - '@cloudflare/workerd-linux-64@1.20260504.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260317.1': - optional: true - '@cloudflare/workerd-linux-arm64@1.20260504.1': optional: true - '@cloudflare/workerd-windows-64@1.20260317.1': - optional: true - '@cloudflare/workerd-windows-64@1.20260504.1': optional: true @@ -20642,18 +20577,6 @@ snapshots: mimic-fn@4.0.0: {} - miniflare@4.20260317.0: - dependencies: - '@cspotcode/source-map-support': 0.8.1 - sharp: 0.34.5 - undici: 7.24.4 - workerd: 1.20260317.1 - ws: 8.18.0 - youch: 4.1.0-beta.10 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - miniflare@4.20260504.0: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -23026,8 +22949,6 @@ snapshots: undici@7.21.0: {} - undici@7.24.4: {} - undici@7.24.8: {} unenv@1.10.0: @@ -23810,14 +23731,6 @@ snapshots: word-wrap@1.2.5: {} - workerd@1.20260317.1: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260317.1 - '@cloudflare/workerd-darwin-arm64': 1.20260317.1 - '@cloudflare/workerd-linux-64': 1.20260317.1 - '@cloudflare/workerd-linux-arm64': 1.20260317.1 - '@cloudflare/workerd-windows-64': 1.20260317.1 - workerd@1.20260504.1: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20260504.1