openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
402 lines (401 loc) • 12.7 kB
JavaScript
import { t as isPlainObject } from "./plain-object-5a0EzLzX.js";
import "./utils-P__uGsPB.js";
import "./account-id-CETVCrTz.js";
import { t as canHotReloadGatewayAuthCredentials } from "./auth-resolve-O5AKX-sb.js";
import { c as getActivePluginHttpRouteRegistry, l as getActivePluginHttpRouteRegistryVersion } from "./runtime-BL4wZfTq.js";
import { i as listChannelPlugins } from "./registry-Cz6cv4VC.js";
import "./plugins-Cozj1Enf.js";
//#region src/gateway/config-reload-plan.ts
const RELOAD_ACTIONS = [
"reloadHooks",
"reloadInternalHooks",
"refreshHooksPolicy",
"restartGmailWatcher",
"restartCron",
"restartHeartbeat",
"reconcileSystemJobs",
"reloadPlugins",
"disposeMcpRuntimes"
];
function isNoopGatewayReloadPlan(plan) {
return !plan.restartGateway && plan.hotReasons.length === 0 && RELOAD_ACTIONS.every((action) => !plan[action]) && plan.restartChannels.size === 0 && (plan.restartServices?.size ?? 0) === 0 && (plan.restartChannelAccounts?.size ?? 0) === 0;
}
const PLUGIN_INSTALL_TIMESTAMP_KEYS = ["installedAt", "resolvedAt"];
const AUTH_CREDENTIAL_PATHS = ["gateway.auth.token", "gateway.auth.password"];
const SHARED_CHANNEL_PREFIXES = [
"agents.defaults.mediaMaxMb",
"channels.defaults",
"channels.modelByChannel",
"messages.inbound",
"messages.ackReactionScope",
"commands",
"accessGroups",
"tts",
"surfaces",
"acp.stream",
"diagnostics.flags"
];
function matchesReloadPrefix(path, prefix) {
return path === prefix || path.startsWith(`${prefix}.`);
}
function expandReloadPolicies(policies) {
return policies.flatMap(({ prefixes, ...policy }) => prefixes.map((prefix) => ({
...policy,
prefix
}))).toSorted((a, b) => b.prefix.length - a.prefix.length);
}
const CORE_RELOAD_POLICIES = [
{
prefixes: ["gateway.remote", "gateway.reload"],
kind: "none"
},
{
prefixes: [
...AUTH_CREDENTIAL_PATHS,
"mcp.apps",
"secrets.egressProxy",
"plugins.load",
"plugins.installs"
],
kind: "restart"
},
{
prefixes: [
"gateway.http.endpoints",
"gateway.http.securityHeaders.strictTransportSecurity",
"gateway.tools",
"gateway.cliAgents",
"gateway.controlUi.enabled",
"gateway.controlUi.environment",
"gateway.controlUi.communityInvite",
"gateway.controlUi.github",
"gateway.controlUi.sessionObserver",
"gateway.controlUi.embedSandbox",
"gateway.controlUi.allowExternalEmbedUrls",
"gateway.controlUi.automaticallyFetchFavicons",
"gateway.controlUi.allowedOrigins",
"gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback",
"gateway.nodes.browser",
"gateway.nodes.pairing",
"gateway.nodes.commands",
"gateway.nodes.pluginTools.enabled",
"gateway.nodes.allowSkills",
"gateway.push.apns.relay",
"gateway.terminal",
"gateway.auth.rateLimit",
"diagnostics.enabled",
"discovery.mdns.mode",
"mcp.apps.sandboxOrigin",
"agents.defaults"
],
kind: "hot"
},
{
prefixes: ["hooks.gmail"],
kind: "hot",
actions: ["restartGmailWatcher", "reloadHooks"]
},
{
prefixes: ["hooks.internal", "agents.defaults.workspace"],
kind: "hot",
actions: ["reloadInternalHooks"]
},
{
prefixes: ["hooks"],
kind: "hot",
actions: ["reloadHooks"]
},
{
prefixes: [
"agents.defaults.heartbeat",
"agents.defaults.models",
"agents.defaults.modelPolicy",
"agents.defaults.model",
"models",
"agent.heartbeat"
],
kind: "hot",
actions: ["restartHeartbeat", "reconcileSystemJobs"]
},
{
prefixes: ["agents.entries"],
kind: "hot",
actions: [
"restartHeartbeat",
"reconcileSystemJobs",
"refreshHooksPolicy",
"reloadInternalHooks"
]
},
{
prefixes: ["agents.defaults.sessionStore", "agents.ownership"],
kind: "hot",
actions: ["refreshHooksPolicy"]
},
{
prefixes: ["skills.workshop.autonomous.mode"],
kind: "hot",
actions: ["reconcileSystemJobs"]
},
{
prefixes: ["cron"],
kind: "hot",
actions: ["restartCron"]
},
{
prefixes: ["mcp", "gateway.publicOrigin"],
kind: "hot",
actions: ["disposeMcpRuntimes"]
},
{
prefixes: ["talk.provider", "talk.realtime.provider"],
kind: "hot",
actions: ["reloadPlugins"]
}
];
const DEFAULT_RELOAD_POLICIES = [
{
prefixes: [
"meta",
"identity",
"wizard",
"logging",
"agents",
"bindings",
"audio",
"agent",
"routing",
"messages",
"session",
"talk",
"skills",
"secrets",
"tui",
"ui"
],
kind: "none"
},
{
prefixes: [
"tools",
"approvals.exec",
"approvals.plugin",
"auth.order",
"auth.profiles",
"broadcast",
"memory.citations",
"worktreeRoot",
"cloudWorkers.projectProfiles",
"security.audit.suppressions",
"security.installPolicy",
"diagnostics.cacheTrace.enabled",
"acp.runtime.installCommand",
"attachments.ttlHours",
"update.checkOnStart",
"update.channel",
"update.auto.enabled",
"telemetry.enabled",
"telemetry.consentedAt"
],
kind: "hot"
},
{
prefixes: ["plugins"],
kind: "hot",
actions: ["reloadPlugins", "disposeMcpRuntimes"]
},
{
prefixes: ["gateway", "discovery"],
kind: "restart"
}
];
let cachedCatalog;
function getReloadPolicyCatalog() {
const registry = getActivePluginHttpRouteRegistry();
const version = getActivePluginHttpRouteRegistryVersion();
if (cachedCatalog?.registry === registry && cachedCatalog.version === version) return cachedCatalog;
const channelPlugins = listChannelPlugins();
const servicePolicies = (registry?.services ?? []).map(({ service }) => ({
prefixes: service.reload?.configPrefixes ?? [],
services: [service.id]
}));
const channelPolicies = channelPlugins.flatMap((plugin) => [{
prefixes: plugin.reload?.configPrefixes ?? [],
kind: "hot",
channels: [plugin],
accountScoped: plugin.reload?.accountScopedRestart
}, {
prefixes: plugin.reload?.noopPrefixes ?? [],
kind: "none",
channels: [plugin]
}]);
const channelRules = expandReloadPolicies(channelPolicies);
const sharedPrefixes = /* @__PURE__ */ new Set([...SHARED_CHANNEL_PREFIXES, ...channelRules.filter(({ prefix }) => SHARED_CHANNEL_PREFIXES.some((root) => matchesReloadPrefix(prefix, root))).map(({ prefix }) => prefix)]);
const ownedRules = expandReloadPolicies([
...CORE_RELOAD_POLICIES,
...(registry?.reloads ?? []).flatMap(({ registration }) => [
["restart", registration.restartPrefixes],
["hot", registration.hotPrefixes],
["none", registration.noopPrefixes]
].map(([kind, prefixes]) => ({
kind,
prefixes: prefixes ?? []
}))),
...Array.from(sharedPrefixes, (prefix) => {
const channels = channelPlugins.filter((plugin) => channelRules.find((rule) => rule.channels?.includes(plugin) && matchesReloadPrefix(prefix, rule.prefix))?.kind !== "none");
const hasService = servicePolicies.some(({ prefixes }) => prefixes.some((owner) => matchesReloadPrefix(prefix, owner)));
return {
prefixes: [prefix],
kind: channels.length || hasService ? "hot" : "none",
channels
};
}),
...channelPolicies,
...channelPlugins.map((plugin) => ({
prefixes: [`plugins.entries.${plugin.id}`],
kind: "hot",
actions: ["reloadPlugins", "disposeMcpRuntimes"],
channels: [plugin]
})),
{
prefixes: ["session.scope", "session.store"],
kind: "hot",
actions: ["refreshHooksPolicy"]
},
...DEFAULT_RELOAD_POLICIES
]);
const rules = [...ownedRules, ...servicePolicies.flatMap(({ prefixes }) => prefixes.map((prefix) => ({
...ownedRules.find((owner) => matchesReloadPrefix(prefix, owner.prefix)),
kind: "hot",
prefix
})))];
for (const rule of rules) rule.services = servicePolicies.filter((service) => service.prefixes.some((owner) => matchesReloadPrefix(rule.prefix, owner))).flatMap((service) => service.services);
rules.sort((a, b) => b.prefix.length - a.prefix.length);
cachedCatalog = {
registry,
version,
rules,
refinementPrefixes: rules.map((rule) => rule.prefix)
};
return cachedCatalog;
}
function listConfigReloadRefinementPrefixes() {
return getReloadPolicyCatalog().refinementPrefixes;
}
function matchRule(path) {
return getReloadPolicyCatalog().rules.find(({ prefix }) => matchesReloadPrefix(path, prefix));
}
function resolveConfigReloadMetadata(path) {
if (isPluginInstallTimestampPath(path)) return { kind: "none" };
return { kind: matchRule(path)?.kind ?? "restart" };
}
function isPluginInstallTimestampPath(path) {
return /^plugins\.installs\..+\.(installedAt|resolvedAt)$/.test(path);
}
function getPluginInstallRecords(config) {
if (!isPlainObject(config)) return {};
const plugins = config.plugins;
if (!isPlainObject(plugins)) return {};
const installs = plugins.installs;
return isPlainObject(installs) ? installs : {};
}
function listPluginInstallRecordDiffPaths(prevConfig, nextConfig, visit) {
const prevInstalls = getPluginInstallRecords(prevConfig);
const nextInstalls = getPluginInstallRecords(nextConfig);
const ids = /* @__PURE__ */ new Set([...Object.keys(prevInstalls), ...Object.keys(nextInstalls)]);
const paths = [];
for (const id of ids) visit({
id,
prevRecord: prevInstalls[id],
nextRecord: nextInstalls[id],
paths
});
return paths;
}
function listPluginInstallTimestampMetadataPaths(prevConfig, nextConfig) {
return listPluginInstallRecordDiffPaths(prevConfig, nextConfig, ({ id, prevRecord, nextRecord, paths }) => {
if (!isPlainObject(prevRecord) || !isPlainObject(nextRecord)) return;
for (const key of PLUGIN_INSTALL_TIMESTAMP_KEYS) if (prevRecord[key] !== nextRecord[key]) paths.push(`plugins.installs.${id}.${key}`);
});
}
function listPluginInstallWholeRecordPaths(prevConfig, nextConfig) {
return listPluginInstallRecordDiffPaths(prevConfig, nextConfig, ({ id, prevRecord, nextRecord, paths }) => {
if (!isPlainObject(prevRecord) || !isPlainObject(nextRecord)) paths.push(`plugins.installs.${id}`);
});
}
function extractAccountIdFromPath(channel, path) {
const prefix = `channels.${channel}.accounts.`;
const id = path.startsWith(prefix) ? path.slice(prefix.length).split(".", 1)[0] : void 0;
return id && id !== "default" ? id : null;
}
function isResolvableChannelAccount(params) {
try {
if (!params.plugin.config.listAccountIds(params.config).includes(params.accountId)) return false;
params.plugin.config.resolveAccount(params.config, params.accountId);
return true;
} catch {
return false;
}
}
function buildGatewayReloadPlan(changedPaths, options = {}) {
const noopPaths = new Set(options.noopPaths);
const forceChangedPaths = new Set(options.forceChangedPaths);
const restartChannelAccounts = /* @__PURE__ */ new Map();
const plan = {
changedPaths,
restartGateway: false,
restartReasons: [],
hotReasons: [],
reloadHooks: false,
reloadInternalHooks: false,
restartGmailWatcher: false,
restartCron: false,
restartHeartbeat: false,
reconcileSystemJobs: false,
reloadPlugins: false,
restartChannels: /* @__PURE__ */ new Set(),
restartServices: /* @__PURE__ */ new Set(),
disposeMcpRuntimes: false,
restartChannelAccounts,
noopPaths: []
};
for (const path of changedPaths) {
if (!forceChangedPaths.has(path) && (noopPaths.size > 0 ? noopPaths.has(path) : isPluginInstallTimestampPath(path))) {
plan.noopPaths.push(path);
continue;
}
const rule = matchRule(path);
const kind = rule?.kind ?? "restart";
const isCredentialRotation = rule && AUTH_CREDENTIAL_PATHS.includes(rule.prefix) && canHotReloadGatewayAuthCredentials(options.previousConfig, options.candidateConfig);
if (kind === "restart" && !isCredentialRotation) {
plan.restartGateway = true;
plan.restartReasons.push(path);
continue;
}
if (kind === "none") {
plan.noopPaths.push(path);
continue;
}
plan.hotReasons.push(path);
for (const action of rule?.actions ?? []) plan[action] = true;
for (const service of rule?.services ?? []) plan.restartServices?.add(service);
for (const plugin of rule?.channels ?? []) {
const accountId = rule?.accountScoped ? extractAccountIdFromPath(plugin.id, path) : null;
if (accountId === null || options.candidateConfig && !isResolvableChannelAccount({
plugin,
accountId,
config: options.candidateConfig
})) {
plan.restartChannels.add(plugin.id);
continue;
}
const accounts = restartChannelAccounts.get(plugin.id) ?? /* @__PURE__ */ new Set();
accounts.add(accountId);
restartChannelAccounts.set(plugin.id, accounts);
}
}
for (const channel of plan.restartChannels) restartChannelAccounts.delete(channel);
return plan;
}
//#endregion
export { listPluginInstallWholeRecordPaths as a, listPluginInstallTimestampMetadataPaths as i, isNoopGatewayReloadPlan as n, resolveConfigReloadMetadata as o, listConfigReloadRefinementPrefixes as r, buildGatewayReloadPlan as t };