From 615bc2353616bf7ee44397eb68a2496d46de9fc1 Mon Sep 17 00:00:00 2001 From: Alan Pogrebinschi Date: Tue, 21 Jul 2026 15:08:11 +0000 Subject: [PATCH] feat(host-cloudflare): use dynamic-worker runtime when LOADER binding present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap the Cloudflare host's code substrate from QuickJS-wasm to @executor-js/runtime-dynamic-worker (the cloud substrate) whenever the Worker declares a worker_loaders LOADER binding; fall back to QuickJS when the binding is absent, so existing self-host deployments are unaffected. Verified on a live deployment: sandboxed code runs in real workerd isolates (navigator.userAgent === "Cloudflare-Workers") with platform-enforced zero egress — fetch() throws and the only exit is the RPC tool bridge through the host. --- apps/host-cloudflare/package.json | 1 + apps/host-cloudflare/src/execution.ts | 15 ++++++++++----- apps/host-cloudflare/wrangler.jsonc | 9 +++++++++ bun.lock | 1 + 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/apps/host-cloudflare/package.json b/apps/host-cloudflare/package.json index d7159620d..695e2eca7 100644 --- a/apps/host-cloudflare/package.json +++ b/apps/host-cloudflare/package.json @@ -30,6 +30,7 @@ "@executor-js/plugin-provider-service-split": "workspace:*", "@executor-js/plugin-toolkits": "workspace:*", "@executor-js/react": "workspace:*", + "@executor-js/runtime-dynamic-worker": "workspace:*", "@executor-js/runtime-quickjs": "workspace:*", "@executor-js/sdk": "workspace:*", "@jitl/quickjs-wasmfile-release-sync": "catalog:", diff --git a/apps/host-cloudflare/src/execution.ts b/apps/host-cloudflare/src/execution.ts index a3b91e651..1ee0e4b1e 100644 --- a/apps/host-cloudflare/src/execution.ts +++ b/apps/host-cloudflare/src/execution.ts @@ -10,7 +10,9 @@ import { PluginsProvider, type ExecutorDbHandle, } from "@executor-js/api/server"; +import { makeDynamicWorkerExecutor } from "@executor-js/runtime-dynamic-worker"; import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs"; +import { env } from "cloudflare:workers"; import type { CloudflareConfig } from "./config"; import { makeCloudflarePlugins } from "./plugins"; @@ -20,10 +22,10 @@ import { makeCloudflarePlugins } from "./plugins"; // substrate, no-op engine decorator), with the plugins + host config built from // the per-request `env`-derived config rather than process.env. // -// QuickJS-wasm is the default code substrate because it runs in a single Worker -// with no extra binding. When Cloudflare's dynamic Worker Loader leaves closed -// beta, swap CodeExecutorProvider for the dynamic-worker executor (cloud's) — -// it's a one-Layer change behind this same seam. +// Code substrate: when the Worker declares a `worker_loaders` LOADER binding, +// use the dynamic-worker executor (cloud's substrate) — real isolates with +// `globalOutbound: null`. Without the binding, fall back to QuickJS-wasm, +// which runs in a single Worker with no extra binding. // --------------------------------------------------------------------------- export { makeExecutionStack } from "@executor-js/api/server"; @@ -31,7 +33,10 @@ export { EngineDecoratorNoop }; export const CloudflareCodeExecutorProvider: Layer.Layer = Layer.sync( CodeExecutorProvider, - () => makeQuickJsExecutor(), + () => { + const loader = (env as { LOADER?: WorkerLoader }).LOADER; + return loader ? makeDynamicWorkerExecutor({ loader }) : makeQuickJsExecutor(); + }, ); export const makeCloudflarePluginsProvider = ( diff --git a/apps/host-cloudflare/wrangler.jsonc b/apps/host-cloudflare/wrangler.jsonc index 6b3a12b7f..c3b010669 100644 --- a/apps/host-cloudflare/wrangler.jsonc +++ b/apps/host-cloudflare/wrangler.jsonc @@ -34,6 +34,15 @@ "bucket_name": "executor-blobs", }, ], + // Dynamic Worker Loader binding: when present, sandboxed code runs in real + // workerd isolates with `globalOutbound: null` (platform-enforced zero egress) + // via @executor-js/runtime-dynamic-worker; without it the host falls back to + // the QuickJS-wasm substrate. Remove this block to stay on QuickJS. + "worker_loaders": [ + { + "binding": "LOADER", + }, + ], // The MCP session Durable Object: one addressable isolate per MCP session (the // DO id IS the session id) so a session survives across the Worker's stateless // isolates — without it, `tools/list` after `initialize` can land on a fresh diff --git a/bun.lock b/bun.lock index bc5ca40ea..9bbc6b68b 100644 --- a/bun.lock +++ b/bun.lock @@ -182,6 +182,7 @@ "@executor-js/plugin-provider-service-split": "workspace:*", "@executor-js/plugin-toolkits": "workspace:*", "@executor-js/react": "workspace:*", + "@executor-js/runtime-dynamic-worker": "workspace:*", "@executor-js/runtime-quickjs": "workspace:*", "@executor-js/sdk": "workspace:*", "@jitl/quickjs-wasmfile-release-sync": "catalog:",