mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 03:50:44 +00:00
1249 lines
39 KiB
TypeScript
1249 lines
39 KiB
TypeScript
import { compileGlobPatterns, matchesAnyGlobPattern } from "../agents/glob-pattern.js";
|
|
import { DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY, normalizeToolName } from "../agents/tool-policy.js";
|
|
import type { AnyAgentTool } from "../agents/tools/common.js";
|
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
import { getLoadedRuntimePluginRegistry } from "./active-runtime-registry.js";
|
|
import { applyTestPluginDefaults, normalizePluginsConfig } from "./config-state.js";
|
|
import type { PluginLoadOptions } from "./loader.js";
|
|
import {
|
|
isManifestPluginAvailableForControlPlane,
|
|
loadManifestContractSnapshot,
|
|
} from "./manifest-contract-eligibility.js";
|
|
import type { PluginManifestRecord } from "./manifest-registry.js";
|
|
import { hasManifestToolAvailability } from "./manifest-tool-availability.js";
|
|
import type { PluginMetadataManifestView } from "./plugin-metadata-snapshot.types.js";
|
|
import type { PluginRegistry, PluginToolRegistration } from "./registry-types.js";
|
|
import {
|
|
buildPluginRuntimeLoadOptions,
|
|
resolvePluginRuntimeLoadContext,
|
|
} from "./runtime/load-context.js";
|
|
import { ensureStandaloneRuntimePluginRegistryLoaded } from "./runtime/standalone-runtime-registry-loader.js";
|
|
import { findUndeclaredPluginToolNames } from "./tool-contracts.js";
|
|
import {
|
|
buildPluginToolDescriptorCacheKey,
|
|
capturePluginToolDescriptor,
|
|
createPluginToolDescriptorConfigCacheKeyMemo,
|
|
readCachedPluginToolDescriptors,
|
|
type CachedPluginToolDescriptor,
|
|
type PluginToolDescriptorConfigCacheKeyMemo,
|
|
writeCachedPluginToolDescriptors,
|
|
} from "./tool-descriptor-cache.js";
|
|
import type { OpenClawPluginToolContext } from "./types.js";
|
|
|
|
export {
|
|
resetPluginToolDescriptorCache,
|
|
resetPluginToolDescriptorCache as resetPluginToolFactoryCache,
|
|
} from "./tool-descriptor-cache.js";
|
|
|
|
export type PluginToolMeta = {
|
|
pluginId: string;
|
|
optional: boolean;
|
|
};
|
|
|
|
type PluginToolFactoryTimingResult = "array" | "error" | "null" | "single";
|
|
|
|
type PluginToolFactoryTiming = {
|
|
pluginId: string;
|
|
names: string[];
|
|
durationMs: number;
|
|
elapsedMs: number;
|
|
result: PluginToolFactoryTimingResult;
|
|
resultCount: number;
|
|
optional: boolean;
|
|
};
|
|
|
|
type PluginToolFactoryResult = AnyAgentTool | AnyAgentTool[] | null | undefined;
|
|
|
|
const log = createSubsystemLogger("plugins/tools");
|
|
const PLUGIN_TOOL_FACTORY_WARN_TOTAL_MS = 5_000;
|
|
const PLUGIN_TOOL_FACTORY_WARN_FACTORY_MS = 1_000;
|
|
const PLUGIN_TOOL_FACTORY_SUMMARY_LIMIT = 20;
|
|
|
|
const pluginToolMeta = new WeakMap<AnyAgentTool, PluginToolMeta>();
|
|
|
|
export function setPluginToolMeta(tool: AnyAgentTool, meta: PluginToolMeta): void {
|
|
pluginToolMeta.set(tool, meta);
|
|
}
|
|
|
|
export function getPluginToolMeta(tool: AnyAgentTool): PluginToolMeta | undefined {
|
|
return pluginToolMeta.get(tool);
|
|
}
|
|
|
|
export function copyPluginToolMeta(source: AnyAgentTool, target: AnyAgentTool): void {
|
|
const meta = pluginToolMeta.get(source);
|
|
if (meta) {
|
|
pluginToolMeta.set(target, meta);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds a collision-proof key for plugin-owned tool metadata lookups.
|
|
*/
|
|
export function buildPluginToolMetadataKey(pluginId: string, toolName: string): string {
|
|
return JSON.stringify([pluginId, toolName]);
|
|
}
|
|
|
|
function normalizeAllowlist(list?: string[]) {
|
|
return new Set((list ?? []).map(normalizeToolName).filter(Boolean));
|
|
}
|
|
|
|
function normalizeDenylist(list?: string[]) {
|
|
return compileGlobPatterns({
|
|
raw: list,
|
|
normalize: normalizeToolName,
|
|
});
|
|
}
|
|
|
|
function denylistBlocksName(name: string, denylist: ReturnType<typeof normalizeDenylist>): boolean {
|
|
const normalized = normalizeToolName(name);
|
|
return normalized ? matchesAnyGlobPattern(normalized, denylist) : false;
|
|
}
|
|
|
|
function denylistBlocksPlugin(params: {
|
|
pluginId: string;
|
|
denylist: ReturnType<typeof normalizeDenylist>;
|
|
}): boolean {
|
|
return (
|
|
denylistBlocksName(params.pluginId, params.denylist) ||
|
|
matchesAnyGlobPattern("group:plugins", params.denylist)
|
|
);
|
|
}
|
|
|
|
function denylistBlocksPluginTool(params: {
|
|
pluginId: string;
|
|
toolName: string;
|
|
denylist: ReturnType<typeof normalizeDenylist>;
|
|
}): boolean {
|
|
return (
|
|
denylistBlocksPlugin({ pluginId: params.pluginId, denylist: params.denylist }) ||
|
|
denylistBlocksName(params.toolName, params.denylist)
|
|
);
|
|
}
|
|
|
|
function allowlistIncludesDefaultPluginTools(allowlist: Set<string>): boolean {
|
|
return allowlist.size === 0 || allowlist.has(DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY);
|
|
}
|
|
|
|
function isManifestToolOptional(plugin: PluginManifestRecord, toolName: string): boolean {
|
|
return plugin.toolMetadata?.[toolName]?.optional === true;
|
|
}
|
|
|
|
function isPluginToolOptional(params: {
|
|
entry: PluginToolRegistration;
|
|
manifestPlugin: PluginManifestRecord | undefined;
|
|
toolName: string;
|
|
}): boolean {
|
|
return (
|
|
params.entry.optional ||
|
|
(params.manifestPlugin ? isManifestToolOptional(params.manifestPlugin, params.toolName) : false)
|
|
);
|
|
}
|
|
|
|
function isOptionalToolAllowed(params: {
|
|
toolName: string;
|
|
pluginId: string;
|
|
allowlist: Set<string>;
|
|
}): boolean {
|
|
if (params.allowlist.size === 0) {
|
|
return false;
|
|
}
|
|
if (params.allowlist.has("*")) {
|
|
return true;
|
|
}
|
|
const toolName = normalizeToolName(params.toolName);
|
|
if (params.allowlist.has(toolName)) {
|
|
return true;
|
|
}
|
|
const pluginKey = normalizeToolName(params.pluginId);
|
|
if (params.allowlist.has(pluginKey)) {
|
|
return true;
|
|
}
|
|
return params.allowlist.has("group:plugins");
|
|
}
|
|
|
|
function isOptionalToolEntryPotentiallyAllowed(params: {
|
|
names: readonly string[];
|
|
pluginId: string;
|
|
allowlist: Set<string>;
|
|
}): boolean {
|
|
if (params.allowlist.size === 0) {
|
|
return false;
|
|
}
|
|
if (params.allowlist.has("*")) {
|
|
return true;
|
|
}
|
|
const pluginKey = normalizeToolName(params.pluginId);
|
|
if (params.allowlist.has(pluginKey) || params.allowlist.has("group:plugins")) {
|
|
return true;
|
|
}
|
|
if (params.names.length === 0) {
|
|
return true;
|
|
}
|
|
return params.names.some((name) => params.allowlist.has(normalizeToolName(name)));
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
}
|
|
|
|
function readPluginToolName(tool: unknown): string {
|
|
if (!isRecord(tool)) {
|
|
return "";
|
|
}
|
|
// Optional-tool allowlists need a best-effort name before full shape validation.
|
|
return typeof tool.name === "string" ? tool.name.trim() : "";
|
|
}
|
|
|
|
function toElapsedMs(value: number): number {
|
|
return Math.max(0, Math.round(value));
|
|
}
|
|
|
|
function describePluginToolFactoryResult(
|
|
resolved: AnyAgentTool | AnyAgentTool[] | null | undefined,
|
|
failed: boolean,
|
|
): { result: PluginToolFactoryTimingResult; resultCount: number } {
|
|
if (failed) {
|
|
return { result: "error", resultCount: 0 };
|
|
}
|
|
if (!resolved) {
|
|
return { result: "null", resultCount: 0 };
|
|
}
|
|
if (Array.isArray(resolved)) {
|
|
return { result: "array", resultCount: resolved.length };
|
|
}
|
|
return { result: "single", resultCount: 1 };
|
|
}
|
|
|
|
function createPluginToolFactoryTiming(params: {
|
|
pluginId: string;
|
|
names: string[];
|
|
durationMs: number;
|
|
elapsedMs: number;
|
|
resolved: PluginToolFactoryResult;
|
|
failed: boolean;
|
|
optional: boolean;
|
|
}): PluginToolFactoryTiming {
|
|
const result = describePluginToolFactoryResult(params.resolved, params.failed);
|
|
return {
|
|
pluginId: params.pluginId,
|
|
names: params.names,
|
|
durationMs: params.durationMs,
|
|
elapsedMs: params.elapsedMs,
|
|
result: result.result,
|
|
resultCount: result.resultCount,
|
|
optional: params.optional,
|
|
};
|
|
}
|
|
|
|
function resolvePluginToolFactoryEntry(params: {
|
|
entry: PluginToolRegistration;
|
|
ctx: OpenClawPluginToolContext;
|
|
declaredNames: string[];
|
|
factoryTimingStartedAt: number;
|
|
logError: (message: string) => void;
|
|
}): {
|
|
resolved: PluginToolFactoryResult;
|
|
failed: boolean;
|
|
timing: PluginToolFactoryTiming;
|
|
} {
|
|
let resolved: PluginToolFactoryResult = null;
|
|
let failed = false;
|
|
const factoryStartedAt = Date.now();
|
|
|
|
try {
|
|
resolved = params.entry.factory(params.ctx);
|
|
} catch (err) {
|
|
failed = true;
|
|
params.logError(`plugin tool failed (${params.entry.pluginId}): ${String(err)}`);
|
|
}
|
|
|
|
const factoryEndedAt = Date.now();
|
|
return {
|
|
resolved,
|
|
failed,
|
|
timing: createPluginToolFactoryTiming({
|
|
pluginId: params.entry.pluginId,
|
|
names: params.declaredNames,
|
|
durationMs: toElapsedMs(factoryEndedAt - factoryStartedAt),
|
|
elapsedMs: toElapsedMs(factoryEndedAt - params.factoryTimingStartedAt),
|
|
resolved,
|
|
failed,
|
|
optional: params.entry.optional,
|
|
}),
|
|
};
|
|
}
|
|
|
|
function formatPluginToolFactoryTiming(timing: PluginToolFactoryTiming): string {
|
|
const names = timing.names.length > 0 ? timing.names.join("|") : "-";
|
|
return [
|
|
`${timing.pluginId}:${timing.durationMs}ms@${timing.elapsedMs}ms`,
|
|
`names=[${names}]`,
|
|
`result=${timing.result}`,
|
|
`count=${timing.resultCount}`,
|
|
`optional=${String(timing.optional)}`,
|
|
].join(" ");
|
|
}
|
|
|
|
function formatPluginToolFactoryTimingSummary(params: {
|
|
totalMs: number;
|
|
timings: PluginToolFactoryTiming[];
|
|
}): string {
|
|
const ranked = params.timings
|
|
.toSorted(
|
|
(left, right) =>
|
|
right.durationMs - left.durationMs || left.pluginId.localeCompare(right.pluginId),
|
|
)
|
|
.slice(0, PLUGIN_TOOL_FACTORY_SUMMARY_LIMIT);
|
|
const omitted = Math.max(0, params.timings.length - ranked.length);
|
|
const factories =
|
|
ranked.length > 0
|
|
? ranked.map((timing) => formatPluginToolFactoryTiming(timing)).join(", ")
|
|
: "none";
|
|
return [
|
|
"[trace:plugin-tools] factory timings",
|
|
`totalMs=${params.totalMs}`,
|
|
`factoryCount=${params.timings.length}`,
|
|
`shown=${ranked.length}`,
|
|
`omitted=${omitted}`,
|
|
`factories=${factories}`,
|
|
].join(" ");
|
|
}
|
|
|
|
function shouldWarnPluginToolFactoryTimings(params: {
|
|
totalMs: number;
|
|
timings: PluginToolFactoryTiming[];
|
|
}): boolean {
|
|
return (
|
|
params.totalMs >= PLUGIN_TOOL_FACTORY_WARN_TOTAL_MS ||
|
|
params.timings.some((timing) => timing.durationMs >= PLUGIN_TOOL_FACTORY_WARN_FACTORY_MS)
|
|
);
|
|
}
|
|
|
|
function describeMalformedPluginTool(tool: unknown): string | undefined {
|
|
if (!isRecord(tool)) {
|
|
return "tool must be an object";
|
|
}
|
|
const name = readPluginToolName(tool);
|
|
if (!name) {
|
|
return "missing non-empty name";
|
|
}
|
|
if (typeof tool.execute !== "function") {
|
|
return `${name} missing execute function`;
|
|
}
|
|
if (!isRecord(tool.parameters)) {
|
|
return `${name} missing parameters object`;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function pluginToolNamesMatchAllowlist(params: {
|
|
names: readonly string[];
|
|
pluginId: string;
|
|
optional: boolean;
|
|
allowlist: Set<string>;
|
|
}): boolean {
|
|
if (!params.optional && allowlistIncludesDefaultPluginTools(params.allowlist)) {
|
|
return true;
|
|
}
|
|
return isOptionalToolEntryPotentiallyAllowed(params);
|
|
}
|
|
|
|
function listManifestToolNamesForAllowlist(params: {
|
|
plugin: PluginManifestRecord;
|
|
toolNames: readonly string[];
|
|
pluginId: string;
|
|
allowlist: Set<string>;
|
|
}): string[] {
|
|
if (params.toolNames.length === 0) {
|
|
return [];
|
|
}
|
|
if (params.allowlist.has("*") || params.allowlist.has("group:plugins")) {
|
|
return [...params.toolNames];
|
|
}
|
|
const pluginKey = normalizeToolName(params.pluginId);
|
|
if (params.allowlist.has(pluginKey)) {
|
|
return [...params.toolNames];
|
|
}
|
|
const matchedToolNames = params.toolNames.filter((name) =>
|
|
params.allowlist.has(normalizeToolName(name)),
|
|
);
|
|
if (!allowlistIncludesDefaultPluginTools(params.allowlist)) {
|
|
return matchedToolNames;
|
|
}
|
|
const defaultToolNames = params.toolNames.filter(
|
|
(name) => !isManifestToolOptional(params.plugin, name),
|
|
);
|
|
return [...new Set([...defaultToolNames, ...matchedToolNames])];
|
|
}
|
|
|
|
function listManifestToolNamesForAvailability(params: {
|
|
plugin: PluginManifestRecord;
|
|
toolNames: readonly string[];
|
|
pluginId: string;
|
|
allowlist: Set<string>;
|
|
}): string[] {
|
|
return listManifestToolNamesForAllowlist(params);
|
|
}
|
|
|
|
function isManifestToolNameAvailable(params: {
|
|
plugin: PluginManifestRecord;
|
|
toolName: string;
|
|
config: PluginLoadOptions["config"];
|
|
env: NodeJS.ProcessEnv;
|
|
hasAuthForProvider?: (providerId: string) => boolean;
|
|
}): boolean {
|
|
return hasManifestToolAvailability({
|
|
plugin: params.plugin,
|
|
toolNames: [params.toolName],
|
|
config: params.config,
|
|
env: params.env,
|
|
hasAuthForProvider: params.hasAuthForProvider,
|
|
});
|
|
}
|
|
|
|
function filterManifestToolNamesForAvailability(params: {
|
|
plugin: PluginManifestRecord;
|
|
toolNames: readonly string[];
|
|
config: PluginLoadOptions["config"];
|
|
env: NodeJS.ProcessEnv;
|
|
hasAuthForProvider?: (providerId: string) => boolean;
|
|
}): string[] {
|
|
return params.toolNames.filter((toolName) =>
|
|
isManifestToolNameAvailable({
|
|
plugin: params.plugin,
|
|
toolName,
|
|
config: params.config,
|
|
env: params.env,
|
|
hasAuthForProvider: params.hasAuthForProvider,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function resolvePluginToolRuntimePluginIds(params: {
|
|
config: PluginLoadOptions["config"];
|
|
availabilityConfig?: PluginLoadOptions["config"];
|
|
workspaceDir?: string;
|
|
env: NodeJS.ProcessEnv;
|
|
toolAllowlist?: string[];
|
|
toolDenylist?: string[];
|
|
hasAuthForProvider?: (providerId: string) => boolean;
|
|
snapshot?: PluginMetadataManifestView;
|
|
}): string[] {
|
|
const pluginIds = new Set<string>();
|
|
const allowlist = normalizeAllowlist(params.toolAllowlist);
|
|
const denylist = normalizeDenylist(params.toolDenylist);
|
|
const normalizedPlugins = normalizePluginsConfig(params.config?.plugins);
|
|
const snapshot =
|
|
params.snapshot ??
|
|
loadManifestContractSnapshot({
|
|
config: params.config,
|
|
workspaceDir: params.workspaceDir,
|
|
env: params.env,
|
|
});
|
|
for (const plugin of snapshot.plugins) {
|
|
if (
|
|
!isManifestPluginAvailableForControlPlane({
|
|
snapshot,
|
|
plugin,
|
|
config: params.config,
|
|
})
|
|
) {
|
|
continue;
|
|
}
|
|
if (
|
|
normalizedPlugins.entries[plugin.id]?.enabled === false ||
|
|
normalizedPlugins.deny.includes(plugin.id)
|
|
) {
|
|
continue;
|
|
}
|
|
if (denylistBlocksPlugin({ pluginId: plugin.id, denylist })) {
|
|
continue;
|
|
}
|
|
const toolNames = plugin.contracts?.tools ?? [];
|
|
const selectedToolNames = listManifestToolNamesForAvailability({
|
|
toolNames,
|
|
plugin,
|
|
pluginId: plugin.id,
|
|
allowlist,
|
|
}).filter(
|
|
(toolName) =>
|
|
!denylistBlocksPluginTool({
|
|
pluginId: plugin.id,
|
|
toolName,
|
|
denylist,
|
|
}),
|
|
);
|
|
if (
|
|
selectedToolNames.length > 0 &&
|
|
hasManifestToolAvailability({
|
|
plugin,
|
|
toolNames: selectedToolNames,
|
|
config: params.availabilityConfig ?? params.config,
|
|
env: params.env,
|
|
hasAuthForProvider: params.hasAuthForProvider,
|
|
})
|
|
) {
|
|
pluginIds.add(plugin.id);
|
|
}
|
|
}
|
|
return [...pluginIds].toSorted((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
function readPluginCacheSource(plugin: PluginManifestRecord): string {
|
|
const source = (plugin as { source?: unknown; manifestPath?: unknown }).source;
|
|
if (typeof source === "string" && source.trim()) {
|
|
return source;
|
|
}
|
|
const manifestPath = (plugin as { manifestPath?: unknown }).manifestPath;
|
|
if (typeof manifestPath === "string" && manifestPath.trim()) {
|
|
return manifestPath;
|
|
}
|
|
return plugin.id;
|
|
}
|
|
|
|
function buildPluginDescriptorCacheKey(params: {
|
|
plugin: PluginManifestRecord;
|
|
ctx: OpenClawPluginToolContext;
|
|
currentRuntimeConfig?: PluginLoadOptions["config"] | null;
|
|
configCacheKeyMemo?: PluginToolDescriptorConfigCacheKeyMemo;
|
|
}): string {
|
|
return buildPluginToolDescriptorCacheKey({
|
|
pluginId: params.plugin.id,
|
|
source: readPluginCacheSource(params.plugin),
|
|
rootDir: params.plugin.rootDir,
|
|
contractToolNames: params.plugin.contracts?.tools ?? [],
|
|
ctx: params.ctx,
|
|
currentRuntimeConfig: params.currentRuntimeConfig,
|
|
configCacheKeyMemo: params.configCacheKeyMemo,
|
|
});
|
|
}
|
|
|
|
function cachedDescriptorsCoverToolNames(params: {
|
|
descriptors: readonly CachedPluginToolDescriptor[];
|
|
toolNames: readonly string[];
|
|
}): boolean {
|
|
const descriptorNames = new Set(
|
|
params.descriptors.map((entry) => normalizeToolName(entry.descriptor.name)),
|
|
);
|
|
return params.toolNames.every((name) => descriptorNames.has(normalizeToolName(name)));
|
|
}
|
|
|
|
function createCachedDescriptorPluginTool(params: {
|
|
descriptor: CachedPluginToolDescriptor;
|
|
ctx: OpenClawPluginToolContext;
|
|
loadContext: ReturnType<typeof resolvePluginRuntimeLoadContext>;
|
|
runtimeOptions: PluginLoadOptions["runtimeOptions"];
|
|
}): AnyAgentTool {
|
|
const { descriptor } = params.descriptor;
|
|
const pluginId = descriptor.owner.kind === "plugin" ? descriptor.owner.pluginId : "";
|
|
const toolName = descriptor.name;
|
|
const tool: AnyAgentTool = {
|
|
name: descriptor.name,
|
|
label: descriptor.title ?? descriptor.name,
|
|
description: descriptor.description,
|
|
parameters: descriptor.inputSchema as never,
|
|
async execute(toolCallId, executeParams, signal, onUpdate) {
|
|
const loadOptions = buildPluginRuntimeLoadOptions(params.loadContext, {
|
|
activate: false,
|
|
toolDiscovery: true,
|
|
onlyPluginIds: [pluginId],
|
|
...(params.runtimeOptions ? { runtimeOptions: params.runtimeOptions } : {}),
|
|
});
|
|
const registry = resolvePluginToolRegistry({
|
|
loadOptions,
|
|
onlyPluginIds: [pluginId],
|
|
});
|
|
const entry = registry?.tools.find(
|
|
(candidate) =>
|
|
candidate.pluginId === pluginId &&
|
|
(candidate.names.length > 0 ? candidate.names : (candidate.declaredNames ?? [])).some(
|
|
(name) => normalizeToolName(name) === normalizeToolName(toolName),
|
|
),
|
|
);
|
|
if (!entry) {
|
|
throw new Error(`plugin tool runtime unavailable (${pluginId}): ${toolName}`);
|
|
}
|
|
const resolved = entry.factory(params.ctx);
|
|
const listRaw: unknown[] = Array.isArray(resolved) ? resolved : resolved ? [resolved] : [];
|
|
for (const toolRaw of listRaw) {
|
|
const malformedReason = describeMalformedPluginTool(toolRaw);
|
|
if (malformedReason) {
|
|
throw new Error(`plugin tool is malformed (${pluginId}): ${malformedReason}`);
|
|
}
|
|
const runtimeTool = toolRaw as AnyAgentTool;
|
|
if (normalizeToolName(runtimeTool.name) === normalizeToolName(toolName)) {
|
|
return runtimeTool.execute(toolCallId, executeParams, signal, onUpdate);
|
|
}
|
|
}
|
|
throw new Error(`plugin tool runtime missing (${pluginId}): ${toolName}`);
|
|
},
|
|
};
|
|
if (params.descriptor.displaySummary) {
|
|
tool.displaySummary = params.descriptor.displaySummary;
|
|
}
|
|
if (params.descriptor.ownerOnly === true) {
|
|
tool.ownerOnly = true;
|
|
}
|
|
setPluginToolMeta(tool, {
|
|
pluginId,
|
|
optional: params.descriptor.optional,
|
|
});
|
|
return tool;
|
|
}
|
|
|
|
function resolveCachedPluginTools(params: {
|
|
snapshot: PluginMetadataManifestView;
|
|
config: PluginLoadOptions["config"];
|
|
availabilityConfig: PluginLoadOptions["config"];
|
|
env: NodeJS.ProcessEnv;
|
|
allowlist: Set<string>;
|
|
denylist: ReturnType<typeof normalizeDenylist>;
|
|
hasAuthForProvider?: (providerId: string) => boolean;
|
|
onlyPluginIds: readonly string[];
|
|
existing: Set<string>;
|
|
existingNormalized: Set<string>;
|
|
ctx: OpenClawPluginToolContext;
|
|
loadContext: ReturnType<typeof resolvePluginRuntimeLoadContext>;
|
|
runtimeOptions: PluginLoadOptions["runtimeOptions"];
|
|
currentRuntimeConfig?: PluginLoadOptions["config"] | null;
|
|
configCacheKeyMemo: PluginToolDescriptorConfigCacheKeyMemo;
|
|
}): { tools: AnyAgentTool[]; handledPluginIds: Set<string> } {
|
|
const tools: AnyAgentTool[] = [];
|
|
const handledPluginIds = new Set<string>();
|
|
const onlyPluginIdSet = new Set(params.onlyPluginIds);
|
|
for (const plugin of params.snapshot.plugins) {
|
|
if (!onlyPluginIdSet.has(plugin.id)) {
|
|
continue;
|
|
}
|
|
if (denylistBlocksPlugin({ pluginId: plugin.id, denylist: params.denylist })) {
|
|
continue;
|
|
}
|
|
if (
|
|
!isManifestPluginAvailableForControlPlane({
|
|
snapshot: params.snapshot,
|
|
plugin,
|
|
config: params.config,
|
|
})
|
|
) {
|
|
continue;
|
|
}
|
|
const contractToolNames = plugin.contracts?.tools ?? [];
|
|
const allowedToolNames = listManifestToolNamesForAvailability({
|
|
plugin,
|
|
toolNames: contractToolNames,
|
|
pluginId: plugin.id,
|
|
allowlist: params.allowlist,
|
|
}).filter(
|
|
(toolName) =>
|
|
!denylistBlocksPluginTool({
|
|
pluginId: plugin.id,
|
|
toolName,
|
|
denylist: params.denylist,
|
|
}),
|
|
);
|
|
const availableToolNames = filterManifestToolNamesForAvailability({
|
|
plugin,
|
|
toolNames: allowedToolNames,
|
|
config: params.availabilityConfig,
|
|
env: params.env,
|
|
hasAuthForProvider: params.hasAuthForProvider,
|
|
});
|
|
if (availableToolNames.length === 0) {
|
|
continue;
|
|
}
|
|
if (params.existingNormalized.has(normalizeToolName(plugin.id))) {
|
|
continue;
|
|
}
|
|
const cached = readCachedPluginToolDescriptors(
|
|
buildPluginDescriptorCacheKey({
|
|
plugin,
|
|
ctx: params.ctx,
|
|
currentRuntimeConfig: params.currentRuntimeConfig,
|
|
configCacheKeyMemo: params.configCacheKeyMemo,
|
|
}),
|
|
);
|
|
if (
|
|
!cached ||
|
|
!cachedDescriptorsCoverToolNames({
|
|
descriptors: cached,
|
|
toolNames: availableToolNames,
|
|
})
|
|
) {
|
|
continue;
|
|
}
|
|
const pluginTools: AnyAgentTool[] = [];
|
|
let hasNameConflict = false;
|
|
const localNormalizedNames = new Set<string>();
|
|
for (const cachedDescriptor of cached) {
|
|
if (
|
|
!cachedDescriptor.optional &&
|
|
!availableToolNames.some(
|
|
(name) => normalizeToolName(name) === normalizeToolName(cachedDescriptor.descriptor.name),
|
|
)
|
|
) {
|
|
continue;
|
|
}
|
|
if (
|
|
cachedDescriptor.optional &&
|
|
!isOptionalToolAllowed({
|
|
toolName: cachedDescriptor.descriptor.name,
|
|
pluginId: plugin.id,
|
|
allowlist: params.allowlist,
|
|
})
|
|
) {
|
|
continue;
|
|
}
|
|
const normalizedDescriptorName = normalizeToolName(cachedDescriptor.descriptor.name);
|
|
if (
|
|
denylistBlocksPluginTool({
|
|
pluginId: plugin.id,
|
|
toolName: cachedDescriptor.descriptor.name,
|
|
denylist: params.denylist,
|
|
})
|
|
) {
|
|
continue;
|
|
}
|
|
if (
|
|
localNormalizedNames.has(normalizedDescriptorName) ||
|
|
params.existingNormalized.has(normalizedDescriptorName)
|
|
) {
|
|
hasNameConflict = true;
|
|
break;
|
|
}
|
|
localNormalizedNames.add(normalizedDescriptorName);
|
|
pluginTools.push(
|
|
createCachedDescriptorPluginTool({
|
|
descriptor: cachedDescriptor,
|
|
ctx: params.ctx,
|
|
loadContext: params.loadContext,
|
|
runtimeOptions: params.runtimeOptions,
|
|
}),
|
|
);
|
|
}
|
|
if (hasNameConflict) {
|
|
continue;
|
|
}
|
|
for (const pluginTool of pluginTools) {
|
|
params.existing.add(pluginTool.name);
|
|
params.existingNormalized.add(normalizeToolName(pluginTool.name));
|
|
tools.push(pluginTool);
|
|
}
|
|
handledPluginIds.add(plugin.id);
|
|
}
|
|
return { tools, handledPluginIds };
|
|
}
|
|
|
|
function resolvePluginToolRegistry(params: {
|
|
loadOptions: PluginLoadOptions;
|
|
onlyPluginIds?: readonly string[];
|
|
}) {
|
|
const lookup = {
|
|
env: params.loadOptions.env,
|
|
loadOptions: params.loadOptions,
|
|
workspaceDir: params.loadOptions.workspaceDir,
|
|
requiredPluginIds: params.onlyPluginIds,
|
|
};
|
|
const channelRegistry = getLoadedRuntimePluginRegistry({
|
|
...lookup,
|
|
surface: "channel",
|
|
});
|
|
if (registryHasScopedPluginTools(channelRegistry, params.onlyPluginIds)) {
|
|
return channelRegistry;
|
|
}
|
|
|
|
const activeRegistry = getLoadedRuntimePluginRegistry({
|
|
env: lookup.env,
|
|
workspaceDir: lookup.workspaceDir,
|
|
requiredPluginIds: lookup.requiredPluginIds,
|
|
surface: "active",
|
|
});
|
|
if (registryHasScopedPluginTools(activeRegistry, params.onlyPluginIds)) {
|
|
return activeRegistry;
|
|
}
|
|
|
|
const forceStandaloneLoad = Boolean(channelRegistry || activeRegistry);
|
|
const standaloneRegistry = ensureStandaloneRuntimePluginRegistryLoaded({
|
|
surface: "active",
|
|
forceLoad: forceStandaloneLoad,
|
|
installRegistry: !forceStandaloneLoad,
|
|
requiredPluginIds: params.onlyPluginIds,
|
|
loadOptions: params.loadOptions,
|
|
});
|
|
if (registryHasScopedPluginTools(standaloneRegistry, params.onlyPluginIds)) {
|
|
return standaloneRegistry;
|
|
}
|
|
return standaloneRegistry ?? channelRegistry ?? activeRegistry;
|
|
}
|
|
|
|
function registryHasScopedPluginTools(
|
|
registry: PluginRegistry | undefined,
|
|
pluginIds: readonly string[] | undefined,
|
|
): registry is PluginRegistry {
|
|
if (!registry) {
|
|
return false;
|
|
}
|
|
if (pluginIds === undefined) {
|
|
return (registry.tools?.length ?? 0) > 0;
|
|
}
|
|
const scopedPluginIds = new Set(pluginIds);
|
|
if (scopedPluginIds.size === 0) {
|
|
return true;
|
|
}
|
|
const registryPluginIds = new Set(registry.tools.map((entry) => entry.pluginId));
|
|
return Array.from(scopedPluginIds).every((pluginId) => registryPluginIds.has(pluginId));
|
|
}
|
|
|
|
function resolvePluginToolLoadState(params: {
|
|
context: OpenClawPluginToolContext;
|
|
toolAllowlist?: string[];
|
|
toolDenylist?: string[];
|
|
allowGatewaySubagentBinding?: boolean;
|
|
hasAuthForProvider?: (providerId: string) => boolean;
|
|
env?: NodeJS.ProcessEnv;
|
|
}):
|
|
| {
|
|
context: ReturnType<typeof resolvePluginRuntimeLoadContext>;
|
|
env: NodeJS.ProcessEnv;
|
|
loadOptions: PluginLoadOptions;
|
|
onlyPluginIds: string[];
|
|
runtimeOptions: PluginLoadOptions["runtimeOptions"];
|
|
snapshot: PluginMetadataManifestView;
|
|
}
|
|
| undefined {
|
|
const env = params.env ?? process.env;
|
|
const baseConfig = applyTestPluginDefaults(params.context.config ?? {}, env);
|
|
const context = resolvePluginRuntimeLoadContext({
|
|
config: baseConfig,
|
|
env,
|
|
workspaceDir: params.context.workspaceDir,
|
|
});
|
|
const normalized = normalizePluginsConfig(context.config.plugins);
|
|
if (!normalized.enabled) {
|
|
return undefined;
|
|
}
|
|
|
|
const runtimeOptions = params.allowGatewaySubagentBinding
|
|
? { allowGatewaySubagentBinding: true as const }
|
|
: undefined;
|
|
const snapshot = loadManifestContractSnapshot({
|
|
config: context.config,
|
|
workspaceDir: context.workspaceDir,
|
|
env,
|
|
});
|
|
const onlyPluginIds = resolvePluginToolRuntimePluginIds({
|
|
config: context.config,
|
|
availabilityConfig: params.context.runtimeConfig ?? context.config,
|
|
workspaceDir: context.workspaceDir,
|
|
env,
|
|
toolAllowlist: params.toolAllowlist,
|
|
toolDenylist: params.toolDenylist,
|
|
hasAuthForProvider: params.hasAuthForProvider,
|
|
snapshot,
|
|
});
|
|
const loadOptions = buildPluginRuntimeLoadOptions(context, {
|
|
activate: false,
|
|
toolDiscovery: true,
|
|
onlyPluginIds,
|
|
runtimeOptions,
|
|
});
|
|
return { context, env, loadOptions, onlyPluginIds, runtimeOptions, snapshot };
|
|
}
|
|
|
|
export function ensureStandalonePluginToolRegistryLoaded(params: {
|
|
context: OpenClawPluginToolContext;
|
|
toolAllowlist?: string[];
|
|
toolDenylist?: string[];
|
|
allowGatewaySubagentBinding?: boolean;
|
|
hasAuthForProvider?: (providerId: string) => boolean;
|
|
env?: NodeJS.ProcessEnv;
|
|
}): void {
|
|
const loadState = resolvePluginToolLoadState(params);
|
|
if (!loadState) {
|
|
return;
|
|
}
|
|
ensureStandaloneRuntimePluginRegistryLoaded({
|
|
surface: "channel",
|
|
requiredPluginIds: loadState.onlyPluginIds,
|
|
loadOptions: loadState.loadOptions,
|
|
});
|
|
}
|
|
|
|
export function resolvePluginTools(params: {
|
|
context: OpenClawPluginToolContext;
|
|
existingToolNames?: Set<string>;
|
|
toolAllowlist?: string[];
|
|
toolDenylist?: string[];
|
|
suppressNameConflicts?: boolean;
|
|
allowGatewaySubagentBinding?: boolean;
|
|
hasAuthForProvider?: (providerId: string) => boolean;
|
|
env?: NodeJS.ProcessEnv;
|
|
}): AnyAgentTool[] {
|
|
// Fast path: when plugins are effectively disabled, avoid discovery/jiti entirely.
|
|
// This matters a lot for unit tests and for tool construction hot paths.
|
|
const loadState = resolvePluginToolLoadState(params);
|
|
if (!loadState) {
|
|
return [];
|
|
}
|
|
const { context, env, onlyPluginIds, runtimeOptions, snapshot } = loadState;
|
|
const tools: AnyAgentTool[] = [];
|
|
const existing = params.existingToolNames ?? new Set<string>();
|
|
const existingNormalized = new Set(Array.from(existing, (tool) => normalizeToolName(tool)));
|
|
const allowlist = normalizeAllowlist(params.toolAllowlist);
|
|
const denylist = normalizeDenylist(params.toolDenylist);
|
|
const configCacheKeyMemo = createPluginToolDescriptorConfigCacheKeyMemo();
|
|
let currentRuntimeConfigForDescriptorCache: PluginLoadOptions["config"] | null | undefined =
|
|
params.context.runtimeConfig;
|
|
if (currentRuntimeConfigForDescriptorCache === undefined && params.context.getRuntimeConfig) {
|
|
try {
|
|
currentRuntimeConfigForDescriptorCache = params.context.getRuntimeConfig();
|
|
} catch {
|
|
currentRuntimeConfigForDescriptorCache = null;
|
|
}
|
|
}
|
|
const cached = resolveCachedPluginTools({
|
|
snapshot,
|
|
config: context.config,
|
|
availabilityConfig: params.context.runtimeConfig ?? context.config,
|
|
env,
|
|
allowlist,
|
|
denylist,
|
|
hasAuthForProvider: params.hasAuthForProvider,
|
|
onlyPluginIds,
|
|
existing,
|
|
existingNormalized,
|
|
ctx: params.context,
|
|
loadContext: context,
|
|
runtimeOptions,
|
|
currentRuntimeConfig: currentRuntimeConfigForDescriptorCache,
|
|
configCacheKeyMemo,
|
|
});
|
|
tools.push(...cached.tools);
|
|
const runtimePluginIds = onlyPluginIds.filter(
|
|
(pluginId) => !cached.handledPluginIds.has(pluginId),
|
|
);
|
|
if (runtimePluginIds.length === 0) {
|
|
return tools;
|
|
}
|
|
const loadOptions = buildPluginRuntimeLoadOptions(context, {
|
|
activate: false,
|
|
toolDiscovery: true,
|
|
onlyPluginIds: runtimePluginIds,
|
|
runtimeOptions,
|
|
});
|
|
let registry = resolvePluginToolRegistry({
|
|
loadOptions,
|
|
onlyPluginIds: runtimePluginIds,
|
|
});
|
|
if (!registry) {
|
|
// Cold registry: path-based plugins (origin "config") registered via plugins.load.paths
|
|
// are not pinned to any active channel/surface registry until explicitly loaded.
|
|
// Trigger a standalone load so their tool factories become available, then retry.
|
|
try {
|
|
ensureStandaloneRuntimePluginRegistryLoaded({
|
|
surface: "channel",
|
|
requiredPluginIds: runtimePluginIds,
|
|
loadOptions,
|
|
});
|
|
} catch (error) {
|
|
context.logger.error(
|
|
`failed to cold-load plugin tool registry for plugin ids [${runtimePluginIds.join(", ")}]: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}`,
|
|
);
|
|
throw error;
|
|
}
|
|
registry = resolvePluginToolRegistry({
|
|
loadOptions,
|
|
onlyPluginIds: runtimePluginIds,
|
|
});
|
|
if (!registry) {
|
|
context.logger.warn(
|
|
`plugin tool registry still unavailable after cold load for plugin ids [${runtimePluginIds.join(
|
|
", ",
|
|
)}]`,
|
|
);
|
|
return tools;
|
|
}
|
|
}
|
|
|
|
const scopedPluginIds = new Set(runtimePluginIds);
|
|
const registryToolPluginIds = new Set(registry.tools.map((entry) => entry.pluginId));
|
|
const missingRegistryToolPluginIds = runtimePluginIds.filter(
|
|
(pluginId) => !registryToolPluginIds.has(pluginId),
|
|
);
|
|
for (const pluginId of missingRegistryToolPluginIds) {
|
|
registry.diagnostics.push({
|
|
level: "warn",
|
|
pluginId,
|
|
source: "plugin-tools",
|
|
message: `plugin tool registry did not include selected plugin tools after cold load (${pluginId})`,
|
|
});
|
|
}
|
|
const blockedPlugins = new Set<string>();
|
|
const factoryTimingStartedAt = Date.now();
|
|
const factoryTimings: PluginToolFactoryTiming[] = [];
|
|
const capturedDescriptorsByPluginId = new Map<string, CachedPluginToolDescriptor[]>();
|
|
const manifestPluginsById = new Map(snapshot.plugins.map((plugin) => [plugin.id, plugin]));
|
|
|
|
for (const entry of registry.tools) {
|
|
if (!scopedPluginIds.has(entry.pluginId)) {
|
|
continue;
|
|
}
|
|
if (denylistBlocksPlugin({ pluginId: entry.pluginId, denylist })) {
|
|
continue;
|
|
}
|
|
if (blockedPlugins.has(entry.pluginId)) {
|
|
continue;
|
|
}
|
|
const pluginIdKey = normalizeToolName(entry.pluginId);
|
|
if (existingNormalized.has(pluginIdKey)) {
|
|
const message = `plugin id conflicts with core tool name (${entry.pluginId})`;
|
|
if (!params.suppressNameConflicts) {
|
|
context.logger.error(message);
|
|
registry.diagnostics.push({
|
|
level: "error",
|
|
pluginId: entry.pluginId,
|
|
source: entry.source,
|
|
message,
|
|
});
|
|
}
|
|
blockedPlugins.add(entry.pluginId);
|
|
continue;
|
|
}
|
|
const manifestPlugin = manifestPluginsById.get(entry.pluginId);
|
|
const declaredNames = entry.names ?? [];
|
|
const availabilityNames =
|
|
declaredNames.length > 0 ? declaredNames : (entry.declaredNames ?? []);
|
|
const allowlistNames = manifestPlugin
|
|
? filterManifestToolNamesForAvailability({
|
|
plugin: manifestPlugin,
|
|
toolNames: availabilityNames,
|
|
config: params.context.runtimeConfig ?? context.config,
|
|
env,
|
|
hasAuthForProvider: params.hasAuthForProvider,
|
|
}).filter(
|
|
(toolName) =>
|
|
!denylistBlocksPluginTool({
|
|
pluginId: entry.pluginId,
|
|
toolName,
|
|
denylist,
|
|
}),
|
|
)
|
|
: declaredNames;
|
|
if (manifestPlugin && availabilityNames.length > 0 && allowlistNames.length === 0) {
|
|
continue;
|
|
}
|
|
if (
|
|
!pluginToolNamesMatchAllowlist({
|
|
names: allowlistNames,
|
|
pluginId: entry.pluginId,
|
|
optional: entry.optional,
|
|
allowlist,
|
|
})
|
|
) {
|
|
continue;
|
|
}
|
|
const factoryResult = resolvePluginToolFactoryEntry({
|
|
entry,
|
|
ctx: params.context,
|
|
declaredNames,
|
|
factoryTimingStartedAt,
|
|
logError: (message) => context.logger.error(message),
|
|
});
|
|
factoryTimings.push(factoryResult.timing);
|
|
if (factoryResult.failed) {
|
|
continue;
|
|
}
|
|
const { resolved } = factoryResult;
|
|
if (!resolved) {
|
|
if (declaredNames.length > 0) {
|
|
context.logger.debug?.(
|
|
`plugin tool factory returned null (${entry.pluginId}): [${declaredNames.join(", ")}]`,
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
const listRaw: unknown[] = Array.isArray(resolved) ? resolved : [resolved];
|
|
const selectedManifestToolNames =
|
|
manifestPlugin && availabilityNames.length > 0
|
|
? new Set(allowlistNames.map((name) => normalizeToolName(name)))
|
|
: undefined;
|
|
const manifestContractToolNames =
|
|
manifestPlugin && availabilityNames.length > 0
|
|
? new Set(availabilityNames.map((name) => normalizeToolName(name)))
|
|
: undefined;
|
|
const availableList = manifestPlugin
|
|
? listRaw.filter((tool) => {
|
|
const toolName = readPluginToolName(tool);
|
|
const normalizedToolName = normalizeToolName(toolName);
|
|
if (
|
|
isManifestToolOptional(manifestPlugin, toolName) &&
|
|
!isOptionalToolAllowed({
|
|
toolName,
|
|
pluginId: entry.pluginId,
|
|
allowlist,
|
|
})
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
selectedManifestToolNames &&
|
|
manifestContractToolNames?.has(normalizedToolName) &&
|
|
!selectedManifestToolNames.has(normalizedToolName)
|
|
) {
|
|
return false;
|
|
}
|
|
return isManifestToolNameAvailable({
|
|
plugin: manifestPlugin,
|
|
toolName,
|
|
config: params.context.runtimeConfig ?? context.config,
|
|
env,
|
|
hasAuthForProvider: params.hasAuthForProvider,
|
|
});
|
|
})
|
|
: listRaw;
|
|
const policyAvailableList = availableList.filter(
|
|
(tool) =>
|
|
!denylistBlocksPluginTool({
|
|
pluginId: entry.pluginId,
|
|
toolName: readPluginToolName(tool),
|
|
denylist,
|
|
}),
|
|
);
|
|
const list = entry.optional
|
|
? policyAvailableList.filter((tool) =>
|
|
isOptionalToolAllowed({
|
|
toolName: readPluginToolName(tool),
|
|
pluginId: entry.pluginId,
|
|
allowlist,
|
|
}),
|
|
)
|
|
: policyAvailableList;
|
|
if (list.length === 0) {
|
|
continue;
|
|
}
|
|
const normalizedNameSet = new Set<string>();
|
|
for (const toolRaw of list) {
|
|
// Plugin factories run at request time and can return arbitrary values; isolate
|
|
// malformed tools here so one bad plugin tool cannot poison every provider.
|
|
const malformedReason = describeMalformedPluginTool(toolRaw);
|
|
if (malformedReason) {
|
|
const message = `plugin tool is malformed (${entry.pluginId}): ${malformedReason}`;
|
|
context.logger.error(message);
|
|
registry.diagnostics.push({
|
|
level: "error",
|
|
pluginId: entry.pluginId,
|
|
source: entry.source,
|
|
message,
|
|
});
|
|
continue;
|
|
}
|
|
const tool = toolRaw as AnyAgentTool;
|
|
const undeclared = entry.declaredNames
|
|
? findUndeclaredPluginToolNames({
|
|
declaredNames: entry.declaredNames,
|
|
toolNames: [tool.name],
|
|
})
|
|
: [];
|
|
if (undeclared.length > 0) {
|
|
const message = `plugin tool is undeclared (${entry.pluginId}): ${undeclared.join(", ")}`;
|
|
context.logger.error(message);
|
|
registry.diagnostics.push({
|
|
level: "error",
|
|
pluginId: entry.pluginId,
|
|
source: entry.source,
|
|
message,
|
|
});
|
|
continue;
|
|
}
|
|
const normalizedToolName = normalizeToolName(tool.name);
|
|
if (normalizedNameSet.has(normalizedToolName) || existingNormalized.has(normalizedToolName)) {
|
|
const message = `plugin tool name conflict (${entry.pluginId}): ${tool.name}`;
|
|
if (!params.suppressNameConflicts) {
|
|
context.logger.error(message);
|
|
registry.diagnostics.push({
|
|
level: "error",
|
|
pluginId: entry.pluginId,
|
|
source: entry.source,
|
|
message,
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
normalizedNameSet.add(normalizedToolName);
|
|
existing.add(tool.name);
|
|
existingNormalized.add(normalizedToolName);
|
|
const optional = isPluginToolOptional({
|
|
entry,
|
|
manifestPlugin,
|
|
toolName: tool.name,
|
|
});
|
|
pluginToolMeta.set(tool, {
|
|
pluginId: entry.pluginId,
|
|
optional,
|
|
});
|
|
if (manifestPlugin) {
|
|
const capturedDescriptors = capturedDescriptorsByPluginId.get(entry.pluginId) ?? [];
|
|
capturedDescriptors.push(
|
|
capturePluginToolDescriptor({
|
|
pluginId: entry.pluginId,
|
|
tool,
|
|
optional,
|
|
}),
|
|
);
|
|
capturedDescriptorsByPluginId.set(entry.pluginId, capturedDescriptors);
|
|
}
|
|
tools.push(tool);
|
|
}
|
|
}
|
|
|
|
for (const [pluginId, descriptors] of capturedDescriptorsByPluginId) {
|
|
const manifestPlugin = manifestPluginsById.get(pluginId);
|
|
if (!manifestPlugin) {
|
|
continue;
|
|
}
|
|
const availableToolNames = listManifestToolNamesForAvailability({
|
|
plugin: manifestPlugin,
|
|
toolNames: manifestPlugin.contracts?.tools ?? [],
|
|
pluginId,
|
|
allowlist,
|
|
}).filter(
|
|
(toolName) =>
|
|
!denylistBlocksPluginTool({
|
|
pluginId,
|
|
toolName,
|
|
denylist,
|
|
}),
|
|
);
|
|
if (
|
|
cachedDescriptorsCoverToolNames({
|
|
descriptors,
|
|
toolNames: availableToolNames,
|
|
})
|
|
) {
|
|
writeCachedPluginToolDescriptors({
|
|
cacheKey: buildPluginDescriptorCacheKey({
|
|
plugin: manifestPlugin,
|
|
ctx: params.context,
|
|
currentRuntimeConfig: currentRuntimeConfigForDescriptorCache,
|
|
configCacheKeyMemo,
|
|
}),
|
|
descriptors,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (factoryTimings.length > 0) {
|
|
const totalMs =
|
|
factoryTimings.at(-1)?.elapsedMs ?? toElapsedMs(Date.now() - factoryTimingStartedAt);
|
|
const timingSummary = { totalMs, timings: factoryTimings };
|
|
if (shouldWarnPluginToolFactoryTimings(timingSummary)) {
|
|
log.warn(formatPluginToolFactoryTimingSummary(timingSummary));
|
|
} else if (log.isEnabled("trace")) {
|
|
log.trace(formatPluginToolFactoryTimingSummary(timingSummary));
|
|
}
|
|
}
|
|
|
|
return tools;
|
|
}
|