From 55f18f67e29db55acaa7bd606db7cacded9c22d2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 6 Apr 2026 14:40:27 +0100 Subject: [PATCH] perf(test): split secrets runtime provider coverage --- ...untime-provider-and-media-surfaces.test.ts | 487 +++++++++++++++ .../runtime-request-secret-refs.test.ts | 329 ++++++++++ src/secrets/runtime.test.ts | 591 ------------------ 3 files changed, 816 insertions(+), 591 deletions(-) create mode 100644 src/secrets/runtime-provider-and-media-surfaces.test.ts create mode 100644 src/secrets/runtime-request-secret-refs.test.ts diff --git a/src/secrets/runtime-provider-and-media-surfaces.test.ts b/src/secrets/runtime-provider-and-media-surfaces.test.ts new file mode 100644 index 00000000000..92b4dd9cf24 --- /dev/null +++ b/src/secrets/runtime-provider-and-media-surfaces.test.ts @@ -0,0 +1,487 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import { createEmptyPluginRegistry } from "../plugins/registry.js"; +import { setActivePluginRegistry } from "../plugins/runtime.js"; +import type { PluginWebSearchProviderEntry } from "../plugins/types.js"; + +type WebProviderUnderTest = "brave" | "gemini" | "grok" | "kimi" | "perplexity" | "firecrawl"; + +const { resolvePluginWebSearchProvidersMock } = vi.hoisted(() => ({ + resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()), +})); + +vi.mock("../plugins/web-search-providers.runtime.js", () => ({ + resolvePluginWebSearchProviders: resolvePluginWebSearchProvidersMock, +})); + +function asConfig(value: unknown): OpenClawConfig { + return value as OpenClawConfig; +} + +function createTestProvider(params: { + id: WebProviderUnderTest; + pluginId: string; + order: number; +}): PluginWebSearchProviderEntry { + const credentialPath = `plugins.entries.${params.pluginId}.config.webSearch.apiKey`; + const readSearchConfigKey = (searchConfig?: Record): unknown => { + const providerConfig = + searchConfig?.[params.id] && typeof searchConfig[params.id] === "object" + ? (searchConfig[params.id] as { apiKey?: unknown }) + : undefined; + return providerConfig?.apiKey ?? searchConfig?.apiKey; + }; + return { + pluginId: params.pluginId, + id: params.id, + label: params.id, + hint: `${params.id} test provider`, + envVars: [`${params.id.toUpperCase()}_API_KEY`], + placeholder: `${params.id}-...`, + signupUrl: `https://example.com/${params.id}`, + autoDetectOrder: params.order, + credentialPath, + inactiveSecretPaths: [credentialPath], + getCredentialValue: readSearchConfigKey, + setCredentialValue: (searchConfigTarget, value) => { + const providerConfig = + params.id === "brave" || params.id === "firecrawl" + ? searchConfigTarget + : ((searchConfigTarget[params.id] ??= {}) as { apiKey?: unknown }); + providerConfig.apiKey = value; + }, + getConfiguredCredentialValue: (config) => + (config?.plugins?.entries?.[params.pluginId]?.config as { webSearch?: { apiKey?: unknown } }) + ?.webSearch?.apiKey, + setConfiguredCredentialValue: (configTarget, value) => { + const plugins = (configTarget.plugins ??= {}) as { entries?: Record }; + const entries = (plugins.entries ??= {}); + const entry = (entries[params.pluginId] ??= {}) as { config?: Record }; + const config = (entry.config ??= {}); + const webSearch = (config.webSearch ??= {}) as { apiKey?: unknown }; + webSearch.apiKey = value; + }, + resolveRuntimeMetadata: + params.id === "perplexity" + ? () => ({ + perplexityTransport: "search_api" as const, + }) + : undefined, + createTool: () => null, + }; +} + +function buildTestWebSearchProviders(): PluginWebSearchProviderEntry[] { + return [ + createTestProvider({ id: "brave", pluginId: "brave", order: 10 }), + createTestProvider({ id: "gemini", pluginId: "google", order: 20 }), + createTestProvider({ id: "grok", pluginId: "xai", order: 30 }), + createTestProvider({ id: "kimi", pluginId: "moonshot", order: 40 }), + createTestProvider({ id: "perplexity", pluginId: "perplexity", order: 50 }), + createTestProvider({ id: "firecrawl", pluginId: "firecrawl", order: 60 }), + ]; +} + +function createOpenAiFileModelsConfig(): NonNullable { + return { + providers: { + openai: { + baseUrl: "https://api.openai.com/v1", + apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" }, + models: [], + }, + }, + }; +} + +let clearConfigCache: typeof import("../config/config.js").clearConfigCache; +let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntimeConfigSnapshot; +let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot; +let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot; + +describe("secrets runtime provider and media surfaces", () => { + beforeAll(async () => { + ({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js")); + ({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js")); + }); + + beforeEach(() => { + resolvePluginWebSearchProvidersMock.mockReset(); + resolvePluginWebSearchProvidersMock.mockReturnValue(buildTestWebSearchProviders()); + }); + + afterEach(() => { + setActivePluginRegistry(createEmptyPluginRegistry()); + clearSecretsRuntimeSnapshot(); + clearRuntimeConfigSnapshot(); + clearConfigCache(); + }); + + it("resolves file refs via configured file provider", async () => { + if (process.platform === "win32") { + return; + } + const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-file-provider-")); + const secretsPath = path.join(root, "secrets.json"); + try { + await fs.writeFile( + secretsPath, + JSON.stringify( + { + providers: { + openai: { + apiKey: "sk-from-file-provider", + }, + }, + }, + null, + 2, + ), + "utf8", + ); + await fs.chmod(secretsPath, 0o600); + + const config = asConfig({ + secrets: { + providers: { + default: { + source: "file", + path: secretsPath, + mode: "json", + }, + }, + defaults: { + file: "default", + }, + }, + models: { + providers: { + openai: { + baseUrl: "https://api.openai.com/v1", + apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" }, + models: [], + }, + }, + }, + }); + + const snapshot = await prepareSecretsRuntimeSnapshot({ + config, + agentDirs: ["/tmp/openclaw-agent-main"], + loadAuthStore: () => ({ version: 1, profiles: {} }), + }); + + expect(snapshot.config.models?.providers?.openai?.apiKey).toBe("sk-from-file-provider"); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + + it("fails when file provider payload is not a JSON object", async () => { + if (process.platform === "win32") { + return; + } + const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-file-provider-bad-")); + const secretsPath = path.join(root, "secrets.json"); + try { + await fs.writeFile(secretsPath, JSON.stringify(["not-an-object"]), "utf8"); + await fs.chmod(secretsPath, 0o600); + + await expect( + prepareSecretsRuntimeSnapshot({ + config: asConfig({ + secrets: { + providers: { + default: { + source: "file", + path: secretsPath, + mode: "json", + }, + }, + }, + models: { + ...createOpenAiFileModelsConfig(), + }, + }), + agentDirs: ["/tmp/openclaw-agent-main"], + loadAuthStore: () => ({ version: 1, profiles: {} }), + }), + ).rejects.toThrow("payload is not a JSON object"); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + + it("resolves shared media model request refs when capability blocks are omitted", async () => { + const snapshot = await prepareSecretsRuntimeSnapshot({ + config: asConfig({ + tools: { + media: { + models: [ + { + provider: "openai", + model: "gpt-4o-mini-transcribe", + capabilities: ["audio"], + request: { + auth: { + mode: "authorization-bearer", + token: { + source: "env", + provider: "default", + id: "MEDIA_SHARED_AUDIO_TOKEN", + }, + }, + }, + }, + ], + }, + }, + }), + env: { + MEDIA_SHARED_AUDIO_TOKEN: "shared-audio-token", + }, + agentDirs: ["/tmp/openclaw-agent-main"], + loadAuthStore: () => ({ version: 1, profiles: {} }), + }); + + expect(snapshot.config.tools?.media?.models?.[0]?.request?.auth).toEqual({ + mode: "authorization-bearer", + token: "shared-audio-token", + }); + expect(snapshot.warnings.map((warning) => warning.path)).not.toContain( + "tools.media.models.0.request.auth.token", + ); + }); + + it("treats shared media model request refs as inactive when their capabilities are disabled", async () => { + const sharedTokenRef = { + source: "env" as const, + provider: "default" as const, + id: "MEDIA_DISABLED_AUDIO_TOKEN", + }; + const snapshot = await prepareSecretsRuntimeSnapshot({ + config: asConfig({ + tools: { + media: { + models: [ + { + provider: "openai", + model: "gpt-4o-mini-transcribe", + capabilities: ["audio"], + request: { + auth: { + mode: "authorization-bearer", + token: sharedTokenRef, + }, + }, + }, + ], + audio: { + enabled: false, + }, + }, + }, + }), + env: {}, + agentDirs: ["/tmp/openclaw-agent-main"], + loadAuthStore: () => ({ version: 1, profiles: {} }), + }); + + expect(snapshot.config.tools?.media?.models?.[0]?.request?.auth).toEqual({ + mode: "authorization-bearer", + token: sharedTokenRef, + }); + expect(snapshot.warnings.map((warning) => warning.path)).toContain( + "tools.media.models.0.request.auth.token", + ); + }); + + it("resolves shared media model request refs from inferred provider capabilities", async () => { + const pluginRegistry = createEmptyPluginRegistry(); + pluginRegistry.mediaUnderstandingProviders.push({ + pluginId: "deepgram", + pluginName: "Deepgram Plugin", + source: "test", + provider: { + id: "deepgram", + capabilities: ["audio"], + }, + }); + setActivePluginRegistry(pluginRegistry); + + const snapshot = await prepareSecretsRuntimeSnapshot({ + config: asConfig({ + tools: { + media: { + models: [ + { + provider: "deepgram", + request: { + auth: { + mode: "authorization-bearer", + token: { + source: "env", + provider: "default", + id: "MEDIA_INFERRED_AUDIO_TOKEN", + }, + }, + }, + }, + ], + }, + }, + }), + env: { + MEDIA_INFERRED_AUDIO_TOKEN: "inferred-audio-token", + }, + agentDirs: ["/tmp/openclaw-agent-main"], + loadAuthStore: () => ({ version: 1, profiles: {} }), + }); + + expect(snapshot.config.tools?.media?.models?.[0]?.request?.auth).toEqual({ + mode: "authorization-bearer", + token: "inferred-audio-token", + }); + expect(snapshot.warnings.map((warning) => warning.path)).not.toContain( + "tools.media.models.0.request.auth.token", + ); + }); + + it("treats shared media model request refs as inactive when inferred capabilities are disabled", async () => { + const pluginRegistry = createEmptyPluginRegistry(); + pluginRegistry.mediaUnderstandingProviders.push({ + pluginId: "deepgram", + pluginName: "Deepgram Plugin", + source: "test", + provider: { + id: "deepgram", + capabilities: ["audio"], + }, + }); + setActivePluginRegistry(pluginRegistry); + + const inferredTokenRef = { + source: "env" as const, + provider: "default" as const, + id: "MEDIA_INFERRED_DISABLED_AUDIO_TOKEN", + }; + const snapshot = await prepareSecretsRuntimeSnapshot({ + config: asConfig({ + tools: { + media: { + models: [ + { + provider: "deepgram", + request: { + auth: { + mode: "authorization-bearer", + token: inferredTokenRef, + }, + }, + }, + ], + audio: { + enabled: false, + }, + }, + }, + }), + env: {}, + agentDirs: ["/tmp/openclaw-agent-main"], + loadAuthStore: () => ({ version: 1, profiles: {} }), + }); + + expect(snapshot.config.tools?.media?.models?.[0]?.request?.auth).toEqual({ + mode: "authorization-bearer", + token: inferredTokenRef, + }); + expect(snapshot.warnings.map((warning) => warning.path)).toContain( + "tools.media.models.0.request.auth.token", + ); + }); + + it("treats section media model request refs as inactive when model capabilities exclude the section", async () => { + const sectionTokenRef = { + source: "env" as const, + provider: "default" as const, + id: "MEDIA_AUDIO_SECTION_FILTERED_TOKEN", + }; + const snapshot = await prepareSecretsRuntimeSnapshot({ + config: asConfig({ + tools: { + media: { + audio: { + enabled: true, + models: [ + { + provider: "openai", + capabilities: ["video"], + request: { + auth: { + mode: "authorization-bearer", + token: sectionTokenRef, + }, + }, + }, + ], + }, + }, + }, + }), + env: {}, + agentDirs: ["/tmp/openclaw-agent-main"], + loadAuthStore: () => ({ version: 1, profiles: {} }), + }); + + expect(snapshot.config.tools?.media?.audio?.models?.[0]?.request?.auth).toEqual({ + mode: "authorization-bearer", + token: sectionTokenRef, + }); + expect(snapshot.warnings.map((warning) => warning.path)).toContain( + "tools.media.audio.models.0.request.auth.token", + ); + }); + + it("treats defaults memorySearch ref as inactive when all enabled agents disable memorySearch", async () => { + const snapshot = await prepareSecretsRuntimeSnapshot({ + config: asConfig({ + agents: { + defaults: { + memorySearch: { + remote: { + apiKey: { + source: "env", + provider: "default", + id: "DEFAULT_MEMORY_REMOTE_API_KEY", + }, + }, + }, + }, + list: [ + { + enabled: true, + memorySearch: { + enabled: false, + }, + }, + ], + }, + }), + env: {}, + agentDirs: ["/tmp/openclaw-agent-main"], + loadAuthStore: () => ({ version: 1, profiles: {} }), + }); + + expect(snapshot.config.agents?.defaults?.memorySearch?.remote?.apiKey).toEqual({ + source: "env", + provider: "default", + id: "DEFAULT_MEMORY_REMOTE_API_KEY", + }); + expect(snapshot.warnings.map((warning) => warning.path)).toContain( + "agents.defaults.memorySearch.remote.apiKey", + ); + }); +}); diff --git a/src/secrets/runtime-request-secret-refs.test.ts b/src/secrets/runtime-request-secret-refs.test.ts new file mode 100644 index 00000000000..a64d81e4841 --- /dev/null +++ b/src/secrets/runtime-request-secret-refs.test.ts @@ -0,0 +1,329 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import type { AuthProfileStore } from "../agents/auth-profiles.js"; +import type { OpenClawConfig } from "../config/config.js"; +import { createEmptyPluginRegistry } from "../plugins/registry.js"; +import { setActivePluginRegistry } from "../plugins/runtime.js"; +import type { PluginWebSearchProviderEntry } from "../plugins/types.js"; + +type WebProviderUnderTest = "brave" | "gemini" | "grok" | "kimi" | "perplexity" | "firecrawl"; + +const { resolvePluginWebSearchProvidersMock } = vi.hoisted(() => ({ + resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()), +})); + +vi.mock("../plugins/web-search-providers.runtime.js", () => ({ + resolvePluginWebSearchProviders: resolvePluginWebSearchProvidersMock, +})); + +function asConfig(value: unknown): OpenClawConfig { + return value as OpenClawConfig; +} + +function createTestProvider(params: { + id: WebProviderUnderTest; + pluginId: string; + order: number; +}): PluginWebSearchProviderEntry { + const credentialPath = `plugins.entries.${params.pluginId}.config.webSearch.apiKey`; + const readSearchConfigKey = (searchConfig?: Record): unknown => { + const providerConfig = + searchConfig?.[params.id] && typeof searchConfig[params.id] === "object" + ? (searchConfig[params.id] as { apiKey?: unknown }) + : undefined; + return providerConfig?.apiKey ?? searchConfig?.apiKey; + }; + return { + pluginId: params.pluginId, + id: params.id, + label: params.id, + hint: `${params.id} test provider`, + envVars: [`${params.id.toUpperCase()}_API_KEY`], + placeholder: `${params.id}-...`, + signupUrl: `https://example.com/${params.id}`, + autoDetectOrder: params.order, + credentialPath, + inactiveSecretPaths: [credentialPath], + getCredentialValue: readSearchConfigKey, + setCredentialValue: (searchConfigTarget, value) => { + const providerConfig = + params.id === "brave" || params.id === "firecrawl" + ? searchConfigTarget + : ((searchConfigTarget[params.id] ??= {}) as { apiKey?: unknown }); + providerConfig.apiKey = value; + }, + getConfiguredCredentialValue: (config) => + (config?.plugins?.entries?.[params.pluginId]?.config as { webSearch?: { apiKey?: unknown } }) + ?.webSearch?.apiKey, + setConfiguredCredentialValue: (configTarget, value) => { + const plugins = (configTarget.plugins ??= {}) as { entries?: Record }; + const entries = (plugins.entries ??= {}); + const entry = (entries[params.pluginId] ??= {}) as { config?: Record }; + const config = (entry.config ??= {}); + const webSearch = (config.webSearch ??= {}) as { apiKey?: unknown }; + webSearch.apiKey = value; + }, + resolveRuntimeMetadata: + params.id === "perplexity" + ? () => ({ + perplexityTransport: "search_api" as const, + }) + : undefined, + createTool: () => null, + }; +} + +function buildTestWebSearchProviders(): PluginWebSearchProviderEntry[] { + return [ + createTestProvider({ id: "brave", pluginId: "brave", order: 10 }), + createTestProvider({ id: "gemini", pluginId: "google", order: 20 }), + createTestProvider({ id: "grok", pluginId: "xai", order: 30 }), + createTestProvider({ id: "kimi", pluginId: "moonshot", order: 40 }), + createTestProvider({ id: "perplexity", pluginId: "perplexity", order: 50 }), + createTestProvider({ id: "firecrawl", pluginId: "firecrawl", order: 60 }), + ]; +} + +function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore { + return { + version: 1, + profiles, + }; +} + +let clearConfigCache: typeof import("../config/config.js").clearConfigCache; +let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntimeConfigSnapshot; +let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot; +let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot; + +describe("secrets runtime snapshot request secret refs", () => { + beforeAll(async () => { + ({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js")); + ({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js")); + }); + + beforeEach(() => { + resolvePluginWebSearchProvidersMock.mockReset(); + resolvePluginWebSearchProvidersMock.mockReturnValue(buildTestWebSearchProviders()); + }); + + afterEach(() => { + setActivePluginRegistry(createEmptyPluginRegistry()); + clearSecretsRuntimeSnapshot(); + clearRuntimeConfigSnapshot(); + clearConfigCache(); + }); + + it("can skip auth-profile SecretRef resolution when includeAuthStoreRefs is false", async () => { + const missingEnvVar = `OPENCLAW_MISSING_AUTH_PROFILE_SECRET_${Date.now()}`; + delete process.env[missingEnvVar]; + + const loadAuthStore = () => + loadAuthStoreWithProfiles({ + "custom:token": { + type: "token", + provider: "custom", + tokenRef: { source: "env", provider: "default", id: missingEnvVar }, + }, + }); + + await expect( + prepareSecretsRuntimeSnapshot({ + config: asConfig({}), + env: {}, + agentDirs: ["/tmp/openclaw-agent-main"], + loadAuthStore, + }), + ).rejects.toThrow(`Environment variable "${missingEnvVar}" is missing or empty.`); + + const snapshot = await prepareSecretsRuntimeSnapshot({ + config: asConfig({}), + env: {}, + includeAuthStoreRefs: false, + agentDirs: ["/tmp/openclaw-agent-main"], + loadAuthStore, + }); + + expect(snapshot.authStores).toEqual([]); + }); + + it("resolves model provider request secret refs for headers, auth, and tls material", async () => { + const config = asConfig({ + models: { + providers: { + openai: { + baseUrl: "https://api.openai.com/v1", + request: { + headers: { + "X-Tenant": { source: "env", provider: "default", id: "OPENAI_PROVIDER_TENANT" }, + }, + auth: { + mode: "authorization-bearer", + token: { source: "env", provider: "default", id: "OPENAI_PROVIDER_TOKEN" }, + }, + proxy: { + mode: "explicit-proxy", + url: "http://proxy.example:8080", + tls: { + ca: { source: "env", provider: "default", id: "OPENAI_PROVIDER_PROXY_CA" }, + }, + }, + tls: { + cert: { source: "env", provider: "default", id: "OPENAI_PROVIDER_CERT" }, + key: { source: "env", provider: "default", id: "OPENAI_PROVIDER_KEY" }, + }, + }, + models: [], + }, + }, + }, + }); + + const snapshot = await prepareSecretsRuntimeSnapshot({ + config, + env: { + OPENAI_PROVIDER_TENANT: "tenant-acme", + OPENAI_PROVIDER_TOKEN: "sk-provider-runtime", // pragma: allowlist secret + OPENAI_PROVIDER_PROXY_CA: "proxy-ca", + OPENAI_PROVIDER_CERT: "client-cert", + OPENAI_PROVIDER_KEY: "client-key", + }, + agentDirs: ["/tmp/openclaw-agent-main"], + loadAuthStore: () => ({ version: 1, profiles: {} }), + }); + + expect(snapshot.config.models?.providers?.openai?.request).toEqual({ + headers: { + "X-Tenant": "tenant-acme", + }, + auth: { + mode: "authorization-bearer", + token: "sk-provider-runtime", + }, + proxy: { + mode: "explicit-proxy", + url: "http://proxy.example:8080", + tls: { + ca: "proxy-ca", + }, + }, + tls: { + cert: "client-cert", + key: "client-key", + }, + }); + }); + + it("resolves media request secret refs for provider headers, auth, and tls material", async () => { + const snapshot = await prepareSecretsRuntimeSnapshot({ + config: asConfig({ + tools: { + media: { + models: [ + { + provider: "openai", + model: "gpt-4o-mini-transcribe", + capabilities: ["audio"], + request: { + headers: { + "X-Shared-Tenant": { + source: "env", + provider: "default", + id: "MEDIA_SHARED_TENANT", + }, + }, + auth: { + mode: "header", + headerName: "x-shared-key", + value: { + source: "env", + provider: "default", + id: "MEDIA_SHARED_MODEL_KEY", + }, + }, + }, + }, + ], + audio: { + enabled: true, + request: { + headers: { + "X-Tenant": { source: "env", provider: "default", id: "MEDIA_AUDIO_TENANT" }, + }, + auth: { + mode: "authorization-bearer", + token: { source: "env", provider: "default", id: "MEDIA_AUDIO_TOKEN" }, + }, + tls: { + cert: { source: "env", provider: "default", id: "MEDIA_AUDIO_CERT" }, + }, + }, + models: [ + { + provider: "deepgram", + request: { + auth: { + mode: "header", + headerName: "x-api-key", + value: { source: "env", provider: "default", id: "MEDIA_AUDIO_MODEL_KEY" }, + }, + proxy: { + mode: "explicit-proxy", + url: "http://proxy.example:8080", + tls: { + ca: { source: "env", provider: "default", id: "MEDIA_AUDIO_PROXY_CA" }, + }, + }, + }, + }, + ], + }, + }, + }, + }), + env: { + MEDIA_SHARED_TENANT: "tenant-shared", + MEDIA_SHARED_MODEL_KEY: "shared-model-key", // pragma: allowlist secret + MEDIA_AUDIO_TENANT: "tenant-acme", + MEDIA_AUDIO_TOKEN: "audio-token", // pragma: allowlist secret + MEDIA_AUDIO_CERT: "client-cert", + MEDIA_AUDIO_MODEL_KEY: "model-key", // pragma: allowlist secret + MEDIA_AUDIO_PROXY_CA: "proxy-ca", + }, + agentDirs: ["/tmp/openclaw-agent-main"], + loadAuthStore: () => ({ version: 1, profiles: {} }), + }); + + expect(snapshot.config.tools?.media?.audio?.request?.headers?.["X-Tenant"]).toBe("tenant-acme"); + expect(snapshot.config.tools?.media?.audio?.request?.auth).toEqual({ + mode: "authorization-bearer", + token: "audio-token", + }); + expect(snapshot.config.tools?.media?.audio?.request?.tls).toEqual({ + cert: "client-cert", + }); + expect(snapshot.config.tools?.media?.models?.[0]?.request).toEqual({ + headers: { + "X-Shared-Tenant": "tenant-shared", + }, + auth: { + mode: "header", + headerName: "x-shared-key", + value: "shared-model-key", + }, + }); + expect(snapshot.config.tools?.media?.audio?.models?.[0]?.request).toEqual({ + auth: { + mode: "header", + headerName: "x-api-key", + value: "model-key", + }, + proxy: { + mode: "explicit-proxy", + url: "http://proxy.example:8080", + tls: { + ca: "proxy-ca", + }, + }, + }); + }); +}); diff --git a/src/secrets/runtime.test.ts b/src/secrets/runtime.test.ts index 46b0dbd0c0c..17926055121 100644 --- a/src/secrets/runtime.test.ts +++ b/src/secrets/runtime.test.ts @@ -94,18 +94,6 @@ let activateSecretsRuntimeSnapshot: typeof import("./runtime.js").activateSecret let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot; let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot; -function createOpenAiFileModelsConfig(): NonNullable { - return { - providers: { - openai: { - baseUrl: "https://api.openai.com/v1", - apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" }, - models: [], - }, - }, - }; -} - function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore { return { version: 1, @@ -135,39 +123,6 @@ describe("secrets runtime snapshot", () => { clearConfigCache(); }); - it("can skip auth-profile SecretRef resolution when includeAuthStoreRefs is false", async () => { - const missingEnvVar = `OPENCLAW_MISSING_AUTH_PROFILE_SECRET_${Date.now()}`; - delete process.env[missingEnvVar]; - - const loadAuthStore = () => - loadAuthStoreWithProfiles({ - "custom:token": { - type: "token", - provider: "custom", - tokenRef: { source: "env", provider: "default", id: missingEnvVar }, - }, - }); - - await expect( - prepareSecretsRuntimeSnapshot({ - config: asConfig({}), - env: {}, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore, - }), - ).rejects.toThrow(`Environment variable "${missingEnvVar}" is missing or empty.`); - - const snapshot = await prepareSecretsRuntimeSnapshot({ - config: asConfig({}), - env: {}, - includeAuthStoreRefs: false, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore, - }); - - expect(snapshot.authStores).toEqual([]); - }); - it("resolves sandbox ssh secret refs for active ssh backends", async () => { const snapshot = await prepareSecretsRuntimeSnapshot({ config: asConfig({ @@ -324,512 +279,6 @@ describe("secrets runtime snapshot", () => { expect(profile.key).toBe("primary-key-value"); }); - it("resolves model provider request secret refs for headers, auth, and tls material", async () => { - const config = asConfig({ - models: { - providers: { - openai: { - baseUrl: "https://api.openai.com/v1", - request: { - headers: { - "X-Tenant": { source: "env", provider: "default", id: "OPENAI_PROVIDER_TENANT" }, - }, - auth: { - mode: "authorization-bearer", - token: { source: "env", provider: "default", id: "OPENAI_PROVIDER_TOKEN" }, - }, - proxy: { - mode: "explicit-proxy", - url: "http://proxy.example:8080", - tls: { - ca: { source: "env", provider: "default", id: "OPENAI_PROVIDER_PROXY_CA" }, - }, - }, - tls: { - cert: { source: "env", provider: "default", id: "OPENAI_PROVIDER_CERT" }, - key: { source: "env", provider: "default", id: "OPENAI_PROVIDER_KEY" }, - }, - }, - models: [], - }, - }, - }, - }); - - const snapshot = await prepareSecretsRuntimeSnapshot({ - config, - env: { - OPENAI_PROVIDER_TENANT: "tenant-acme", - OPENAI_PROVIDER_TOKEN: "sk-provider-runtime", // pragma: allowlist secret - OPENAI_PROVIDER_PROXY_CA: "proxy-ca", - OPENAI_PROVIDER_CERT: "client-cert", - OPENAI_PROVIDER_KEY: "client-key", - }, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => ({ version: 1, profiles: {} }), - }); - - expect(snapshot.config.models?.providers?.openai?.request).toEqual({ - headers: { - "X-Tenant": "tenant-acme", - }, - auth: { - mode: "authorization-bearer", - token: "sk-provider-runtime", - }, - proxy: { - mode: "explicit-proxy", - url: "http://proxy.example:8080", - tls: { - ca: "proxy-ca", - }, - }, - tls: { - cert: "client-cert", - key: "client-key", - }, - }); - }); - - it("resolves file refs via configured file provider", async () => { - if (process.platform === "win32") { - return; - } - const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-file-provider-")); - const secretsPath = path.join(root, "secrets.json"); - try { - await fs.writeFile( - secretsPath, - JSON.stringify( - { - providers: { - openai: { - apiKey: "sk-from-file-provider", // pragma: allowlist secret - }, - }, - }, - null, - 2, - ), - "utf8", - ); - await fs.chmod(secretsPath, 0o600); - - const config = asConfig({ - secrets: { - providers: { - default: { - source: "file", - path: secretsPath, - mode: "json", - }, - }, - defaults: { - file: "default", - }, - }, - models: { - providers: { - openai: { - baseUrl: "https://api.openai.com/v1", - apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" }, - models: [], - }, - }, - }, - }); - - const snapshot = await prepareSecretsRuntimeSnapshot({ - config, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => ({ version: 1, profiles: {} }), - }); - - expect(snapshot.config.models?.providers?.openai?.apiKey).toBe("sk-from-file-provider"); - } finally { - await fs.rm(root, { recursive: true, force: true }); - } - }); - - it("fails when file provider payload is not a JSON object", async () => { - if (process.platform === "win32") { - return; - } - const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-file-provider-bad-")); - const secretsPath = path.join(root, "secrets.json"); - try { - await fs.writeFile(secretsPath, JSON.stringify(["not-an-object"]), "utf8"); - await fs.chmod(secretsPath, 0o600); - - await expect( - prepareSecretsRuntimeSnapshot({ - config: asConfig({ - secrets: { - providers: { - default: { - source: "file", - path: secretsPath, - mode: "json", - }, - }, - }, - models: { - ...createOpenAiFileModelsConfig(), - }, - }), - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => ({ version: 1, profiles: {} }), - }), - ).rejects.toThrow("payload is not a JSON object"); - } finally { - await fs.rm(root, { recursive: true, force: true }); - } - }); - - it("resolves media request secret refs for provider headers, auth, and tls material", async () => { - const snapshot = await prepareSecretsRuntimeSnapshot({ - config: asConfig({ - tools: { - media: { - models: [ - { - provider: "openai", - model: "gpt-4o-mini-transcribe", - capabilities: ["audio"], - request: { - headers: { - "X-Shared-Tenant": { - source: "env", - provider: "default", - id: "MEDIA_SHARED_TENANT", - }, - }, - auth: { - mode: "header", - headerName: "x-shared-key", - value: { - source: "env", - provider: "default", - id: "MEDIA_SHARED_MODEL_KEY", - }, - }, - }, - }, - ], - audio: { - enabled: true, - request: { - headers: { - "X-Tenant": { source: "env", provider: "default", id: "MEDIA_AUDIO_TENANT" }, - }, - auth: { - mode: "authorization-bearer", - token: { source: "env", provider: "default", id: "MEDIA_AUDIO_TOKEN" }, - }, - tls: { - cert: { source: "env", provider: "default", id: "MEDIA_AUDIO_CERT" }, - }, - }, - models: [ - { - provider: "deepgram", - request: { - auth: { - mode: "header", - headerName: "x-api-key", - value: { source: "env", provider: "default", id: "MEDIA_AUDIO_MODEL_KEY" }, - }, - proxy: { - mode: "explicit-proxy", - url: "http://proxy.example:8080", - tls: { - ca: { source: "env", provider: "default", id: "MEDIA_AUDIO_PROXY_CA" }, - }, - }, - }, - }, - ], - }, - }, - }, - }), - env: { - MEDIA_SHARED_TENANT: "tenant-shared", - MEDIA_SHARED_MODEL_KEY: "shared-model-key", // pragma: allowlist secret - MEDIA_AUDIO_TENANT: "tenant-acme", - MEDIA_AUDIO_TOKEN: "audio-token", // pragma: allowlist secret - MEDIA_AUDIO_CERT: "client-cert", - MEDIA_AUDIO_MODEL_KEY: "model-key", // pragma: allowlist secret - MEDIA_AUDIO_PROXY_CA: "proxy-ca", - }, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => ({ version: 1, profiles: {} }), - }); - - expect(snapshot.config.tools?.media?.audio?.request?.headers?.["X-Tenant"]).toBe("tenant-acme"); - expect(snapshot.config.tools?.media?.audio?.request?.auth).toEqual({ - mode: "authorization-bearer", - token: "audio-token", - }); - expect(snapshot.config.tools?.media?.audio?.request?.tls).toEqual({ - cert: "client-cert", - }); - expect(snapshot.config.tools?.media?.models?.[0]?.request).toEqual({ - headers: { - "X-Shared-Tenant": "tenant-shared", - }, - auth: { - mode: "header", - headerName: "x-shared-key", - value: "shared-model-key", - }, - }); - expect(snapshot.config.tools?.media?.audio?.models?.[0]?.request).toEqual({ - auth: { - mode: "header", - headerName: "x-api-key", - value: "model-key", - }, - proxy: { - mode: "explicit-proxy", - url: "http://proxy.example:8080", - tls: { - ca: "proxy-ca", - }, - }, - }); - }); - - it("resolves shared media model request refs when capability blocks are omitted", async () => { - const snapshot = await prepareSecretsRuntimeSnapshot({ - config: asConfig({ - tools: { - media: { - models: [ - { - provider: "openai", - model: "gpt-4o-mini-transcribe", - capabilities: ["audio"], - request: { - auth: { - mode: "authorization-bearer", - token: { - source: "env", - provider: "default", - id: "MEDIA_SHARED_AUDIO_TOKEN", - }, - }, - }, - }, - ], - }, - }, - }), - env: { - MEDIA_SHARED_AUDIO_TOKEN: "shared-audio-token", // pragma: allowlist secret - }, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => ({ version: 1, profiles: {} }), - }); - - expect(snapshot.config.tools?.media?.models?.[0]?.request?.auth).toEqual({ - mode: "authorization-bearer", - token: "shared-audio-token", - }); - expect(snapshot.warnings.map((warning) => warning.path)).not.toContain( - "tools.media.models.0.request.auth.token", - ); - }); - - it("treats shared media model request refs as inactive when their capabilities are disabled", async () => { - const sharedTokenRef = { - source: "env" as const, - provider: "default" as const, - id: "MEDIA_DISABLED_AUDIO_TOKEN", - }; - const snapshot = await prepareSecretsRuntimeSnapshot({ - config: asConfig({ - tools: { - media: { - models: [ - { - provider: "openai", - model: "gpt-4o-mini-transcribe", - capabilities: ["audio"], - request: { - auth: { - mode: "authorization-bearer", - token: sharedTokenRef, - }, - }, - }, - ], - audio: { - enabled: false, - }, - }, - }, - }), - env: {}, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => ({ version: 1, profiles: {} }), - }); - - expect(snapshot.config.tools?.media?.models?.[0]?.request?.auth).toEqual({ - mode: "authorization-bearer", - token: sharedTokenRef, - }); - expect(snapshot.warnings.map((warning) => warning.path)).toContain( - "tools.media.models.0.request.auth.token", - ); - }); - - it("resolves shared media model request refs from inferred provider capabilities", async () => { - const pluginRegistry = createEmptyPluginRegistry(); - pluginRegistry.mediaUnderstandingProviders.push({ - pluginId: "deepgram", - pluginName: "Deepgram Plugin", - source: "test", - provider: { - id: "deepgram", - capabilities: ["audio"], - }, - }); - setActivePluginRegistry(pluginRegistry); - - const snapshot = await prepareSecretsRuntimeSnapshot({ - config: asConfig({ - tools: { - media: { - models: [ - { - provider: "deepgram", - request: { - auth: { - mode: "authorization-bearer", - token: { - source: "env", - provider: "default", - id: "MEDIA_INFERRED_AUDIO_TOKEN", - }, - }, - }, - }, - ], - }, - }, - }), - env: { - MEDIA_INFERRED_AUDIO_TOKEN: "inferred-audio-token", // pragma: allowlist secret - }, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => ({ version: 1, profiles: {} }), - }); - - expect(snapshot.config.tools?.media?.models?.[0]?.request?.auth).toEqual({ - mode: "authorization-bearer", - token: "inferred-audio-token", - }); - expect(snapshot.warnings.map((warning) => warning.path)).not.toContain( - "tools.media.models.0.request.auth.token", - ); - }); - - it("treats shared media model request refs as inactive when inferred capabilities are disabled", async () => { - const pluginRegistry = createEmptyPluginRegistry(); - pluginRegistry.mediaUnderstandingProviders.push({ - pluginId: "deepgram", - pluginName: "Deepgram Plugin", - source: "test", - provider: { - id: "deepgram", - capabilities: ["audio"], - }, - }); - setActivePluginRegistry(pluginRegistry); - - const inferredTokenRef = { - source: "env" as const, - provider: "default" as const, - id: "MEDIA_INFERRED_DISABLED_AUDIO_TOKEN", - }; - const snapshot = await prepareSecretsRuntimeSnapshot({ - config: asConfig({ - tools: { - media: { - models: [ - { - provider: "deepgram", - request: { - auth: { - mode: "authorization-bearer", - token: inferredTokenRef, - }, - }, - }, - ], - audio: { - enabled: false, - }, - }, - }, - }), - env: {}, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => ({ version: 1, profiles: {} }), - }); - - expect(snapshot.config.tools?.media?.models?.[0]?.request?.auth).toEqual({ - mode: "authorization-bearer", - token: inferredTokenRef, - }); - expect(snapshot.warnings.map((warning) => warning.path)).toContain( - "tools.media.models.0.request.auth.token", - ); - }); - - it("treats section media model request refs as inactive when model capabilities exclude the section", async () => { - const sectionTokenRef = { - source: "env" as const, - provider: "default" as const, - id: "MEDIA_AUDIO_SECTION_FILTERED_TOKEN", - }; - const snapshot = await prepareSecretsRuntimeSnapshot({ - config: asConfig({ - tools: { - media: { - audio: { - enabled: true, - models: [ - { - provider: "openai", - capabilities: ["video"], - request: { - auth: { - mode: "authorization-bearer", - token: sectionTokenRef, - }, - }, - }, - ], - }, - }, - }, - }), - env: {}, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => ({ version: 1, profiles: {} }), - }); - - expect(snapshot.config.tools?.media?.audio?.models?.[0]?.request?.auth).toEqual({ - mode: "authorization-bearer", - token: sectionTokenRef, - }); - expect(snapshot.warnings.map((warning) => warning.path)).toContain( - "tools.media.audio.models.0.request.auth.token", - ); - }); - it("fails when an active exec ref id contains traversal segments", async () => { await expect( prepareSecretsRuntimeSnapshot({ @@ -853,46 +302,6 @@ describe("secrets runtime snapshot", () => { ).rejects.toThrow(/must not include "\." or "\.\." path segments/i); }); - it("treats defaults memorySearch ref as inactive when all enabled agents disable memorySearch", async () => { - const snapshot = await prepareSecretsRuntimeSnapshot({ - config: asConfig({ - agents: { - defaults: { - memorySearch: { - remote: { - apiKey: { - source: "env", - provider: "default", - id: "DEFAULT_MEMORY_REMOTE_API_KEY", - }, - }, - }, - }, - list: [ - { - enabled: true, - memorySearch: { - enabled: false, - }, - }, - ], - }, - }), - env: {}, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => ({ version: 1, profiles: {} }), - }); - - expect(snapshot.config.agents?.defaults?.memorySearch?.remote?.apiKey).toEqual({ - source: "env", - provider: "default", - id: "DEFAULT_MEMORY_REMOTE_API_KEY", - }); - expect(snapshot.warnings.map((warning) => warning.path)).toContain( - "agents.defaults.memorySearch.remote.apiKey", - ); - }); - it("does not write inherited auth stores during runtime secret activation", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-runtime-")); const stateDir = path.join(root, ".openclaw");