fix(plugins): respect manifest optional tool siblings

This commit is contained in:
Vincent Koc
2026-05-03 20:41:51 -07:00
parent a8b38bb742
commit e3cba91ef0
3 changed files with 89 additions and 5 deletions

View File

@@ -1214,6 +1214,62 @@ describe("resolvePluginTools optional tools", () => {
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("does not materialize manifest-optional sibling tools from non-optional factories by default", async () => {
const config = createContext().config;
installToolManifestSnapshot({
config,
plugin: {
id: "multi",
origin: "bundled",
enabledByDefault: true,
channels: [],
providers: [],
contracts: {
tools: ["other_tool", "optional_tool"],
},
toolMetadata: {
optional_tool: {
optional: true,
},
},
},
});
const factory = vi.fn(() => [makeTool("other_tool"), makeTool("optional_tool")]);
setActivePluginRegistry(
createToolRegistry([
{
pluginId: "multi",
optional: false,
source: "/tmp/multi.js",
names: ["other_tool", "optional_tool"],
declaredNames: ["other_tool", "optional_tool"],
factory,
},
]) as never,
"test-tool-registry",
"gateway-bindable",
"/tmp",
);
const { loadManifestContractSnapshot } = await import("./manifest-contract-eligibility.js");
const snapshot = loadManifestContractSnapshot({ config, workspaceDir: "/tmp" });
expect(
snapshot.plugins.find((plugin) => plugin.id === "multi")?.toolMetadata?.optional_tool,
).toMatchObject({ optional: true });
const tools = resolvePluginTools(
createResolveToolsParams({
context: {
...createContext(),
config,
},
}),
);
expectResolvedToolNames(tools, ["other_tool"]);
expect(factory).toHaveBeenCalledTimes(1);
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("rejects plugin id collisions with core tool names", () => {
const registry = setRegistry([
{

View File

@@ -1053,16 +1053,43 @@ export function resolvePluginTools(params: {
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) =>
isManifestToolNameAvailable({
? 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: readPluginToolName(tool),
toolName,
config: params.context.runtimeConfig ?? context.config,
env,
hasAuthForProvider: params.hasAuthForProvider,
}),
)
});
})
: listRaw;
const policyAvailableList = availableList.filter(
(tool) =>