import { Deferred, Duration, Effect, Layer, Match, Option, Predicate, Result, Schema, Semaphore,} from "effect";import { FetchHttpClient, type HttpClient } from "effect/unstable/http";import { fumadb } from "fumadb";import { memoryAdapter } from "fumadb/adapters/memory";import { withQueryContext, type Condition, type ConditionBuilder } from "fumadb/query";import { schema as fumaSchema, type RelationsMap } from "fumadb/schema";import type { AnyColumn } from "fumadb/schema";import type { OAuthEndpointUrlPolicy } from "./oauth-helpers";import { generateKeyBetween } from "fractional-indexing";import { StorageError, isStorageFailure, makeFumaClient, type FumaDb, type FumaRow, type FumaTables, type StorageFailure,} from "./fuma-runtime"; import { makeFumaBlobStore, pluginBlobStore } from "./blob";import { coreToolsPlugin } from "./core-tools";import { ConnectionProviderState, ConnectionRef, ConnectionRefreshError, type ConnectionProvider, type ConnectionRefreshResult, type CreateConnectionInput, type RemoveConnectionInput, type UpdateConnectionTokensInput,} from "./connections";import { credentialBindingId, credentialBindingRowToRef, type CredentialBindingRef, type CredentialBindingsFacade, type CredentialBindingSlotInput, type CredentialBindingSourceInput, type RemoveCredentialBindingInput, type RemoveSourceCredentialBindingInput, type ReplaceCredentialBindingsInput, type ReplaceSourceCredentialBindingsInput, ResolvedCredentialSlot, type SetPluginCredentialBindingInput, type SetSourceCredentialBindingInput, type SourceCredentialBindingSlotInput, type SourceCredentialBindingSourceInput,} from "./credential-bindings";import { coreSchema, isToolPolicyAction, type ConnectionRow, type CredentialBindingRow, type CoreSchema, type DefinitionsInput, type SecretRow, type SourceInput, type SourceRow, type ToolAnnotations, type ToolPolicyRow, type ToolRow,} from "./core-schema";import { ElicitationDeclinedError, ElicitationResponse, FormElicitation, type ElicitationHandler, type ElicitationRequest,} from "./elicitation";import { ConnectionInUseError, ConnectionNotFoundError, ConnectionProviderNotRegisteredError, ConnectionReauthRequiredError, ConnectionRefreshNotSupportedError, NoHandlerError, PluginNotLoadedError, SecretInUseError, SecretOwnedByConnectionError, SourceRemovalNotAllowedError, ToolBlockedError, ToolInvocationError, ToolNotFoundError,} from "./errors";import { ConnectionId, ScopeId, SecretId, ToolId } from "./ids";import { makeOAuth2Service } from "./oauth-service";import type { OAuthService } from "./oauth";import { comparePolicyRow, isValidPattern, resolveToolPolicy, rowToToolPolicy, type CreateToolPolicyInput, type PolicyMatch, type RemoveToolPolicyInput, type ToolPolicy, type UpdateToolPolicyInput,} from "./policies";import type { AnyPlugin, Elicit, PluginCtx, PluginExtensions, SourceConfigureSchema, StaticSourceDecl, StaticToolDecl, StaticToolSchema, StorageDeps,} from "./plugin";import { pluginStorageId, type PluginStorageEntry, type PluginStorageFacade,} from "./plugin-storage";import type { Scope } from "./scope";import { RemoveSecretInput, SecretRef, SetSecretInput, type SecretProvider } from "./secrets";import { Usage } from "./usages";import { ToolSchema, type RefreshSourceInput, type RemoveSourceInput, type Source, type SourceDetectionResult, type Tool, type ToolListFilter,} from "./types";import { buildToolTypeScriptPreview, type ToolTypeScriptPreview } from "./schema-types";import { collectReferencedDefinitions } from "./schema-refs";import { assertExecutorScopePolicyTable, type ExecutorScopePolicyContext } from "./scope-policy";import { validateHostedOutboundUrl } from "./hosted-http-client"; const MAX_ANNOTATION_GROUPS = 64;const MAX_APPROVAL_ARGUMENT_PREVIEW_CHARS = 4_000; // ---------------------------------------------------------------------------// Elicitation handler — set once at `createExecutor({ onElicitation })`// and threaded into every tool invocation. A tool that requests user// input mid-execution suspends the fiber and the handler decides how to// respond. Tools that never elicit simply don't trigger the handler.//// The "accept-all" sentinel is convenient for tests and CLI automation —// every elicitation request gets auto-accepted with an empty content// payload. For real interactive hosts, pass a real handler.//// Required at the executor level rather than per-invoke, so the// "what if a caller forgot to pass a handler" branch is structurally// impossible. Higher layers that need per-invocation handler control// (an MCP server bridging different per-client handlers, the execution// engine threading agent-loop callbacks) can pass an override via// `tools.invoke(id, args, { onElicitation })` — the executor-level// handler is the fallback, never null.// --------------------------------------------------------------------------- export type OnElicitation = ElicitationHandler | "accept-all"; export interface InvokeOptions { /** Override the executor-level handler for this single call. */ readonly onElicitation?: OnElicitation;} const acceptAllHandler: ElicitationHandler = () => Effect.succeed(ElicitationResponse.make({ action: "accept" })); const resolveElicitationHandler = (onElicitation: OnElicitation): ElicitationHandler => onElicitation === "accept-all" ? acceptAllHandler : onElicitation; // ---------------------------------------------------------------------------// Executor — public surface. Every list/invoke/schema call is a direct// core-table query (for dynamic rows) unioned with the in-memory static// pool. No ToolRegistry, no SourceRegistry, no SecretStore services.// --------------------------------------------------------------------------- export type Executor<TPlugins extends readonly AnyPlugin[] = readonly []> = { /** * Precedence-ordered scope stack this executor was configured with. * Innermost first. Consumers that need "the display scope" typically * pick `scopes.at(-1)` (outermost, e.g. the organization) or * `scopes[0]` (innermost, e.g. the current user-in-org) depending on * what they're rendering. */ readonly scopes: readonly Scope[]; readonly tools: { readonly list: (filter?: ToolListFilter) => Effect.Effect<readonly Tool[], StorageFailure>; /** Fetch a tool's schema view: JSON schemas with `$defs` * attached from the core `definition` table, plus TypeScript * preview strings. Returns `null` for unknown tool ids. */ readonly schema: (toolId: string) => Effect.Effect<ToolSchema | null, StorageFailure>; /** Every `$defs` entry across every source, grouped by source id. * Used for bulk schema export and downstream TypeScript rendering. */ readonly definitions: () => Effect.Effect< Record<string, Record<string, unknown>>, StorageFailure >; readonly invoke: ( toolId: string, args: unknown, options?: InvokeOptions, ) => Effect.Effect< unknown, | ToolNotFoundError | ToolBlockedError | PluginNotLoadedError | NoHandlerError | ToolInvocationError | ElicitationDeclinedError | StorageFailure >; }; readonly sources: { readonly list: () => Effect.Effect<readonly Source[], StorageFailure>; readonly remove: ( input: RemoveSourceInput, ) => Effect.Effect<void, SourceRemovalNotAllowedError | StorageFailure>; readonly refresh: (input: RefreshSourceInput) => Effect.Effect<void, StorageFailure>; /** URL autodetection — fans out to every plugin's `detect` hook * (if declared), returns every high/medium/low-confidence match. * UI picks a winner from the list. */ readonly detect: ( url: string, ) => Effect.Effect<readonly SourceDetectionResult[], StorageFailure>; /** All `$defs` registered for a single source, keyed by def name. */ readonly definitions: ( sourceId: string, ) => Effect.Effect<Record<string, unknown>, StorageFailure>; readonly configure: (input: { readonly source: { readonly id: string; readonly scope: ScopeId | string; }; readonly scope: ScopeId | string; readonly type?: string; readonly config: unknown; }) => Effect.Effect<unknown, StorageFailure>; readonly listBindings: ( input: SourceCredentialBindingSourceInput, ) => Effect.Effect<readonly CredentialBindingRef[], StorageFailure>; readonly resolveBinding: ( input: SourceCredentialBindingSlotInput, ) => Effect.Effect<CredentialBindingRef | null, StorageFailure>; readonly setBinding: ( input: SetSourceCredentialBindingInput, ) => Effect.Effect<CredentialBindingRef, StorageFailure>; readonly removeBinding: ( input: RemoveSourceCredentialBindingInput, ) => Effect.Effect<void, StorageFailure>; readonly replaceBindings: ( input: ReplaceSourceCredentialBindingsInput, ) => Effect.Effect<readonly CredentialBindingRef[], StorageFailure>; }; readonly secrets: { readonly get: ( id: string, ) => Effect.Effect<string | null, SecretOwnedByConnectionError | StorageFailure>; readonly getAtScope: ( id: string, scope: string, ) => Effect.Effect<string | null, SecretOwnedByConnectionError | StorageFailure>; /** Fast-path existence check — hits the core `secret` routing table * only, never calls the provider. Use this for UI state ("secret * missing, prompt to add") to avoid keychain permission prompts * or 1password IPC roundtrips on a pre-flight check. */ readonly status: (id: string) => Effect.Effect<"resolved" | "missing", StorageFailure>; readonly set: (input: SetSecretInput) => Effect.Effect<SecretRef, StorageFailure>; /** Delete a bare (non-connection-owned) secret. Connection-owned * secrets are rejected with `SecretOwnedByConnectionError` — use * `connections.remove` instead. Refuses with `SecretInUseError` * if any plugin reports the secret as in use; the caller should * show the `usages(id)` list and ask the user to detach first. */ readonly remove: ( input: RemoveSecretInput, ) => Effect.Effect<void, SecretOwnedByConnectionError | SecretInUseError | StorageFailure>; readonly list: () => Effect.Effect<readonly SecretRef[], StorageFailure>; /** Management view of visible secret rows. Unlike `list`, this does * not collapse same-id rows across scopes, so UI that writes exact * credential targets can show both personal and shared rows. */ readonly listAll: () => Effect.Effect<readonly SecretRef[], StorageFailure>; /** All places this secret is referenced — fans out across every * plugin's `usagesForSecret`. Used by the Secrets-tab "Used by" * list and by `remove` for its RESTRICT check. */ readonly usages: (id: string) => Effect.Effect<readonly Usage[], StorageFailure>; readonly providers: () => Effect.Effect<readonly string[]>; }; readonly connections: { readonly get: (id: string) => Effect.Effect<ConnectionRef | null, StorageFailure>; readonly getAtScope: ( id: string, scope: string, ) => Effect.Effect<ConnectionRef | null, StorageFailure>; readonly list: () => Effect.Effect<readonly ConnectionRef[], StorageFailure>; readonly create: ( input: CreateConnectionInput, ) => Effect.Effect<ConnectionRef, ConnectionProviderNotRegisteredError | StorageFailure>; readonly updateTokens: ( input: UpdateConnectionTokensInput, ) => Effect.Effect<ConnectionRef, ConnectionNotFoundError | StorageFailure>; readonly setIdentityLabel: ( id: string, label: string | null, ) => Effect.Effect<void, ConnectionNotFoundError | StorageFailure>; readonly accessToken: ( id: string, ) => Effect.Effect< string, | ConnectionNotFoundError | ConnectionProviderNotRegisteredError | ConnectionRefreshNotSupportedError | ConnectionReauthRequiredError | ConnectionRefreshError | StorageFailure >; readonly accessTokenAtScope: ( id: string, scope: string, ) => Effect.Effect< string, | ConnectionNotFoundError | ConnectionProviderNotRegisteredError | ConnectionRefreshNotSupportedError | ConnectionReauthRequiredError | ConnectionRefreshError | StorageFailure >; /** Refuses with `ConnectionInUseError` if any plugin reports the * connection as in use. */ readonly remove: ( input: RemoveConnectionInput, ) => Effect.Effect<void, ConnectionInUseError | StorageFailure>; /** All places this connection is referenced — fans out across every * plugin's `usagesForConnection`. */ readonly usages: (id: string) => Effect.Effect<readonly Usage[], StorageFailure>; readonly providers: () => Effect.Effect<readonly string[]>; }; /** Shared credential slot bindings. Plugins decide what slot keys mean; * core owns scoped storage, resolution status, and usage visibility. */ readonly credentialBindings: CredentialBindingsFacade; /** Shared OAuth service. Hosts use this through the core HTTP OAuth group; * plugins see the same service as `ctx.oauth`. */ readonly oauth: OAuthService; readonly policies: { /** All policies visible across the executor's scope stack, sorted * by (innermost-scope-first, position ascending) — i.e. the order * in which they're evaluated by first-match-wins. */ readonly list: () => Effect.Effect<readonly ToolPolicy[], StorageFailure>; /** Create a new policy. Defaults to the top of the target scope's * list (highest precedence) when `position` is omitted. */ readonly create: (input: CreateToolPolicyInput) => Effect.Effect<ToolPolicy, StorageFailure>; readonly update: (input: UpdateToolPolicyInput) => Effect.Effect<ToolPolicy, StorageFailure>; readonly remove: (input: RemoveToolPolicyInput) => Effect.Effect<void, StorageFailure>; /** Resolve the effective policy for a tool id by walking the scope- * stacked policy list with first-match-wins semantics. Returns * `undefined` when no rule matches (caller falls back to the * plugin's `resolveAnnotations` output). */ readonly resolve: (toolId: string) => Effect.Effect<PolicyMatch | undefined, StorageFailure>; }; readonly close: () => Effect.Effect<void, StorageFailure>;} & PluginExtensions<TPlugins>; export interface ExecutorDb { readonly db: FumaDb<any>; readonly close?: () => Effect.Effect<void, StorageFailure> | Promise<void> | void;} export type ExecutorDbInput = FumaDb<any> | ExecutorDb; export type ExecutorDbFactory = (config: { readonly tables: FumaTables;}) => ExecutorDbInput | Effect.Effect<ExecutorDbInput, StorageFailure>; export interface ExecutorConfig<TPlugins extends readonly AnyPlugin[] = readonly []> { /** * Precedence-ordered scope stack. Innermost first; typical shape is * `[userInOrgScope, orgScope]`. Reads on scoped tables walk the * stack (first hit wins for shadow-by-id consumers like secrets and * blobs); writes require callers to name an explicit target scope. * Must be non-empty. */ readonly scopes: readonly Scope[]; readonly db?: ExecutorDbInput | ExecutorDbFactory; readonly plugins?: TPlugins; /** * How to respond when a tool requests user input mid-invocation. Pass * `"accept-all"` for tests / non-interactive hosts, or a handler * `(ctx) => Effect<ElicitationResponse>` for interactive ones. * Required at construction so per-invoke calls don't have to thread * an options arg. */ readonly onElicitation: OnElicitation; readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>; readonly oauthEndpointUrlPolicy?: OAuthEndpointUrlPolicy; readonly sourceDetection?: { readonly maxUrlLength?: number; readonly maxDetectors?: number; readonly maxResults?: number; readonly timeout?: Duration.Input; readonly hostedOutboundPolicy?: boolean; }; /** * Enable the built-in `core-tools` plugin which contributes * agent-facing static tools (`scopes.list`, `secrets.list`, * `secrets.create`). The `webBaseUrl` is where the executor's web * UI lives; `secrets.create` builds a URL elicitation that points * the user at `${webBaseUrl}/secrets?...` so the plaintext value * never crosses the agent. * * Omit to skip registration (tests, MCP-only hosts that don't * surface a web UI, etc.). */ readonly coreTools?: { readonly webBaseUrl: string; };} // ---------------------------------------------------------------------------// collectTables — merge core tables with every plugin's declared Fuma table.// Hosts pass the result to FumaDB when constructing the database client.// --------------------------------------------------------------------------- export const collectTables = (plugins: readonly AnyPlugin[]): FumaTables => { const merged: FumaTables = { ...coreSchema }; for (const plugin of plugins) { if (!plugin.schema) continue; for (const [tableKey, tableDef] of Object.entries(plugin.schema)) { if (merged[tableKey]) { // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: collectTables is a synchronous configuration API throw new StorageError({ message: `Duplicate storage table "${tableKey}" contributed by plugin "${plugin.id}"` + ` (reserved by core or another plugin)`, cause: undefined, }); } merged[tableKey] = tableDef as FumaTables[string]; } } validateExecutorScopePolicyTables(merged); return merged;}; const validateExecutorScopePolicyTables = (tables: FumaTables): void => { for (const [tableKey, tableDef] of Object.entries(tables)) { assertExecutorScopePolicyTable(tableDef, tableKey); }}; const validateExecutorDbTables = (required: FumaTables, actual: FumaTables): void => { const missing = Object.keys(required) .filter((tableName) => !actual[tableName]) .sort(); if (missing.length === 0) return; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: synchronous startup validation before Executor services are built throw new StorageError({ message: `Executor database is missing required table definitions: ${missing.join(", ")}`, cause: { missing, available: Object.keys(actual).sort(), }, });}; const storageFailureFromUnknown = (message: string, cause: unknown): StorageFailure => isStorageFailure(cause) ? cause : new StorageError({ message, cause }); const pluginStorageFailure = (pluginId: string, hook: string, cause: unknown): StorageFailure => storageFailureFromUnknown(`${hook} failed for plugin ${pluginId}`, cause); const createDefaultMemoryDb = (tables: FumaTables): ExecutorDb => { const version = "1.0.0"; const latestSchema = fumaSchema<string, FumaTables, RelationsMap<FumaTables>>({ version, tables, }); const factory = fumadb({ namespace: "executor_memory", schemas: [latestSchema], }); // oxlint-disable-next-line executor/no-double-cast -- boundary: dynamic plugin table map is known only after collectTables() const db = factory.client(memoryAdapter()).orm(version) as unknown as FumaDb;Lines 1–500 of 4221. Load more
import { Deferred, Duration, Effect, Layer, Match, Option, Predicate, Result, Schema, Semaphore, } from "effect"; import { FetchHttpClient, type HttpClient } from "effect/unstable/http"; import { fumadb } from "fumadb"; import { memoryAdapter } from "fumadb/adapters/memory"; import { withQueryContext, type Condition, type ConditionBuilder } from "fumadb/query"; import { schema as fumaSchema, type RelationsMap } from "fumadb/schema"; import type { AnyColumn } from "fumadb/schema"; import type { OAuthEndpointUrlPolicy } from "./oauth-helpers"; import { generateKeyBetween } from "fractional-indexing"; import { StorageError, isStorageFailure, makeFumaClient, type FumaDb, type FumaRow, type FumaTables, type StorageFailure, } from "./fuma-runtime"; import { makeFumaBlobStore, pluginBlobStore } from "./blob"; import { coreToolsPlugin } from "./core-tools"; import { ConnectionProviderState, ConnectionRef, ConnectionRefreshError, type ConnectionProvider, type ConnectionRefreshResult, type CreateConnectionInput, type RemoveConnectionInput, type UpdateConnectionTokensInput, } from "./connections"; import { credentialBindingId, credentialBindingRowToRef, type CredentialBindingRef, type CredentialBindingsFacade, type CredentialBindingSlotInput, type CredentialBindingSourceInput, type RemoveCredentialBindingInput, type RemoveSourceCredentialBindingInput, type ReplaceCredentialBindingsInput, type ReplaceSourceCredentialBindingsInput, ResolvedCredentialSlot, type SetPluginCredentialBindingInput, type SetSourceCredentialBindingInput, type SourceCredentialBindingSlotInput, type SourceCredentialBindingSourceInput, } from "./credential-bindings"; import { coreSchema, isToolPolicyAction, type ConnectionRow, type CredentialBindingRow, type CoreSchema, type DefinitionsInput, type SecretRow, type SourceInput, type SourceRow, type ToolAnnotations, type ToolPolicyRow, type ToolRow, } from "./core-schema"; import { ElicitationDeclinedError, ElicitationResponse, FormElicitation, type ElicitationHandler, type ElicitationRequest, } from "./elicitation"; import { ConnectionInUseError, ConnectionNotFoundError, ConnectionProviderNotRegisteredError, ConnectionReauthRequiredError, ConnectionRefreshNotSupportedError, NoHandlerError, PluginNotLoadedError, SecretInUseError, SecretOwnedByConnectionError, SourceRemovalNotAllowedError, ToolBlockedError, ToolInvocationError, ToolNotFoundError, } from "./errors"; import { ConnectionId, ScopeId, SecretId, ToolId } from "./ids"; import { makeOAuth2Service } from "./oauth-service"; import type { OAuthService } from "./oauth"; import { comparePolicyRow, isValidPattern, resolveToolPolicy, rowToToolPolicy, type CreateToolPolicyInput, type PolicyMatch, type RemoveToolPolicyInput, type ToolPolicy, type UpdateToolPolicyInput, } from "./policies"; import type { AnyPlugin, Elicit, PluginCtx, PluginExtensions, SourceConfigureSchema, StaticSourceDecl, StaticToolDecl, StaticToolSchema, StorageDeps, } from "./plugin"; import { pluginStorageId, type PluginStorageEntry, type PluginStorageFacade, } from "./plugin-storage"; import type { Scope } from "./scope"; import { RemoveSecretInput, SecretRef, SetSecretInput, type SecretProvider } from "./secrets"; import { Usage } from "./usages"; import { ToolSchema, type RefreshSourceInput, type RemoveSourceInput, type Source, type SourceDetectionResult, type Tool, type ToolListFilter, } from "./types"; import { buildToolTypeScriptPreview, type ToolTypeScriptPreview } from "./schema-types"; import { collectReferencedDefinitions } from "./schema-refs"; import { assertExecutorScopePolicyTable, type ExecutorScopePolicyContext } from "./scope-policy"; import { validateHostedOutboundUrl } from "./hosted-http-client"; const MAX_ANNOTATION_GROUPS = 64; const MAX_APPROVAL_ARGUMENT_PREVIEW_CHARS = 4_000; // --------------------------------------------------------------------------- // Elicitation handler — set once at `createExecutor({ onElicitation })` // and threaded into every tool invocation. A tool that requests user // input mid-execution suspends the fiber and the handler decides how to // respond. Tools that never elicit simply don't trigger the handler. // // The "accept-all" sentinel is convenient for tests and CLI automation — // every elicitation request gets auto-accepted with an empty content // payload. For real interactive hosts, pass a real handler. // // Required at the executor level rather than per-invoke, so the // "what if a caller forgot to pass a handler" branch is structurally // impossible. Higher layers that need per-invocation handler control // (an MCP server bridging different per-client handlers, the execution // engine threading agent-loop callbacks) can pass an override via // `tools.invoke(id, args, { onElicitation })` — the executor-level // handler is the fallback, never null. // --------------------------------------------------------------------------- export type OnElicitation = ElicitationHandler | "accept-all"; export interface InvokeOptions { /** Override the executor-level handler for this single call. */ readonly onElicitation?: OnElicitation; } const acceptAllHandler: ElicitationHandler = () => Effect.succeed(ElicitationResponse.make({ action: "accept" })); const resolveElicitationHandler = (onElicitation: OnElicitation): ElicitationHandler => onElicitation === "accept-all" ? acceptAllHandler : onElicitation; // --------------------------------------------------------------------------- // Executor — public surface. Every list/invoke/schema call is a direct // core-table query (for dynamic rows) unioned with the in-memory static // pool. No ToolRegistry, no SourceRegistry, no SecretStore services. // --------------------------------------------------------------------------- export type Executor<TPlugins extends readonly AnyPlugin[] = readonly []> = { /** * Precedence-ordered scope stack this executor was configured with. * Innermost first. Consumers that need "the display scope" typically * pick `scopes.at(-1)` (outermost, e.g. the organization) or * `scopes[0]` (innermost, e.g. the current user-in-org) depending on * what they're rendering. */ readonly scopes: readonly Scope[]; readonly tools: { readonly list: (filter?: ToolListFilter) => Effect.Effect<readonly Tool[], StorageFailure>; /** Fetch a tool's schema view: JSON schemas with `$defs` * attached from the core `definition` table, plus TypeScript * preview strings. Returns `null` for unknown tool ids. */ readonly schema: (toolId: string) => Effect.Effect<ToolSchema | null, StorageFailure>; /** Every `$defs` entry across every source, grouped by source id. * Used for bulk schema export and downstream TypeScript rendering. */ readonly definitions: () => Effect.Effect< Record<string, Record<string, unknown>>, StorageFailure >; readonly invoke: ( toolId: string, args: unknown, options?: InvokeOptions, ) => Effect.Effect< unknown, | ToolNotFoundError | ToolBlockedError | PluginNotLoadedError | NoHandlerError | ToolInvocationError | ElicitationDeclinedError | StorageFailure >; }; readonly sources: { readonly list: () => Effect.Effect<readonly Source[], StorageFailure>; readonly remove: ( input: RemoveSourceInput, ) => Effect.Effect<void, SourceRemovalNotAllowedError | StorageFailure>; readonly refresh: (input: RefreshSourceInput) => Effect.Effect<void, StorageFailure>; /** URL autodetection — fans out to every plugin's `detect` hook * (if declared), returns every high/medium/low-confidence match. * UI picks a winner from the list. */ readonly detect: ( url: string, ) => Effect.Effect<readonly SourceDetectionResult[], StorageFailure>; /** All `$defs` registered for a single source, keyed by def name. */ readonly definitions: ( sourceId: string, ) => Effect.Effect<Record<string, unknown>, StorageFailure>; readonly configure: (input: { readonly source: { readonly id: string; readonly scope: ScopeId | string; }; readonly scope: ScopeId | string; readonly type?: string; readonly config: unknown; }) => Effect.Effect<unknown, StorageFailure>; readonly listBindings: ( input: SourceCredentialBindingSourceInput, ) => Effect.Effect<readonly CredentialBindingRef[], StorageFailure>; readonly resolveBinding: ( input: SourceCredentialBindingSlotInput, ) => Effect.Effect<CredentialBindingRef | null, StorageFailure>; readonly setBinding: ( input: SetSourceCredentialBindingInput, ) => Effect.Effect<CredentialBindingRef, StorageFailure>; readonly removeBinding: ( input: RemoveSourceCredentialBindingInput, ) => Effect.Effect<void, StorageFailure>; readonly replaceBindings: ( input: ReplaceSourceCredentialBindingsInput, ) => Effect.Effect<readonly CredentialBindingRef[], StorageFailure>; }; readonly secrets: { readonly get: ( id: string, ) => Effect.Effect<string | null, SecretOwnedByConnectionError | StorageFailure>; readonly getAtScope: ( id: string, scope: string, ) => Effect.Effect<string | null, SecretOwnedByConnectionError | StorageFailure>; /** Fast-path existence check — hits the core `secret` routing table * only, never calls the provider. Use this for UI state ("secret * missing, prompt to add") to avoid keychain permission prompts * or 1password IPC roundtrips on a pre-flight check. */ readonly status: (id: string) => Effect.Effect<"resolved" | "missing", StorageFailure>; readonly set: (input: SetSecretInput) => Effect.Effect<SecretRef, StorageFailure>; /** Delete a bare (non-connection-owned) secret. Connection-owned * secrets are rejected with `SecretOwnedByConnectionError` — use * `connections.remove` instead. Refuses with `SecretInUseError` * if any plugin reports the secret as in use; the caller should * show the `usages(id)` list and ask the user to detach first. */ readonly remove: ( input: RemoveSecretInput, ) => Effect.Effect<void, SecretOwnedByConnectionError | SecretInUseError | StorageFailure>; readonly list: () => Effect.Effect<readonly SecretRef[], StorageFailure>; /** Management view of visible secret rows. Unlike `list`, this does * not collapse same-id rows across scopes, so UI that writes exact * credential targets can show both personal and shared rows. */ readonly listAll: () => Effect.Effect<readonly SecretRef[], StorageFailure>; /** All places this secret is referenced — fans out across every * plugin's `usagesForSecret`. Used by the Secrets-tab "Used by" * list and by `remove` for its RESTRICT check. */ readonly usages: (id: string) => Effect.Effect<readonly Usage[], StorageFailure>; readonly providers: () => Effect.Effect<readonly string[]>; }; readonly connections: { readonly get: (id: string) => Effect.Effect<ConnectionRef | null, StorageFailure>; readonly getAtScope: ( id: string, scope: string, ) => Effect.Effect<ConnectionRef | null, StorageFailure>; readonly list: () => Effect.Effect<readonly ConnectionRef[], StorageFailure>; readonly create: ( input: CreateConnectionInput, ) => Effect.Effect<ConnectionRef, ConnectionProviderNotRegisteredError | StorageFailure>; readonly updateTokens: ( input: UpdateConnectionTokensInput, ) => Effect.Effect<ConnectionRef, ConnectionNotFoundError | StorageFailure>; readonly setIdentityLabel: ( id: string, label: string | null, ) => Effect.Effect<void, ConnectionNotFoundError | StorageFailure>; readonly accessToken: ( id: string, ) => Effect.Effect< string, | ConnectionNotFoundError | ConnectionProviderNotRegisteredError | ConnectionRefreshNotSupportedError | ConnectionReauthRequiredError | ConnectionRefreshError | StorageFailure >; readonly accessTokenAtScope: ( id: string, scope: string, ) => Effect.Effect< string, | ConnectionNotFoundError | ConnectionProviderNotRegisteredError | ConnectionRefreshNotSupportedError | ConnectionReauthRequiredError | ConnectionRefreshError | StorageFailure >; /** Refuses with `ConnectionInUseError` if any plugin reports the * connection as in use. */ readonly remove: ( input: RemoveConnectionInput, ) => Effect.Effect<void, ConnectionInUseError | StorageFailure>; /** All places this connection is referenced — fans out across every * plugin's `usagesForConnection`. */ readonly usages: (id: string) => Effect.Effect<readonly Usage[], StorageFailure>; readonly providers: () => Effect.Effect<readonly string[]>; }; /** Shared credential slot bindings. Plugins decide what slot keys mean; * core owns scoped storage, resolution status, and usage visibility. */ readonly credentialBindings: CredentialBindingsFacade; /** Shared OAuth service. Hosts use this through the core HTTP OAuth group; * plugins see the same service as `ctx.oauth`. */ readonly oauth: OAuthService; readonly policies: { /** All policies visible across the executor's scope stack, sorted * by (innermost-scope-first, position ascending) — i.e. the order * in which they're evaluated by first-match-wins. */ readonly list: () => Effect.Effect<readonly ToolPolicy[], StorageFailure>; /** Create a new policy. Defaults to the top of the target scope's * list (highest precedence) when `position` is omitted. */ readonly create: (input: CreateToolPolicyInput) => Effect.Effect<ToolPolicy, StorageFailure>; readonly update: (input: UpdateToolPolicyInput) => Effect.Effect<ToolPolicy, StorageFailure>; readonly remove: (input: RemoveToolPolicyInput) => Effect.Effect<void, StorageFailure>; /** Resolve the effective policy for a tool id by walking the scope- * stacked policy list with first-match-wins semantics. Returns * `undefined` when no rule matches (caller falls back to the * plugin's `resolveAnnotations` output). */ readonly resolve: (toolId: string) => Effect.Effect<PolicyMatch | undefined, StorageFailure>; }; readonly close: () => Effect.Effect<void, StorageFailure>; } & PluginExtensions<TPlugins>; export interface ExecutorDb { readonly db: FumaDb<any>; readonly close?: () => Effect.Effect<void, StorageFailure> | Promise<void> | void; } export type ExecutorDbInput = FumaDb<any> | ExecutorDb; export type ExecutorDbFactory = (config: { readonly tables: FumaTables; }) => ExecutorDbInput | Effect.Effect<ExecutorDbInput, StorageFailure>; export interface ExecutorConfig<TPlugins extends readonly AnyPlugin[] = readonly []> { /** * Precedence-ordered scope stack. Innermost first; typical shape is * `[userInOrgScope, orgScope]`. Reads on scoped tables walk the * stack (first hit wins for shadow-by-id consumers like secrets and * blobs); writes require callers to name an explicit target scope. * Must be non-empty. */ readonly scopes: readonly Scope[]; readonly db?: ExecutorDbInput | ExecutorDbFactory; readonly plugins?: TPlugins; /** * How to respond when a tool requests user input mid-invocation. Pass * `"accept-all"` for tests / non-interactive hosts, or a handler * `(ctx) => Effect<ElicitationResponse>` for interactive ones. * Required at construction so per-invoke calls don't have to thread * an options arg. */ readonly onElicitation: OnElicitation; readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>; readonly oauthEndpointUrlPolicy?: OAuthEndpointUrlPolicy; readonly sourceDetection?: { readonly maxUrlLength?: number; readonly maxDetectors?: number; readonly maxResults?: number; readonly timeout?: Duration.Input; readonly hostedOutboundPolicy?: boolean; }; /** * Enable the built-in `core-tools` plugin which contributes * agent-facing static tools (`scopes.list`, `secrets.list`, * `secrets.create`). The `webBaseUrl` is where the executor's web * UI lives; `secrets.create` builds a URL elicitation that points * the user at `${webBaseUrl}/secrets?...` so the plaintext value * never crosses the agent. * * Omit to skip registration (tests, MCP-only hosts that don't * surface a web UI, etc.). */ readonly coreTools?: { readonly webBaseUrl: string; }; } // --------------------------------------------------------------------------- // collectTables — merge core tables with every plugin's declared Fuma table. // Hosts pass the result to FumaDB when constructing the database client. // --------------------------------------------------------------------------- export const collectTables = (plugins: readonly AnyPlugin[]): FumaTables => { const merged: FumaTables = { ...coreSchema }; for (const plugin of plugins) { if (!plugin.schema) continue; for (const [tableKey, tableDef] of Object.entries(plugin.schema)) { if (merged[tableKey]) { // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: collectTables is a synchronous configuration API throw new StorageError({ message: `Duplicate storage table "${tableKey}" contributed by plugin "${plugin.id}"` + ` (reserved by core or another plugin)`, cause: undefined, }); } merged[tableKey] = tableDef as FumaTables[string]; } } validateExecutorScopePolicyTables(merged); return merged; }; const validateExecutorScopePolicyTables = (tables: FumaTables): void => { for (const [tableKey, tableDef] of Object.entries(tables)) { assertExecutorScopePolicyTable(tableDef, tableKey); } }; const validateExecutorDbTables = (required: FumaTables, actual: FumaTables): void => { const missing = Object.keys(required) .filter((tableName) => !actual[tableName]) .sort(); if (missing.length === 0) return; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: synchronous startup validation before Executor services are built throw new StorageError({ message: `Executor database is missing required table definitions: ${missing.join(", ")}`, cause: { missing, available: Object.keys(actual).sort(), }, }); }; const storageFailureFromUnknown = (message: string, cause: unknown): StorageFailure => isStorageFailure(cause) ? cause : new StorageError({ message, cause }); const pluginStorageFailure = (pluginId: string, hook: string, cause: unknown): StorageFailure => storageFailureFromUnknown(`${hook} failed for plugin ${pluginId}`, cause); const createDefaultMemoryDb = (tables: FumaTables): ExecutorDb => { const version = "1.0.0"; const latestSchema = fumaSchema<string, FumaTables, RelationsMap<FumaTables>>({ version, tables, }); const factory = fumadb({ namespace: "executor_memory", schemas: [latestSchema], }); // oxlint-disable-next-line executor/no-double-cast -- boundary: dynamic plugin table map is known only after collectTables() const db = factory.client(memoryAdapter()).orm(version) as unknown as FumaDb;