mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 17:51:22 +00:00
perf(secrets): trim runtime import walls
This commit is contained in:
@@ -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";
|
||||
|
||||
130
src/secrets/runtime-auth.integration.test-helpers.ts
Normal file
130
src/secrets/runtime-auth.integration.test-helpers.ts
Normal file
@@ -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<typeof captureEnv>;
|
||||
|
||||
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();
|
||||
}
|
||||
39
src/secrets/runtime-command-secrets.ts
Normal file
39
src/secrets/runtime-command-secrets.ts
Normal file
@@ -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<string>;
|
||||
}): { 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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string>;
|
||||
}): { 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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user