mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 22:50:22 +00:00
Discord: move group policy behind plugin boundary
This commit is contained in:
@@ -209,3 +209,42 @@ describe("discordPlugin outbound", () => {
|
||||
expect(runtimeMonitorDiscordProvider).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("discordPlugin groups", () => {
|
||||
it("uses plugin-owned group policy resolvers", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
token: "discord-test",
|
||||
guilds: {
|
||||
guild1: {
|
||||
requireMention: false,
|
||||
tools: { allow: ["message.guild"] },
|
||||
channels: {
|
||||
"123": {
|
||||
requireMention: true,
|
||||
tools: { allow: ["message.channel"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
discordPlugin.groups?.resolveRequireMention?.({
|
||||
cfg,
|
||||
groupSpace: "guild1",
|
||||
groupId: "123",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
discordPlugin.groups?.resolveToolPolicy?.({
|
||||
cfg,
|
||||
groupSpace: "guild1",
|
||||
groupId: "123",
|
||||
}),
|
||||
).toEqual({ allow: ["message.channel"] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,8 +21,6 @@ import {
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
projectCredentialSnapshotFields,
|
||||
resolveConfiguredFromCredentialStatuses,
|
||||
resolveDiscordGroupRequireMention,
|
||||
resolveDiscordGroupToolPolicy,
|
||||
type ChannelMessageActionAdapter,
|
||||
type ChannelPlugin,
|
||||
type OpenClawConfig,
|
||||
@@ -38,6 +36,10 @@ import {
|
||||
isDiscordExecApprovalClientEnabled,
|
||||
shouldSuppressLocalDiscordExecApprovalPrompt,
|
||||
} from "./exec-approvals.js";
|
||||
import {
|
||||
resolveDiscordGroupRequireMention,
|
||||
resolveDiscordGroupToolPolicy,
|
||||
} from "./group-policy.js";
|
||||
import { monitorDiscordProvider } from "./monitor.js";
|
||||
import {
|
||||
looksLikeDiscordTargetId,
|
||||
|
||||
79
extensions/discord/src/group-policy.test.ts
Normal file
79
extensions/discord/src/group-policy.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
resolveDiscordGroupRequireMention,
|
||||
resolveDiscordGroupToolPolicy,
|
||||
} from "./group-policy.js";
|
||||
|
||||
describe("discord group policy", () => {
|
||||
it("prefers channel policy, then guild policy, with sender-specific overrides", () => {
|
||||
const discordCfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
token: "discord-test",
|
||||
guilds: {
|
||||
guild1: {
|
||||
requireMention: false,
|
||||
tools: { allow: ["message.guild"] },
|
||||
toolsBySender: {
|
||||
"id:user:guild-admin": { allow: ["sessions.list"] },
|
||||
},
|
||||
channels: {
|
||||
"123": {
|
||||
requireMention: true,
|
||||
tools: { allow: ["message.channel"] },
|
||||
toolsBySender: {
|
||||
"id:user:channel-admin": { deny: ["exec"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any;
|
||||
|
||||
expect(
|
||||
resolveDiscordGroupRequireMention({ cfg: discordCfg, groupSpace: "guild1", groupId: "123" }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
resolveDiscordGroupRequireMention({
|
||||
cfg: discordCfg,
|
||||
groupSpace: "guild1",
|
||||
groupId: "missing",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
resolveDiscordGroupToolPolicy({
|
||||
cfg: discordCfg,
|
||||
groupSpace: "guild1",
|
||||
groupId: "123",
|
||||
senderId: "user:channel-admin",
|
||||
}),
|
||||
).toEqual({ deny: ["exec"] });
|
||||
expect(
|
||||
resolveDiscordGroupToolPolicy({
|
||||
cfg: discordCfg,
|
||||
groupSpace: "guild1",
|
||||
groupId: "123",
|
||||
senderId: "user:someone",
|
||||
}),
|
||||
).toEqual({ allow: ["message.channel"] });
|
||||
expect(
|
||||
resolveDiscordGroupToolPolicy({
|
||||
cfg: discordCfg,
|
||||
groupSpace: "guild1",
|
||||
groupId: "missing",
|
||||
senderId: "user:guild-admin",
|
||||
}),
|
||||
).toEqual({ allow: ["sessions.list"] });
|
||||
expect(
|
||||
resolveDiscordGroupToolPolicy({
|
||||
cfg: discordCfg,
|
||||
groupSpace: "guild1",
|
||||
groupId: "missing",
|
||||
senderId: "user:someone",
|
||||
}),
|
||||
).toEqual({ allow: ["message.guild"] });
|
||||
});
|
||||
});
|
||||
111
extensions/discord/src/group-policy.ts
Normal file
111
extensions/discord/src/group-policy.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
resolveToolsBySender,
|
||||
type GroupToolPolicyBySenderConfig,
|
||||
type GroupToolPolicyConfig,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
import { type ChannelGroupContext } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { normalizeAtHashSlug } from "openclaw/plugin-sdk/core";
|
||||
import type { DiscordConfig } from "openclaw/plugin-sdk/discord";
|
||||
|
||||
function normalizeDiscordSlug(value?: string | null) {
|
||||
return normalizeAtHashSlug(value);
|
||||
}
|
||||
|
||||
type SenderScopedToolsEntry = {
|
||||
tools?: GroupToolPolicyConfig;
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
requireMention?: boolean;
|
||||
};
|
||||
|
||||
function resolveDiscordGuildEntry(guilds: DiscordConfig["guilds"], groupSpace?: string | null) {
|
||||
if (!guilds || Object.keys(guilds).length === 0) {
|
||||
return null;
|
||||
}
|
||||
const space = groupSpace?.trim() ?? "";
|
||||
if (space && guilds[space]) {
|
||||
return guilds[space];
|
||||
}
|
||||
const normalized = normalizeDiscordSlug(space);
|
||||
if (normalized && guilds[normalized]) {
|
||||
return guilds[normalized];
|
||||
}
|
||||
if (normalized) {
|
||||
const match = Object.values(guilds).find(
|
||||
(entry) => normalizeDiscordSlug(entry?.slug ?? undefined) === normalized,
|
||||
);
|
||||
if (match) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return guilds["*"] ?? null;
|
||||
}
|
||||
|
||||
function resolveDiscordChannelEntry<TEntry extends SenderScopedToolsEntry>(
|
||||
channelEntries: Record<string, TEntry> | undefined,
|
||||
params: { groupId?: string | null; groupChannel?: string | null },
|
||||
): TEntry | undefined {
|
||||
if (!channelEntries || Object.keys(channelEntries).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const groupChannel = params.groupChannel;
|
||||
const channelSlug = normalizeDiscordSlug(groupChannel);
|
||||
return (
|
||||
(params.groupId ? channelEntries[params.groupId] : undefined) ??
|
||||
(channelSlug
|
||||
? (channelEntries[channelSlug] ?? channelEntries[`#${channelSlug}`])
|
||||
: undefined) ??
|
||||
(groupChannel ? channelEntries[normalizeDiscordSlug(groupChannel)] : undefined)
|
||||
);
|
||||
}
|
||||
|
||||
function resolveSenderToolsEntry(
|
||||
entry: SenderScopedToolsEntry | undefined | null,
|
||||
params: ChannelGroupContext,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
const senderPolicy = resolveToolsBySender({
|
||||
toolsBySender: entry.toolsBySender,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
return senderPolicy ?? entry.tools;
|
||||
}
|
||||
|
||||
function resolveDiscordPolicyContext(params: ChannelGroupContext) {
|
||||
const guildEntry = resolveDiscordGuildEntry(
|
||||
params.cfg.channels?.discord?.guilds,
|
||||
params.groupSpace,
|
||||
);
|
||||
const channelEntries = guildEntry?.channels;
|
||||
const channelEntry =
|
||||
channelEntries && Object.keys(channelEntries).length > 0
|
||||
? resolveDiscordChannelEntry(channelEntries, params)
|
||||
: undefined;
|
||||
return { guildEntry, channelEntry };
|
||||
}
|
||||
|
||||
export function resolveDiscordGroupRequireMention(params: ChannelGroupContext): boolean {
|
||||
const context = resolveDiscordPolicyContext(params);
|
||||
if (typeof context.channelEntry?.requireMention === "boolean") {
|
||||
return context.channelEntry.requireMention;
|
||||
}
|
||||
if (typeof context.guildEntry?.requireMention === "boolean") {
|
||||
return context.guildEntry.requireMention;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function resolveDiscordGroupToolPolicy(
|
||||
params: ChannelGroupContext,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
const context = resolveDiscordPolicyContext(params);
|
||||
const channelPolicy = resolveSenderToolsEntry(context.channelEntry, params);
|
||||
if (channelPolicy) {
|
||||
return channelPolicy;
|
||||
}
|
||||
return resolveSenderToolsEntry(context.guildEntry, params);
|
||||
}
|
||||
Reference in New Issue
Block a user