mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
refactor(channels): unify trusted configured-state probes (#130320)
This commit is contained in:
committed by
GitHub
parent
81f2d1eb61
commit
e668e96034
@@ -3,15 +3,15 @@ import { createRequire } from "node:module";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import {
|
||||
hasBundledChannelConfiguredState,
|
||||
listBundledChannelIdsWithConfiguredState,
|
||||
} from "./configured-state.js";
|
||||
hasBundledChannelPackageState,
|
||||
listBundledChannelIdsForPackageState,
|
||||
} from "./package-state-probes.js";
|
||||
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
|
||||
describe("bundled channel configured-state metadata", () => {
|
||||
it("lists the shipped metadata-first configured-state channels", () => {
|
||||
expect(listBundledChannelIdsWithConfiguredState()).toEqual([
|
||||
expect(listBundledChannelIdsForPackageState("configuredState")).toEqual([
|
||||
"buzz",
|
||||
"clickclack",
|
||||
"discord",
|
||||
@@ -37,28 +37,32 @@ describe("bundled channel configured-state metadata", () => {
|
||||
|
||||
it("resolves Discord, Slack, Telegram, and IRC env probes without full plugin loads", () => {
|
||||
expect(
|
||||
hasBundledChannelConfiguredState({
|
||||
hasBundledChannelPackageState({
|
||||
metadataKey: "configuredState",
|
||||
channelId: "discord",
|
||||
cfg: {},
|
||||
env: { DISCORD_BOT_TOKEN: "token" },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
hasBundledChannelConfiguredState({
|
||||
hasBundledChannelPackageState({
|
||||
metadataKey: "configuredState",
|
||||
channelId: "slack",
|
||||
cfg: {},
|
||||
env: { SLACK_BOT_TOKEN: "xoxb-test", SLACK_APP_TOKEN: "xapp-test" },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
hasBundledChannelConfiguredState({
|
||||
hasBundledChannelPackageState({
|
||||
metadataKey: "configuredState",
|
||||
channelId: "telegram",
|
||||
cfg: {},
|
||||
env: { TELEGRAM_BOT_TOKEN: "token" },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
hasBundledChannelConfiguredState({
|
||||
hasBundledChannelPackageState({
|
||||
metadataKey: "configuredState",
|
||||
channelId: "irc",
|
||||
cfg: {},
|
||||
env: { IRC_HOST: "irc.example.com", IRC_NICK: "openclaw" },
|
||||
@@ -79,7 +83,9 @@ describe("bundled channel configured-state metadata", () => {
|
||||
{ channelId: "nextcloud-talk", env: { NEXTCLOUD_TALK_BOT_SECRET: "secret" } },
|
||||
{ channelId: "zalo", env: { ZALO_WEBHOOK_SECRET: "secret" } },
|
||||
])("rejects incomplete $channelId environment credentials", ({ channelId, env }) => {
|
||||
expect(hasBundledChannelConfiguredState({ channelId, cfg: {}, env })).toBe(false);
|
||||
expect(
|
||||
hasBundledChannelPackageState({ metadataKey: "configuredState", channelId, cfg: {}, env }),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -109,12 +115,15 @@ describe("bundled channel configured-state metadata", () => {
|
||||
env: { SYNOLOGY_CHAT_TOKEN: "token", SYNOLOGY_CHAT_INCOMING_URL: "https://example.test" },
|
||||
},
|
||||
])("accepts complete $channelId environment credentials", ({ channelId, env }) => {
|
||||
expect(hasBundledChannelConfiguredState({ channelId, cfg: {}, env })).toBe(true);
|
||||
expect(
|
||||
hasBundledChannelPackageState({ metadataKey: "configuredState", channelId, cfg: {}, env }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps explicit blank Teams credentials authoritative over ambient credentials", () => {
|
||||
expect(
|
||||
hasBundledChannelConfiguredState({
|
||||
hasBundledChannelPackageState({
|
||||
metadataKey: "configuredState",
|
||||
channelId: "msteams",
|
||||
cfg: { channels: { msteams: { appId: "", appPassword: "", tenantId: "" } } },
|
||||
env: {
|
||||
@@ -168,7 +177,9 @@ describe("bundled channel configured-state metadata", () => {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}>)("accepts the owner-specific $name contract", ({ channelId, cfg, env }) => {
|
||||
expect(hasBundledChannelConfiguredState({ channelId, cfg, env })).toBe(true);
|
||||
expect(
|
||||
hasBundledChannelPackageState({ metadataKey: "configuredState", channelId, cfg, env }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("uses declarative env metadata without a TypeScript source require hook", () => {
|
||||
@@ -176,7 +187,8 @@ describe("bundled channel configured-state metadata", () => {
|
||||
delete nodeRequire.extensions[".ts"];
|
||||
try {
|
||||
expect(
|
||||
hasBundledChannelConfiguredState({
|
||||
hasBundledChannelPackageState({
|
||||
metadataKey: "configuredState",
|
||||
channelId: "discord",
|
||||
cfg: {},
|
||||
env: { DISCORD_BOT_TOKEN: "token" },
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* Bundled channel configured-state probes.
|
||||
*
|
||||
* Lists and checks bundled channels that can report configured account state.
|
||||
*/
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { PluginDiscoveryResult } from "../../plugins/discovery.js";
|
||||
import {
|
||||
hasBundledChannelPackageState,
|
||||
listBundledChannelIdsForPackageState,
|
||||
} from "./package-state-probes.js";
|
||||
|
||||
/**
|
||||
* Lists bundled channel ids that expose configured-state detectors.
|
||||
*/
|
||||
export function listBundledChannelIdsWithConfiguredState(
|
||||
discovery?: PluginDiscoveryResult,
|
||||
): string[] {
|
||||
return listBundledChannelIdsForPackageState("configuredState", discovery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a bundled channel reports configured state for the current config.
|
||||
*/
|
||||
export function hasBundledChannelConfiguredState(params: {
|
||||
channelId: string;
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
discovery?: PluginDiscoveryResult;
|
||||
}): boolean {
|
||||
return hasBundledChannelPackageState({
|
||||
metadataKey: "configuredState",
|
||||
channelId: params.channelId,
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
discovery: params.discovery,
|
||||
});
|
||||
}
|
||||
@@ -133,6 +133,27 @@ describe("channel package-state probes", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "PATH", env: { PATH: "/usr/bin" }, configured: false },
|
||||
{ name: "mixed_case_token", env: { MIXED_CASE_TOKEN: "token" }, configured: true },
|
||||
])("applies the safe channel env-trigger contract to $name", ({ name, env, configured }) => {
|
||||
listChannelCatalogEntriesMock.mockReturnValue([
|
||||
{
|
||||
...makeBundledChannelCatalogEntry({ pluginId: "env-chat", channelId: "env-chat" }),
|
||||
channel: { id: "env-chat", configuredState: { env: { allOf: [name] } } },
|
||||
} satisfies PluginChannelCatalogEntry,
|
||||
]);
|
||||
|
||||
expect(
|
||||
hasBundledChannelPackageState({
|
||||
metadataKey: "configuredState",
|
||||
channelId: "env-chat",
|
||||
cfg: {},
|
||||
env,
|
||||
}),
|
||||
).toBe(configured);
|
||||
});
|
||||
|
||||
it("prefers built bundled package-state probes when the catalog root is source", () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-package-state-probe-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
getCachedPluginModuleLoader,
|
||||
type PluginModuleLoaderCache,
|
||||
} from "../../plugins/plugin-module-loader-cache.js";
|
||||
import { isSafeChannelEnvVarTriggerName } from "../../secrets/channel-env-var-names.js";
|
||||
import { loadChannelPluginModule, resolveExistingPluginModulePath } from "./module-loader.js";
|
||||
|
||||
type ChannelPackageStateChecker = (params: {
|
||||
@@ -81,7 +82,12 @@ function loadChannelPackageStateModule(params: { modulePath: string; rootDir: st
|
||||
}
|
||||
|
||||
function hasNonEmptyEnvValue(env: NodeJS.ProcessEnv | undefined, key: string): boolean {
|
||||
return typeof env?.[key] === "string" && env[key].trim().length > 0;
|
||||
if (!env || !isSafeChannelEnvVarTriggerName(key)) {
|
||||
return false;
|
||||
}
|
||||
const normalized = key.trim();
|
||||
const value = env[normalized] ?? env[normalized.toUpperCase()];
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
}
|
||||
|
||||
function resolveSourceBundledPluginRoot(rootDir: string): {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Determines whether a channel is configured from bootstrap and plugin state.
|
||||
import { getBootstrapChannelPlugin } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { hasBundledChannelConfiguredState } from "../channels/plugins/configured-state.js";
|
||||
import { hasBundledChannelPackageState } from "../channels/plugins/package-state-probes.js";
|
||||
import {
|
||||
hasMeaningfulChannelConfigShallow,
|
||||
resolveChannelConfigRecord,
|
||||
@@ -19,7 +19,7 @@ export function isChannelConfigured(
|
||||
return true;
|
||||
}
|
||||
// Bundled channels can expose configured state through env vars or persisted credential files.
|
||||
if (hasBundledChannelConfiguredState({ channelId, cfg, env })) {
|
||||
if (hasBundledChannelPackageState({ metadataKey: "configuredState", channelId, cfg, env })) {
|
||||
return true;
|
||||
}
|
||||
// Bootstrap plugins cover channels that are available before full plugin registry loading.
|
||||
|
||||
@@ -20,15 +20,17 @@ import {
|
||||
import type { OpenClawConfig } from "./types.openclaw.js";
|
||||
import { validateConfigObject } from "./validation.js";
|
||||
|
||||
vi.mock("../channels/plugins/configured-state.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../channels/plugins/configured-state.js")>();
|
||||
vi.mock("../channels/plugins/package-state-probes.js", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("../channels/plugins/package-state-probes.js")>();
|
||||
return {
|
||||
...actual,
|
||||
hasBundledChannelConfiguredState: (params: {
|
||||
channelId: string;
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}) => {
|
||||
hasBundledChannelPackageState: (
|
||||
params: Parameters<typeof actual.hasBundledChannelPackageState>[0],
|
||||
) => {
|
||||
if (params.metadataKey !== "configuredState") {
|
||||
return actual.hasBundledChannelPackageState(params);
|
||||
}
|
||||
if (params.channelId === "cache-channel") {
|
||||
return Boolean(params.env?.CACHE_CHANNEL_TOKEN?.trim());
|
||||
}
|
||||
@@ -40,7 +42,7 @@ vi.mock("../channels/plugins/configured-state.js", async (importOriginal) => {
|
||||
Boolean(params.env?.[key]?.trim()),
|
||||
);
|
||||
}
|
||||
return actual.hasBundledChannelConfiguredState(params);
|
||||
return actual.hasBundledChannelPackageState(params);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
type ChannelPresenceSignalSource,
|
||||
} from "../channels/config-presence.js";
|
||||
import {
|
||||
hasBundledChannelConfiguredState,
|
||||
listBundledChannelIdsWithConfiguredState,
|
||||
} from "../channels/plugins/configured-state.js";
|
||||
hasBundledChannelPackageState,
|
||||
listBundledChannelIdsForPackageState,
|
||||
} from "../channels/plugins/package-state-probes.js";
|
||||
import { findChatChannelMeta, normalizeChatChannelId } from "../channels/registry.js";
|
||||
import { isBlockedObjectKey } from "../infra/prototype-keys.js";
|
||||
import { normalizePluginsConfig } from "../plugins/config-state.js";
|
||||
@@ -299,7 +299,9 @@ function collectConfiguredChannelIds(
|
||||
discovery?: PluginDiscoveryResult,
|
||||
ambientEnvTriggers: AmbientEnvTriggerPolicy = "allow",
|
||||
): string[] {
|
||||
const configuredStateChannelIds = new Set(listBundledChannelIdsWithConfiguredState(discovery));
|
||||
const configuredStateChannelIds = new Set(
|
||||
listBundledChannelIdsForPackageState("configuredState", discovery),
|
||||
);
|
||||
return listPotentialConfiguredChannelPresenceSignals(cfg, env, {
|
||||
includePersistedAuthState: false,
|
||||
discovery,
|
||||
@@ -333,7 +335,8 @@ function isAutoEnableConfiguredChannelSignal(params: {
|
||||
if (
|
||||
params.source === "env" &&
|
||||
params.configuredStateChannelIds.has(params.channelId) &&
|
||||
!hasBundledChannelConfiguredState({
|
||||
!hasBundledChannelPackageState({
|
||||
metadataKey: "configuredState",
|
||||
channelId: params.channelId,
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
import { hasChannelPackageState } from "../channels/plugins/package-state-probes.js";
|
||||
import { resolveConfigWidePluginManifestRegistry } from "../config/io.plugin-metadata.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { isSafeChannelEnvVarTriggerName } from "../secrets/channel-env-var-names.js";
|
||||
import { resolveManifestActivationPluginIds } from "./activation-planner.js";
|
||||
import {
|
||||
createPluginActivationSource,
|
||||
@@ -74,15 +73,6 @@ function normalizeChannelIds(channelIds: Iterable<string>): string[] {
|
||||
);
|
||||
}
|
||||
|
||||
function hasNonEmptyEnvValue(env: NodeJS.ProcessEnv, key: string): boolean {
|
||||
if (!isSafeChannelEnvVarTriggerName(key)) {
|
||||
return false;
|
||||
}
|
||||
const trimmed = key.trim();
|
||||
const value = env[trimmed] ?? env[trimmed.toUpperCase()];
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
}
|
||||
|
||||
/** True when config contains meaningful enabled channel settings. */
|
||||
export function hasExplicitChannelConfig(params: {
|
||||
config: OpenClawConfig;
|
||||
@@ -175,27 +165,20 @@ function listManifestEnvConfiguredChannelSignals(params: {
|
||||
continue;
|
||||
}
|
||||
contractChannelIds.add(normalizedChannelId);
|
||||
if (hasModuleContract) {
|
||||
if (
|
||||
!params.envSignalChannelIds.has(normalizedChannelId) ||
|
||||
!packageChannel ||
|
||||
!hasChannelPackageState({
|
||||
entry: {
|
||||
pluginId: record.id,
|
||||
origin: record.origin,
|
||||
rootDir: record.rootDir,
|
||||
channel: packageChannel,
|
||||
},
|
||||
metadataKey: "configuredState",
|
||||
cfg: params.config,
|
||||
env: params.env,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
} else if (
|
||||
!allOf.every((envVar) => hasNonEmptyEnvValue(params.env, envVar)) ||
|
||||
(anyOf.length > 0 && !anyOf.some((envVar) => hasNonEmptyEnvValue(params.env, envVar)))
|
||||
if (
|
||||
(hasModuleContract && !params.envSignalChannelIds.has(normalizedChannelId)) ||
|
||||
!packageChannel ||
|
||||
!hasChannelPackageState({
|
||||
entry: {
|
||||
pluginId: record.id,
|
||||
origin: record.origin,
|
||||
rootDir: record.rootDir,
|
||||
channel: packageChannel,
|
||||
},
|
||||
metadataKey: "configuredState",
|
||||
cfg: params.config,
|
||||
env: params.env,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user