diff --git a/src/secrets/runtime-auth-store-inline-refs.test.ts b/src/secrets/runtime-auth-store-inline-refs.test.ts new file mode 100644 index 00000000000..5f5b7ad4d58 --- /dev/null +++ b/src/secrets/runtime-auth-store-inline-refs.test.ts @@ -0,0 +1,110 @@ +import { afterEach, beforeAll, describe, expect, it } from "vitest"; +import type { AuthProfileStore } from "../agents/auth-profiles.js"; +import type { OpenClawConfig } from "../config/config.js"; +import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js"; +import { + activateSecretsRuntimeSnapshot, + clearSecretsRuntimeSnapshot, + prepareSecretsRuntimeSnapshot, +} from "./runtime.js"; + +const EMPTY_LOADABLE_PLUGIN_ORIGINS = new Map(); + +function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore { + return { + version: 1, + profiles, + }; +} + +describe("secrets runtime snapshot inline auth-store refs", () => { + beforeAll(() => {}); + + afterEach(() => { + clearSecretsRuntimeSnapshot(); + clearRuntimeConfigSnapshot(); + clearConfigCache(); + }); + + it("normalizes inline SecretRef object on token to tokenRef", async () => { + const config: OpenClawConfig = { models: {}, secrets: {} }; + const snapshot = await prepareSecretsRuntimeSnapshot({ + config, + env: { MY_TOKEN: "resolved-token-value" }, + agentDirs: ["/tmp/openclaw-agent-main"], + loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS, + loadAuthStore: () => + loadAuthStoreWithProfiles({ + "custom:inline-token": { + type: "token", + provider: "custom", + token: { source: "env", provider: "default", id: "MY_TOKEN" } as unknown as string, + }, + }), + }); + + const profile = snapshot.authStores[0]?.store.profiles["custom:inline-token"] as Record< + string, + unknown + >; + expect(profile.tokenRef).toEqual({ source: "env", provider: "default", id: "MY_TOKEN" }); + activateSecretsRuntimeSnapshot(snapshot); + expect(profile.token).toBe("resolved-token-value"); + }); + + it("normalizes inline SecretRef object on key to keyRef", async () => { + const config: OpenClawConfig = { models: {}, secrets: {} }; + const snapshot = await prepareSecretsRuntimeSnapshot({ + config, + env: { MY_KEY: "resolved-key-value" }, + agentDirs: ["/tmp/openclaw-agent-main"], + loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS, + loadAuthStore: () => + loadAuthStoreWithProfiles({ + "custom:inline-key": { + type: "api_key", + provider: "custom", + key: { source: "env", provider: "default", id: "MY_KEY" } as unknown as string, + }, + }), + }); + + const profile = snapshot.authStores[0]?.store.profiles["custom:inline-key"] as Record< + string, + unknown + >; + expect(profile.keyRef).toEqual({ source: "env", provider: "default", id: "MY_KEY" }); + activateSecretsRuntimeSnapshot(snapshot); + expect(profile.key).toBe("resolved-key-value"); + }); + + it("keeps explicit keyRef when inline key SecretRef is also present", async () => { + const config: OpenClawConfig = { models: {}, secrets: {} }; + const snapshot = await prepareSecretsRuntimeSnapshot({ + config, + env: { + PRIMARY_KEY: "primary-key-value", + SHADOW_KEY: "shadow-key-value", + }, + agentDirs: ["/tmp/openclaw-agent-main"], + loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS, + loadAuthStore: () => + loadAuthStoreWithProfiles({ + "custom:explicit-keyref": { + type: "api_key", + provider: "custom", + keyRef: { source: "env", provider: "default", id: "PRIMARY_KEY" }, + key: { source: "env", provider: "default", id: "SHADOW_KEY" } as unknown as string, + }, + }), + }); + + const profile = snapshot.authStores[0]?.store.profiles["custom:explicit-keyref"] as Record< + string, + unknown + >; + expect(profile.keyRef).toEqual({ source: "env", provider: "default", id: "PRIMARY_KEY" }); + activateSecretsRuntimeSnapshot(snapshot); + expect(profile.key).toBe("primary-key-value"); + }); +}); diff --git a/src/secrets/runtime.test.ts b/src/secrets/runtime.test.ts index 17926055121..d7ab00e944b 100644 --- a/src/secrets/runtime.test.ts +++ b/src/secrets/runtime.test.ts @@ -1,123 +1,23 @@ -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 { AuthProfileStore } from "../agents/auth-profiles.js"; +import { afterEach, beforeAll, describe, expect, it } 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 }), - ]; -} - -const OPENAI_ENV_KEY_REF = { source: "env", provider: "default", id: "OPENAI_API_KEY" } as const; - +const EMPTY_LOADABLE_PLUGIN_ORIGINS = new Map(); let clearConfigCache: typeof import("../config/config.js").clearConfigCache; let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntimeConfigSnapshot; -let activateSecretsRuntimeSnapshot: typeof import("./runtime.js").activateSecretsRuntimeSnapshot; let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot; let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot; -function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore { - return { - version: 1, - profiles, - }; -} - describe("secrets runtime snapshot", () => { beforeAll(async () => { ({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js")); - ({ - activateSecretsRuntimeSnapshot, - clearSecretsRuntimeSnapshot, - prepareSecretsRuntimeSnapshot, - } = await import("./runtime.js")); - }); - - beforeEach(() => { - resolvePluginWebSearchProvidersMock.mockReset(); - resolvePluginWebSearchProvidersMock.mockReturnValue(buildTestWebSearchProviders()); + ({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js")); }); afterEach(() => { - setActivePluginRegistry(createEmptyPluginRegistry()); clearSecretsRuntimeSnapshot(); clearRuntimeConfigSnapshot(); clearConfigCache(); @@ -154,6 +54,8 @@ describe("secrets runtime snapshot", () => { SSH_CERTIFICATE_DATA: "SSH CERT", SSH_KNOWN_HOSTS_DATA: "example.com ssh-ed25519 AAAATEST", }, + includeAuthStoreRefs: false, + loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS, }); expect(snapshot.config.agents?.defaults?.sandbox?.ssh).toMatchObject({ @@ -179,6 +81,8 @@ describe("secrets runtime snapshot", () => { }, }), env: {}, + includeAuthStoreRefs: false, + loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS, }); expect(snapshot.config.agents?.defaults?.sandbox?.ssh?.identityData).toEqual({ @@ -196,89 +100,6 @@ describe("secrets runtime snapshot", () => { ); }); - it("normalizes inline SecretRef object on token to tokenRef", async () => { - const config: OpenClawConfig = { models: {}, secrets: {} }; - const snapshot = await prepareSecretsRuntimeSnapshot({ - config, - env: { MY_TOKEN: "resolved-token-value" }, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => - loadAuthStoreWithProfiles({ - "custom:inline-token": { - type: "token", - provider: "custom", - token: { source: "env", provider: "default", id: "MY_TOKEN" } as unknown as string, - }, - }), - }); - - const profile = snapshot.authStores[0]?.store.profiles["custom:inline-token"] as Record< - string, - unknown - >; - // tokenRef should be set from the inline SecretRef - expect(profile.tokenRef).toEqual({ source: "env", provider: "default", id: "MY_TOKEN" }); - // token should be resolved to the actual value after activation - activateSecretsRuntimeSnapshot(snapshot); - expect(profile.token).toBe("resolved-token-value"); - }); - - it("normalizes inline SecretRef object on key to keyRef", async () => { - const config: OpenClawConfig = { models: {}, secrets: {} }; - const snapshot = await prepareSecretsRuntimeSnapshot({ - config, - env: { MY_KEY: "resolved-key-value" }, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => - loadAuthStoreWithProfiles({ - "custom:inline-key": { - type: "api_key", - provider: "custom", - key: { source: "env", provider: "default", id: "MY_KEY" } as unknown as string, - }, - }), - }); - - const profile = snapshot.authStores[0]?.store.profiles["custom:inline-key"] as Record< - string, - unknown - >; - // keyRef should be set from the inline SecretRef - expect(profile.keyRef).toEqual({ source: "env", provider: "default", id: "MY_KEY" }); - // key should be resolved to the actual value after activation - activateSecretsRuntimeSnapshot(snapshot); - expect(profile.key).toBe("resolved-key-value"); - }); - - it("keeps explicit keyRef when inline key SecretRef is also present", async () => { - const config: OpenClawConfig = { models: {}, secrets: {} }; - const snapshot = await prepareSecretsRuntimeSnapshot({ - config, - env: { - PRIMARY_KEY: "primary-key-value", - SHADOW_KEY: "shadow-key-value", - }, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => - loadAuthStoreWithProfiles({ - "custom:explicit-keyref": { - type: "api_key", - provider: "custom", - keyRef: { source: "env", provider: "default", id: "PRIMARY_KEY" }, - key: { source: "env", provider: "default", id: "SHADOW_KEY" } as unknown as string, - }, - }), - }); - - const profile = snapshot.authStores[0]?.store.profiles["custom:explicit-keyref"] as Record< - string, - unknown - >; - expect(profile.keyRef).toEqual({ source: "env", provider: "default", id: "PRIMARY_KEY" }); - activateSecretsRuntimeSnapshot(snapshot); - expect(profile.key).toBe("primary-key-value"); - }); - it("fails when an active exec ref id contains traversal segments", async () => { await expect( prepareSecretsRuntimeSnapshot({ @@ -296,53 +117,11 @@ describe("secrets runtime snapshot", () => { }, }), env: {}, + includeAuthStoreRefs: false, agentDirs: ["/tmp/openclaw-agent-main"], loadAuthStore: () => ({ version: 1, profiles: {} }), + loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS, }), ).rejects.toThrow(/must not include "\." or "\.\." path segments/i); }); - - 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"); - const mainAgentDir = path.join(stateDir, "agents", "main", "agent"); - const workerStorePath = path.join(stateDir, "agents", "worker", "agent", "auth-profiles.json"); - const prevStateDir = process.env.OPENCLAW_STATE_DIR; - - try { - await fs.mkdir(mainAgentDir, { recursive: true }); - await fs.writeFile( - path.join(mainAgentDir, "auth-profiles.json"), - JSON.stringify({ - ...loadAuthStoreWithProfiles({ - "openai:default": { - type: "api_key", - provider: "openai", - keyRef: OPENAI_ENV_KEY_REF, - }, - }), - }), - "utf8", - ); - process.env.OPENCLAW_STATE_DIR = stateDir; - - await prepareSecretsRuntimeSnapshot({ - config: { - agents: { - list: [{ id: "worker" }], - }, - }, - env: { OPENAI_API_KEY: "sk-runtime-worker" }, // pragma: allowlist secret - }); - - await expect(fs.access(workerStorePath)).rejects.toMatchObject({ code: "ENOENT" }); - } finally { - if (prevStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = prevStateDir; - } - await fs.rm(root, { recursive: true, force: true }); - } - }); });