fix(secrets): honor plugin install ledger for web fetch discovery

This commit is contained in:
Vincent Koc
2026-04-25 13:54:00 -07:00
parent 9e9e024188
commit 74059aaa29
2 changed files with 69 additions and 13 deletions

View File

@@ -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({

View File

@@ -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<boolean> {
@@ -143,7 +146,7 @@ async function hasCustomWebSearchPluginRisk(params: {
const { resolveManifestContractPluginIds } = await loadRuntimeWebToolsManifest();
const bundledPluginIds = new Set<string>(
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<PluginWebFetchProviderEntry[]> {
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<boolean> | undefined;
const getHasCustomWebSearchRisk = (): Promise<boolean> => {
hasCustomWebSearchRisk ??= hasCustomWebSearchPluginRisk({
hasCustomWebSearchRisk ??= hasCustomWebProviderPluginRisk({
contract: "webSearchProviders",
config: params.sourceConfig,
env,
});
return hasCustomWebSearchRisk;
};
let hasCustomWebFetchRisk: Promise<boolean> | undefined;
const getHasCustomWebFetchRisk = (): Promise<boolean> => {
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 }) =>