History

refactor(core): generic executor-wrapper seam; move toolkit narrowing into the plugin

Core no longer contains any toolkit vocabulary. The MCP narrowing seam (toolkit-scope.ts: ResolvedToolkitScope/applyToolkitScope/EMPTY_TOOLKIT_SCOPE) moves out of @executor-js/sdk into the toolkits plugin, where 'toolkit' naming belongs. In its place core gains a domain-agnostic hook: a plugin may expose a `wrapExecutor` method on its extension; the assembler collects these into the executor and the execution stack runs each before the engine is built, threading a per-request `selector` string. Core knows nothing about what a wrapper narrows to. The toolkits plugin contributes the wrapper (its resolveScope + applyToolkitScope, fail-closed to the empty slice). - removes the hardcoded `base.toolkits` plugin-id lookup in execution-stack - renames the core/host build option toolkitId -> selector (neutral) - ExecutorWrapper exported from @executor-js/sdk Behavior unchanged: full repo typecheck green; all 12 toolkit e2e files (13 tests) pass.

Rhys Sullivan committed 4 months ago

Commit ef950471 parent 30c6c278 files · +971 −381Browse files

Unified · Split

8 files
  1. packages/core/api/src/server/execution-stack.ts +56 −31
  2. packages/core/api/src/server/mcp-build.ts +18 −7
  3. packages/core/sdk/src/executor.ts +632 −209
  4. packages/core/sdk/src/index.ts +21 −14
  5. packages/core/sdk/src/tool.ts +10 −4
  6. packages/hosts/mcp/src/in-memory-session-store.ts +84 −30
  7. packages/plugins/toolkits/src/server.ts +110 −58
  8. packages/plugins/toolkits/src/toolkit-scope.ts +40 −28
