From 74059aaa29a2d5237d907a114a125f31366b9e31 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 25 Apr 2026 13:54:00 -0700 Subject: [PATCH] fix(secrets): honor plugin install ledger for web fetch discovery --- src/secrets/runtime-web-tools.test.ts | 32 +++++++++++++++++ src/secrets/runtime-web-tools.ts | 50 ++++++++++++++++++++------- 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/src/secrets/runtime-web-tools.test.ts b/src/secrets/runtime-web-tools.test.ts index 713ce8debc1..427998702f5 100644 --- a/src/secrets/runtime-web-tools.test.ts +++ b/src/secrets/runtime-web-tools.test.ts @@ -1141,6 +1141,38 @@ describe("runtime web tools resolution", () => { expect(resolvePluginWebFetchProvidersMock).not.toHaveBeenCalled(); }); + it("uses runtime web fetch discovery when the managed plugin install ledger is populated", async () => { + loadPluginInstallRecordsSyncMock.mockReturnValue({ + "external-fetch": { + source: "npm", + spec: "@openclaw/external-fetch", + }, + }); + + const { metadata } = await runRuntimeWebTools({ + config: asConfig({ + tools: { + web: { + fetch: { + enabled: true, + }, + }, + }, + }), + env: { + FIRECRAWL_API_KEY: "firecrawl-key", // pragma: allowlist secret + }, + }); + + expect(metadata.fetch.selectedProvider).toBe("firecrawl"); + expect(resolveBundledWebFetchProvidersFromPublicArtifactsMock).not.toHaveBeenCalled(); + expect(resolvePluginWebFetchProvidersMock).toHaveBeenCalledWith( + expect.objectContaining({ + bundledAllowlistCompat: true, + }), + ); + }); + it("uses env fallback for unresolved web fetch provider SecretRef when active", async () => { const { metadata, resolvedConfig, context } = await runRuntimeWebTools({ config: asConfig({ diff --git a/src/secrets/runtime-web-tools.ts b/src/secrets/runtime-web-tools.ts index f984c784af8..227a149f7f8 100644 --- a/src/secrets/runtime-web-tools.ts +++ b/src/secrets/runtime-web-tools.ts @@ -121,7 +121,10 @@ function inferExactBundledPluginScopedWebToolConfigOwner(params: { return isRecord(pluginConfig?.[params.key]) ? params.pluginId : undefined; } -async function hasCustomWebSearchPluginRisk(params: { +type WebProviderContract = "webSearchProviders" | "webFetchProviders"; + +async function hasCustomWebProviderPluginRisk(params: { + contract: WebProviderContract; config: OpenClawConfig; env: NodeJS.ProcessEnv; }): Promise { @@ -143,7 +146,7 @@ async function hasCustomWebSearchPluginRisk(params: { const { resolveManifestContractPluginIds } = await loadRuntimeWebToolsManifest(); const bundledPluginIds = new Set( resolveManifestContractPluginIds({ - contract: "webSearchProviders", + contract: params.contract, origin: "bundled", config: params.config, env: params.env, @@ -377,6 +380,7 @@ async function resolveBundledWebFetchProviders(params: { sourceConfig: OpenClawConfig; context: ResolverContext; configuredBundledPluginId?: string; + hasCustomWebFetchPluginRisk: boolean; }): Promise { const env = { ...process.env, ...params.context.env }; if (params.configuredBundledPluginId) { @@ -395,15 +399,24 @@ async function resolveBundledWebFetchProviders(params: { origin: "bundled", }); } - const { resolveBundledWebFetchProvidersFromPublicArtifacts } = - await loadRuntimeWebToolsPublicArtifacts(); - const bundled = resolveBundledWebFetchProvidersFromPublicArtifacts({ - config: params.sourceConfig, - env, - bundledAllowlistCompat: true, - }); - if (bundled && bundled.length > 0) { - return bundled; + if (!params.hasCustomWebFetchPluginRisk) { + const { resolveBundledWebFetchProvidersFromPublicArtifacts } = + await loadRuntimeWebToolsPublicArtifacts(); + const bundled = resolveBundledWebFetchProvidersFromPublicArtifacts({ + config: params.sourceConfig, + env, + bundledAllowlistCompat: true, + }); + if (bundled && bundled.length > 0) { + return bundled; + } + const { resolvePluginWebFetchProviders } = await loadRuntimeWebToolsFallbackProviders(); + return resolvePluginWebFetchProviders({ + config: params.sourceConfig, + env, + bundledAllowlistCompat: true, + origin: "bundled", + }); } const { resolvePluginWebFetchProviders } = await loadRuntimeWebToolsFallbackProviders(); return resolvePluginWebFetchProviders({ @@ -481,12 +494,22 @@ export async function resolveRuntimeWebTools(params: { const resolvedWeb = isRecord(resolvedTools?.web) ? resolvedTools.web : undefined; let hasCustomWebSearchRisk: Promise | undefined; const getHasCustomWebSearchRisk = (): Promise => { - hasCustomWebSearchRisk ??= hasCustomWebSearchPluginRisk({ + hasCustomWebSearchRisk ??= hasCustomWebProviderPluginRisk({ + contract: "webSearchProviders", config: params.sourceConfig, env, }); return hasCustomWebSearchRisk; }; + let hasCustomWebFetchRisk: Promise | undefined; + const getHasCustomWebFetchRisk = (): Promise => { + hasCustomWebFetchRisk ??= hasCustomWebProviderPluginRisk({ + contract: "webFetchProviders", + config: params.sourceConfig, + env, + }); + return hasCustomWebFetchRisk; + }; const legacyXSearchSource = isRecord(sourceWeb?.x_search) ? sourceWeb.x_search : undefined; const legacyXSearchResolved = isRecord(resolvedWeb?.x_search) ? resolvedWeb.x_search : undefined; @@ -670,11 +693,12 @@ export async function resolveRuntimeWebTools(params: { invalidAutoDetectCode: "WEB_FETCH_PROVIDER_INVALID_AUTODETECT", sourceConfig: params.sourceConfig, context: params.context, - resolveProviders: ({ configuredBundledPluginId }) => + resolveProviders: async ({ configuredBundledPluginId }) => resolveBundledWebFetchProviders({ sourceConfig: params.sourceConfig, context: params.context, configuredBundledPluginId, + hasCustomWebFetchPluginRisk: await getHasCustomWebFetchRisk(), }), sortProviders: sortWebFetchProvidersForAutoDetect, readConfiguredCredential: ({ provider, config, toolConfig }) =>