UNPKG

@mastra/core

Version:
344 lines (343 loc) 13 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const require_request_context = require("../request-context-ByoZMp-j.cjs"); //#region src/tool-provider/types.ts /** * Constant authorId used to bucket {@link ToolProviderConnection}s with * `scope: 'shared'`. */ const SHARED_BUCKET_ID = "shared"; //#endregion //#region src/tool-provider/base.ts /** * Shared base class for concrete {@link ToolProvider} implementations. * * Subclasses implement the SDK-specific `listAllToolkits` and * `listAllTools` methods (and the runtime / auth methods); the base class * layers admin allowlist filtering on top so every adapter behaves the * same way. */ var BaseToolProvider = class { allowedToolkits; allowedTools; defaultScope; constructor(options = {}) { this.allowedToolkits = options.allowedToolkits ?? []; this.allowedTools = options.allowedTools ?? {}; this.defaultScope = options.defaultScope; } async listToolkitsVNext() { const all = await this.listAllToolkits(); return { data: this.allowedToolkits.length === 0 ? all : all.filter((toolkit) => matchesAny(toolkit.slug, this.allowedToolkits)) }; } async listToolsVNext(opts = {}) { if (opts.toolkit !== void 0 && this.allowedToolkits.length > 0 && !matchesAny(opts.toolkit, this.allowedToolkits)) return { data: [], pagination: { page: opts.page ?? 1, perPage: opts.perPage, hasMore: false } }; const result = await this.listAllTools(opts); if (Object.keys(this.allowedTools).length === 0) return result; return { ...result, data: result.data.filter((tool) => { const toolkit = tool.toolkit; if (!toolkit) return true; const patterns = this.allowedTools[toolkit]; if (patterns === void 0) return true; return matchesAny(tool.slug, patterns); }) }; } async listToolkits() { return { data: (await this.listToolkitsVNext()).data }; } async listTools(options = {}) { const result = await this.listToolsVNext(options); return { data: result.data, pagination: result.pagination }; } /** * Legacy `resolveTools` shim — subclasses that opt into the VNext surface * normally implement `resolveToolsVNext` instead; the legacy signature * delegates so existing callers keep working. */ async resolveTools(toolSlugs, toolConfigs, options) { return this.resolveToolsVNext({ toolSlugs, toolMeta: Object.fromEntries(Object.entries(toolConfigs ?? {}).map(([slug, cfg]) => [slug, { description: cfg?.description }])), connectionId: "", authorId: options?.userId, requestContext: options?.requestContext }); } /** * Default connection-fields implementation — returns `[]`. Subclasses * whose underlying provider requires user-supplied custom fields at * authorize time (e.g. Confluence subdomain) should override. */ async listConnectionFields(_opts) { return []; } /** * Default health implementation — returns `{ ok: true }`. Subclasses that * need to probe SDK reachability or configuration should override. */ async getHealth() { return { ok: true }; } }; /** * Matches `slug` against an allowlist entry. Supports exact match and a * `prefix*` suffix wildcard. */ function matchesAny(slug, patterns) { for (const pattern of patterns) { if (pattern === slug) return true; if (pattern.endsWith("*")) { const prefix = pattern.slice(0, -1); if (slug.startsWith(prefix)) return true; } } return false; } //#endregion //#region src/tool-provider/runtime.ts /** * Sanitize a connection label into the suffix segment appended to a tool slug * (`__<SUFFIX>`). * * Rules: * - Uppercase. * - Non-`[A-Z0-9_]` characters become `_`. * - On collision with `usedSuffixes`, append `_2`, `_3`, ... until unique. * - The returned suffix is added to `usedSuffixes` in place. */ function buildConnectionSuffix(label, usedSuffixes) { const raw = (label ?? "").toUpperCase(); let base = ""; let prevWasUnderscore = true; for (let i = 0; i < raw.length; i++) { const c = raw.charCodeAt(i); if (c >= 65 && c <= 90 || c >= 48 && c <= 57) { base += raw[i]; prevWasUnderscore = false; } else if (!prevWasUnderscore) { base += "_"; prevWasUnderscore = true; } } if (base.endsWith("_")) base = base.slice(0, -1); if (!base) base = "CONN"; let candidate = base; let n = 2; while (usedSuffixes.has(candidate)) { candidate = `${base}_${n}`; n += 1; } usedSuffixes.add(candidate); return candidate; } /** * Provider-agnostic runtime fan-out. * * For every stored `toolProviders[providerId].connections[toolkit]` * entry, calls `provider.resolveToolsVNext` once per connection. A provider * whose default scope is `caller-supplied` is also called once for each * selected toolkit without a pinned connection, using the request resource id * as its dynamic connection bucket. This lets connection-management and other * connectionless tools bootstrap a caller's first OAuth connection. * * Tools resolved through multiple pinned connections are renamed with a * `__<LABEL>` suffix. Single-connection and unpinned caller-supplied toolkits * keep the natural slug. * * Each renamed tool also gets a routing hint appended to its description so * the LLM can disambiguate between connections. * * Errors from one connection do **not** poison sibling connections — they are * logged and skipped. */ async function resolveStoredToolProviders(toolProviders, lookup, opts = {}) { const { requestContext, authorId, logger } = opts; const out = {}; logger?.debug(`[resolveStoredToolProviders] called`, { providerIds: Object.keys(toolProviders ?? {}), authorId }); if (!toolProviders || Object.keys(toolProviders).length === 0) { logger?.debug(`[resolveStoredToolProviders] no toolProviders on agent — returning {}`); return out; } for (const [providerId, cfg] of Object.entries(toolProviders)) { let provider; try { provider = lookup(providerId); } catch (error) { logger?.warn(`[resolveStoredToolProviders] Unknown provider "${providerId}"`, { error }); continue; } if (!provider.resolveToolsVNext) { logger?.warn(`[resolveStoredToolProviders] Provider "${providerId}" does not implement resolveToolsVNext`); continue; } const tools = cfg.tools ?? {}; const connectionsByToolkit = cfg.connections ?? {}; if (provider.defaultScope === "caller-supplied") { const unpinnedSlugsByToolkit = /* @__PURE__ */ new Map(); for (const [slug, meta] of Object.entries(tools)) { const separatorIndex = slug.indexOf("."); const toolkit = meta?.toolkit ?? (separatorIndex > 0 ? slug.slice(0, separatorIndex) : void 0); if (!toolkit || connectionsByToolkit[toolkit]?.length) continue; const slugs = unpinnedSlugsByToolkit.get(toolkit) ?? []; slugs.push(slug); unpinnedSlugsByToolkit.set(toolkit, slugs); } if (unpinnedSlugsByToolkit.size > 0) { const resolvedAuthorId = resolveCallerSuppliedAuthorId(requestContext, logger); for (const [toolkit, toolSlugs] of unpinnedSlugsByToolkit) { logger?.debug(`[resolveStoredToolProviders] resolving unpinned caller-supplied tools for ${providerId}/${toolkit}`, { slugs: toolSlugs }); try { const resolved = await provider.resolveToolsVNext({ toolSlugs, toolMeta: tools, connectionId: resolvedAuthorId, authorId: resolvedAuthorId, scope: "caller-supplied", requestContext }); for (const [slug, tool] of Object.entries(resolved)) out[slug] = { ...tool, id: slug }; } catch (error) { logger?.warn(`[resolveStoredToolProviders] Failed to resolve unpinned caller-supplied tools for ${providerId}/${toolkit}`, { error }); } } } } for (const [toolkit, connections] of Object.entries(connectionsByToolkit)) { if (!connections || connections.length === 0) { logger?.debug(`[resolveStoredToolProviders] toolkit "${toolkit}" on provider "${providerId}" has no pinned connections — skipping`); continue; } if (connections.length > 1 && !provider.capabilities?.multipleConnectionsPerToolkit) { logger?.warn(`[resolveStoredToolProviders] provider "${providerId}" does not support multiple connections per toolkit but received ${connections.length} for "${toolkit}" — skipping`); continue; } const slugsForToolkit = Object.entries(tools).filter(([slug, meta]) => meta?.toolkit ? meta.toolkit === toolkit : slug.startsWith(`${toolkit}.`)).map(([slug]) => slug); if (slugsForToolkit.length === 0) { logger?.debug(`[resolveStoredToolProviders] toolkit "${toolkit}" on provider "${providerId}" has connections but no matching tool slugs — skipping`, { availableSlugs: Object.keys(tools) }); continue; } logger?.debug(`[resolveStoredToolProviders] resolving tools for ${providerId}/${toolkit}`, { slugs: slugsForToolkit, connectionCount: connections.length }); const skipSuffix = connections.length === 1; const usedSuffixes = /* @__PURE__ */ new Set(); for (const connection of connections) { const suffix = skipSuffix ? "" : `__${buildConnectionSuffix(connection.label, usedSuffixes)}`; const resolvedAuthorId = resolveConnectionAuthorId(connection, authorId, requestContext, logger); let resolved; try { resolved = await provider.resolveToolsVNext({ toolSlugs: slugsForToolkit, toolMeta: cfg.tools ?? {}, connectionId: connection.connectionId, authorId: resolvedAuthorId, scope: connection.scope, requestContext }); } catch (error) { logger?.warn(`[resolveStoredToolProviders] Failed to resolve tools for ${providerId}/${toolkit} connection ${connection.connectionId}`, { error }); continue; } for (const [slug, tool] of Object.entries(resolved)) { const renamedSlug = `${slug}${suffix}`; const baseDescription = tool.description ?? ""; const description = skipSuffix ? baseDescription : appendRoutingHint(baseDescription, connection); out[renamedSlug] = { ...tool, id: renamedSlug, description }; } } } } return out; } let defaultBucketWarned = false; function warnDefaultBucketFallback(logger) { if (defaultBucketWarned) return; defaultBucketWarned = true; logger?.warn("[resolveStoredToolProviders] caller-supplied scope falling back to shared \"default\" bucket — wire authConfig.mapUserToResourceId to avoid cross-tenant OAuth sharing"); } /** * Resolve the provider user bucket for a pinned connection. * * - `kind !== 'author'` → undefined (invoker/platform are reserved for later phases). * - `scope === 'shared'` → {@link SHARED_BUCKET_ID}. * - `scope === 'caller-supplied'` → `requestContext[MASTRA_RESOURCE_ID_KEY]` when * present, otherwise falls back to the shared `'default'` bucket (matching legacy * `ComposioToolProvider` semantics on main). Multi-tenant deployments should wire * `authConfig.mapUserToResourceId` to avoid cross-user bucket sharing. * - otherwise → the caller's resolved authorId. */ function resolveConnectionAuthorId(connection, callerAuthorId, requestContext, logger) { if (connection.kind !== "author") return void 0; if (connection.scope === "shared") return SHARED_BUCKET_ID; if (connection.scope === "caller-supplied") return resolveCallerSuppliedAuthorId(requestContext, logger); return callerAuthorId; } function resolveCallerSuppliedAuthorId(requestContext, logger) { const resourceId = requestContext?.[require_request_context.MASTRA_RESOURCE_ID_KEY]; if (typeof resourceId === "string" && resourceId.length > 0) return resourceId; warnDefaultBucketFallback(logger); return "default"; } function appendRoutingHint(description, connection) { const hint = `Routes through connection: ${connection.label ?? connection.connectionId}`; if (!description) return hint; return `${description}\n\n${hint}`; } //#endregion //#region src/tool-provider/errors.ts /** * Thrown when two {@link ToolProvider} entries share the same `id` during * editor / Mastra construction. */ var DuplicateToolProviderError = class extends Error { ids; constructor(ids) { super(`Duplicate tool provider ids: ${ids.join(", ")}`); this.name = "DuplicateToolProviderError"; this.ids = ids; } }; /** * Thrown when no registered tool provider matches the requested id. */ var UnknownToolProviderError = class extends Error { id; knownIds; constructor(id, knownIds) { super(`Unknown tool provider "${id}". Known ids: ${knownIds.length ? knownIds.join(", ") : "(none)"}`); this.name = "UnknownToolProviderError"; this.id = id; this.knownIds = knownIds; } }; //#endregion exports.BaseToolProvider = BaseToolProvider; exports.DuplicateToolProviderError = DuplicateToolProviderError; exports.SHARED_BUCKET_ID = SHARED_BUCKET_ID; exports.UnknownToolProviderError = UnknownToolProviderError; exports.buildConnectionSuffix = buildConnectionSuffix; exports.resolveStoredToolProviders = resolveStoredToolProviders; //# sourceMappingURL=index.cjs.map