diff --git a/src/secrets/runtime-web-tools.shared.ts b/src/secrets/runtime-web-tools.shared.ts index 064353916fc..cbad9aed4b9 100644 --- a/src/secrets/runtime-web-tools.shared.ts +++ b/src/secrets/runtime-web-tools.shared.ts @@ -141,6 +141,7 @@ export type ResolveRuntimeWebProviderSurfaceParams< invalidAutoDetectCode: RuntimeWebWarningCode; sourceConfig: OpenClawConfig; context: ResolverContext; + configuredBundledPluginIdHint?: string; resolveProviders: (params: { configuredBundledPluginId?: string }) => Promise; sortProviders: (providers: TProvider[]) => TProvider[]; readConfiguredCredential: (params: { @@ -162,19 +163,43 @@ export async function resolveRuntimeWebProviderSurface< >( params: ResolveRuntimeWebProviderSurfaceParams, ): Promise> { - const configuredBundledPluginId = resolveManifestContractOwnerPluginId({ - contract: params.contract, - value: params.rawProvider, - origin: "bundled", - config: params.sourceConfig, - env: { ...process.env, ...params.context.env }, - }); - - const allProviders = params.sortProviders( + let configuredBundledPluginId = params.configuredBundledPluginIdHint; + if (!configuredBundledPluginId && params.rawProvider) { + configuredBundledPluginId = resolveManifestContractOwnerPluginId({ + contract: params.contract, + value: params.rawProvider, + origin: "bundled", + config: params.sourceConfig, + env: { ...process.env, ...params.context.env }, + }); + } + let allProviders = params.sortProviders( await params.resolveProviders({ configuredBundledPluginId, }), ); + if ( + params.rawProvider && + params.configuredBundledPluginIdHint && + configuredBundledPluginId && + !allProviders.some((provider) => provider.id === params.rawProvider) + ) { + configuredBundledPluginId = undefined; + } + if (params.rawProvider && !configuredBundledPluginId) { + configuredBundledPluginId = resolveManifestContractOwnerPluginId({ + contract: params.contract, + value: params.rawProvider, + origin: "bundled", + config: params.sourceConfig, + env: { ...process.env, ...params.context.env }, + }); + allProviders = params.sortProviders( + await params.resolveProviders({ + configuredBundledPluginId, + }), + ); + } const hasConfiguredSurface = Boolean(params.toolConfig) || allProviders.some((provider) => { diff --git a/src/secrets/runtime-web-tools.test.ts b/src/secrets/runtime-web-tools.test.ts index 6838fc981e5..955ba896a00 100644 --- a/src/secrets/runtime-web-tools.test.ts +++ b/src/secrets/runtime-web-tools.test.ts @@ -37,6 +37,14 @@ const { const { resolveManifestContractPluginIdsByCompatibilityRuntimePathMock } = vi.hoisted(() => ({ resolveManifestContractPluginIdsByCompatibilityRuntimePathMock: vi.fn(() => ["brave"]), })); +const { resolveManifestContractOwnerPluginIdMock, manifestRegistryActual } = vi.hoisted(() => ({ + resolveManifestContractOwnerPluginIdMock: vi.fn(), + manifestRegistryActual: { + resolveManifestContractOwnerPluginId: undefined as + | typeof import("../plugins/manifest-registry.js").resolveManifestContractOwnerPluginId + | undefined, + }, +})); let secretResolve: typeof import("./resolve.js"); let createResolverContext: typeof import("./runtime-shared.js").createResolverContext; let resolveRuntimeWebTools: typeof import("./runtime-web-tools.js").resolveRuntimeWebTools; @@ -73,8 +81,14 @@ vi.mock("../plugins/manifest-registry.js", async () => { const actual = await vi.importActual( "../plugins/manifest-registry.js", ); + manifestRegistryActual.resolveManifestContractOwnerPluginId = + actual.resolveManifestContractOwnerPluginId; + resolveManifestContractOwnerPluginIdMock.mockImplementation( + actual.resolveManifestContractOwnerPluginId, + ); return { ...actual, + resolveManifestContractOwnerPluginId: resolveManifestContractOwnerPluginIdMock, resolveManifestContractPluginIdsByCompatibilityRuntimePath: resolveManifestContractPluginIdsByCompatibilityRuntimePathMock, }; @@ -297,6 +311,11 @@ describe("runtime web tools resolution", () => { resolveBundledExplicitWebFetchProvidersFromPublicArtifactsMock.mockClear(); resolveBundledWebSearchProvidersFromPublicArtifactsMock.mockClear(); resolveBundledWebFetchProvidersFromPublicArtifactsMock.mockClear(); + resolveManifestContractOwnerPluginIdMock.mockReset(); + resolveManifestContractOwnerPluginIdMock.mockImplementation( + manifestRegistryActual.resolveManifestContractOwnerPluginId!, + ); + resolveManifestContractOwnerPluginIdMock.mockClear(); resolveManifestContractPluginIdsByCompatibilityRuntimePathMock.mockClear(); }); @@ -798,6 +817,7 @@ describe("runtime web tools resolution", () => { expect(resolveBundledExplicitWebSearchProvidersFromPublicArtifactsMock).toHaveBeenCalledWith({ onlyPluginIds: ["google"], }); + expect(resolveManifestContractOwnerPluginIdMock).not.toHaveBeenCalled(); expect(resolveBundledWebSearchProvidersFromPublicArtifactsMock).not.toHaveBeenCalled(); expect(resolvePluginWebSearchProvidersMock).not.toHaveBeenCalled(); }); diff --git a/src/secrets/runtime-web-tools.ts b/src/secrets/runtime-web-tools.ts index c9bda4baa18..341824e7ec3 100644 --- a/src/secrets/runtime-web-tools.ts +++ b/src/secrets/runtime-web-tools.ts @@ -82,6 +82,31 @@ function hasPluginScopedWebToolConfig( }); } +function inferSingleBundledPluginScopedWebToolConfigOwner( + config: OpenClawConfig, + key: "webSearch" | "webFetch", +): string | undefined { + const entries = config.plugins?.entries; + if (!entries) { + return undefined; + } + const matches: string[] = []; + for (const [pluginId, entry] of Object.entries(entries)) { + if (!isRecord(entry) || entry.enabled === false) { + continue; + } + const pluginConfig = isRecord(entry.config) ? entry.config : undefined; + if (!isRecord(pluginConfig?.[key])) { + continue; + } + matches.push(pluginId); + if (matches.length > 1) { + return undefined; + } + } + return matches[0]; +} + function hasCustomWebSearchPluginRisk(config: OpenClawConfig): boolean { const plugins = config.plugins; if (!plugins) { @@ -490,6 +515,10 @@ export async function resolveRuntimeWebTools(params: { }; } const rawProvider = normalizeLowercaseStringOrEmpty(search?.provider); + const configuredBundledWebSearchPluginIdHint = + rawProvider && hasPluginWebSearchConfig && !hasCustomWebSearchPluginRisk(params.sourceConfig) + ? inferSingleBundledPluginScopedWebToolConfigOwner(params.sourceConfig, "webSearch") + : undefined; const searchMetadata: RuntimeWebSearchMetadata = { providerSource: "none", diagnostics: [], @@ -518,6 +547,7 @@ export async function resolveRuntimeWebTools(params: { invalidAutoDetectCode: "WEB_SEARCH_PROVIDER_INVALID_AUTODETECT", sourceConfig: params.sourceConfig, context: params.context, + configuredBundledPluginIdHint: configuredBundledWebSearchPluginIdHint, resolveProviders: ({ configuredBundledPluginId }) => resolveBundledWebSearchProviders({ sourceConfig: params.sourceConfig,