mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-02 12:30:22 +00:00
refactor: resolve channel env vars from plugin manifests
This commit is contained in:
@@ -1,18 +1,28 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const listKnownChannelEnvVarNames = vi.hoisted(() => vi.fn(() => ["DISCORD_BOT_TOKEN"]));
|
||||
const listKnownProviderAuthEnvVarNames = vi.hoisted(() => vi.fn(() => ["OPENAI_API_KEY"]));
|
||||
|
||||
vi.mock("../secrets/channel-env-vars.js", () => ({
|
||||
listKnownChannelEnvVarNames,
|
||||
}));
|
||||
|
||||
vi.mock("../secrets/provider-env-vars.js", () => ({
|
||||
listKnownProviderAuthEnvVarNames,
|
||||
}));
|
||||
|
||||
describe("config io shell env expected keys", () => {
|
||||
it("includes provider auth env vars from manifest-driven provider metadata", async () => {
|
||||
it("includes provider and channel env vars from manifest-driven plugin metadata", async () => {
|
||||
listKnownProviderAuthEnvVarNames.mockReturnValueOnce([
|
||||
"OPENAI_API_KEY",
|
||||
"ARCEEAI_API_KEY",
|
||||
"FIREWORKS_ALT_API_KEY",
|
||||
]);
|
||||
listKnownChannelEnvVarNames.mockReturnValueOnce([
|
||||
"DISCORD_BOT_TOKEN",
|
||||
"SLACK_BOT_TOKEN",
|
||||
"SLACK_APP_TOKEN",
|
||||
]);
|
||||
|
||||
vi.resetModules();
|
||||
const { resolveShellEnvExpectedKeys } = await import("./shell-env-expected-keys.js");
|
||||
@@ -22,8 +32,9 @@ describe("config io shell env expected keys", () => {
|
||||
"OPENAI_API_KEY",
|
||||
"ARCEEAI_API_KEY",
|
||||
"FIREWORKS_ALT_API_KEY",
|
||||
"OPENCLAW_GATEWAY_TOKEN",
|
||||
"DISCORD_BOT_TOKEN",
|
||||
"SLACK_BOT_TOKEN",
|
||||
"OPENCLAW_GATEWAY_TOKEN",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { listKnownChannelEnvVarNames } from "../secrets/channel-env-vars.js";
|
||||
import { listKnownProviderAuthEnvVarNames } from "../secrets/provider-env-vars.js";
|
||||
|
||||
const CORE_SHELL_ENV_EXPECTED_KEYS = [
|
||||
"TELEGRAM_BOT_TOKEN",
|
||||
"DISCORD_BOT_TOKEN",
|
||||
"SLACK_BOT_TOKEN",
|
||||
"SLACK_APP_TOKEN",
|
||||
"OPENCLAW_GATEWAY_TOKEN",
|
||||
"OPENCLAW_GATEWAY_PASSWORD",
|
||||
];
|
||||
const CORE_SHELL_ENV_EXPECTED_KEYS = ["OPENCLAW_GATEWAY_TOKEN", "OPENCLAW_GATEWAY_PASSWORD"];
|
||||
|
||||
export function resolveShellEnvExpectedKeys(env: NodeJS.ProcessEnv): string[] {
|
||||
return [
|
||||
...new Set([...listKnownProviderAuthEnvVarNames({ env }), ...CORE_SHELL_ENV_EXPECTED_KEYS]),
|
||||
...new Set([
|
||||
...listKnownProviderAuthEnvVarNames({ env }),
|
||||
...listKnownChannelEnvVarNames({ env }),
|
||||
...CORE_SHELL_ENV_EXPECTED_KEYS,
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -417,6 +417,28 @@ describe("loadPluginManifestRegistry", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves channel env metadata from plugin manifests", () => {
|
||||
const dir = makeTempDir();
|
||||
writeManifest(dir, {
|
||||
id: "slack",
|
||||
channels: ["slack"],
|
||||
channelEnvVars: {
|
||||
slack: ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_USER_TOKEN"],
|
||||
},
|
||||
configSchema: { type: "object" },
|
||||
});
|
||||
|
||||
const registry = loadSingleCandidateRegistry({
|
||||
idHint: "slack",
|
||||
rootDir: dir,
|
||||
origin: "bundled",
|
||||
});
|
||||
|
||||
expect(registry.plugins[0]?.channelEnvVars).toEqual({
|
||||
slack: ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_USER_TOKEN"],
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves channel config metadata from plugin manifests", () => {
|
||||
const dir = makeTempDir();
|
||||
writeManifest(dir, {
|
||||
|
||||
@@ -73,6 +73,7 @@ export type PluginManifestRecord = {
|
||||
modelSupport?: PluginManifestModelSupport;
|
||||
cliBackends: string[];
|
||||
providerAuthEnvVars?: Record<string, string[]>;
|
||||
channelEnvVars?: Record<string, string[]>;
|
||||
providerAuthChoices?: PluginManifest["providerAuthChoices"];
|
||||
skills: string[];
|
||||
settingsFiles?: string[];
|
||||
@@ -292,6 +293,7 @@ function buildRecord(params: {
|
||||
modelSupport: params.manifest.modelSupport,
|
||||
cliBackends: params.manifest.cliBackends ?? [],
|
||||
providerAuthEnvVars: params.manifest.providerAuthEnvVars,
|
||||
channelEnvVars: params.manifest.channelEnvVars,
|
||||
providerAuthChoices: params.manifest.providerAuthChoices,
|
||||
skills: params.manifest.skills ?? [],
|
||||
settingsFiles: [],
|
||||
|
||||
@@ -89,6 +89,8 @@ export type PluginManifest = {
|
||||
cliBackends?: string[];
|
||||
/** Cheap provider-auth env lookup without booting plugin runtime. */
|
||||
providerAuthEnvVars?: Record<string, string[]>;
|
||||
/** Cheap channel env lookup without booting plugin runtime. */
|
||||
channelEnvVars?: Record<string, string[]>;
|
||||
/**
|
||||
* Cheap onboarding/auth-choice metadata used by config validation, CLI help,
|
||||
* and non-runtime auth-choice routing before provider runtime loads.
|
||||
@@ -500,6 +502,7 @@ export function loadPluginManifest(
|
||||
const modelSupport = normalizeManifestModelSupport(raw.modelSupport);
|
||||
const cliBackends = normalizeStringList(raw.cliBackends);
|
||||
const providerAuthEnvVars = normalizeStringListRecord(raw.providerAuthEnvVars);
|
||||
const channelEnvVars = normalizeStringListRecord(raw.channelEnvVars);
|
||||
const providerAuthChoices = normalizeProviderAuthChoices(raw.providerAuthChoices);
|
||||
const skills = normalizeStringList(raw.skills);
|
||||
const contracts = normalizeManifestContracts(raw.contracts);
|
||||
@@ -527,6 +530,7 @@ export function loadPluginManifest(
|
||||
modelSupport,
|
||||
cliBackends,
|
||||
providerAuthEnvVars,
|
||||
channelEnvVars,
|
||||
providerAuthChoices,
|
||||
skills,
|
||||
name,
|
||||
|
||||
48
src/secrets/channel-env-vars.dynamic.test.ts
Normal file
48
src/secrets/channel-env-vars.dynamic.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
type MockManifestRegistry = {
|
||||
plugins: Array<{
|
||||
id: string;
|
||||
origin: string;
|
||||
channelEnvVars?: Record<string, string[]>;
|
||||
}>;
|
||||
diagnostics: unknown[];
|
||||
};
|
||||
|
||||
const loadPluginManifestRegistry = vi.hoisted(() =>
|
||||
vi.fn<() => MockManifestRegistry>(() => ({ plugins: [], diagnostics: [] })),
|
||||
);
|
||||
|
||||
vi.mock("../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry,
|
||||
}));
|
||||
|
||||
describe("channel env vars dynamic manifest metadata", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
loadPluginManifestRegistry.mockReset();
|
||||
loadPluginManifestRegistry.mockReturnValue({ plugins: [], diagnostics: [] });
|
||||
});
|
||||
|
||||
it("includes later-installed plugin env vars without a bundled generated map", async () => {
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "external-mattermost",
|
||||
origin: "global",
|
||||
channelEnvVars: {
|
||||
mattermost: ["MATTERMOST_BOT_TOKEN", "MATTERMOST_URL"],
|
||||
},
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const mod = await import("./channel-env-vars.js");
|
||||
|
||||
expect(mod.getChannelEnvVars("mattermost")).toEqual(["MATTERMOST_BOT_TOKEN", "MATTERMOST_URL"]);
|
||||
expect(mod.listKnownChannelEnvVarNames()).toEqual(
|
||||
expect.arrayContaining(["MATTERMOST_BOT_TOKEN", "MATTERMOST_URL"]),
|
||||
);
|
||||
});
|
||||
});
|
||||
61
src/secrets/channel-env-vars.ts
Normal file
61
src/secrets/channel-env-vars.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
|
||||
type ChannelEnvVarLookupParams = {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
function appendUniqueEnvVarCandidates(
|
||||
target: Record<string, string[]>,
|
||||
channelId: string,
|
||||
keys: readonly string[],
|
||||
) {
|
||||
const normalizedChannelId = channelId.trim();
|
||||
if (!normalizedChannelId || keys.length === 0) {
|
||||
return;
|
||||
}
|
||||
const bucket = (target[normalizedChannelId] ??= []);
|
||||
const seen = new Set(bucket);
|
||||
for (const key of keys) {
|
||||
const normalizedKey = key.trim();
|
||||
if (!normalizedKey || seen.has(normalizedKey)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(normalizedKey);
|
||||
bucket.push(normalizedKey);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveChannelEnvVars(
|
||||
params?: ChannelEnvVarLookupParams,
|
||||
): Record<string, readonly string[]> {
|
||||
const registry = loadPluginManifestRegistry({
|
||||
config: params?.config,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env: params?.env,
|
||||
});
|
||||
const candidates: Record<string, string[]> = Object.create(null) as Record<string, string[]>;
|
||||
for (const plugin of registry.plugins) {
|
||||
if (!plugin.channelEnvVars) {
|
||||
continue;
|
||||
}
|
||||
for (const [channelId, keys] of Object.entries(plugin.channelEnvVars).toSorted(
|
||||
([left], [right]) => left.localeCompare(right),
|
||||
)) {
|
||||
appendUniqueEnvVarCandidates(candidates, channelId, keys);
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
export function getChannelEnvVars(channelId: string, params?: ChannelEnvVarLookupParams): string[] {
|
||||
const channelEnvVars = resolveChannelEnvVars(params);
|
||||
const envVars = Object.hasOwn(channelEnvVars, channelId) ? channelEnvVars[channelId] : undefined;
|
||||
return Array.isArray(envVars) ? [...envVars] : [];
|
||||
}
|
||||
|
||||
export function listKnownChannelEnvVarNames(params?: ChannelEnvVarLookupParams): string[] {
|
||||
return [...new Set(Object.values(resolveChannelEnvVars(params)).flatMap((keys) => keys))];
|
||||
}
|
||||
Reference in New Issue
Block a user