Slack: move group policy behind plugin boundary

This commit is contained in:
Gustavo Madeira Santana 2026-03-18 03:26:21 +00:00
parent 889011c08c
commit 9e556f75f5
No known key found for this signature in database
10 changed files with 140 additions and 111 deletions

View File

@ -6,6 +6,7 @@ export * from "./src/blocks-render.js";
export * from "./src/http/index.js";
export * from "./src/interactive-replies.js";
export * from "./src/message-actions.js";
export * from "./src/group-policy.js";
export * from "./src/sent-thread-cache.js";
export * from "./src/targets.js";
export * from "./src/threading-tool-context.js";

View File

@ -20,8 +20,6 @@ import {
PAIRING_APPROVED_MESSAGE,
projectCredentialSnapshotFields,
resolveConfiguredFromRequiredCredentialStatuses,
resolveSlackGroupRequireMention,
resolveSlackGroupToolPolicy,
type ChannelPlugin,
type OpenClawConfig,
type SlackActionContext,
@ -36,6 +34,7 @@ import {
import { parseSlackBlocksInput } from "./blocks-input.js";
import { createSlackActions } from "./channel-actions.js";
import { createSlackWebClient } from "./client.js";
import { resolveSlackGroupRequireMention, resolveSlackGroupToolPolicy } from "./group-policy.js";
import { isSlackInteractiveRepliesEnabled } from "./interactive-replies.js";
import { normalizeAllowListLower } from "./monitor/allow-list.js";
import type { SlackProbe } from "./probe.js";

View File

@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { resolveSlackGroupRequireMention, resolveSlackGroupToolPolicy } from "./group-policy.js";
const cfg = {
channels: {
slack: {
botToken: "xoxb-test",
appToken: "xapp-test",
channels: {
alerts: {
requireMention: false,
tools: { allow: ["message.send"] },
toolsBySender: {
"id:user:alice": { allow: ["sessions.list"] },
},
},
"*": {
requireMention: true,
tools: { deny: ["exec"] },
},
},
},
},
// oxlint-disable-next-line typescript/no-explicit-any
} as any;
describe("slack group policy", () => {
it("uses matched channel requireMention and wildcard fallback", () => {
expect(resolveSlackGroupRequireMention({ cfg, groupChannel: "#alerts" })).toBe(false);
expect(resolveSlackGroupRequireMention({ cfg, groupChannel: "#missing" })).toBe(true);
});
it("resolves sender override, then channel tools, then wildcard tools", () => {
const senderOverride = resolveSlackGroupToolPolicy({
cfg,
groupChannel: "#alerts",
senderId: "user:alice",
});
expect(senderOverride).toEqual({ allow: ["sessions.list"] });
const channelTools = resolveSlackGroupToolPolicy({
cfg,
groupChannel: "#alerts",
senderId: "user:bob",
});
expect(channelTools).toEqual({ allow: ["message.send"] });
const wildcardTools = resolveSlackGroupToolPolicy({
cfg,
groupChannel: "#missing",
senderId: "user:bob",
});
expect(wildcardTools).toEqual({ deny: ["exec"] });
});
});

View File

@ -0,0 +1,74 @@
import {
resolveToolsBySender,
type GroupToolPolicyBySenderConfig,
type GroupToolPolicyConfig,
} from "openclaw/plugin-sdk/channel-policy";
import { type ChannelGroupContext } from "openclaw/plugin-sdk/channel-runtime";
import { normalizeHyphenSlug } from "openclaw/plugin-sdk/core";
import { inspectSlackAccount } from "./account-inspect.js";
type SlackChannelPolicyEntry = {
requireMention?: boolean;
tools?: GroupToolPolicyConfig;
toolsBySender?: GroupToolPolicyBySenderConfig;
};
function resolveSlackChannelPolicyEntry(
params: ChannelGroupContext,
): SlackChannelPolicyEntry | undefined {
const account = inspectSlackAccount({
cfg: params.cfg,
accountId: params.accountId,
});
const channels = (account.channels ?? {}) as Record<string, SlackChannelPolicyEntry>;
if (Object.keys(channels).length === 0) {
return undefined;
}
const channelId = params.groupId?.trim();
const groupChannel = params.groupChannel;
const channelName = groupChannel?.replace(/^#/, "");
const normalizedName = normalizeHyphenSlug(channelName);
const candidates = [
channelId ?? "",
channelName ? `#${channelName}` : "",
channelName ?? "",
normalizedName,
].filter(Boolean);
for (const candidate of candidates) {
if (candidate && channels[candidate]) {
return channels[candidate];
}
}
return channels["*"];
}
function resolveSenderToolsEntry(
entry: SlackChannelPolicyEntry | undefined,
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;
}
export function resolveSlackGroupRequireMention(params: ChannelGroupContext): boolean {
const resolved = resolveSlackChannelPolicyEntry(params);
if (typeof resolved?.requireMention === "boolean") {
return resolved.requireMention;
}
return true;
}
export function resolveSlackGroupToolPolicy(
params: ChannelGroupContext,
): GroupToolPolicyConfig | undefined {
return resolveSenderToolsEntry(resolveSlackChannelPolicyEntry(params), params);
}

View File

@ -22,4 +22,5 @@ export {
export { monitorSlackProvider } from "./monitor.js";
export { probeSlack } from "./probe.js";
export { sendMessageSlack } from "./send.js";
export { resolveSlackGroupRequireMention, resolveSlackGroupToolPolicy } from "./group-policy.js";
export { resolveSlackAppToken, resolveSlackBotToken } from "./token.js";

View File

@ -6,65 +6,10 @@ import {
resolveDiscordGroupToolPolicy,
resolveLineGroupRequireMention,
resolveLineGroupToolPolicy,
resolveSlackGroupRequireMention,
resolveSlackGroupToolPolicy,
resolveTelegramGroupRequireMention,
resolveTelegramGroupToolPolicy,
} from "./group-mentions.js";
const cfg = {
channels: {
slack: {
botToken: "xoxb-test",
appToken: "xapp-test",
channels: {
alerts: {
requireMention: false,
tools: { allow: ["message.send"] },
toolsBySender: {
"id:user:alice": { allow: ["sessions.list"] },
},
},
"*": {
requireMention: true,
tools: { deny: ["exec"] },
},
},
},
},
// oxlint-disable-next-line typescript/no-explicit-any
} as any;
describe("group mentions (slack)", () => {
it("uses matched channel requireMention and wildcard fallback", () => {
expect(resolveSlackGroupRequireMention({ cfg, groupChannel: "#alerts" })).toBe(false);
expect(resolveSlackGroupRequireMention({ cfg, groupChannel: "#missing" })).toBe(true);
});
it("resolves sender override, then channel tools, then wildcard tools", () => {
const senderOverride = resolveSlackGroupToolPolicy({
cfg,
groupChannel: "#alerts",
senderId: "user:alice",
});
expect(senderOverride).toEqual({ allow: ["sessions.list"] });
const channelTools = resolveSlackGroupToolPolicy({
cfg,
groupChannel: "#alerts",
senderId: "user:bob",
});
expect(channelTools).toEqual({ allow: ["message.send"] });
const wildcardTools = resolveSlackGroupToolPolicy({
cfg,
groupChannel: "#missing",
senderId: "user:bob",
});
expect(wildcardTools).toEqual({ deny: ["exec"] });
});
});
describe("group mentions (telegram)", () => {
it("resolves topic-level requireMention and chat-level tools for topic ids", () => {
const telegramCfg = {

View File

@ -10,8 +10,7 @@ import type {
GroupToolPolicyConfig,
} from "../../config/types.tools.js";
import { resolveExactLineGroupConfigKey } from "../../line/group-keys.js";
import { inspectSlackAccount } from "../../plugin-sdk/slack.js";
import { normalizeAtHashSlug, normalizeHyphenSlug } from "../../shared/string-normalization.js";
import { normalizeAtHashSlug } from "../../shared/string-normalization.js";
import type { ChannelGroupContext } from "./types.js";
type GroupMentionParams = ChannelGroupContext;
@ -110,12 +109,6 @@ function resolveDiscordChannelEntry<TEntry>(
);
}
type SlackChannelPolicyEntry = {
requireMention?: boolean;
tools?: GroupToolPolicyConfig;
toolsBySender?: GroupToolPolicyBySenderConfig;
};
type SenderScopedToolsEntry = {
tools?: GroupToolPolicyConfig;
toolsBySender?: GroupToolPolicyBySenderConfig;
@ -129,35 +122,6 @@ type ChannelGroupPolicyChannel =
| "bluebubbles"
| "line";
function resolveSlackChannelPolicyEntry(
params: GroupMentionParams,
): SlackChannelPolicyEntry | undefined {
const account = inspectSlackAccount({
cfg: params.cfg,
accountId: params.accountId,
});
const channels = (account.channels ?? {}) as Record<string, SlackChannelPolicyEntry>;
if (Object.keys(channels).length === 0) {
return undefined;
}
const channelId = params.groupId?.trim();
const groupChannel = params.groupChannel;
const channelName = groupChannel?.replace(/^#/, "");
const normalizedName = normalizeHyphenSlug(channelName);
const candidates = [
channelId ?? "",
channelName ? `#${channelName}` : "",
channelName ?? "",
normalizedName,
].filter(Boolean);
for (const candidate of candidates) {
if (candidate && channels[candidate]) {
return channels[candidate];
}
}
return channels["*"];
}
function resolveChannelRequireMention(
params: GroupMentionParams,
channel: ChannelGroupPolicyChannel,
@ -270,14 +234,6 @@ export function resolveGoogleChatGroupToolPolicy(
return resolveChannelToolPolicyForSender(params, "googlechat");
}
export function resolveSlackGroupRequireMention(params: GroupMentionParams): boolean {
const resolved = resolveSlackChannelPolicyEntry(params);
if (typeof resolved?.requireMention === "boolean") {
return resolved.requireMention;
}
return true;
}
export function resolveBlueBubblesGroupRequireMention(params: GroupMentionParams): boolean {
return resolveChannelRequireMention(params, "bluebubbles");
}
@ -312,13 +268,6 @@ export function resolveDiscordGroupToolPolicy(
return resolveSenderToolsEntry(context.guildEntry, params);
}
export function resolveSlackGroupToolPolicy(
params: GroupMentionParams,
): GroupToolPolicyConfig | undefined {
const resolved = resolveSlackChannelPolicyEntry(params);
return resolveSenderToolsEntry(resolved, params);
}
export function resolveBlueBubblesGroupToolPolicy(
params: GroupMentionParams,
): GroupToolPolicyConfig | undefined {

View File

@ -1,4 +1,8 @@
/** Shared policy warnings and DM/group policy helpers for channel plugins. */
export type {
GroupToolPolicyBySenderConfig,
GroupToolPolicyConfig,
} from "../config/types.tools.js";
export {
buildOpenGroupPolicyConfigureRouteAllowlistWarning,
buildOpenGroupPolicyRestrictSendersWarning,
@ -10,7 +14,7 @@ export {
collectOpenProviderGroupPolicyWarnings,
} from "../channels/plugins/group-policy-warnings.js";
export { buildAccountScopedDmSecurityPolicy } from "../channels/plugins/helpers.js";
export { resolveChannelGroupRequireMention } from "../config/group-policy.js";
export { resolveChannelGroupRequireMention, resolveToolsBySender } from "../config/group-policy.js";
export {
DM_GROUP_ACCESS_REASON,
readStoreAllowFromForDmPolicy,

View File

@ -89,6 +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 { resolveTailnetHostWithRunner } from "../shared/tailscale-status.js";
export type {

View File

@ -43,7 +43,7 @@ export {
export {
resolveSlackGroupRequireMention,
resolveSlackGroupToolPolicy,
} from "../channels/plugins/group-mentions.js";
} from "../../extensions/slack/src/group-policy.js";
export { SlackConfigSchema } from "../config/zod-schema.providers-core.js";
export { buildComputedAccountStatusSnapshot } from "./status-helpers.js";