openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
368 lines (367 loc) • 17.3 kB
JavaScript
import { a as asOptionalRecord, c as isRecord, i as asOptionalObjectRecord } from "./record-coerce-DItp3I4t.js";
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { d as normalizeStringEntries } from "./string-normalization-DsCfAx8q.js";
import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js";
import { t as isBlockedObjectKey } from "./prototype-keys-CuYw53fZ.js";
import { d as resolveConfigDir } from "./utils-P__uGsPB.js";
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import { a as pluginCacheRealpathSync, l as readPluginCacheJsonFile, r as pluginCacheExistsSync } from "./plugin-cache-files-DLPF_Tw2.js";
import { i as normalizeChatChannelId } from "./ids-BVZRYG0I.js";
import { u as isOfficialExternalPluginId } from "./official-external-plugin-catalog-Dzu7dwBN.js";
import { i as listBundledChannelIdsForPackageState, n as hasBundledChannelPackageState } from "./package-state-probes-B2iR-0SK.js";
import { i as listPotentialConfiguredChannelPresenceSignals } from "./config-presence-Cd62ZvYR.js";
import { t as ensurePluginAllowlisted } from "./plugins-allowlist-DGbUrepm.js";
import { t as isChannelConfigured } from "./channel-configured-CdbUVGCQ.js";
import { t as findChatChannelMeta } from "./chat-meta-tsXGPguN.js";
import path from "node:path";
//#region src/config/plugin-auto-enable.channels.ts
function normalizeManifestChannelId(channelId) {
return normalizeChatChannelId(channelId) ?? channelId;
}
function collectPluginIdsForConfiguredChannel(channelId, registry) {
const normalizedChannelId = normalizeManifestChannelId(channelId);
const builtInId = normalizeChatChannelId(normalizedChannelId);
const claims = registry.plugins.filter((record) => record.channels.some((id) => normalizeManifestChannelId(id) === normalizedChannelId));
if (claims.length === 0) return builtInId ? [builtInId] : [];
const claimIds = new Set(claims.map((claim) => claim.id));
if (builtInId) claimIds.add(builtInId);
const preferredIds = /* @__PURE__ */ new Set();
for (const claim of claims) for (const preferredOverId of claim.channelConfigs?.[normalizedChannelId]?.preferOver ?? []) if (claimIds.has(preferredOverId)) {
preferredIds.add(claim.id);
preferredIds.add(preferredOverId);
}
if (preferredIds.size > 0) return [...preferredIds].toSorted((left, right) => left.localeCompare(right));
return [claims[0]?.id ?? builtInId ?? normalizedChannelId];
}
function collectAutoEnableChannelIds(cfg, env, discovery, ambientEnvTriggers = "allow") {
const configuredStateChannelIds = new Set(listBundledChannelIdsForPackageState("configuredState", discovery));
return listPotentialConfiguredChannelPresenceSignals(cfg, env, {
includePersistedAuthState: false,
discovery,
ambientEnvTriggers
}).map((signal) => ({
source: signal.source,
channelId: normalizeChatChannelId(signal.channelId) ?? signal.channelId
})).filter(({ channelId, source }) => isAutoEnableConfiguredChannelSignal({
cfg,
env,
channelId,
source,
configuredStateChannelIds,
discovery
})).map(({ channelId }) => channelId);
}
function isAutoEnableConfiguredChannelSignal(params) {
if (params.source === "env" && params.configuredStateChannelIds.has(params.channelId) && !hasBundledChannelPackageState({
metadataKey: "configuredState",
channelId: params.channelId,
cfg: params.cfg,
env: params.env,
discovery: params.discovery
})) return false;
return isChannelConfigured(params.cfg, params.channelId, params.env);
}
function resolveConfiguredChannelAutoEnableCandidates(params) {
const changes = [];
for (const channelId of params.configuredChannelIds ?? collectAutoEnableChannelIds(params.config, params.env)) for (const pluginId of collectPluginIdsForConfiguredChannel(channelId, params.registry)) changes.push({
pluginId,
kind: "channel-configured",
channelId
});
return changes;
}
//#endregion
//#region src/config/plugin-auto-enable.prefer-over.ts
/** Maximum bytes to read from an external catalog file before rejecting it. */
const MAX_EXTERNAL_CATALOG_BYTES = 16777216;
const log = createSubsystemLogger("config/plugin-catalog");
const ENV_CATALOG_PATHS = ["OPENCLAW_PLUGIN_CATALOG_PATHS", "OPENCLAW_MPM_CATALOG_PATHS"];
function splitEnvPaths(value) {
const trimmed = normalizeOptionalString(value) ?? "";
if (!trimmed) return [];
return normalizeStringEntries(trimmed.split(/[;,]/g).flatMap((chunk) => chunk.split(path.delimiter)));
}
function resolveExternalCatalogPaths(env) {
for (const key of ENV_CATALOG_PATHS) {
const raw = normalizeOptionalString(env[key]);
if (raw) return splitEnvPaths(raw);
}
const configDir = resolveConfigDir(env);
return [
path.join(configDir, "mpm", "plugins.json"),
path.join(configDir, "mpm", "catalog.json"),
path.join(configDir, "plugins", "catalog.json")
];
}
function parseExternalCatalogChannelEntries(raw) {
const list = (() => {
if (Array.isArray(raw)) return raw;
if (!isRecord(raw)) return [];
const entries = raw.entries ?? raw.packages ?? raw.plugins;
return Array.isArray(entries) ? entries : [];
})();
const channels = [];
for (const entry of list) {
if (!isRecord(entry) || !isRecord(entry.openclaw) || !isRecord(entry.openclaw.channel)) continue;
const channel = entry.openclaw.channel;
const id = normalizeOptionalString(channel.id) ?? "";
if (!id) continue;
const preferOver = Array.isArray(channel.preferOver) ? channel.preferOver.filter((value) => typeof value === "string") : [];
channels.push({
id,
preferOver
});
}
return channels;
}
function resolveExternalCatalogPreferOver(channelId, env) {
for (const rawPath of resolveExternalCatalogPaths(env)) {
const resolved = resolveUserPath(rawPath, env);
if (!pluginCacheExistsSync(resolved)) continue;
try {
const resolvedRealPath = pluginCacheRealpathSync(resolved);
if (!resolvedRealPath) continue;
const payload = readPluginCacheJsonFile(resolvedRealPath, { maxBytes: MAX_EXTERNAL_CATALOG_BYTES });
if (!payload.ok) throw payload.error;
const channel = parseExternalCatalogChannelEntries(payload.value).find((entry) => entry.id === channelId);
if (channel) return channel.preferOver;
} catch (err) {
if (err instanceof Error && err.message.startsWith("File exceeds")) log.warn(`skipping oversized external catalog file (max ${MAX_EXTERNAL_CATALOG_BYTES} bytes): ${resolved}`);
}
}
return [];
}
function resolveBuiltInChannelPreferOver(channelId) {
const builtInChannelId = normalizeChatChannelId(channelId);
if (!builtInChannelId) return [];
return findChatChannelMeta(builtInChannelId)?.preferOver ?? [];
}
function resolvePreferredOverIds(candidate, env, registry) {
const channelId = candidate.kind === "channel-configured" ? candidate.channelId : candidate.pluginId;
const installedPlugin = registry.plugins.find((record) => record.id === candidate.pluginId);
const manifestChannelPreferOver = installedPlugin?.channelConfigs?.[channelId]?.preferOver;
if (manifestChannelPreferOver?.length) return [...manifestChannelPreferOver];
const installedChannelMeta = installedPlugin?.channelCatalogMeta;
if (installedChannelMeta?.preferOver?.length) return [...installedChannelMeta.preferOver];
const builtInChannelPreferOver = resolveBuiltInChannelPreferOver(channelId);
if (builtInChannelPreferOver.length) return [...builtInChannelPreferOver];
return resolveExternalCatalogPreferOver(channelId, env);
}
function getPluginAutoEnableCandidateCacheKey(candidate) {
return `${candidate.pluginId}:${candidate.kind === "channel-configured" ? candidate.channelId : candidate.pluginId}`;
}
function shouldSkipPreferredPluginAutoEnable(params) {
const getPreferredOverIds = (candidate) => {
const cacheKey = getPluginAutoEnableCandidateCacheKey(candidate);
const cached = params.preferOverCache.get(cacheKey);
if (cached) return cached;
const resolved = resolvePreferredOverIds(candidate, params.env, params.registry);
params.preferOverCache.set(cacheKey, resolved);
return resolved;
};
for (const other of params.configured) {
if (other.pluginId === params.entry.pluginId) continue;
if (params.isPluginDenied(params.config, other.pluginId) || params.isPluginExplicitlyDisabled(params.config, other.pluginId)) continue;
if (getPreferredOverIds(other).includes(params.entry.pluginId)) return true;
}
return false;
}
//#endregion
//#region src/config/plugin-auto-enable.materialize.ts
function resolvePluginAutoEnableCandidateReason(candidate) {
switch (candidate.kind) {
case "channel-configured": return `${candidate.channelId} configured`;
case "provider-auth-configured": return `${candidate.providerId} auth configured`;
case "provider-model-configured": return `${candidate.modelRef} model configured`;
case "speech-provider-selected": return `${candidate.providerId} speech provider selected`;
case "worker-provider-selected": return `${candidate.providerId} worker provider selected`;
case "agent-harness-runtime-configured": return `${candidate.runtime} agent runtime configured`;
case "web-search-provider-selected": return `${candidate.providerId} web search provider selected`;
case "web-fetch-provider-selected": return `${candidate.providerId} web fetch provider selected`;
case "plugin-web-search-configured": return `${candidate.pluginId} web search configured`;
case "plugin-web-fetch-configured": return `${candidate.pluginId} web fetch configured`;
case "plugin-tool-configured": return `${candidate.pluginId} tool configured`;
case "configured-plugin-repaired": return `${candidate.pluginId} installed for existing configuration`;
case "setup-auto-enable": return candidate.reason;
}
throw new Error("Unsupported plugin auto-enable candidate");
}
function isPluginExplicitlyDisabled(cfg, pluginId) {
const builtInChannelId = normalizeChatChannelId(pluginId);
if (builtInChannelId) {
const channels = cfg.channels;
if (asOptionalRecord(channels?.[builtInChannelId])?.enabled === false) return true;
}
return cfg.plugins?.entries?.[pluginId]?.enabled === false;
}
function isPluginDenied(cfg, pluginId) {
const deny = cfg.plugins?.deny;
return Array.isArray(deny) && deny.includes(pluginId);
}
function isPluginExplicitlySelected(cfg, pluginId) {
const allow = cfg.plugins?.allow;
if (Array.isArray(allow) && allow.includes(pluginId)) return true;
return hasMaterialPluginEntryConfig(cfg.plugins?.entries?.[pluginId]);
}
function disableImplicitPreferredOverPlugin(params) {
if (isPluginExplicitlySelected(params.originalConfig, params.pluginId)) return params.config;
if (!params.manifestRegistry.plugins.some((plugin) => plugin.id === params.pluginId)) return params.config;
if (!normalizeChatChannelId(params.pluginId) && !isKnownPluginId(params.pluginId, params.manifestRegistry)) return params.config;
const existingEntry = params.config.plugins?.entries?.[params.pluginId];
return {
...params.config,
plugins: {
...params.config.plugins,
entries: {
...params.config.plugins?.entries,
[params.pluginId]: {
...asOptionalObjectRecord(existingEntry),
enabled: false
}
}
}
};
}
function isBuiltInChannelAlreadyEnabled(cfg, channelId) {
const channels = cfg.channels;
return asOptionalRecord(channels?.[channelId])?.enabled === true;
}
function resolveAutoEnableChannelId(params) {
if (params.entry.kind === "configured-plugin-repaired") return null;
const plugin = params.manifestRegistry.plugins.find((record) => record.id === params.entry.pluginId);
if (plugin && plugin.origin !== "bundled") {
if (params.entry.kind !== "channel-configured") return null;
const channelId = normalizeChatChannelId(params.entry.channelId) ?? params.entry.channelId;
if ((plugin.channels ?? []).some((id) => (normalizeChatChannelId(id) ?? id) === channelId)) return null;
}
const builtInChannelId = normalizeChatChannelId(params.entry.pluginId);
if (builtInChannelId) return builtInChannelId;
if (params.entry.kind !== "channel-configured") return null;
if (plugin?.origin !== "bundled") return null;
const channelId = normalizeChatChannelId(params.entry.channelId) ?? params.entry.channelId;
return (plugin.channels ?? []).some((id) => (normalizeChatChannelId(id) ?? id) === channelId) ? channelId : null;
}
function registerPluginEntry(cfg, entry, manifestRegistry) {
const builtInChannelId = resolveAutoEnableChannelId({
entry,
manifestRegistry
});
if (builtInChannelId) {
const channels = cfg.channels;
return {
...cfg,
channels: {
...cfg.channels,
[builtInChannelId]: {
...asOptionalRecord(channels?.[builtInChannelId]),
enabled: true
}
}
};
}
return {
...cfg,
plugins: {
...cfg.plugins,
entries: {
...cfg.plugins?.entries,
[entry.pluginId]: {
...cfg.plugins?.entries?.[entry.pluginId],
enabled: true
}
}
}
};
}
function hasMaterialPluginEntryConfig(entry) {
if (!isRecord(entry)) return false;
return entry.enabled === true || isRecord(entry.config) || isRecord(entry.hooks) || isRecord(entry.subagent) || isRecord(entry.llm) || entry.apiKey !== void 0 || entry.env !== void 0;
}
function isKnownPluginId(pluginId, manifestRegistry) {
if (normalizeChatChannelId(pluginId)) return true;
return manifestRegistry.plugins.some((plugin) => plugin.id === pluginId) || isOfficialExternalPluginId(pluginId);
}
function materializeConfiguredPluginEntryAllowlist(params) {
let next = params.config;
const allow = next.plugins?.allow;
const entries = asOptionalObjectRecord(next.plugins?.entries);
if (!Array.isArray(allow) || allow.length === 0 || !entries) return next;
for (const pluginId of Object.keys(entries).toSorted((left, right) => left.localeCompare(right))) {
const entry = entries[pluginId];
if (!hasMaterialPluginEntryConfig(entry) || isPluginDenied(next, pluginId) || isPluginExplicitlyDisabled(next, pluginId) || allow.includes(pluginId) || !isKnownPluginId(pluginId, params.manifestRegistry)) continue;
next = ensurePluginAllowlisted(next, pluginId);
params.changes.push(`${pluginId} plugin config present, added to plugin allowlist.`);
}
return next;
}
function resolveChannelAutoEnableDisplayLabel(entry, manifestRegistry) {
const builtInChannelId = normalizeChatChannelId(entry.channelId);
const plugin = manifestRegistry.plugins.find((record) => record.id === entry.pluginId);
return (builtInChannelId ? findChatChannelMeta(builtInChannelId)?.label : void 0) ?? plugin?.channelConfigs?.[entry.channelId]?.label ?? plugin?.channelCatalogMeta?.label;
}
function formatAutoEnableChange(entry, manifestRegistry) {
if (entry.kind === "channel-configured") {
const label = resolveChannelAutoEnableDisplayLabel(entry, manifestRegistry);
if (label) return `${label} configured, enabled automatically.`;
}
return `${resolvePluginAutoEnableCandidateReason(entry).trim()}, enabled automatically.`;
}
function materializePluginAutoEnableCandidatesInternal(params) {
let next = params.config ?? {};
const changes = [];
const autoEnabledReasons = /* @__PURE__ */ new Map();
if (next.plugins?.enabled === false) return {
config: next,
changes,
autoEnabledReasons: {}
};
const preferOverCache = /* @__PURE__ */ new Map();
for (const entry of params.candidates) {
const builtInChannelId = resolveAutoEnableChannelId({
entry,
manifestRegistry: params.manifestRegistry
});
if (isPluginDenied(next, entry.pluginId) || isPluginExplicitlyDisabled(next, entry.pluginId)) continue;
if (shouldSkipPreferredPluginAutoEnable({
config: next,
entry,
configured: params.candidates,
env: params.env,
registry: params.manifestRegistry,
isPluginDenied,
isPluginExplicitlyDisabled,
preferOverCache
})) {
next = disableImplicitPreferredOverPlugin({
config: next,
originalConfig: params.config ?? {},
pluginId: entry.pluginId,
manifestRegistry: params.manifestRegistry
});
continue;
}
const allow = next.plugins?.allow;
const hasRestrictiveAllowlist = Array.isArray(allow) && allow.length > 0;
const allowMissing = hasRestrictiveAllowlist && !allow.includes(entry.pluginId);
if ((builtInChannelId != null ? isBuiltInChannelAlreadyEnabled(next, builtInChannelId) : next.plugins?.entries?.[entry.pluginId]?.enabled === true) && !allowMissing) continue;
next = registerPluginEntry(next, entry, params.manifestRegistry);
if (hasRestrictiveAllowlist) next = ensurePluginAllowlisted(next, entry.pluginId);
const reason = resolvePluginAutoEnableCandidateReason(entry);
autoEnabledReasons.set(entry.pluginId, [...autoEnabledReasons.get(entry.pluginId) ?? [], reason]);
changes.push(formatAutoEnableChange(entry, params.manifestRegistry));
}
next = materializeConfiguredPluginEntryAllowlist({
config: next,
changes,
manifestRegistry: params.manifestRegistry
});
const autoEnabledReasonRecord = Object.create(null);
for (const [pluginId, reasons] of autoEnabledReasons) if (!isBlockedObjectKey(pluginId)) autoEnabledReasonRecord[pluginId] = [...reasons];
return {
config: next,
changes,
autoEnabledReasons: autoEnabledReasonRecord
};
}
//#endregion
export { resolveConfiguredChannelAutoEnableCandidates as a, collectAutoEnableChannelIds as i, materializePluginAutoEnableCandidatesInternal as n, resolvePluginAutoEnableCandidateReason as r, hasMaterialPluginEntryConfig as t };