mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
fix(plugins): remove xai boundary leaks
This commit is contained in:
@@ -93,4 +93,34 @@ describe("models-config provider auth provenance", () => {
|
||||
profileId: "openai:default",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves plugin-owned synthetic auth through the provider hook", () => {
|
||||
const auth = createProviderAuthResolver(
|
||||
{} as NodeJS.ProcessEnv,
|
||||
{
|
||||
version: 1,
|
||||
profiles: {},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
xai: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "xai-plugin-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(auth("xai")).toEqual({
|
||||
apiKey: NON_ENV_SECRETREF_MARKER,
|
||||
discoveryApiKey: "xai-plugin-key",
|
||||
mode: "api_key",
|
||||
source: "none",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { coerceSecretRef, resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import { resolveProviderWebSearchPluginConfig } from "../plugin-sdk/provider-web-search.js";
|
||||
import { resolveProviderSyntheticAuthWithPlugin } from "../plugins/provider-runtime.js";
|
||||
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
|
||||
import { listProfilesForProvider } from "./auth-profiles/profiles.js";
|
||||
@@ -439,16 +438,15 @@ function resolveConfigBackedProviderAuth(params: { provider: string; config?: Op
|
||||
// Providers own any provider-specific fallback auth logic via
|
||||
// resolveSyntheticAuth(...). Discovery/bootstrap callers may consume
|
||||
// non-secret markers from source config, but must never persist plaintext.
|
||||
const synthetic =
|
||||
resolveProviderSyntheticAuthWithPlugin({
|
||||
provider: params.provider,
|
||||
const synthetic = resolveProviderSyntheticAuthWithPlugin({
|
||||
provider: params.provider,
|
||||
config: params.config,
|
||||
context: {
|
||||
config: params.config,
|
||||
context: {
|
||||
config: params.config,
|
||||
provider: params.provider,
|
||||
providerConfig: params.config?.models?.providers?.[params.provider],
|
||||
},
|
||||
}) ?? resolveXaiConfigFallbackAuth(params);
|
||||
provider: params.provider,
|
||||
providerConfig: params.config?.models?.providers?.[params.provider],
|
||||
},
|
||||
});
|
||||
const apiKey = synthetic?.apiKey?.trim();
|
||||
if (!apiKey) {
|
||||
return undefined;
|
||||
@@ -467,74 +465,3 @@ function resolveConfigBackedProviderAuth(params: { provider: string; config?: Op
|
||||
source: "config",
|
||||
};
|
||||
}
|
||||
|
||||
function resolveXaiConfigFallbackAuth(params: { provider: string; config?: OpenClawConfig }):
|
||||
| {
|
||||
apiKey: string;
|
||||
source: string;
|
||||
mode: "api-key";
|
||||
}
|
||||
| undefined {
|
||||
if (params.provider.trim().toLowerCase() !== "xai") {
|
||||
return undefined;
|
||||
}
|
||||
const xaiPluginEntry = params.config?.plugins?.entries?.xai;
|
||||
if (xaiPluginEntry?.enabled === false) {
|
||||
return undefined;
|
||||
}
|
||||
const pluginApiKey = normalizeOptionalSecretInput(
|
||||
resolveProviderWebSearchPluginConfig(
|
||||
params.config as Record<string, unknown> | undefined,
|
||||
"xai",
|
||||
)?.apiKey,
|
||||
);
|
||||
if (pluginApiKey) {
|
||||
return {
|
||||
apiKey: pluginApiKey,
|
||||
source: "plugins.entries.xai.config.webSearch.apiKey",
|
||||
mode: "api-key",
|
||||
};
|
||||
}
|
||||
const pluginApiKeyRef = coerceSecretRef(
|
||||
resolveProviderWebSearchPluginConfig(
|
||||
params.config as Record<string, unknown> | undefined,
|
||||
"xai",
|
||||
)?.apiKey,
|
||||
);
|
||||
if (pluginApiKeyRef) {
|
||||
return {
|
||||
apiKey:
|
||||
pluginApiKeyRef.source === "env"
|
||||
? pluginApiKeyRef.id.trim()
|
||||
: resolveNonEnvSecretRefApiKeyMarker(pluginApiKeyRef.source),
|
||||
source: "plugins.entries.xai.config.webSearch.apiKey",
|
||||
mode: "api-key",
|
||||
};
|
||||
}
|
||||
const legacyGrokApiKey = normalizeOptionalSecretInput(
|
||||
(params.config?.tools?.web?.search as { grok?: { apiKey?: unknown } } | undefined | null)?.grok
|
||||
?.apiKey,
|
||||
);
|
||||
if (legacyGrokApiKey) {
|
||||
return {
|
||||
apiKey: legacyGrokApiKey,
|
||||
source: "tools.web.search.grok.apiKey",
|
||||
mode: "api-key",
|
||||
};
|
||||
}
|
||||
const legacyGrokApiKeyRef = coerceSecretRef(
|
||||
(params.config?.tools?.web?.search as { grok?: { apiKey?: unknown } } | undefined | null)?.grok
|
||||
?.apiKey,
|
||||
);
|
||||
if (legacyGrokApiKeyRef) {
|
||||
return {
|
||||
apiKey:
|
||||
legacyGrokApiKeyRef.source === "env"
|
||||
? legacyGrokApiKeyRef.id.trim()
|
||||
: resolveNonEnvSecretRefApiKeyMarker(legacyGrokApiKeyRef.source),
|
||||
source: "tools.web.search.grok.apiKey",
|
||||
mode: "api-key",
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -209,6 +209,44 @@ describe("applyPluginAutoEnable providers", () => {
|
||||
expect(result.changes).toContain("acme web search configured, enabled automatically.");
|
||||
});
|
||||
|
||||
it("auto-enables third-party plugins when manifest-owned tool config exists", () => {
|
||||
const result = applyPluginAutoEnable({
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
acme: {
|
||||
config: {
|
||||
acmeTool: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: makeIsolatedEnv(),
|
||||
manifestRegistry: makeRegistry([
|
||||
{
|
||||
id: "acme",
|
||||
channels: [],
|
||||
contracts: {
|
||||
tools: ["acme_tool"],
|
||||
},
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
webSearch: { type: "object" },
|
||||
acmeTool: { type: "object" },
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
expect(result.config.plugins?.entries?.acme?.enabled).toBe(true);
|
||||
expect(result.changes).toContain("acme tool configured, enabled automatically.");
|
||||
});
|
||||
|
||||
it("auto-enables acpx plugin when ACP is configured", () => {
|
||||
const result = applyPluginAutoEnable({
|
||||
config: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getChatChannelMeta, normalizeChatChannelId } from "../channels/registry
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
resolveManifestContractOwnerPluginId,
|
||||
type PluginManifestRecord,
|
||||
type PluginManifestRegistry,
|
||||
} from "../plugins/manifest-registry.js";
|
||||
import { resolveOwningPluginIdsForModelRef } from "../plugins/providers.js";
|
||||
@@ -178,40 +179,47 @@ function hasPluginOwnedWebFetchConfig(cfg: OpenClawConfig, pluginId: string): bo
|
||||
return isRecord(pluginConfig) && isRecord(pluginConfig.webFetch);
|
||||
}
|
||||
|
||||
function hasPluginOwnedToolConfig(cfg: OpenClawConfig, pluginId: string): boolean {
|
||||
const pluginConfig = cfg.plugins?.entries?.xai?.config;
|
||||
const web = cfg.tools?.web as Record<string, unknown> | undefined;
|
||||
return (
|
||||
pluginId === "xai" &&
|
||||
Boolean(
|
||||
isRecord(web?.x_search) ||
|
||||
(isRecord(pluginConfig) &&
|
||||
(isRecord(pluginConfig.xSearch) || isRecord(pluginConfig.codeExecution))),
|
||||
)
|
||||
);
|
||||
function resolvePluginOwnedToolConfigKeys(plugin: PluginManifestRecord): string[] {
|
||||
if ((plugin.contracts?.tools?.length ?? 0) === 0) {
|
||||
return [];
|
||||
}
|
||||
const properties = isRecord(plugin.configSchema) ? plugin.configSchema.properties : undefined;
|
||||
if (!isRecord(properties)) {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(properties).filter((key) => key !== "webSearch" && key !== "webFetch");
|
||||
}
|
||||
|
||||
function hasPluginOwnedToolConfig(cfg: OpenClawConfig, plugin: PluginManifestRecord): boolean {
|
||||
const pluginConfig = cfg.plugins?.entries?.[plugin.id]?.config;
|
||||
if (!isRecord(pluginConfig)) {
|
||||
return false;
|
||||
}
|
||||
return resolvePluginOwnedToolConfigKeys(plugin).some((key) => pluginConfig[key] !== undefined);
|
||||
}
|
||||
|
||||
function resolveProviderPluginsWithOwnedWebSearch(
|
||||
registry: PluginManifestRegistry,
|
||||
): ReadonlySet<string> {
|
||||
return new Set(
|
||||
registry.plugins
|
||||
.filter((plugin) => plugin.providers.length > 0)
|
||||
.filter((plugin) => (plugin.contracts?.webSearchProviders?.length ?? 0) > 0)
|
||||
.map((plugin) => plugin.id),
|
||||
);
|
||||
): PluginManifestRecord[] {
|
||||
return registry.plugins
|
||||
.filter((plugin) => plugin.providers.length > 0)
|
||||
.filter((plugin) => (plugin.contracts?.webSearchProviders?.length ?? 0) > 0);
|
||||
}
|
||||
|
||||
function resolveProviderPluginsWithOwnedWebFetch(
|
||||
registry: PluginManifestRegistry,
|
||||
): ReadonlySet<string> {
|
||||
return new Set(
|
||||
registry.plugins
|
||||
.filter((plugin) => (plugin.contracts?.webFetchProviders?.length ?? 0) > 0)
|
||||
.map((plugin) => plugin.id),
|
||||
): PluginManifestRecord[] {
|
||||
return registry.plugins.filter(
|
||||
(plugin) => (plugin.contracts?.webFetchProviders?.length ?? 0) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
function resolvePluginsWithOwnedToolConfig(
|
||||
registry: PluginManifestRegistry,
|
||||
): PluginManifestRecord[] {
|
||||
return registry.plugins.filter((plugin) => (plugin.contracts?.tools?.length ?? 0) > 0);
|
||||
}
|
||||
|
||||
function resolvePluginIdForConfiguredWebFetchProvider(
|
||||
providerId: string | undefined,
|
||||
env: NodeJS.ProcessEnv,
|
||||
@@ -275,6 +283,15 @@ function hasConfiguredWebFetchPluginEntry(cfg: OpenClawConfig): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function hasConfiguredPluginConfigEntry(cfg: OpenClawConfig): boolean {
|
||||
const entries = cfg.plugins?.entries;
|
||||
return (
|
||||
!!entries &&
|
||||
typeof entries === "object" &&
|
||||
Object.values(entries).some((entry) => isRecord(entry) && isRecord(entry.config))
|
||||
);
|
||||
}
|
||||
|
||||
function configMayNeedPluginManifestRegistry(cfg: OpenClawConfig): boolean {
|
||||
const pluginEntries = cfg.plugins?.entries;
|
||||
if (
|
||||
@@ -311,6 +328,9 @@ export function configMayNeedPluginAutoEnable(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): boolean {
|
||||
if (hasConfiguredPluginConfigEntry(cfg)) {
|
||||
return true;
|
||||
}
|
||||
if (hasPotentialConfiguredChannels(cfg, env)) {
|
||||
return true;
|
||||
}
|
||||
@@ -323,12 +343,7 @@ export function configMayNeedPluginAutoEnable(
|
||||
if (collectModelRefs(cfg).length > 0) {
|
||||
return true;
|
||||
}
|
||||
const web = cfg.tools?.web as Record<string, unknown> | undefined;
|
||||
if (
|
||||
isRecord(web?.x_search) ||
|
||||
hasConfiguredWebSearchPluginEntry(cfg) ||
|
||||
hasConfiguredWebFetchPluginEntry(cfg)
|
||||
) {
|
||||
if (hasConfiguredWebSearchPluginEntry(cfg) || hasConfiguredWebFetchPluginEntry(cfg)) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
@@ -416,16 +431,22 @@ export function resolveConfiguredPluginAutoEnableCandidates(params: {
|
||||
});
|
||||
}
|
||||
|
||||
for (const pluginId of resolveProviderPluginsWithOwnedWebSearch(params.registry)) {
|
||||
for (const plugin of resolveProviderPluginsWithOwnedWebSearch(params.registry)) {
|
||||
const pluginId = plugin.id;
|
||||
if (hasPluginOwnedWebSearchConfig(params.config, pluginId)) {
|
||||
changes.push({ pluginId, kind: "plugin-web-search-configured" });
|
||||
}
|
||||
if (hasPluginOwnedToolConfig(params.config, pluginId)) {
|
||||
}
|
||||
|
||||
for (const plugin of resolvePluginsWithOwnedToolConfig(params.registry)) {
|
||||
const pluginId = plugin.id;
|
||||
if (hasPluginOwnedToolConfig(params.config, plugin)) {
|
||||
changes.push({ pluginId, kind: "plugin-tool-configured" });
|
||||
}
|
||||
}
|
||||
|
||||
for (const pluginId of resolveProviderPluginsWithOwnedWebFetch(params.registry)) {
|
||||
for (const plugin of resolveProviderPluginsWithOwnedWebFetch(params.registry)) {
|
||||
const pluginId = plugin.id;
|
||||
if (hasPluginOwnedWebFetchConfig(params.config, pluginId)) {
|
||||
changes.push({ pluginId, kind: "plugin-web-fetch-configured" });
|
||||
}
|
||||
|
||||
@@ -61,8 +61,9 @@ export function makeRegistry(
|
||||
channels: string[];
|
||||
autoEnableWhenConfiguredProviders?: string[];
|
||||
modelSupport?: { modelPrefixes?: string[]; modelPatterns?: string[] };
|
||||
contracts?: { webSearchProviders?: string[]; webFetchProviders?: string[] };
|
||||
contracts?: { webSearchProviders?: string[]; webFetchProviders?: string[]; tools?: string[] };
|
||||
providers?: string[];
|
||||
configSchema?: Record<string, unknown>;
|
||||
channelConfigs?: Record<string, { schema: Record<string, unknown>; preferOver?: string[] }>;
|
||||
}>,
|
||||
): PluginManifestRegistry {
|
||||
@@ -73,6 +74,7 @@ export function makeRegistry(
|
||||
autoEnableWhenConfiguredProviders: plugin.autoEnableWhenConfiguredProviders,
|
||||
modelSupport: plugin.modelSupport,
|
||||
contracts: plugin.contracts,
|
||||
configSchema: plugin.configSchema,
|
||||
channelConfigs: plugin.channelConfigs,
|
||||
providers: plugin.providers ?? [],
|
||||
skills: [],
|
||||
|
||||
@@ -88,6 +88,24 @@ describe("model-pricing-cache", () => {
|
||||
expect(new Set(refs).size).toBe(refs.length);
|
||||
});
|
||||
|
||||
it("collects manifest-owned web search plugin model refs without a hardcoded plugin list", () => {
|
||||
const refs = collectConfiguredModelPricingRefs({
|
||||
plugins: {
|
||||
entries: {
|
||||
tavily: {
|
||||
config: {
|
||||
webSearch: {
|
||||
model: "tavily/search-preview",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig).map((ref) => modelKey(ref.provider, ref.model));
|
||||
|
||||
expect(refs).toContain("tavily/search-preview");
|
||||
});
|
||||
|
||||
it("loads openrouter pricing and maps provider aliases, wrappers, and anthropic dotted ids", async () => {
|
||||
const config = {
|
||||
agents: {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolvePluginWebSearchConfig } from "../config/plugin-web-search-config.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { resolveManifestContractPluginIds } from "../plugins/manifest-registry.js";
|
||||
import { normalizeProviderModelIdWithPlugin } from "../plugins/provider-runtime.js";
|
||||
import {
|
||||
clearGatewayModelPricingCacheState,
|
||||
@@ -250,6 +251,23 @@ function addProviderModelPair(params: {
|
||||
params.refs.set(modelKey(normalized.provider, normalized.model), normalized);
|
||||
}
|
||||
|
||||
function addConfiguredWebSearchPluginModels(params: {
|
||||
config: OpenClawConfig;
|
||||
aliasIndex: ReturnType<typeof buildModelAliasIndex>;
|
||||
refs: Map<string, ModelRef>;
|
||||
}): void {
|
||||
for (const pluginId of resolveManifestContractPluginIds({
|
||||
contract: "webSearchProviders",
|
||||
config: params.config,
|
||||
})) {
|
||||
addResolvedModelRef({
|
||||
raw: resolvePluginWebSearchConfig(params.config, pluginId)?.model as string | undefined,
|
||||
aliasIndex: params.aliasIndex,
|
||||
refs: params.refs,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function collectConfiguredModelPricingRefs(config: OpenClawConfig): ModelRef[] {
|
||||
const refs = new Map<string, ModelRef>();
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
@@ -289,26 +307,7 @@ export function collectConfiguredModelPricingRefs(config: OpenClawConfig): Model
|
||||
}
|
||||
}
|
||||
|
||||
addResolvedModelRef({
|
||||
raw: resolvePluginWebSearchConfig(config, "google")?.model as string | undefined,
|
||||
aliasIndex,
|
||||
refs,
|
||||
});
|
||||
addResolvedModelRef({
|
||||
raw: resolvePluginWebSearchConfig(config, "xai")?.model as string | undefined,
|
||||
aliasIndex,
|
||||
refs,
|
||||
});
|
||||
addResolvedModelRef({
|
||||
raw: resolvePluginWebSearchConfig(config, "moonshot")?.model as string | undefined,
|
||||
aliasIndex,
|
||||
refs,
|
||||
});
|
||||
addResolvedModelRef({
|
||||
raw: resolvePluginWebSearchConfig(config, "perplexity")?.model as string | undefined,
|
||||
aliasIndex,
|
||||
refs,
|
||||
});
|
||||
addConfiguredWebSearchPluginModels({ config, aliasIndex, refs });
|
||||
|
||||
for (const entry of config.tools?.media?.models ?? []) {
|
||||
addProviderModelPair({ provider: entry.provider, model: entry.model, refs });
|
||||
|
||||
Reference in New Issue
Block a user