mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(plugins): scope tool registry reuse to plugin plan
This commit is contained in:
@@ -165,7 +165,7 @@ describe("getCompatibleActivePluginRegistry", () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("reuses a scoped gateway-bindable registry for an unscoped default-mode request", () => {
|
||||
it("reuses a scoped gateway-bindable registry for a matching default-mode tool scope", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.plugins.push(
|
||||
{ id: "acpx" } as (typeof registry.plugins)[number],
|
||||
@@ -190,11 +190,12 @@ describe("getCompatibleActivePluginRegistry", () => {
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
config: startupOptions.config,
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
onlyPluginIds: ["acpx", "telegram"],
|
||||
}),
|
||||
).toBe(registry);
|
||||
});
|
||||
|
||||
it("reuses a scoped gateway-bindable registry for an unscoped snapshot-mode request", () => {
|
||||
it("reuses a scoped gateway-bindable registry for a matching snapshot-mode tool scope", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.plugins.push(
|
||||
{ id: "acpx" } as (typeof registry.plugins)[number],
|
||||
@@ -219,18 +220,52 @@ describe("getCompatibleActivePluginRegistry", () => {
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
config: startupOptions.config,
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
onlyPluginIds: ["acpx", "telegram"],
|
||||
activate: false,
|
||||
}),
|
||||
).toBe(registry);
|
||||
});
|
||||
|
||||
it("does not reuse a scoped registry when plugin IDs differ", () => {
|
||||
it("does not reuse a scoped registry when the requested tool scope needs another plugin", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.plugins.push({ id: "acpx" } as (typeof registry.plugins)[number]);
|
||||
registry.plugins.push(
|
||||
{ id: "acpx" } as (typeof registry.plugins)[number],
|
||||
{ id: "telegram" } as (typeof registry.plugins)[number],
|
||||
);
|
||||
const startupOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["acpx", "telegram"],
|
||||
allow: ["acpx", "telegram", "tavily"],
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
onlyPluginIds: ["acpx", "telegram"],
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
};
|
||||
const { cacheKey } = __testing.resolvePluginLoadCacheContext(startupOptions);
|
||||
setActivePluginRegistry(registry, cacheKey, "gateway-bindable");
|
||||
|
||||
expect(
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
config: startupOptions.config,
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
onlyPluginIds: ["acpx", "telegram", "tavily"],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not treat an unscoped request as compatible with the scoped startup registry", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.plugins.push(
|
||||
{ id: "acpx" } as (typeof registry.plugins)[number],
|
||||
{ id: "telegram" } as (typeof registry.plugins)[number],
|
||||
);
|
||||
const startupOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["acpx", "telegram", "tavily"],
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
@@ -309,6 +344,7 @@ describe("getCompatibleActivePluginRegistry", () => {
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
config: startupOptions.config,
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
onlyPluginIds: ["acpx", "telegram"],
|
||||
}),
|
||||
).toBe(registry);
|
||||
});
|
||||
|
||||
@@ -1002,7 +1002,23 @@ function getCompatibleActivePluginRegistry(
|
||||
return undefined;
|
||||
}
|
||||
const loadContext = resolvePluginLoadCacheContext(options);
|
||||
if (pluginLoadOptionsMatchCacheKey(options, activeCacheKey)) {
|
||||
const matchesActiveCacheKey = (candidate: PluginLoadOptions): boolean => {
|
||||
if (pluginLoadOptionsMatchCacheKey(candidate, activeCacheKey)) {
|
||||
return true;
|
||||
}
|
||||
if (candidate.coreGatewayMethodNames !== undefined) {
|
||||
return false;
|
||||
}
|
||||
return pluginLoadOptionsMatchCacheKey(
|
||||
{
|
||||
...candidate,
|
||||
coreGatewayMethodNames: activeRegistry.coreGatewayMethodNames ?? [],
|
||||
},
|
||||
activeCacheKey,
|
||||
);
|
||||
};
|
||||
|
||||
if (matchesActiveCacheKey(options)) {
|
||||
return activeRegistry;
|
||||
}
|
||||
if (!loadContext.shouldActivate) {
|
||||
@@ -1010,7 +1026,7 @@ function getCompatibleActivePluginRegistry(
|
||||
...options,
|
||||
activate: true,
|
||||
};
|
||||
if (pluginLoadOptionsMatchCacheKey(activatingOptions, activeCacheKey)) {
|
||||
if (matchesActiveCacheKey(activatingOptions)) {
|
||||
return activeRegistry;
|
||||
}
|
||||
}
|
||||
@@ -1025,7 +1041,7 @@ function getCompatibleActivePluginRegistry(
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
};
|
||||
if (pluginLoadOptionsMatchCacheKey(gatewayBindableOptions, activeCacheKey)) {
|
||||
if (matchesActiveCacheKey(gatewayBindableOptions)) {
|
||||
return activeRegistry;
|
||||
}
|
||||
if (!loadContext.shouldActivate) {
|
||||
@@ -1037,56 +1053,11 @@ function getCompatibleActivePluginRegistry(
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
};
|
||||
if (pluginLoadOptionsMatchCacheKey(activatingGatewayBindableOptions, activeCacheKey)) {
|
||||
if (matchesActiveCacheKey(activatingGatewayBindableOptions)) {
|
||||
return activeRegistry;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (loadContext.onlyPluginIds === undefined) {
|
||||
const scopedOptions = {
|
||||
...options,
|
||||
onlyPluginIds: activeRegistry.plugins.map((entry) => entry.id).toSorted(),
|
||||
coreGatewayMethodNames: activeRegistry.coreGatewayMethodNames ?? [],
|
||||
};
|
||||
if (pluginLoadOptionsMatchCacheKey(scopedOptions, activeCacheKey)) {
|
||||
return activeRegistry;
|
||||
}
|
||||
if (!loadContext.shouldActivate) {
|
||||
const activatingScopedOptions = {
|
||||
...scopedOptions,
|
||||
activate: true,
|
||||
};
|
||||
if (pluginLoadOptionsMatchCacheKey(activatingScopedOptions, activeCacheKey)) {
|
||||
return activeRegistry;
|
||||
}
|
||||
}
|
||||
if (
|
||||
loadContext.runtimeSubagentMode === "default" &&
|
||||
getActivePluginRuntimeSubagentMode() === "gateway-bindable"
|
||||
) {
|
||||
const gatewayBindableScopedOptions = {
|
||||
...scopedOptions,
|
||||
runtimeOptions: {
|
||||
...options.runtimeOptions,
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
};
|
||||
if (pluginLoadOptionsMatchCacheKey(gatewayBindableScopedOptions, activeCacheKey)) {
|
||||
return activeRegistry;
|
||||
}
|
||||
if (!loadContext.shouldActivate) {
|
||||
const activatingGatewayBindableScopedOptions = {
|
||||
...gatewayBindableScopedOptions,
|
||||
activate: true,
|
||||
};
|
||||
if (
|
||||
pluginLoadOptionsMatchCacheKey(activatingGatewayBindableScopedOptions, activeCacheKey)
|
||||
) {
|
||||
return activeRegistry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@ function resolveAutoEnabledOptionalDemoTools() {
|
||||
|
||||
function createOptionalDemoActiveRegistry() {
|
||||
return {
|
||||
plugins: [{ id: "optional-demo", status: "loaded" }],
|
||||
tools: [createOptionalDemoEntry()],
|
||||
diagnostics: [],
|
||||
};
|
||||
@@ -403,10 +404,10 @@ describe("resolvePluginTools optional tools", () => {
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reuses the active registry for gateway-bindable tool loads before reloading", () => {
|
||||
it("routes gateway-bindable tool loads through scoped runtime compatibility", () => {
|
||||
const activeRegistry = createOptionalDemoActiveRegistry();
|
||||
setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable");
|
||||
resolveRuntimePluginRegistryMock.mockReturnValue(undefined);
|
||||
resolveRuntimePluginRegistryMock.mockReturnValue(activeRegistry);
|
||||
|
||||
const tools = resolvePluginTools(
|
||||
createResolveToolsParams({
|
||||
@@ -416,10 +417,46 @@ describe("resolvePluginTools optional tools", () => {
|
||||
);
|
||||
|
||||
expectResolvedToolNames(tools, ["optional_tool"]);
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["optional-demo"],
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds enabled non-startup tool plugins to the active tool runtime scope", () => {
|
||||
const activeRegistry = createOptionalDemoActiveRegistry();
|
||||
setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable");
|
||||
resolveRuntimePluginRegistryMock.mockReturnValue(activeRegistry);
|
||||
|
||||
resolvePluginTools({
|
||||
context: {
|
||||
...createContext(),
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
allow: ["tavily"],
|
||||
entries: {
|
||||
tavily: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
toolAllowlist: ["optional_tool"],
|
||||
allowGatewaySubagentBinding: true,
|
||||
});
|
||||
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["optional-demo", "tavily"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("loads plugin tools when gateway-bindable tool loads have no active registry", () => {
|
||||
setOptionalDemoRegistry();
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { normalizeToolName } from "../agents/tool-policy.js";
|
||||
import type { AnyAgentTool } from "../agents/tools/common.js";
|
||||
import { applyTestPluginDefaults, normalizePluginsConfig } from "./config-state.js";
|
||||
import { listEnabledInstalledPluginRecords } from "./installed-plugin-index.js";
|
||||
import { resolveRuntimePluginRegistry, type PluginLoadOptions } from "./loader.js";
|
||||
import {
|
||||
getActivePluginRegistry,
|
||||
getActivePluginRegistryKey,
|
||||
getActivePluginRuntimeSubagentMode,
|
||||
} from "./runtime.js";
|
||||
import { loadPluginRegistrySnapshot } from "./plugin-registry-snapshot.js";
|
||||
import { getActivePluginRegistry } from "./runtime.js";
|
||||
import {
|
||||
buildPluginRuntimeLoadOptions,
|
||||
resolvePluginRuntimeLoadContext,
|
||||
@@ -94,18 +92,29 @@ function describeMalformedPluginTool(tool: unknown): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolvePluginToolRegistry(params: {
|
||||
loadOptions: PluginLoadOptions;
|
||||
allowGatewaySubagentBinding?: boolean;
|
||||
}) {
|
||||
if (
|
||||
params.allowGatewaySubagentBinding &&
|
||||
getActivePluginRegistryKey() &&
|
||||
getActivePluginRuntimeSubagentMode() === "gateway-bindable"
|
||||
) {
|
||||
return getActivePluginRegistry() ?? resolveRuntimePluginRegistry(params.loadOptions);
|
||||
function resolvePluginToolRuntimePluginIds(params: {
|
||||
config: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): string[] | undefined {
|
||||
const pluginIds = new Set<string>();
|
||||
const activeRegistry = getActivePluginRegistry();
|
||||
for (const plugin of activeRegistry?.plugins ?? []) {
|
||||
if (plugin.status === undefined || plugin.status === "loaded") {
|
||||
pluginIds.add(plugin.id);
|
||||
}
|
||||
}
|
||||
return resolveRuntimePluginRegistry(params.loadOptions);
|
||||
const index = loadPluginRegistrySnapshot({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
for (const plugin of listEnabledInstalledPluginRecords(index, params.config)) {
|
||||
pluginIds.add(plugin.pluginId);
|
||||
}
|
||||
return pluginIds.size > 0
|
||||
? [...pluginIds].toSorted((left, right) => left.localeCompare(right))
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function resolvePluginTools(params: {
|
||||
@@ -133,16 +142,19 @@ export function resolvePluginTools(params: {
|
||||
const runtimeOptions = params.allowGatewaySubagentBinding
|
||||
? { allowGatewaySubagentBinding: true as const }
|
||||
: undefined;
|
||||
const onlyPluginIds = resolvePluginToolRuntimePluginIds({
|
||||
config: context.config,
|
||||
workspaceDir: context.workspaceDir,
|
||||
env,
|
||||
});
|
||||
const loadOptions = buildPluginRuntimeLoadOptions(context, {
|
||||
installBundledRuntimeDeps: false,
|
||||
activate: false,
|
||||
toolDiscovery: true,
|
||||
...(onlyPluginIds !== undefined ? { onlyPluginIds } : {}),
|
||||
runtimeOptions,
|
||||
});
|
||||
const registry = resolvePluginToolRegistry({
|
||||
loadOptions,
|
||||
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
|
||||
});
|
||||
const registry = resolveRuntimePluginRegistry(loadOptions);
|
||||
if (!registry) {
|
||||
return [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user