fix(runtime): resolve web search SecretRefs from snapshots (#72563)

This commit is contained in:
Josh Avant
2026-04-27 00:35:21 -05:00
committed by GitHub
parent 332cdd7aca
commit 510718bedf
4 changed files with 95 additions and 19 deletions

View File

@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
- Plugins/install: stage bundled plugin runtime dependencies before Gateway startup and drain update restarts while preserving per-plugin isolation when pre-stage scan or install fails. Thanks @codex.
- CLI/startup: read generated startup metadata from the bundled `dist` layout before falling back to live help rendering, so root/browser help and channel-option bootstrap stay on the fast path. Thanks @vincentkoc.
- CLI/help: treat positional `help` invocations like `openclaw channels help` as help paths for startup gating, avoiding model/auth warmup while preserving positional arguments such as `openclaw docs help`. Thanks @gumadeiras.
- Web search: route plugin-scoped web_search SecretRefs through the active runtime config snapshot so provider execution receives resolved credentials across app/runtime paths, including `plugins.entries.brave.config.webSearch.apiKey`. Fixes #68690. Thanks @VACInc.
- Matrix/E2EE: stabilize recovery and broken-device QA flows while avoiding Matrix device-cleanup sync races that could leave shutdown-time crypto work running. Thanks @gumadeiras.
- Cron: treat isolated run-level agent failures as job errors even when no reply payload is produced, synthesizing a safe error payload so model/provider failures increment error counters and trigger failure notifications instead of clearing as successful. Fixes #43604; carries forward #43631. Thanks @SPFAdvisors.
- Cron: preserve exact `NO_REPLY` tool results from isolated jobs with empty final assistant turns as quiet successes instead of surfacing incomplete-turn errors. Fixes #68452; carries forward #68453. Thanks @anyech.

View File

@@ -185,7 +185,8 @@ error prompting you to configure one).
<Note>
All provider key fields support SecretRef objects. Plugin-scoped SecretRefs
under `plugins.entries.<plugin>.config.webSearch.apiKey` are resolved for the
bundled Exa, Firecrawl, Gemini, Grok, Kimi, Perplexity, and Tavily providers
bundled API-backed web search providers, including Brave, Exa, Firecrawl,
Gemini, Grok, Kimi, MiniMax, Perplexity, and Tavily,
whether the provider is picked explicitly via `tools.web.search.provider` or
selected through auto-detect. In auto-detect mode, OpenClaw resolves only the
selected provider key -- non-selected SecretRefs stay inactive, so you can

View File

