Skip to content

Commit 09e7eb6

Browse files
committed
fix(plugins): preserve optional tool metadata
1 parent 3f732ae commit 09e7eb6

3 files changed

Lines changed: 85 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai
4646

4747
### Fixes
4848

49+
- Plugins/tools: mark manifest-optional sibling tools as optional even when they come from a shared non-optional factory, so cached/status/MCP metadata keeps opt-in tool policy accurate. Thanks @vincentkoc.
4950
- Matrix: keep `streaming.progress.toolProgress` scoped to progress draft mode, so partial and quiet Matrix previews do not lose tool progress unless `streaming.preview.toolProgress` is disabled. Thanks @vincentkoc.
5051
- Channels/streaming: keep `streaming.progress.toolProgress` scoped to progress draft mode, so disabling compact progress lines does not silence partial/block preview tool updates. Thanks @vincentkoc.
5152
- Plugins/update: treat OpenClaw stable correction versions like `2026.5.3-1` as stable releases for npm installs, plugin updates, and bundled-version comparisons, so `latest` can advance official plugins without prerelease opt-in. Thanks @vincentkoc.

src/plugins/tools.optional.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ vi.mock("../config/plugin-auto-enable.js", () => ({
3232
let resolvePluginTools: typeof import("./tools.js").resolvePluginTools;
3333
let ensureStandalonePluginToolRegistryLoaded: typeof import("./tools.js").ensureStandalonePluginToolRegistryLoaded;
3434
let buildPluginToolMetadataKey: typeof import("./tools.js").buildPluginToolMetadataKey;
35+
let getPluginToolMeta: typeof import("./tools.js").getPluginToolMeta;
3536
let resetPluginToolFactoryCache: typeof import("./tools.js").resetPluginToolFactoryCache;
3637
let getActivePluginRegistry: typeof import("./runtime.js").getActivePluginRegistry;
3738
let pinActivePluginChannelRegistry: typeof import("./runtime.js").pinActivePluginChannelRegistry;
@@ -410,6 +411,7 @@ describe("resolvePluginTools optional tools", () => {
410411
({
411412
buildPluginToolMetadataKey,
412413
ensureStandalonePluginToolRegistryLoaded,
414+
getPluginToolMeta,
413415
resetPluginToolFactoryCache,
414416
resolvePluginTools,
415417
} = await import("./tools.js"));
@@ -1270,6 +1272,70 @@ describe("resolvePluginTools optional tools", () => {
12701272
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
12711273
});
12721274

1275+
it("marks allowlisted manifest-optional sibling tools from non-optional factories as optional", () => {
1276+
const config = createContext().config;
1277+
installToolManifestSnapshot({
1278+
config,
1279+
plugin: {
1280+
id: "multi",
1281+
origin: "bundled",
1282+
enabledByDefault: true,
1283+
channels: [],
1284+
providers: [],
1285+
contracts: {
1286+
tools: ["other_tool", "optional_tool"],
1287+
},
1288+
toolMetadata: {
1289+
optional_tool: {
1290+
optional: true,
1291+
},
1292+
},
1293+
},
1294+
});
1295+
const factory = vi.fn(() => [makeTool("other_tool"), makeTool("optional_tool")]);
1296+
setActivePluginRegistry(
1297+
createToolRegistry([
1298+
{
1299+
pluginId: "multi",
1300+
optional: false,
1301+
source: "/tmp/multi.js",
1302+
names: ["other_tool", "optional_tool"],
1303+
declaredNames: ["other_tool", "optional_tool"],
1304+
factory,
1305+
},
1306+
]) as never,
1307+
"test-tool-registry",
1308+
"gateway-bindable",
1309+
"/tmp",
1310+
);
1311+
1312+
const first = resolvePluginTools(
1313+
createResolveToolsParams({
1314+
context: {
1315+
...createContext(),
1316+
config,
1317+
},
1318+
toolAllowlist: [DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY, "optional_tool"],
1319+
}),
1320+
);
1321+
const second = resolvePluginTools(
1322+
createResolveToolsParams({
1323+
context: {
1324+
...createContext(),
1325+
config,
1326+
},
1327+
toolAllowlist: [DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY, "optional_tool"],
1328+
}),
1329+
);
1330+
1331+
expectResolvedToolNames(first, ["other_tool", "optional_tool"]);
1332+
expectResolvedToolNames(second, ["other_tool", "optional_tool"]);
1333+
expect(getPluginToolMeta(first[0])?.optional).toBe(false);
1334+
expect(getPluginToolMeta(first[1])?.optional).toBe(true);
1335+
expect(getPluginToolMeta(second[1])?.optional).toBe(true);
1336+
expect(factory).toHaveBeenCalledTimes(1);
1337+
});
1338+
12731339
it("rejects plugin id collisions with core tool names", () => {
12741340
const registry = setRegistry([
12751341
{

src/plugins/tools.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,17 @@ function isManifestToolOptional(plugin: PluginManifestRecord, toolName: string):
128128
return plugin.toolMetadata?.[toolName]?.optional === true;
129129
}
130130

131+
function isPluginToolOptional(params: {
132+
entry: PluginToolRegistration;
133+
manifestPlugin: PluginManifestRecord | undefined;
134+
toolName: string;
135+
}): boolean {
136+
return (
137+
params.entry.optional ||
138+
(params.manifestPlugin ? isManifestToolOptional(params.manifestPlugin, params.toolName) : false)
139+
);
140+
}
141+
131142
function isOptionalToolAllowed(params: {
132143
toolName: string;
133144
pluginId: string;
@@ -1162,17 +1173,22 @@ export function resolvePluginTools(params: {
11621173
normalizedNameSet.add(normalizedToolName);
11631174
existing.add(tool.name);
11641175
existingNormalized.add(normalizedToolName);
1176+
const optional = isPluginToolOptional({
1177+
entry,
1178+
manifestPlugin,
1179+
toolName: tool.name,
1180+
});
11651181
pluginToolMeta.set(tool, {
11661182
pluginId: entry.pluginId,
1167-
optional: entry.optional,
1183+
optional,
11681184
});
11691185
if (manifestPlugin) {
11701186
const capturedDescriptors = capturedDescriptorsByPluginId.get(entry.pluginId) ?? [];
11711187
capturedDescriptors.push(
11721188
capturePluginToolDescriptor({
11731189
pluginId: entry.pluginId,
11741190
tool,
1175-
optional: entry.optional,
1191+
optional,
11761192
}),
11771193
);
11781194
capturedDescriptorsByPluginId.set(entry.pluginId, capturedDescriptors);

0 commit comments

Comments
 (0)