Skip to content

Commit 5e9135f

Browse files
committed
fix: keep active memory tools available
1 parent 02c2160 commit 5e9135f

5 files changed

Lines changed: 301 additions & 16 deletions

File tree

CHANGELOG.md

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

2020
### Fixes
2121

22+
- Plugins/tools: keep disabled bundled tool plugins out of explicit runtime allowlist ownership and fall back from loaded-but-empty channel registries to tool-bearing plugin registries, so Active Memory can use bundled `memory-core` search/get tools even when `memory-lancedb` is disabled. Fixes #76603. Thanks @jwong-art.
2223
- Plugins/externalization: keep official ACPX, Google Chat, and LINE install specs on production package names, leaving beta-tag probing to the explicit OpenClaw beta update channel. Thanks @vincentkoc.
2324
- CLI/doctor: keep missing-plugin repair from overriding official catalog metadata with runtime fallbacks, so ACPX repairs preserve the official npm spec during the externalization rollout. Thanks @vincentkoc.
2425
- Plugins/catalog: preserve ClawHub install specs when generating the packaged channel catalog so future storepack-first channel plugins keep their remote source instead of becoming npm-only. Thanks @vincentkoc.

extensions/qa-lab/src/providers/mock-openai/server.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1210,7 +1210,7 @@ describe("qa mock openai server", () => {
12101210
}),
12111211
});
12121212
expect(activeMemorySearch.status).toBe(200);
1213-
expect(await activeMemorySearch.text()).toContain('"name":"memory_recall"');
1213+
expect(await activeMemorySearch.text()).toContain('"name":"memory_search"');
12141214

