import { Effect, Layer } from "effect"; import { CodeExecutorProvider, DbProvider, dbProviderLayer, EngineDecorator, EngineDecoratorNoop, HostConfig, 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, waitUntil } from "cloudflare:workers"; import type { CloudflareConfig } from "./config"; import { makeCloudflarePlugins } from "./plugins"; // --------------------------------------------------------------------------- // Cloudflare execution-stack seams — the same shape as self-host (QuickJS code // substrate, no-op engine decorator), with the plugins + host config built from // the per-request `env`-derived config rather than process.env. // // 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"; export { EngineDecoratorNoop }; export const CloudflareCodeExecutorProvider: Layer.Layer = Layer.sync( CodeExecutorProvider, () => { const loader = (env as { LOADER?: WorkerLoader }).LOADER; return loader ? makeDynamicWorkerExecutor({ loader }) : makeQuickJsExecutor(); }, ); export const makeCloudflarePluginsProvider = ( config: CloudflareConfig, ): Layer.Layer => Layer.succeed(PluginsProvider)({ plugins: (context) => makeCloudflarePlugins(config.secretKey, { activeToolkitSlug: context?.mcpResource?.kind === "toolkit" ? context.mcpResource.slug : undefined, allowLocalNetwork: config.allowLocalNetwork, }), }); export const makeCloudflareHostConfig = (config: CloudflareConfig): Layer.Layer => Layer.succeed(HostConfig)({ allowLocalNetwork: config.allowLocalNetwork, webBaseUrl: config.webBaseUrl, oauthCallbackPath: "/api/oauth/callback", // A Worker, so a tools read's speculative catalog refresh needs `waitUntil` // to survive the response at all. Unlike cloud's Hyperdrive plane there is // nothing to drain it before: the storage here is a D1 binding read // straight off `env`, with no pool and no close finalizer, so a detached // batch still has everything it needs after the response is written. // // BARE `runPromise`, unlike cloud's DO, which re-provides its tracer here. // This app has no OTel and no Sentry anywhere — `observability.ts` is a // console error capture and the request path itself runs on the runtime's // default no-op tracer — so there is nothing to provide, and the deferred // batch's spans are dropped exactly like the read's own. Give it the tracer // the way the DO does the day host-cf gains one. deferToolSync: (task) => Effect.sync(() => waitUntil(Effect.runPromise(task))), }); /** * The five execution-stack seams the shared `makeExecutionStack` reads from, * bundled into one Layer over the long-lived D1 handle. Mirrors self-host's * `SelfHostExecutionStackLayer`. The HTTP path wires these seams individually * through `ExecutorApp.make`; the MCP session store provides this whole Layer to * build a per-session engine off the envelope's request pipeline. */ export const makeCloudflareExecutionStackLayer = ( config: CloudflareConfig, dbHandle: ExecutorDbHandle, ): Layer.Layer< DbProvider | PluginsProvider | HostConfig | CodeExecutorProvider | EngineDecorator > => Layer.mergeAll( dbProviderLayer(Effect.succeed(dbHandle)), makeCloudflarePluginsProvider(config), makeCloudflareHostConfig(config), CloudflareCodeExecutorProvider, EngineDecoratorNoop, );