mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
fix(plugins): preserve optional tool metadata
This commit is contained in:
@@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -32,6 +32,7 @@ vi.mock("../config/plugin-auto-enable.js", () => ({
|
||||
let resolvePluginTools: typeof import("./tools.js").resolvePluginTools;
|
||||
let ensureStandalonePluginToolRegistryLoaded: typeof import("./tools.js").ensureStandalonePluginToolRegistryLoaded;
|
||||
let buildPluginToolMetadataKey: typeof import("./tools.js").buildPluginToolMetadataKey;
|
||||
let getPluginToolMeta: typeof import("./tools.js").getPluginToolMeta;
|
||||
let resetPluginToolFactoryCache: typeof import("./tools.js").resetPluginToolFactoryCache;
|
||||
let getActivePluginRegistry: typeof import("./runtime.js").getActivePluginRegistry;
|
||||
let pinActivePluginChannelRegistry: typeof import("./runtime.js").pinActivePluginChannelRegistry;
|
||||
@@ -410,6 +411,7 @@ describe("resolvePluginTools optional tools", () => {
|
||||
({
|
||||
buildPluginToolMetadataKey,
|
||||
ensureStandalonePluginToolRegistryLoaded,
|
||||
getPluginToolMeta,
|
||||
resetPluginToolFactoryCache,
|
||||
resolvePluginTools,
|
||||
} = await import("./tools.js"));
|
||||
@@ -1270,6 +1272,70 @@ describe("resolvePluginTools optional tools", () => {
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("marks allowlisted manifest-optional sibling tools from non-optional factories as optional", () => {
|
||||
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 first = resolvePluginTools(
|
||||
createResolveToolsParams({
|
||||
context: {
|
||||
...createContext(),
|
||||
config,
|
||||
},
|
||||
toolAllowlist: [DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY, "optional_tool"],
|
||||
}),
|
||||
);
|
||||
const second = resolvePluginTools(
|
||||
createResolveToolsParams({
|
||||
context: {
|
||||
...createContext(),
|
||||
config,
|
||||
},
|
||||
toolAllowlist: [DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY, "optional_tool"],
|
||||
}),
|
||||
);
|
||||
|
||||
expectResolvedToolNames(first, ["other_tool", "optional_tool"]);
|
||||
expectResolvedToolNames(second, ["other_tool", "optional_tool"]);
|
||||
expect(getPluginToolMeta(first[0])?.optional).toBe(false);
|
||||
expect(getPluginToolMeta(first[1])?.optional).toBe(true);
|
||||
expect(getPluginToolMeta(second[1])?.optional).toBe(true);
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("rejects plugin id collisions with core tool names", () => {
|
||||
const registry = setRegistry([
|
||||
{
|
||||
|
||||
@@ -128,6 +128,17 @@ function isManifestToolOptional(plugin: PluginManifestRecord, toolName: string):
|
||||
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;
|
||||
@@ -1162,9 +1173,14 @@ export function resolvePluginTools(params: {
|
||||
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: entry.optional,
|
||||
optional,
|
||||
});
|
||||
if (manifestPlugin) {
|
||||
const capturedDescriptors = capturedDescriptorsByPluginId.get(entry.pluginId) ?? [];
|
||||
@@ -1172,7 +1188,7 @@ export function resolvePluginTools(params: {
|
||||
capturePluginToolDescriptor({
|
||||
pluginId: entry.pluginId,
|
||||
tool,
|
||||
optional: entry.optional,
|
||||
optional,
|
||||
}),
|
||||
);
|
||||
capturedDescriptorsByPluginId.set(entry.pluginId, capturedDescriptors);
|
||||
|
||||
Reference in New Issue
Block a user