From 2e7635f4f97fd394f2788e24a5d0ec68093b101d Mon Sep 17 00:00:00 2001 From: Shakker Date: Sun, 26 Apr 2026 04:30:22 +0100 Subject: [PATCH] fix: scope web provider ownership to plugin index --- CHANGELOG.md | 1 + src/agents/tools/web-search.ts | 2 +- src/cli/command-secret-gateway.ts | 2 +- src/gateway/model-pricing-cache.ts | 2 +- src/plugins/manifest-registry.ts | 2 +- src/plugins/plugin-registry.test.ts | 24 +++++ src/plugins/plugin-registry.ts | 92 ++++++++++++++++++- .../runtime-web-tools-manifest.runtime.ts | 2 +- 8 files changed, 121 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce7126696a..66f32901f36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Providers/Z.AI: map OpenClaw thinking controls to Z.AI's `thinking` payload and add opt-in preserved thinking replay via `params.preserveThinking`, so GLM 5.x can keep prior `reasoning_content` when requested. Fixes #58680. Thanks @xuanmingguo. +- Plugins/registry: resolve web provider ownership from the installed plugin index instead of broad manifest scans on secret, tool, and pricing paths. Thanks @shakkernerd. - TTS: strip model-emitted TTS directives from streamed block text before channel delivery, including directives split across adjacent blocks, while preserving the accumulated raw reply for final-mode synthesis. Fixes #38937. diff --git a/src/agents/tools/web-search.ts b/src/agents/tools/web-search.ts index 88e238597fb..7eada7a71c3 100644 --- a/src/agents/tools/web-search.ts +++ b/src/agents/tools/web-search.ts @@ -1,5 +1,5 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { resolveManifestContractOwnerPluginId } from "../../plugins/manifest-registry.js"; +import { resolveManifestContractOwnerPluginId } from "../../plugins/plugin-registry.js"; import type { RuntimeWebSearchMetadata } from "../../secrets/runtime-web-tools.types.js"; import { resolveWebSearchDefinition, diff --git a/src/cli/command-secret-gateway.ts b/src/cli/command-secret-gateway.ts index 9e1a7bf3b3d..bb6726f63cc 100644 --- a/src/cli/command-secret-gateway.ts +++ b/src/cli/command-secret-gateway.ts @@ -4,7 +4,7 @@ import { callGateway } from "../gateway/call.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/client-info.js"; import { validateSecretsResolveResult } from "../gateway/protocol/index.js"; import { formatErrorMessage } from "../infra/errors.js"; -import { resolveManifestContractOwnerPluginId } from "../plugins/manifest-registry.js"; +import { resolveManifestContractOwnerPluginId } from "../plugins/plugin-registry.js"; import { analyzeCommandSecretAssignmentsFromSnapshot, type UnresolvedCommandSecretAssignment, diff --git a/src/gateway/model-pricing-cache.ts b/src/gateway/model-pricing-cache.ts index afa42a689e7..b75e6f1cdb2 100644 --- a/src/gateway/model-pricing-cache.ts +++ b/src/gateway/model-pricing-cache.ts @@ -10,7 +10,7 @@ import { import { resolvePluginWebSearchConfig } from "../config/plugin-web-search-config.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; -import { resolveManifestContractPluginIds } from "../plugins/manifest-registry.js"; +import { resolveManifestContractPluginIds } from "../plugins/plugin-registry.js"; import { normalizeProviderModelIdWithPlugin } from "../plugins/provider-runtime.js"; import { normalizeOptionalString, resolvePrimaryStringValue } from "../shared/string-coerce.js"; import { diff --git a/src/plugins/manifest-registry.ts b/src/plugins/manifest-registry.ts index d4e751cf6b4..51e26fba39b 100644 --- a/src/plugins/manifest-registry.ts +++ b/src/plugins/manifest-registry.ts @@ -68,7 +68,7 @@ function resolvePluginSourcePath(sourcePath: string): string { return sourcePath; } -type PluginManifestContractListKey = +export type PluginManifestContractListKey = | "speechProviders" | "externalAuthProviders" | "mediaUnderstandingProviders" diff --git a/src/plugins/plugin-registry.test.ts b/src/plugins/plugin-registry.test.ts index edfaca04c74..3020411627d 100644 --- a/src/plugins/plugin-registry.test.ts +++ b/src/plugins/plugin-registry.test.ts @@ -24,6 +24,9 @@ import { refreshPluginRegistry, resolveChannelOwners, resolveCliBackendOwners, + resolveManifestContractOwnerPluginId, + resolveManifestContractPluginIds, + resolveManifestContractPluginIdsByCompatibilityRuntimePath, resolvePluginContributionOwners, resolveProviderOwners, resolveSetupProviderOwners, @@ -85,6 +88,10 @@ function createCandidate(rootDir: string): PluginCandidate { commandAliases: [{ name: "demo-command" }], contracts: { tools: ["demo-tool"], + webSearchProviders: ["demo-search"], + }, + configContracts: { + compatibilityRuntimePaths: ["tools.web.search.demo-search.apiKey"], }, }), "utf8", @@ -159,6 +166,23 @@ describe("plugin registry facade", () => { }), ).toEqual(["demo"]); expect(resolveSetupProviderOwners({ index, setupProviderId: "demo-setup" })).toEqual(["demo"]); + expect(resolveManifestContractPluginIds({ index, contract: "webSearchProviders" })).toEqual([ + "demo", + ]); + expect( + resolveManifestContractOwnerPluginId({ + index, + contract: "webSearchProviders", + value: "demo-search", + }), + ).toBe("demo"); + expect( + resolveManifestContractPluginIdsByCompatibilityRuntimePath({ + index, + contract: "webSearchProviders", + path: "tools.web.search.demo-search.apiKey", + }), + ).toEqual(["demo"]); }); it("keeps disabled records inspectable while excluding owners by default", () => { diff --git a/src/plugins/plugin-registry.ts b/src/plugins/plugin-registry.ts index bd236b10cbd..207a3e35d96 100644 --- a/src/plugins/plugin-registry.ts +++ b/src/plugins/plugin-registry.ts @@ -24,7 +24,12 @@ import { type RefreshInstalledPluginIndexParams, } from "./installed-plugin-index.js"; import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js"; -import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js"; +import type { + PluginManifestContractListKey, + PluginManifestRecord, + PluginManifestRegistry, +} from "./manifest-registry.js"; +import type { PluginOrigin } from "./plugin-origin.types.js"; export type PluginRegistrySnapshot = InstalledPluginIndex; export type PluginRegistryRecord = InstalledPluginIndexRecord; @@ -107,6 +112,25 @@ export type ResolveSetupProviderOwnersParams = PluginRegistryContributionOptions setupProviderId: string; }; +export type ResolveManifestContractPluginIdsParams = LoadPluginRegistryParams & { + contract: PluginManifestContractListKey; + origin?: PluginOrigin; + onlyPluginIds?: readonly string[]; +}; + +export type ResolveManifestContractOwnerPluginIdParams = LoadPluginRegistryParams & { + contract: PluginManifestContractListKey; + value: string | undefined; + origin?: PluginOrigin; +}; + +export type ResolveManifestContractPluginIdsByCompatibilityRuntimePathParams = + LoadPluginRegistryParams & { + contract: PluginManifestContractListKey; + path: string | undefined; + origin?: PluginOrigin; + }; + function normalizeContributionId(value: string): string { return value.trim(); } @@ -139,6 +163,25 @@ function collectContractKeys(plugin: PluginManifestRecord): readonly string[] { ); } +function listManifestContractValues( + plugin: PluginManifestRecord, + contract: PluginManifestContractListKey, +): readonly string[] { + return plugin.contracts?.[contract] ?? []; +} + +function loadManifestContractRegistry( + params: LoadPluginRegistryParams & { + onlyPluginIds?: readonly string[]; + }, +): PluginManifestRegistry { + return loadPluginManifestRegistryForPluginRegistry({ + ...params, + pluginIds: params.onlyPluginIds, + includeDisabled: true, + }); +} + function listManifestContributionIds( plugin: PluginManifestRecord, contribution: PluginRegistryContributionKey, @@ -440,6 +483,53 @@ export function resolveSetupProviderOwners( }); } +export function resolveManifestContractPluginIds( + params: ResolveManifestContractPluginIdsParams, +): string[] { + return loadManifestContractRegistry(params) + .plugins.filter( + (plugin) => + (!params.origin || plugin.origin === params.origin) && + listManifestContractValues(plugin, params.contract).length > 0, + ) + .map((plugin) => plugin.id) + .toSorted((left, right) => left.localeCompare(right)); +} + +export function resolveManifestContractPluginIdsByCompatibilityRuntimePath( + params: ResolveManifestContractPluginIdsByCompatibilityRuntimePathParams, +): string[] { + const normalizedPath = params.path?.trim(); + if (!normalizedPath) { + return []; + } + return loadManifestContractRegistry(params) + .plugins.filter( + (plugin) => + (!params.origin || plugin.origin === params.origin) && + listManifestContractValues(plugin, params.contract).length > 0 && + (plugin.configContracts?.compatibilityRuntimePaths ?? []).includes(normalizedPath), + ) + .map((plugin) => plugin.id) + .toSorted((left, right) => left.localeCompare(right)); +} + +export function resolveManifestContractOwnerPluginId( + params: ResolveManifestContractOwnerPluginIdParams, +): string | undefined { + const normalizedValue = normalizeContributionId(params.value ?? "").toLowerCase(); + if (!normalizedValue) { + return undefined; + } + return loadManifestContractRegistry(params).plugins.find( + (plugin) => + (!params.origin || plugin.origin === params.origin) && + listManifestContractValues(plugin, params.contract).some( + (candidate) => normalizeContributionId(candidate).toLowerCase() === normalizedValue, + ), + )?.id; +} + export function inspectPluginRegistry( params: LoadInstalledPluginIndexParams & InstalledPluginIndexStoreOptions = {}, ): Promise { diff --git a/src/secrets/runtime-web-tools-manifest.runtime.ts b/src/secrets/runtime-web-tools-manifest.runtime.ts index 4d4a657b0dd..149c82b6737 100644 --- a/src/secrets/runtime-web-tools-manifest.runtime.ts +++ b/src/secrets/runtime-web-tools-manifest.runtime.ts @@ -2,4 +2,4 @@ export { resolveManifestContractOwnerPluginId, resolveManifestContractPluginIds, resolveManifestContractPluginIdsByCompatibilityRuntimePath, -} from "../plugins/manifest-registry.js"; +} from "../plugins/plugin-registry.js";