@mastra/core
Version:
344 lines (343 loc) • 13 kB
JavaScript
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