From d5fafbe3ceb6fdebf5b49efc42f42770a7fa5206 Mon Sep 17 00:00:00 2001 From: huntharo Date: Fri, 27 Mar 2026 13:45:24 -0400 Subject: [PATCH] xAI: honor config-backed auth during provider bootstrap --- extensions/xai/web-search.test.ts | 26 ++++++++ ...ls-config.providers.discovery-auth.test.ts | 46 +++++++++++++ .../models-config.providers.implicit.ts | 4 +- src/agents/models-config.providers.secrets.ts | 66 ++++++++++++++++++- 4 files changed, 138 insertions(+), 4 deletions(-) diff --git a/extensions/xai/web-search.test.ts b/extensions/xai/web-search.test.ts index 4e422df7214..0ffe69693dc 100644 --- a/extensions/xai/web-search.test.ts +++ b/extensions/xai/web-search.test.ts @@ -137,6 +137,32 @@ describe("xai web search config resolution", () => { }); }); + it("reuses the legacy grok web search api key for provider auth fallback", () => { + const captured = capturePluginRegistration(xaiPlugin); + const provider = captured.providers[0]; + expect( + provider?.resolveSyntheticAuth?.({ + config: { + tools: { + web: { + search: { + grok: { + apiKey: "xai-legacy-fallback", // pragma: allowlist secret + }, + }, + }, + }, + }, + provider: "xai", + providerConfig: undefined, + }), + ).toEqual({ + apiKey: "xai-legacy-fallback", + source: "plugins.entries.xai.config.webSearch.apiKey", + mode: "api-key", + }); + }); + it("uses default model when not specified", () => { expect(resolveXaiWebSearchModel({})).toBe("grok-4-1-fast"); expect(resolveXaiWebSearchModel(undefined)).toBe("grok-4-1-fast"); diff --git a/src/agents/models-config.providers.discovery-auth.test.ts b/src/agents/models-config.providers.discovery-auth.test.ts index 6fc492c1565..4f8db7a7cb5 100644 --- a/src/agents/models-config.providers.discovery-auth.test.ts +++ b/src/agents/models-config.providers.discovery-auth.test.ts @@ -113,4 +113,50 @@ describe("provider discovery auth marker guardrails", () => { const request = vllmCall?.[1] as { headers?: Record } | undefined; expect(request?.headers?.Authorization).toBe("Bearer ALLCAPS_SAMPLE"); }); + + it("surfaces xai provider auth from plugin web search config without persisting plaintext", async () => { + const agentDir = await createAgentDirWithAuthProfiles({}); + + const providers = await resolveImplicitProvidersForTest({ + agentDir, + env: {}, + config: { + plugins: { + entries: { + xai: { + config: { + webSearch: { + apiKey: "xai-plugin-config-key", // pragma: allowlist secret + }, + }, + }, + }, + }, + }, + }); + + expect(providers?.xai?.apiKey).toBe(NON_ENV_SECRETREF_MARKER); + }); + + it("surfaces xai provider auth from legacy grok web search config without persisting plaintext", async () => { + const agentDir = await createAgentDirWithAuthProfiles({}); + + const providers = await resolveImplicitProvidersForTest({ + agentDir, + env: {}, + config: { + tools: { + web: { + search: { + grok: { + apiKey: "xai-legacy-config-key", // pragma: allowlist secret + }, + }, + }, + }, + }, + }); + + expect(providers?.xai?.apiKey).toBe(NON_ENV_SECRETREF_MARKER); + }); }); diff --git a/src/agents/models-config.providers.implicit.ts b/src/agents/models-config.providers.implicit.ts index 76629f21b04..2d09fb11835 100644 --- a/src/agents/models-config.providers.implicit.ts +++ b/src/agents/models-config.providers.implicit.ts @@ -305,8 +305,8 @@ export async function resolveImplicitProviders( ...params, authStore, env, - resolveProviderApiKey: createProviderApiKeyResolver(env, authStore), - resolveProviderAuth: createProviderAuthResolver(env, authStore), + resolveProviderApiKey: createProviderApiKeyResolver(env, authStore, params.config), + resolveProviderAuth: createProviderAuthResolver(env, authStore, params.config), }; for (const order of PLUGIN_DISCOVERY_ORDERS) { diff --git a/src/agents/models-config.providers.secrets.ts b/src/agents/models-config.providers.secrets.ts index cbda53ff88e..d05a662c6e1 100644 --- a/src/agents/models-config.providers.secrets.ts +++ b/src/agents/models-config.providers.secrets.ts @@ -1,5 +1,6 @@ import type { OpenClawConfig } from "../config/config.js"; import { coerceSecretRef, resolveSecretInputRef } from "../config/types.secrets.js"; +import { resolveProviderSyntheticAuthWithPlugin } from "../plugins/provider-runtime.js"; import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js"; import { listProfilesForProvider } from "./auth-profiles/profiles.js"; import { ensureAuthProfileStore } from "./auth-profiles/store.js"; @@ -304,6 +305,7 @@ export function resolveMissingProviderApiKey(params: { export function createProviderApiKeyResolver( env: NodeJS.ProcessEnv, authStore: ReturnType, + config?: OpenClawConfig, ): ProviderApiKeyResolver { return (provider: string): { apiKey: string | undefined; discoveryApiKey?: string } => { const envVar = resolveEnvApiKeyVarName(provider, env); @@ -314,9 +316,19 @@ export function createProviderApiKeyResolver( }; } const fromProfiles = resolveApiKeyFromProfiles({ provider, store: authStore, env }); + if (fromProfiles?.apiKey) { + return { + apiKey: fromProfiles.apiKey, + discoveryApiKey: fromProfiles.discoveryApiKey, + }; + } + const fromConfig = resolveConfigBackedProviderAuth({ + provider, + config, + }); return { - apiKey: fromProfiles?.apiKey, - discoveryApiKey: fromProfiles?.discoveryApiKey, + apiKey: fromConfig?.apiKey, + discoveryApiKey: fromConfig?.discoveryApiKey, }; }; } @@ -324,6 +336,7 @@ export function createProviderApiKeyResolver( export function createProviderAuthResolver( env: NodeJS.ProcessEnv, authStore: ReturnType, + config?: OpenClawConfig, ): ProviderAuthResolver { return (provider: string, options?: { oauthMarker?: string }) => { const ids = listProfilesForProvider(authStore, provider); @@ -377,6 +390,19 @@ export function createProviderAuthResolver( }; } + const fromConfig = resolveConfigBackedProviderAuth({ + provider, + config, + }); + if (fromConfig) { + return { + apiKey: fromConfig.apiKey, + discoveryApiKey: fromConfig.discoveryApiKey, + mode: fromConfig.mode, + source: "none", + }; + } + return { apiKey: undefined, discoveryApiKey: undefined, @@ -385,3 +411,39 @@ export function createProviderAuthResolver( }; }; } + +function resolveConfigBackedProviderAuth(params: { provider: string; config?: OpenClawConfig }): + | { + apiKey: string; + discoveryApiKey?: string; + mode: "api_key"; + source: "config"; + } + | undefined { + const synthetic = resolveProviderSyntheticAuthWithPlugin({ + provider: params.provider, + config: params.config, + context: { + config: params.config, + provider: params.provider, + providerConfig: params.config?.models?.providers?.[params.provider], + }, + }); + const apiKey = synthetic?.apiKey?.trim(); + if (!apiKey) { + return undefined; + } + return isNonSecretApiKeyMarker(apiKey) + ? { + apiKey, + discoveryApiKey: toDiscoveryApiKey(apiKey), + mode: "api_key", + source: "config", + } + : { + apiKey: resolveNonEnvSecretRefApiKeyMarker("file"), + discoveryApiKey: toDiscoveryApiKey(apiKey), + mode: "api_key", + source: "config", + }; +}