mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:10:51 +00:00
fix(config): validate web search providers
This commit is contained in:
@@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Cron: keep implicit/default isolated cron announce deliveries out of the main session awareness queue, so isolated jobs do not accumulate in the main conversation. Fixes #61426. Thanks @Lihannon.
|
||||
- Subagents: avoid duplicate parent-visible replies when a parent uses `sessions_send` on its own persistent native subagent session, while preserving announce delivery for async sends. Fixes #73550. Thanks @sylviazhang2006-design.
|
||||
- Web search/Brave: add opt-in `brave.http` diagnostics for Brave request URLs/query params, response status/timing, and cache hit/miss/write events without logging API keys or response bodies. Fixes #55196. Thanks @mecampbellsoup.
|
||||
- Web search/config: validate explicit `tools.web.search.provider` values against bundled and installed plugin manifests, while warning for stale third-party plugin config. Fixes #53092. Thanks @TinyTb.
|
||||
- Agents/sandbox: preserve existing workspace file modes when sandbox edits atomically replace files, so 0644 files do not collapse to 0600 after Write/Edit/apply_patch. Fixes #44077. Thanks @patosullivan.
|
||||
- Agents/models: keep legacy CLI runtime model refs such as `claude-cli/*` in the configured allowlist after canonical runtime migration, so cron `payload.model` overrides keep working. Fixes #75753. Thanks @RyanSandoval.
|
||||
- Codex/app-server: restart the shared Codex app-server client once when it closes during startup thread resume, preserving the existing thread binding instead of retrying `thread/start` on a closed client. Thanks @vincentkoc.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
0020a49cdf07b79fddfed9584f10323ef77e3ca5c7f907d441c1f2e3f45932c9 config-baseline.json
|
||||
8193fd771c9fa50e77c71ff69d41015e6dc02d140182ceea3baaa17713f04b18 config-baseline.core.json
|
||||
3545cf963a093d50b69904f859544088212e6522905b72710eb7818caa154b89 config-baseline.json
|
||||
2d132b4c2e3b0e0f2524fc1cc889d3be658ad0e40c970b2d367bf27348883658 config-baseline.core.json
|
||||
f42329d45c095881bd226bdb192c235980658fd250606d0c0badc2b12f12f5d3 config-baseline.channel.json
|
||||
af71b84b2411d8ccabcc6e09de0ee41f8212ff9869a6677698b6e7e3afdfaa47 config-baseline.plugin.json
|
||||
38b16427911ba4ff19240097e5002fb892178fb3cefdc9c50fd98ad2044c02bf config-baseline.plugin.json
|
||||
|
||||
@@ -218,6 +218,14 @@ Provider-specific config (API keys, base URLs, modes) lives under
|
||||
fallbacks after its dedicated web-search config and `GEMINI_API_KEY`. See the
|
||||
provider pages for examples.
|
||||
|
||||
`tools.web.search.provider` is validated against the web-search provider ids
|
||||
declared by bundled and installed plugin manifests. A typo such as `"brvae"`
|
||||
fails config validation instead of silently falling back to auto-detection. If a
|
||||
configured provider only has stale plugin evidence, such as a leftover
|
||||
`plugins.entries.<plugin>` block after uninstalling a third-party plugin,
|
||||
OpenClaw keeps startup resilient and reports a warning so you can reinstall the
|
||||
plugin or run `openclaw doctor --fix` to clean up the stale config.
|
||||
|
||||
`web_fetch` fallback provider selection is separate:
|
||||
|
||||
- choose it with `tools.web.fetch.provider`
|
||||
|
||||
@@ -185,32 +185,42 @@ vi.mock("../plugins/manifest-registry.js", () => {
|
||||
schemaCacheKey: "test:brave",
|
||||
configSchema: buildSchema(),
|
||||
},
|
||||
...[
|
||||
"firecrawl",
|
||||
"google",
|
||||
"minimax",
|
||||
"moonshot",
|
||||
"perplexity",
|
||||
"searxng",
|
||||
"tavily",
|
||||
"xai",
|
||||
].map((id) => ({
|
||||
id,
|
||||
origin: "bundled",
|
||||
...mockWebSearchProviders
|
||||
.filter((provider) => provider.pluginId !== "brave")
|
||||
.map((provider) => ({
|
||||
id: provider.pluginId,
|
||||
origin: "bundled",
|
||||
channels: [],
|
||||
providers: [],
|
||||
contracts: {
|
||||
webSearchProviders: [provider.id],
|
||||
},
|
||||
cliBackends: [],
|
||||
skills: [],
|
||||
hooks: [],
|
||||
rootDir: `/tmp/plugins/${provider.pluginId}`,
|
||||
source: "test",
|
||||
manifestPath: `/tmp/plugins/${provider.pluginId}/openclaw.plugin.json`,
|
||||
schemaCacheKey: `test:${provider.pluginId}`,
|
||||
configSchema: buildSchema(),
|
||||
})),
|
||||
{
|
||||
id: "acme-search",
|
||||
origin: "installed",
|
||||
channels: [],
|
||||
providers: [],
|
||||
contracts: {
|
||||
webSearchProviders: [id],
|
||||
webSearchProviders: ["acme-search"],
|
||||
},
|
||||
cliBackends: [],
|
||||
skills: [],
|
||||
hooks: [],
|
||||
rootDir: `/tmp/plugins/${id}`,
|
||||
rootDir: "/tmp/plugins/acme-search",
|
||||
source: "test",
|
||||
manifestPath: `/tmp/plugins/${id}/openclaw.plugin.json`,
|
||||
schemaCacheKey: `test:${id}`,
|
||||
manifestPath: "/tmp/plugins/acme-search/openclaw.plugin.json",
|
||||
schemaCacheKey: "test:acme-search",
|
||||
configSchema: buildSchema(),
|
||||
})),
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
}),
|
||||
@@ -413,6 +423,74 @@ describe("web search provider config", () => {
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts provider ids registered by installed plugin manifests", () => {
|
||||
const res = validateConfigObjectWithPlugins(
|
||||
buildWebSearchProviderConfig({
|
||||
provider: "acme-search",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects unknown provider ids without plugin evidence", () => {
|
||||
const res = validateConfigObjectWithPlugins({
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "brvae",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.ok).toBe(false);
|
||||
if (res.ok) {
|
||||
return;
|
||||
}
|
||||
expect(res.issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
path: "tools.web.search.provider",
|
||||
message: "unknown web_search provider: brvae",
|
||||
allowedValues: expect.arrayContaining(["acme-search", "brave", "gemini"]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("warns for unknown provider ids when stale plugin config is present", () => {
|
||||
const res = validateConfigObjectWithPlugins({
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "missing-third-party",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"missing-third-party": {
|
||||
config: {
|
||||
webSearch: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
if (!res.ok) {
|
||||
return;
|
||||
}
|
||||
expect(res.warnings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: "tools.web.search.provider",
|
||||
message: expect.stringContaining("unknown web_search provider: missing-third-party"),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("web search provider auto-detection", () => {
|
||||
|
||||
@@ -997,6 +997,80 @@ function validateConfigObjectWithPluginsBase(
|
||||
return ensureInstalledPluginRecordIds().has(normalizedChannelId);
|
||||
};
|
||||
|
||||
const collectKnownWebSearchProviderIds = (): string[] => {
|
||||
const { registry } = ensureRegistry();
|
||||
return [
|
||||
...new Set(
|
||||
registry.plugins.flatMap((record) =>
|
||||
(record.contracts?.webSearchProviders ?? [])
|
||||
.map((providerId) => providerId.trim())
|
||||
.filter((providerId) => providerId.length > 0),
|
||||
),
|
||||
),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
};
|
||||
|
||||
const hasStalePluginEvidenceForUnknownWebSearchProvider = (providerId: string): boolean => {
|
||||
const normalizedProviderId = normalizePluginId(providerId);
|
||||
if (!normalizedProviderId || ensureKnownIds().has(normalizedProviderId)) {
|
||||
return false;
|
||||
}
|
||||
const pluginConfig = config.plugins;
|
||||
if (
|
||||
Array.isArray(pluginConfig?.allow) &&
|
||||
pluginConfig.allow.some((pluginId) => normalizePluginId(pluginId) === normalizedProviderId)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
isRecord(pluginConfig?.entries) &&
|
||||
Object.keys(pluginConfig.entries).some(
|
||||
(pluginId) => normalizePluginId(pluginId) === normalizedProviderId,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
isRecord(pluginConfig?.installs) &&
|
||||
Object.keys(pluginConfig.installs).some(
|
||||
(pluginId) => normalizePluginId(pluginId) === normalizedProviderId,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return ensureInstalledPluginRecordIds().has(normalizedProviderId);
|
||||
};
|
||||
|
||||
const validateWebSearchProvider = () => {
|
||||
const provider = config.tools?.web?.search?.provider;
|
||||
if (typeof provider !== "string") {
|
||||
return;
|
||||
}
|
||||
const trimmed = provider.trim();
|
||||
const path = "tools.web.search.provider";
|
||||
if (!trimmed) {
|
||||
issues.push({ path, message: "web_search provider must not be empty" });
|
||||
return;
|
||||
}
|
||||
const allowedValues = collectKnownWebSearchProviderIds();
|
||||
if (allowedValues.length === 0 || allowedValues.includes(trimmed)) {
|
||||
return;
|
||||
}
|
||||
const issue = {
|
||||
path,
|
||||
message: `unknown web_search provider: ${trimmed}`,
|
||||
allowedValues,
|
||||
};
|
||||
if (hasStalePluginEvidenceForUnknownWebSearchProvider(trimmed)) {
|
||||
warnings.push({
|
||||
...issue,
|
||||
message: `${issue.message} (stale web search plugin config ignored; run openclaw doctor --fix to remove stale config, or install the plugin)`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
issues.push(issue);
|
||||
};
|
||||
|
||||
const replaceChannelConfig = (channelId: string, nextValue: unknown) => {
|
||||
if (!channelsCloned) {
|
||||
mutatedConfig = {
|
||||
@@ -1143,6 +1217,8 @@ function validateConfigObjectWithPluginsBase(
|
||||
}
|
||||
}
|
||||
|
||||
validateWebSearchProvider();
|
||||
|
||||
if (!hasExplicitPluginsConfig) {
|
||||
if (issues.length > 0) {
|
||||
return { ok: false, issues, warnings };
|
||||
|
||||
Reference in New Issue
Block a user