mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 17:51:22 +00:00
565 lines
18 KiB
TypeScript
565 lines
18 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
import type { AuthProfileStore } from "../agents/auth-profiles.js";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import type { PluginOrigin } from "../plugins/types.js";
|
|
import { getPath, setPathCreateStrict } from "./path-utils.js";
|
|
import { canonicalizeSecretTargetCoverageId } from "./target-registry-test-helpers.js";
|
|
|
|
type SecretRegistryEntry = {
|
|
id: string;
|
|
configFile: "openclaw.json" | "auth-profiles.json";
|
|
pathPattern: string;
|
|
refPathPattern?: string;
|
|
secretShape: "secret_input" | "sibling_ref";
|
|
expectedResolvedValue: "string";
|
|
authProfileType?: "api_key" | "token";
|
|
};
|
|
|
|
type SecretRefCredentialMatrix = {
|
|
entries: Array<{
|
|
id: string;
|
|
configFile: "openclaw.json" | "auth-profiles.json";
|
|
path: string;
|
|
refPath?: string;
|
|
secretShape: SecretRegistryEntry["secretShape"];
|
|
when?: {
|
|
type?: SecretRegistryEntry["authProfileType"];
|
|
};
|
|
}>;
|
|
};
|
|
|
|
function loadCoverageRegistryEntries(): SecretRegistryEntry[] {
|
|
const matrixPath = path.join(
|
|
process.cwd(),
|
|
"docs",
|
|
"reference",
|
|
"secretref-user-supplied-credentials-matrix.json",
|
|
);
|
|
const matrix = JSON.parse(fs.readFileSync(matrixPath, "utf8")) as SecretRefCredentialMatrix;
|
|
return matrix.entries.map((entry) => ({
|
|
id: entry.id,
|
|
configFile: entry.configFile,
|
|
pathPattern: entry.path,
|
|
...(entry.refPath ? { refPathPattern: entry.refPath } : {}),
|
|
secretShape: entry.secretShape,
|
|
expectedResolvedValue: "string",
|
|
...(entry.when?.type ? { authProfileType: entry.when.type } : {}),
|
|
}));
|
|
}
|
|
|
|
const COVERAGE_REGISTRY_ENTRIES = loadCoverageRegistryEntries();
|
|
const DEBUG_COVERAGE_BATCHES = process.env.OPENCLAW_DEBUG_RUNTIME_COVERAGE === "1";
|
|
const COVERAGE_LOADABLE_PLUGIN_ORIGINS =
|
|
buildCoverageLoadablePluginOrigins(COVERAGE_REGISTRY_ENTRIES);
|
|
|
|
let applyResolvedAssignments: typeof import("./runtime-shared.js").applyResolvedAssignments;
|
|
let collectAuthStoreAssignments: typeof import("./runtime-auth-collectors.js").collectAuthStoreAssignments;
|
|
let collectConfigAssignments: typeof import("./runtime-config-collectors.js").collectConfigAssignments;
|
|
let createResolverContext: typeof import("./runtime-shared.js").createResolverContext;
|
|
let resolveSecretRefValues: typeof import("./resolve.js").resolveSecretRefValues;
|
|
let resolveRuntimeWebTools: typeof import("./runtime-web-tools.js").resolveRuntimeWebTools;
|
|
|
|
async function ensureConfigCoverageRuntimeLoaded(): Promise<void> {
|
|
if (!collectConfigAssignments) {
|
|
({ collectConfigAssignments } = await import("./runtime-config-collectors.js"));
|
|
}
|
|
}
|
|
|
|
async function ensureAuthCoverageRuntimeLoaded(): Promise<void> {
|
|
if (!collectAuthStoreAssignments) {
|
|
({ collectAuthStoreAssignments } = await import("./runtime-auth-collectors.js"));
|
|
}
|
|
}
|
|
|
|
async function ensureRuntimeWebToolsLoaded(): Promise<void> {
|
|
if (!resolveRuntimeWebTools) {
|
|
({ resolveRuntimeWebTools } = await import("./runtime-web-tools.js"));
|
|
}
|
|
}
|
|
|
|
function toConcretePathSegments(pathPattern: string, wildcardToken = "sample"): string[] {
|
|
const segments = pathPattern.split(".").filter(Boolean);
|
|
const out: string[] = [];
|
|
for (const segment of segments) {
|
|
if (segment === "*") {
|
|
out.push(wildcardToken);
|
|
continue;
|
|
}
|
|
if (segment.endsWith("[]")) {
|
|
out.push(segment.slice(0, -2), "0");
|
|
continue;
|
|
}
|
|
out.push(segment);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function resolveCoverageEnvId(entry: SecretRegistryEntry, fallbackEnvId: string): string {
|
|
return entry.id === "plugins.entries.firecrawl.config.webFetch.apiKey" ||
|
|
entry.id === "tools.web.fetch.firecrawl.apiKey"
|
|
? "FIRECRAWL_API_KEY"
|
|
: fallbackEnvId;
|
|
}
|
|
|
|
function resolveCoverageResolvedPath(entry: SecretRegistryEntry): string {
|
|
return canonicalizeSecretTargetCoverageId(entry.id);
|
|
}
|
|
|
|
function resolveCoverageWildcardToken(index: number): string {
|
|
return `sample-${index}`;
|
|
}
|
|
|
|
function resolveCoverageResolvedSegments(
|
|
entry: SecretRegistryEntry,
|
|
wildcardToken: string,
|
|
): string[] {
|
|
return toConcretePathSegments(resolveCoverageResolvedPath(entry), wildcardToken);
|
|
}
|
|
|
|
function buildCoverageLoadablePluginOrigins(
|
|
entries: readonly SecretRegistryEntry[],
|
|
): ReadonlyMap<string, PluginOrigin> {
|
|
const origins = new Map<string, PluginOrigin>();
|
|
for (const entry of entries) {
|
|
const [scope, entriesKey, pluginId] = entry.id.split(".");
|
|
if (scope === "plugins" && entriesKey === "entries" && pluginId) {
|
|
origins.set(pluginId, "bundled");
|
|
}
|
|
}
|
|
return origins;
|
|
}
|
|
|
|
function resolveCoverageBatchKey(entry: SecretRegistryEntry): string {
|
|
if (entry.id.startsWith("agents.defaults.")) {
|
|
return entry.id;
|
|
}
|
|
if (entry.id.startsWith("agents.list[].")) {
|
|
return entry.id;
|
|
}
|
|
if (entry.id.startsWith("gateway.auth.")) {
|
|
return entry.id;
|
|
}
|
|
if (entry.id.startsWith("gateway.remote.")) {
|
|
return entry.id;
|
|
}
|
|
if (entry.id.startsWith("models.providers.*.request.auth.")) {
|
|
return entry.id;
|
|
}
|
|
if (entry.id.startsWith("channels.")) {
|
|
const segments = entry.id.split(".");
|
|
const channelId = segments[1] ?? "unknown";
|
|
const field = segments.at(-1);
|
|
if (
|
|
field === "accessToken" ||
|
|
field === "password" ||
|
|
(channelId === "slack" &&
|
|
(field === "appToken" ||
|
|
field === "botToken" ||
|
|
field === "signingSecret" ||
|
|
field === "userToken"))
|
|
) {
|
|
return entry.id;
|
|
}
|
|
const scope = segments[2] === "accounts" ? "accounts" : "root";
|
|
return `channels.${channelId}.${scope}`;
|
|
}
|
|
if (entry.id.startsWith("messages.tts.providers.")) {
|
|
return "messages.tts.providers";
|
|
}
|
|
if (entry.id.startsWith("models.providers.")) {
|
|
return "models.providers";
|
|
}
|
|
if (entry.id.startsWith("plugins.entries.")) {
|
|
return entry.id;
|
|
}
|
|
if (entry.id.startsWith("skills.entries.")) {
|
|
return "skills.entries";
|
|
}
|
|
if (entry.id.startsWith("talk.providers.")) {
|
|
return "talk.providers";
|
|
}
|
|
if (entry.id.startsWith("talk.")) {
|
|
return "talk";
|
|
}
|
|
return entry.id;
|
|
}
|
|
|
|
function buildCoverageBatches(entries: readonly SecretRegistryEntry[]): SecretRegistryEntry[][] {
|
|
const batches = new Map<string, SecretRegistryEntry[]>();
|
|
for (const entry of entries) {
|
|
const batchKey = resolveCoverageBatchKey(entry);
|
|
const batch = batches.get(batchKey);
|
|
if (batch) {
|
|
batch.push(entry);
|
|
continue;
|
|
}
|
|
batches.set(batchKey, [entry]);
|
|
}
|
|
return [...batches.values()];
|
|
}
|
|
|
|
function logCoverageBatch(label: string, batch: readonly SecretRegistryEntry[]): void {
|
|
if (!DEBUG_COVERAGE_BATCHES || batch.length === 0) {
|
|
return;
|
|
}
|
|
process.stderr.write(
|
|
`[runtime.coverage] ${label} batch (${batch.length}): ${batch.map((entry) => entry.id).join(", ")}\n`,
|
|
);
|
|
}
|
|
|
|
function batchNeedsRuntimeWebTools(batch: readonly SecretRegistryEntry[]): boolean {
|
|
return batch.some(
|
|
(entry) =>
|
|
entry.id.startsWith("tools.web.") ||
|
|
(entry.id.startsWith("plugins.entries.") &&
|
|
(entry.id.includes(".config.webSearch.") || entry.id.includes(".config.webFetch."))),
|
|
);
|
|
}
|
|
|
|
function applyConfigForOpenClawTarget(
|
|
config: OpenClawConfig,
|
|
entry: SecretRegistryEntry,
|
|
envId: string,
|
|
wildcardToken: string,
|
|
): void {
|
|
const resolvedEnvId = resolveCoverageEnvId(entry, envId);
|
|
const refTargetPath =
|
|
entry.secretShape === "sibling_ref" && entry.refPathPattern // pragma: allowlist secret
|
|
? entry.refPathPattern
|
|
: entry.pathPattern;
|
|
setPathCreateStrict(config, toConcretePathSegments(refTargetPath, wildcardToken), {
|
|
source: "env",
|
|
provider: "default",
|
|
id: resolvedEnvId,
|
|
});
|
|
if (entry.id.startsWith("models.providers.")) {
|
|
setPathCreateStrict(
|
|
config,
|
|
["models", "providers", wildcardToken, "baseUrl"],
|
|
"https://api.example/v1",
|
|
);
|
|
setPathCreateStrict(config, ["models", "providers", wildcardToken, "models"], []);
|
|
}
|
|
if (entry.id.startsWith("plugins.entries.")) {
|
|
const pluginId = entry.id.split(".")[2];
|
|
if (pluginId) {
|
|
setPathCreateStrict(config, ["plugins", "entries", pluginId, "enabled"], true);
|
|
}
|
|
}
|
|
if (entry.id === "agents.defaults.memorySearch.remote.apiKey") {
|
|
setPathCreateStrict(config, ["agents", "list", "0", "id"], "sample-agent");
|
|
}
|
|
if (entry.id === "gateway.auth.password") {
|
|
setPathCreateStrict(config, ["gateway", "auth", "mode"], "password");
|
|
}
|
|
if (entry.id === "gateway.remote.token" || entry.id === "gateway.remote.password") {
|
|
setPathCreateStrict(config, ["gateway", "mode"], "remote");
|
|
setPathCreateStrict(config, ["gateway", "remote", "url"], "wss://gateway.example");
|
|
}
|
|
if (entry.id === "channels.telegram.webhookSecret") {
|
|
setPathCreateStrict(config, ["channels", "telegram", "webhookUrl"], "https://example.com/hook");
|
|
}
|
|
if (entry.id === "channels.telegram.accounts.*.webhookSecret") {
|
|
setPathCreateStrict(
|
|
config,
|
|
["channels", "telegram", "accounts", wildcardToken, "webhookUrl"],
|
|
"https://example.com/hook",
|
|
);
|
|
}
|
|
if (entry.id === "channels.slack.signingSecret") {
|
|
setPathCreateStrict(config, ["channels", "slack", "mode"], "http");
|
|
}
|
|
if (entry.id === "channels.slack.accounts.*.signingSecret") {
|
|
setPathCreateStrict(config, ["channels", "slack", "accounts", wildcardToken, "mode"], "http");
|
|
}
|
|
if (entry.id === "channels.zalo.webhookSecret") {
|
|
setPathCreateStrict(config, ["channels", "zalo", "webhookUrl"], "https://example.com/hook");
|
|
}
|
|
if (entry.id === "channels.zalo.accounts.*.webhookSecret") {
|
|
setPathCreateStrict(
|
|
config,
|
|
["channels", "zalo", "accounts", wildcardToken, "webhookUrl"],
|
|
"https://example.com/hook",
|
|
);
|
|
}
|
|
if (entry.id === "channels.feishu.verificationToken") {
|
|
setPathCreateStrict(config, ["channels", "feishu", "connectionMode"], "webhook");
|
|
}
|
|
if (entry.id === "channels.feishu.encryptKey") {
|
|
setPathCreateStrict(config, ["channels", "feishu", "connectionMode"], "webhook");
|
|
}
|
|
if (entry.id === "channels.feishu.accounts.*.verificationToken") {
|
|
setPathCreateStrict(
|
|
config,
|
|
["channels", "feishu", "accounts", wildcardToken, "connectionMode"],
|
|
"webhook",
|
|
);
|
|
}
|
|
if (entry.id === "channels.feishu.accounts.*.encryptKey") {
|
|
setPathCreateStrict(
|
|
config,
|
|
["channels", "feishu", "accounts", wildcardToken, "connectionMode"],
|
|
"webhook",
|
|
);
|
|
}
|
|
if (entry.id === "plugins.entries.brave.config.webSearch.apiKey") {
|
|
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "brave");
|
|
}
|
|
if (entry.id === "plugins.entries.google.config.webSearch.apiKey") {
|
|
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "gemini");
|
|
}
|
|
if (entry.id === "plugins.entries.xai.config.webSearch.apiKey") {
|
|
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "grok");
|
|
}
|
|
if (entry.id === "plugins.entries.moonshot.config.webSearch.apiKey") {
|
|
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "kimi");
|
|
}
|
|
if (entry.id === "plugins.entries.perplexity.config.webSearch.apiKey") {
|
|
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "perplexity");
|
|
}
|
|
if (entry.id === "plugins.entries.firecrawl.config.webSearch.apiKey") {
|
|
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "firecrawl");
|
|
}
|
|
if (entry.id === "plugins.entries.minimax.config.webSearch.apiKey") {
|
|
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "minimax");
|
|
}
|
|
if (entry.id === "plugins.entries.tavily.config.webSearch.apiKey") {
|
|
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "tavily");
|
|
}
|
|
if (entry.id === "models.providers.*.request.auth.token") {
|
|
setPathCreateStrict(
|
|
config,
|
|
["models", "providers", wildcardToken, "request", "auth", "mode"],
|
|
"authorization-bearer",
|
|
);
|
|
}
|
|
if (entry.id === "models.providers.*.request.auth.value") {
|
|
setPathCreateStrict(
|
|
config,
|
|
["models", "providers", wildcardToken, "request", "auth", "mode"],
|
|
"header",
|
|
);
|
|
setPathCreateStrict(
|
|
config,
|
|
["models", "providers", wildcardToken, "request", "auth", "headerName"],
|
|
"x-api-key",
|
|
);
|
|
}
|
|
if (entry.id.startsWith("models.providers.*.request.proxy.tls.")) {
|
|
setPathCreateStrict(
|
|
config,
|
|
["models", "providers", wildcardToken, "request", "proxy", "mode"],
|
|
"explicit-proxy",
|
|
);
|
|
setPathCreateStrict(
|
|
config,
|
|
["models", "providers", wildcardToken, "request", "proxy", "url"],
|
|
"http://proxy.example:8080",
|
|
);
|
|
}
|
|
}
|
|
|
|
function applyAuthStoreTarget(
|
|
store: AuthProfileStore,
|
|
entry: SecretRegistryEntry,
|
|
envId: string,
|
|
wildcardToken: string,
|
|
): void {
|
|
if (entry.authProfileType === "token") {
|
|
setPathCreateStrict(store, ["profiles", wildcardToken], {
|
|
type: "token" as const,
|
|
provider: "sample-provider",
|
|
token: "legacy-token",
|
|
tokenRef: {
|
|
source: "env" as const,
|
|
provider: "default",
|
|
id: envId,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
setPathCreateStrict(store, ["profiles", wildcardToken], {
|
|
type: "api_key" as const,
|
|
provider: "sample-provider",
|
|
key: "legacy-key",
|
|
keyRef: {
|
|
source: "env" as const,
|
|
provider: "default",
|
|
id: envId,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function prepareConfigCoverageSnapshot(params: {
|
|
config: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
loadablePluginOrigins?: ReadonlyMap<string, PluginOrigin>;
|
|
includeRuntimeWebTools?: boolean;
|
|
}) {
|
|
await ensureConfigCoverageRuntimeLoaded();
|
|
const sourceConfig = structuredClone(params.config);
|
|
const resolvedConfig = structuredClone(params.config);
|
|
const context = createResolverContext({
|
|
sourceConfig,
|
|
env: params.env,
|
|
});
|
|
|
|
collectConfigAssignments({
|
|
config: resolvedConfig,
|
|
context,
|
|
loadablePluginOrigins: params.loadablePluginOrigins,
|
|
});
|
|
|
|
if (context.assignments.length > 0) {
|
|
const resolved = await resolveSecretRefValues(
|
|
context.assignments.map((assignment) => assignment.ref),
|
|
{
|
|
config: sourceConfig,
|
|
env: context.env,
|
|
cache: context.cache,
|
|
},
|
|
);
|
|
applyResolvedAssignments({
|
|
assignments: context.assignments,
|
|
resolved,
|
|
});
|
|
}
|
|
|
|
if (params.includeRuntimeWebTools) {
|
|
await ensureRuntimeWebToolsLoaded();
|
|
await resolveRuntimeWebTools({
|
|
sourceConfig,
|
|
resolvedConfig,
|
|
context,
|
|
});
|
|
}
|
|
|
|
return {
|
|
config: resolvedConfig,
|
|
warnings: context.warnings,
|
|
};
|
|
}
|
|
|
|
async function prepareAuthCoverageSnapshot(params: {
|
|
config: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
agentDirs: string[];
|
|
loadAuthStore: (agentDir?: string) => AuthProfileStore;
|
|
}) {
|
|
await ensureAuthCoverageRuntimeLoaded();
|
|
const sourceConfig = structuredClone(params.config);
|
|
const context = createResolverContext({
|
|
sourceConfig,
|
|
env: params.env,
|
|
});
|
|
|
|
const authStores = params.agentDirs.map((agentDir) => {
|
|
const store = structuredClone(params.loadAuthStore(agentDir));
|
|
collectAuthStoreAssignments({
|
|
store,
|
|
context,
|
|
agentDir,
|
|
});
|
|
return { agentDir, store };
|
|
});
|
|
|
|
if (context.assignments.length > 0) {
|
|
const resolved = await resolveSecretRefValues(
|
|
context.assignments.map((assignment) => assignment.ref),
|
|
{
|
|
config: sourceConfig,
|
|
env: context.env,
|
|
cache: context.cache,
|
|
},
|
|
);
|
|
applyResolvedAssignments({
|
|
assignments: context.assignments,
|
|
resolved,
|
|
});
|
|
}
|
|
|
|
return {
|
|
authStores,
|
|
warnings: context.warnings,
|
|
};
|
|
}
|
|
|
|
describe("secrets runtime target coverage", () => {
|
|
beforeAll(async () => {
|
|
const [sharedRuntime, resolver] = await Promise.all([
|
|
import("./runtime-shared.js"),
|
|
import("./resolve.js"),
|
|
]);
|
|
({ applyResolvedAssignments, createResolverContext } = sharedRuntime);
|
|
({ resolveSecretRefValues } = resolver);
|
|
});
|
|
|
|
it("handles every openclaw.json registry target when configured as active", async () => {
|
|
const entries = COVERAGE_REGISTRY_ENTRIES.filter(
|
|
(entry) => entry.configFile === "openclaw.json",
|
|
);
|
|
for (const batch of buildCoverageBatches(entries)) {
|
|
logCoverageBatch("openclaw.json", batch);
|
|
const config = {} as OpenClawConfig;
|
|
const env: Record<string, string> = {};
|
|
for (const [index, entry] of batch.entries()) {
|
|
const envId = `OPENCLAW_SECRET_TARGET_${entry.id}`;
|
|
const runtimeEnvId = resolveCoverageEnvId(entry, envId);
|
|
const expectedValue = `resolved-${entry.id}`;
|
|
const wildcardToken = resolveCoverageWildcardToken(index);
|
|
env[runtimeEnvId] = expectedValue;
|
|
applyConfigForOpenClawTarget(config, entry, envId, wildcardToken);
|
|
}
|
|
const snapshot = await prepareConfigCoverageSnapshot({
|
|
config,
|
|
env,
|
|
loadablePluginOrigins: COVERAGE_LOADABLE_PLUGIN_ORIGINS,
|
|
includeRuntimeWebTools: batchNeedsRuntimeWebTools(batch),
|
|
});
|
|
for (const [index, entry] of batch.entries()) {
|
|
const resolved = getPath(
|
|
snapshot.config,
|
|
resolveCoverageResolvedSegments(entry, resolveCoverageWildcardToken(index)),
|
|
);
|
|
expect(resolved).toBe(`resolved-${entry.id}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("handles every auth-profiles registry target", async () => {
|
|
const entries = COVERAGE_REGISTRY_ENTRIES.filter(
|
|
(entry) => entry.configFile === "auth-profiles.json",
|
|
);
|
|
for (const batch of buildCoverageBatches(entries)) {
|
|
logCoverageBatch("auth-profiles.json", batch);
|
|
const env: Record<string, string> = {};
|
|
const authStore: AuthProfileStore = {
|
|
version: 1,
|
|
profiles: {},
|
|
};
|
|
for (const [index, entry] of batch.entries()) {
|
|
const envId = `OPENCLAW_AUTH_SECRET_TARGET_${entry.id}`;
|
|
env[envId] = `resolved-${entry.id}`;
|
|
applyAuthStoreTarget(authStore, entry, envId, resolveCoverageWildcardToken(index));
|
|
}
|
|
const snapshot = await prepareAuthCoverageSnapshot({
|
|
config: {} as OpenClawConfig,
|
|
env,
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => authStore,
|
|
});
|
|
const resolvedStore = snapshot.authStores[0]?.store;
|
|
expect(resolvedStore).toBeDefined();
|
|
for (const [index, entry] of batch.entries()) {
|
|
const resolved = getPath(
|
|
resolvedStore,
|
|
toConcretePathSegments(entry.pathPattern, resolveCoverageWildcardToken(index)),
|
|
);
|
|
expect(resolved).toBe(`resolved-${entry.id}`);
|
|
}
|
|
}
|
|
});
|
|
});
|