fix: keep active memory tools available

This commit is contained in:
Peter Steinberger
2026-05-03 12:23:55 +01:00
parent 02c2160478
commit 5e9135f2e2
5 changed files with 301 additions and 16 deletions

View File

@@ -389,7 +389,7 @@ describe("resolvePluginTools optional tools", () => {
});
beforeEach(() => {
loadOpenClawPluginsMock.mockClear();
loadOpenClawPluginsMock.mockReset();
resolveRuntimePluginRegistryMock.mockReset();
resolveRuntimePluginRegistryMock.mockImplementation((params) =>
loadOpenClawPluginsMock(params),
@@ -1185,6 +1185,217 @@ describe("resolvePluginTools optional tools", () => {
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("does not let disabled bundled tool owners poison explicit runtime allowlists", () => {
const config = {
plugins: {
enabled: true,
allow: ["memory-core", "memory-lancedb"],
load: { paths: [] },
entries: {
"memory-core": { enabled: true },
"memory-lancedb": { enabled: false },
},
slots: { memory: "memory-core" },
},
};
installToolManifestSnapshots({
config,
plugins: [
{
id: "memory-core",
origin: "bundled",
enabledByDefault: false,
channels: [],
providers: [],
contracts: {
tools: ["memory_get", "memory_search"],
},
},
{
id: "memory-lancedb",
origin: "bundled",
enabledByDefault: false,
channels: [],
providers: [],
contracts: {
tools: ["memory_recall"],
},
},
],
});
const memorySearchFactory = vi.fn(() => [makeTool("memory_search"), makeTool("memory_get")]);
const activeRegistry = {
plugins: [
{ id: "memory-core", status: "loaded" },
{ id: "memory-lancedb", status: "disabled" },
],
tools: [
{
pluginId: "memory-core",
optional: false,
source: "/tmp/memory-core.js",
names: ["memory_search", "memory_get"],
declaredNames: ["memory_search", "memory_get"],
factory: memorySearchFactory,
},
],
diagnostics: [],
};
setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable", "/tmp");
resolveRuntimePluginRegistryMock.mockReturnValue(undefined);
const tools = resolvePluginTools(
createResolveToolsParams({
context: { ...createContext(), config },
toolAllowlist: ["memory_recall", "memory_search", "memory_get"],
allowGatewaySubagentBinding: true,
}),
);
expectResolvedToolNames(tools, ["memory_search", "memory_get"]);
expect(memorySearchFactory).toHaveBeenCalledTimes(1);
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("falls back from a loaded channel registry without matching tool entries", () => {
const config = {
plugins: {
enabled: true,
allow: ["memory-core"],
load: { paths: [] },
entries: {
"memory-core": { enabled: true },
},
slots: { memory: "memory-core" },
},
};
installToolManifestSnapshot({
config,
plugin: {
id: "memory-core",
origin: "bundled",
enabledByDefault: false,
channels: [],
providers: [],
contracts: {
tools: ["memory_get", "memory_search"],
},
},
});
const memorySearchFactory = vi.fn(() => [makeTool("memory_search"), makeTool("memory_get")]);
const activeRegistry = {
plugins: [{ id: "memory-core", status: "loaded" }],
tools: [
{
pluginId: "memory-core",
optional: false,
source: "/tmp/memory-core.js",
names: ["memory_search", "memory_get"],
declaredNames: ["memory_search", "memory_get"],
factory: memorySearchFactory,
},
],
diagnostics: [],
};
setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable", "/tmp");
pinActivePluginChannelRegistry({
plugins: [{ id: "memory-core", status: "loaded" }],
tools: [],
diagnostics: [],
} as never);
resolveRuntimePluginRegistryMock.mockReturnValue(undefined);
const tools = resolvePluginTools(
createResolveToolsParams({
context: { ...createContext(), config },
toolAllowlist: ["memory_search", "memory_get"],
allowGatewaySubagentBinding: true,
}),
);
expectResolvedToolNames(tools, ["memory_search", "memory_get"]);
expect(memorySearchFactory).toHaveBeenCalledTimes(1);
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("loads a standalone registry when cached runtime registries lack matching tool entries", () => {
const config = {
plugins: {
enabled: true,
allow: ["memory-core"],
load: { paths: [] },
entries: {
"memory-core": { enabled: true },
},
slots: { memory: "memory-core" },
},
};
installToolManifestSnapshot({
config,
plugin: {
id: "memory-core",
origin: "bundled",
enabledByDefault: false,
channels: [],
providers: [],
contracts: {
tools: ["memory_get", "memory_search"],
},
},
});
const memorySearchFactory = vi.fn(() => [makeTool("memory_search"), makeTool("memory_get")]);
const loadedRegistry = {
plugins: [{ id: "memory-core", status: "loaded" }],
tools: [
{
pluginId: "memory-core",
optional: false,
source: "/tmp/memory-core.js",
names: ["memory_search", "memory_get"],
declaredNames: ["memory_search", "memory_get"],
factory: memorySearchFactory,
},
],
diagnostics: [],
};
setActivePluginRegistry(
{
plugins: [{ id: "memory-core", status: "loaded" }],
tools: [],
diagnostics: [],
} as never,
"gateway-startup",
"gateway-bindable",
"/tmp",
);
pinActivePluginChannelRegistry({
plugins: [{ id: "memory-core", status: "loaded" }],
tools: [],
diagnostics: [],
} as never);
resolveRuntimePluginRegistryMock.mockReturnValue(undefined);
loadOpenClawPluginsMock.mockReturnValue(loadedRegistry);
const tools = resolvePluginTools(
createResolveToolsParams({
context: { ...createContext(), config },
toolAllowlist: ["memory_search", "memory_get"],
allowGatewaySubagentBinding: true,
}),
);
expectResolvedToolNames(tools, ["memory_search", "memory_get"]);
expect(memorySearchFactory).toHaveBeenCalledTimes(1);
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
expect.objectContaining({
activate: false,
onlyPluginIds: ["memory-core"],
toolDiscovery: true,
}),
);
});
it("adds enabled non-startup tool plugins to the active tool runtime scope", () => {
const activeRegistry = createOptionalDemoActiveRegistry();
setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable", "/tmp");
@@ -1207,7 +1418,18 @@ describe("resolvePluginTools optional tools", () => {
allowGatewaySubagentBinding: true,
});
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(
expect.objectContaining({
onlyPluginIds: expect.arrayContaining(["tavily"]),
toolDiscovery: true,
}),
);
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
expect.objectContaining({
onlyPluginIds: expect.arrayContaining(["tavily"]),
toolDiscovery: true,
}),
);
});
it("reuses the pinned gateway channel registry after provider runtime loads replace active registry", () => {

View File

@@ -11,7 +11,7 @@ import {
import type { PluginManifestRecord } from "./manifest-registry.js";
import { hasManifestToolAvailability } from "./manifest-tool-availability.js";
import type { PluginMetadataManifestView } from "./plugin-metadata-snapshot.types.js";
import type { PluginToolRegistration } from "./registry-types.js";
import type { PluginRegistry, PluginToolRegistration } from "./registry-types.js";
import {
buildPluginRuntimeLoadOptions,
resolvePluginRuntimeLoadContext,
@@ -345,6 +345,7 @@ function resolvePluginToolRuntimePluginIds(params: {
}): string[] {
const pluginIds = new Set<string>();
const allowlist = normalizeAllowlist(params.toolAllowlist);
const normalizedPlugins = normalizePluginsConfig(params.config?.plugins);
const snapshot =
params.snapshot ??
loadManifestContractSnapshot({
@@ -362,6 +363,12 @@ function resolvePluginToolRuntimePluginIds(params: {
) {
continue;
}
if (
normalizedPlugins.entries[plugin.id]?.enabled === false ||
normalizedPlugins.deny.includes(plugin.id)
) {
continue;
}
const toolNames = plugin.contracts?.tools ?? [];
if (
manifestToolContractMatchesAllowlist({
@@ -620,18 +627,50 @@ function resolvePluginToolRegistry(params: {
workspaceDir: params.loadOptions.workspaceDir,
requiredPluginIds: params.onlyPluginIds,
};
return (
getLoadedRuntimePluginRegistry({
...lookup,
surface: "channel",
}) ??
getLoadedRuntimePluginRegistry({
env: lookup.env,
workspaceDir: lookup.workspaceDir,
requiredPluginIds: lookup.requiredPluginIds,
surface: "active",
})
);
const channelRegistry = getLoadedRuntimePluginRegistry({
...lookup,
surface: "channel",
});
if (registryHasScopedPluginTools(channelRegistry, params.onlyPluginIds)) {
return channelRegistry;
}
const activeRegistry = getLoadedRuntimePluginRegistry({
env: lookup.env,
workspaceDir: lookup.workspaceDir,
requiredPluginIds: lookup.requiredPluginIds,
surface: "active",
});
if (registryHasScopedPluginTools(activeRegistry, params.onlyPluginIds)) {
return activeRegistry;
}
const standaloneRegistry = ensureStandaloneRuntimePluginRegistryLoaded({
surface: "active",
requiredPluginIds: params.onlyPluginIds,
loadOptions: params.loadOptions,
});
if (registryHasScopedPluginTools(standaloneRegistry, params.onlyPluginIds)) {
return standaloneRegistry;
}
return channelRegistry ?? activeRegistry ?? standaloneRegistry;
}
function registryHasScopedPluginTools(
registry: PluginRegistry | undefined,
pluginIds: readonly string[] | undefined,
): registry is PluginRegistry {
if (!registry) {
return false;
}
if (pluginIds === undefined) {
return (registry.tools?.length ?? 0) > 0;
}
const scopedPluginIds = new Set(pluginIds);
if (scopedPluginIds.size === 0) {
return true;
}
return registry.tools.some((entry) => scopedPluginIds.has(entry.pluginId));
}
function resolvePluginToolLoadState(params: {