181 lines · 7.7 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// 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<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;    /** Workspace-settings permission for this binding (see     *  `ExecutorConfig.orgWrites`), derived from the acting member's role. */    readonly orgWrites?: ExecutorConfig<TPlugins>["orgWrites"];  },): 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 },        ...(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<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"),  );