From 73485c2300a4821db70869b0f34c8dc70804ace8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 6 Apr 2026 07:30:04 +0100 Subject: [PATCH] perf(secrets): trim runtime import walls --- src/gateway/server.impl.ts | 6 +- .../runtime-auth.integration.test-helpers.ts | 130 ++++++++++++++++++ src/secrets/runtime-command-secrets.ts | 39 ++++++ .../runtime-config-collectors-channels.ts | 3 + src/secrets/runtime.auth.integration.test.ts | 2 +- src/secrets/runtime.test.ts | 63 +-------- src/secrets/runtime.ts | 35 ----- 7 files changed, 183 insertions(+), 95 deletions(-) create mode 100644 src/secrets/runtime-auth.integration.test-helpers.ts create mode 100644 src/secrets/runtime-command-secrets.ts diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index 6e1f3fac718..3bc6a6e540e 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -59,7 +59,10 @@ import { createPluginRuntime } from "../plugins/runtime/index.js"; import type { PluginServicesHandle } from "../plugins/services.js"; import { getTotalQueueSize } from "../process/command-queue.js"; import type { RuntimeEnv } from "../runtime.js"; -import type { CommandSecretAssignment } from "../secrets/command-config.js"; +import { + resolveCommandSecretsFromActiveRuntimeSnapshot, + type CommandSecretAssignment, +} from "../secrets/runtime-command-secrets.js"; import { GATEWAY_AUTH_SURFACE_PATHS, evaluateGatewayAuthSurfaceStates, @@ -69,7 +72,6 @@ import { clearSecretsRuntimeSnapshot, getActiveSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot, - resolveCommandSecretsFromActiveRuntimeSnapshot, } from "../secrets/runtime.js"; import { onSessionLifecycleEvent } from "../sessions/session-lifecycle-events.js"; import { onSessionTranscriptUpdate } from "../sessions/transcript-events.js"; diff --git a/src/secrets/runtime-auth.integration.test-helpers.ts b/src/secrets/runtime-auth.integration.test-helpers.ts new file mode 100644 index 00000000000..1be41367614 --- /dev/null +++ b/src/secrets/runtime-auth.integration.test-helpers.ts @@ -0,0 +1,130 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { expect, vi } from "vitest"; +import { ensureAuthProfileStore, type AuthProfileStore } from "../agents/auth-profiles.js"; +import type { OpenClawConfig } from "../config/config.js"; +import { clearConfigCache, clearRuntimeConfigSnapshot, loadConfig } from "../config/config.js"; +import { captureEnv } from "../test-utils/env.js"; +import { clearSecretsRuntimeSnapshot } from "./runtime.js"; + +export const OPENAI_ENV_KEY_REF = { + source: "env", + provider: "default", + id: "OPENAI_API_KEY", +} as const; + +export const OPENAI_FILE_KEY_REF = { + source: "file", + provider: "default", + id: "/providers/openai/apiKey", +} as const; + +export const EMPTY_LOADABLE_PLUGIN_ORIGINS = new Map(); +export type SecretsRuntimeEnvSnapshot = ReturnType; + +const allowInsecureTempSecretFile = process.platform === "win32"; + +export function asConfig(value: unknown): OpenClawConfig { + return value as OpenClawConfig; +} + +export function loadAuthStoreWithProfiles( + profiles: AuthProfileStore["profiles"], +): AuthProfileStore { + return { + version: 1, + profiles, + }; +} + +export async function createOpenAIFileRuntimeFixture(home: string) { + const configDir = path.join(home, ".openclaw"); + const secretFile = path.join(configDir, "secrets.json"); + const agentDir = path.join(configDir, "agents", "main", "agent"); + const authStorePath = path.join(agentDir, "auth-profiles.json"); + + await fs.mkdir(agentDir, { recursive: true }); + await fs.chmod(configDir, 0o700).catch(() => {}); + await fs.writeFile( + secretFile, + `${JSON.stringify({ providers: { openai: { apiKey: "sk-file-runtime" } } }, null, 2)}\n`, + { encoding: "utf8", mode: 0o600 }, + ); + await fs.writeFile( + authStorePath, + `${JSON.stringify( + { + version: 1, + profiles: { + "openai:default": { + type: "api_key", + provider: "openai", + keyRef: OPENAI_FILE_KEY_REF, + }, + }, + }, + null, + 2, + )}\n`, + { encoding: "utf8", mode: 0o600 }, + ); + + return { + configDir, + secretFile, + agentDir, + }; +} + +export function createOpenAIFileRuntimeConfig(secretFile: string): OpenClawConfig { + return asConfig({ + secrets: { + providers: { + default: { + source: "file", + path: secretFile, + mode: "json", + ...(allowInsecureTempSecretFile ? { allowInsecurePath: true } : {}), + }, + }, + }, + models: { + providers: { + openai: { + baseUrl: "https://api.openai.com/v1", + apiKey: OPENAI_FILE_KEY_REF, + models: [], + }, + }, + }, + }); +} + +export function expectResolvedOpenAIRuntime(agentDir: string) { + expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime"); + expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({ + type: "api_key", + key: "sk-file-runtime", + }); +} + +export function beginSecretsRuntimeIsolationForTest(): SecretsRuntimeEnvSnapshot { + const envSnapshot = captureEnv([ + "OPENCLAW_BUNDLED_PLUGINS_DIR", + "OPENCLAW_DISABLE_BUNDLED_PLUGINS", + "OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE", + "OPENCLAW_VERSION", + ]); + delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; + process.env.OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE = "1"; + delete process.env.OPENCLAW_VERSION; + return envSnapshot; +} + +export function endSecretsRuntimeIsolationForTest(envSnapshot: SecretsRuntimeEnvSnapshot) { + vi.restoreAllMocks(); + envSnapshot.restore(); + clearSecretsRuntimeSnapshot(); + clearRuntimeConfigSnapshot(); + clearConfigCache(); +} diff --git a/src/secrets/runtime-command-secrets.ts b/src/secrets/runtime-command-secrets.ts new file mode 100644 index 00000000000..5630fc66531 --- /dev/null +++ b/src/secrets/runtime-command-secrets.ts @@ -0,0 +1,39 @@ +import { + collectCommandSecretAssignmentsFromSnapshot, + type CommandSecretAssignment, +} from "./command-config.js"; +import { getActiveSecretsRuntimeSnapshot } from "./runtime.js"; + +export type { CommandSecretAssignment } from "./command-config.js"; + +export function resolveCommandSecretsFromActiveRuntimeSnapshot(params: { + commandName: string; + targetIds: ReadonlySet; +}): { assignments: CommandSecretAssignment[]; diagnostics: string[]; inactiveRefPaths: string[] } { + const activeSnapshot = getActiveSecretsRuntimeSnapshot(); + if (!activeSnapshot) { + throw new Error("Secrets runtime snapshot is not active."); + } + if (params.targetIds.size === 0) { + return { assignments: [], diagnostics: [], inactiveRefPaths: [] }; + } + const inactiveRefPaths = [ + ...new Set( + activeSnapshot.warnings + .filter((warning) => warning.code === "SECRETS_REF_IGNORED_INACTIVE_SURFACE") + .map((warning) => warning.path), + ), + ]; + const resolved = collectCommandSecretAssignmentsFromSnapshot({ + sourceConfig: activeSnapshot.sourceConfig, + resolvedConfig: activeSnapshot.config, + commandName: params.commandName, + targetIds: params.targetIds, + inactiveRefPaths: new Set(inactiveRefPaths), + }); + return { + assignments: resolved.assignments, + diagnostics: resolved.diagnostics, + inactiveRefPaths, + }; +} diff --git a/src/secrets/runtime-config-collectors-channels.ts b/src/secrets/runtime-config-collectors-channels.ts index 1209e13fcab..fdd4b9ce43c 100644 --- a/src/secrets/runtime-config-collectors-channels.ts +++ b/src/secrets/runtime-config-collectors-channels.ts @@ -7,6 +7,9 @@ export function collectChannelConfigAssignments(params: { defaults: SecretDefaults | undefined; context: ResolverContext; }): void { + if (!params.config.channels || Object.keys(params.config.channels).length === 0) { + return; + } for (const plugin of iterateBootstrapChannelPlugins()) { plugin.secrets?.collectRuntimeConfigAssignments?.(params); } diff --git a/src/secrets/runtime.auth.integration.test.ts b/src/secrets/runtime.auth.integration.test.ts index 67cfb38fb1b..b06ba4be902 100644 --- a/src/secrets/runtime.auth.integration.test.ts +++ b/src/secrets/runtime.auth.integration.test.ts @@ -18,7 +18,7 @@ import { OPENAI_ENV_KEY_REF, OPENAI_FILE_KEY_REF, type SecretsRuntimeEnvSnapshot, -} from "./runtime.integration.test-helpers.js"; +} from "./runtime-auth.integration.test-helpers.js"; import { activateSecretsRuntimeSnapshot, getActiveSecretsRuntimeSnapshot, diff --git a/src/secrets/runtime.test.ts b/src/secrets/runtime.test.ts index 917cac3731b..464a5db4b66 100644 --- a/src/secrets/runtime.test.ts +++ b/src/secrets/runtime.test.ts @@ -135,7 +135,7 @@ describe("secrets runtime snapshot", () => { clearConfigCache(); }); - it("resolves env refs for config and auth profiles", async () => { + it("resolves core env refs for config and auth profiles", async () => { const config = asConfig({ agents: { defaults: { @@ -185,39 +185,6 @@ describe("secrets runtime snapshot", () => { password: { source: "env", provider: "default", id: "REMOTE_GATEWAY_PASSWORD" }, }, }, - channels: { - telegram: { - botToken: { source: "env", provider: "default", id: "TELEGRAM_BOT_TOKEN_REF" }, - webhookUrl: "https://example.test/telegram-webhook", - webhookSecret: { source: "env", provider: "default", id: "TELEGRAM_WEBHOOK_SECRET_REF" }, - accounts: { - work: { - botToken: { - source: "env", - provider: "default", - id: "TELEGRAM_WORK_BOT_TOKEN_REF", - }, - }, - }, - }, - slack: { - mode: "http", - signingSecret: { source: "env", provider: "default", id: "SLACK_SIGNING_SECRET_REF" }, - accounts: { - work: { - botToken: { source: "env", provider: "default", id: "SLACK_WORK_BOT_TOKEN_REF" }, - appToken: { source: "env", provider: "default", id: "SLACK_WORK_APP_TOKEN_REF" }, - }, - }, - }, - }, - tools: { - web: { - search: { - apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_API_KEY" }, - }, - }, - }, }); const snapshot = await prepareSecretsRuntimeSnapshot({ @@ -231,15 +198,9 @@ describe("secrets runtime snapshot", () => { TALK_PROVIDER_API_KEY: "talk-provider-ref-key", // pragma: allowlist secret REMOTE_GATEWAY_TOKEN: "remote-token-ref", REMOTE_GATEWAY_PASSWORD: "remote-password-ref", // pragma: allowlist secret - TELEGRAM_BOT_TOKEN_REF: "telegram-bot-ref", - TELEGRAM_WEBHOOK_SECRET_REF: "telegram-webhook-ref", // pragma: allowlist secret - TELEGRAM_WORK_BOT_TOKEN_REF: "telegram-work-ref", - SLACK_SIGNING_SECRET_REF: "slack-signing-ref", // pragma: allowlist secret - SLACK_WORK_BOT_TOKEN_REF: "slack-work-bot-ref", - SLACK_WORK_APP_TOKEN_REF: "slack-work-app-ref", - WEB_SEARCH_API_KEY: "web-search-ref", // pragma: allowlist secret }, agentDirs: ["/tmp/openclaw-agent-main"], + loadablePluginOrigins: new Map(), loadAuthStore: () => loadAuthStoreWithProfiles({ "openai:default": { @@ -272,23 +233,11 @@ describe("secrets runtime snapshot", () => { expect(snapshot.config.talk?.providers?.["acme-speech"]?.apiKey).toBe("talk-provider-ref-key"); expect(snapshot.config.gateway?.remote?.token).toBe("remote-token-ref"); expect(snapshot.config.gateway?.remote?.password).toBe("remote-password-ref"); - expect(snapshot.config.channels?.telegram?.botToken).toEqual({ - source: "env", - provider: "default", - id: "TELEGRAM_BOT_TOKEN_REF", - }); - expect(snapshot.config.channels?.telegram?.webhookSecret).toBe("telegram-webhook-ref"); - expect(snapshot.config.channels?.telegram?.accounts?.work?.botToken).toBe("telegram-work-ref"); - expect(snapshot.config.channels?.slack?.signingSecret).toBe("slack-signing-ref"); - expect(snapshot.config.channels?.slack?.accounts?.work?.botToken).toBe("slack-work-bot-ref"); - expect(snapshot.config.channels?.slack?.accounts?.work?.appToken).toEqual({ - source: "env", - provider: "default", - id: "SLACK_WORK_APP_TOKEN_REF", - }); - expect(snapshot.config.tools?.web?.search?.apiKey).toBe("web-search-ref"); expect(snapshot.warnings.map((warning) => warning.path)).toEqual( - expect.arrayContaining(["channels.slack.accounts.work.appToken"]), + expect.arrayContaining([ + "/tmp/openclaw-agent-main.auth-profiles.openai:default.key", + "/tmp/openclaw-agent-main.auth-profiles.github-copilot:default.token", + ]), ); expect(snapshot.authStores[0]?.store.profiles["openai:default"]).toMatchObject({ type: "api_key", diff --git a/src/secrets/runtime.ts b/src/secrets/runtime.ts index ee989ab48fc..632fd58d496 100644 --- a/src/secrets/runtime.ts +++ b/src/secrets/runtime.ts @@ -19,10 +19,6 @@ import { } from "../config/config.js"; import type { PluginOrigin } from "../plugins/types.js"; import { resolveUserPath } from "../utils.js"; -import { - collectCommandSecretAssignmentsFromSnapshot, - type CommandSecretAssignment, -} from "./command-config.js"; import { type SecretResolverWarning } from "./runtime-shared.js"; import { clearActiveRuntimeWebToolsMetadata, @@ -302,37 +298,6 @@ export function getActiveRuntimeWebToolsMetadata(): RuntimeWebToolsMetadata | nu return getActiveRuntimeWebToolsMetadataFromState(); } -export function resolveCommandSecretsFromActiveRuntimeSnapshot(params: { - commandName: string; - targetIds: ReadonlySet; -}): { assignments: CommandSecretAssignment[]; diagnostics: string[]; inactiveRefPaths: string[] } { - if (!activeSnapshot) { - throw new Error("Secrets runtime snapshot is not active."); - } - if (params.targetIds.size === 0) { - return { assignments: [], diagnostics: [], inactiveRefPaths: [] }; - } - const inactiveRefPaths = [ - ...new Set( - activeSnapshot.warnings - .filter((warning) => warning.code === "SECRETS_REF_IGNORED_INACTIVE_SURFACE") - .map((warning) => warning.path), - ), - ]; - const resolved = collectCommandSecretAssignmentsFromSnapshot({ - sourceConfig: activeSnapshot.sourceConfig, - resolvedConfig: activeSnapshot.config, - commandName: params.commandName, - targetIds: params.targetIds, - inactiveRefPaths: new Set(inactiveRefPaths), - }); - return { - assignments: resolved.assignments, - diagnostics: resolved.diagnostics, - inactiveRefPaths, - }; -} - export function clearSecretsRuntimeSnapshot(): void { clearActiveSecretsRuntimeState(); }