// ---------------------------------------------------------------------------// Shared execution stack — turn a (user, org) into a runnable executor + engine.//// Cloud and self-host both had an identical `makeExecutionStack`:// createScopedExecutor -> createExecutionEngine({ executor, codeExecutor }) ->// { executor, engine }// differing only in (a) the code substrate (cloud's Cloudflare dynamic-worker vs// self-host's in-process QuickJS) and (b) cloud's usage-metering decorator// (an app-only billing overlay), absent on self-host.//// This factory owns the common body. The two differences are injected:// - `CodeExecutorProvider` — the `codeExecutor` value. Cloud's Layer wraps// `makeDynamicWorkerExecutor({ loader: env.LOADER })`; self-host's wraps// `makeQuickJsExecutor()`.// - `EngineDecorator` — `decorate(engine) => engine`. Cloud's app layer applies// a usage-metering overlay; the default Layer is a no-op (self-host, local,// tests, and cloud's non-metering MCP session path).//// The per-(user, org) executor itself comes from `makeScopedExecutor` (sdk),// which reads the DB handle / plugins / host config from its own seams. This// lives in `@executor-js/api` because it is the only package that depends on// both `@executor-js/sdk` (for `makeScopedExecutor`) and `@executor-js/execution`// (for `createExecutionEngine`).// --------------------------------------------------------------------------- import { Context, Effect, Layer } from "effect";import type * as Cause from "effect/Cause"; import type { McpResource } from "@executor-js/host-mcp";import type { AnyPlugin, Executor, StorageFailure } from "@executor-js/sdk";import { createExecutionEngine, type ExecutionEngine, type ExecutionEngineConfig,} from "@executor-js/execution"; import { DbProvider } from "./executor-fuma-db";import { HostConfig, PluginsProvider, makePlatformExecutor, makeScopedExecutor,} from "./scoped-executor"; // ---------------------------------------------------------------------------// CodeExecutorProvider seam — the host's code-execution substrate. Typed to the// widened `Cause.YieldableError` channel (matching `ExecutionEngineService`) so// a runtime-specific tagged error (DynamicWorkerExecutionError, QuickJS errors)// assigns structurally.// --------------------------------------------------------------------------- export type CodeExecutor = ExecutionEngineConfig<Cause.YieldableError>["codeExecutor"]; export class CodeExecutorProvider extends Context.Service<CodeExecutorProvider, CodeExecutor>()( "@executor-js/api/CodeExecutorProvider",) {} // ---------------------------------------------------------------------------// EngineDecorator seam — wrap the freshly built engine (e.g. with usage// metering). `decorate` receives the same `(accountId, organizationId,// organizationName)` identity the stack was built for, so a host can bind the// decorator to the org (cloud's per-org usage metering needs the org id). The// default Layer is a no-op so hosts that do not decorate (self-host, local,// tests) get an identity transform for free.// --------------------------------------------------------------------------- export interface EngineStackIdentity { readonly accountId: string; readonly organizationId: string; readonly organizationName: string;} export interface EngineDecoratorShape { readonly decorate: <E extends Cause.YieldableError>( engine: ExecutionEngine<E>, identity: EngineStackIdentity, context: EngineStackContext, ) => ExecutionEngine<E>;} /** * What the stack was built to serve, beyond the identity: the MCP resource * when the caller is an MCP session (absent on the HTTP plane). Lets a * decorator distinguish a toolkit-scoped engine without the host threading a * side channel. */export interface EngineStackContext { readonly mcpResource?: McpResource;} export class EngineDecorator extends Context.Service<EngineDecorator, EngineDecoratorShape>()( "@executor-js/api/EngineDecorator",) {} /** No-op decorator: the engine passes through unchanged. */export const EngineDecoratorNoop: Layer.Layer<EngineDecorator> = Layer.succeed(EngineDecorator)({ decorate: (engine) => engine,}); // ---------------------------------------------------------------------------// makeExecutionStack — shared (user, org) -> { executor, engine }.//// Reads `makeScopedExecutor` (sdk), the code substrate from// `CodeExecutorProvider`, and the engine wrap from `EngineDecorator`. The// returned engine error channel is widened to `Cause.YieldableError`, matching// `ExecutionEngineService` and the runtime-specific code executors.// --------------------------------------------------------------------------- export const makeExecutionStack = < const TPlugins extends readonly AnyPlugin[] = readonly AnyPlugin[],>( accountId: string, organizationId: string, organizationName: string, options?: { readonly mcpResource?: McpResource },): Effect.Effect< { readonly executor: Executor<TPlugins>; readonly engine: ExecutionEngine<Cause.YieldableError> }, StorageFailure, DbProvider | PluginsProvider | HostConfig | CodeExecutorProvider | EngineDecorator> => Effect.gen(function* () { const executor = yield* makeScopedExecutor<TPlugins>( accountId, organizationId, organizationName, { plugins: { mcpResource: options?.mcpResource } }, ); const codeExecutor = yield* CodeExecutorProvider.asEffect(); const { decorate } = yield* EngineDecorator.asEffect(); const engine = yield* Effect.sync(() => decorate( createExecutionEngine({ executor, codeExecutor }), { accountId, organizationId, organizationName, }, { mcpResource: options?.mcpResource }, ), ); return { executor, engine }; }).pipe(Effect.withSpan("executor.stack.build")); // ---------------------------------------------------------------------------// makePlatformExecutionStack — the org-credential sibling: a subject-less,// write-refusing executor (`makePlatformExecutor`) and no REAL engine. An org// key is an observer; the execution engine exists to run code as an acting// member, so no engine is built here. The middleware still provides// `ExecutionEngineService` (handlers' service requirements demand one) — as a// stub whose reachable reads answer "nothing here" and whose execute/resume// members sit behind the middleware's safe-request gate (see// `readOnlyExecutionEngine` in ./execution-stack-middleware.ts).// --------------------------------------------------------------------------- export const makePlatformExecutionStack = < const TPlugins extends readonly AnyPlugin[] = readonly AnyPlugin[],>( organizationId: string,): Effect.Effect< { readonly executor: Executor<TPlugins> }, StorageFailure, DbProvider | PluginsProvider | HostConfig> => makePlatformExecutor(organizationId).pipe( // The platform executor is built against the erased plugin set; re-narrow // via the same phantom cast `makeScopedExecutor` performs for the scoped one. Effect.map((executor) => ({ executor: executor as Executor<TPlugins> })), Effect.withSpan("executor.stack.platform.build"), );
// ---------------------------------------------------------------------------
// Shared execution stack — turn a (user, org) into a runnable executor + engine.
//
// Cloud and self-host both had an identical `makeExecutionStack`:
// createScopedExecutor -> createExecutionEngine({ executor, codeExecutor }) ->
// { executor, engine }
// differing only in (a) the code substrate (cloud's Cloudflare dynamic-worker vs
// self-host's in-process QuickJS) and (b) cloud's usage-metering decorator
// (an app-only billing overlay), absent on self-host.
//
// This factory owns the common body. The two differences are injected:
// - `CodeExecutorProvider` — the `codeExecutor` value. Cloud's Layer wraps
// `makeDynamicWorkerExecutor({ loader: env.LOADER })`; self-host's wraps
// `makeQuickJsExecutor()`.
// - `EngineDecorator` — `decorate(engine) => engine`. Cloud's app layer applies
// a usage-metering overlay; the default Layer is a no-op (self-host, local,
// tests, and cloud's non-metering MCP session path).
//
// The per-(user, org) executor itself comes from `makeScopedExecutor` (sdk),
// which reads the DB handle / plugins / host config from its own seams. This
// lives in `@executor-js/api` because it is the only package that depends on
// both `@executor-js/sdk` (for `makeScopedExecutor`) and `@executor-js/execution`
// (for `createExecutionEngine`).
// ---------------------------------------------------------------------------
import { Context, Effect, Layer } from "effect";
import type * as Cause from "effect/Cause";
import type { McpResource } from "@executor-js/host-mcp";
import type { AnyPlugin, Executor, StorageFailure } from "@executor-js/sdk";
import {
createExecutionEngine,
type ExecutionEngine,
type ExecutionEngineConfig,
} from "@executor-js/execution";
import { DbProvider } from "./executor-fuma-db";
import {
HostConfig,
PluginsProvider,
makePlatformExecutor,
makeScopedExecutor,
} from "./scoped-executor";
// ---------------------------------------------------------------------------
// CodeExecutorProvider seam — the host's code-execution substrate. Typed to the
// widened `Cause.YieldableError` channel (matching `ExecutionEngineService`) so
// a runtime-specific tagged error (DynamicWorkerExecutionError, QuickJS errors)
// assigns structurally.
// ---------------------------------------------------------------------------
export type CodeExecutor = ExecutionEngineConfig<Cause.YieldableError>["codeExecutor"];
export class CodeExecutorProvider extends Context.Service<CodeExecutorProvider, CodeExecutor>()(
"@executor-js/api/CodeExecutorProvider",
) {}
// ---------------------------------------------------------------------------
// EngineDecorator seam — wrap the freshly built engine (e.g. with usage
// metering). `decorate` receives the same `(accountId, organizationId,
// organizationName)` identity the stack was built for, so a host can bind the
// decorator to the org (cloud's per-org usage metering needs the org id). The
// default Layer is a no-op so hosts that do not decorate (self-host, local,
// tests) get an identity transform for free.
// ---------------------------------------------------------------------------
export interface EngineStackIdentity {
readonly accountId: string;
readonly organizationId: string;
readonly organizationName: string;
}
export interface EngineDecoratorShape {
readonly decorate: <E extends Cause.YieldableError>(
engine: ExecutionEngine<E>,
identity: EngineStackIdentity,
context: EngineStackContext,
) => ExecutionEngine<E>;
}
/**
* What the stack was built to serve, beyond the identity: the MCP resource
* when the caller is an MCP session (absent on the HTTP plane). Lets a
* decorator distinguish a toolkit-scoped engine without the host threading a
* side channel.
*/
export interface EngineStackContext {
readonly mcpResource?: McpResource;
}
export class EngineDecorator extends Context.Service<EngineDecorator, EngineDecoratorShape>()(
"@executor-js/api/EngineDecorator",
) {}
/** No-op decorator: the engine passes through unchanged. */
export const EngineDecoratorNoop: Layer.Layer<EngineDecorator> = Layer.succeed(EngineDecorator)({
decorate: (engine) => engine,
});
// ---------------------------------------------------------------------------
// makeExecutionStack — shared (user, org) -> { executor, engine }.
//
// Reads `makeScopedExecutor` (sdk), the code substrate from
// `CodeExecutorProvider`, and the engine wrap from `EngineDecorator`. The
// returned engine error channel is widened to `Cause.YieldableError`, matching
// `ExecutionEngineService` and the runtime-specific code executors.
// ---------------------------------------------------------------------------
export const makeExecutionStack = <
const TPlugins extends readonly AnyPlugin[] = readonly AnyPlugin[],
>(
accountId: string,
organizationId: string,
organizationName: string,
options?: { readonly mcpResource?: McpResource },
): Effect.Effect<
{ readonly executor: Executor<TPlugins>; readonly engine: ExecutionEngine<Cause.YieldableError> },
StorageFailure,
DbProvider | PluginsProvider | HostConfig | CodeExecutorProvider | EngineDecorator
> =>
Effect.gen(function* () {
const executor = yield* makeScopedExecutor<TPlugins>(
accountId,
organizationId,
organizationName,
{ plugins: { mcpResource: options?.mcpResource } },
);
const codeExecutor = yield* CodeExecutorProvider.asEffect();
const { decorate } = yield* EngineDecorator.asEffect();
const engine = yield* Effect.sync(() =>
decorate(
createExecutionEngine({ executor, codeExecutor }),
{
accountId,
organizationId,
organizationName,
},
{ mcpResource: options?.mcpResource },
),
);
return { executor, engine };
}).pipe(Effect.withSpan("executor.stack.build"));
// ---------------------------------------------------------------------------
// makePlatformExecutionStack — the org-credential sibling: a subject-less,
// write-refusing executor (`makePlatformExecutor`) and no REAL engine. An org
// key is an observer; the execution engine exists to run code as an acting
// member, so no engine is built here. The middleware still provides
// `ExecutionEngineService` (handlers' service requirements demand one) — as a
// stub whose reachable reads answer "nothing here" and whose execute/resume
// members sit behind the middleware's safe-request gate (see
// `readOnlyExecutionEngine` in ./execution-stack-middleware.ts).
// ---------------------------------------------------------------------------
export const makePlatformExecutionStack = <
const TPlugins extends readonly AnyPlugin[] = readonly AnyPlugin[],
>(
organizationId: string,
): Effect.Effect<
{ readonly executor: Executor<TPlugins> },
StorageFailure,
DbProvider | PluginsProvider | HostConfig
> =>
makePlatformExecutor(organizationId).pipe(
// The platform executor is built against the erased plugin set; re-narrow
// via the same phantom cast `makeScopedExecutor` performs for the scoped one.
Effect.map((executor) => ({ executor: executor as Executor<TPlugins> })),
Effect.withSpan("executor.stack.platform.build"),
);