// --------------------------------------------------------------------------- // 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, ExecutorConfig, 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["codeExecutor"]; export class CodeExecutorProvider extends Context.Service()( "@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: ( engine: ExecutionEngine, identity: EngineStackIdentity, context: EngineStackContext, ) => ExecutionEngine; } /** * 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()( "@executor-js/api/EngineDecorator", ) {} /** No-op decorator: the engine passes through unchanged. */ export const EngineDecoratorNoop: Layer.Layer = 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; /** Workspace-settings permission for this binding (see * `ExecutorConfig.orgWrites`), derived from the acting member's role. */ readonly orgWrites?: ExecutorConfig["orgWrites"]; }, ): Effect.Effect< { readonly executor: Executor; readonly engine: ExecutionEngine }, StorageFailure, DbProvider | PluginsProvider | HostConfig | CodeExecutorProvider | EngineDecorator > => Effect.gen(function* () { const executor = yield* makeScopedExecutor( accountId, organizationId, organizationName, { plugins: { mcpResource: options?.mcpResource }, ...(options?.orgWrites === undefined ? {} : { orgWrites: options.orgWrites }), }, ).pipe(Effect.withSpan("executor.stack.scoped_executor")); const codeExecutor = yield* CodeExecutorProvider.asEffect().pipe( Effect.withSpan("executor.stack.code_executor"), ); const { decorate } = yield* EngineDecorator.asEffect().pipe( Effect.withSpan("executor.stack.decorator"), ); 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 }, 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 })), Effect.withSpan("executor.stack.platform.build"), );