fix(plugins): normalize tool name conflicts

This commit is contained in:
Vincent Koc
2026-05-03 11:27:24 -07:00
parent 2b7e8dacd3
commit 9d5fedb9b5
3 changed files with 61 additions and 7 deletions

View File

@@ -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.

View File

@@ -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",

View File

@@ -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,