144 lines · 6.9 KB · TypeScript
RawDownload
import { HttpApiSwagger } from "effect/unstable/httpapi";import { Layer } from "effect"; import {  ArtifactUsageObserver,  composePluginApi,  ExecutorApp,  FixedExecutionProvider,  textFailureStrategy,} from "@executor-js/api/server";import { withExecutionAnalytics } from "@executor-js/analytics";import { createExecutionEngine } from "@executor-js/execution";import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs"; import { localAnalytics } from "./analytics";import { getExecutorBundle, type LocalExecutor } from "./executor";import { makeLocalIdentityLayer } from "./identity";import { ErrorCaptureLive } from "./observability"; // ===========================================================================// The LOCAL Executor app, as ONE `ExecutorApp.make` call.//// The whole scenario in 60 seconds: single-user identity (always the one local// Principal) over a SINGLE boot-built executor scoped to the working directory// (`<basename>-<hash>`, with `oauthEndpointUrlPolicy: { allowHttp: true }`),// QuickJS in-process code execution, console error capture, Swagger at /docs —// and NO account API, NO usage metering. `diff` against// `apps/host-selfhost/src/app.ts` is the whole product difference: local serves// its ONE cwd executor directly (the `fixedExecution` seam) instead of building// a per-request `[user-org:…, org]` scoped executor from identity.//// `ExecutorApp.make` owns the assembly (the fixed-execution middleware wrapping// the protected API, the extension routes, provideMerge(boot)). This file's job// is the eager async boot — building the ONE executor + engine — and slotting// local's seam Layers into the named slots.//// What legitimately stays LOCAL-PLATFORM (the thin `serve.ts` Bun shell, NOT// make()'s job): the socket binding + idleTimeout, static SPA serving (embedded// /disk/dev-vite), the one-time legacy SQLite import (run inside the boot bundle,// BEFORE the executor reaches this seam), the single-credential network gate, the// /mcp + /api/mcp-sessions resume + /api/oauth/await routes (an in-process,// single-engine MCP handler with a browser-approval store — local's own surface,// not the shared multi-user McpServingRoutes envelope), and the `/api`-prefix// stripping. So `mcp` and `account` are OMITTED, and `mountPrefix` is left at// root (the Bun shell strips `/api` before the handler).// =========================================================================== /** * The fixed-execution seam: the ONE boot executor + engine + plugin extension * map, projected under `FixedExecutionProvider`. The executor already holds its * cwd scope, libSQL db handle, plugins, and `allowHttp` policy (built in * `executor.ts`), so local supplies no `DbProvider`/`PluginsProvider`/ * `HostConfig`/`CodeExecutorProvider` seams — the fixed executor is the whole * execution model. */const localFixedExecutionLayer = (executor: LocalExecutor): Layer.Layer<FixedExecutionProvider> =>  Layer.succeed(FixedExecutionProvider)({    executor,    // This engine serves the HTTP executions API (`executor call`/`resume`,    // the web console) — the wrap binds the "api" plane structurally.    engine: withExecutionAnalytics(      createExecutionEngine({        executor,        codeExecutor: makeQuickJsExecutor(),      }),      localAnalytics,      { plane: "api", toolkit: false },    ),    // The executor IS its own plugin-extension map (`executor[pluginId]`); the    // fixed middleware reads `executor[id]` to satisfy each plugin's    // `*ExtensionService` Tag per request — identical binding to the prior    // `composePluginHandlers(plugins, executor)` boot-bind.    extensions: executor,  }); export interface LocalApiHandler {  /** The unified web handler: serves the typed API (at root — the Bun shell strips `/api`) + /docs. */  readonly handler: (request: Request) => Promise<Response>;  readonly dispose: () => Promise<void>;} /** * Build the local app's API web-handler. Awaits the shared boot bundle (the one * cwd-scoped executor, after the legacy SQLite import), then composes * `ExecutorApp.make` over local's seams and binds it to a `fetch`-style handler. * * Mirrors self-host's `makeSelfHostApiHandler`: production-unconditional wiring * (no test-only branches), with `serve.ts`'s `handlers` injection hook as the * test seam where a test wants to bypass the boot graph. */export const makeLocalApiHandler = async (token: string): Promise<LocalApiHandler> => {  const { executor, plugins } = await getExecutorBundle();   // Build the fixed-execution seam ONCE (one executor + one engine). The same  // Layer is the `fixedExecution` seam declaration AND lives in `boot` so the  // fixed middleware's residual `FixedExecutionProvider` resolves there — exactly  // as self-host declares `db: SelfHostDbProvider` and puts the handle in `boot`.  const fixedExecution = localFixedExecutionLayer(executor);   // The authoritative identity gate for the typed `/api`: validates the boot  // bearer token and resolves the one local Principal. The Bun shell  // (`serve.ts`) fast-path-rejects unauthenticated requests with the same token.  const identity = makeLocalIdentityLayer(token);   const { toWebHandler } = ExecutorApp.make({    plugins,    providers: {      // Single-user: validates the boot bearer token and resolves the one local      // Principal. Boot-scoped (`RIdentity = never`), captured once.      identity,      // The ONE boot executor + engine, served directly — local's fixed      // execution model (no per-request scoped-executor rebuild).      fixedExecution,      // account omitted (local has no account API).      // mcp omitted (local's /mcp is its own in-process surface in serve.ts).      errorCapture: ErrorCaptureLive,    },    extensions: {      // Swagger UI at /docs, over the root-mounted spec (matches the served      // paths — local serves the API at root; the Bun shell strips `/api`).      routes: [HttpApiSwagger.layer(composePluginApi(plugins), { path: "/docs" })],    },    // No mountPrefix: local serves the typed API at root and the Bun shell    // strips the `/api` prefix before dispatching here. Local renders identity    // failures as text (matching self-host); the single-user provider never    // produces one in practice.    config: { failure: textFailureStrategy },    // The boot-scoped context provideMerge'd under everything: the identity    // provider (captured once by the fixed-execution middleware) + the fixed    // execution seam (the one executor + engine + extension map) + the    // artifact-usage observer (this HTTP plane is the console UI's data layer,    // so operations it serves file as `via: "ui"`).    boot: Layer.mergeAll(      identity,      fixedExecution,      Layer.succeed(ArtifactUsageObserver)((action) =>        localAnalytics.record(`artifact_${action}`, { via: "ui" }),      ),    ),  });   const web = toWebHandler();  return { handler: web.handler, dispose: web.dispose };};