@@ -155,6 +155,60 @@ describe("web search runtime", () => {
});
});
it("uses the active resolved runtime config for matching source config callers", async () => {
const provider = createCustomSearchProvider({
createTool: ({ config }) => ({
description: "custom",
parameters: {},
execute: async (args) => ({
...args,
apiKey: getCustomSearchApiKey(config),
}),
}),
});
resolveRuntimeWebSearchProvidersMock.mockReturnValue([provider]);
resolvePluginWebSearchProvidersMock.mockReturnValue([provider]);
const sourceConfig = createCustomSearchConfig({
source: "exec",
provider: "mockexec",
id: "custom-search/api-key",
});
const resolvedConfig = createCustomSearchConfig("resolved-custom-key");
activateSecretsRuntimeSnapshot({
sourceConfig,
config: resolvedConfig,
authStores: [],
warnings: [],
webTools: {
search: {
providerSource: "auto-detect",
selectedProvider: "custom",
diagnostics: [],
},
fetch: {
providerSource: "none",
diagnostics: [],
},
diagnostics: [],
},
});
await expect(
runWebSearch({
config: structuredClone(sourceConfig),
args: { query: "runtime-source" },
}),
).resolves.toEqual({
provider: "custom",
result: {
query: "runtime-source",
apiKey: "resolved-custom-key",
},
});
});
it("treats non-env SecretRefs as configured credentials for provider auto-detect", async () => {
const provider = createCustomSearchProvider();
resolveRuntimeWebSearchProvidersMock.mockReturnValue([provider]);

View File

@@ -1,3 +1,8 @@
import {
getRuntimeConfigSnapshot,
getRuntimeConfigSourceSnapshot,
selectApplicableRuntimeConfig,
} from "../config/runtime-snapshot.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { logVerbose } from "../globals.js";
import type {
@@ -41,6 +46,14 @@ function resolveSearchConfig(cfg?: OpenClawConfig): WebSearchConfig {
return resolveWebProviderConfig(cfg, "search") as NonNullable<WebSearchConfig> | undefined;
}
function resolveWebSearchRuntimeConfig(config?: OpenClawConfig): OpenClawConfig | undefined {
return selectApplicableRuntimeConfig({
inputConfig: config,
runtimeConfig: getRuntimeConfigSnapshot(),
runtimeSourceConfig: getRuntimeConfigSourceSnapshot(),
});
}
export function resolveWebSearchEnabled(params: {
search?: WebSearchConfig;
sandboxed?: boolean;
@@ -91,14 +104,16 @@ export function isWebSearchProviderConfigured(params: {
>;
config?: OpenClawConfig;
}): boolean {
return hasEntryCredential(params.provider, params.config, resolveSearchConfig(params.config));
const config = resolveWebSearchRuntimeConfig(params.config);
return hasEntryCredential(params.provider, config, resolveSearchConfig(config));
}
export function listWebSearchProviders(params?: {
config?: OpenClawConfig;
}): PluginWebSearchProviderEntry[] {
const config = resolveWebSearchRuntimeConfig(params?.config);
return resolveRuntimeWebSearchProviders({
config: params?.config,
config,
bundledAllowlistCompat: true,
});
}
@@ -106,8 +121,9 @@ export function listWebSearchProviders(params?: {
export function listConfiguredWebSearchProviders(params?: {
config?: OpenClawConfig;
}): PluginWebSearchProviderEntry[] {
const config = resolveWebSearchRuntimeConfig(params?.config);
return resolvePluginWebSearchProviders({
config: params?.config,
config,
bundledAllowlistCompat: true,
});
}
@@ -117,18 +133,18 @@ export function resolveWebSearchProviderId(params: {
config?: OpenClawConfig;
providers?: PluginWebSearchProviderEntry[];
}): string {
const config = resolveWebSearchRuntimeConfig(params.config);
const search = params.search ?? resolveSearchConfig(config);
const providers = sortWebSearchProvidersForAutoDetect(
params.providers ??
resolvePluginWebSearchProviders({
config: params.config,
config,
bundledAllowlistCompat: true,
origin: "bundled",
}),
);
const raw =
params.search && "provider" in params.search
? normalizeLowercaseStringOrEmpty(params.search.provider)
: "";
search && "provider" in search ? normalizeLowercaseStringOrEmpty(search.provider) : "";
if (raw) {
const explicit = providers.find((provider) => provider.id === raw);
@@ -144,7 +160,7 @@ export function resolveWebSearchProviderId(params: {
keylessFallbackProviderId ||= provider.id;
continue;
}
if (!hasEntryCredential(provider, params.config, params.search)) {
if (!hasEntryCredential(provider, config, search)) {
continue;
}
logVerbose(
@@ -166,22 +182,23 @@ export function resolveWebSearchProviderId(params: {
export function resolveWebSearchDefinition(
options?: ResolveWebSearchDefinitionParams,
): { provider: PluginWebSearchProviderEntry; definition: WebSearchProviderToolDefinition } | null {
const search = resolveSearchConfig(options?.config);
const config = resolveWebSearchRuntimeConfig(options?.config);
const search = resolveSearchConfig(config);
const runtimeWebSearch = options?.runtimeWebSearch ?? getActiveRuntimeWebToolsMetadata()?.search;
const providers = sortWebSearchProvidersForAutoDetect(
options?.preferRuntimeProviders
? resolveRuntimeWebSearchProviders({
config: options?.config,
config,
bundledAllowlistCompat: true,
})
: resolvePluginWebSearchProviders({
config: options?.config,
config,
bundledAllowlistCompat: true,
origin: "bundled",
}),
);
return resolveWebProviderDefinition({
config: options?.config,
config,
toolConfig: search as Record<string, unknown> | undefined,
runtimeMetadata: runtimeWebSearch,
sandboxed: options?.sandboxed,
@@ -216,7 +233,8 @@ export function resolveWebSearchDefinition(
function resolveWebSearchCandidates(
options?: ResolveWebSearchDefinitionParams,
): PluginWebSearchProviderEntry[] {
const search = resolveSearchConfig(options?.config);
const config = resolveWebSearchRuntimeConfig(options?.config);
const search = resolveSearchConfig(config);
const runtimeWebSearch = options?.runtimeWebSearch ?? getActiveRuntimeWebToolsMetadata()?.search;
if (!resolveWebSearchEnabled({ search, sandboxed: options?.sandboxed })) {
return [];
@@ -225,11 +243,11 @@ function resolveWebSearchCandidates(
const providers = sortWebSearchProvidersForAutoDetect(
options?.preferRuntimeProviders
? resolveRuntimeWebSearchProviders({
config: options?.config,
config,
bundledAllowlistCompat: true,
})
: resolvePluginWebSearchProviders({
config: options?.config,
config,
bundledAllowlistCompat: true,
origin: "bundled",
}),
@@ -242,7 +260,7 @@ function resolveWebSearchCandidates(
options?.providerId,
runtimeWebSearch?.selectedProvider,
runtimeWebSearch?.providerConfigured,
resolveWebSearchProviderId({ config: options?.config, search, providers }),
resolveWebSearchProviderId({ config, search, providers }),
].filter(
(value, index, array): value is string => Boolean(value) && array.indexOf(value) === index,
);
@@ -294,10 +312,12 @@ function hasExplicitWebSearchSelection(params: {
}
export async function runWebSearch(params: RunWebSearchParams): Promise<RunWebSearchResult> {
const search = resolveSearchConfig(params.config);
const config = resolveWebSearchRuntimeConfig(params.config);
const search = resolveSearchConfig(config);
const runtimeWebSearch = params.runtimeWebSearch ?? getActiveRuntimeWebToolsMetadata()?.search;
const candidates = resolveWebSearchCandidates({
...params,
config,
runtimeWebSearch,
preferRuntimeProviders: params.preferRuntimeProviders ?? true,
});
@@ -316,7 +336,7 @@ export async function runWebSearch(params: RunWebSearchParams): Promise<RunWebSe
for (const candidate of candidates) {
try {
const definition = candidate.createTool({
config: params.config,
config,
searchConfig: search as Record<string, unknown> | undefined,
runtimeMetadata: runtimeWebSearch,
});