4221 lines · 163.0 KB · TypeScript
RawDownload
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.