import { Effect, FiberRef } from "effect";import { StorageError, typedAdapter, type DBAdapter, type DBSchema, type DBTransactionAdapter, type StorageFailure, type TypedAdapter,} from "@executor/storage-core"; import { scopeBlobStore, type BlobStore,} from "./blob";import { coreSchema, type CoreSchema, type DefinitionsInput, type SourceInput, type SourceRow, type ToolAnnotations, type ToolRow,} from "./core-schema";import { ElicitationDeclinedError, ElicitationResponse, FormElicitation, type ElicitationHandler, type ElicitationRequest,} from "./elicitation";import { NoHandlerError, PluginNotLoadedError, SourceRemovalNotAllowedError, ToolInvocationError, ToolNotFoundError,} from "./errors";import { SecretId, ToolId } from "./ids";import type { AnyPlugin, Elicit, PluginCtx, PluginExtensions, StaticSourceDecl, StaticToolDecl, StorageDeps,} from "./plugin";import type { Scope } from "./scope";import { SecretRef, SetSecretInput, type SecretProvider,} from "./secrets";import { ToolSchema, type Source, type SourceDetectionResult, type Tool, type ToolListFilter,} from "./types";import { buildToolTypeScriptPreview } from "./schema-types";import { scopeAdapter } from "./scoped-adapter"; // ---------------------------------------------------------------------------// InvokeOptions — passed to `executor.tools.invoke(id, args, options)`.// The `onElicitation` handler is threaded into the `elicit` function// exposed on plugin ctx / InvokeToolInput. Tools that never elicit// simply don't call it.//// The "accept-all" sentinel is convenient for tests and CLI automation —// every elicitation request gets auto-accepted with an empty content// payload. For real interactive hosts, pass a real handler.// --------------------------------------------------------------------------- export interface InvokeOptions { readonly onElicitation?: ElicitationHandler | "accept-all";} const acceptAllHandler: ElicitationHandler = () => Effect.succeed(new ElicitationResponse({ action: "accept" })); const resolveElicitationHandler = ( options: InvokeOptions | undefined,): ElicitationHandler => { const handler = options?.onElicitation; if (!handler || handler === "accept-all") return acceptAllHandler; return handler;}; // ---------------------------------------------------------------------------// Executor — public surface. Every list/invoke/schema call is a direct// core-table query (for dynamic rows) unioned with the in-memory static// pool. No ToolRegistry, no SourceRegistry, no SecretStore services.// --------------------------------------------------------------------------- export type Executor<TPlugins extends readonly AnyPlugin[] = []> = { readonly scope: Scope; readonly tools: { readonly list: ( filter?: ToolListFilter, ) => Effect.Effect<readonly Tool[], StorageFailure>; /** Fetch a tool's full schema view: JSON schemas with `$defs` * attached from the core `definition` table, plus TypeScript * preview strings rendered from them. Returns `null` for unknown * tool ids. */ readonly schema: ( toolId: string, ) => Effect.Effect<ToolSchema | null, StorageFailure>; /** Every `$defs` entry across every source, grouped by source id. * Used for bulk schema export and downstream TypeScript rendering. */ readonly definitions: () => Effect.Effect< Record<string, Record<string, unknown>>, StorageFailure >; readonly invoke: ( toolId: string, args: unknown, options?: InvokeOptions, ) => Effect.Effect< unknown, | ToolNotFoundError | PluginNotLoadedError | NoHandlerError | ToolInvocationError | ElicitationDeclinedError | StorageFailure >; }; readonly sources: { readonly list: () => Effect.Effect<readonly Source[], StorageFailure>; readonly remove: ( sourceId: string, ) => Effect.Effect<void, SourceRemovalNotAllowedError | StorageFailure>; readonly refresh: (sourceId: string) => Effect.Effect<void, StorageFailure>; /** URL autodetection — fans out to every plugin's `detect` hook * (if declared), returns every high/medium/low-confidence match. * UI picks a winner from the list. */ readonly detect: ( url: string, ) => Effect.Effect<readonly SourceDetectionResult[], StorageFailure>; /** All `$defs` registered for a single source, keyed by def name. */ readonly definitions: ( sourceId: string, ) => Effect.Effect<Record<string, unknown>, StorageFailure>; }; readonly secrets: { readonly get: ( id: string, ) => Effect.Effect<string | null, StorageFailure>; /** Fast-path existence check — hits the core `secret` routing table * only, never calls the provider. Use this for UI state ("secret * missing, prompt to add") to avoid keychain permission prompts * or 1password IPC roundtrips on a pre-flight check. */ readonly status: ( id: string, ) => Effect.Effect<"resolved" | "missing", StorageFailure>; readonly set: ( input: SetSecretInput, ) => Effect.Effect<SecretRef, StorageFailure>; readonly remove: (id: string) => Effect.Effect<void, StorageFailure>; readonly list: () => Effect.Effect<readonly SecretRef[], StorageFailure>; readonly providers: () => Effect.Effect<readonly string[]>; }; readonly close: () => Effect.Effect<void, StorageFailure>;} & PluginExtensions<TPlugins>; export interface ExecutorConfig< TPlugins extends readonly AnyPlugin[] = [],> { readonly scope: Scope; readonly adapter: DBAdapter; readonly blobs: BlobStore; readonly plugins?: TPlugins;} // ---------------------------------------------------------------------------// collectSchemas — merge coreSchema with every plugin's declared schema.// Hosts call this and pass the result to the migration runner (or to// the adapter factory for backends that auto-migrate from a schema// manifest) before constructing the executor.// --------------------------------------------------------------------------- export const collectSchemas = ( plugins: readonly AnyPlugin[],): DBSchema => { const merged: Record<string, DBSchema[string]> = { ...coreSchema }; for (const plugin of plugins) { if (!plugin.schema) continue; for (const [modelKey, model] of Object.entries(plugin.schema)) { if (merged[modelKey]) { throw new Error( `Duplicate model "${modelKey}" contributed by plugin "${plugin.id}"` + ` (reserved by core or another plugin)`, ); } merged[modelKey] = model as DBSchema[string]; } } return merged;}; // ---------------------------------------------------------------------------// Row → public projection conversions// --------------------------------------------------------------------------- const rowToSource = (row: SourceRow): Source => ({ id: row.id, kind: row.kind, name: row.name, url: row.url ?? undefined, pluginId: row.plugin_id, canRemove: Boolean(row.can_remove), canRefresh: Boolean(row.can_refresh), canEdit: Boolean(row.can_edit), runtime: false,}); const staticDeclToSource = ( decl: StaticSourceDecl, pluginId: string,): Source => ({ id: decl.id, kind: decl.kind, name: decl.name, url: decl.url, pluginId, canRemove: decl.canRemove ?? false, canRefresh: decl.canRefresh ?? false, canEdit: decl.canEdit ?? false, runtime: true,}); const decodeJsonColumn = (value: unknown): unknown => { if (value === null || value === undefined) return undefined; if (typeof value !== "string") return value; try { return JSON.parse(value); } catch { return value; }}; const rowToTool = ( row: ToolRow, annotations?: ToolAnnotations,): Tool => ({ id: row.id, sourceId: row.source_id, pluginId: row.plugin_id, name: row.name, description: row.description, inputSchema: decodeJsonColumn(row.input_schema), outputSchema: decodeJsonColumn(row.output_schema), annotations,}); const staticDeclToTool = ( source: StaticSourceDecl, tool: StaticToolDecl, pluginId: string,): Tool => ({ id: `${source.id}.${tool.name}`, sourceId: source.id, pluginId, name: tool.name, description: tool.description, inputSchema: tool.inputSchema, outputSchema: tool.outputSchema, annotations: tool.annotations,}); // ---------------------------------------------------------------------------// Dynamic-row writers — used by ctx.core.sources.register. Static sources// never touch these functions.// --------------------------------------------------------------------------- // Upsert shape: delete any existing source + tools + definitions for// `input.id` before creating fresh rows. Keeps replayable — boot-time// sync from executor.jsonc can call register() on rows that already// exist without tripping a UNIQUE constraint.const writeSourceInput = ( core: TypedAdapter<CoreSchema>, pluginId: string, input: SourceInput,): Effect.Effect<void, StorageFailure> => Effect.gen(function* () { yield* deleteSourceById(core, input.id); const now = new Date(); yield* core.create({ model: "source", data: { id: input.id, plugin_id: pluginId, kind: input.kind, name: input.name, url: input.url ?? undefined, can_remove: input.canRemove ?? true, can_refresh: input.canRefresh ?? false, can_edit: input.canEdit ?? false, created_at: now, updated_at: now, }, forceAllowId: true, }); if (input.tools.length > 0) { yield* core.createMany({ model: "tool", data: input.tools.map((tool) => ({ id: `${input.id}.${tool.name}`, source_id: input.id, plugin_id: pluginId, name: tool.name, description: tool.description, input_schema: tool.inputSchema ?? undefined, output_schema: tool.outputSchema ?? undefined, created_at: now, updated_at: now, })), forceAllowId: true, }); } }); const deleteSourceById = ( core: TypedAdapter<CoreSchema>, sourceId: string,): Effect.Effect<void, StorageFailure> => Effect.gen(function* () { yield* core.deleteMany({ model: "tool", where: [{ field: "source_id", value: sourceId }], }); yield* core.deleteMany({ model: "definition", where: [{ field: "source_id", value: sourceId }], }); yield* core.delete({ model: "source", where: [{ field: "id", value: sourceId }], }); }); const writeDefinitions = ( core: TypedAdapter<CoreSchema>, pluginId: string, input: DefinitionsInput,): Effect.Effect<void, StorageFailure> => Effect.gen(function* () { yield* core.deleteMany({ model: "definition", where: [{ field: "source_id", value: input.sourceId }], }); const entries = Object.entries(input.definitions); if (entries.length === 0) return; const now = new Date(); yield* core.createMany({ model: "definition", data: entries.map(([name, schema]) => ({ id: `${input.sourceId}.${name}`, source_id: input.sourceId, plugin_id: pluginId, name, schema: schema as Record<string, unknown>, created_at: now, })), forceAllowId: true, }); }); // ---------------------------------------------------------------------------// Filtering — shared between dynamic (DB) and static (in-memory) pools// so `tools.list({ query, sourceId })` matches across both.// --------------------------------------------------------------------------- const toolMatchesFilter = (tool: Tool, filter: ToolListFilter): boolean => { if (filter.sourceId && tool.sourceId !== filter.sourceId) return false; if (filter.query) { const q = filter.query.toLowerCase(); const hay = `${tool.name} ${tool.description}`.toLowerCase(); if (!hay.includes(q)) return false; } return true;}; // ---------------------------------------------------------------------------// Active-adapter FiberRef. Nested plugin writes read this ref so that// `ctx.transaction` can swap in a tx-bound adapter handle without changing// the ctx shape — the root adapter returned by `buildAdapterRouter` below// resolves its target per call, so any Effect running inside// `Effect.locally(_, activeAdapterRef, trx)` automatically routes every// query through the same sql.begin connection. This is what makes nested// writes atomic on postgres + Hyperdrive without deadlocking a pool of 1.// ---------------------------------------------------------------------------const activeAdapterRef = FiberRef.unsafeMake<DBTransactionAdapter | null>( null,); // A `DBAdapter` whose methods dispatch to the active adapter (tx handle or// root) on every call. Stable identity for consumers (plugin storage,// `typedAdapter`, etc.) — they see one adapter object, but the routing is// decided at call time via the FiberRef above.const buildAdapterRouter = (root: DBAdapter): DBAdapter => { const pick = <A, E>( use: (active: DBTransactionAdapter) => Effect.Effect<A, E>, ): Effect.Effect<A, E> => Effect.flatMap(FiberRef.get(activeAdapterRef), (active) => use((active ?? (root as DBTransactionAdapter))), ); return { id: root.id, create: (data) => pick((a) => a.create(data)), createMany: (data) => pick((a) => a.createMany(data)), findOne: (data) => pick((a) => a.findOne(data)), findMany: (data) => pick((a) => a.findMany(data)), count: (data) => pick((a) => a.count(data)), update: (data) => pick((a) => a.update(data)), updateMany: (data) => pick((a) => a.updateMany(data)), delete: (data) => pick((a) => a.delete(data)), deleteMany: (data) => pick((a) => a.deleteMany(data)), // transaction() always opens a real boundary on the ROOT adapter so the // tx uses one real connection from the pool. If we're already inside a // parent tx (FiberRef set), skip opening a nested sql.begin — that's // the postgres.js + Hyperdrive deadlock path — and just run the // callback with the existing tx handle. In both cases the callback // sees a FiberRef-substituted adapter so further nested writes thread // through. transaction: (callback) => Effect.flatMap(FiberRef.get(activeAdapterRef), (active) => { if (active) return callback(active); return root.transaction((trx) => Effect.locally(callback(trx), activeAdapterRef, trx), ); }), } as DBAdapter;}; // ---------------------------------------------------------------------------// createExecutor// --------------------------------------------------------------------------- interface StaticTools { readonly source: StaticSourceDecl; readonly tool: StaticToolDecl; readonly pluginId: string; readonly ctx: PluginCtx<unknown>;} interface StaticSources { readonly source: StaticSourceDecl; readonly pluginId: string;} interface PluginRuntime { readonly plugin: AnyPlugin; readonly storage: unknown; readonly ctx: PluginCtx<unknown>;} export const createExecutor = < const TPlugins extends readonly AnyPlugin[] = [],>( config: ExecutorConfig<TPlugins>,): Effect.Effect<Executor<TPlugins>, Error> => Effect.gen(function* () { const { scope, adapter: rootAdapter, blobs, plugins = [] as unknown as TPlugins, } = config; // Scope-wrap the root adapter so every read on a tenant-scoped table // filters by the current scope stack and every write stamps the // write target. Today the stack has one element; the adapter's // `ScopeContext` shape already accepts an ordered list so layering // (org → workspace → user) can land later without changing plugin // code. Only tables whose schema declares `scope_id` are scoped. const schema = collectSchemas(plugins); const scopedRoot = scopeAdapter( rootAdapter, { read: [scope.id], write: scope.id }, schema, ); const adapter = buildAdapterRouter(scopedRoot); const core = typedAdapter<CoreSchema>(adapter); // Populated once, never mutated after startup. const staticTools = new Map<string, StaticTools>(); const staticSources = new Map<string, StaticSources>(); // Per-plugin runtime state. const runtimes = new Map<string, PluginRuntime>();Lines 1–500 of 1334. Load more
import { Effect, FiberRef } from "effect"; import { StorageError, typedAdapter, type DBAdapter, type DBSchema, type DBTransactionAdapter, type StorageFailure, type TypedAdapter, } from "@executor/storage-core"; import { scopeBlobStore, type BlobStore, } from "./blob"; import { coreSchema, type CoreSchema, type DefinitionsInput, type SourceInput, type SourceRow, type ToolAnnotations, type ToolRow, } from "./core-schema"; import { ElicitationDeclinedError, ElicitationResponse, FormElicitation, type ElicitationHandler, type ElicitationRequest, } from "./elicitation"; import { NoHandlerError, PluginNotLoadedError, SourceRemovalNotAllowedError, ToolInvocationError, ToolNotFoundError, } from "./errors"; import { SecretId, ToolId } from "./ids"; import type { AnyPlugin, Elicit, PluginCtx, PluginExtensions, StaticSourceDecl, StaticToolDecl, StorageDeps, } from "./plugin"; import type { Scope } from "./scope"; import { SecretRef, SetSecretInput, type SecretProvider, } from "./secrets"; import { ToolSchema, type Source, type SourceDetectionResult, type Tool, type ToolListFilter, } from "./types"; import { buildToolTypeScriptPreview } from "./schema-types"; import { scopeAdapter } from "./scoped-adapter"; // --------------------------------------------------------------------------- // InvokeOptions — passed to `executor.tools.invoke(id, args, options)`. // The `onElicitation` handler is threaded into the `elicit` function // exposed on plugin ctx / InvokeToolInput. Tools that never elicit // simply don't call it. // // The "accept-all" sentinel is convenient for tests and CLI automation — // every elicitation request gets auto-accepted with an empty content // payload. For real interactive hosts, pass a real handler. // --------------------------------------------------------------------------- export interface InvokeOptions { readonly onElicitation?: ElicitationHandler | "accept-all"; } const acceptAllHandler: ElicitationHandler = () => Effect.succeed(new ElicitationResponse({ action: "accept" })); const resolveElicitationHandler = ( options: InvokeOptions | undefined, ): ElicitationHandler => { const handler = options?.onElicitation; if (!handler || handler === "accept-all") return acceptAllHandler; return handler; }; // --------------------------------------------------------------------------- // Executor — public surface. Every list/invoke/schema call is a direct // core-table query (for dynamic rows) unioned with the in-memory static // pool. No ToolRegistry, no SourceRegistry, no SecretStore services. // --------------------------------------------------------------------------- export type Executor<TPlugins extends readonly AnyPlugin[] = []> = { readonly scope: Scope; readonly tools: { readonly list: ( filter?: ToolListFilter, ) => Effect.Effect<readonly Tool[], StorageFailure>; /** Fetch a tool's full schema view: JSON schemas with `$defs` * attached from the core `definition` table, plus TypeScript * preview strings rendered from them. Returns `null` for unknown * tool ids. */ readonly schema: ( toolId: string, ) => Effect.Effect<ToolSchema | null, StorageFailure>; /** Every `$defs` entry across every source, grouped by source id. * Used for bulk schema export and downstream TypeScript rendering. */ readonly definitions: () => Effect.Effect< Record<string, Record<string, unknown>>, StorageFailure >; readonly invoke: ( toolId: string, args: unknown, options?: InvokeOptions, ) => Effect.Effect< unknown, | ToolNotFoundError | PluginNotLoadedError | NoHandlerError | ToolInvocationError | ElicitationDeclinedError | StorageFailure >; }; readonly sources: { readonly list: () => Effect.Effect<readonly Source[], StorageFailure>; readonly remove: ( sourceId: string, ) => Effect.Effect<void, SourceRemovalNotAllowedError | StorageFailure>; readonly refresh: (sourceId: string) => Effect.Effect<void, StorageFailure>; /** URL autodetection — fans out to every plugin's `detect` hook * (if declared), returns every high/medium/low-confidence match. * UI picks a winner from the list. */ readonly detect: ( url: string, ) => Effect.Effect<readonly SourceDetectionResult[], StorageFailure>; /** All `$defs` registered for a single source, keyed by def name. */ readonly definitions: ( sourceId: string, ) => Effect.Effect<Record<string, unknown>, StorageFailure>; }; readonly secrets: { readonly get: ( id: string, ) => Effect.Effect<string | null, StorageFailure>; /** Fast-path existence check — hits the core `secret` routing table * only, never calls the provider. Use this for UI state ("secret * missing, prompt to add") to avoid keychain permission prompts * or 1password IPC roundtrips on a pre-flight check. */ readonly status: ( id: string, ) => Effect.Effect<"resolved" | "missing", StorageFailure>; readonly set: ( input: SetSecretInput, ) => Effect.Effect<SecretRef, StorageFailure>; readonly remove: (id: string) => Effect.Effect<void, StorageFailure>; readonly list: () => Effect.Effect<readonly SecretRef[], StorageFailure>; readonly providers: () => Effect.Effect<readonly string[]>; }; readonly close: () => Effect.Effect<void, StorageFailure>; } & PluginExtensions<TPlugins>; export interface ExecutorConfig< TPlugins extends readonly AnyPlugin[] = [], > { readonly scope: Scope; readonly adapter: DBAdapter; readonly blobs: BlobStore; readonly plugins?: TPlugins; } // --------------------------------------------------------------------------- // collectSchemas — merge coreSchema with every plugin's declared schema. // Hosts call this and pass the result to the migration runner (or to // the adapter factory for backends that auto-migrate from a schema // manifest) before constructing the executor. // --------------------------------------------------------------------------- export const collectSchemas = ( plugins: readonly AnyPlugin[], ): DBSchema => { const merged: Record<string, DBSchema[string]> = { ...coreSchema }; for (const plugin of plugins) { if (!plugin.schema) continue; for (const [modelKey, model] of Object.entries(plugin.schema)) { if (merged[modelKey]) { throw new Error( `Duplicate model "${modelKey}" contributed by plugin "${plugin.id}"` + ` (reserved by core or another plugin)`, ); } merged[modelKey] = model as DBSchema[string]; } } return merged; }; // --------------------------------------------------------------------------- // Row → public projection conversions // --------------------------------------------------------------------------- const rowToSource = (row: SourceRow): Source => ({ id: row.id, kind: row.kind, name: row.name, url: row.url ?? undefined, pluginId: row.plugin_id, canRemove: Boolean(row.can_remove), canRefresh: Boolean(row.can_refresh), canEdit: Boolean(row.can_edit), runtime: false, }); const staticDeclToSource = ( decl: StaticSourceDecl, pluginId: string, ): Source => ({ id: decl.id, kind: decl.kind, name: decl.name, url: decl.url, pluginId, canRemove: decl.canRemove ?? false, canRefresh: decl.canRefresh ?? false, canEdit: decl.canEdit ?? false, runtime: true, }); const decodeJsonColumn = (value: unknown): unknown => { if (value === null || value === undefined) return undefined; if (typeof value !== "string") return value; try { return JSON.parse(value); } catch { return value; } }; const rowToTool = ( row: ToolRow, annotations?: ToolAnnotations, ): Tool => ({ id: row.id, sourceId: row.source_id, pluginId: row.plugin_id, name: row.name, description: row.description, inputSchema: decodeJsonColumn(row.input_schema), outputSchema: decodeJsonColumn(row.output_schema), annotations, }); const staticDeclToTool = ( source: StaticSourceDecl, tool: StaticToolDecl, pluginId: string, ): Tool => ({ id: `${source.id}.${tool.name}`, sourceId: source.id, pluginId, name: tool.name, description: tool.description, inputSchema: tool.inputSchema, outputSchema: tool.outputSchema, annotations: tool.annotations, }); // --------------------------------------------------------------------------- // Dynamic-row writers — used by ctx.core.sources.register. Static sources // never touch these functions. // --------------------------------------------------------------------------- // Upsert shape: delete any existing source + tools + definitions for // `input.id` before creating fresh rows. Keeps replayable — boot-time // sync from executor.jsonc can call register() on rows that already // exist without tripping a UNIQUE constraint. const writeSourceInput = ( core: TypedAdapter<CoreSchema>, pluginId: string, input: SourceInput, ): Effect.Effect<void, StorageFailure> => Effect.gen(function* () { yield* deleteSourceById(core, input.id); const now = new Date(); yield* core.create({ model: "source", data: { id: input.id, plugin_id: pluginId, kind: input.kind, name: input.name, url: input.url ?? undefined, can_remove: input.canRemove ?? true, can_refresh: input.canRefresh ?? false, can_edit: input.canEdit ?? false, created_at: now, updated_at: now, }, forceAllowId: true, }); if (input.tools.length > 0) { yield* core.createMany({ model: "tool", data: input.tools.map((tool) => ({ id: `${input.id}.${tool.name}`, source_id: input.id, plugin_id: pluginId, name: tool.name, description: tool.description, input_schema: tool.inputSchema ?? undefined, output_schema: tool.outputSchema ?? undefined, created_at: now, updated_at: now, })), forceAllowId: true, }); } }); const deleteSourceById = ( core: TypedAdapter<CoreSchema>, sourceId: string, ): Effect.Effect<void, StorageFailure> => Effect.gen(function* () { yield* core.deleteMany({ model: "tool", where: [{ field: "source_id", value: sourceId }], }); yield* core.deleteMany({ model: "definition", where: [{ field: "source_id", value: sourceId }], }); yield* core.delete({ model: "source", where: [{ field: "id", value: sourceId }], }); }); const writeDefinitions = ( core: TypedAdapter<CoreSchema>, pluginId: string, input: DefinitionsInput, ): Effect.Effect<void, StorageFailure> => Effect.gen(function* () { yield* core.deleteMany({ model: "definition", where: [{ field: "source_id", value: input.sourceId }], }); const entries = Object.entries(input.definitions); if (entries.length === 0) return; const now = new Date(); yield* core.createMany({ model: "definition", data: entries.map(([name, schema]) => ({ id: `${input.sourceId}.${name}`, source_id: input.sourceId, plugin_id: pluginId, name, schema: schema as Record<string, unknown>, created_at: now, })), forceAllowId: true, }); }); // --------------------------------------------------------------------------- // Filtering — shared between dynamic (DB) and static (in-memory) pools // so `tools.list({ query, sourceId })` matches across both. // --------------------------------------------------------------------------- const toolMatchesFilter = (tool: Tool, filter: ToolListFilter): boolean => { if (filter.sourceId && tool.sourceId !== filter.sourceId) return false; if (filter.query) { const q = filter.query.toLowerCase(); const hay = `${tool.name} ${tool.description}`.toLowerCase(); if (!hay.includes(q)) return false; } return true; }; // --------------------------------------------------------------------------- // Active-adapter FiberRef. Nested plugin writes read this ref so that // `ctx.transaction` can swap in a tx-bound adapter handle without changing // the ctx shape — the root adapter returned by `buildAdapterRouter` below // resolves its target per call, so any Effect running inside // `Effect.locally(_, activeAdapterRef, trx)` automatically routes every // query through the same sql.begin connection. This is what makes nested // writes atomic on postgres + Hyperdrive without deadlocking a pool of 1. // --------------------------------------------------------------------------- const activeAdapterRef = FiberRef.unsafeMake<DBTransactionAdapter | null>( null, ); // A `DBAdapter` whose methods dispatch to the active adapter (tx handle or // root) on every call. Stable identity for consumers (plugin storage, // `typedAdapter`, etc.) — they see one adapter object, but the routing is // decided at call time via the FiberRef above. const buildAdapterRouter = (root: DBAdapter): DBAdapter => { const pick = <A, E>( use: (active: DBTransactionAdapter) => Effect.Effect<A, E>, ): Effect.Effect<A, E> => Effect.flatMap(FiberRef.get(activeAdapterRef), (active) => use((active ?? (root as DBTransactionAdapter))), ); return { id: root.id, create: (data) => pick((a) => a.create(data)), createMany: (data) => pick((a) => a.createMany(data)), findOne: (data) => pick((a) => a.findOne(data)), findMany: (data) => pick((a) => a.findMany(data)), count: (data) => pick((a) => a.count(data)), update: (data) => pick((a) => a.update(data)), updateMany: (data) => pick((a) => a.updateMany(data)), delete: (data) => pick((a) => a.delete(data)), deleteMany: (data) => pick((a) => a.deleteMany(data)), // transaction() always opens a real boundary on the ROOT adapter so the // tx uses one real connection from the pool. If we're already inside a // parent tx (FiberRef set), skip opening a nested sql.begin — that's // the postgres.js + Hyperdrive deadlock path — and just run the // callback with the existing tx handle. In both cases the callback // sees a FiberRef-substituted adapter so further nested writes thread // through. transaction: (callback) => Effect.flatMap(FiberRef.get(activeAdapterRef), (active) => { if (active) return callback(active); return root.transaction((trx) => Effect.locally(callback(trx), activeAdapterRef, trx), ); }), } as DBAdapter; }; // --------------------------------------------------------------------------- // createExecutor // --------------------------------------------------------------------------- interface StaticTools { readonly source: StaticSourceDecl; readonly tool: StaticToolDecl; readonly pluginId: string; readonly ctx: PluginCtx<unknown>; } interface StaticSources { readonly source: StaticSourceDecl; readonly pluginId: string; } interface PluginRuntime { readonly plugin: AnyPlugin; readonly storage: unknown; readonly ctx: PluginCtx<unknown>; } export const createExecutor = < const TPlugins extends readonly AnyPlugin[] = [], >( config: ExecutorConfig<TPlugins>, ): Effect.Effect<Executor<TPlugins>, Error> => Effect.gen(function* () { const { scope, adapter: rootAdapter, blobs, plugins = [] as unknown as TPlugins, } = config; // Scope-wrap the root adapter so every read on a tenant-scoped table // filters by the current scope stack and every write stamps the // write target. Today the stack has one element; the adapter's // `ScopeContext` shape already accepts an ordered list so layering // (org → workspace → user) can land later without changing plugin // code. Only tables whose schema declares `scope_id` are scoped. const schema = collectSchemas(plugins); const scopedRoot = scopeAdapter( rootAdapter, { read: [scope.id], write: scope.id }, schema, ); const adapter = buildAdapterRouter(scopedRoot); const core = typedAdapter<CoreSchema>(adapter); // Populated once, never mutated after startup. const staticTools = new Map<string, StaticTools>(); const staticSources = new Map<string, StaticSources>(); // Per-plugin runtime state. const runtimes = new Map<string, PluginRuntime>();