import { Effect } from "effect"; import { definePlugin, ToolName, type ResolveToolsInput, type ResolveToolsResult, type InvokeToolInput, type ToolDef, } from "@executor-js/sdk"; import type { AppsRuntime } from "./runtime"; import { makeAppsStore } from "./store"; import type { Bindings } from "./bindings"; // --------------------------------------------------------------------------- // The apps source plugin. Published custom tools become catalog citizens: the // plugin registers one integration per scope (`apps`), and a connection to it // makes the published tools resolvable + invocable like any catalog tool, so // policy / approval / audit / toolkits / tools.list all apply unchanged. // // `resolveTools` projects the published descriptor into `ToolDef[]`. // `invokeTool` bundles + runs the tool in the sandbox with connections bound. // The plugin is thin: all logic lives in `AppsRuntime` (shared with the HTTP + // MCP surfaces). The runtime is supplied via options because it owns the seam // instances built at host boot. // --------------------------------------------------------------------------- export const APPS_INTEGRATION_SLUG = "apps"; export const APPS_PLUGIN_ID = "apps"; export interface AppsPluginOptions { /** The shared runtime (seams + store). Built at host boot. */ readonly runtime: AppsRuntime; /** How a tool's declared connection roles are bound to the caller's * connections at invoke time. Self-host resolves these from the scope's * configured connections; a default binds each role to a connection of the * same name as the integration. */ readonly resolveBindings?: (input: { readonly scope: string; readonly tool: string; readonly declared: Readonly>; }) => Bindings; } const defaultBindings = ( declared: Readonly>, ): Bindings => { const out: Record = {}; for (const [role, decl] of Object.entries(declared)) { if (decl.kind === "array") { out[role] = { kind: "array", connections: [decl.integration ?? role] }; } else if (decl.kind === "catalog") { // no binding } else { out[role] = { kind: "single", connection: decl.integration ?? role }; } } return out; }; interface AppsStoreShape { readonly runtime: AppsRuntime; } export const appsPlugin = definePlugin((options?: AppsPluginOptions) => { if (!options?.runtime) { throw new Error("appsPlugin requires a `runtime` (built from the five seams at host boot)"); } const runtime = options.runtime; const resolveBindings = options.resolveBindings; return { id: APPS_PLUGIN_ID as "apps", packageName: "@executor-js/plugin-apps", // The plugin's store facade is host-owned plugin storage + blobs; the apps // runtime already holds its own store, so this is a thin passthrough kept // for the ctx shape (extension methods read the runtime). storage: (deps): AppsStoreShape => { void makeAppsStore({ pluginStorage: deps.pluginStorage, blobs: deps.blobs, }); return { runtime }; }, // Declare the plugin's storage collection so the host provisions it. pluginStorage: { published_descriptor: { name: "published_descriptor", schema: { Type: {} as Record }, indexes: [], }, }, extension: () => ({ runtime }), // Per-connection tool production: project the published descriptor into // ToolDefs. Called at connection create/refresh; the SDK stamps addresses // and persists per connection. resolveTools: ({ connection }: ResolveToolsInput) => Effect.gen(function* () { // The scope is the connection owner's tenant; for self-host single // tenant we key the descriptor by the connection's integration-scoped // name. We read the published descriptor for the scope encoded in the // connection name (`apps/`), falling back to the connection name. const scope = scopeFromConnection(connection.name); const descriptor = yield* runtime.getDescriptor(scope); if (!descriptor) return { tools: [] } satisfies ResolveToolsResult; const tools: ToolDef[] = descriptor.tools.map((t) => ({ name: ToolName.make(t.name), description: t.description, inputSchema: t.inputSchema, outputSchema: t.outputSchema, annotations: { requiresApproval: t.annotations?.destructive === true, }, })); return { tools } satisfies ResolveToolsResult; }), invokeTool: ({ toolRow, args }: InvokeToolInput) => Effect.gen(function* () { const scope = scopeFromConnection(toolRow.connection); const descriptor = yield* runtime.getDescriptor(scope); const toolDesc = descriptor?.tools.find((t) => t.name === toolRow.name); const declared = toolDesc?.connections ?? {}; const bindings = resolveBindings ? resolveBindings({ scope, tool: toolRow.name, declared }) : defaultBindings(declared); return yield* runtime .invokeTool({ scope, tool: toolRow.name, args, bindings }) .pipe( Effect.mapError( (cause) => new Error( "message" in cause && typeof cause.message === "string" ? cause.message : "apps tool invocation failed", ), ), ); }), }; }); // Connection names encode the scope as `apps/` (or are the scope itself). const scopeFromConnection = (connectionName: string): string => { const slash = connectionName.indexOf("/"); return slash === -1 ? connectionName : connectionName.slice(slash + 1); };