perf: scope plugin tool discovery to manifest tool owners

This commit is contained in:
Shakker
2026-05-01 21:51:15 +01:00
parent 1de7362679
commit e6825fceaa
5 changed files with 161 additions and 49 deletions

View File

@@ -112,6 +112,7 @@ Docs: https://docs.openclaw.ai
- Voice Call/Twilio: honor TTS directive text and provider voice/model overrides during telephony synthesis, so `[[tts:...]]` tags are not spoken literally and voiceId overrides reach OpenAI/ElevenLabs calls. Fixes #58114. Thanks @legonhilltech-jpg.
- Agents/session-locks: reclaim untracked current-process session locks with matching starttime during acquisition and startup cleanup, so Gateway restarts recover from self-owned orphan `.jsonl.lock` files. Fixes #75805; refs #49603. Thanks @cdznho.
- Agents/subagents: initialize built-in context engines before native `sessions_spawn` resolves spawn preparation, so cliBackend-only cold starts no longer fail with an unregistered `legacy` context engine. Fixes #73095. (#73904) Thanks @brokemac79.
- Agents/tools: scope reply plugin-tool discovery to manifest-declared tool owners and already-active matching tool entries, avoiding broad plugin runtime loading for narrow or core-only tool allowlists. Thanks @shakkernerd.
- Agents/Codex: stop prompting message-tool-only source turns to finish with `NO_REPLY`, so quiet turns are represented by not calling the visible message tool instead of conflicting final-text instructions. Thanks @pashpashpash.
- Gateway/config: report failed backup restores as failed in logs and config observe audit records instead of marking them valid. (#70515) Thanks @davidangularme.
- Compaction: use the active session model fallback chain for implicit summarization failures without persisting fallback model selection, so Azure content-filter 400s can recover. Fixes #64960. (#74470) Thanks @jalehman and @OpenCodeEngineer.

View File

@@ -5,24 +5,22 @@ import {
withBundledPluginEnablementCompat,
withBundledPluginVitestCompat,
} from "./bundled-compat.js";
import {
resolvePluginRegistryLoadCacheKey,
resolveRuntimePluginRegistry,
type PluginLoadOptions,
} from "./loader.js";
import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js";
import {
resolveConfigScopedRuntimeCacheValue,
type ConfigScopedRuntimeCache,
} from "./plugin-cache-primitives.js";
import {
resolvePluginRegistryLoadCacheKey,
resolveRuntimePluginRegistry,
type PluginLoadOptions,
} from "./loader.js";
import {
hasManifestContractValue,
isManifestPluginAvailableForControlPlane,
loadManifestContractSnapshot,
listAvailableManifestContractValues,
} from "./manifest-contract-eligibility.js";
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.types.js";
import { loadPluginRegistrySnapshot } from "./plugin-registry.js";
import type { PluginRegistry } from "./registry-types.js";
type CapabilityProviderRegistryKey =
@@ -89,29 +87,10 @@ export function loadCapabilityManifestSnapshot(params: {
cfg?: OpenClawConfig;
workspaceDir?: string;
}): Pick<PluginMetadataSnapshot, "index" | "plugins"> {
const current = getCurrentPluginMetadataSnapshot({
return loadManifestContractSnapshot({
config: params.cfg,
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
});
if (current) {
return current;
}
const env = process.env;
const index = loadPluginRegistrySnapshot({
config: params.cfg,
env,
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
});
return {
index,
plugins: loadPluginManifestRegistryForInstalledIndex({
index,
config: params.cfg,
env,
includeDisabled: true,
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
}).plugins,
};
}
function resolveCapabilityPluginIds(params: {

View File

@@ -1,7 +1,10 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js";
import { isInstalledPluginEnabled } from "./installed-plugin-index.js";
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
import type { PluginManifestContractListKey, PluginManifestRecord } from "./manifest-registry.js";
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.types.js";
import { loadPluginRegistrySnapshot } from "./plugin-registry.js";
export function isManifestPluginAvailableForControlPlane(params: {
snapshot: Pick<PluginMetadataSnapshot, "index">;
@@ -57,3 +60,33 @@ export function listAvailableManifestContractValues(params: {
}
return [...values].toSorted((left, right) => left.localeCompare(right));
}
export function loadManifestContractSnapshot(params: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
}): Pick<PluginMetadataSnapshot, "index" | "plugins"> {
const current = getCurrentPluginMetadataSnapshot({
config: params.config,
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
});
if (current) {
return current;
}
const env = params.env ?? process.env;
const index = loadPluginRegistrySnapshot({
config: params.config,
env,
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
});
return {
index,
plugins: loadPluginManifestRegistryForInstalledIndex({
index,
config: params.config,
env,
includeDisabled: true,
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
}).plugins,
};
}

View File

@@ -306,7 +306,7 @@ describe("resolvePluginTools optional tools", () => {
},
{
name: "allows optional tools via plugin-scoped allowlist entries",
toolAllowlist: ["group:plugins"],
toolAllowlist: ["optional_tool", "tavily"],
},
] as const)("$name", ({ toolAllowlist }) => {
setOptionalDemoRegistry();
@@ -563,6 +563,41 @@ describe("resolvePluginTools optional tools", () => {
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("does not widen active registry reuse to non-matching plugin tool owners", () => {
const heavyFactory = vi.fn(() => makeTool("heavy_tool"));
const activeRegistry = {
plugins: [
{ id: "optional-demo", status: "loaded" },
{ id: "heavy-startup", status: "loaded" },
],
tools: [
createOptionalDemoEntry(),
{
pluginId: "heavy-startup",
optional: false,
source: "/tmp/heavy-startup.js",
names: ["heavy_tool"],
factory: heavyFactory,
},
],
diagnostics: [],
};
setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable");
resolveRuntimePluginRegistryMock.mockReturnValue(undefined);
const tools = resolvePluginTools(
createResolveToolsParams({
toolAllowlist: ["optional_tool"],
allowGatewaySubagentBinding: true,
}),
);
expectResolvedToolNames(tools, ["optional_tool"]);
expect(heavyFactory).not.toHaveBeenCalled();
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
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");
@@ -581,7 +616,7 @@ describe("resolvePluginTools optional tools", () => {
},
},
} as never,
toolAllowlist: ["optional_tool"],
toolAllowlist: ["optional_tool", "tavily"],
allowGatewaySubagentBinding: true,
});

View File

@@ -2,9 +2,11 @@ import { normalizeToolName } from "../agents/tool-policy.js";
import type { AnyAgentTool } from "../agents/tools/common.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { applyTestPluginDefaults, normalizePluginsConfig } from "./config-state.js";
import { listEnabledInstalledPluginRecords } from "./installed-plugin-index.js";
import { resolveRuntimePluginRegistry, type PluginLoadOptions } from "./loader.js";
import { loadPluginRegistrySnapshot } from "./plugin-registry-snapshot.js";
import {
isManifestPluginAvailableForControlPlane,
loadManifestContractSnapshot,
} from "./manifest-contract-eligibility.js";
import {
getActivePluginChannelRegistry,
getActivePluginRegistry,
@@ -199,13 +201,54 @@ function describeMalformedPluginTool(tool: unknown): string | undefined {
return undefined;
}
function addLoadedPluginIdsFromRegistry(
function pluginToolNamesMatchAllowlist(params: {
names: readonly string[];
pluginId: string;
optional: boolean;
allowlist: Set<string>;
}): boolean {
if (params.allowlist.size === 0) {
return !params.optional;
}
return isOptionalToolEntryPotentiallyAllowed(params);
}
function manifestToolContractMatchesAllowlist(params: {
toolNames: readonly string[];
pluginId: string;
allowlist: Set<string>;
}): boolean {
if (params.toolNames.length === 0) {
return false;
}
if (params.allowlist.size === 0) {
return true;
}
if (params.allowlist.has("*") || params.allowlist.has("group:plugins")) {
return true;
}
const pluginKey = normalizeToolName(params.pluginId);
if (params.allowlist.has(pluginKey)) {
return true;
}
return params.toolNames.some((name) => params.allowlist.has(normalizeToolName(name)));
}
function addToolPluginIdsFromRegistry(
registry: ReturnType<typeof getActivePluginRegistry>,
pluginIds: Set<string>,
allowlist: Set<string>,
): void {
for (const plugin of registry?.plugins ?? []) {
if (plugin.status === undefined || plugin.status === "loaded") {
pluginIds.add(plugin.id);
for (const entry of registry?.tools ?? []) {
if (
pluginToolNamesMatchAllowlist({
names: entry.names,
pluginId: entry.pluginId,
optional: entry.optional,
allowlist,
})
) {
pluginIds.add(entry.pluginId);
}
}
}
@@ -214,21 +257,38 @@ function resolvePluginToolRuntimePluginIds(params: {
config: PluginLoadOptions["config"];
workspaceDir?: string;
env: NodeJS.ProcessEnv;
}): string[] | undefined {
toolAllowlist?: string[];
}): string[] {
const pluginIds = new Set<string>();
addLoadedPluginIdsFromRegistry(getActivePluginChannelRegistry(), pluginIds);
addLoadedPluginIdsFromRegistry(getActivePluginRegistry(), pluginIds);
const index = loadPluginRegistrySnapshot({
const allowlist = normalizeAllowlist(params.toolAllowlist);
addToolPluginIdsFromRegistry(getActivePluginChannelRegistry(), pluginIds, allowlist);
addToolPluginIdsFromRegistry(getActivePluginRegistry(), pluginIds, allowlist);
const snapshot = loadManifestContractSnapshot({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
});
for (const plugin of listEnabledInstalledPluginRecords(index, params.config)) {
pluginIds.add(plugin.pluginId);
for (const plugin of snapshot.plugins) {
if (
!isManifestPluginAvailableForControlPlane({
snapshot,
plugin,
config: params.config,
})
) {
continue;
}
if (
manifestToolContractMatchesAllowlist({
toolNames: plugin.contracts?.tools ?? [],
pluginId: plugin.id,
allowlist,
})
) {
pluginIds.add(plugin.id);
}
}
return pluginIds.size > 0
? [...pluginIds].toSorted((left, right) => left.localeCompare(right))
: undefined;
return [...pluginIds].toSorted((left, right) => left.localeCompare(right));
}
function registryContainsPluginIds(
@@ -238,8 +298,11 @@ function registryContainsPluginIds(
if (!registry || pluginIds === undefined) {
return false;
}
const loadedPluginIds = new Set<string>();
addLoadedPluginIdsFromRegistry(registry, loadedPluginIds);
const loadedPluginIds = new Set(
(registry.plugins ?? [])
.filter((plugin) => plugin.status === undefined || plugin.status === "loaded")
.map((plugin) => plugin.id),
);
return pluginIds.every((pluginId) => loadedPluginIds.has(pluginId));
}
@@ -291,6 +354,7 @@ export function resolvePluginTools(params: {
config: context.config,
workspaceDir: context.workspaceDir,
env,
toolAllowlist: params.toolAllowlist,
});
const loadOptions = buildPluginRuntimeLoadOptions(context, {
activate: false,
@@ -335,10 +399,10 @@ export function resolvePluginTools(params: {
}
const declaredNames = entry.names ?? [];
if (
entry.optional &&
!isOptionalToolEntryPotentiallyAllowed({
!pluginToolNamesMatchAllowlist({
names: declaredNames,
pluginId: entry.pluginId,
optional: entry.optional,
allowlist,
})
) {