mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
perf: scope plugin tool discovery to manifest tool owners
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user