12151215
const activeMemoryStreamSummary = await fetch(`${server.baseUrl}/v1/responses`, {
12161216
method: "POST",

extensions/qa-lab/src/providers/mock-openai/server.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,6 +1465,12 @@ async function buildResponsesPayload(
14651465
/silent snack recall check/i.test(allInputText)
14661466
) {
14671467
if (!toolOutput) {
1468+
if (!hasDeclaredTool(body, "memory_recall")) {
1469+
return buildToolCallEventsWithArgs("memory_search", {
1470+
query: "QA movie night snack lemon pepper wings blue cheese",
1471+
maxResults: 3,
1472+
});
1473+
}
14681474
return buildToolCallEventsWithArgs("memory_recall", {
14691475
query: "QA movie night snack lemon pepper wings blue cheese",
14701476
limit: 3,
@@ -1490,6 +1496,23 @@ async function buildResponsesPayload(
14901496
}
14911497
return buildAssistantEvents("NONE");
14921498
}
1499+
const results = Array.isArray(toolJson?.results)
1500+
? (toolJson.results as Array<Record<string, unknown>>)
1501+
: [];
1502+
const first = results[0];
1503+
if (typeof first?.path === "string") {
1504+
const from =
1505+
typeof first.startLine === "number"
1506+
? Math.max(1, first.startLine)
1507+
: typeof first.endLine === "number"
1508+
? Math.max(1, first.endLine)
1509+
: 1;
1510+
return buildToolCallEventsWithArgs("memory_get", {
1511+
path: first.path,
1512+
from,
1513+
lines: 4,
1514+
});
1515+
}
14931516
const memorySnippet = Array.isArray(toolJson?.results)
14941517
? JSON.stringify(toolJson.results)
14951518
: toolOutput;

src/plugins/tools.optional.test.ts

Lines changed: 224 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ describe("resolvePluginTools optional tools", () => {
389389
});
390390

391391
beforeEach(() => {
392-
loadOpenClawPluginsMock.mockClear();
392+
loadOpenClawPluginsMock.mockReset();
393393
resolveRuntimePluginRegistryMock.mockReset();
394394
resolveRuntimePluginRegistryMock.mockImplementation((params) =>
395395
loadOpenClawPluginsMock(params),
@@ -1185,6 +1185,217 @@ describe("resolvePluginTools optional tools", () => {
11851185
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
11861186
});
11871187

1188+
it("does not let disabled bundled tool owners poison explicit runtime allowlists", () => {
1189+
const config = {
1190+
plugins: {
1191+
enabled: true,
1192+
allow: ["memory-core", "memory-lancedb"],
1193+
load: { paths: [] },
1194+
entries: {
1195+
"memory-core": { enabled: true },
1196+
"memory-lancedb": { enabled: false },
1197+
},
1198+
slots: { memory: "memory-core" },
1199+
},
1200+
};
1201+
installToolManifestSnapshots({
1202+
config,
1203+
plugins: [
1204+
{
1205+
id: "memory-core",
1206+
origin: "bundled",
1207+
enabledByDefault: false,
1208+
channels: [],
1209+
providers: [],
1210+
contracts: {
1211+
tools: ["memory_get", "memory_search"],
1212+
},
1213+
},
1214+
{
1215+
id: "memory-lancedb",
1216+
origin: "bundled",
1217+
enabledByDefault: false,
1218+
channels: [],
1219+
providers: [],
1220+
contracts: {
1221+
tools: ["memory_recall"],
1222+
},
1223+
},
1224+
],
1225+
});
1226+
const memorySearchFactory = vi.fn(() => [makeTool("memory_search"), makeTool("memory_get")]);
1227+
const activeRegistry = {
1228+
plugins: [
1229+
{ id: "memory-core", status: "loaded" },
1230+
{ id: "memory-lancedb", status: "disabled" },
1231+
],
1232+
tools: [
1233+
{
1234+
pluginId: "memory-core",
1235+
optional: false,
1236+
source: "/tmp/memory-core.js",
1237+
names: ["memory_search", "memory_get"],
1238+
declaredNames: ["memory_search", "memory_get"],
1239+
factory: memorySearchFactory,
1240+
},
1241+
],
1242+
diagnostics: [],
1243+
};
1244+
setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable", "/tmp");
1245+
resolveRuntimePluginRegistryMock.mockReturnValue(undefined);
1246+
1247+
const tools = resolvePluginTools(
1248+
createResolveToolsParams({
1249+
context: { ...createContext(), config },
1250+
toolAllowlist: ["memory_recall", "memory_search", "memory_get"],
1251+
allowGatewaySubagentBinding: true,
1252+
}),
1253+
);
1254+
1255+
expectResolvedToolNames(tools, ["memory_search", "memory_get"]);
1256+
expect(memorySearchFactory).toHaveBeenCalledTimes(1);
1257+
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
1258+
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
1259+
});
1260+
1261+
it("falls back from a loaded channel registry without matching tool entries", () => {
1262+
const config = {
1263+
plugins: {
1264+
enabled: true,
1265+
allow: ["memory-core"],
1266+
load: { paths: [] },
1267+
entries: {
1268+
"memory-core": { enabled: true },
1269+
},
1270+
slots: { memory: "memory-core" },
1271+
},
1272+
};
1273+
installToolManifestSnapshot({
1274+
config,
1275+
plugin: {
1276+
id: "memory-core",
1277+
origin: "bundled",
1278+
enabledByDefault: false,
1279+
channels: [],
1280+
providers: [],
1281+
contracts: {
1282+
tools: ["memory_get", "memory_search"],
1283+
},
1284+
},
1285+
});
1286+
const memorySearchFactory = vi.fn(() => [makeTool("memory_search"), makeTool("memory_get")]);
1287+
const activeRegistry = {
1288+
plugins: [{ id: "memory-core", status: "loaded" }],
1289+
tools: [
1290+
{
1291+
pluginId: "memory-core",
1292+
optional: false,
1293+
source: "/tmp/memory-core.js",
1294+
names: ["memory_search", "memory_get"],
1295+
declaredNames: ["memory_search", "memory_get"],
1296+
factory: memorySearchFactory,
1297+
},
1298+
],
1299+
diagnostics: [],
1300+
};
1301+
setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable", "/tmp");
1302+
pinActivePluginChannelRegistry({
1303+
plugins: [{ id: "memory-core", status: "loaded" }],
1304+
tools: [],
1305+
diagnostics: [],
1306+
} as never);
1307+
resolveRuntimePluginRegistryMock.mockReturnValue(undefined);
1308+
1309+
const tools = resolvePluginTools(
1310+
createResolveToolsParams({
1311+
context: { ...createContext(), config },
1312+
toolAllowlist: ["memory_search", "memory_get"],
1313+
allowGatewaySubagentBinding: true,
1314+
}),
1315+
);
1316+
1317+
expectResolvedToolNames(tools, ["memory_search", "memory_get"]);
1318+
expect(memorySearchFactory).toHaveBeenCalledTimes(1);
1319+
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
1320+
});
1321+
1322+
it("loads a standalone registry when cached runtime registries lack matching tool entries", () => {
1323+
const config = {
1324+
plugins: {
1325+
enabled: true,
1326+
allow: ["memory-core"],
1327+
load: { paths: [] },
1328+
entries: {
1329+
"memory-core": { enabled: true },
1330+
},
1331+
slots: { memory: "memory-core" },
1332+
},
1333+
};
1334+
installToolManifestSnapshot({
1335+
config,
1336+
plugin: {
1337+
id: "memory-core",
1338+
origin: "bundled",
1339+
enabledByDefault: false,
1340+
channels: [],
1341+
providers: [],
1342+
contracts: {
1343+
tools: ["memory_get", "memory_search"],
1344+
},
1345+
},
1346+
});
1347+
const memorySearchFactory = vi.fn(() => [makeTool("memory_search"), makeTool("memory_get")]);
1348+
const loadedRegistry = {
1349+
plugins: [{ id: "memory-core", status: "loaded" }],
1350+
tools: [
1351+
{
1352+
pluginId: "memory-core",
1353+
optional: false,
1354+
source: "/tmp/memory-core.js",
1355+
names: ["memory_search", "memory_get"],
1356+
declaredNames: ["memory_search", "memory_get"],
1357+
factory: memorySearchFactory,
1358+
},
1359+
],
1360+
diagnostics: [],
1361+
};
1362+
setActivePluginRegistry(
1363+
{
1364+
plugins: [{ id: "memory-core", status: "loaded" }],
1365+
tools: [],
1366+
diagnostics: [],
1367+
} as never,
1368+
"gateway-startup",
1369+
"gateway-bindable",
1370+
"/tmp",
1371+
);
1372+
pinActivePluginChannelRegistry({
1373+
plugins: [{ id: "memory-core", status: "loaded" }],
1374+
tools: [],
1375+
diagnostics: [],
1376+
} as never);
1377+
resolveRuntimePluginRegistryMock.mockReturnValue(undefined);
1378+
loadOpenClawPluginsMock.mockReturnValue(loadedRegistry);
1379+
1380+
const tools = resolvePluginTools(
1381+
createResolveToolsParams({
1382+
context: { ...createContext(), config },
1383+
toolAllowlist: ["memory_search", "memory_get"],
1384+
allowGatewaySubagentBinding: true,
1385+
}),
1386+
);
1387+
1388+
expectResolvedToolNames(tools, ["memory_search", "memory_get"]);
1389+
expect(memorySearchFactory).toHaveBeenCalledTimes(1);
1390+
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
1391+
expect.objectContaining({
1392+
activate: false,
1393+
onlyPluginIds: ["memory-core"],
1394+
toolDiscovery: true,
1395+
}),
1396+
);
1397+
});
1398+
11881399
it("adds enabled non-startup tool plugins to the active tool runtime scope", () => {
11891400
const activeRegistry = createOptionalDemoActiveRegistry();
11901401
setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable", "/tmp");
@@ -1207,7 +1418,18 @@ describe("resolvePluginTools optional tools", () => {
12071418
allowGatewaySubagentBinding: true,
12081419
});
12091420

1210-
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
1421+
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(
1422+
expect.objectContaining({
1423+
onlyPluginIds: expect.arrayContaining(["tavily"]),
1424+
toolDiscovery: true,
1425+
}),
1426+
);
1427+
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
1428+
expect.objectContaining({
1429+
onlyPluginIds: expect.arrayContaining(["tavily"]),
1430+
toolDiscovery: true,
1431+
}),
1432+
);
12111433
});
12121434

12131435
it("reuses the pinned gateway channel registry after provider runtime loads replace active registry", () => {

src/plugins/tools.ts

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
import type { PluginManifestRecord } from "./manifest-registry.js";
1212
import { hasManifestToolAvailability } from "./manifest-tool-availability.js";
1313
import type { PluginMetadataManifestView } from "./plugin-metadata-snapshot.types.js";
14-
import type { PluginToolRegistration } from "./registry-types.js";
14+
import type { PluginRegistry, PluginToolRegistration } from "./registry-types.js";
1515
import {
1616
buildPluginRuntimeLoadOptions,
1717
resolvePluginRuntimeLoadContext,
@@ -345,6 +345,7 @@ function resolvePluginToolRuntimePluginIds(params: {
345345
}): string[] {
346346
const pluginIds = new Set<string>();
347347
const allowlist = normalizeAllowlist(params.toolAllowlist);
348+
const normalizedPlugins = normalizePluginsConfig(params.config?.plugins);
348349
const snapshot =
349350
params.snapshot ??
350351
loadManifestContractSnapshot({
@@ -362,6 +363,12 @@ function resolvePluginToolRuntimePluginIds(params: {
362363
) {
363364
continue;
364365
}
366+
if (
367+
normalizedPlugins.entries[plugin.id]?.enabled === false ||
368+
normalizedPlugins.deny.includes(plugin.id)
369+
) {
370+
continue;
371+
}
365372
const toolNames = plugin.contracts?.tools ?? [];
366373
if (
367374
manifestToolContractMatchesAllowlist({
@@ -620,18 +627,50 @@ function resolvePluginToolRegistry(params: {
620627
workspaceDir: params.loadOptions.workspaceDir,
621628
requiredPluginIds: params.onlyPluginIds,
622629
};
623-
return (
624-
getLoadedRuntimePluginRegistry({
625-
...lookup,
626-
surface: "channel",
627-
}) ??
628-
getLoadedRuntimePluginRegistry({
629-
env: lookup.env,
630-
workspaceDir: lookup.workspaceDir,
631-
requiredPluginIds: lookup.requiredPluginIds,
632-
surface: "active",
633-
})
634-
);
630+
const channelRegistry = getLoadedRuntimePluginRegistry({
631+
...lookup,
632+
surface: "channel",
633+
});
634+
if (registryHasScopedPluginTools(channelRegistry, params.onlyPluginIds)) {
635+
return channelRegistry;
636+
}
637+
638+
const activeRegistry = getLoadedRuntimePluginRegistry({
639+
env: lookup.env,
640+
workspaceDir: lookup.workspaceDir,
641+
requiredPluginIds: lookup.requiredPluginIds,
642+
surface: "active",
643+
});
644+
if (registryHasScopedPluginTools(activeRegistry, params.onlyPluginIds)) {
645+
return activeRegistry;
646+
}
647+
648+
const standaloneRegistry = ensureStandaloneRuntimePluginRegistryLoaded({
649+
surface: "active",
650+
requiredPluginIds: params.onlyPluginIds,
651+
loadOptions: params.loadOptions,
652+
});
653+
if (registryHasScopedPluginTools(standaloneRegistry, params.onlyPluginIds)) {
654+
return standaloneRegistry;
655+
}
656+
return channelRegistry ?? activeRegistry ?? standaloneRegistry;
657+
}
658+
659+
function registryHasScopedPluginTools(
660+
registry: PluginRegistry | undefined,
661+
pluginIds: readonly string[] | undefined,
662+
): registry is PluginRegistry {
663+
if (!registry) {
664+
return false;
665+
}
666+
if (pluginIds === undefined) {
667+
return (registry.tools?.length ?? 0) > 0;
668+
}
669+
const scopedPluginIds = new Set(pluginIds);
670+
if (scopedPluginIds.size === 0) {
671+
return true;
672+
}
673+
return registry.tools.some((entry) => scopedPluginIds.has(entry.pluginId));
635674
}
636675

637676
function resolvePluginToolLoadState(params: {

0 commit comments

Comments
 (0)