diff --git a/CHANGELOG.md b/CHANGELOG.md index 42959f47455..b1d1264e160 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/plugins/capability-provider-runtime.ts b/src/plugins/capability-provider-runtime.ts index b7fe4ef0074..204bf4ea6a9 100644 --- a/src/plugins/capability-provider-runtime.ts +++ b/src/plugins/capability-provider-runtime.ts @@ -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 { - 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: { diff --git a/src/plugins/manifest-contract-eligibility.ts b/src/plugins/manifest-contract-eligibility.ts index c94d94bbe44..a32f63f82be 100644 --- a/src/plugins/manifest-contract-eligibility.ts +++ b/src/plugins/manifest-contract-eligibility.ts @@ -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; @@ -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 { + 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, + }; +} diff --git a/src/plugins/tools.optional.test.ts b/src/plugins/tools.optional.test.ts index 5e3eaf8a17d..2e38bf4d647 100644 --- a/src/plugins/tools.optional.test.ts +++ b/src/plugins/tools.optional.test.ts @@ -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, }); diff --git a/src/plugins/tools.ts b/src/plugins/tools.ts index 70cfeadf991..eeb4cda97fb 100644 --- a/src/plugins/tools.ts +++ b/src/plugins/tools.ts @@ -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; +}): boolean { + if (params.allowlist.size === 0) { + return !params.optional; + } + return isOptionalToolEntryPotentiallyAllowed(params); +} + +function manifestToolContractMatchesAllowlist(params: { + toolNames: readonly string[]; + pluginId: string; + allowlist: Set; +}): 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, pluginIds: Set, + allowlist: Set, ): 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(); - 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(); - 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, }) ) {