mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
271 lines
8.1 KiB
TypeScript
271 lines
8.1 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
|
import { loadConfig, type OpenClawConfig } from "../config/config.js";
|
|
import {
|
|
activateSecretsRuntimeSnapshot,
|
|
clearSecretsRuntimeSnapshot,
|
|
prepareSecretsRuntimeSnapshot,
|
|
} from "./runtime.js";
|
|
|
|
describe("secrets runtime snapshot", () => {
|
|
afterEach(() => {
|
|
clearSecretsRuntimeSnapshot();
|
|
});
|
|
|
|
it("resolves env refs for config and auth profiles", async () => {
|
|
const config: OpenClawConfig = {
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
|
models: [],
|
|
},
|
|
},
|
|
},
|
|
skills: {
|
|
entries: {
|
|
"review-pr": {
|
|
enabled: true,
|
|
apiKey: { source: "env", provider: "default", id: "REVIEW_SKILL_API_KEY" },
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const snapshot = await prepareSecretsRuntimeSnapshot({
|
|
config,
|
|
env: {
|
|
OPENAI_API_KEY: "sk-env-openai",
|
|
GITHUB_TOKEN: "ghp-env-token",
|
|
REVIEW_SKILL_API_KEY: "sk-skill-ref",
|
|
},
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({
|
|
version: 1,
|
|
profiles: {
|
|
"openai:default": {
|
|
type: "api_key",
|
|
provider: "openai",
|
|
key: "old-openai",
|
|
keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
|
},
|
|
"github-copilot:default": {
|
|
type: "token",
|
|
provider: "github-copilot",
|
|
token: "old-gh",
|
|
tokenRef: { source: "env", provider: "default", id: "GITHUB_TOKEN" },
|
|
},
|
|
"openai:inline": {
|
|
type: "api_key",
|
|
provider: "openai",
|
|
key: "${OPENAI_API_KEY}",
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(snapshot.config.models?.providers?.openai?.apiKey).toBe("sk-env-openai");
|
|
expect(snapshot.config.skills?.entries?.["review-pr"]?.apiKey).toBe("sk-skill-ref");
|
|
expect(snapshot.warnings).toHaveLength(2);
|
|
expect(snapshot.authStores[0]?.store.profiles["openai:default"]).toMatchObject({
|
|
type: "api_key",
|
|
key: "sk-env-openai",
|
|
});
|
|
expect(snapshot.authStores[0]?.store.profiles["github-copilot:default"]).toMatchObject({
|
|
type: "token",
|
|
token: "ghp-env-token",
|
|
});
|
|
expect(snapshot.authStores[0]?.store.profiles["openai:inline"]).toMatchObject({
|
|
type: "api_key",
|
|
key: "sk-env-openai",
|
|
});
|
|
});
|
|
|
|
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: OpenClawConfig = {
|
|
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: {
|
|
secrets: {
|
|
providers: {
|
|
default: {
|
|
source: "file",
|
|
path: secretsPath,
|
|
mode: "json",
|
|
},
|
|
},
|
|
},
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
|
|
models: [],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
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("activates runtime snapshots for loadConfig and ensureAuthProfileStore", async () => {
|
|
const prepared = await prepareSecretsRuntimeSnapshot({
|
|
config: {
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
|
models: [],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
env: { OPENAI_API_KEY: "sk-runtime" },
|
|
agentDirs: ["/tmp/openclaw-agent-main"],
|
|
loadAuthStore: () => ({
|
|
version: 1,
|
|
profiles: {
|
|
"openai:default": {
|
|
type: "api_key",
|
|
provider: "openai",
|
|
keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
|
|
activateSecretsRuntimeSnapshot(prepared);
|
|
|
|
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-runtime");
|
|
const store = ensureAuthProfileStore("/tmp/openclaw-agent-main");
|
|
expect(store.profiles["openai:default"]).toMatchObject({
|
|
type: "api_key",
|
|
key: "sk-runtime",
|
|
});
|
|
});
|
|
|
|
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({
|
|
version: 1,
|
|
profiles: {
|
|
"openai:default": {
|
|
type: "api_key",
|
|
provider: "openai",
|
|
keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
|
},
|
|
},
|
|
}),
|
|
"utf8",
|
|
);
|
|
process.env.OPENCLAW_STATE_DIR = stateDir;
|
|
|
|
await prepareSecretsRuntimeSnapshot({
|
|
config: {
|
|
agents: {
|
|
list: [{ id: "worker" }],
|
|
},
|
|
},
|
|
env: { OPENAI_API_KEY: "sk-runtime-worker" },
|
|
});
|
|
|
|
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 });
|
|
}
|
|
});
|
|
});
|