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. Load more
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-any export 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'