mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:20:43 +00:00
fix(plugins): normalize tool name conflicts
This commit is contained in:
@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord/native commands: skip slash-command registration and cleanup REST calls when `channels.discord.commands.native=false`, letting low-power gateways start without waiting on disabled native-command lifecycle requests. Fixes #76202. Thanks @vincentkoc.
|
||||
- Plugins/commands: normalize empty plugin command handler results and let Telegram native plugin commands send the empty-response fallback instead of throwing when a handler returns `undefined`. Fixes #74800. Thanks @vincentkoc.
|
||||
- Plugins/tools: cold-load selected plugin tool registries when the active registry only has partial tool coverage, so wildcard-expanded allowlists no longer hide installed plugin tools from `tools.effective`. Fixes #76780. Thanks @lilesjtu.
|
||||
- Plugins/tools: compare cached and runtime plugin tool name conflicts with normalized core tool names, so case variants of core tools are blocked instead of leaking duplicate tool registrations. Thanks @vincentkoc.
|
||||
- Plugins/OpenRouter: advertise DeepSeek V4 thinking levels, including `xhigh` and `max`, through the runtime and lightweight provider policy surfaces so `/think` validation no longer rejects OpenRouter-routed DeepSeek V4 models. Fixes #74788. Thanks @vincentkoc.
|
||||
- Status/sessions: ignore malformed non-string persisted session provider/model metadata instead of throwing while rendering status summaries. Thanks @vincentkoc.
|
||||
- CLI/config: remove only the targeted array element for `openclaw config unset array[index]` instead of replaying the unset during config write and deleting the shifted next element. Fixes #76290. Thanks @SymbolStar and @vincentkoc.
|
||||
|
||||
@@ -982,6 +982,56 @@ describe("resolvePluginTools optional tools", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects normalized plugin tool name collisions with core tools", () => {
|
||||
const registry = setRegistry([
|
||||
{
|
||||
pluginId: "multi",
|
||||
optional: false,
|
||||
source: "/tmp/multi.js",
|
||||
names: ["Message", "other_tool"],
|
||||
declaredNames: ["Message", "other_tool"],
|
||||
factory: () => [makeTool("Message"), makeTool("other_tool")],
|
||||
},
|
||||
]);
|
||||
|
||||
const tools = resolvePluginTools(
|
||||
createResolveToolsParams({
|
||||
existingToolNames: new Set(["message"]),
|
||||
}),
|
||||
);
|
||||
|
||||
expectResolvedToolNames(tools, ["other_tool"]);
|
||||
expectSingleDiagnosticMessage(
|
||||
registry.diagnostics,
|
||||
"plugin tool name conflict (multi): Message",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects normalized cached plugin tool name collisions with core tools", () => {
|
||||
const factory = vi.fn(() => makeTool("Message"));
|
||||
setRegistry([
|
||||
{
|
||||
pluginId: "multi",
|
||||
optional: false,
|
||||
source: "/tmp/multi.js",
|
||||
names: ["Message"],
|
||||
declaredNames: ["Message"],
|
||||
factory,
|
||||
},
|
||||
]);
|
||||
|
||||
const first = resolvePluginTools(createResolveToolsParams());
|
||||
const second = resolvePluginTools(
|
||||
createResolveToolsParams({
|
||||
existingToolNames: new Set(["message"]),
|
||||
}),
|
||||
);
|
||||
|
||||
expectResolvedToolNames(first, ["Message"]);
|
||||
expect(second).toEqual([]);
|
||||
expect(factory).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "uses loaded plugin tools with an explicit env",
|
||||
|
||||
@@ -567,7 +567,7 @@ function resolveCachedPluginTools(params: {
|
||||
}
|
||||
const pluginTools: AnyAgentTool[] = [];
|
||||
let hasNameConflict = false;
|
||||
const localNames = new Set<string>();
|
||||
const localNormalizedNames = new Set<string>();
|
||||
for (const cachedDescriptor of cached) {
|
||||
if (
|
||||
!cachedDescriptor.optional &&
|
||||
@@ -587,14 +587,15 @@ function resolveCachedPluginTools(params: {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const normalizedDescriptorName = normalizeToolName(cachedDescriptor.descriptor.name);
|
||||
if (
|
||||
localNames.has(cachedDescriptor.descriptor.name) ||
|
||||
params.existing.has(cachedDescriptor.descriptor.name)
|
||||
localNormalizedNames.has(normalizedDescriptorName) ||
|
||||
params.existingNormalized.has(normalizedDescriptorName)
|
||||
) {
|
||||
hasNameConflict = true;
|
||||
break;
|
||||
}
|
||||
localNames.add(cachedDescriptor.descriptor.name);
|
||||
localNormalizedNames.add(normalizedDescriptorName);
|
||||
pluginTools.push(
|
||||
createCachedDescriptorPluginTool({
|
||||
descriptor: cachedDescriptor,
|
||||
@@ -929,7 +930,7 @@ export function resolvePluginTools(params: {
|
||||
if (list.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const nameSet = new Set<string>();
|
||||
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.
|
||||
@@ -963,7 +964,8 @@ export function resolvePluginTools(params: {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (nameSet.has(tool.name) || existing.has(tool.name)) {
|
||||
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);
|
||||
@@ -976,8 +978,9 @@ export function resolvePluginTools(params: {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
nameSet.add(tool.name);
|
||||
normalizedNameSet.add(normalizedToolName);
|
||||
existing.add(tool.name);
|
||||
existingNormalized.add(normalizedToolName);
|
||||
pluginToolMeta.set(tool, {
|
||||
pluginId: entry.pluginId,
|
||||
optional: entry.optional,
|
||||
|
||||
Reference in New Issue
Block a user