mirror of https://github.com/openclaw/openclaw.git
Discord: move group policy behind plugin boundary
This commit is contained in:
parent
9350cb19dd
commit
0bfaa36126
|
|
@ -3,6 +3,7 @@ export * from "./src/accounts.js";
|
|||
export * from "./src/actions/handle-action.guild-admin.js";
|
||||
export * from "./src/actions/handle-action.js";
|
||||
export * from "./src/components.js";
|
||||
export * from "./src/group-policy.js";
|
||||
export * from "./src/normalize.js";
|
||||
export * from "./src/pluralkit.js";
|
||||
export * from "./src/session-key-normalization.js";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"] });
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@ import { describe, expect, it } from "vitest";
|
|||
import {
|
||||
resolveBlueBubblesGroupRequireMention,
|
||||
resolveBlueBubblesGroupToolPolicy,
|
||||
resolveDiscordGroupRequireMention,
|
||||
resolveDiscordGroupToolPolicy,
|
||||
resolveLineGroupRequireMention,
|
||||
resolveLineGroupToolPolicy,
|
||||
resolveTelegramGroupRequireMention,
|
||||
|
|
@ -45,80 +43,6 @@ describe("group mentions (telegram)", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("group mentions (discord)", () => {
|
||||
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"] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("group mentions (bluebubbles)", () => {
|
||||
it("uses generic channel group policy helpers", () => {
|
||||
const blueBubblesCfg = {
|
||||
|
|
|
|||
|
|
@ -2,23 +2,13 @@ import type { OpenClawConfig } from "../../config/config.js";
|
|||
import {
|
||||
resolveChannelGroupRequireMention,
|
||||
resolveChannelGroupToolsPolicy,
|
||||
resolveToolsBySender,
|
||||
} from "../../config/group-policy.js";
|
||||
import type { DiscordConfig } from "../../config/types.js";
|
||||
import type {
|
||||
GroupToolPolicyBySenderConfig,
|
||||
GroupToolPolicyConfig,
|
||||
} from "../../config/types.tools.js";
|
||||
import type { GroupToolPolicyConfig } from "../../config/types.tools.js";
|
||||
import { resolveExactLineGroupConfigKey } from "../../line/group-keys.js";
|
||||
import { normalizeAtHashSlug } from "../../shared/string-normalization.js";
|
||||
import type { ChannelGroupContext } from "./types.js";
|
||||
|
||||
type GroupMentionParams = ChannelGroupContext;
|
||||
|
||||
function normalizeDiscordSlug(value?: string | null) {
|
||||
return normalizeAtHashSlug(value);
|
||||
}
|
||||
|
||||
function parseTelegramGroupId(value?: string | null) {
|
||||
const raw = value?.trim() ?? "";
|
||||
if (!raw) {
|
||||
|
|
@ -68,52 +58,6 @@ function resolveTelegramRequireMention(params: {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
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>(
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
type SenderScopedToolsEntry = {
|
||||
tools?: GroupToolPolicyConfig;
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
};
|
||||
|
||||
type ChannelGroupPolicyChannel =
|
||||
| "telegram"
|
||||
| "whatsapp"
|
||||
|
|
@ -152,39 +96,6 @@ function resolveChannelToolPolicyForSender(
|
|||
});
|
||||
}
|
||||
|
||||
function resolveSenderToolsEntry(
|
||||
entry: SenderScopedToolsEntry | undefined | null,
|
||||
params: GroupMentionParams,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
const senderPolicy = resolveToolsBySender({
|
||||
toolsBySender: entry.toolsBySender,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
if (senderPolicy) {
|
||||
return senderPolicy;
|
||||
}
|
||||
return entry.tools;
|
||||
}
|
||||
|
||||
function resolveDiscordPolicyContext(params: GroupMentionParams) {
|
||||
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 resolveTelegramGroupRequireMention(
|
||||
params: GroupMentionParams,
|
||||
): boolean | undefined {
|
||||
|
|
@ -213,17 +124,6 @@ export function resolveIMessageGroupRequireMention(params: GroupMentionParams):
|
|||
return resolveChannelRequireMention(params, "imessage");
|
||||
}
|
||||
|
||||
export function resolveDiscordGroupRequireMention(params: GroupMentionParams): 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 resolveGoogleChatGroupRequireMention(params: GroupMentionParams): boolean {
|
||||
return resolveChannelRequireMention(params, "googlechat");
|
||||
}
|
||||
|
|
@ -257,17 +157,6 @@ export function resolveIMessageGroupToolPolicy(
|
|||
return resolveChannelToolPolicyForSender(params, "imessage");
|
||||
}
|
||||
|
||||
export function resolveDiscordGroupToolPolicy(
|
||||
params: GroupMentionParams,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
const context = resolveDiscordPolicyContext(params);
|
||||
const channelPolicy = resolveSenderToolsEntry(context.channelEntry, params);
|
||||
if (channelPolicy) {
|
||||
return channelPolicy;
|
||||
}
|
||||
return resolveSenderToolsEntry(context.guildEntry, params);
|
||||
}
|
||||
|
||||
export function resolveBlueBubblesGroupToolPolicy(
|
||||
params: GroupMentionParams,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export type { SecretFileReadOptions, SecretFileReadResult } from "../infra/secre
|
|||
|
||||
export { resolveGatewayBindUrl } from "../shared/gateway-bind-url.js";
|
||||
export type { GatewayBindUrlResult } from "../shared/gateway-bind-url.js";
|
||||
export { normalizeHyphenSlug } from "../shared/string-normalization.js";
|
||||
export { normalizeAtHashSlug, normalizeHyphenSlug } from "../shared/string-normalization.js";
|
||||
|
||||
export { resolveTailnetHostWithRunner } from "../shared/tailscale-status.js";
|
||||
export type {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export {
|
|||
export {
|
||||
resolveDiscordGroupRequireMention,
|
||||
resolveDiscordGroupToolPolicy,
|
||||
} from "../channels/plugins/group-mentions.js";
|
||||
} from "../../extensions/discord/src/group-policy.js";
|
||||
export { DiscordConfigSchema } from "../config/zod-schema.providers-core.js";
|
||||
|
||||
export {
|
||||
|
|
|
|||
Loading…
Reference in New Issue