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<CodeExecutorProvider> = Layer.sync( CodeExecutorProvider, () => { const loader = (env as { LOADER?: WorkerLoader }).LOADER; return loader ? makeDynamicWorkerExecutor({ loader }) : makeQuickJsExecutor(); },);export const makeCloudflarePluginsProvider = ( config: CloudflareConfig,): Layer.Layer<PluginsProvider> => 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<HostConfig> => 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, );
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<CodeExecutorProvider> = Layer.sync(
CodeExecutorProvider,
() => {
const loader = (env as { LOADER?: WorkerLoader }).LOADER;
return loader ? makeDynamicWorkerExecutor({ loader }) : makeQuickJsExecutor();
},
);
export const makeCloudflarePluginsProvider = (
config: CloudflareConfig,
): Layer.Layer<PluginsProvider> =>
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<HostConfig> =>
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,
);