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 };};
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 };
};