mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 01:20:42 +00:00
* Config: add inspect/strict SecretRef string resolver * CLI: pass resolved/source config snapshots to plugin preload * Slack: keep HTTP route registration config-only * Providers: normalize SecretRef handling for auth and web tools * Secrets: add Exa web search target to registry and docs * Telegram: resolve env SecretRef tokens at runtime * Agents: resolve custom provider env SecretRef ids * Providers: fail closed on blocked SecretRef fallback * Telegram: enforce env SecretRef policy for runtime token refs * Status/Providers/Telegram: tighten SecretRef preload and fallback handling * Providers: enforce env SecretRef policy checks in fallback auth paths * fix: add SecretRef lifecycle changelog entry (#66818) (thanks @joshavant)
179 lines
6.3 KiB
TypeScript
179 lines
6.3 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
const REGISTRY_IDS = [
|
|
"agents.defaults.memorySearch.remote.apiKey",
|
|
"agents.list[].memorySearch.remote.apiKey",
|
|
"channels.discord.token",
|
|
"channels.discord.accounts.ops.token",
|
|
"channels.discord.accounts.chat.token",
|
|
"channels.telegram.botToken",
|
|
"gateway.auth.token",
|
|
"gateway.auth.password",
|
|
"gateway.remote.token",
|
|
"gateway.remote.password",
|
|
"models.providers.openai.apiKey",
|
|
"messages.tts.providers.openai.apiKey",
|
|
"plugins.entries.firecrawl.config.webFetch.apiKey",
|
|
"plugins.entries.exa.config.webSearch.apiKey",
|
|
"skills.entries.demo.apiKey",
|
|
"tools.web.search.apiKey",
|
|
] as const;
|
|
|
|
vi.mock("../secrets/target-registry.js", () => ({
|
|
listSecretTargetRegistryEntries: vi.fn(() =>
|
|
REGISTRY_IDS.map((id) => ({
|
|
id,
|
|
})),
|
|
),
|
|
discoverConfigSecretTargetsByIds: vi.fn((config: unknown, targetIds?: Iterable<string>) => {
|
|
const allowed = targetIds ? new Set(targetIds) : null;
|
|
const out: Array<{ path: string; pathSegments: string[] }> = [];
|
|
const record = (path: string) => {
|
|
if (allowed && !allowed.has(path)) {
|
|
return;
|
|
}
|
|
out.push({ path, pathSegments: path.split(".") });
|
|
};
|
|
|
|
const channels = (config as { channels?: Record<string, unknown> } | undefined)?.channels;
|
|
const discord = channels?.discord as
|
|
| { token?: unknown; accounts?: Record<string, { token?: unknown }> }
|
|
| undefined;
|
|
|
|
if (discord?.token !== undefined) {
|
|
record("channels.discord.token");
|
|
}
|
|
for (const [accountId, account] of Object.entries(discord?.accounts ?? {})) {
|
|
if (account?.token !== undefined) {
|
|
record(`channels.discord.accounts.${accountId}.token`);
|
|
}
|
|
}
|
|
return out;
|
|
}),
|
|
}));
|
|
|
|
import {
|
|
getAgentRuntimeCommandSecretTargetIds,
|
|
getModelsCommandSecretTargetIds,
|
|
getQrRemoteCommandSecretTargetIds,
|
|
getScopedChannelsCommandSecretTargets,
|
|
getSecurityAuditCommandSecretTargetIds,
|
|
} from "./command-secret-targets.js";
|
|
|
|
describe("command secret target ids", () => {
|
|
it("keeps static qr remote targets out of the registry path", () => {
|
|
const ids = getQrRemoteCommandSecretTargetIds();
|
|
expect(ids).toEqual(new Set(["gateway.remote.token", "gateway.remote.password"]));
|
|
});
|
|
|
|
it("keeps static model targets out of the registry path", () => {
|
|
const ids = getModelsCommandSecretTargetIds();
|
|
expect(ids.has("models.providers.*.apiKey")).toBe(true);
|
|
expect(ids.has("models.providers.*.request.tls.key")).toBe(true);
|
|
expect(ids.has("channels.discord.token")).toBe(false);
|
|
});
|
|
|
|
it("includes memorySearch remote targets for agent runtime commands", () => {
|
|
const ids = getAgentRuntimeCommandSecretTargetIds();
|
|
expect(ids.has("agents.defaults.memorySearch.remote.apiKey")).toBe(true);
|
|
expect(ids.has("agents.list[].memorySearch.remote.apiKey")).toBe(true);
|
|
expect(ids.has("plugins.entries.firecrawl.config.webFetch.apiKey")).toBe(true);
|
|
expect(ids.has("plugins.entries.exa.config.webSearch.apiKey")).toBe(true);
|
|
expect(ids.has("channels.discord.token")).toBe(false);
|
|
});
|
|
|
|
it("includes channel targets for agent runtime when delivery needs them", () => {
|
|
const ids = getAgentRuntimeCommandSecretTargetIds({ includeChannelTargets: true });
|
|
expect(ids.has("channels.discord.token")).toBe(true);
|
|
expect(ids.has("channels.telegram.botToken")).toBe(true);
|
|
});
|
|
|
|
it("includes gateway auth and channel targets for security audit", () => {
|
|
const ids = getSecurityAuditCommandSecretTargetIds();
|
|
expect(ids.has("channels.discord.token")).toBe(true);
|
|
expect(ids.has("gateway.auth.token")).toBe(true);
|
|
expect(ids.has("gateway.auth.password")).toBe(true);
|
|
expect(ids.has("gateway.remote.token")).toBe(true);
|
|
expect(ids.has("gateway.remote.password")).toBe(true);
|
|
});
|
|
|
|
it("scopes channel targets to the requested channel", () => {
|
|
const scoped = getScopedChannelsCommandSecretTargets({
|
|
config: {} as never,
|
|
channel: "discord",
|
|
});
|
|
|
|
expect(scoped.targetIds.size).toBeGreaterThan(0);
|
|
expect([...scoped.targetIds].every((id) => id.startsWith("channels.discord."))).toBe(true);
|
|
expect([...scoped.targetIds].some((id) => id.startsWith("channels.telegram."))).toBe(false);
|
|
});
|
|
|
|
it("does not coerce missing accountId to default when channel is scoped", () => {
|
|
const scoped = getScopedChannelsCommandSecretTargets({
|
|
config: {
|
|
channels: {
|
|
discord: {
|
|
defaultAccount: "ops",
|
|
accounts: {
|
|
ops: {
|
|
token: { source: "env", provider: "default", id: "DISCORD_OPS" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as never,
|
|
channel: "discord",
|
|
});
|
|
|
|
expect(scoped.allowedPaths).toBeUndefined();
|
|
expect(scoped.targetIds.size).toBeGreaterThan(0);
|
|
expect([...scoped.targetIds].every((id) => id.startsWith("channels.discord."))).toBe(true);
|
|
});
|
|
|
|
it("scopes allowed paths to channel globals + selected account", () => {
|
|
const scoped = getScopedChannelsCommandSecretTargets({
|
|
config: {
|
|
channels: {
|
|
discord: {
|
|
token: { source: "env", provider: "default", id: "DISCORD_DEFAULT" },
|
|
accounts: {
|
|
ops: {
|
|
token: { source: "env", provider: "default", id: "DISCORD_OPS" },
|
|
},
|
|
chat: {
|
|
token: { source: "env", provider: "default", id: "DISCORD_CHAT" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as never,
|
|
channel: "discord",
|
|
accountId: "ops",
|
|
});
|
|
|
|
expect(scoped.allowedPaths).toBeDefined();
|
|
expect(scoped.allowedPaths?.has("channels.discord.token")).toBe(true);
|
|
expect(scoped.allowedPaths?.has("channels.discord.accounts.ops.token")).toBe(true);
|
|
expect(scoped.allowedPaths?.has("channels.discord.accounts.chat.token")).toBe(false);
|
|
});
|
|
|
|
it("keeps account-scoped allowedPaths as an empty set when scoped target paths are absent", () => {
|
|
const scoped = getScopedChannelsCommandSecretTargets({
|
|
config: {
|
|
channels: {
|
|
discord: {
|
|
accounts: {
|
|
ops: { enabled: true },
|
|
},
|
|
},
|
|
},
|
|
} as never,
|
|
channel: "custom-plugin-channel-without-secret-targets",
|
|
accountId: "ops",
|
|
});
|
|
|
|
expect(scoped.allowedPaths).toBeDefined();
|
|
expect(scoped.allowedPaths?.size).toBe(0);
|
|
});
|
|
});
|