800 lines · 34.8 KB · TypeScript
RawDownload
import { Effect, type Schema as EffectSchema } from "effect";import type { Context, Layer } from "effect";import type { HttpClient } from "effect/unstable/http";import type { HttpApiGroup } from "effect/unstable/httpapi";import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec";import type { FumaTables, IFumaClient, StorageFailure, TablesToFumaSchema } from "./fuma-runtime"; import type { PluginBlobStore } from "./blob";import type {  ConnectionProvider,  ConnectionRef,  ConnectionRefreshError,  CreateConnectionInput,  RemoveConnectionInput,  UpdateConnectionTokensInput,} from "./connections";import type {  CredentialBindingRef,  CredentialBindingsFacade,  RemoveSourceCredentialBindingInput,  SetSourceCredentialBindingInput,  SourceCredentialBindingSlotInput,  SourceCredentialBindingSourceInput,} from "./credential-bindings";import type { DefinitionsInput, SourceInput, ToolAnnotations, ToolRow } from "./core-schema";import type { ScopeId } from "./ids";import type { RefreshSourceInput, RemoveSourceInput, Source, SourceDetectionResult } from "./types";import type {  ElicitationDeclinedError,  ElicitationHandler,  ElicitationRequest,  ElicitationResponse,} from "./elicitation";import type {  ConnectionInUseError,  ConnectionNotFoundError,  ConnectionProviderNotRegisteredError,  ConnectionReauthRequiredError,  ConnectionRefreshNotSupportedError,  SecretInUseError,  SecretOwnedByConnectionError,  SourceRemovalNotAllowedError,} from "./errors";import type { OAuthService } from "./oauth";import type { PluginStorageFacade } from "./plugin-storage";import type {  CreateToolPolicyInput,  RemoveToolPolicyInput,  ToolPolicy,  UpdateToolPolicyInput,} from "./policies";import type { Scope } from "./scope";import type { RemoveSecretInput, SecretProvider, SecretRef, SetSecretInput } from "./secrets";import type { Usage, UsagesForConnectionInput, UsagesForSecretInput } from "./usages"; // ---------------------------------------------------------------------------// StorageDeps — backing passed to a plugin's `storage` factory. Plugins see// FumaDB through the Effect boundary, narrowed to their declared tables. Scope// behavior is domain code, not hidden adapter behavior: reads should include// `scopedWhere(...)` and writes stamp an explicit `scope_id`.// --------------------------------------------------------------------------- export interface StorageDeps<TTables extends FumaTables | undefined = FumaTables> {  /**   * Precedence-ordered scope stack visible to this executor. Innermost   * first. Reads on scoped tables walk every scope; writes require the   * plugin to name a target scope explicitly (via `scope_id` on the   * row payload, via `options.scope` on the blob store).   */  readonly scopes: readonly Scope[];  /** Plugin-facing FumaDB query boundary. */  readonly fuma: IFumaClient<TablesToFumaSchema<TTables>>;  readonly blobs: PluginBlobStore;  readonly pluginStorage: PluginStorageFacade;} // ---------------------------------------------------------------------------// Elicit — suspends the fiber, calls the invoke-time elicitation// handler, resumes with the user's response. Available on both static// tool handlers and dynamic `invokeTool` handlers. Threaded through// the executor from `createExecutor({ onElicitation })`.// --------------------------------------------------------------------------- export type Elicit = (  request: ElicitationRequest,) => Effect.Effect<ElicitationResponse, ElicitationDeclinedError>; // ---------------------------------------------------------------------------// PluginCtx — threaded into every extension method, static tool handler,// and dynamic tool handler. No raw adapter, no raw blobs. Core writes// go through `core.sources.register` / `core.definitions.register`.// --------------------------------------------------------------------------- export interface PluginCtx<TStore = unknown> {  /**   * Precedence-ordered scope stack visible to this executor. Innermost   * first. Plugins that write scoped rows must pick an element of   * `scopes` as the `scope`/`scope_id` they stamp; reads should apply   * explicit FumaDB scope predicates or use `ctx.secrets` / `ctx.connections`   * helpers.   */  readonly scopes: readonly Scope[];  readonly storage: TStore;  readonly pluginStorage: PluginStorageFacade;  readonly httpClientLayer: Layer.Layer<HttpClient.HttpClient>;   readonly core: {    readonly sources: {      readonly register: (input: SourceInput) => Effect.Effect<void, StorageFailure>;      readonly unregister: (input: RemoveSourceInput) => Effect.Effect<void, StorageFailure>;      readonly update: (input: {        readonly id: string;        readonly scope: string;        readonly name?: string;        readonly url?: string | null;      }) => Effect.Effect<void, StorageFailure>;      readonly list: () => Effect.Effect<readonly Source[], StorageFailure>;      readonly remove: (        input: RemoveSourceInput,      ) => Effect.Effect<void, SourceRemovalNotAllowedError | StorageFailure>;      readonly refresh: (input: RefreshSourceInput) => Effect.Effect<void, StorageFailure>;      readonly detect: (        url: string,      ) => Effect.Effect<readonly SourceDetectionResult[], 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>;      /** Source configuration declarations for every plugin that       *  supports `executor.sources.configure`. Exposed to core tools       *  so agent-facing configuration surfaces can describe the       *  plugin-specific payload shape before dispatching a write. */      readonly configureSchemas: () => readonly SourceConfigureSchema[];      readonly presets: () => readonly SourcePresetCatalogEntry[];    };    readonly policies: {      readonly list: () => Effect.Effect<readonly ToolPolicy[], StorageFailure>;      readonly create: (input: CreateToolPolicyInput) => Effect.Effect<ToolPolicy, StorageFailure>;      readonly update: (input: UpdateToolPolicyInput) => Effect.Effect<ToolPolicy, StorageFailure>;      readonly remove: (input: RemoveToolPolicyInput) => Effect.Effect<void, StorageFailure>;    };    /** Register shared JSON-schema `$defs` for a source. Tool     *  input/output schemas registered via `sources.register` can carry     *  `$ref: "#/$defs/X"` pointers; `executor.tools.schema(toolId)`     *  returns matching reachable defs alongside the schema roots. Call     *  inside the same `ctx.transaction` as `sources.register` for atomicity.     *  Replaces any existing defs for the given sourceId. */    readonly definitions: {      readonly register: (input: DefinitionsInput) => Effect.Effect<void, 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>;    /** List user-visible secrets. Connection-owned secrets (rows with     *  `owned_by_connection_id` set) are filtered out so they don't     *  clutter the UI — users see the Connection instead. */    readonly list: () => Effect.Effect<      readonly {        readonly id: string;        readonly scopeId: ScopeId;        readonly name: string;        readonly provider: string;      }[],      StorageFailure    >;    readonly status: (id: string) => Effect.Effect<"resolved" | "missing", StorageFailure>;    readonly usages: (id: string) => Effect.Effect<readonly Usage[], StorageFailure>;    readonly providers: () => Effect.Effect<readonly string[], never>;    /** Write a secret value through a provider. Used by plugins that     *  mint secrets on behalf of the user (OAuth2 token storage,     *  interactive onboarding flows). Normally writes go through     *  `executor.secrets.set` on the host surface, but OAuth2 refresh     *  and one-shot token capture from plugin-owned flows need it here     *  too. Same routing rules as the host-level setter. */    readonly set: (input: SetSecretInput) => Effect.Effect<SecretRef, StorageFailure>;    /** Delete a secret from its pinned provider and the core table.     *  Rejects with `SecretOwnedByConnectionError` if the row is owned     *  by a connection — callers must go through `connections.remove`     *  to drop the whole sign-in. Rejects with `SecretInUseError` if     *  any plugin reports the secret as in use; the caller should ask     *  the user to detach the listed sources first. */    readonly remove: (      input: RemoveSecretInput,    ) => Effect.Effect<void, SecretOwnedByConnectionError | SecretInUseError | StorageFailure>;  };   /** Connections — product-level sign-in state. Owns backing secret   *  rows via `secret.owned_by_connection_id`. Plugins call   *  `connections.accessToken(id)` at invoke time to get a guaranteed-   *  fresh token (the SDK handles refresh via the registered provider   *  keyed by `connection.provider`). */  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 usages: (id: string) => Effect.Effect<readonly Usage[], StorageFailure>;    readonly providers: () => Effect.Effect<readonly string[], never>;    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>;    /** Get a guaranteed-fresh access token. Calls the provider's     *  `refresh` handler if `expires_at` is in the past / within the     *  refresh skew window. */    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. Caller surfaces the `usages` list to the     *  user. */    readonly remove: (      input: RemoveConnectionInput,    ) => Effect.Effect<void, ConnectionInUseError | StorageFailure>;  };   readonly credentialBindings: CredentialBindingsFacade;   /** Shared OAuth service. Plugins use this to probe/start/complete OAuth   *  flows; invocation should still resolve tokens via `connections.accessToken`. */  readonly oauth: OAuthService;   /** Run `effect` inside a FumaDB transaction. Use this in extension methods that   *  need atomicity across plugin storage writes AND core source/tool   *  registration. */  readonly transaction: <A, E>(effect: Effect.Effect<A, E>) => Effect.Effect<A, E | StorageFailure>;} // ---------------------------------------------------------------------------// Static tool / source declarations. Pure data + handlers declared at// plugin-definition time.//// Importantly, `StaticToolDecl.handler` does NOT reference TExtension.// If it did, the nested generic would break inference for the whole// PluginSpec (TS would fall back to the `object` constraint on TExtension).// `self: NoInfer<TExtension>` lives on `staticSources` one level up// instead, and plugin authors close over it via the arrow-function// closure when they write their handler.// --------------------------------------------------------------------------- export interface StaticToolHandlerInput<TStore = unknown> {  readonly ctx: PluginCtx<TStore>;  readonly args: unknown;  /** Suspend the fiber to request user input. The handler passed to   *  `createExecutor({ onElicitation })` is called. */  readonly elicit: Elicit;} export interface StaticToolExecuteContext<TStore = unknown> {  readonly ctx: PluginCtx<TStore>;  /** Suspend the fiber to request user input. The handler passed to   *  `createExecutor({ onElicitation })` is called. */  readonly elicit: Elicit;} export type StaticToolSchema<Input = unknown, Output = Input> = StandardSchemaV1<Input, Output> &  StandardJSONSchemaV1<Input, Output>; export interface StaticToolDecl<TStore = unknown> {  readonly name: string;  readonly description: string;  readonly inputSchema?: StaticToolSchema;  readonly outputSchema?: StaticToolSchema;  /** Default-policy annotations — `requiresApproval`, `approvalDescription`,   *  `mayElicit`. Enforced by the executor before the handler runs.   *  Inline because static tools have no plugin storage to resolve from;   *  the plugin author literally writes this at definition time. */  readonly annotations?: ToolAnnotations;  readonly handler: (input: StaticToolHandlerInput<TStore>) => Effect.Effect<unknown, unknown>;} const decodeStaticToolArgs = (  schema: StaticToolSchema | undefined,  args: unknown,): Effect.Effect<unknown, unknown> => {  if (schema == null) return Effect.succeed(args);  return Effect.promise(() => Promise.resolve(schema["~standard"].validate(args))).pipe(    Effect.flatMap((result) =>      "value" in result ? Effect.succeed(result.value) : Effect.fail(result),    ),  );}; export interface StaticToolInput<  TStore = unknown,  TInputSchema extends StaticToolSchema | undefined = StaticToolSchema | undefined,> {  readonly name: string;  readonly description: string;  readonly inputSchema?: TInputSchema;  readonly outputSchema?: StaticToolSchema;  /** Default-policy annotations — `requiresApproval`, `approvalDescription`,   *  `mayElicit`. Enforced by the executor before the handler runs. */  readonly annotations?: ToolAnnotations;  readonly execute: (    args: TInputSchema extends StaticToolSchema      ? StandardSchemaV1.InferOutput<TInputSchema>      : unknown,    context: StaticToolExecuteContext<TStore>,  ) => Effect.Effect<unknown, unknown>;} export const tool = <  TStore = unknown,  TInputSchema extends StaticToolSchema | undefined = StaticToolSchema | undefined,>(  input: StaticToolInput<TStore, TInputSchema>,): StaticToolDecl<TStore> => ({  name: input.name,  description: input.description,  inputSchema: input.inputSchema,  outputSchema: input.outputSchema,  annotations: input.annotations,  handler: ({ args, ctx, elicit }) =>    decodeStaticToolArgs(input.inputSchema, args).pipe(      Effect.flatMap((decoded) =>        input.execute(          decoded as TInputSchema extends StaticToolSchema            ? StandardSchemaV1.InferOutput<TInputSchema>            : unknown,          { ctx, elicit },        ),      ),    ),}); export interface StaticSourceDecl<TStore = unknown> {  readonly id: string;  readonly kind: string;  readonly name: string;  readonly url?: string;  /** Static sources default to `canRemove: false` because they are   *  plugin-provided surfaces and shouldn't usually be user-removable.   *  Override only if you really want that. */  readonly canRemove?: boolean;  readonly canRefresh?: boolean;  readonly canEdit?: boolean;  readonly tools: readonly StaticToolDecl<TStore>[];} // ---------------------------------------------------------------------------// Dynamic invoke / source lifecycle inputs.// --------------------------------------------------------------------------- export interface InvokeToolInput<TStore = unknown> {  readonly ctx: PluginCtx<TStore>;  /** Already-loaded tool row. Plugin doesn't need to re-fetch or parse   *  the tool id. Carries source_id, name, input/output schemas,   *  annotations. */  readonly toolRow: ToolRow;  readonly args: unknown;  /** Elicitation handle for plugins that need mid-invocation user input   *  (onepassword auth prompt, interactive MCP tools, etc.). */  readonly elicit: Elicit;} export interface SourceLifecycleInput<TStore = unknown> {  readonly ctx: PluginCtx<TStore>;  readonly sourceId: string;  /**   * Scope of the source row being removed/refreshed — resolved by the   * SDK's `sources.remove` / `sources.refresh` via innermost-wins lookup   * across the executor's scope stack. Plugins that own a side table   * keyed by (id, scope_id) must pin their own cleanup to this scope;   * relying on stack-wide scope fall-through   * would widen the mutation across the whole stack and wipe a   * shadowed outer-scope row.   */  readonly scope: string;} export interface ConfigureSourceHandlerInput<TStore = unknown> {  readonly ctx: PluginCtx<TStore>;  readonly sourceId: string;  readonly sourceScope: string;  readonly targetScope: string;  readonly config: unknown;} export interface SourceConfigureDecl<TStore = unknown> {  readonly type: string;  readonly schema?: StaticToolSchema | EffectSchema.Decoder<unknown, never>;  readonly configure: (    input: ConfigureSourceHandlerInput<TStore>,  ) => Effect.Effect<unknown, unknown>;} export interface SourceConfigureSchema {  readonly pluginId: string;  readonly type: string;  readonly schema?: unknown;} export interface SourcePreset {  readonly id: string;  readonly name: string;  readonly summary: string;  readonly url?: string;  readonly endpoint?: string;  readonly icon?: string;  readonly featured?: boolean;  readonly transport?: "remote" | "stdio";  readonly command?: string;  readonly args?: readonly string[];  readonly env?: Readonly<Record<string, string>>;} export interface SourcePresetCatalogEntry extends SourcePreset {  readonly pluginId: string;} // ---------------------------------------------------------------------------// PluginSpec — what a `definePlugin(factory)` call returns.// --------------------------------------------------------------------------- // Defaults are `any` for slots that surface in contravariant positions// (storage/extension callbacks consume `TStore`/`TSchema`; `staticSources`// closes over `TExtension` via `NoInfer`). `any` is bivariant, so// `Plugin<string>` is a structural supertype of every concrete plugin// — `AnyPlugin = Plugin<string>` keeps the generic explosion contained// to this single declaration. Concrete specs ignore the defaults; TS// infers each slot from the literal returned by the author factory.//// eslint-disable-next-line @typescript-eslint/no-explicit-anyexport interface PluginSpec<  TId extends string = string,  // eslint-disable-next-line @typescript-eslint/no-explicit-any  TExtension extends object = any,  // eslint-disable-next-line @typescript-eslint/no-explicit-any  TStore = any,  // eslint-disable-next-line @typescript-eslint/no-explicit-any  TSchema extends FumaTables | undefined = any,  // eslint-disable-next-line @typescript-eslint/no-explicit-any  TExtensionService extends Context.Service<any, any> | undefined = any,  // eslint-disable-next-line @typescript-eslint/no-explicit-any  THandlersLayer extends Layer.Layer<any, any, any> = any,  TGroup extends HttpApiGroup.Any = HttpApiGroup.Any,> {  readonly id: TId;  /** npm package name. The Vite plugin uses this to derive the   *  `./client` import path for the frontend bundle (so the same   *  `executor.config.ts` drives both server and client) — `${packageName}/client`   *  is what gets bundled. The author writes the same string they   *  publish to npm; no transforms, no scope conventions. Required for   *  plugins that ship a `./client` entry; can be omitted for SDK-only   *  plugins (no client bundle = nothing to resolve). */  readonly packageName?: string;  /** Plugin-declared schema. Merged with coreSchema and other plugins'

Lines 1–500 of 800.