refactor: harden plugin metadata and bundled channel entry seams

This commit is contained in:
Peter Steinberger 2026-04-06 00:15:29 +01:00
parent 95079949c3
commit 8cb85ff85f
No known key found for this signature in database
43 changed files with 607 additions and 199 deletions

View File

@ -1,5 +1,5 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
import { createBlueBubblesDebounceRegistry } from "./monitor-debounce.js";
import { normalizeWebhookMessage, normalizeWebhookReaction } from "./monitor-normalize.js";

View File

@ -0,0 +1,4 @@
// Keep bundled channel entry imports narrow so bootstrap/discovery paths do
// not drag the broad Discord API barrel into lightweight plugin loads.
export { discordPlugin } from "./src/channel.js";
export { discordSetupPlugin } from "./src/channel.setup.js";

View File

@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest";
import entry from "./index.js";
import setupEntry from "./setup-entry.js";
describe("discord bundled entries", () => {
it("loads the channel plugin without importing the broad api barrel", () => {
const plugin = entry.loadChannelPlugin();
expect(plugin.id).toBe("discord");
});
it("loads the setup plugin without importing the broad api barrel", () => {
const plugin = setupEntry.loadSetupPlugin();
expect(plugin.id).toBe("discord");
});
});

View File

@ -1,11 +1,11 @@
import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";
type DiscordSubagentHooksModule = typeof import("./api.js");
type DiscordSubagentHooksModule = typeof import("./subagent-hooks-api.js");
let discordSubagentHooksPromise: Promise<DiscordSubagentHooksModule> | null = null;
function loadDiscordSubagentHooksModule() {
discordSubagentHooksPromise ??= import("./api.js");
discordSubagentHooksPromise ??= import("./subagent-hooks-api.js");
return discordSubagentHooksPromise;
}
@ -15,7 +15,7 @@ export default defineBundledChannelEntry({
description: "Discord channel plugin",
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "discordPlugin",
},
runtime: {

View File

@ -3,7 +3,7 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr
export default defineBundledChannelSetupEntry({
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "discordSetupPlugin",
},
});

View File

@ -0,0 +1,7 @@
// Subagent hooks live behind a dedicated barrel so the bundled entry can lazy
// load only the handlers it needs.
export {
handleDiscordSubagentDeliveryTarget,
handleDiscordSubagentEnded,
handleDiscordSubagentSpawning,
} from "./src/subagent-hooks.js";

View File

@ -1,5 +1,5 @@
export type { RuntimeEnv } from "../runtime-api.js";
export { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
export { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
export {
applyBasicWebhookRequestGuards,
isRequestBodyLimitError,

View File

@ -61,7 +61,7 @@ export { evaluateSenderGroupAccessForPolicy } from "openclaw/plugin-sdk/group-ac
export { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
export { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
export { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
export { rawDataToString } from "openclaw/plugin-sdk/browser-support";
export { rawDataToString } from "openclaw/plugin-sdk/browser-node-runtime";
export { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";
export {
DEFAULT_GROUP_HISTORY_LIMIT,

View File

@ -1,6 +1,6 @@
import { createHmac } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import { getMattermostRuntime } from "../runtime.js";
import { updateMattermostPost, type MattermostClient, type MattermostPost } from "./client.js";
import { isTrustedProxyAddress, resolveClientIp, type OpenClawConfig } from "./runtime-api.js";

View File

@ -6,7 +6,7 @@
*/
import type { IncomingMessage, ServerResponse } from "node:http";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
import type { ResolvedMattermostAccount } from "../mattermost/accounts.js";
import { getMattermostRuntime } from "../runtime.js";

View File

@ -2,7 +2,7 @@
* Security module: token validation, rate limiting, input sanitization, user allowlist.
*/
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import {
createFixedWindowRateLimiter,
type FixedWindowRateLimiter,

View File

@ -2,7 +2,7 @@ import { createServer } from "node:http";
import type { IncomingMessage } from "node:http";
import net from "node:net";
import * as grammy from "grammy";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { isDiagnosticsEnabled } from "openclaw/plugin-sdk/diagnostic-runtime";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";

View File

@ -1,5 +1,5 @@
import crypto from "node:crypto";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import type { TwilioConfig, WebhookSecurityConfig } from "../config.js";
import { getHeader } from "../http-headers.js";
import type { MediaStreamHandler } from "../media-stream.js";

View File

@ -1,5 +1,5 @@
import crypto from "node:crypto";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import { getHeader } from "./http-headers.js";
import type { WebhookContext } from "./types.js";

View File

@ -0,0 +1,4 @@
// Keep bundled channel bootstrap loads narrow so lightweight config-presence
// probes do not import the broad WhatsApp API barrel.
export { whatsappPlugin } from "./src/channel.js";
export { whatsappSetupPlugin } from "./src/channel.setup.js";

View File

@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest";
import entry from "./index.js";
import setupEntry from "./setup-entry.js";
describe("whatsapp bundled entries", () => {
it("loads the channel plugin without importing the broad api barrel", () => {
const plugin = entry.loadChannelPlugin();
expect(plugin.id).toBe("whatsapp");
});
it("loads the setup plugin without importing the broad api barrel", () => {
const plugin = setupEntry.loadSetupPlugin();
expect(plugin.id).toBe("whatsapp");
});
});

View File

@ -6,7 +6,7 @@ export default defineBundledChannelEntry({
description: "WhatsApp channel plugin",
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "whatsappPlugin",
},
runtime: {

View File

@ -31,7 +31,11 @@
"docsPath": "/channels/whatsapp",
"docsLabel": "whatsapp",
"blurb": "works with your own number; recommend a separate phone + eSIM.",
"systemImage": "message"
"systemImage": "message",
"persistedAuthState": {
"specifier": "./auth-presence",
"exportName": "hasAnyWhatsAppAuth"
}
},
"install": {
"npmSpec": "@openclaw/whatsapp",

View File

@ -3,7 +3,7 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr
export default defineBundledChannelSetupEntry({
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
specifier: "./channel-plugin-api.js",
exportName: "whatsappSetupPlugin",
},
});

View File

@ -1,9 +1,36 @@
import { execFile } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { describe, expect, it } from "vitest";
const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const zaloRuntimeImportEnv = {
HOME: process.env.HOME,
NODE_OPTIONS: process.env.NODE_OPTIONS,
NODE_PATH: process.env.NODE_PATH,
PATH: process.env.PATH,
TERM: process.env.TERM,
} satisfies NodeJS.ProcessEnv;
describe("zalo runtime api", () => {
it("exports the channel plugin without reentering setup surfaces", async () => {
const runtimeApi = await import("./runtime-api.js");
const { stdout } = await execFileAsync(
process.execPath,
[
"--import",
"tsx",
"-e",
'const runtimeApi = await import("./extensions/zalo/runtime-api.ts"); process.stdout.write(runtimeApi.zaloPlugin.id);',
],
{
cwd: repoRoot,
env: zaloRuntimeImportEnv,
timeout: 40_000,
},
);
expect(runtimeApi.zaloPlugin.id).toBe("zalo");
});
expect(stdout).toBe("zalo");
}, 45_000);
});

View File

@ -2,94 +2,4 @@
// Keep this barrel thin and free of local plugin self-imports so the bundled
// entry loader can resolve the channel plugin without re-entering this module.
export { zaloPlugin } from "./src/channel.js";
export type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
export type { OpenClawConfig, GroupPolicy } from "openclaw/plugin-sdk/config-runtime";
export type { MarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
export type { BaseTokenResolution } from "openclaw/plugin-sdk/channel-contract";
export type {
BaseProbeResult,
ChannelAccountSnapshot,
ChannelMessageActionAdapter,
ChannelMessageActionName,
ChannelStatusIssue,
} from "openclaw/plugin-sdk/channel-contract";
export type { SecretInput } from "openclaw/plugin-sdk/secret-input";
export type { SenderGroupAccessDecision } from "openclaw/plugin-sdk/group-access";
export type { ChannelPlugin, PluginRuntime, WizardPrompter } from "openclaw/plugin-sdk/core";
export type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
export type { OutboundReplyPayload } from "openclaw/plugin-sdk/reply-payload";
export {
DEFAULT_ACCOUNT_ID,
buildChannelConfigSchema,
createDedupeCache,
formatPairingApproveHint,
jsonResult,
normalizeAccountId,
readStringParam,
resolveClientIp,
} from "openclaw/plugin-sdk/core";
export {
applyAccountNameToChannelSection,
applySetupAccountConfigPatch,
buildSingleChannelSecretPromptState,
mergeAllowFromEntries,
migrateBaseNameToDefaultAccount,
promptSingleChannelSecretInput,
runSingleChannelSecretStep,
setTopLevelChannelDmPolicyWithAllowFrom,
} from "openclaw/plugin-sdk/setup";
export {
buildSecretInputSchema,
hasConfiguredSecretInput,
normalizeResolvedSecretInputString,
normalizeSecretInputString,
} from "openclaw/plugin-sdk/secret-input";
export {
buildTokenChannelStatusSummary,
PAIRING_APPROVED_MESSAGE,
} from "openclaw/plugin-sdk/channel-status";
export { buildBaseAccountStatusSnapshot } from "openclaw/plugin-sdk/status-helpers";
export { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";
export {
formatAllowFromLowercase,
isNormalizedSenderAllowed,
} from "openclaw/plugin-sdk/allow-from";
export { addWildcardAllowFrom } from "openclaw/plugin-sdk/setup";
export { evaluateSenderGroupAccess } from "openclaw/plugin-sdk/group-access";
export { resolveOpenProviderRuntimeGroupPolicy } from "openclaw/plugin-sdk/config-runtime";
export {
warnMissingProviderGroupPolicyFallbackOnce,
resolveDefaultGroupPolicy,
} from "openclaw/plugin-sdk/config-runtime";
export { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
export { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
export { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
export {
deliverTextOrMediaReply,
isNumericTargetId,
sendPayloadWithChunkedTextAndMedia,
} from "openclaw/plugin-sdk/reply-payload";
export {
resolveDirectDmAuthorizationOutcome,
resolveSenderCommandAuthorizationWithRuntime,
} from "openclaw/plugin-sdk/command-auth";
export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk/inbound-envelope";
export { waitForAbortSignal } from "openclaw/plugin-sdk/runtime";
export {
applyBasicWebhookRequestGuards,
createFixedWindowRateLimiter,
createWebhookAnomalyTracker,
readJsonWebhookBodyOrReject,
registerWebhookTarget,
registerWebhookTargetWithPluginRoute,
resolveWebhookPath,
resolveWebhookTargetWithAuthOrRejectSync,
WEBHOOK_ANOMALY_COUNTER_DEFAULTS,
WEBHOOK_RATE_LIMIT_DEFAULTS,
withResolvedWebhookRequestPipeline,
} from "openclaw/plugin-sdk/webhook-ingress";
export type {
RegisterWebhookPluginRouteOptions,
RegisterWebhookTargetOptions,
} from "openclaw/plugin-sdk/webhook-ingress";
export { setZaloRuntime } from "./src/runtime.js";
export * from "./src/runtime-api.js";

View File

@ -1,5 +1,5 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-security-runtime";
import type { ResolvedZaloAccount } from "./accounts.js";
import type { ZaloFetch, ZaloUpdate } from "./api.js";
import type { ZaloRuntimeEnv } from "./monitor.js";

View File

@ -1 +1,5 @@
export * from "../runtime-api.js";
// Internal runtime barrel. Keep this independent from the public top-level
// runtime barrel so local imports do not loop back through the plugin export
// surface during entry loading.
export * from "./runtime-support.js";
export { setZaloRuntime } from "./runtime.js";

View File

@ -0,0 +1,90 @@
export type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
export type { OpenClawConfig, GroupPolicy } from "openclaw/plugin-sdk/config-runtime";
export type { MarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
export type { BaseTokenResolution } from "openclaw/plugin-sdk/channel-contract";
export type {
BaseProbeResult,
ChannelAccountSnapshot,
ChannelMessageActionAdapter,
ChannelMessageActionName,
ChannelStatusIssue,
} from "openclaw/plugin-sdk/channel-contract";
export type { SecretInput } from "openclaw/plugin-sdk/secret-input";
export type { SenderGroupAccessDecision } from "openclaw/plugin-sdk/group-access";
export type { ChannelPlugin, PluginRuntime, WizardPrompter } from "openclaw/plugin-sdk/core";
export type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
export type { OutboundReplyPayload } from "openclaw/plugin-sdk/reply-payload";
export {
DEFAULT_ACCOUNT_ID,
buildChannelConfigSchema,
createDedupeCache,
formatPairingApproveHint,
jsonResult,
normalizeAccountId,
readStringParam,
resolveClientIp,
} from "openclaw/plugin-sdk/core";
export {
applyAccountNameToChannelSection,
applySetupAccountConfigPatch,
buildSingleChannelSecretPromptState,
mergeAllowFromEntries,
migrateBaseNameToDefaultAccount,
promptSingleChannelSecretInput,
runSingleChannelSecretStep,
setTopLevelChannelDmPolicyWithAllowFrom,
} from "openclaw/plugin-sdk/setup";
export {
buildSecretInputSchema,
hasConfiguredSecretInput,
normalizeResolvedSecretInputString,
normalizeSecretInputString,
} from "openclaw/plugin-sdk/secret-input";
export {
buildTokenChannelStatusSummary,
PAIRING_APPROVED_MESSAGE,
} from "openclaw/plugin-sdk/channel-status";
export { buildBaseAccountStatusSnapshot } from "openclaw/plugin-sdk/status-helpers";
export { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";
export {
formatAllowFromLowercase,
isNormalizedSenderAllowed,
} from "openclaw/plugin-sdk/allow-from";
export { addWildcardAllowFrom } from "openclaw/plugin-sdk/setup";
export { evaluateSenderGroupAccess } from "openclaw/plugin-sdk/group-access";
export { resolveOpenProviderRuntimeGroupPolicy } from "openclaw/plugin-sdk/config-runtime";
export {
warnMissingProviderGroupPolicyFallbackOnce,
resolveDefaultGroupPolicy,
} from "openclaw/plugin-sdk/config-runtime";
export { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
export { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
export { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
export {
deliverTextOrMediaReply,
isNumericTargetId,
sendPayloadWithChunkedTextAndMedia,
} from "openclaw/plugin-sdk/reply-payload";
export {
resolveDirectDmAuthorizationOutcome,
resolveSenderCommandAuthorizationWithRuntime,
} from "openclaw/plugin-sdk/command-auth";
export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk/inbound-envelope";
export { waitForAbortSignal } from "openclaw/plugin-sdk/runtime";
export {
applyBasicWebhookRequestGuards,
createFixedWindowRateLimiter,
createWebhookAnomalyTracker,
readJsonWebhookBodyOrReject,
registerWebhookTarget,
registerWebhookTargetWithPluginRoute,
resolveWebhookPath,
resolveWebhookTargetWithAuthOrRejectSync,
WEBHOOK_ANOMALY_COUNTER_DEFAULTS,
WEBHOOK_RATE_LIMIT_DEFAULTS,
withResolvedWebhookRequestPipeline,
} from "openclaw/plugin-sdk/webhook-ingress";
export type {
RegisterWebhookPluginRouteOptions,
RegisterWebhookTargetOptions,
} from "openclaw/plugin-sdk/webhook-ingress";

View File

@ -58,4 +58,4 @@ export {
sendPayloadWithChunkedTextAndMedia,
type OutboundReplyPayload,
} from "openclaw/plugin-sdk/reply-payload";
export { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/browser-support";
export { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/browser-security-runtime";

View File

@ -1,8 +1,11 @@
import fs from "node:fs";
import os from "node:os";
import {
hasBundledChannelPersistedAuthState,
listBundledChannelIdsWithPersistedAuthState,
} from "../channels/plugins/persisted-auth-state.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveStateDir } from "../config/paths.js";
import { getBootstrapChannelPlugin } from "./plugins/bootstrap-registry.js";
import { listBundledChannelPluginIds } from "./plugins/bundled-ids.js";
const IGNORED_CHANNEL_CONFIG_KEYS = new Set(["defaults", "modelByChannel"]);
@ -39,6 +42,8 @@ function hasPersistedChannelState(env: NodeJS.ProcessEnv): boolean {
return fs.existsSync(resolveStateDir(env, os.homedir));
}
const PERSISTED_AUTH_STATE_CHANNEL_IDS = listBundledChannelIdsWithPersistedAuthState();
export function listPotentialConfiguredChannelIds(
cfg: OpenClawConfig,
env: NodeJS.ProcessEnv = process.env,
@ -71,9 +76,8 @@ export function listPotentialConfiguredChannelIds(
}
if (options.includePersistedAuthState !== false && hasPersistedChannelState(env)) {
for (const channelId of channelIds) {
const plugin = getBootstrapChannelPlugin(channelId);
if (plugin?.config?.hasPersistedAuthState?.({ cfg, env })) {
for (const channelId of PERSISTED_AUTH_STATE_CHANNEL_IDS) {
if (hasBundledChannelPersistedAuthState({ channelId, cfg, env })) {
configuredChannelIds.add(channelId);
}
}
@ -100,8 +104,8 @@ function hasEnvConfiguredChannel(
if (options.includePersistedAuthState === false || !hasPersistedChannelState(env)) {
return false;
}
return channelIds.some((channelId) =>
Boolean(getBootstrapChannelPlugin(channelId)?.config?.hasPersistedAuthState?.({ cfg, env })),
return PERSISTED_AUTH_STATE_CHANNEL_IDS.some((channelId) =>
hasBundledChannelPersistedAuthState({ channelId, cfg, env }),
);
}

View File

@ -61,11 +61,21 @@ function getBootstrapPlugins(): CachedBootstrapPlugins {
return cachedBootstrapPlugins;
}
export function listBootstrapChannelPlugins(): readonly ChannelPlugin[] {
return getBootstrapPlugins().sortedIds.flatMap((id) => {
export function listBootstrapChannelPluginIds(): readonly string[] {
return getBootstrapPlugins().sortedIds;
}
export function* iterateBootstrapChannelPlugins(): IterableIterator<ChannelPlugin> {
for (const id of listBootstrapChannelPluginIds()) {
const plugin = getBootstrapChannelPlugin(id);
return plugin ? [plugin] : [];
});
if (plugin) {
yield plugin;
}
}
}
export function listBootstrapChannelPlugins(): readonly ChannelPlugin[] {
return [...iterateBootstrapChannelPlugins()];
}
export function getBootstrapChannelPlugin(id: ChannelId): ChannelPlugin | undefined {

View File

@ -1,6 +1,10 @@
import type { LegacyConfigRule } from "../../config/legacy.shared.js";
import { listBootstrapChannelPlugins } from "./bootstrap-registry.js";
import { iterateBootstrapChannelPlugins } from "./bootstrap-registry.js";
export function collectChannelLegacyConfigRules(): LegacyConfigRule[] {
return listBootstrapChannelPlugins().flatMap((plugin) => plugin.doctor?.legacyConfigRules ?? []);
const rules: LegacyConfigRule[] = [];
for (const plugin of iterateBootstrapChannelPlugins()) {
rules.push(...(plugin.doctor?.legacyConfigRules ?? []));
}
return rules;
}

View File

@ -0,0 +1,178 @@
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { createJiti } from "jiti";
import type { OpenClawConfig } from "../../config/config.js";
import { openBoundaryFileSync } from "../../infra/boundary-file-read.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import {
listChannelCatalogEntries,
type PluginChannelCatalogEntry,
} from "../../plugins/channel-catalog-registry.js";
import {
buildPluginLoaderAliasMap,
buildPluginLoaderJitiOptions,
shouldPreferNativeJiti,
} from "../../plugins/sdk-alias.js";
type PersistedAuthStateChecker = (params: {
cfg: OpenClawConfig;
env?: NodeJS.ProcessEnv;
}) => boolean;
type PersistedAuthStateMetadata = {
specifier?: string;
exportName?: string;
};
const log = createSubsystemLogger("channels");
const nodeRequire = createRequire(import.meta.url);
const persistedAuthStateCatalog = listChannelCatalogEntries({ origin: "bundled" }).filter((entry) =>
Boolean(resolvePersistedAuthStateMetadata(entry)),
);
const persistedAuthStateEntriesById = new Map(
persistedAuthStateCatalog.map((entry) => [entry.pluginId, entry] as const),
);
const persistedAuthStateCheckerCache = new Map<string, PersistedAuthStateChecker | null>();
function resolvePersistedAuthStateMetadata(
entry: PluginChannelCatalogEntry,
): PersistedAuthStateMetadata | null {
const metadata = entry.channel.persistedAuthState;
if (!metadata || typeof metadata !== "object") {
return null;
}
const specifier = typeof metadata.specifier === "string" ? metadata.specifier.trim() : "";
const exportName = typeof metadata.exportName === "string" ? metadata.exportName.trim() : "";
if (!specifier || !exportName) {
return null;
}
return { specifier, exportName };
}
function createModuleLoader() {
const jitiLoaders = new Map<string, ReturnType<typeof createJiti>>();
return (modulePath: string) => {
const tryNative =
shouldPreferNativeJiti(modulePath) || modulePath.includes(`${path.sep}dist${path.sep}`);
const aliasMap = buildPluginLoaderAliasMap(modulePath, process.argv[1], import.meta.url);
const cacheKey = JSON.stringify({
tryNative,
aliasMap: Object.entries(aliasMap).toSorted(([left], [right]) => left.localeCompare(right)),
});
const cached = jitiLoaders.get(cacheKey);
if (cached) {
return cached;
}
const loader = createJiti(import.meta.url, {
...buildPluginLoaderJitiOptions(aliasMap),
tryNative,
});
jitiLoaders.set(cacheKey, loader);
return loader;
};
}
const loadModule = createModuleLoader();
function resolveModuleCandidates(entry: PluginChannelCatalogEntry, specifier: string): string[] {
const normalizedSpecifier = specifier.replace(/\\/g, "/");
const resolvedPath = path.resolve(entry.rootDir, normalizedSpecifier);
const ext = path.extname(resolvedPath);
if (ext) {
return [resolvedPath];
}
return [
resolvedPath,
`${resolvedPath}.ts`,
`${resolvedPath}.js`,
`${resolvedPath}.mjs`,
`${resolvedPath}.cjs`,
];
}
function resolveExistingModulePath(entry: PluginChannelCatalogEntry, specifier: string): string {
for (const candidate of resolveModuleCandidates(entry, specifier)) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return path.resolve(entry.rootDir, specifier);
}
function loadPersistedAuthStateModule(modulePath: string, rootDir: string): unknown {
const opened = openBoundaryFileSync({
absolutePath: modulePath,
rootPath: rootDir,
boundaryLabel: "plugin root",
rejectHardlinks: false,
skipLexicalRootCheck: true,
});
if (!opened.ok) {
throw new Error("plugin persisted-auth module escapes plugin root or fails alias checks");
}
const safePath = opened.path;
fs.closeSync(opened.fd);
if (
process.platform === "win32" &&
[".js", ".mjs", ".cjs"].includes(path.extname(safePath).toLowerCase())
) {
try {
return nodeRequire(safePath);
} catch {
// Fall back to Jiti when native require cannot load the target.
}
}
return loadModule(safePath)(safePath);
}
function resolvePersistedAuthStateChecker(
entry: PluginChannelCatalogEntry,
): PersistedAuthStateChecker | null {
const cached = persistedAuthStateCheckerCache.get(entry.pluginId);
if (cached !== undefined) {
return cached;
}
const metadata = resolvePersistedAuthStateMetadata(entry);
if (!metadata) {
persistedAuthStateCheckerCache.set(entry.pluginId, null);
return null;
}
try {
const moduleExport = loadPersistedAuthStateModule(
resolveExistingModulePath(entry, metadata.specifier!),
entry.rootDir,
) as Record<string, unknown>;
const checker = moduleExport[metadata.exportName!] as PersistedAuthStateChecker | undefined;
if (typeof checker !== "function") {
throw new Error(`missing persisted auth export ${metadata.exportName}`);
}
persistedAuthStateCheckerCache.set(entry.pluginId, checker);
return checker;
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
log.warn(`[channels] failed to load persisted auth checker for ${entry.pluginId}: ${detail}`);
persistedAuthStateCheckerCache.set(entry.pluginId, null);
return null;
}
}
export function listBundledChannelIdsWithPersistedAuthState(): string[] {
return persistedAuthStateCatalog.map((entry) => entry.pluginId);
}
export function hasBundledChannelPersistedAuthState(params: {
channelId: string;
cfg: OpenClawConfig;
env?: NodeJS.ProcessEnv;
}): boolean {
const entry = persistedAuthStateEntriesById.get(params.channelId);
if (!entry) {
return false;
}
const checker = resolvePersistedAuthStateChecker(entry);
return checker ? Boolean(checker({ cfg: params.cfg, env: params.env })) : false;
}

View File

@ -2,7 +2,8 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
import { listBundledChannelPluginIds } from "../channels/plugins/bundled-ids.js";
import { hasBundledChannelPersistedAuthState } from "../channels/plugins/persisted-auth-state.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveOAuthDir, resolveStateDir } from "../config/paths.js";
@ -463,8 +464,8 @@ function shouldRequireOAuthDir(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boo
if (!isRecord(channels)) {
return false;
}
for (const plugin of listBootstrapChannelPlugins()) {
if (plugin.config.hasPersistedAuthState?.({ cfg, env })) {
for (const channelId of listBundledChannelPluginIds()) {
if (hasBundledChannelPersistedAuthState({ channelId, cfg, env })) {
return true;
}
}

View File

@ -1,4 +1,4 @@
import { listBootstrapChannelPlugins } from "../../../channels/plugins/bootstrap-registry.js";
import { iterateBootstrapChannelPlugins } from "../../../channels/plugins/bootstrap-registry.js";
import type { OpenClawConfig } from "../../../config/types.js";
export function applyChannelDoctorCompatibilityMigrations(cfg: Record<string, unknown>): {
@ -7,7 +7,7 @@ export function applyChannelDoctorCompatibilityMigrations(cfg: Record<string, un
} {
let nextCfg = cfg as OpenClawConfig & Record<string, unknown>;
const changes: string[] = [];
for (const plugin of listBootstrapChannelPlugins()) {
for (const plugin of iterateBootstrapChannelPlugins()) {
const mutation = plugin.doctor?.normalizeCompatibilityConfig?.({ cfg: nextCfg });
if (!mutation || mutation.changes.length === 0) {
continue;

View File

@ -1,5 +1,6 @@
import { hasMeaningfulChannelConfig } from "../channels/config-presence.js";
import { getBootstrapChannelPlugin } from "../channels/plugins/bootstrap-registry.js";
import { hasBundledChannelPersistedAuthState } from "../channels/plugins/persisted-auth-state.js";
import { isRecord } from "../utils.js";
import type { OpenClawConfig } from "./config.js";
@ -27,7 +28,7 @@ export function isChannelConfigured(
if (pluginConfigured) {
return true;
}
const pluginPersistedAuthState = plugin?.config?.hasPersistedAuthState?.({ cfg, env });
const pluginPersistedAuthState = hasBundledChannelPersistedAuthState({ channelId, cfg, env });
if (pluginPersistedAuthState) {
return true;
}

View File

@ -1,4 +1,4 @@
import { listBootstrapChannelPlugins } from "../../channels/plugins/bootstrap-registry.js";
import { getBootstrapChannelPlugin } from "../../channels/plugins/bootstrap-registry.js";
import type { ChannelMessageActionName } from "../../channels/plugins/types.js";
export type MessageActionTargetMode = "to" | "channelId" | "none";
@ -91,14 +91,10 @@ function listActionTargetAliasSpecs(
if (!normalizedChannel) {
return specs;
}
for (const plugin of listBootstrapChannelPlugins()) {
if (plugin.id !== normalizedChannel) {
continue;
}
const channelSpec = plugin.actions?.messageActionTargetAliases?.[action];
if (channelSpec) {
specs.push(channelSpec);
}
const plugin = getBootstrapChannelPlugin(normalizedChannel);
const channelSpec = plugin?.actions?.messageActionTargetAliases?.[action];
if (channelSpec) {
specs.push(channelSpec);
}
return specs;
}

View File

@ -2,7 +2,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
import { iterateBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
import { listBundledChannelPlugins } from "../channels/plugins/bundled.js";
import type { ChannelLegacyStateMigrationPlan } from "../channels/plugins/types.core.js";
import type { OpenClawConfig } from "../config/config.js";
@ -79,11 +79,14 @@ type LegacySessionSurface = {
};
function getLegacySessionSurfaces(): LegacySessionSurface[] {
return listBootstrapChannelPlugins()
.map((plugin) => plugin.messaging)
.filter(
(surface): surface is LegacySessionSurface => Boolean(surface) && typeof surface === "object",
);
const surfaces: LegacySessionSurface[] = [];
for (const plugin of iterateBootstrapChannelPlugins()) {
const surface = plugin.messaging;
if (surface && typeof surface === "object") {
surfaces.push(surface);
}
}
return surfaces;
}
function isSurfaceGroupKey(key: string): boolean {

View File

@ -438,6 +438,10 @@ export type PluginPackageChannel = {
quickstartAllowFrom?: boolean;
forceAccountBinding?: boolean;
preferSessionLookupForAnnounceTarget?: boolean;
persistedAuthState?: {
specifier?: string;
exportName?: string;
};
};
export type PluginPackageInstall = {

View File

@ -0,0 +1,81 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const loadConfigMock = vi.fn();
const applyPluginAutoEnableMock = vi.fn();
const loadOpenClawPluginsMock = vi.fn();
let loadPluginMetadataRegistrySnapshot: typeof import("./metadata-registry-loader.js").loadPluginMetadataRegistrySnapshot;
vi.mock("../../config/config.js", () => ({
loadConfig: () => loadConfigMock(),
}));
vi.mock("../../config/plugin-auto-enable.js", () => ({
applyPluginAutoEnable: (...args: unknown[]) => applyPluginAutoEnableMock(...args),
}));
vi.mock("../loader.js", () => ({
loadOpenClawPlugins: (...args: unknown[]) => loadOpenClawPluginsMock(...args),
}));
vi.mock("../../agents/agent-scope.js", () => ({
resolveAgentWorkspaceDir: () => "/resolved-workspace",
resolveDefaultAgentId: () => "default",
}));
describe("loadPluginMetadataRegistrySnapshot", () => {
beforeAll(async () => {
({ loadPluginMetadataRegistrySnapshot } = await import("./metadata-registry-loader.js"));
});
beforeEach(() => {
loadConfigMock.mockReset();
applyPluginAutoEnableMock.mockReset();
loadOpenClawPluginsMock.mockReset();
loadConfigMock.mockReturnValue({ plugins: {} });
applyPluginAutoEnableMock.mockImplementation((params: { config: unknown }) => ({
config: params.config,
changes: [],
autoEnabledReasons: {},
}));
loadOpenClawPluginsMock.mockReturnValue({ plugins: [], diagnostics: [] });
});
it("defaults to a non-activating validate snapshot", () => {
loadPluginMetadataRegistrySnapshot({
config: { plugins: {} },
activationSourceConfig: { plugins: { allow: ["demo"] } },
env: { HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv,
workspaceDir: "/workspace",
onlyPluginIds: ["demo"],
});
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
expect.objectContaining({
config: { plugins: {} },
activationSourceConfig: { plugins: { allow: ["demo"] } },
workspaceDir: "/workspace",
env: { HOME: "/tmp/openclaw-home" },
onlyPluginIds: ["demo"],
cache: false,
activate: false,
mode: "validate",
loadModules: undefined,
}),
);
});
it("forwards explicit manifest-only requests", () => {
loadPluginMetadataRegistrySnapshot({
config: { plugins: {} },
loadModules: false,
});
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
expect.objectContaining({
loadModules: false,
mode: "validate",
}),
);
});
});

View File

@ -15,6 +15,7 @@ export function loadPluginMetadataRegistrySnapshot(options?: {
env?: NodeJS.ProcessEnv;
workspaceDir?: string;
onlyPluginIds?: string[];
loadModules?: boolean;
}): PluginRegistry {
const env = options?.env ?? process.env;
const baseConfig = options?.config ?? loadConfig();
@ -35,11 +36,13 @@ export function loadPluginMetadataRegistrySnapshot(options?: {
activationSourceConfig: options?.activationSourceConfig ?? baseConfig,
autoEnabledReasons: autoEnabled.autoEnabledReasons,
workspaceDir,
env,
logger,
throwOnLoadError: true,
cache: false,
activate: false,
mode: "validate",
loadModules: options?.loadModules,
...(options?.onlyPluginIds?.length ? { onlyPluginIds: options.onlyPluginIds } : {}),
});
}

View File

@ -11,6 +11,7 @@ import {
const loadConfigMock = vi.fn();
const loadOpenClawPluginsMock = vi.fn();
const loadPluginMetadataRegistrySnapshotMock = vi.fn();
const applyPluginAutoEnableMock = vi.fn();
const resolveBundledProviderCompatPluginIdsMock = vi.fn();
const withBundledPluginAllowlistCompatMock = vi.fn();
@ -38,6 +39,11 @@ vi.mock("./loader.js", () => ({
loadOpenClawPlugins: (...args: unknown[]) => loadOpenClawPluginsMock(...args),
}));
vi.mock("./runtime/metadata-registry-loader.js", () => ({
loadPluginMetadataRegistrySnapshot: (...args: unknown[]) =>
loadPluginMetadataRegistrySnapshotMock(...args),
}));
vi.mock("./providers.js", () => ({
resolveBundledProviderCompatPluginIds: (...args: unknown[]) =>
resolveBundledProviderCompatPluginIdsMock(...args),
@ -69,12 +75,12 @@ vi.mock("../agents/workspace.js", () => ({
}));
function setPluginLoadResult(overrides: Partial<ReturnType<typeof createPluginLoadResult>>) {
loadOpenClawPluginsMock.mockReturnValue(
createPluginLoadResult({
plugins: [],
...overrides,
}),
);
const result = createPluginLoadResult({
plugins: [],
...overrides,
});
loadOpenClawPluginsMock.mockReturnValue(result);
loadPluginMetadataRegistrySnapshotMock.mockReturnValue(result);
}
function setSinglePluginLoadResult(
@ -122,20 +128,31 @@ function expectPluginLoaderCall(params: {
);
}
function expectAutoEnabledStatusLoad(params: {
rawConfig: unknown;
autoEnabledConfig: unknown;
autoEnabledReasons?: Record<string, string[]>;
function expectMetadataSnapshotLoaderCall(params: {
config?: unknown;
activationSourceConfig?: unknown;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
loadModules?: boolean;
}) {
expect(loadPluginMetadataRegistrySnapshotMock).toHaveBeenCalledWith(
expect.objectContaining({
...(params.config !== undefined ? { config: params.config } : {}),
...(params.activationSourceConfig !== undefined
? { activationSourceConfig: params.activationSourceConfig }
: {}),
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
...(params.env ? { env: params.env } : {}),
...(params.loadModules !== undefined ? { loadModules: params.loadModules } : {}),
}),
);
}
function expectAutoEnabledStatusLoad(params: { rawConfig: unknown }) {
expect(applyPluginAutoEnableMock).toHaveBeenCalledWith({
config: params.rawConfig,
env: process.env,
});
expectPluginLoaderCall({
config: params.autoEnabledConfig,
activationSourceConfig: params.rawConfig,
autoEnabledReasons: params.autoEnabledReasons ?? {},
});
}
function createCompatChainFixture() {
@ -159,6 +176,7 @@ function expectBundledCompatChainApplied(params: {
pluginIds: string[];
compatConfig: unknown;
enabledConfig: unknown;
loadModules: boolean;
}) {
expect(withBundledPluginAllowlistCompatMock).toHaveBeenCalledWith({
config: params.config,
@ -168,7 +186,11 @@ function expectBundledCompatChainApplied(params: {
config: params.compatConfig,
pluginIds: params.pluginIds,
});
expectPluginLoaderCall({ config: params.enabledConfig });
if (params.loadModules) {
expectPluginLoaderCall({ config: params.enabledConfig, loadModules: true });
return;
}
expectMetadataSnapshotLoaderCall({ config: params.enabledConfig, loadModules: false });
}
function createAutoEnabledStatusConfig(
@ -258,6 +280,7 @@ describe("plugin status reports", () => {
beforeEach(() => {
loadConfigMock.mockReset();
loadOpenClawPluginsMock.mockReset();
loadPluginMetadataRegistrySnapshotMock.mockReset();
applyPluginAutoEnableMock.mockReset();
resolveBundledProviderCompatPluginIdsMock.mockReset();
withBundledPluginAllowlistCompatMock.mockReset();
@ -291,7 +314,7 @@ describe("plugin status reports", () => {
env,
});
expectPluginLoaderCall({
expectMetadataSnapshotLoaderCall({
config: {},
workspaceDir: "/workspace",
env,
@ -299,16 +322,15 @@ describe("plugin status reports", () => {
});
});
it("uses a non-activating snapshot load for snapshot reports", () => {
it("uses a metadata snapshot load for snapshot reports", () => {
buildPluginSnapshotReport({ config: {}, workspaceDir: "/workspace" });
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
expect(loadPluginMetadataRegistrySnapshotMock).toHaveBeenCalledWith(
expect.objectContaining({
activate: false,
cache: false,
loadModules: false,
}),
);
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
});
it("loads plugin status from the auto-enabled config snapshot", () => {
@ -330,12 +352,12 @@ describe("plugin status reports", () => {
expectAutoEnabledStatusLoad({
rawConfig,
autoEnabledConfig,
autoEnabledReasons: {
demo: ["demo configured"],
},
});
expectPluginLoaderCall({ loadModules: false });
expectMetadataSnapshotLoaderCall({
config: autoEnabledConfig,
activationSourceConfig: rawConfig,
loadModules: false,
});
});
it("uses the auto-enabled config snapshot for inspect policy summaries", () => {
@ -415,12 +437,15 @@ describe("plugin status reports", () => {
expectAutoEnabledStatusLoad({
rawConfig,
autoEnabledConfig,
});
expectPluginLoaderCall({
config: autoEnabledConfig,
activationSourceConfig: rawConfig,
autoEnabledReasons: {
demo: ["demo configured"],
},
loadModules: true,
});
expectPluginLoaderCall({ loadModules: true });
});
it("applies the full bundled provider compat chain before loading plugins", () => {
@ -437,6 +462,7 @@ describe("plugin status reports", () => {
pluginIds,
compatConfig,
enabledConfig,
loadModules: false,
});
});
@ -474,10 +500,14 @@ describe("plugin status reports", () => {
expectAutoEnabledStatusLoad({
rawConfig,
autoEnabledConfig,
});
expectPluginLoaderCall({
config: autoEnabledConfig,
activationSourceConfig: rawConfig,
autoEnabledReasons: {
demo: ["demo configured"],
},
loadModules: true,
});
});

View File

@ -18,6 +18,7 @@ import { createPluginLoaderLogger } from "./logger.js";
import { resolveBundledProviderCompatPluginIds } from "./providers.js";
import type { PluginRegistry } from "./registry.js";
import { listImportedRuntimePluginIds } from "./runtime.js";
import { loadPluginMetadataRegistrySnapshot } from "./runtime/metadata-registry-loader.js";
import type { PluginDiagnostic, PluginHookName } from "./types.js";
export type PluginStatusReport = PluginRegistry & {
@ -189,17 +190,25 @@ function buildPluginReport(
pluginIds: bundledProviderIds,
});
const registry = loadOpenClawPlugins({
config: runtimeCompatConfig,
activationSourceConfig: rawConfig,
autoEnabledReasons: autoEnabled.autoEnabledReasons,
workspaceDir,
env: params?.env,
logger: createPluginLoaderLogger(log),
activate: false,
cache: false,
loadModules,
});
const registry = loadModules
? loadOpenClawPlugins({
config: runtimeCompatConfig,
activationSourceConfig: rawConfig,
autoEnabledReasons: autoEnabled.autoEnabledReasons,
workspaceDir,
env: params?.env,
logger: createPluginLoaderLogger(log),
activate: false,
cache: false,
loadModules,
})
: loadPluginMetadataRegistrySnapshot({
config: runtimeCompatConfig,
activationSourceConfig: rawConfig,
workspaceDir,
env: params?.env,
loadModules: false,
});
const importedPluginIds = new Set([
...(loadModules
? registry.plugins

View File

@ -1,4 +1,4 @@
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
import { iterateBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
import type { OpenClawConfig } from "../config/config.js";
import { type ResolverContext, type SecretDefaults } from "./runtime-shared.js";
@ -7,7 +7,7 @@ export function collectChannelConfigAssignments(params: {
defaults: SecretDefaults | undefined;
context: ResolverContext;
}): void {
for (const plugin of listBootstrapChannelPlugins()) {
for (const plugin of iterateBootstrapChannelPlugins()) {
plugin.secrets?.collectRuntimeConfigAssignments?.(params);
}
}

View File

@ -1,13 +1,15 @@
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
import { iterateBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
import type { SecretTargetRegistryEntry } from "./target-registry-types.js";
const SECRET_INPUT_SHAPE = "secret_input"; // pragma: allowlist secret
const SIBLING_REF_SHAPE = "sibling_ref"; // pragma: allowlist secret
function listChannelSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] {
return listBootstrapChannelPlugins().flatMap(
(plugin) => plugin.secrets?.secretTargetRegistryEntries ?? [],
);
const entries: SecretTargetRegistryEntry[] = [];
for (const plugin of iterateBootstrapChannelPlugins()) {
entries.push(...(plugin.secrets?.secretTargetRegistryEntries ?? []));
}
return entries;
}
const CORE_SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [

View File

@ -1,4 +1,4 @@
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
import { iterateBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
import { isRecord } from "../utils.js";
const CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS = [
@ -10,9 +10,11 @@ const CORE_UNSUPPORTED_SECRETREF_SURFACE_PATTERNS = [
] as const;
function collectChannelUnsupportedSecretRefSurfacePatterns(): string[] {
return listBootstrapChannelPlugins().flatMap(
(plugin) => plugin.secrets?.unsupportedSecretRefSurfacePatterns ?? [],
);
const patterns: string[] = [];
for (const plugin of iterateBootstrapChannelPlugins()) {
patterns.push(...(plugin.secrets?.unsupportedSecretRefSurfacePatterns ?? []));
}
return patterns;
}
let cachedUnsupportedSecretRefSurfacePatterns: string[] | null = null;
@ -74,7 +76,7 @@ export function collectUnsupportedSecretRefConfigCandidates(
}
if (isRecord(raw.channels)) {
for (const plugin of listBootstrapChannelPlugins()) {
for (const plugin of iterateBootstrapChannelPlugins()) {
const channelCandidates = plugin.secrets?.collectUnsupportedSecretRefConfigCandidates?.(raw);
if (!channelCandidates?.length) {
continue;

View File

@ -1,4 +1,4 @@
import { listBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
import { iterateBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
import { parseAgentSessionKey } from "./session-key-utils.js";
export type SessionKeyChatType = "direct" | "group" | "channel" | "unknown";
@ -44,7 +44,7 @@ export function deriveSessionChatType(sessionKey: string | undefined | null): Se
if (builtInLegacy) {
return builtInLegacy;
}
for (const plugin of listBootstrapChannelPlugins()) {
for (const plugin of iterateBootstrapChannelPlugins()) {
const derived = plugin.messaging?.deriveLegacySessionChatType?.(scoped);
if (derived) {
return derived;