packages/core/api/src/server/execution-stack.ts+56−31
@@ -27,12 +27,10 @@ import { Context, Effect, Layer } from "effect"; · 26 unmodified lines
2727 import type * as Cause from "effect/Cause";
2828
2929 import {
30− applyToolkitScope,
31− EMPTY_TOOLKIT_SCOPE,
3230 type AnyPlugin,
3331 type Executor,
32+ type ExecutorWrapper,
3433 type StorageFailure,
35− type ToolkitResolver,
3634 } from "@executor-js/sdk";
3735 import {
3836 createExecutionEngine,
@@ -41,7 +39,11 @@ import { · 2 unmodified lines
4139 } from "@executor-js/execution";
4240
4341 import { DbProvider } from "./executor-fuma-db";
44−import { HostConfig, PluginsProvider, makeScopedExecutor } from "./scoped-executor";
42+import {
43+ HostConfig,
44+ PluginsProvider,
45+ makeScopedExecutor,
46+} from "./scoped-executor";
4547
4648 // ---------------------------------------------------------------------------
4749 // CodeExecutorProvider seam — the host's code-execution substrate. Typed to the
@@ -50,11 +52,13 @@ import { HostConfig, PluginsProvider, makeScopedExecutor } from "./scoped-execut · 2 unmodified lines
5052 // assigns structurally.
5153 // ---------------------------------------------------------------------------
5254
53−export type CodeExecutor = ExecutionEngineConfig<Cause.YieldableError>["codeExecutor"];
55+export type CodeExecutor =
56+ ExecutionEngineConfig<Cause.YieldableError>["codeExecutor"];
5457
55−export class CodeExecutorProvider extends Context.Service<CodeExecutorProvider, CodeExecutor>()(
56− "@executor-js/api/CodeExecutorProvider",
57−) {}
58+export class CodeExecutorProvider extends Context.Service<
59+ CodeExecutorProvider,
60+ CodeExecutor
61+>()("@executor-js/api/CodeExecutorProvider") {}
5862
5963 // ---------------------------------------------------------------------------
6064 // EngineDecorator seam — wrap the freshly built engine (e.g. with usage
@@ -78,12 +82,15 @@ export interface EngineDecoratorShape { · 17 unmodified lines
7882 ) => ExecutionEngine<E>;
7983 }
8084
81−export class EngineDecorator extends Context.Service<EngineDecorator, EngineDecoratorShape>()(
82− "@executor-js/api/EngineDecorator",
83−) {}
85+export class EngineDecorator extends Context.Service<
86+ EngineDecorator,
87+ EngineDecoratorShape
88+>()("@executor-js/api/EngineDecorator") {}
8489
8590 /** No-op decorator: the engine passes through unchanged. */
86−export const EngineDecoratorNoop: Layer.Layer<EngineDecorator> = Layer.succeed(EngineDecorator)({
91+export const EngineDecoratorNoop: Layer.Layer<EngineDecorator> = Layer.succeed(
92+ EngineDecorator,
93+)({
8794 decorate: (engine) => engine,
8895 });
8996
@@ -102,20 +109,34 @@ export const makeExecutionStack = < · 12 unmodified lines
102109 accountId: string,
103110 organizationId: string,
104111 organizationName: string,
105− /** Optional toolkit selector (slug or id). When set, the executor is narrowed
106− * to that toolkit's slice before the engine is built — so listing, search,
107− * description, core-tools, AND execute all see only the slice. Resolves via
108− * the `toolkits` plugin extension; an unknown/unauthorized selector applies
109− * an EMPTY slice (fail-closed — never the full account). */
110− toolkitId?: string,
112+ /** Optional per-request narrowing selector (e.g. an MCP `?toolkit=` value).
113+ * When set, the executor is run through every plugin-contributed
114+ * `ExecutorWrapper` before the engine is built — so listing, search,
115+ * description, core-tools, AND execute all see only the narrowed view. Core
116+ * is agnostic about what a wrapper narrows to; a plugin (e.g. toolkits) owns
117+ * that, including fail-closed behavior for an unknown selector. */
118+ selector?: string,
111119 ): Effect.Effect<
112− { readonly executor: Executor<TPlugins>; readonly engine: ExecutionEngine<Cause.YieldableError> },
120+ {
121+ readonly executor: Executor<TPlugins>;
122+ readonly engine: ExecutionEngine<Cause.YieldableError>;
123+ },
113124 StorageFailure,
114− DbProvider | PluginsProvider | HostConfig | CodeExecutorProvider | EngineDecorator
125+ | DbProvider
126+ | PluginsProvider
127+ | HostConfig
128+ | CodeExecutorProvider
129+ | EngineDecorator
115130 > =>
116131 Effect.gen(function* () {
117− const base = yield* makeScopedExecutor<TPlugins>(accountId, organizationId, organizationName);
118− const executor = toolkitId ? yield* narrowToToolkit(base, toolkitId) : base;
132+ const base = yield* makeScopedExecutor<TPlugins>(
133+ accountId,
134+ organizationId,
135+ organizationName,
136+ );
137+ const executor = selector
138+ ? yield* applyExecutorWrappers(base, selector)
139+ : base;
119140 const codeExecutor = yield* CodeExecutorProvider;
120141 const { decorate } = yield* EngineDecorator;
121142 const engine = decorate(createExecutionEngine({ executor, codeExecutor }), {
@@ -126,16 +147,20 @@ export const makeExecutionStack = < · 4 unmodified lines
126147 return { executor, engine };
127148 });
128149
129−// Resolve the toolkit slice via the `toolkits` plugin extension (stamped on the
130−// executor) and wrap the executor. Fail-closed: no extension or no match -> an
131−// empty slice, so a bad/cross-tenant selector exposes only static tools, never
132−// the full account.
133−const narrowToToolkit = <TPlugins extends readonly AnyPlugin[]>(
150+// Run every plugin-contributed executor wrapper (collected on the executor at
151+// assembly) with the request selector. Core does not know what any wrapper
152+// narrows to — the toolkits plugin's wrapper, for instance, fails closed to an
153+// empty slice for an unknown/cross-tenant selector. With no wrapper registered,
154+// the executor passes through unchanged.
155+const applyExecutorWrappers = <TPlugins extends readonly AnyPlugin[]>(
134156 base: Executor<TPlugins>,
135− toolkitId: string,
157+ selector: string,
136158 ): Effect.Effect<Executor<TPlugins>, StorageFailure> =>
137159 Effect.gen(function* () {
138− const resolver = (base as { toolkits?: ToolkitResolver }).toolkits;
139− const scope = resolver?.resolveScope ? yield* resolver.resolveScope(toolkitId) : null;
140− return yield* applyToolkitScope(base, scope ?? EMPTY_TOOLKIT_SCOPE);
160+ const wrappers = (base as { executorWrappers?: readonly ExecutorWrapper[] })
161+ .executorWrappers;
162+ let executor = base;
163+ for (const wrap of wrappers ?? [])
164+ executor = yield* wrap(executor, selector);
165+ return executor;
141166 });
packages/core/api/src/server/mcp-build.ts+18−7
@@ -9,7 +9,11 @@ import { · 8 unmodified lines
99 import { createExecutorMcpServer } from "@executor-js/host-mcp/tool-server";
1010
1111 import { ErrorCapture } from "../observability";
12−import { CodeExecutorProvider, EngineDecorator, makeExecutionStack } from "./execution-stack";
12+import {
13+ CodeExecutorProvider,
14+ EngineDecorator,
15+ makeExecutionStack,
16+} from "./execution-stack";
1317 import { DbProvider } from "./executor-fuma-db";
1418 import { HostConfig, PluginsProvider } from "./scoped-executor";
1519
@@ -28,7 +32,11 @@ import { HostConfig, PluginsProvider } from "./scoped-executor"; · 12 unmodified lines
2832
2933 /** The five execution-stack seams a host fully provides (no residual). */
3034 export type McpExecutionStackLayer = Layer.Layer<
31− DbProvider | PluginsProvider | HostConfig | CodeExecutorProvider | EngineDecorator
35+ | DbProvider
36+ | PluginsProvider
37+ | HostConfig
38+ | CodeExecutorProvider
39+ | EngineDecorator
3240 >;
3341
3442 /**
@@ -39,14 +47,15 @@ export type McpExecutionStackLayer = Layer.Layer< · 4 unmodified lines
3947 export const makeMcpBuildServer =
4048 (executionStack: McpExecutionStackLayer): McpBuildServer =>
4149 (principal: Principal, options?: McpBuildServerOptions) => {
42− // `toolkitId` narrows the engine in `makeExecutionStack`; the rest of the
43− // options configure the MCP server (elicitation/approval).
44− const { toolkitId, ...serverOptions } = options ?? {};
50+ // `selector` narrows the engine in `makeExecutionStack` (a plugin's
51+ // executor wrapper applies it); the rest configure the MCP server
52+ // (elicitation/approval).
53+ const { selector, ...serverOptions } = options ?? {};
4554 return makeExecutionStack(
4655 principal.accountId,
4756 principal.organizationId,
4857 principal.organizationName,
49− toolkitId,
58+ selector,
5059 ).pipe(
5160 Effect.map(({ engine }) => engine),
5261 Effect.provide(executionStack),
@@ -72,6 +81,8 @@ export const makeConsoleMcpErrorReporter = ( · 19 unmodified lines
7281 McpErrorReporter,
7382 Effect.gen(function* () {
7483 const capture = yield* ErrorCapture;
75− return { report: (cause) => Effect.asVoid(capture.captureException(cause)) };
84+ return {
85+ report: (cause) => Effect.asVoid(capture.captureException(cause)),
86+ };
7687 }),
7788 ).pipe(Layer.provide(errorCapture));
packages/core/sdk/src/executor.ts+632−209
@@ -2,8 +2,15 @@ import { Effect, Inspectable, Layer, Option, Predicate, Schema } from "effect"; · 1 unmodified line
22 import { FetchHttpClient, type HttpClient } from "effect/unstable/http";
33 import { fumadb } from "@executor-js/fumadb";
44 import { memoryAdapter } from "@executor-js/fumadb/adapters/memory";
5−import { withQueryContext, type Condition, type ConditionBuilder } from "@executor-js/fumadb/query";
6−import { schema as fumaSchema, type RelationsMap } from "@executor-js/fumadb/schema";
5+import {
6+ withQueryContext,
7+ type Condition,
8+ type ConditionBuilder,
9+} from "@executor-js/fumadb/query";
10+import {
11+ schema as fumaSchema,
12+ type RelationsMap,
13+} from "@executor-js/fumadb/schema";
714 import type { AnyColumn } from "@executor-js/fumadb/schema";
815 import { generateKeyBetween } from "fractional-indexing";
916
@@ -16,7 +23,12 @@ import { · 6 unmodified lines
1623 type FumaTables,
1724 type StorageFailure,
1825 } from "./fuma-runtime";
19−import { makeFumaBlobStore, pluginBlobStore, type BlobStore, type OwnerPartitions } from "./blob";
26+import {
27+ makeFumaBlobStore,
28+ pluginBlobStore,
29+ type BlobStore,
30+ type OwnerPartitions,
31+} from "./blob";
2032 import { coreToolsPlugin } from "./core-tools";
2133 import type {
2234 Connection,
@@ -85,7 +97,10 @@ import type { · 62 unmodified lines
8597 IntegrationConfig,
8698 RegisterIntegrationInput,
8799 } from "./integration";
88−import { makeOAuthService, type MintOAuthConnectionInput } from "./oauth-service";
100+import {
101+ makeOAuthService,
102+ type MintOAuthConnectionInput,
103+} from "./oauth-service";
89104 import type { OAuthService } from "./oauth-client";
90105 import {
91106 comparePolicyRow,
@@ -130,7 +145,12 @@ import { · 38 unmodified lines
130145 type ExecutorOwnerPolicyContext,
131146 } from "./owner-policy";
132147 import { ToolSchemaView, type IntegrationDetectionResult } from "./types";
133−import { type Tool, type ToolAnnotations, type ToolDef, type ToolListFilter } from "./tool";
148+import {
149+ type Tool,
150+ type ToolAnnotations,
151+ type ToolDef,
152+ type ToolListFilter,
153+} from "./tool";
134154 import { buildToolTypeScriptPreview } from "./schema-types";
135155 import { collectReferencedDefinitions } from "./schema-refs";
136156 import {
@@ -155,7 +175,9 @@ const MAX_APPROVAL_ARGUMENT_PREVIEW_CHARS = 4_000; · 18 unmodified lines
155175 const acceptAllHandler: ElicitationHandler = () =>
156176 Effect.succeed(ElicitationResponse.make({ action: "accept" }));
157177
158−const resolveElicitationHandler = (onElicitation: OnElicitation): ElicitationHandler =>
178+const resolveElicitationHandler = (
179+ onElicitation: OnElicitation,
180+): ElicitationHandler =>
159181 onElicitation === "accept-all" ? acceptAllHandler : onElicitation;
160182
161183 // ---------------------------------------------------------------------------
@@ -171,7 +193,8 @@ export interface ParsedToolAddress { · 9 unmodified lines
171193 readonly tool: ToolName;
172194 }
173195
174−const isOwner = (value: string): value is Owner => value === "org" || value === "user";
196+const isOwner = (value: string): value is Owner =>
197+ value === "org" || value === "user";
175198
176199 /** Parse a callable address; null when it's not a well-formed
177200 * `tools.<integration>.<owner>.<connection>.<tool>`.
@@ -190,16 +213,17 @@ export const parseToolAddress = (address: string): ParsedToolAddress | null => { · 12 unmodified lines
190213 cut = address.indexOf(".", cut + 1);
191214 if (cut === -1) return null;
192215 }
193− const [prefix, integration, owner, connection] = address.slice(0, cut).split(".") as [
194− string,
195− string,
196− string,
197− string,
198− ];
216+ const [prefix, integration, owner, connection] = address
217+ .slice(0, cut)
218+ .split(".") as [string, string, string, string];
199219 const tool = address.slice(cut + 1);
200220 if (prefix !== ADDRESS_PREFIX) return null;
201221 if (!isOwner(owner)) return null;
202− if (integration.length === 0 || connection.length === 0 || tool.length === 0) {
222+ if (
223+ integration.length === 0 ||
224+ connection.length === 0 ||
225+ tool.length === 0
226+ ) {
203227 return null;
204228 }
205229 return {
@@ -215,7 +239,9 @@ export const connectionAddress = ( · 9 unmodified lines
215239 integration: IntegrationSlug,
216240 connection: ConnectionName,
217241 ): ConnectionAddress =>
218− ConnectionAddress.make(`${ADDRESS_PREFIX}.${integration}.${owner}.${connection}`);
242+ ConnectionAddress.make(
243+ `${ADDRESS_PREFIX}.${integration}.${owner}.${connection}`,
244+ );
219245
220246 export const toolAddress = (
221247 owner: Owner,
@@ -223,7 +249,9 @@ export const toolAddress = ( · 1 unmodified line
223249 connection: ConnectionName,
224250 tool: ToolName,
225251 ): ToolAddress =>
226− ToolAddress.make(`${ADDRESS_PREFIX}.${integration}.${owner}.${connection}.${tool}`);
252+ ToolAddress.make(
253+ `${ADDRESS_PREFIX}.${integration}.${owner}.${connection}.${tool}`,
254+ );
227255
228256 // ---------------------------------------------------------------------------
229257 // Owner key helpers — every owned-row write stamps `tenant`, `owner`,
@@ -241,17 +269,34 @@ interface OwnedKeys { · 11 unmodified lines
241269 // core-table query unioned with the in-memory static pool.
242270 // ---------------------------------------------------------------------------
243271
272+// ExecutorWrapper — a plugin-contributed executor middleware. Given the
273+// assembled executor and the per-request selector string (e.g. an MCP
274+// `?toolkit=` value), it returns a possibly-narrowed executor. A plugin
275+// contributes one by exposing a `wrapExecutor` method on its extension; the
276+// assembler collects them and the execution stack runs each before the engine
277+// is built. Core knows nothing about what a wrapper narrows to — that domain
278+// (toolkits, etc.) lives entirely in the contributing plugin.
279+export type ExecutorWrapper = <TPlugins extends readonly AnyPlugin[]>(
280+ base: Executor<TPlugins>,
281+ selector: string,
282+) => Effect.Effect<Executor<TPlugins>, StorageFailure>;
283+
244284 export type Executor<TPlugins extends readonly AnyPlugin[] = readonly []> = {
245285 readonly integrations: {
246286 readonly list: () => Effect.Effect<readonly Integration[], StorageFailure>;
247− readonly get: (slug: IntegrationSlug) => Effect.Effect<Integration | null, StorageFailure>;
287+ readonly get: (
288+ slug: IntegrationSlug,
289+ ) => Effect.Effect<Integration | null, StorageFailure>;
248290 readonly update: (
249291 slug: IntegrationSlug,
250292 patch: { readonly name?: string; readonly description?: string },
251293 ) => Effect.Effect<void, IntegrationNotFoundError | StorageFailure>;
252294 readonly remove: (
253295 slug: IntegrationSlug,
254− ) => Effect.Effect<void, IntegrationRemovalNotAllowedError | StorageFailure>;
296+ ) => Effect.Effect<
297+ void,
298+ IntegrationRemovalNotAllowedError | StorageFailure
299+ >;
255300 readonly detect: (
256301 url: string,
257302 ) => Effect.Effect<readonly IntegrationDetectionResult[], StorageFailure>;
@@ -271,7 +316,9 @@ export type Executor<TPlugins extends readonly AnyPlugin[] = readonly []> = { · 13 unmodified lines
271316 readonly integration?: IntegrationSlug;
272317 readonly owner?: Owner;
273318 }) => Effect.Effect<readonly Connection[], StorageFailure>;
274− readonly get: (ref: ConnectionRef) => Effect.Effect<Connection | null, StorageFailure>;
319+ readonly get: (
320+ ref: ConnectionRef,
321+ ) => Effect.Effect<Connection | null, StorageFailure>;
275322 /** Edit user-curated metadata (description, identityLabel). Credentials and
276323 * OAuth lifecycle fields are not editable here. */
277324 readonly update: (
@@ -294,21 +341,35 @@ export type Executor<TPlugins extends readonly AnyPlugin[] = readonly []> = { · 16 unmodified lines
294341 readonly oauth: OAuthService;
295342
296343 readonly tools: {
297− readonly list: (filter?: ToolListFilter) => Effect.Effect<readonly Tool[], StorageFailure>;
298− readonly schema: (address: ToolAddress) => Effect.Effect<ToolSchemaView | null, StorageFailure>;
344+ readonly list: (
345+ filter?: ToolListFilter,
346+ ) => Effect.Effect<readonly Tool[], StorageFailure>;
347+ readonly schema: (
348+ address: ToolAddress,
349+ ) => Effect.Effect<ToolSchemaView | null, StorageFailure>;
299350 };
300351
301352 readonly providers: {
302353 readonly list: () => Effect.Effect<readonly ProviderKey[]>;
303− readonly items: (key: ProviderKey) => Effect.Effect<readonly ProviderEntry[], StorageFailure>;
354+ readonly items: (
355+ key: ProviderKey,
356+ ) => Effect.Effect<readonly ProviderEntry[], StorageFailure>;
304357 };
305358
306359 readonly policies: {
307360 readonly list: () => Effect.Effect<readonly ToolPolicy[], StorageFailure>;
308− readonly create: (input: CreateToolPolicyInput) => Effect.Effect<ToolPolicy, StorageFailure>;
309− readonly update: (input: UpdateToolPolicyInput) => Effect.Effect<ToolPolicy, StorageFailure>;
310− readonly remove: (input: RemoveToolPolicyInput) => Effect.Effect<void, StorageFailure>;
311− readonly resolve: (address: ToolAddress) => Effect.Effect<EffectivePolicy, StorageFailure>;
361+ readonly create: (
362+ input: CreateToolPolicyInput,
363+ ) => Effect.Effect<ToolPolicy, StorageFailure>;
364+ readonly update: (
365+ input: UpdateToolPolicyInput,
366+ ) => Effect.Effect<ToolPolicy, StorageFailure>;
367+ readonly remove: (
368+ input: RemoveToolPolicyInput,
369+ ) => Effect.Effect<void, StorageFailure>;
370+ readonly resolve: (
371+ address: ToolAddress,
372+ ) => Effect.Effect<EffectivePolicy, StorageFailure>;
312373 };
313374
314375 readonly execute: (
@@ -322,7 +383,10 @@ export type Executor<TPlugins extends readonly AnyPlugin[] = readonly []> = { · 7 unmodified lines
322383
323384 export interface ExecutorDb {
324385 readonly db: FumaDb<any>;
325− readonly close?: () => Effect.Effect<void, StorageFailure> | Promise<void> | void;
386+ readonly close?: () =>
387+ | Effect.Effect<void, StorageFailure>
388+ | Promise<void>
389+ | void;
326390 }
327391
328392 export type ExecutorDbInput = FumaDb<any> | ExecutorDb;
@@ -331,7 +395,9 @@ export type ExecutorDbFactory = (config: { · 2 unmodified lines
331395 readonly tables: FumaTables;
332396 }) => ExecutorDbInput | Effect.Effect<ExecutorDbInput, StorageFailure>;
333397
334−export interface ExecutorConfig<TPlugins extends readonly AnyPlugin[] = readonly []> {
398+export interface ExecutorConfig<
399+ TPlugins extends readonly AnyPlugin[] = readonly [],
400+> {
335401 /** The org / workspace this executor is bound to. `owner: "org"` rows file
336402 * here. */
337403 readonly tenant: Tenant;
@@ -392,7 +458,10 @@ const validateExecutorOwnerPolicyTables = (tables: FumaTables): void => { · 54 unmodified lines
392458 }
393459 };
394460
395−const validateExecutorDbTables = (required: FumaTables, actual: FumaTables): void => {
461+const validateExecutorDbTables = (
462+ required: FumaTables,
463+ actual: FumaTables,
464+): void => {
396465 const missing = Object.keys(required)
397466 .filter((tableName) => !actual[tableName])
398467 .sort();
@@ -408,18 +477,27 @@ const validateExecutorDbTables = (required: FumaTables, actual: FumaTables): voi · 9 unmodified lines
408477 });
409478 };
410479
411−const storageFailureFromUnknown = (message: string, cause: unknown): StorageFailure =>
480+const storageFailureFromUnknown = (
481+ message: string,
482+ cause: unknown,
483+): StorageFailure =>
412484 isStorageFailure(cause) ? cause : new StorageError({ message, cause });
413485
414−const pluginStorageFailure = (pluginId: string, hook: string, cause: unknown): StorageFailure =>
486+const pluginStorageFailure = (
487+ pluginId: string,
488+ hook: string,
489+ cause: unknown,
490+): StorageFailure =>
415491 storageFailureFromUnknown(`${hook} failed for plugin ${pluginId}`, cause);
416492
417493 const createDefaultMemoryDb = (tables: FumaTables): ExecutorDb => {
418494 const version = "1.0.0";
419− const latestSchema = fumaSchema<string, FumaTables, RelationsMap<FumaTables>>({
420− version,
421− tables,
422− });
495+ const latestSchema = fumaSchema<string, FumaTables, RelationsMap<FumaTables>>(
496+ {
497+ version,
498+ tables,
499+ },
500+ );
423501 const factory = fumadb({
424502 namespace: "executor_memory",
425503 schemas: [latestSchema],
@@ -434,7 +512,9 @@ const createDefaultMemoryDb = (tables: FumaTables): ExecutorDb => { · 8 unmodified lines
434512 // JSON helpers + row → public projection conversions
435513 // ---------------------------------------------------------------------------
436514
437−const decodeJsonFromString = Schema.decodeUnknownOption(Schema.UnknownFromJsonString);
515+const decodeJsonFromString = Schema.decodeUnknownOption(
516+ Schema.UnknownFromJsonString,
517+);
438518
439519 const decodeJsonColumn = (value: unknown): unknown => {
440520 if (value === null || value === undefined) return undefined;
@@ -482,9 +562,14 @@ const rowToConnection = (row: ConnectionRow): Connection => { · 41 unmodified lines
482562 identityLabel: row.identity_label ?? null,
483563 description: row.description ?? null,
484564 expiresAt: row.expires_at == null ? null : Number(row.expires_at),
485− oauthClient: row.oauth_client == null ? null : OAuthClientSlug.make(String(row.oauth_client)),
565+ oauthClient:
566+ row.oauth_client == null
567+ ? null
568+ : OAuthClientSlug.make(String(row.oauth_client)),
486569 oauthClientOwner:
487− row.oauth_client_owner == null ? null : (String(row.oauth_client_owner) as Owner),
570+ row.oauth_client_owner == null
571+ ? null
572+ : (String(row.oauth_client_owner) as Owner),
488573 oauthScope: row.oauth_scope == null ? null : String(row.oauth_scope),
489574 };
490575 };
@@ -505,7 +590,10 @@ const normalizeConnectionInputs = ( · 14 unmodified lines
505590 input: ConnectionValueInput,
506591 ): readonly NormalizedConnectionInput[] => {
507592 if ("inputs" in input) {
508− return Object.entries(input.inputs).map(([variable, origin]) => ({ variable, origin }));
593+ return Object.entries(input.inputs).map(([variable, origin]) => ({
594+ variable,
595+ origin,
596+ }));
509597 }
510598 if ("values" in input) {
511599 return Object.entries(input.values).map(([variable, value]) => ({
@@ -532,7 +620,8 @@ const connectionItemIds = (row: ConnectionRow): Record<string, string> => { · 20 unmodified lines
532620 // schema columns); `Tool.inputSchema`/`outputSchema` are optional and stay
533621 // absent for those callers — `tools.schema` is the schema-bearing surface.
534622 const rowToTool = (
535− row: ToolInvocationRow & Partial<Pick<ToolRow, "input_schema" | "output_schema">>,
623+ row: ToolInvocationRow &
624+ Partial<Pick<ToolRow, "input_schema" | "output_schema">>,
536625 annotations?: ToolAnnotations,
537626 ): Tool => {
538627 const owner = row.owner as Owner;
@@ -549,7 +638,9 @@ const rowToTool = ( · 10 unmodified lines
549638 description: row.description,
550639 inputSchema: decodeJsonColumn(row.input_schema),
551640 outputSchema: decodeJsonColumn(row.output_schema),
552− annotations: annotations ?? (decodeJsonColumn(row.annotations) as ToolAnnotations | undefined),
641+ annotations:
642+ annotations ??
643+ (decodeJsonColumn(row.annotations) as ToolAnnotations | undefined),
553644 };
554645 };
555646
@@ -580,7 +671,10 @@ type CoreFindFirstOptions<TName extends CoreTableName = CoreTableName> = Omit< · 24 unmodified lines
580671 >;
581672 /** The narrowed row a projected query returns: the selected columns keep
582673 * their types, everything else is absent. */
583−type CoreProjectedRow<TName extends CoreTableName, TSelect> = TSelect extends readonly (infer K)[]
674+type CoreProjectedRow<
675+ TName extends CoreTableName,
676+ TSelect,
677+> = TSelect extends readonly (infer K)[]
584678 ? Pick<CoreRow<TName>, Extract<K, keyof CoreRow<TName>>>
585679 : CoreRow<TName>;
586680
@@ -613,7 +707,9 @@ const makeCoreDb = (fuma: ReturnType<typeof makeFumaClient>) => ({ · 26 unmodified lines
613707 tableName: TName,
614708 options?: { readonly where?: CoreWhere },
615709 ): Effect.Effect<number, StorageFailure> =>
616− fuma.use(`${tableName}.count`, (db) => asLooseStorageDb(db).count(tableName, options)),
710+ fuma.use(`${tableName}.count`, (db) =>
711+ asLooseStorageDb(db).count(tableName, options),
712+ ),
617713 create: <TName extends CoreTableName>(
618714 tableName: TName,
619715 row: Record<string, unknown>,
@@ -628,7 +724,9 @@ const makeCoreDb = (fuma: ReturnType<typeof makeFumaClient>) => ({ · 8 unmodified lines
628724 rows.length === 0
629725 ? Effect.void
630726 : fuma
631− .use(`${tableName}.createMany`, (db) => asLooseStorageDb(db).createMany(tableName, rows))
727+ .use(`${tableName}.createMany`, (db) =>
728+ asLooseStorageDb(db).createMany(tableName, rows),
729+ )
632730 .pipe(Effect.asVoid),
633731 deleteMany: <TName extends CoreTableName>(
634732 tableName: TName,
@@ -637,20 +735,38 @@ const makeCoreDb = (fuma: ReturnType<typeof makeFumaClient>) => ({ · 2 unmodified lines
637735 fuma.use(`${tableName}.deleteMany`, (db) =>
638736 asLooseStorageDb(db).deleteMany(tableName, options),
639737 ),
640− findFirst: <TName extends CoreTableName, const TOptions extends CoreFindFirstOptions<TName>>(
738+ findFirst: <
739+ TName extends CoreTableName,
740+ const TOptions extends CoreFindFirstOptions<TName>,
741+ >(
641742 tableName: TName,
642743 options: TOptions,
643− ): Effect.Effect<CoreProjectedRow<TName, TOptions["select"]> | null, StorageFailure> =>
744+ ): Effect.Effect<
745+ CoreProjectedRow<TName, TOptions["select"]> | null,
746+ StorageFailure
747+ > =>
644748 fuma.use(`${tableName}.findFirst`, (db) =>
645749 asLooseStorageDb(db).findFirst(tableName, options),
646− ) as Effect.Effect<CoreProjectedRow<TName, TOptions["select"]> | null, StorageFailure>,
647− findMany: <TName extends CoreTableName, const TOptions extends CoreFindManyOptions<TName>>(
750+ ) as Effect.Effect<
751+ CoreProjectedRow<TName, TOptions["select"]> | null,
752+ StorageFailure
753+ >,
754+ findMany: <
755+ TName extends CoreTableName,
756+ const TOptions extends CoreFindManyOptions<TName>,
757+ >(
648758 tableName: TName,
649759 options: TOptions = {} as TOptions,
650− ): Effect.Effect<readonly CoreProjectedRow<TName, TOptions["select"]>[], StorageFailure> =>
760+ ): Effect.Effect<
761+ readonly CoreProjectedRow<TName, TOptions["select"]>[],
762+ StorageFailure
763+ > =>
651764 fuma.use(`${tableName}.findMany`, (db) =>
652765 asLooseStorageDb(db).findMany(tableName, options),
653− ) as Effect.Effect<readonly CoreProjectedRow<TName, TOptions["select"]>[], StorageFailure>,
766+ ) as Effect.Effect<
767+ readonly CoreProjectedRow<TName, TOptions["select"]>[],
768+ StorageFailure
769+ >,
654770 updateMany: <TName extends CoreTableName>(
655771 tableName: TName,
656772 options: {
@@ -670,7 +786,9 @@ type CoreDb = ReturnType<typeof makeCoreDb>; · 13 unmodified lines
670786 // [user, org]; writes/deletes name an explicit owner.
671787 // ---------------------------------------------------------------------------
672788
673−const pluginStorageEntryFromRow = <T>(row: CoreRow<"plugin_storage">): PluginStorageEntry<T> => ({
789+const pluginStorageEntryFromRow = <T>(
790+ row: CoreRow<"plugin_storage">,
791+): PluginStorageEntry<T> => ({
674792 id: pluginStorageId({
675793 pluginId: row.plugin_id,
676794 collection: row.collection,
@@ -681,21 +799,28 @@ const pluginStorageEntryFromRow = <T>(row: CoreRow<"plugin_storage">): PluginSto · 4 unmodified lines
681799 collection: row.collection,
682800 key: row.key,
683801 data: row.data as T,
684− createdAt: row.created_at instanceof Date ? row.created_at : new Date(row.created_at),
685− updatedAt: row.updated_at instanceof Date ? row.updated_at : new Date(row.updated_at),
802+ createdAt:
803+ row.created_at instanceof Date ? row.created_at : new Date(row.created_at),
804+ updatedAt:
805+ row.updated_at instanceof Date ? row.updated_at : new Date(row.updated_at),
686806 });
687807
688−const pluginStorageIndexSpecFields = (spec: PluginStorageRuntimeIndexSpec): readonly string[] =>
689− typeof spec === "string" ? [spec] : spec;
808+const pluginStorageIndexSpecFields = (
809+ spec: PluginStorageRuntimeIndexSpec,
810+): readonly string[] => (typeof spec === "string" ? [spec] : spec);
690811
691812 const pluginStorageCollectionIndexedFields = (
692813 definition: PluginStorageRuntimeCollectionDefinition,
693814 ): ReadonlySet<string> =>
694− new Set(definition.indexes.flatMap((spec) => pluginStorageIndexSpecFields(spec)));
815+ new Set(
816+ definition.indexes.flatMap((spec) => pluginStorageIndexSpecFields(spec)),
817+ );
695818
696819 const pluginStorageQueryValidationError = (
697820 definition: PluginStorageRuntimeCollectionDefinition,
698− query: PluginStorageCollectionQueryInput<PluginStorageCollectionDefinition> | undefined,
821+ query:
822+ | PluginStorageCollectionQueryInput<PluginStorageCollectionDefinition>
823+ | undefined,
699824 ): StorageError | null => {
700825 if (!query) return null;
701826 const indexedFields = pluginStorageCollectionIndexedFields(definition);
@@ -711,13 +836,19 @@ const pluginStorageQueryValidationError = ( · 9 unmodified lines
711836 });
712837 }
713838 }
714− if (query.limit !== undefined && (!Number.isInteger(query.limit) || query.limit < 0)) {
839+ if (
840+ query.limit !== undefined &&
841+ (!Number.isInteger(query.limit) || query.limit < 0)
842+ ) {
715843 return new StorageError({
716844 message: `Plugin storage collection "${definition.name}" received an invalid query limit`,
717845 cause: undefined,
718846 });
719847 }
720− if (query.offset !== undefined && (!Number.isInteger(query.offset) || query.offset < 0)) {
848+ if (
849+ query.offset !== undefined &&
850+ (!Number.isInteger(query.offset) || query.offset < 0)
851+ ) {
721852 return new StorageError({
722853 message: `Plugin storage collection "${definition.name}" received an invalid query offset`,
723854 cause: undefined,
@@ -726,17 +857,35 @@ const pluginStorageQueryValidationError = ( · 2 unmodified lines
726857 return null;
727858 };
728859
729−const isPluginStorageRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
860+const isPluginStorageRecord = (
861+ value: unknown,
862+): value is Readonly<Record<string, unknown>> =>
730863 value !== null && typeof value === "object" && !Array.isArray(value);
731864
732−const pluginStorageWhereOperators = ["eq", "in", "gt", "gte", "lt", "lte"] as const;
865+const pluginStorageWhereOperators = [
866+ "eq",
867+ "in",
868+ "gt",
869+ "gte",
870+ "lt",
871+ "lte",
872+] as const;
733873
734−const isPluginStorageWhereFilter = (value: unknown): value is Readonly<Record<string, unknown>> =>
735− isPluginStorageRecord(value) && pluginStorageWhereOperators.some((operator) => operator in value);
874+const isPluginStorageWhereFilter = (
875+ value: unknown,
876+): value is Readonly<Record<string, unknown>> =>
877+ isPluginStorageRecord(value) &&
878+ pluginStorageWhereOperators.some((operator) => operator in value);
736879
737−const pluginStorageComparableValue = (value: unknown): string | number | boolean | null => {
880+const pluginStorageComparableValue = (
881+ value: unknown,
882+): string | number | boolean | null => {
738883 if (value instanceof Date) return value.getTime();
739− if (typeof value === "number" || typeof value === "string" || typeof value === "boolean") {
884+ if (
885+ typeof value === "number" ||
886+ typeof value === "string" ||
887+ typeof value === "boolean"
888+ ) {
740889 return value;
741890 }
742891 if (value == null) return null;
@@ -755,8 +904,13 @@ const comparePluginStorageValues = (left: unknown, right: unknown): number => { · 12 unmodified lines
755904 const pluginStorageDataField = (data: unknown, field: string): unknown =>
756905 isPluginStorageRecord(data) ? data[field] : undefined;
757906
758−const matchesWhereOperator = (operator: string, value: unknown, operand: unknown): boolean => {
759− if (operator === "eq") return comparePluginStorageValues(value, operand) === 0;
907+const matchesWhereOperator = (
908+ operator: string,
909+ value: unknown,
910+ operand: unknown,
911+): boolean => {
912+ if (operator === "eq")
913+ return comparePluginStorageValues(value, operand) === 0;
760914 if (operator === "in") {
761915 return (
762916 Array.isArray(operand) &&
@@ -764,9 +918,11 @@ const matchesWhereOperator = (operator: string, value: unknown, operand: unknown · 1 unmodified line
764918 );
765919 }
766920 if (operator === "gt") return comparePluginStorageValues(value, operand) > 0;
767− if (operator === "gte") return comparePluginStorageValues(value, operand) >= 0;
921+ if (operator === "gte")
922+ return comparePluginStorageValues(value, operand) >= 0;
768923 if (operator === "lt") return comparePluginStorageValues(value, operand) < 0;
769− if (operator === "lte") return comparePluginStorageValues(value, operand) <= 0;
924+ if (operator === "lte")
925+ return comparePluginStorageValues(value, operand) <= 0;
770926 return false;
771927 };
772928
@@ -802,9 +958,12 @@ const makePluginStorageFacade = (input: { · 29 unmodified lines
802958 readonly owner: OwnerBinding;
803959 }): PluginStorageFacade => {
804960 // Owner partitions: org always, plus this subject's user partition.
805− const readOwners: readonly Owner[] = input.owner.subject == null ? ["org"] : ["user", "org"];
961+ const readOwners: readonly Owner[] =
962+ input.owner.subject == null ? ["org"] : ["user", "org"];
806963
807− const ownerSubject = (owner: Owner): { owner: Owner; subject: string } | null => {
964+ const ownerSubject = (
965+ owner: Owner,
966+ ): { owner: Owner; subject: string } | null => {
808967 if (owner === "org") return { owner: "org", subject: ORG_SUBJECT };
809968 if (input.owner.subject == null) return null;
810969 return { owner: "user", subject: String(input.owner.subject) };
@@ -821,7 +980,11 @@ const makePluginStorageFacade = (input: { · 10 unmodified lines
821980 key === undefined ? true : b("key", "=", key),
822981 );
823982
824− const whereOwner = (owner: Owner, collection: string, key: string): CoreWhere => {
983+ const whereOwner = (
984+ owner: Owner,
985+ collection: string,
986+ key: string,
987+ ): CoreWhere => {
825988 const os = ownerSubject(owner);
826989 return (b: AnyCb) =>
827990 b.and(
@@ -843,19 +1006,28 @@ const makePluginStorageFacade = (input: { · 15 unmodified lines
8431006 });
8441007
8451008 const getVisible = <T>(collection: string, key: string) =>
846− input.core.findMany("plugin_storage", { where: whereFor(collection, key) }).pipe(
847− Effect.map((rows) => sortByOwnerPrecedence(rows)[0] ?? null),
848− Effect.map((row) => (row ? pluginStorageEntryFromRow<T>(row) : null)),
849− );
1009+ input.core
1010+ .findMany("plugin_storage", { where: whereFor(collection, key) })
1011+ .pipe(
1012+ Effect.map((rows) => sortByOwnerPrecedence(rows)[0] ?? null),
1013+ Effect.map((row) => (row ? pluginStorageEntryFromRow<T>(row) : null)),
1014+ );
8501015
8511016 const getForOwnerImpl = <T>(owner: Owner, collection: string, key: string) =>
8521017 input.core
8531018 .findFirst("plugin_storage", {
8541019 where: whereOwner(owner, collection, key),
8551020 })
856− .pipe(Effect.map((row) => (row ? pluginStorageEntryFromRow<T>(row) : null)));
1021+ .pipe(
1022+ Effect.map((row) => (row ? pluginStorageEntryFromRow<T>(row) : null)),
1023+ );
8571024
858− const putImpl = <T>(owner: Owner, collection: string, key: string, data: unknown) =>
1025+ const putImpl = <T>(
1026+ owner: Owner,
1027+ collection: string,
1028+ key: string,
1029+ data: unknown,
1030+ ) =>
8591031 Effect.gen(function* () {
8601032 const os = ownerSubject(owner);
8611033 if (!os) {
@@ -935,7 +1107,10 @@ const makePluginStorageFacade = (input: { · 73 unmodified lines
9351107 offset < uniqueKeys.length;
9361108 offset += PLUGIN_STORAGE_DELETE_KEY_BATCH_SIZE
9371109 ) {
938− const batchKeys = uniqueKeys.slice(offset, offset + PLUGIN_STORAGE_DELETE_KEY_BATCH_SIZE);
1110+ const batchKeys = uniqueKeys.slice(
1111+ offset,
1112+ offset + PLUGIN_STORAGE_DELETE_KEY_BATCH_SIZE,
1113+ );
9391114 yield* input.core.deleteMany("plugin_storage", {
9401115 where: (b) =>
9411116 b.and(
@@ -1013,7 +1188,9 @@ const makePluginStorageFacade = (input: { · 71 unmodified lines
10131188 yield* deleteManyImpl(owner, os.subject, entries);
10141189 });
10151190
1016− const queryCollection = <TDefinition extends PluginStorageCollectionDefinition>(
1191+ const queryCollection = <
1192+ TDefinition extends PluginStorageCollectionDefinition,
1193+ >(
10171194 definition: TDefinition,
10181195 queryInput?: PluginStorageCollectionQueryInput<TDefinition>,
10191196 ) =>
@@ -1031,7 +1208,9 @@ const makePluginStorageFacade = (input: { · 11 unmodified lines
10311208 });
10321209 const filtered = sortByOwnerPrecedence(rows)
10331210 .filter((row) =>
1034− queryInput?.keyPrefix === undefined ? true : row.key.startsWith(queryInput.keyPrefix),
1211+ queryInput?.keyPrefix === undefined
1212+ ? true
1213+ : row.key.startsWith(queryInput.keyPrefix),
10351214 )
10361215 .filter((row) =>
10371216 rowMatchesPluginStorageWhere(
@@ -1053,7 +1232,8 @@ const makePluginStorageFacade = (input: { · 15 unmodified lines
10531232 if (compared !== 0) return compared;
10541233 }
10551234 return (
1056− ownerRank(left.owner as Owner) - ownerRank(right.owner as Owner) ||
1235+ ownerRank(left.owner as Owner) -
1236+ ownerRank(right.owner as Owner) ||
10571237 left.key.localeCompare(right.key)
10581238 );
10591239 })
@@ -1065,7 +1245,9 @@ const makePluginStorageFacade = (input: { · 5 unmodified lines
10651245 ? sorted.slice(offset)
10661246 : sorted.slice(offset, offset + queryInput.limit);
10671247 return limited.map((row) =>
1068− pluginStorageEntryFromRow<PluginStorageCollectionData<TDefinition>>(row),
1248+ pluginStorageEntryFromRow<PluginStorageCollectionData<TDefinition>>(
1249+ row,
1250+ ),
10691251 );
10701252 });
10711253
@@ -1073,15 +1255,24 @@ const makePluginStorageFacade = (input: { · 1 unmodified line
10731255 collection: (definition) => ({
10741256 get: (storageInput) =>
10751257 getVisible(definition.name, storageInput.key) as Effect.Effect<
1076− PluginStorageEntry<PluginStorageCollectionData<typeof definition>> | null,
1258+ PluginStorageEntry<
1259+ PluginStorageCollectionData<typeof definition>
1260+ > | null,
10771261 StorageFailure
10781262 >,
10791263 getForOwner: (storageInput) =>
1080− getForOwnerImpl(storageInput.owner, definition.name, storageInput.key) as Effect.Effect<
1081− PluginStorageEntry<PluginStorageCollectionData<typeof definition>> | null,
1264+ getForOwnerImpl(
1265+ storageInput.owner,
1266+ definition.name,
1267+ storageInput.key,
1268+ ) as Effect.Effect<
1269+ PluginStorageEntry<
1270+ PluginStorageCollectionData<typeof definition>
1271+ > | null,
10821272 StorageFailure
10831273 >,
1084− list: (storageInput) => queryCollection(definition, { keyPrefix: storageInput?.keyPrefix }),
1274+ list: (storageInput) =>
1275+ queryCollection(definition, { keyPrefix: storageInput?.keyPrefix }),
10851276 put: (storageInput) =>
10861277 putImpl(
10871278 storageInput.owner,
@@ -1094,12 +1285,20 @@ const makePluginStorageFacade = (input: { · 6 unmodified lines
10941285 >,
10951286 query: (storageInput) => queryCollection(definition, storageInput),
10961287 count: (storageInput) =>
1097− queryCollection(definition, storageInput).pipe(Effect.map((rows) => rows.length)),
1098− remove: (storageInput) => removeImpl(storageInput.owner, definition.name, storageInput.key),
1288+ queryCollection(definition, storageInput).pipe(
1289+ Effect.map((rows) => rows.length),
1290+ ),
1291+ remove: (storageInput) =>
1292+ removeImpl(storageInput.owner, definition.name, storageInput.key),
10991293 }),
1100− get: (storageInput) => getVisible(storageInput.collection, storageInput.key),
1294+ get: (storageInput) =>
1295+ getVisible(storageInput.collection, storageInput.key),
11011296 getForOwner: (storageInput) =>
1102− getForOwnerImpl(storageInput.owner, storageInput.collection, storageInput.key),
1297+ getForOwnerImpl(
1298+ storageInput.owner,
1299+ storageInput.collection,
1300+ storageInput.key,
1301+ ),
11031302 list: (storageInput) =>
11041303 Effect.gen(function* () {
11051304 const rows = yield* input.core.findMany("plugin_storage", {
@@ -1114,11 +1313,18 @@ const makePluginStorageFacade = (input: { · 8 unmodified lines
11141313 .map((row) => pluginStorageEntryFromRow(row));
11151314 }),
11161315 put: (storageInput) =>
1117− putImpl(storageInput.owner, storageInput.collection, storageInput.key, storageInput.data),
1118− putMany: (storageInput) => putManyImpl(storageInput.owner, storageInput.entries),
1316+ putImpl(
1317+ storageInput.owner,
1318+ storageInput.collection,
1319+ storageInput.key,
1320+ storageInput.data,
1321+ ),
1322+ putMany: (storageInput) =>
1323+ putManyImpl(storageInput.owner, storageInput.entries),
11191324 remove: (storageInput) =>
11201325 removeImpl(storageInput.owner, storageInput.collection, storageInput.key),
1121− removeMany: (storageInput) => removeManyImpl(storageInput.owner, storageInput.entries),
1326+ removeMany: (storageInput) =>
1327+ removeManyImpl(storageInput.owner, storageInput.entries),
11221328 };
11231329 };
11241330
@@ -1161,7 +1367,9 @@ const EXECUTOR_SOURCE: StaticSourceDecl = { · 36 unmodified lines
11611367 tools: [],
11621368 };
11631369
1164−const isReadonlyRecord = (value: unknown): value is Readonly<Record<PropertyKey, unknown>> =>
1370+const isReadonlyRecord = (
1371+ value: unknown,
1372+): value is Readonly<Record<PropertyKey, unknown>> =>
11651373 typeof value === "object" && value !== null;
11661374
11671375 type StandardJsonSchemaSide = "input" | "output";
@@ -1180,10 +1388,14 @@ const staticToolSchemaRoot = ( · 12 unmodified lines
11801388 const jsonSchema = standard["jsonSchema"];
11811389 if (!isReadonlyRecord(jsonSchema)) return schema;
11821390 const materialize = (jsonSchema as StandardJsonSchemaFns)[side];
1183− return typeof materialize === "function" ? materialize({ target: "draft-07" }) : jsonSchema;
1391+ return typeof materialize === "function"
1392+ ? materialize({ target: "draft-07" })
1393+ : jsonSchema;
11841394 };
11851395
1186−export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = readonly []>(
1396+export const createExecutor = <
1397+ const TPlugins extends readonly AnyPlugin[] = readonly [],
1398+>(
11871399 config: ExecutorConfig<TPlugins>,
11881400 ): Effect.Effect<Executor<TPlugins>, StorageFailure> =>
11891401 Effect.gen(function* () {
@@ -1213,7 +1425,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 23 unmodified lines
12131425 return { tenant, owner, subject };
12141426 };
12151427
1216− const requireUserSubject = (owner: Owner): Effect.Effect<void, StorageFailure> =>
1428+ const requireUserSubject = (
1429+ owner: Owner,
1430+ ): Effect.Effect<void, StorageFailure> =>
12171431 owner === "user" && subject == null
12181432 ? Effect.fail(
12191433 new StorageError({
@@ -1236,7 +1450,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 16 unmodified lines
12361450
12371451 const tables = yield* Effect.try({
12381452 try: () => collectTables(),
1239− catch: (cause) => storageFailureFromUnknown("Failed to collect executor tables", cause),
1453+ catch: (cause) =>
1454+ storageFailureFromUnknown("Failed to collect executor tables", cause),
12401455 });
12411456 const dbInput = yield* Effect.suspend(() => {
12421457 if (!config.db) return Effect.succeed(createDefaultMemoryDb(tables));
@@ -1251,7 +1466,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 8 unmodified lines
12511466 validateExecutorDbTables(tables, rootDbUntyped.internal.tables);
12521467 validateExecutorOwnerPolicyTables(rootDbUntyped.internal.tables);
12531468 },
1254− catch: (cause) => storageFailureFromUnknown("Failed to validate executor tables", cause),
1469+ catch: (cause) =>
1470+ storageFailureFromUnknown("Failed to validate executor tables", cause),
12551471 });
12561472
12571473 const ownerContext: ExecutorOwnerPolicyContext = { tenant, subject };
@@ -1259,7 +1475,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 1 unmodified line
12591475 const fuma = makeFumaClient(rootDb);
12601476 const core = makeCoreDb(fuma);
12611477 const blobs = config.blobs ?? makeFumaBlobStore(fuma);
1262− const transaction = <A, E>(effect: Effect.Effect<A, E>) => fuma.transaction(effect);
1478+ const transaction = <A, E>(effect: Effect.Effect<A, E>) =>
1479+ fuma.transaction(effect);
12631480
12641481 // Populated once, never mutated after startup.
12651482 const staticTools = new Map<string, StaticTools>();
@@ -1270,7 +1487,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 4 unmodified lines
12701487
12711488 const staticToolOwner = (): Owner => (subject == null ? "org" : "user");
12721489 const staticToolConnection = (source: StaticSourceDecl): ConnectionName =>
1273− ConnectionName.make(source.id === EXECUTOR_SOURCE_ID ? "coreTools" : "static");
1490+ ConnectionName.make(
1491+ source.id === EXECUTOR_SOURCE_ID ? "coreTools" : "static",
1492+ );
12741493
12751494 const staticSources = (): readonly StaticSourceDecl[] => {
12761495 const byId = new Map<string, StaticSourceDecl>();
@@ -1280,7 +1499,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 3 unmodified lines
12801499 return [...byId.values()];
12811500 };
12821501
1283− const staticSourceToIntegration = (source: StaticSourceDecl): Integration => ({
1502+ const staticSourceToIntegration = (
1503+ source: StaticSourceDecl,
1504+ ): Integration => ({
12841505 slug: IntegrationSlug.make(source.id),
12851506 name: source.name,
12861507 description: source.name,
@@ -1336,6 +1557,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 49 unmodified lines
13361557 };
13371558
13381559 const extensions: Record<string, object> = {};
1560+ // Generic executor-wrapper hook: a plugin may expose `wrapExecutor` on its
1561+ // extension to narrow the executor per request (the toolkits plugin does).
1562+ // Collected here, run by the execution stack — core stays domain-agnostic.
1563+ const executorWrappers: ExecutorWrapper[] = [];
13391564
13401565 // ------------------------------------------------------------------
13411566 // Owner condition builders. The owner policy already restricts reads to
@@ -1389,7 +1614,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 47 unmodified lines
13891614 const performTokenRefresh = (
13901615 row: ConnectionRow,
13911616 provider: CredentialProvider,
1392− ): Effect.Effect<string | null, StorageFailure | CredentialResolutionError> =>
1617+ ): Effect.Effect<
1618+ string | null,
1619+ StorageFailure | CredentialResolutionError
1620+ > =>
13931621 Effect.gen(function* () {
13941622 const owner = row.owner as Owner;
13951623 const reauth = (message: string): CredentialResolutionError =>
@@ -1404,15 +1632,21 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 8 unmodified lines
14041632 // Load the backing app by the owner STORED on the connection (a Personal
14051633 // connection may be backed by a shared Workspace app) — no derivation.
14061634 const clientOwner = (row.oauth_client_owner ?? row.owner) as Owner;
1407− const clientRow = yield* loadOAuthClientRow(clientOwner, String(row.oauth_client));
1635+ const clientRow = yield* loadOAuthClientRow(
1636+ clientOwner,
1637+ String(row.oauth_client),
1638+ );
14081639 if (!clientRow) {
1409− return yield* reauth(`OAuth client "${row.oauth_client}" is no longer registered.`);
1640+ return yield* reauth(
1641+ `OAuth client "${row.oauth_client}" is no longer registered.`,
1642+ );
14101643 }
14111644
14121645 // The secret is stored in the provider (a vault item id), not inline.
14131646 const clientSecret = clientRow.client_secret_item_id
1414− ? ((yield* provider.get(ProviderItemId.make(String(clientRow.client_secret_item_id)))) ??
1415− "")
1647+ ? ((yield* provider.get(
1648+ ProviderItemId.make(String(clientRow.client_secret_item_id)),
1649+ )) ?? "")
14161650 : "";
14171651 // Re-request the scopes this connection was GRANTED (RFC 6749 §6: a
14181652 // refresh must not exceed the originally-granted scope). Empty → omit
@@ -1433,7 +1667,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 14 unmodified lines
14331667 clientId: String(clientRow.client_id),
14341668 clientSecret,
14351669 scopes: grantedScopes,
1436− resource: clientRow.resource ? String(clientRow.resource) : undefined,
1670+ resource: clientRow.resource
1671+ ? String(clientRow.resource)
1672+ : undefined,
14371673 endpointUrlPolicy: config.oauthEndpointUrlPolicy,
14381674 }).pipe(
14391675 // A client_credentials failure is never a rotated-refresh-token
@@ -1451,11 +1687,17 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 11 unmodified lines
14511687 )
14521688 : yield* Effect.gen(function* () {
14531689 if (!row.refresh_item_id) {
1454− return yield* reauth("No refresh token is stored for this connection.");
1690+ return yield* reauth(
1691+ "No refresh token is stored for this connection.",
1692+ );
14551693 }
1456− const refreshToken = yield* provider.get(ProviderItemId.make(row.refresh_item_id));
1694+ const refreshToken = yield* provider.get(
1695+ ProviderItemId.make(row.refresh_item_id),
1696+ );
14571697 if (!refreshToken) {
1458− return yield* reauth("Stored refresh token could not be resolved.");
1698+ return yield* reauth(
1699+ "Stored refresh token could not be resolved.",
1700+ );
14591701 }
14601702 return yield* refreshAccessToken({
14611703 tokenUrl: String(clientRow.token_url),
@@ -1465,7 +1707,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 3 unmodified lines
14651707 scopes: grantedScopes,
14661708 // RFC 8707: keep the re-minted token bound to the same resource
14671709 // (MCP servers require this on refresh).
1468− resource: clientRow.resource ? String(clientRow.resource) : undefined,
1710+ resource: clientRow.resource
1711+ ? String(clientRow.resource)
1712+ : undefined,
14691713 endpointUrlPolicy: config.oauthEndpointUrlPolicy,
14701714 }).pipe(
14711715 Effect.mapError((cause) =>
@@ -1489,14 +1733,22 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 17 unmodified lines
14891733 const tokenItemId =
14901734 connectionItemIds(row)[PRIMARY_INPUT_VARIABLE] ??
14911735 `connection:${row.owner}:${row.integration}:${row.name}:${PRIMARY_INPUT_VARIABLE}`;
1492− yield* provider.set(ProviderItemId.make(tokenItemId), token.access_token);
1736+ yield* provider.set(
1737+ ProviderItemId.make(tokenItemId),
1738+ token.access_token,
1739+ );
14931740 if (token.refresh_token && row.refresh_item_id) {
1494− yield* provider.set(ProviderItemId.make(row.refresh_item_id), token.refresh_token);
1741+ yield* provider.set(
1742+ ProviderItemId.make(row.refresh_item_id),
1743+ token.refresh_token,
1744+ );
14951745 }
14961746 }
14971747
14981748 const nextExpiresAt =
1499− typeof token.expires_in === "number" ? Date.now() + token.expires_in * 1000 : null;
1749+ typeof token.expires_in === "number"
1750+ ? Date.now() + token.expires_in * 1000
1751+ : null;
15001752 const set: Record<string, unknown> = {
15011753 expires_at: nextExpiresAt,
15021754 updated_at: new Date(),
@@ -1518,7 +1770,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 15 unmodified lines
15181770 const refreshConnectionToken = (
15191771 row: ConnectionRow,
15201772 provider: CredentialProvider,
1521− ): Effect.Effect<string | null, StorageFailure | CredentialResolutionError> =>
1773+ ): Effect.Effect<
1774+ string | null,
1775+ StorageFailure | CredentialResolutionError
1776+ > =>
15221777 // Share a single refresh per connection so concurrent resolves of the same
15231778 // connection all await one refresh-token grant (the AS rotates the refresh
15241779 // token; parallel grants would race on a consumed token — v1's refresh
@@ -1530,7 +1785,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 5 unmodified lines
15301785 if (existing) return yield* existing;
15311786 // `Effect.cached` memoizes the grant onto a deferred: it runs once and
15321787 // replays to every awaiter sharing this entry.
1533− const memoized = yield* Effect.cached(performTokenRefresh(row, provider));
1788+ const memoized = yield* Effect.cached(
1789+ performTokenRefresh(row, provider),
1790+ );
15341791 const gated = memoized.pipe(
15351792 Effect.ensuring(Effect.sync(() => refreshInFlight.delete(key))),
15361793 );
@@ -1547,7 +1804,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 10 unmodified lines
15471804 // first (always single-input → `{ token: <access> }`).
15481805 const resolveConnectionValues = (
15491806 row: ConnectionRow,
1550− ): Effect.Effect<Record<string, string | null>, StorageFailure | CredentialResolutionError> =>
1807+ ): Effect.Effect<
1808+ Record<string, string | null>,
1809+ StorageFailure | CredentialResolutionError
1810+ > =>
15511811 Effect.gen(function* () {
15521812 const provider = credentialProviders.get(row.provider);
15531813 if (!provider) {
@@ -1557,13 +1817,16 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 3 unmodified lines
15571817 }
15581818 // OAuth connections refresh their access token before resolving when
15591819 // it has expired (or is within the skew window).
1560− const expiresAt = row.expires_at == null ? null : Number(row.expires_at);
1820+ const expiresAt =
1821+ row.expires_at == null ? null : Number(row.expires_at);
15611822 if (row.oauth_client != null && shouldRefreshToken({ expiresAt })) {
15621823 const access = yield* refreshConnectionToken(row, provider);
15631824 return { [PRIMARY_INPUT_VARIABLE]: access };
15641825 }
15651826 const out: Record<string, string | null> = {};
1566− for (const [variable, itemId] of Object.entries(connectionItemIds(row))) {
1827+ for (const [variable, itemId] of Object.entries(
1828+ connectionItemIds(row),
1829+ )) {
15671830 out[variable] = yield* provider.get(ProviderItemId.make(itemId));
15681831 }
15691832 return out;
@@ -1584,7 +1847,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 14 unmodified lines
15841847 * callers that only ever need one value. */
15851848 const resolveConnectionValue = (
15861849 row: ConnectionRow,
1587− ): Effect.Effect<string | null, StorageFailure | CredentialResolutionError> =>
1850+ ): Effect.Effect<
1851+ string | null,
1852+ StorageFailure | CredentialResolutionError
1853+ > =>
15881854 resolveConnectionValues(row).pipe(
15891855 Effect.map((values) => values[PRIMARY_INPUT_VARIABLE] ?? null),
15901856 );
@@ -1644,7 +1910,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 53 unmodified lines
16441910 // plugin's `describeAuthMethods` hook. The hook is plugin-authored, so a
16451911 // throw (malformed config it didn't guard) degrades to `[]` rather than
16461912 // failing the catalog read.
1647− const describeAuthMethodsForRow = (row: IntegrationRow): readonly AuthMethodDescriptor[] => {
1913+ const describeAuthMethodsForRow = (
1914+ row: IntegrationRow,
1915+ ): readonly AuthMethodDescriptor[] => {
16481916 const runtime = runtimes.get(row.plugin_id);
16491917 const describe = runtime?.plugin.describeAuthMethods;
16501918 if (!describe) return [];
@@ -1657,7 +1925,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 6 unmodified lines
16571925 }
16581926 };
16591927
1660− const describeDisplayUrlForRow = (row: IntegrationRow): string | undefined => {
1928+ const describeDisplayUrlForRow = (
1929+ row: IntegrationRow,
1930+ ): string | undefined => {
16611931 const runtime = runtimes.get(row.plugin_id);
16621932 const describe = runtime?.plugin.describeIntegrationDisplay;
16631933 if (!describe) return undefined;
@@ -1671,14 +1941,21 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 7 unmodified lines
16711941 }
16721942 };
16731943
1674− const integrationsList = (): Effect.Effect<readonly Integration[], StorageFailure> =>
1944+ const integrationsList = (): Effect.Effect<
1945+ readonly Integration[],
1946+ StorageFailure
1947+ > =>
16751948 core
16761949 .findMany("integration", {})
16771950 .pipe(
16781951 Effect.map((rows) => [
16791952 ...staticSources().map(staticSourceToIntegration),
16801953 ...rows.map((row) =>
1681− rowToIntegration(row, describeAuthMethodsForRow(row), describeDisplayUrlForRow(row)),
1954+ rowToIntegration(
1955+ row,
1956+ describeAuthMethodsForRow(row),
1957+ describeDisplayUrlForRow(row),
1958+ ),
16821959 ),
16831960 ]),
16841961 );
@@ -1687,11 +1964,17 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 2 unmodified lines
16871964 slug: IntegrationSlug,
16881965 ): Effect.Effect<Integration | null, StorageFailure> =>
16891966 Effect.gen(function* () {
1690− const staticSource = staticSources().find((source) => source.id === String(slug));
1967+ const staticSource = staticSources().find(
1968+ (source) => source.id === String(slug),
1969+ );
16911970 if (staticSource) return staticSourceToIntegration(staticSource);
16921971 const row = yield* findIntegrationRow(slug);
16931972 return row
1694− ? rowToIntegration(row, describeAuthMethodsForRow(row), describeDisplayUrlForRow(row))
1973+ ? rowToIntegration(
1974+ row,
1975+ describeAuthMethodsForRow(row),
1976+ describeDisplayUrlForRow(row),
1977+ )
16951978 : null;
16961979 });
16971980
@@ -1700,7 +1983,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 2 unmodified lines
17001983 ): Effect.Effect<IntegrationRecord | null, StorageFailure> =>
17011984 findIntegrationRow(slug).pipe(
17021985 Effect.map((row) =>
1703− row ? rowToIntegrationRecord(row, describeAuthMethodsForRow(row)) : null,
1986+ row
1987+ ? rowToIntegrationRecord(row, describeAuthMethodsForRow(row))
1988+ : null,
17041989 ),
17051990 );
17061991
@@ -1755,7 +2040,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 48 unmodified lines
17552040 const now = new Date();
17562041 const set: Record<string, unknown> = { updated_at: now };
17572042 if (patch.name !== undefined) set.name = patch.name;
1758− if (patch.description !== undefined) set.description = patch.description;
2043+ if (patch.description !== undefined)
2044+ set.description = patch.description;
17592045 if (patch.config !== undefined) {
17602046 set.config = patch.config;
17612047 // A config change can change the derived tools. The writer can only
@@ -1782,7 +2068,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 20 unmodified lines
17822068
17832069 const integrationsRemove = (
17842070 slug: IntegrationSlug,
1785− ): Effect.Effect<void, IntegrationRemovalNotAllowedError | StorageFailure> =>
2071+ ): Effect.Effect<
2072+ void,
2073+ IntegrationRemovalNotAllowedError | StorageFailure
2074+ > =>
17862075 transaction(
17872076 Effect.gen(function* () {
17882077 const existing = yield* findIntegrationRow(slug);
@@ -1811,7 +2100,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 22 unmodified lines
18112100 const result = yield* runtime.plugin
18122101 .detect({ ctx: runtime.ctx, url })
18132102 .pipe(
1814− Effect.mapError((cause) => pluginStorageFailure(runtime.plugin.id, "detect", cause)),
2103+ Effect.mapError((cause) =>
2104+ pluginStorageFailure(runtime.plugin.id, "detect", cause),
2105+ ),
18152106 );
18162107 if (result) results.push(result);
18172108 }
@@ -1825,7 +2116,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 7 unmodified lines
18252116 const produceConnectionTools = (
18262117 integrationRow: IntegrationRow,
18272118 ref: ConnectionRef,
1828− ): Effect.Effect<readonly Tool[], IntegrationNotFoundError | StorageFailure> =>
2119+ ): Effect.Effect<
2120+ readonly Tool[],
2121+ IntegrationNotFoundError | StorageFailure
2122+ > =>
18292123 Effect.gen(function* () {
18302124 const runtime = runtimes.get(integrationRow.plugin_id);
18312125 const keys = yield* Effect.try({
@@ -1894,14 +2188,20 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 62 unmodified lines
18942188 integration: rowToIntegration(integrationRow),
18952189 config: decodeJsonColumn(integrationRow.config),
18962190 connection: ref,
1897− template: existingRow ? AuthTemplateSlug.make(existingRow.template) : null,
2191+ template: existingRow
2192+ ? AuthTemplateSlug.make(existingRow.template)
2193+ : null,
18982194 storage: runtime.storage,
18992195 getValue: () => resolveConnectionValueByRef(ref),
19002196 getValues: () => resolveConnectionValuesByRef(ref),
19012197 })
19022198 .pipe(
19032199 Effect.mapError((cause) =>
1904− pluginStorageFailure(integrationRow.plugin_id, "resolveTools", cause),
2200+ pluginStorageFailure(
2201+ integrationRow.plugin_id,
2202+ "resolveTools",
2203+ cause,
2204+ ),
19052205 ),
19062206 );
19072207
@@ -1922,17 +2222,19 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 14 unmodified lines
19222222 updated_at: now,
19232223 }));
19242224
1925− const definitionRows = Object.entries(result.definitions ?? {}).map(([name, schema]) => ({
1926− tenant: keys.tenant,
1927− owner: keys.owner,
1928− subject: keys.subject,
1929− integration: String(ref.integration),
1930− connection: String(ref.name),
1931− plugin_id: integrationRow.plugin_id,
1932− name,
1933− schema,
1934− created_at: now,
1935− }));
2225+ const definitionRows = Object.entries(result.definitions ?? {}).map(
2226+ ([name, schema]) => ({
2227+ tenant: keys.tenant,
2228+ owner: keys.owner,
2229+ subject: keys.subject,
2230+ integration: String(ref.integration),
2231+ connection: String(ref.name),
2232+ plugin_id: integrationRow.plugin_id,
2233+ name,
2234+ schema,
2235+ created_at: now,
2236+ }),
2237+ );
19362238
19372239 yield* transaction(
19382240 Effect.gen(function* () {
@@ -2024,16 +2326,20 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 85 unmodified lines
20242326 const itemIds: Record<string, string> = {};
20252327 if (external.length > 0 && pasted.length > 0) {
20262328 return yield* new InvalidConnectionInputError({
2027− message: "A connection cannot mix pasted and external-provider inputs.",
2329+ message:
2330+ "A connection cannot mix pasted and external-provider inputs.",
20282331 });
20292332 }
20302333 if (external.length > 0) {
20312334 const providers = new Set(
2032− external.map((i) => ("from" in i.origin ? String(i.origin.from.provider) : "")),
2335+ external.map((i) =>
2336+ "from" in i.origin ? String(i.origin.from.provider) : "",
2337+ ),
20332338 );
20342339 if (providers.size > 1) {
20352340 return yield* new InvalidConnectionInputError({
2036− message: "A connection's inputs must all use the same external provider.",
2341+ message:
2342+ "A connection's inputs must all use the same external provider.",
20372343 });
20382344 }
20392345 const [only] = [...providers];
@@ -2045,7 +2351,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 5 unmodified lines
20452351 }
20462352 providerKey = only ?? "";
20472353 for (const i of external) {
2048− if ("from" in i.origin) itemIds[i.variable] = String(i.origin.from.id);
2354+ if ("from" in i.origin)
2355+ itemIds[i.variable] = String(i.origin.from.id);
20492356 }
20502357 } else {
20512358 const provider = defaultWritableProvider();
@@ -2083,7 +2390,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 31 unmodified lines
20832390 identity_label: input.identityLabel ?? null,
20842391 // Re-saving a credential keeps an existing curated description
20852392 // unless the caller explicitly provides one.
2086− ...(input.description !== undefined ? { description: input.description } : {}),
2393+ ...(input.description !== undefined
2394+ ? { description: input.description }
2395+ : {}),
20872396 updated_at: now,
20882397 };
20892398 if (existing) {
@@ -2127,7 +2436,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 37 unmodified lines
21272436 };
21282437 // Produce + persist tools for the new connection.
21292438 yield* produceConnectionTools(integrationRow, ref).pipe(
2130− Effect.catchTag("IntegrationNotFoundError", () => Effect.succeed([] as readonly Tool[])),
2439+ Effect.catchTag("IntegrationNotFoundError", () =>
2440+ Effect.succeed([] as readonly Tool[]),
2441+ ),
21312442 );
21322443
21332444 const row = yield* findConnectionRow(ref);
@@ -2236,7 +2547,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 102 unmodified lines
22362547 // Produce + persist tools for the minted connection (same path
22372548 // connections.create uses).
22382549 yield* produceConnectionTools(integrationRow, ref).pipe(
2239− Effect.catchTag("IntegrationNotFoundError", () => Effect.succeed([] as readonly Tool[])),
2550+ Effect.catchTag("IntegrationNotFoundError", () =>
2551+ Effect.succeed([] as readonly Tool[]),
2552+ ),
22402553 );
22412554
22422555 const row = yield* findConnectionRow(ref);
@@ -2275,13 +2588,19 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 32 unmodified lines
22752588 filter?.integration === undefined
22762589 ? true
22772590 : b("integration", "=", String(filter.integration)),
2278− filter?.owner === undefined ? true : b("owner", "=", filter.owner),
2591+ filter?.owner === undefined
2592+ ? true
2593+ : b("owner", "=", filter.owner),
22792594 ),
22802595 })
22812596 .pipe(Effect.map((rows) => rows.map(rowToConnection)));
22822597
2283− const connectionsGet = (ref: ConnectionRef): Effect.Effect<Connection | null, StorageFailure> =>
2284− findConnectionRow(ref).pipe(Effect.map((row) => (row ? rowToConnection(row) : null)));
2598+ const connectionsGet = (
2599+ ref: ConnectionRef,
2600+ ): Effect.Effect<Connection | null, StorageFailure> =>
2601+ findConnectionRow(ref).pipe(
2602+ Effect.map((row) => (row ? rowToConnection(row) : null)),
2603+ );
22852604
22862605 const connectionsUpdate = (
22872606 ref: ConnectionRef,
@@ -2297,8 +2616,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 9 unmodified lines
22972616 });
22982617 }
22992618 const set: Record<string, unknown> = { updated_at: new Date() };
2300− if (input.description !== undefined) set.description = input.description;
2301− if (input.identityLabel !== undefined) set.identity_label = input.identityLabel;
2619+ if (input.description !== undefined)
2620+ set.description = input.description;
2621+ if (input.identityLabel !== undefined)
2622+ set.identity_label = input.identityLabel;
23022623 yield* core.updateMany("connection", {
23032624 where: (b: AnyCb) =>
23042625 b.and(
@@ -2326,7 +2647,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 21 unmodified lines
23262647 });
23272648 }
23282649 const integrationRow = yield* findIntegrationRow(ref.integration);
2329− const runtime = integrationRow ? runtimes.get(integrationRow.plugin_id) : undefined;
2650+ const runtime = integrationRow
2651+ ? runtimes.get(integrationRow.plugin_id)
2652+ : undefined;
23302653 if (integrationRow && runtime?.plugin.removeConnection) {
23312654 yield* runtime.plugin
23322655 .removeConnection({
@@ -2336,7 +2659,11 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 3 unmodified lines
23362659 })
23372660 .pipe(
23382661 Effect.mapError((cause) =>
2339− pluginStorageFailure(integrationRow.plugin_id, "removeConnection", cause),
2662+ pluginStorageFailure(
2663+ integrationRow.plugin_id,
2664+ "removeConnection",
2665+ cause,
2666+ ),
23402667 ),
23412668 );
23422669 }
@@ -2385,11 +2712,23 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 42 unmodified lines
23852712 // Tools (read surface)
23862713 // ------------------------------------------------------------------
23872714
2388− const matchesToolFilter = (tool: Tool, filter: ToolListFilter | undefined): boolean => {
2715+ const matchesToolFilter = (
2716+ tool: Tool,
2717+ filter: ToolListFilter | undefined,
2718+ ): boolean => {
23892719 if (!filter) return true;
2390− if (filter.integration !== undefined && tool.integration !== filter.integration) return false;
2391− if (filter.owner !== undefined && tool.owner !== filter.owner) return false;
2392− if (filter.connection !== undefined && tool.connection !== filter.connection) return false;
2720+ if (
2721+ filter.integration !== undefined &&
2722+ tool.integration !== filter.integration
2723+ )
2724+ return false;
2725+ if (filter.owner !== undefined && tool.owner !== filter.owner)
2726+ return false;
2727+ if (
2728+ filter.connection !== undefined &&
2729+ tool.connection !== filter.connection
2730+ )
2731+ return false;
23932732 if (filter.query !== undefined) {
23942733 const q = filter.query.toLowerCase();
23952734 const hay = `${tool.name} ${tool.description}`.toLowerCase();
@@ -2410,18 +2749,25 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 14 unmodified lines
24102749 });
24112750 if (revised.length === 0) return;
24122751 const revisedAt = new Map(
2413− revised.map((row) => [row.slug, Number(row.config_revised_at)] as const),
2752+ revised.map(
2753+ (row) => [row.slug, Number(row.config_revised_at)] as const,
2754+ ),
24142755 );
24152756 const connections = yield* core.findMany("connection", {
2416− where: (b: AnyCb) => b.or(...revised.map((row) => b("integration", "=", row.slug))),
2757+ where: (b: AnyCb) =>
2758+ b.or(...revised.map((row) => b("integration", "=", row.slug))),
24172759 });
24182760 for (const connection of connections) {
24192761 const revisedTime = revisedAt.get(connection.integration);
24202762 if (revisedTime === undefined) continue;
24212763 const syncedAt =
2422− connection.tools_synced_at == null ? 0 : Number(connection.tools_synced_at);
2764+ connection.tools_synced_at == null
2765+ ? 0
2766+ : Number(connection.tools_synced_at);
24232767 if (syncedAt >= revisedTime) continue;
2424− const integrationRow = revised.find((row) => row.slug === connection.integration);
2768+ const integrationRow = revised.find(
2769+ (row) => row.slug === connection.integration,
2770+ );
24252771 if (!integrationRow) continue;
24262772 yield* produceConnectionTools(integrationRow, {
24272773 owner: connection.owner as Owner,
@@ -2439,7 +2785,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 11 unmodified lines
24392785 }
24402786 });
24412787
2442− const toolsList = (filter?: ToolListFilter): Effect.Effect<readonly Tool[], StorageFailure> =>
2788+ const toolsList = (
2789+ filter?: ToolListFilter,
2790+ ): Effect.Effect<readonly Tool[], StorageFailure> =>
24432791 Effect.gen(function* () {
24442792 yield* syncStaleConnectionTools;
24452793 // Projected: the list surface is metadata (address, description,
@@ -2451,7 +2799,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 5 unmodified lines
24512799 filter?.integration === undefined
24522800 ? true
24532801 : b("integration", "=", String(filter.integration)),
2454− filter?.owner === undefined ? true : b("owner", "=", filter.owner),
2802+ filter?.owner === undefined
2803+ ? true
2804+ : b("owner", "=", filter.owner),
24552805 filter?.connection === undefined
24562806 ? true
24572807 : b("connection", "=", String(filter.connection)),
@@ -2507,7 +2857,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 49 unmodified lines
25072857 defs: new Map(),
25082858 }),
25092859 catch: (cause) =>
2510− storageFailureFromUnknown("Failed to build static tool TypeScript preview", cause),
2860+ storageFailureFromUnknown(
2861+ "Failed to build static tool TypeScript preview",
2862+ cause,
2863+ ),
25112864 }).pipe(Effect.option);
25122865 return ToolSchemaView.make({
25132866 address,
@@ -2517,7 +2870,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 3 unmodified lines
25172870 outputSchema: tool.outputSchema,
25182871 inputTypeScript: Option.getOrUndefined(preview)?.inputTypeScript,
25192872 outputTypeScript: Option.getOrUndefined(preview)?.outputTypeScript,
2520− typeScriptDefinitions: Option.getOrUndefined(preview)?.typeScriptDefinitions,
2873+ typeScriptDefinitions:
2874+ Option.getOrUndefined(preview)?.typeScriptDefinitions,
25212875 });
25222876 }
25232877
@@ -2544,7 +2898,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 20 unmodified lines
25442898 ),
25452899 });
25462900 const defs = new Map<string, unknown>();
2547− for (const def of definitionRows) defs.set(def.name, decodeJsonColumn(def.schema));
2901+ for (const def of definitionRows)
2902+ defs.set(def.name, decodeJsonColumn(def.schema));
25482903
25492904 const referenced = collectReferencedDefinitions(
25502905 [tool.inputSchema, tool.outputSchema],
@@ -2558,7 +2913,10 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 7 unmodified lines
25582913 defs,
25592914 }),
25602915 catch: (cause) =>
2561− storageFailureFromUnknown("Failed to build tool TypeScript preview", cause),
2916+ storageFailureFromUnknown(
2917+ "Failed to build tool TypeScript preview",
2918+ cause,
2919+ ),
25622920 }).pipe(Effect.option);
25632921
25642922 const view = preview;
@@ -2574,7 +2932,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 9 unmodified lines
25742932 : undefined,
25752933 inputTypeScript: Option.getOrUndefined(view)?.inputTypeScript,
25762934 outputTypeScript: Option.getOrUndefined(view)?.outputTypeScript,
2577− typeScriptDefinitions: Option.getOrUndefined(view)?.typeScriptDefinitions,
2935+ typeScriptDefinitions:
2936+ Option.getOrUndefined(view)?.typeScriptDefinitions,
25782937 });
25792938 });
25802939
@@ -2583,7 +2942,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 2 unmodified lines
25832942 // ------------------------------------------------------------------
25842943
25852944 const providersList = (): Effect.Effect<readonly ProviderKey[]> =>
2586− Effect.sync(() => credentialProviderOrder.map((key) => ProviderKey.make(key)));
2945+ Effect.sync(() =>
2946+ credentialProviderOrder.map((key) => ProviderKey.make(key)),
2947+ );
25872948
25882949 const providersItems = (
25892950 key: ProviderKey,
@@ -2609,13 +2970,20 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 19 unmodified lines
26092970 ? String(tool.address)
26102971 : `${tool.integration}.${tool.owner}.${tool.connection}.${tool.name}`;
26112972
2612− const policiesList = (): Effect.Effect<readonly ToolPolicy[], StorageFailure> =>
2973+ const policiesList = (): Effect.Effect<
2974+ readonly ToolPolicy[],
2975+ StorageFailure
2976+ > =>
26132977 core
26142978 .findMany("tool_policy", {})
26152979 .pipe(
26162980 Effect.map((rows) =>
26172981 [...rows]
2618− .sort((a, b) => ownerRankForRow(a) - ownerRankForRow(b) || comparePolicyRow(a, b))
2982+ .sort(
2983+ (a, b) =>
2984+ ownerRankForRow(a) - ownerRankForRow(b) ||
2985+ comparePolicyRow(a, b),
2986+ )
26192987 .map(rowToToolPolicy),
26202988 ),
26212989 );
@@ -2648,7 +3016,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 26 unmodified lines
26483016 .map((row) => row.position)
26493017 .sort()
26503018 .at(0);
2651− const position = input.position ?? generateKeyBetween(null, minPosition ?? null);
3019+ const position =
3020+ input.position ?? generateKeyBetween(null, minPosition ?? null);
26523021 const id = PolicyId.make(
26533022 `pol_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`,
26543023 );
@@ -2677,7 +3046,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 22 unmodified lines
26773046 cause: undefined,
26783047 });
26793048 }
2680− const where = (b: AnyCb) => b.and(byOwner(input.owner)(b), b("id", "=", input.id));
3049+ const where = (b: AnyCb) =>
3050+ b.and(byOwner(input.owner)(b), b("id", "=", input.id));
26813051 const existing = yield* core.findFirst("tool_policy", { where });
26823052 if (!existing) {
26833053 return yield* new StorageError({
@@ -2691,12 +3061,17 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 7 unmodified lines
26913061 if (input.position !== undefined) set.position = input.position;
26923062 yield* core.updateMany("tool_policy", { where, set });
26933063 const updated = yield* core.findFirst("tool_policy", { where });
2694− return rowToToolPolicy(updated ?? ({ ...existing, ...set } as ToolPolicyRow));
3064+ return rowToToolPolicy(
3065+ updated ?? ({ ...existing, ...set } as ToolPolicyRow),
3066+ );
26953067 });
26963068
2697− const policiesRemove = (input: RemoveToolPolicyInput): Effect.Effect<void, StorageFailure> =>
3069+ const policiesRemove = (
3070+ input: RemoveToolPolicyInput,
3071+ ): Effect.Effect<void, StorageFailure> =>
26983072 core.deleteMany("tool_policy", {
2699− where: (b: AnyCb) => b.and(byOwner(input.owner)(b), b("id", "=", input.id)),
3073+ where: (b: AnyCb) =>
3074+ b.and(byOwner(input.owner)(b), b("id", "=", input.id)),
27003075 });
27013076
27023077 const policiesResolve = (
@@ -2721,20 +3096,31 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 18 unmodified lines
27213096 ),
27223097 });
27233098 if (row) {
2724− const annotations = decodeJsonColumn(row.annotations) as ToolAnnotations | undefined;
3099+ const annotations = decodeJsonColumn(row.annotations) as
3100+ | ToolAnnotations
3101+ | undefined;
27253102 requiresApproval = annotations?.requiresApproval;
27263103 }
27273104 }
2728− return resolveEffectivePolicy(toolId, policyRows, ownerRankForRow, requiresApproval);
3105+ return resolveEffectivePolicy(
3106+ toolId,
3107+ policyRows,
3108+ ownerRankForRow,
3109+ requiresApproval,
3110+ );
27293111 });
27303112
27313113 // ------------------------------------------------------------------
27323114 // Elicitation
27333115 // ------------------------------------------------------------------
27343116
2735− const defaultElicitationHandler = resolveElicitationHandler(config.onElicitation);
3117+ const defaultElicitationHandler = resolveElicitationHandler(
3118+ config.onElicitation,
3119+ );
27363120
2737− const pickHandler = (options: InvokeOptions | undefined): ElicitationHandler =>
3121+ const pickHandler = (
3122+ options: InvokeOptions | undefined,
3123+ ): ElicitationHandler =>
27383124 options?.onElicitation
27393125 ? resolveElicitationHandler(options.onElicitation)
27403126 : defaultElicitationHandler;
@@ -2796,15 +3182,17 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 55 unmodified lines
27963182
27973183 const TOOL_SUGGESTION_LIMIT = 5;
27983184
2799− const toolSuggestions = (rows: readonly ToolInvocationRow[]): readonly ToolAddress[] =>
2800− rows.map((row) => rowToTool(row).address);
3185+ const toolSuggestions = (
3186+ rows: readonly ToolInvocationRow[],
3187+ ): readonly ToolAddress[] => rows.map((row) => rowToTool(row).address);
28013188
2802− const toolRowsForConnectionWhere = (parsed: ParsedToolAddress) => (b: AnyCb) =>
2803− b.and(
2804− byOwner(parsed.owner)(b),
2805− b("integration", "=", String(parsed.integration)),
2806− b("connection", "=", String(parsed.connection)),
2807− );
3189+ const toolRowsForConnectionWhere =
3190+ (parsed: ParsedToolAddress) => (b: AnyCb) =>
3191+ b.and(
3192+ byOwner(parsed.owner)(b),
3193+ b("integration", "=", String(parsed.integration)),
3194+ b("connection", "=", String(parsed.connection)),
3195+ );
28083196
28093197 const searchToolRowsForConnection = (
28103198 parsed: ParsedToolAddress,
@@ -2842,7 +3230,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 31 unmodified lines
28423230 return Effect.gen(function* () {
28433231 // oxlint-disable executor/no-instanceof-error, executor/no-unknown-error-message, executor/no-manual-tag-check -- boundary: normalize arbitrary unknown plugin failures into a human-readable message for ToolInvocationError/telemetry
28443232 const formatInvocationCauseMessage = (cause: unknown): string => {
2845− if (cause instanceof Error && cause.message.length > 0) return cause.message;
3233+ if (cause instanceof Error && cause.message.length > 0)
3234+ return cause.message;
28463235 // Non-Error / empty-message causes: `String(plainObject)` renders
28473236 // "[object Object]", which is what telemetry then shows as the only
28483237 // label for the failure. Prefer the tag, else stringify structurally.
@@ -2886,7 +3275,13 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 37 unmodified lines
28863275 pattern: policy.pattern ?? "*",
28873276 });
28883277 }
2889− yield* enforceApproval(staticEntry.tool.annotations, address, args, policy, handler);
3278+ yield* enforceApproval(
3279+ staticEntry.tool.annotations,
3280+ address,
3281+ args,
3282+ policy,
3283+ handler,
3284+ );
28903285 return yield* wrapInvocationError(
28913286 staticEntry.tool.handler({
28923287 ctx: staticEntry.ctx,
@@ -2917,7 +3312,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 24 unmodified lines
29173312 if (!row) {
29183313 const searchMatches = yield* searchToolRowsForConnection(parsed);
29193314 const connectionTools =
2920− searchMatches.length > 0 ? searchMatches : yield* findToolRowsForConnection(parsed);
3315+ searchMatches.length > 0
3316+ ? searchMatches
3317+ : yield* findToolRowsForConnection(parsed);
29213318 return yield* new ToolNotFoundError({
29223319 address,
29233320 suggestions: toolSuggestions(connectionTools),
@@ -2927,7 +3324,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 3 unmodified lines
29273324 // Resolve policy (owner-ranked).
29283325 const toolForPolicy = rowToTool(row);
29293326 const policyRows = yield* core.findMany("tool_policy", {});
2930− const annotations = decodeJsonColumn(row.annotations) as ToolAnnotations | undefined;
3327+ const annotations = decodeJsonColumn(row.annotations) as
3328+ | ToolAnnotations
3329+ | undefined;
29313330 const policy = resolveEffectivePolicy(
29323331 normalizedPolicyId(toolForPolicy),
29333332 policyRows,
@@ -2982,7 +3381,13 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 48 unmodified lines
29823381 .pipe(wrapInvocationError);
29833382 resolvedAnnotations = map[String(parsed.tool)] ?? annotations;
29843383 }
2985− yield* enforceApproval(resolvedAnnotations, address, args, policy, handler);
3384+ yield* enforceApproval(
3385+ resolvedAnnotations,
3386+ address,
3387+ args,
3388+ policy,
3389+ handler,
3390+ );
29863391
29873392 // Resolve every named credential input (`variable → value`); `value` is
29883393 // the primary `token` for single-input + OAuth callers.
@@ -2995,7 +3400,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 6 unmodified lines
29953400 template: AuthTemplateSlug.make(connectionRow.template),
29963401 value: values[PRIMARY_INPUT_VARIABLE] ?? null,
29973402 values,
2998− config: integrationRow ? decodeJsonColumn(integrationRow.config) : undefined,
3403+ config: integrationRow
3404+ ? decodeJsonColumn(integrationRow.config)
3405+ : undefined,
29993406 };
30003407
30013408 return yield* wrapInvocationError(
@@ -3035,7 +3442,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 33 unmodified lines
30353442 subject,
30363443 ownedKeys: (owner: Owner) => ownedKeys(owner),
30373444 defaultWritableProvider,
3038− mintOAuthConnection: (input: MintOAuthConnectionInput) => mintOAuthConnection(input),
3445+ mintOAuthConnection: (input: MintOAuthConnectionInput) =>
3446+ mintOAuthConnection(input),
30393447 // Resolve the integration's DECLARED oauth scopes for a (integration,
30403448 // template): load the row, run the owning plugin's auth-method projector,
30413449 // and return the matching oauth method's declared `oauth.scopes`. Drives
@@ -3043,15 +3451,20 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 1 unmodified line
30433451 // client on a broad integration still requests the integration's full
30443452 // scope set. Empty (no row / no oauth method / no declared scopes) ⇒ the
30453453 // union collapses to the client's scopes (current behavior).
3046− resolveDeclaredOAuthScopes: (integration: IntegrationSlug, template: AuthTemplateSlug) =>
3454+ resolveDeclaredOAuthScopes: (
3455+ integration: IntegrationSlug,
3456+ template: AuthTemplateSlug,
3457+ ) =>
30473458 findIntegrationRow(integration).pipe(
30483459 Effect.map((row): readonly string[] => {
30493460 if (!row) return [];
30503461 const methods = describeAuthMethodsForRow(row);
30513462 const match =
30523463 methods.find(
3053− (m: AuthMethodDescriptor) => m.kind === "oauth" && m.template === String(template),
3054− ) ?? methods.find((m: AuthMethodDescriptor) => m.kind === "oauth");
3464+ (m: AuthMethodDescriptor) =>
3465+ m.kind === "oauth" && m.template === String(template),
3466+ ) ??
3467+ methods.find((m: AuthMethodDescriptor) => m.kind === "oauth");
30553468 return match?.oauth?.scopes ?? [];
30563469 }),
30573470 ),
@@ -3101,7 +3514,8 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 43 unmodified lines
31013514 httpClientLayer: config.httpClientLayer ?? FetchHttpClient.layer,
31023515 core: {
31033516 integrations: {
3104− register: (input: RegisterIntegrationInput) => integrationsRegister(plugin.id, input),
3517+ register: (input: RegisterIntegrationInput) =>
3518+ integrationsRegister(plugin.id, input),
31053519 update: (slug, patch) => integrationsUpdate(slug, patch),
31063520 list: () => integrationsList(),
31073521 get: (slug) => integrationsGetRecord(slug),
@@ -3156,6 +3570,12 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 48 unmodified lines
31563570 if (plugin.extension) {
31573571 extensions[plugin.id] = extension;
31583572 }
3573+ // A plugin that narrows the executor exposes `wrapExecutor` on its
3574+ // extension; collect it into the generic hook the execution stack runs.
3575+ const wrapExecutor = (extension as { wrapExecutor?: ExecutorWrapper })
3576+ .wrapExecutor;
3577+ if (typeof wrapExecutor === "function")
3578+ executorWrappers.push(wrapExecutor);
31593579
31603580 const decls = plugin.staticSources ? plugin.staticSources(extension) : [];
31613581 for (const source of decls) {
@@ -3212,7 +3632,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 50 unmodified lines
32123632 yield* runtime.plugin
32133633 .close()
32143634 .pipe(
3215− Effect.mapError((cause) => pluginStorageFailure(runtime.plugin.id, "close", cause)),
3635+ Effect.mapError((cause) =>
3636+ pluginStorageFailure(runtime.plugin.id, "close", cause),
3637+ ),
32163638 );
32173639 }
32183640 }
@@ -3269,8 +3691,9 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 50 unmodified lines
32693691 close,
32703692 };
32713693
3272− const toExecutor = (value: unknown): Executor<TPlugins> => value as Executor<TPlugins>;
3273− return toExecutor(Object.assign(base, extensions));
3694+ const toExecutor = (value: unknown): Executor<TPlugins> =>
3695+ value as Executor<TPlugins>;
3696+ return toExecutor(Object.assign(base, extensions, { executorWrappers }));
32743697 });
32753698
32763699 // Helper alias so the inline literal used for the optimistic projection in
packages/core/sdk/src/index.ts+21−14
@@ -18,8 +18,17 @@ export { · 17 unmodified lines
1818 // FumaDB integration.
1919 export { fumadb } from "@executor-js/fumadb";
2020 export type { FumaDB } from "@executor-js/fumadb";
21−export type { AbstractQuery, Condition, ConditionBuilder } from "@executor-js/fumadb/query";
22−export { column, idColumn, schema as fumaSchema, table } from "@executor-js/fumadb/schema";
21+export type {
22+ AbstractQuery,
23+ Condition,
24+ ConditionBuilder,
25+} from "@executor-js/fumadb/query";
26+export {
27+ column,
28+ idColumn,
29+ schema as fumaSchema,
30+ table,
31+} from "@executor-js/fumadb/schema";
2332 export type {
2433 AnyColumn,
2534 AnySchema,
@@ -36,7 +45,11 @@ export type { · 10 unmodified lines
3645 IFumaClient,
3746 StorageFailure,
3847 } from "./fuma-runtime";
39−export { StorageError, UniqueViolationError, isStorageFailure } from "./fuma-runtime";
48+export {
49+ StorageError,
50+ UniqueViolationError,
51+ isStorageFailure,
52+} from "./fuma-runtime";
4053
4154 // IDs (branded) — the v2 set.
4255 export {
@@ -56,7 +69,10 @@ export { · 13 unmodified lines
5669 Subject,
5770 Owner,
5871 } from "./ids";
59−export { connectionIdentifier, isConnectionIdentifier } from "./connection-name-identifier";
72+export {
73+ connectionIdentifier,
74+ isConnectionIdentifier,
75+} from "./connection-name-identifier";
6076
6177 // Errors (tagged) — the ExecuteError set + integration lifecycle.
6278 export {
@@ -299,6 +315,7 @@ export { · 236 unmodified lines
299315 // uses it; the host surface (`@executor-js/api/server`) re-exports it.
300316 export {
301317 type Executor,
318+ type ExecutorWrapper,
302319 type ExecutorConfig,
303320 type ExecutorDb,
304321 type ExecutorDbFactory,
@@ -353,13 +370,3 @@ export { · 48 unmodified lines
353370 type AuthToolFailureCode,
354371 type AuthToolFailureInput,
355372 } from "./auth-tool-failure";
356−export {
357− applyToolkitScope,
358− EMPTY_TOOLKIT_SCOPE,
359− type ResolvedToolkitScope,
360− type ToolkitAccess,
361− type ToolkitPolicyAction,
362− type ToolkitPolicyRule,
363− type ToolkitResolver,
364− type ToolkitScopeEntry,
365−} from "./toolkit-scope";
packages/core/sdk/src/tool.ts+10−4
@@ -1,4 +1,10 @@
1−import type { ConnectionName, IntegrationSlug, Owner, ToolAddress, ToolName } from "./ids";
1+import type {
2+ ConnectionName,
3+ IntegrationSlug,
4+ Owner,
5+ ToolAddress,
6+ ToolName,
7+} from "./ids";
28
39 /* Tools belong to a connection and are PERSISTED, like v1 — not resolved live on
410 * every list. A plugin produces them at create/refresh (openapi from the
@@ -13,9 +19,9 @@ export interface ToolAnnotations { · 8 unmodified lines
1319 readonly approvalDescription?: string;
1420 readonly mayElicit?: boolean;
1521 /** True when the tool only reads (no side effects). Set by the owning plugin
16− * (e.g. OpenAPI classifies GET/HEAD/OPTIONS as read-only). Toolkits use this
17− * to enforce read-only access: in a read-only slice a tool is hidden unless
18− * `readOnly === true` (fail-closed — unclassified counts as write). */
22+ * (e.g. OpenAPI classifies GET/HEAD/OPTIONS as read-only). Consumers that
23+ * offer a read-only mode use this fail-closed: a tool is hidden unless
24+ * `readOnly === true` (unclassified counts as write). */
1925 readonly readOnly?: boolean;
2026 }
2127
packages/hosts/mcp/src/in-memory-session-store.ts+84−30
@@ -2,7 +2,10 @@ import { Cause, Data, Effect, Layer } from "effect"; · 1 unmodified line
22 import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33 import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
44
5−import { formatPausedExecution, type ExecutionEngine } from "@executor-js/execution";
5+import {
6+ formatPausedExecution,
7+ type ExecutionEngine,
8+} from "@executor-js/execution";
69
710 import {
811 buildResumeApprovalUrl,
@@ -50,7 +53,9 @@ import type { BrowserApprovalStore } from "./tool-server"; · 41 unmodified lines
5053 // ---------------------------------------------------------------------------
5154
5255 /** Engine construction failed for a principal. The store surfaces it as a 500. */
53−export class McpEngineBuildError extends Data.TaggedError("McpEngineBuildError")<{
56+export class McpEngineBuildError extends Data.TaggedError(
57+ "McpEngineBuildError",
58+)<{
5459 readonly cause: unknown;
5560 }> {}
5661
@@ -63,13 +68,17 @@ export interface BuiltMcpServer { · 6 unmodified lines
6368 /** The browser-mode wiring the store hands a build call when a session opts in. */
6469 export interface McpBuildServerOptions {
6570 readonly elicitationMode?:
66− | { readonly mode: "browser"; readonly approvalUrl: (executionId: string) => string }
71+ | {
72+ readonly mode: "browser";
73+ readonly approvalUrl: (executionId: string) => string;
74+ }
6775 | { readonly mode: "model" }
6876 | { readonly mode: "native" };
6977 readonly browserApprovalStore?: BrowserApprovalStore;
70− /** Toolkit selector (slug or id) pinned for this session — narrows the engine
71− * to that toolkit's slice. Read once at session create from the request. */
72− readonly toolkitId?: string;
78+ /** Per-request narrowing selector (the MCP `?toolkit=` value) pinned for this
79+ * session. Core forwards it to plugin executor wrappers; an unset selector
80+ * leaves the executor unnarrowed. Read once at session create. */
81+ readonly selector?: string;
7382 }
7483
7584 /** Build the per-session `McpServer` + engine for a principal (the host's engine + tools). */
@@ -103,9 +112,15 @@ export interface InMemoryMcpSessionStore { · 27 unmodified lines
103112 readonly close: () => Promise<void>;
104113 }
105114
106−const ignoreClose = (close: (() => Promise<void>) | undefined): Promise<void> =>
115+const ignoreClose = (
116+ close: (() => Promise<void>) | undefined,
117+): Promise<void> =>
107118 close
108− ? Effect.runPromise(Effect.ignore(Effect.tryPromise({ try: close, catch: () => undefined })))
119+ ? Effect.runPromise(
120+ Effect.ignore(
121+ Effect.tryPromise({ try: close, catch: () => undefined }),
122+ ),
123+ )
109124 : Promise.resolve();
110125
111126 const formatBoundaryError = (error: unknown): unknown =>
@@ -115,14 +130,21 @@ const formatBoundaryError = (error: unknown): unknown => · 3 unmodified lines
115130 // The store's error bodies are INNER responses (no CORS): the serving envelope
116131 // re-wraps the store `Response` with CORS before it leaves the origin, so the
117132 // canonical renderer is called with `cors: false` (content-type only).
118−const jsonRpcError = (status: number, code: number, message: string): Response =>
119− jsonRpcErrorBody(status, code, message, { cors: false });
133+const jsonRpcError = (
134+ status: number,
135+ code: number,
136+ message: string,
137+): Response => jsonRpcErrorBody(status, code, message, { cors: false });
120138
121139 const json = (value: unknown, status = 200): Response =>
122− new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } });
140+ new Response(JSON.stringify(value), {
141+ status,
142+ headers: { "content-type": "application/json" },
143+ });
123144
124145 const PAUSED_PATH = /^\/api\/mcp-sessions\/([^/?#]+)\/executions\/([^/?#]+)$/;
125−const RESUME_PATH = /^\/api\/mcp-sessions\/([^/?#]+)\/executions\/([^/?#]+)\/resume$/;
146+const RESUME_PATH =
147+ /^\/api\/mcp-sessions\/([^/?#]+)\/executions\/([^/?#]+)\/resume$/;
126148
127149 /**
128150 * Build the in-process session store plus an explicit `close()` that disposes
@@ -139,21 +161,30 @@ export const makeInMemoryMcpSessionStore = ( · 10 unmodified lines
139161 // loopback hosts (local/desktop), where the request URL is already correct.
140162 options: { readonly webBaseUrl?: string } = {},
141163 ): InMemoryMcpSessionStore => {
142− const transports = new Map<string, WebStandardStreamableHTTPServerTransport>();
164+ const transports = new Map<
165+ string,
166+ WebStandardStreamableHTTPServerTransport
167+ >();
143168 const servers = new Map<string, McpServer>();
144169 const owners = new Map<string, Principal>();
145170 const engines = new Map<string, ExecutionEngine<Cause.YieldableError>>();
146− const approvals: InProcessBrowserApprovalStore = makeInProcessBrowserApprovalStore();
171+ const approvals: InProcessBrowserApprovalStore =
172+ makeInProcessBrowserApprovalStore();
147173
148− const dispose = async (id: string, opts: { transport?: boolean; server?: boolean } = {}) => {
174+ const dispose = async (
175+ id: string,
176+ opts: { transport?: boolean; server?: boolean } = {},
177+ ) => {
149178 const transport = transports.get(id);
150179 const server = servers.get(id);
151180 transports.delete(id);
152181 servers.delete(id);
153182 owners.delete(id);
154183 engines.delete(id);
155− if (opts.transport) await ignoreClose(transport ? () => transport.close() : undefined);
156− if (opts.server) await ignoreClose(server ? () => server.close() : undefined);
184+ if (opts.transport)
185+ await ignoreClose(transport ? () => transport.close() : undefined);
186+ if (opts.server)
187+ await ignoreClose(server ? () => server.close() : undefined);
157188 };
158189
159190 /**
@@ -173,7 +204,10 @@ export const makeInMemoryMcpSessionStore = ( · 13 unmodified lines
173204 Effect.tap(() => Effect.sync(finish)),
174205 Effect.catchCause((cause) =>
175206 Effect.sync(() => {
176− console.error("[mcp] handleRequest error:", formatBoundaryError(cause));
207+ console.error(
208+ "[mcp] handleRequest error:",
209+ formatBoundaryError(cause),
210+ );
177211 finish();
178212 return jsonRpcError(500, -32603, "Internal server error");
179213 }),
@@ -204,7 +238,8 @@ export const makeInMemoryMcpSessionStore = ( · 24 unmodified lines
204238 request: Request,
205239 sessionId: () => string | null,
206240 ): McpBuildServerOptions => {
207− if (readElicitationMode(request) !== "browser") return { elicitationMode: { mode: "model" } };
241+ if (readElicitationMode(request) !== "browser")
242+ return { elicitationMode: { mode: "model" } };
208243 return {
209244 elicitationMode: {
210245 mode: "browser",
@@ -222,11 +257,17 @@ export const makeInMemoryMcpSessionStore = ( · 11 unmodified lines
222257 };
223258
224259 /** Open a new session: build the server, connect a transport, drive the request. */
225− const create = (principal: Principal, request: Request): Effect.Effect<McpDispatchResult> => {
260+ const create = (
261+ principal: Principal,
262+ request: Request,
263+ ): Effect.Effect<McpDispatchResult> => {
226264 let createdSessionId: string | null = null;
227265 const baseOptions = buildOptionsFor(request, () => createdSessionId);
228− const toolkitId = readToolkitSelector(request);
229− return buildServer(principal, toolkitId ? { ...baseOptions, toolkitId } : baseOptions).pipe(
266+ const selector = readToolkitSelector(request);
267+ return buildServer(
268+ principal,
269+ selector ? { ...baseOptions, selector } : baseOptions,
270+ ).pipe(
230271 Effect.flatMap(({ mcpServer, engine }) =>
231272 Effect.gen(function* () {
232273 const transport = new WebStandardStreamableHTTPServerTransport({
@@ -263,9 +304,13 @@ export const makeInMemoryMcpSessionStore = ( · 30 unmodified lines
263304
264305 const store: McpSessionStore["Service"] = {
265306 dispatch: ({ request, principal, sessionId }: McpDispatchInput) =>
266− sessionId ? forward(sessionId, principal, request) : create(principal, request),
307+ sessionId
308+ ? forward(sessionId, principal, request)
309+ : create(principal, request),
267310 dispose: (sessionId) =>
268− Effect.promise(() => dispose(sessionId, { transport: true, server: true })),
311+ Effect.promise(() =>
312+ dispose(sessionId, { transport: true, server: true }),
313+ ),
269314 };
270315
271316 const ownerAccess = (
@@ -299,12 +344,17 @@ export const makeInMemoryMcpSessionStore = ( · 27 unmodified lines
299344 ): Promise<Response | null> => {
300345 const match = PAUSED_PATH.exec(new URL(request.url).pathname);
301346 if (!match) return null;
302− if (request.method !== "GET") return json({ error: "Method not allowed" }, 405);
347+ if (request.method !== "GET")
348+ return json({ error: "Method not allowed" }, 405);
303349 const sessionId = decodeURIComponent(match[1]!);
304350 const access = ownerAccess(sessionId, principal);
305351 if (access === "forbidden") return json({ error: "Forbidden" }, 403);
306− if (access === "not-found") return json({ error: "Paused execution not found" }, 404);
307− const paused = await pausedFromSession(sessionId, decodeURIComponent(match[2]!));
352+ if (access === "not-found")
353+ return json({ error: "Paused execution not found" }, 404);
354+ const paused = await pausedFromSession(
355+ sessionId,
356+ decodeURIComponent(match[2]!),
357+ );
308358 if (!paused) return json({ error: "Paused execution not found" }, 404);
309359 return json({ text: paused.text, structured: paused.structured });
310360 };
@@ -315,13 +365,15 @@ export const makeInMemoryMcpSessionStore = ( · 4 unmodified lines
315365 ): Promise<Response | null> => {
316366 const match = RESUME_PATH.exec(new URL(request.url).pathname);
317367 if (!match) return null;
318− if (request.method !== "POST") return json({ error: "Method not allowed" }, 405);
368+ if (request.method !== "POST")
369+ return json({ error: "Method not allowed" }, 405);
319370
320371 const sessionId = decodeURIComponent(match[1]!);
321372 const executionId = decodeURIComponent(match[2]!);
322373 const access = ownerAccess(sessionId, principal);
323374 if (access === "forbidden") return json({ error: "Forbidden" }, 403);
324− if (access === "not-found") return json({ error: "Paused execution not found" }, 404);
375+ if (access === "not-found")
376+ return json({ error: "Paused execution not found" }, 404);
325377 // The session must still hold the paused execution — guards stale ids and
326378 // confirms the execution belongs to this session before recording.
327379 const paused = await pausedFromSession(sessionId, executionId);
@@ -349,7 +401,9 @@ export const makeInMemoryMcpSessionStore = ( · 21 unmodified lines
349401 handleApprovalRequest,
350402 close: async () => {
351403 const ids = new Set([...transports.keys(), ...servers.keys()]);
352− await Promise.all([...ids].map((id) => dispose(id, { transport: true, server: true })));
404+ await Promise.all(
405+ [...ids].map((id) => dispose(id, { transport: true, server: true })),
406+ );
353407 },
354408 };
355409 };
packages/plugins/toolkits/src/server.ts+110−58
@@ -13,12 +13,17 @@ · 12 unmodified lines
1313 // ---------------------------------------------------------------------------
1414
1515 import { Schema } from "effect";
16−import { Context, definePlugin, Effect, HttpApiBuilder } from "@executor-js/sdk/core";
16+import {
17+ Context,
18+ definePlugin,
19+ Effect,
20+ HttpApiBuilder,
21+} from "@executor-js/sdk/core";
1722 import {
1823 definePluginStorageCollection,
1924 Owner,
25+ type ExecutorWrapper,
2026 type PluginCtx,
21− type ResolvedToolkitScope,
2227 type StorageFailure,
2328 } from "@executor-js/sdk";
2429 import { addGroup, capture } from "@executor-js/api";
@@ -34,6 +39,11 @@ import { · 9 unmodified lines
3439 type ToolkitView,
3540 type UpdateToolkitPayload,
3641 } from "./shared";
42+import {
43+ applyToolkitScope,
44+ EMPTY_TOOLKIT_SCOPE,
45+ type ResolvedToolkitScope,
46+} from "./toolkit-scope";
3747
3848 const ORG = Owner.make("org");
3949 const USER = Owner.make("user");
@@ -64,14 +74,23 @@ const PolicyRow = Schema.Struct({ · 24 unmodified lines
6474 action: Schema.String,
6575 });
6676
67−const TOOLKITS = definePluginStorageCollection("toolkits", ToolkitRow, { indexes: ["slug"] });
68−const CONNECTIONS = definePluginStorageCollection("connections", ConnectionRow, {
77+const TOOLKITS = definePluginStorageCollection("toolkits", ToolkitRow, {
78+ indexes: ["slug"],
79+});
80+const CONNECTIONS = definePluginStorageCollection(
81+ "connections",
82+ ConnectionRow,
83+ {
84+ indexes: ["toolkitId"],
85+ },
86+);
87+const POLICIES = definePluginStorageCollection("policies", PolicyRow, {
6988 indexes: ["toolkitId"],
7089 });
71−const POLICIES = definePluginStorageCollection("policies", PolicyRow, { indexes: ["toolkitId"] });
7290
7391 const newId = Effect.sync(() => crypto.randomUUID());
74−const scopeOfOwner = (owner: Owner): ToolkitScope => (owner === ORG ? "workspace" : "personal");
92+const scopeOfOwner = (owner: Owner): ToolkitScope =>
93+ owner === ORG ? "workspace" : "personal";
7594
7695 // ---------------------------------------------------------------------------
7796 // Extension — the canonical implementation. Handlers (and later the MCP
@@ -116,9 +135,14 @@ const makeToolkitsExtension = (ctx: PluginCtx) => { · 38 unmodified lines
116135 // candidate connections honoring scope: a workspace toolkit may use only
117136 // org connections; a personal toolkit may use org + the caller's own.
118137 const candidateConnections = (scope: ToolkitScope) =>
119− scope === "workspace" ? ctx.connections.list({ owner: ORG }) : ctx.connections.list();
138+ scope === "workspace"
139+ ? ctx.connections.list({ owner: ORG })
140+ : ctx.connections.list();
120141
121− const validate = (scope: ToolkitScope, entries: ReadonlyArray<ToolkitConnectionEntryInput>) =>
142+ const validate = (
143+ scope: ToolkitScope,
144+ entries: ReadonlyArray<ToolkitConnectionEntryInput>,
145+ ) =>
122146 Effect.gen(function* () {
123147 const allowed = yield* candidateConnections(scope);
124148 const pairs = new Set(allowed.map((c) => `${c.integration}/${c.name}`));
@@ -207,7 +231,9 @@ const makeToolkitsExtension = (ctx: PluginCtx) => { · 82 unmodified lines
207231 });
208232
209233 const list = () =>
210− toolkits.list().pipe(Effect.flatMap((rows) => Effect.forEach(rows, viewFromRow)));
234+ toolkits
235+ .list()
236+ .pipe(Effect.flatMap((rows) => Effect.forEach(rows, viewFromRow)));
211237
212238 const update = (id: string, patch: UpdateToolkitPayload) =>
213239 Effect.gen(function* () {
@@ -216,8 +242,10 @@ const makeToolkitsExtension = (ctx: PluginCtx) => { · 2 unmodified lines
216242 const data = {
217243 slug: row.data.slug,
218244 name: patch.name ?? row.data.name,
219− inheritOrgPolicies: patch.inheritOrgPolicies ?? row.data.inheritOrgPolicies,
220− briefing: patch.briefing === undefined ? row.data.briefing : patch.briefing,
245+ inheritOrgPolicies:
246+ patch.inheritOrgPolicies ?? row.data.inheritOrgPolicies,
247+ briefing:
248+ patch.briefing === undefined ? row.data.briefing : patch.briefing,
221249 };
222250 yield* toolkits.put({ key: id, owner: row.owner, data });
223251 if (patch.connections !== undefined) {
@@ -230,7 +258,9 @@ const makeToolkitsExtension = (ctx: PluginCtx) => { · 6 unmodified lines
230258 }
231259 if (patch.policies !== undefined) {
232260 const existing = yield* policies.query({ where: { toolkitId: id } });
233− yield* Effect.forEach(existing, (p) => policies.remove({ key: p.key, owner: row.owner }));
261+ yield* Effect.forEach(existing, (p) =>
262+ policies.remove({ key: p.key, owner: row.owner }),
263+ );
234264 yield* putPolicies(id, row.owner, patch.policies);
235265 }
236266 return yield* viewFromRow({ key: id, owner: row.owner, data });
@@ -241,9 +271,13 @@ const makeToolkitsExtension = (ctx: PluginCtx) => { · 4 unmodified lines
241271 const row = yield* toolkits.get({ key: id });
242272 if (row == null) return yield* new ToolkitNotFound({ id });
243273 const conns = yield* connections.query({ where: { toolkitId: id } });
244− yield* Effect.forEach(conns, (c) => connections.remove({ key: c.key, owner: row.owner }));
274+ yield* Effect.forEach(conns, (c) =>
275+ connections.remove({ key: c.key, owner: row.owner }),
276+ );
245277 const pols = yield* policies.query({ where: { toolkitId: id } });
246− yield* Effect.forEach(pols, (p) => policies.remove({ key: p.key, owner: row.owner }));
278+ yield* Effect.forEach(pols, (p) =>
279+ policies.remove({ key: p.key, owner: row.owner }),
280+ );
247281 yield* toolkits.remove({ key: id, owner: row.owner });
248282 return { removed: true };
249283 });
@@ -256,7 +290,10 @@ const makeToolkitsExtension = (ctx: PluginCtx) => { · 6 unmodified lines
256290 selector: string,
257291 ): Effect.Effect<ResolvedToolkitScope | null, StorageFailure> =>
258292 Effect.gen(function* () {
259− const bySlug = yield* toolkits.query({ where: { slug: selector }, limit: 1 });
293+ const bySlug = yield* toolkits.query({
294+ where: { slug: selector },
295+ limit: 1,
296+ });
260297 const row = bySlug[0] ?? (yield* toolkits.get({ key: selector }));
261298 if (row == null) return null;
262299 const conns = yield* connections.query({ where: { toolkitId: row.key } });
@@ -275,12 +312,24 @@ const makeToolkitsExtension = (ctx: PluginCtx) => { · 12 unmodified lines
275312 };
276313 });
277314
278− return { create, get, list, update, remove, resolveScope };
315+ // The plugin's contribution to core's generic executor-wrapper seam: given a
316+ // selector, resolve it to a scope (fail-closed to the empty slice when it
317+ // doesn't resolve for this caller) and narrow the executor. Core collects and
318+ // runs this without knowing it's about toolkits.
319+ const wrapExecutor: ExecutorWrapper = (base, selector) =>
320+ resolveScope(selector).pipe(
321+ Effect.flatMap((scope) =>
322+ applyToolkitScope(base, scope ?? EMPTY_TOOLKIT_SCOPE),
323+ ),
324+ );
325+
326+ return { create, get, list, update, remove, resolveScope, wrapExecutor };
279327 };
280328
281329 // Local aliases so the storage/extension layer stays decoupled from the wire
282330 // schema's branded types without re-importing them everywhere.
283−type ToolkitConnectionIntegration = ToolkitView["connections"][number]["integration"];
331+type ToolkitConnectionIntegration =
332+ ToolkitView["connections"][number]["integration"];
284333 type ToolkitConnectionEntryInput = {
285334 readonly integration: ToolkitConnectionIntegration;
286335 readonly connection: string;
@@ -300,48 +349,51 @@ export class ToolkitsExtensionService extends Context.Service< · 13 unmodified lines
300349 // the host's composed API requires) — not a standalone single-group bundle.
301350 const ExecutorApiWithToolkits = addGroup(ToolkitsApi);
302351
303−const ToolkitsHandlers = HttpApiBuilder.group(ExecutorApiWithToolkits, "toolkits", (h) =>
304− h
305− .handle("list", () =>
306− capture(
307− Effect.gen(function* () {
308− const ext = yield* ToolkitsExtensionService;
309− return yield* ext.list();
310− }),
311− ),
312− )
313− .handle("create", ({ payload }) =>
314− capture(
315− Effect.gen(function* () {
316− const ext = yield* ToolkitsExtensionService;
317− return yield* ext.create(payload);
318− }),
319− ),
320− )
321− .handle("get", ({ params }) =>
322− capture(
323− Effect.gen(function* () {
324− const ext = yield* ToolkitsExtensionService;
325− return yield* ext.get(params.id);
326− }),
327− ),
328− )
329− .handle("update", ({ params, payload }) =>
330− capture(
331− Effect.gen(function* () {
332− const ext = yield* ToolkitsExtensionService;
333− return yield* ext.update(params.id, payload);
334− }),
335− ),
336− )
337− .handle("remove", ({ params }) =>
338− capture(
339− Effect.gen(function* () {
340− const ext = yield* ToolkitsExtensionService;
341− return yield* ext.remove(params.id);
342− }),
352+const ToolkitsHandlers = HttpApiBuilder.group(
353+ ExecutorApiWithToolkits,
354+ "toolkits",
355+ (h) =>
356+ h
357+ .handle("list", () =>
358+ capture(
359+ Effect.gen(function* () {
360+ const ext = yield* ToolkitsExtensionService;
361+ return yield* ext.list();
362+ }),
363+ ),
364+ )
365+ .handle("create", ({ payload }) =>
366+ capture(
367+ Effect.gen(function* () {
368+ const ext = yield* ToolkitsExtensionService;
369+ return yield* ext.create(payload);
370+ }),
371+ ),
372+ )
373+ .handle("get", ({ params }) =>
374+ capture(
375+ Effect.gen(function* () {
376+ const ext = yield* ToolkitsExtensionService;
377+ return yield* ext.get(params.id);
378+ }),
379+ ),
380+ )
381+ .handle("update", ({ params, payload }) =>
382+ capture(
383+ Effect.gen(function* () {
384+ const ext = yield* ToolkitsExtensionService;
385+ return yield* ext.update(params.id, payload);
386+ }),
387+ ),
388+ )
389+ .handle("remove", ({ params }) =>
390+ capture(
391+ Effect.gen(function* () {
392+ const ext = yield* ToolkitsExtensionService;
393+ return yield* ext.remove(params.id);
394+ }),
395+ ),
343396 ),
344− ),
345397 );
346398
347399 export const toolkitsPlugin = definePlugin(() => ({
packages/plugins/toolkits/src/toolkit-scope.tsrenamed from packages/core/sdk/src/toolkit-scope.ts+40−28
@@ -1,8 +1,9 @@
11 // ---------------------------------------------------------------------------
22 // Toolkit scope — wraps an Executor so the MCP surface only sees, and can only
3−// run, a toolkit's slice of connections. Toolkit-agnostic: it operates on a
4−// plain ResolvedToolkitScope (produced by the toolkits plugin's `resolveScope`)
5−// and the base Executor. Because the wrapped Executor is handed to the engine
3+// run, a toolkit's slice of connections. This is the toolkits plugin's
4+// implementation of core's generic `ExecutorWrapper` seam: the plugin
5+// contributes `wrapExecutor` (server.ts) and core runs it without ever knowing
6+// what "toolkit" means. Because the wrapped Executor is handed to the engine
67 // before construction, EVERY surface (tools.list, search, describe,
78 // sources/connections/integrations.list, core-tools, and execute) flows through
89 // it — enforcement is at execute, not just listing, so guessed out-of-slice
@@ -11,15 +12,15 @@ · 2 unmodified lines
1112
1213 import { Effect } from "effect";
1314
14−import { ToolBlockedError } from "./errors";
15−import type { Executor } from "./executor";
16−import type { ToolAddress } from "./ids";
17−import type { AnyPlugin } from "./plugin";
18−import type { StorageFailure } from "./fuma-runtime";
15+import {
16+ ToolBlockedError,
17+ type AnyPlugin,
18+ type Executor,
19+ type StorageFailure,
20+ type ToolAddress,
21+} from "@executor-js/sdk";
1922
20−export type ToolkitAccess = "off" | "read" | "full";
21−
22−export type ToolkitPolicyAction = "approve" | "require_approval" | "block";
23+import type { ToolkitAccess, ToolkitPolicyAction } from "./shared";
2324
2425 export interface ToolkitPolicyRule {
2526 /** Glob over `<integration>.<connection>.<tool>` — `*` matches one segment,
@@ -62,28 +63,23 @@ const matchPattern = (pattern: string, target: string): boolean => { · 36 unmodified lines
6263 if (p[i] === "*" && i === p.length - 1) return true;
6364 if (a[i] === undefined) return false;
6465 const re = new RegExp(
65− "^" + p[i].replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$",
66+ "^" +
67+ p[i].replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") +
68+ "$",
6669 );
6770 if (!re.test(a[i])) return false;
6871 }
6972 return p.length === a.length;
7073 };
7174
72−/** A plugin extension capable of resolving a selector (slug or id) to a scope,
73− * scoped to the caller. `resolveScope` returns null when nothing matches. */
74−export interface ToolkitResolver {
75− readonly resolveScope: (
76− selector: string,
77− ) => Effect.Effect<ResolvedToolkitScope | null, StorageFailure>;
78−}
79−
8075 const accessFor = (
8176 scope: ResolvedToolkitScope,
8277 integration: string,
8378 connection: string,
8479 ): ToolkitAccess => {
8580 for (const e of scope.entries) {
86− if (e.integration === integration && e.connection === connection) return e.access;
81+ if (e.integration === integration && e.connection === connection)
82+ return e.access;
8783 }
8884 for (const e of scope.entries) {
8985 if (e.integration === integration && e.connection === "*") return e.access;
@@ -101,7 +97,9 @@ export const applyToolkitScope = <TPlugins extends readonly AnyPlugin[]>( · 11 unmodified lines
10197 Effect.gen(function* () {
10298 const blockRules = scope.policies.filter((p) => p.action === "block");
10399 const isBlocked = (integration: string, connection: string, name: string) =>
104− blockRules.some((p) => matchPattern(p.pattern, `${integration}.${connection}.${name}`));
100+ blockRules.some((p) =>
101+ matchPattern(p.pattern, `${integration}.${connection}.${name}`),
102+ );
105103
106104 const all = yield* base.tools.list();
107105 const allowed = new Set<string>();
@@ -112,7 +110,10 @@ export const applyToolkitScope = <TPlugins extends readonly AnyPlugin[]>( · 4 unmodified lines
112110 allowed.add(String(t.address));
113111 continue;
114112 }
115− if (isBlocked(String(t.integration), String(t.connection), String(t.name))) continue;
113+ if (
114+ isBlocked(String(t.integration), String(t.connection), String(t.name))
115+ )
116+ continue;
116117 const a = accessFor(scope, String(t.integration), String(t.connection));
117118 if (a === "full" || (a === "read" && t.annotations?.readOnly === true)) {
118119 allowed.add(String(t.address));
@@ -132,7 +133,11 @@ export const applyToolkitScope = <TPlugins extends readonly AnyPlugin[]>( · 13 unmodified lines
132133 list: () =>
133134 base.integrations
134135 .list()
135− .pipe(Effect.map((xs) => xs.filter((i) => allowedIntegrations.has(String(i.slug))))),
136+ .pipe(
137+ Effect.map((xs) =>
138+ xs.filter((i) => allowedIntegrations.has(String(i.slug))),
139+ ),
140+ ),
136141 get: (slug) =>
137142 allowedIntegrations.has(String(slug))
138143 ? base.integrations.get(slug)
@@ -144,20 +149,27 @@ export const applyToolkitScope = <TPlugins extends readonly AnyPlugin[]>( · 5 unmodified lines
144149 base.connections
145150 .list(filter)
146151 .pipe(
147− Effect.map((xs) => xs.filter((c) => connOk(String(c.integration), String(c.name)))),
152+ Effect.map((xs) =>
153+ xs.filter((c) => connOk(String(c.integration), String(c.name))),
154+ ),
148155 ),
149156 get: (ref) =>
150157 base.connections
151158 .get(ref)
152159 .pipe(
153− Effect.map((c) => (c && connOk(String(c.integration), String(c.name)) ? c : null)),
160+ Effect.map((c) =>
161+ c && connOk(String(c.integration), String(c.name)) ? c : null,
162+ ),
154163 ),
155164 },
156165 tools: {
157166 ...base.tools,
158167 list: (filter) =>
159− base.tools.list(filter).pipe(Effect.map((xs) => xs.filter((t) => addrOk(t.address)))),
160− schema: (address) => (addrOk(address) ? base.tools.schema(address) : Effect.succeed(null)),
168+ base.tools
169+ .list(filter)
170+ .pipe(Effect.map((xs) => xs.filter((t) => addrOk(t.address)))),
171+ schema: (address) =>
172+ addrOk(address) ? base.tools.schema(address) : Effect.succeed(null),
161173 },
162174 execute: (address, args, options) =>
163175 addrOk(address)