fix: add channel read-only security adapters

This commit is contained in:
Gustavo Madeira Santana
2026-04-20 18:15:30 -04:00
parent c10b48a43c
commit c7365fd583
4 changed files with 159 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
import { createOpenProviderConfiguredRouteWarningCollector } from "openclaw/plugin-sdk/channel-policy";
import type { ResolvedDiscordAccount } from "./accounts.js";
import type { ChannelPlugin } from "./channel-api.js";
const resolveDiscordDmPolicy = createScopedDmSecurityResolver<ResolvedDiscordAccount>({
channelKey: "discord",
resolvePolicy: (account) => account.config.dm?.policy,
resolveAllowFrom: (account) => account.config.dm?.allowFrom,
allowFromPathSuffix: "dm.",
normalizeEntry: (raw) =>
raw
.trim()
.replace(/^(discord|user):/i, "")
.replace(/^<@!?(\d+)>$/, "$1"),
});
const collectDiscordSecurityWarnings =
createOpenProviderConfiguredRouteWarningCollector<ResolvedDiscordAccount>({
providerConfigPresent: (cfg) => cfg.channels?.discord !== undefined,
resolveGroupPolicy: (account) => account.config.groupPolicy,
resolveRouteAllowlistConfigured: (account) =>
Object.keys(account.config.guilds ?? {}).length > 0,
configureRouteAllowlist: {
surface: "Discord guilds",
openScope: "any channel not explicitly denied",
groupPolicyPath: "channels.discord.groupPolicy",
routeAllowlistPath: "channels.discord.guilds.<id>.channels",
},
missingRouteAllowlist: {
surface: "Discord guilds",
openBehavior: "with no guild/channel allowlist; any channel can trigger (mention-gated)",
remediation:
'Set channels.discord.groupPolicy="allowlist" and configure channels.discord.guilds.<id>.channels',
},
});
let discordSecurityAuditModulePromise:
| Promise<typeof import("./security-audit.runtime.js")>
| undefined;
async function loadDiscordSecurityAuditModule() {
discordSecurityAuditModulePromise ??= import("./security-audit.runtime.js");
return await discordSecurityAuditModulePromise;
}
export const discordSecurityAdapter = {
resolveDmPolicy: resolveDiscordDmPolicy,
collectWarnings: collectDiscordSecurityWarnings,
collectAuditFindings: async (params) =>
(await loadDiscordSecurityAuditModule()).collectDiscordSecurityAuditFindings(params),
} satisfies NonNullable<ChannelPlugin<ResolvedDiscordAccount>["security"]>;

View File

@@ -0,0 +1,43 @@
import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
import { createOpenProviderConfiguredRouteWarningCollector } from "openclaw/plugin-sdk/channel-policy";
import type { ResolvedSlackAccount } from "./accounts.js";
import type { ChannelPlugin } from "./channel-api.js";
import { collectSlackSecurityAuditFindings } from "./security-audit.js";
const resolveSlackDmPolicy = createScopedDmSecurityResolver<ResolvedSlackAccount>({
channelKey: "slack",
resolvePolicy: (account) => account.dm?.policy,
resolveAllowFrom: (account) => account.dm?.allowFrom,
allowFromPathSuffix: "dm.",
normalizeEntry: (raw) =>
raw
.trim()
.replace(/^(slack|user):/i, "")
.trim(),
});
const collectSlackSecurityWarnings =
createOpenProviderConfiguredRouteWarningCollector<ResolvedSlackAccount>({
providerConfigPresent: (cfg) => cfg.channels?.slack !== undefined,
resolveGroupPolicy: (account) => account.config.groupPolicy,
resolveRouteAllowlistConfigured: (account) =>
Boolean(account.config.channels) && Object.keys(account.config.channels ?? {}).length > 0,
configureRouteAllowlist: {
surface: "Slack channels",
openScope: "any channel not explicitly denied",
groupPolicyPath: "channels.slack.groupPolicy",
routeAllowlistPath: "channels.slack.channels",
},
missingRouteAllowlist: {
surface: "Slack channels",
openBehavior: "with no channel allowlist; any channel can trigger (mention-gated)",
remediation:
'Set channels.slack.groupPolicy="allowlist" and configure channels.slack.channels',
},
});
export const slackSecurityAdapter = {
resolveDmPolicy: resolveSlackDmPolicy,
collectWarnings: collectSlackSecurityWarnings,
collectAuditFindings: collectSlackSecurityAuditFindings,
} satisfies NonNullable<ChannelPlugin<ResolvedSlackAccount>["security"]>;

View File

@@ -0,0 +1,36 @@
import { createAllowlistProviderRouteAllowlistWarningCollector } from "openclaw/plugin-sdk/channel-policy";
import type { ResolvedTelegramAccount } from "./accounts.js";
import { collectTelegramSecurityAuditFindings } from "./security-audit.js";
const collectTelegramSecurityWarnings =
createAllowlistProviderRouteAllowlistWarningCollector<ResolvedTelegramAccount>({
providerConfigPresent: (cfg) => cfg.channels?.telegram !== undefined,
resolveGroupPolicy: (account) => account.config.groupPolicy,
resolveRouteAllowlistConfigured: (account) =>
Boolean(account.config.groups) && Object.keys(account.config.groups ?? {}).length > 0,
restrictSenders: {
surface: "Telegram groups",
openScope: "any member in allowed groups",
groupPolicyPath: "channels.telegram.groupPolicy",
groupAllowFromPath: "channels.telegram.groupAllowFrom",
},
noRouteAllowlist: {
surface: "Telegram groups",
routeAllowlistPath: "channels.telegram.groups",
routeScope: "group",
groupPolicyPath: "channels.telegram.groupPolicy",
groupAllowFromPath: "channels.telegram.groupAllowFrom",
},
});
export const telegramSecurityAdapter = {
dm: {
channelKey: "telegram",
resolvePolicy: (account: ResolvedTelegramAccount) => account.config.dmPolicy,
resolveAllowFrom: (account: ResolvedTelegramAccount) => account.config.allowFrom,
policyPathSuffix: "dmPolicy",
normalizeEntry: (raw: string) => raw.replace(/^(telegram|tg):/i, ""),
},
collectWarnings: collectTelegramSecurityWarnings,
collectAuditFindings: collectTelegramSecurityAuditFindings,
};

View File

@@ -0,0 +1,28 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { listPotentialConfiguredChannelIds } from "../config-presence.js";
import { getBundledChannelSetupPlugin } from "./bundled.js";
import { listChannelPlugins } from "./registry.js";
import type { ChannelPlugin } from "./types.plugin.js";
export function listReadOnlyChannelPluginsForConfig(
cfg: OpenClawConfig,
env: NodeJS.ProcessEnv = process.env,
): ChannelPlugin[] {
const byId = new Map<string, ChannelPlugin>();
for (const plugin of listChannelPlugins()) {
byId.set(plugin.id, plugin);
}
for (const channelId of listPotentialConfiguredChannelIds(cfg, env)) {
if (byId.has(channelId)) {
continue;
}
const setupPlugin = getBundledChannelSetupPlugin(channelId);
if (setupPlugin) {
byId.set(setupPlugin.id, setupPlugin);
}
}
return [...byId.values()];
}