Enhance web search provider config validation and compatibility handling

- Added a test to ensure no warnings for legacy Brave config when bundled web search allowlist compatibility is applied.
- Updated validation logic to incorporate compatibility configuration for bundled web search plugins.
- Refactored the ensureRegistry function to utilize the new compatibility handling.
This commit is contained in:
Tyler Yust 2026-03-19 09:18:37 -07:00
parent c3be293dd5
commit b71686ab44
4 changed files with 187 additions and 4 deletions

View File

@ -136,6 +136,35 @@ function pluginWebSearchApiKey(
}
describe("web search provider config", () => {
it("does not warn for legacy brave config when bundled web search allowlist compat applies", () => {
const res = validateConfigObjectWithPlugins({
plugins: {
allow: ["bluebubbles", "memory-core"],
},
tools: {
web: {
search: {
enabled: true,
apiKey: "test-brave-key", // pragma: allowlist secret
},
},
},
});
expect(res.ok).toBe(true);
if (!res.ok) {
return;
}
expect(res.warnings).not.toContainEqual(
expect.objectContaining({
path: "plugins.entries.brave",
message: expect.stringContaining(
"plugin disabled (not in allowlist) but config is present",
),
}),
);
});
it("accepts perplexity provider and config", () => {
const res = validateConfigObjectWithPlugins(
buildWebSearchProviderConfig({

View File

@ -1,6 +1,8 @@
import path from "node:path";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { CHANNEL_IDS, normalizeChatChannelId } from "../channels/registry.js";
import { withBundledPluginAllowlistCompat } from "../plugins/bundled-compat.js";
import { resolveBundledWebSearchPluginIds } from "../plugins/bundled-web-search.js";
import {
normalizePluginsConfig,
resolveEffectiveEnableState,
@ -351,15 +353,38 @@ function validateConfigObjectWithPluginsBase(
};
let registryInfo: RegistryInfo | null = null;
let compatConfig: OpenClawConfig | null | undefined;
const ensureCompatConfig = (): OpenClawConfig => {
if (compatConfig !== undefined) {
return compatConfig ?? config;
}
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
const bundledWebSearchPluginIds = resolveBundledWebSearchPluginIds({
config,
workspaceDir: workspaceDir ?? undefined,
env: opts.env,
});
compatConfig = withBundledPluginAllowlistCompat({
config,
pluginIds: bundledWebSearchPluginIds,
});
return compatConfig ?? config;
};
const ensureRegistry = (): RegistryInfo => {
if (registryInfo) {
return registryInfo;
}
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
const effectiveConfig = ensureCompatConfig();
const workspaceDir = resolveAgentWorkspaceDir(
effectiveConfig,
resolveDefaultAgentId(effectiveConfig),
);
const registry = loadPluginManifestRegistry({
config,
config: effectiveConfig,
workspaceDir: workspaceDir ?? undefined,
env: opts.env,
});
@ -393,7 +418,7 @@ function validateConfigObjectWithPluginsBase(
const ensureNormalizedPlugins = (): ReturnType<typeof normalizePluginsConfig> => {
const info = ensureRegistry();
if (!info.normalizedPlugins) {
info.normalizedPlugins = normalizePluginsConfig(config.plugins);
info.normalizedPlugins = normalizePluginsConfig(ensureCompatConfig().plugins);
}
return info.normalizedPlugins;
};

View File

@ -0,0 +1,129 @@
import fs from "node:fs";
import path from "node:path";
import { createJiti } from "jiti";
import { loadConfig } from "../../config/config.js";
import { loadPluginManifestRegistry } from "../manifest-registry.js";
import {
buildPluginLoaderJitiOptions,
resolvePluginSdkAliasFile,
resolvePluginSdkScopedAliasMap,
shouldPreferNativeJiti,
} from "../sdk-alias.js";
const MATRIX_PLUGIN_ID = "matrix";
type MatrixModule = typeof import("../../../extensions/matrix/runtime-api.js");
type MatrixPluginRecord = {
rootDir?: string;
source: string;
};
let cachedModulePath: string | null = null;
let cachedModule: MatrixModule | null = null;
const jitiLoaders = new Map<boolean, ReturnType<typeof createJiti>>();
function readConfigSafely() {
try {
return loadConfig();
} catch {
return {};
}
}
function resolveMatrixPluginRecord(): MatrixPluginRecord | null {
const manifestRegistry = loadPluginManifestRegistry({
config: readConfigSafely(),
cache: true,
});
const record = manifestRegistry.plugins.find((plugin) => plugin.id === MATRIX_PLUGIN_ID);
if (!record?.source) {
return null;
}
return {
rootDir: record.rootDir,
source: record.source,
};
}
function resolveMatrixRuntimeModulePath(record: MatrixPluginRecord): string | null {
const candidates = [
path.join(path.dirname(record.source), "runtime-api.js"),
path.join(path.dirname(record.source), "runtime-api.ts"),
...(record.rootDir
? [path.join(record.rootDir, "runtime-api.js"), path.join(record.rootDir, "runtime-api.ts")]
: []),
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return null;
}
function getJiti(modulePath: string) {
const tryNative = shouldPreferNativeJiti(modulePath);
const cached = jitiLoaders.get(tryNative);
if (cached) {
return cached;
}
const pluginSdkAlias = resolvePluginSdkAliasFile({
srcFile: "root-alias.cjs",
distFile: "root-alias.cjs",
modulePath,
});
const aliasMap = {
...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
...resolvePluginSdkScopedAliasMap({ modulePath }),
};
const loader = createJiti(import.meta.url, {
...buildPluginLoaderJitiOptions(aliasMap),
tryNative,
});
jitiLoaders.set(tryNative, loader);
return loader;
}
function loadWithJiti<TModule>(modulePath: string): TModule {
return getJiti(modulePath)(modulePath) as TModule;
}
function loadMatrixModule(): MatrixModule | null {
const record = resolveMatrixPluginRecord();
if (!record) {
return null;
}
const modulePath = resolveMatrixRuntimeModulePath(record);
if (!modulePath) {
return null;
}
if (cachedModule && cachedModulePath === modulePath) {
return cachedModule;
}
const loaded = loadWithJiti<MatrixModule>(modulePath);
cachedModulePath = modulePath;
cachedModule = loaded;
return loaded;
}
export function setMatrixThreadBindingIdleTimeoutBySessionKey(
...args: Parameters<MatrixModule["setMatrixThreadBindingIdleTimeoutBySessionKey"]>
): ReturnType<MatrixModule["setMatrixThreadBindingIdleTimeoutBySessionKey"]> {
const fn = loadMatrixModule()?.setMatrixThreadBindingIdleTimeoutBySessionKey;
if (typeof fn !== "function") {
return [];
}
return fn(...args);
}
export function setMatrixThreadBindingMaxAgeBySessionKey(
...args: Parameters<MatrixModule["setMatrixThreadBindingMaxAgeBySessionKey"]>
): ReturnType<MatrixModule["setMatrixThreadBindingMaxAgeBySessionKey"]> {
const fn = loadMatrixModule()?.setMatrixThreadBindingMaxAgeBySessionKey;
if (typeof fn !== "function") {
return [];
}
return fn(...args);
}

View File

@ -1,7 +1,7 @@
import {
setMatrixThreadBindingIdleTimeoutBySessionKey,
setMatrixThreadBindingMaxAgeBySessionKey,
} from "../../../extensions/matrix/runtime-api.js";
} from "./runtime-matrix-boundary.js";
import type { PluginRuntimeChannel } from "./types-channel.js";
export function createRuntimeMatrix(): PluginRuntimeChannel["matrix"] {