mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(config): add canonical group-policy scope-tree resolver and migrate the first six channels (#106846)
* feat(config): add canonical group-policy scope-tree resolver * chore(plugin-sdk): refresh API baseline hash for scope-tree exports * refactor(channels): migrate googlechat, imessage, and whatsapp group policy onto the scope tree * chore(plugin-sdk): refresh API baseline hash for groups scope-tree builder * refactor(channels): migrate line, qqbot, and mattermost group policy onto the scope tree * chore(plugin-sdk): refresh API baseline hash for case-insensitive scope key helper * chore(plugin-sdk): refresh API baseline hash after rebase onto current main * chore(plugin-sdk): refresh API baseline hash after rebase * chore(plugin-sdk): refresh API baseline hash after rebase * chore(plugin-sdk): refresh API baseline hash after rebase
This commit is contained in:
committed by
GitHub
parent
8ac4a52a4e
commit
b9c4815403
@@ -1,2 +1,2 @@
|
||||
d43b631bf84fdef65d44514e34f77dc97de14a3e5016d728e59585a1c47d1b2b plugin-sdk-api-baseline.json
|
||||
5c272b15c12c2f7ff0756c57f33706662702564882b5c9fbdd46c58cf8dcf314 plugin-sdk-api-baseline.jsonl
|
||||
2828a432f8c8fcec39fed6a2f612907aef001e1e5bb7bc295aa3f6d05aa053e5 plugin-sdk-api-baseline.json
|
||||
c847fbaf67e4159a9305f8f985b00696d3d830a84082ef54df222ef627ea22a5 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Googlechat tests cover group policy plugin behavior.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveGoogleChatGroupRequireMention } from "./group-policy.js";
|
||||
|
||||
describe("googlechat group policy", () => {
|
||||
it("resolves exact, wildcard, and unconfigured mention policies", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
googlechat: {
|
||||
groups: {
|
||||
"spaces/exact": { requireMention: false },
|
||||
"*": { requireMention: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveGoogleChatGroupRequireMention({ cfg, groupId: "spaces/exact" })).toBe(false);
|
||||
expect(resolveGoogleChatGroupRequireMention({ cfg, groupId: "spaces/other" })).toBe(true);
|
||||
expect(resolveGoogleChatGroupRequireMention({ cfg: {}, groupId: "spaces/other" })).toBe(true);
|
||||
});
|
||||
|
||||
it("uses account groups instead of root groups", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
googlechat: {
|
||||
groups: { "spaces/exact": { requireMention: false } },
|
||||
accounts: {
|
||||
work: { groups: { "spaces/exact": { requireMention: true } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
resolveGoogleChatGroupRequireMention({ cfg, accountId: "work", groupId: "spaces/exact" }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to root groups for one account with an empty groups map", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
googlechat: {
|
||||
groups: { "spaces/exact": { requireMention: false } },
|
||||
accounts: { work: { groups: {} } },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
resolveGoogleChatGroupRequireMention({ cfg, accountId: "work", groupId: "spaces/exact" }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,17 @@
|
||||
// Googlechat plugin module implements group policy behavior.
|
||||
import { resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy";
|
||||
import {
|
||||
buildChannelGroupsScopeTree,
|
||||
resolveScopeRequireMention,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
||||
|
||||
type GoogleChatGroupContext = {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
groupId?: string | null;
|
||||
};
|
||||
type GroupContext = { cfg: OpenClawConfig; accountId?: string | null; groupId?: string | null };
|
||||
function resolveScopePath(params: GroupContext) {
|
||||
return params.groupId ? [params.groupId] : [];
|
||||
}
|
||||
|
||||
export function resolveGoogleChatGroupRequireMention(params: GoogleChatGroupContext): boolean {
|
||||
return resolveChannelGroupRequireMention({
|
||||
cfg: params.cfg,
|
||||
channel: "googlechat",
|
||||
groupId: params.groupId,
|
||||
accountId: params.accountId,
|
||||
export function resolveGoogleChatGroupRequireMention(params: GroupContext): boolean {
|
||||
return resolveScopeRequireMention({
|
||||
tree: buildChannelGroupsScopeTree(params.cfg, "googlechat", params.accountId),
|
||||
path: resolveScopePath(params),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// Imessage tests cover group policy plugin behavior.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
resolveIMessageGroupRequireMention,
|
||||
resolveIMessageGroupToolPolicy,
|
||||
} from "./group-policy.js";
|
||||
|
||||
describe("imessage group policy", () => {
|
||||
it("resolves exact, wildcard, and unconfigured policies", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
imessage: {
|
||||
groups: {
|
||||
exact: { requireMention: false, tools: { deny: ["exec"] } },
|
||||
"*": { requireMention: true, tools: { allow: ["message.send"] } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveIMessageGroupRequireMention({ cfg, groupId: "exact" })).toBe(false);
|
||||
expect(resolveIMessageGroupRequireMention({ cfg, groupId: "other" })).toBe(true);
|
||||
expect(resolveIMessageGroupToolPolicy({ cfg, groupId: "exact" })).toEqual({
|
||||
deny: ["exec"],
|
||||
});
|
||||
expect(resolveIMessageGroupToolPolicy({ cfg, groupId: "other" })).toEqual({
|
||||
allow: ["message.send"],
|
||||
});
|
||||
expect(resolveIMessageGroupRequireMention({ cfg: {}, groupId: "other" })).toBe(true);
|
||||
expect(resolveIMessageGroupToolPolicy({ cfg: {}, groupId: "other" })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses account groups and preserves the single-account empty fallback", () => {
|
||||
const overrideCfg = {
|
||||
channels: {
|
||||
imessage: {
|
||||
groups: { exact: { requireMention: false } },
|
||||
accounts: { work: { groups: { exact: { requireMention: true } } } },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const fallbackCfg = {
|
||||
channels: {
|
||||
imessage: {
|
||||
groups: { exact: { requireMention: false } },
|
||||
accounts: { work: { groups: {} } },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
resolveIMessageGroupRequireMention({
|
||||
cfg: overrideCfg,
|
||||
accountId: "work",
|
||||
groupId: "exact",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
resolveIMessageGroupRequireMention({
|
||||
cfg: fallbackCfg,
|
||||
accountId: "work",
|
||||
groupId: "exact",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("prefers sender-scoped tools", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
imessage: {
|
||||
groups: {
|
||||
exact: {
|
||||
tools: { deny: ["exec"] },
|
||||
toolsBySender: { "channel:imessage:alice": { allow: ["message.send"] } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveIMessageGroupToolPolicy({ cfg, groupId: "exact", senderId: "alice" })).toEqual({
|
||||
allow: ["message.send"],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
// Imessage plugin module implements group policy behavior.
|
||||
import {
|
||||
resolveChannelGroupRequireMention,
|
||||
resolveChannelGroupToolsPolicy,
|
||||
buildChannelGroupsScopeTree,
|
||||
resolveScopeRequireMention,
|
||||
resolveScopeToolsPolicy,
|
||||
type GroupToolPolicyConfig,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
||||
@@ -16,26 +17,24 @@ type IMessageGroupContext = {
|
||||
senderE164?: string | null;
|
||||
};
|
||||
|
||||
function resolveScopePath(params: IMessageGroupContext) {
|
||||
return params.groupId ? [params.groupId] : [];
|
||||
}
|
||||
|
||||
export function resolveIMessageGroupRequireMention(params: IMessageGroupContext): boolean {
|
||||
return resolveChannelGroupRequireMention({
|
||||
cfg: params.cfg,
|
||||
channel: "imessage",
|
||||
groupId: params.groupId,
|
||||
accountId: params.accountId,
|
||||
return resolveScopeRequireMention({
|
||||
tree: buildChannelGroupsScopeTree(params.cfg, "imessage", params.accountId),
|
||||
path: resolveScopePath(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveIMessageGroupToolPolicy(
|
||||
params: IMessageGroupContext,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
return resolveChannelGroupToolsPolicy({
|
||||
cfg: params.cfg,
|
||||
channel: "imessage",
|
||||
groupId: params.groupId,
|
||||
accountId: params.accountId,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
return resolveScopeToolsPolicy({
|
||||
...params,
|
||||
tree: buildChannelGroupsScopeTree(params.cfg, "imessage", params.accountId),
|
||||
path: resolveScopePath(params),
|
||||
messageProvider: "imessage",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,40 +62,57 @@ describe("account-scoped LINE groups", () => {
|
||||
expect(resolveLineGroupsConfig(cfg, "work")).toEqual({
|
||||
"group:g1": { requireMention: false },
|
||||
});
|
||||
expect(resolveExactLineGroupConfigKey({ cfg, accountId: "work", groupId: "g1" })).toBe(
|
||||
"group:g1",
|
||||
);
|
||||
expect(resolveExactLineGroupConfigKey({ cfg, accountId: "default", groupId: "g1" })).toBe(
|
||||
undefined,
|
||||
);
|
||||
expect(
|
||||
resolveExactLineGroupConfigKey({
|
||||
groups: resolveLineGroupsConfig(cfg, "work"),
|
||||
groupId: "g1",
|
||||
}),
|
||||
).toBe("group:g1");
|
||||
expect(
|
||||
resolveExactLineGroupConfigKey({
|
||||
groups: resolveLineGroupsConfig(cfg, "default"),
|
||||
groupId: "g1",
|
||||
}),
|
||||
).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("line group policy", () => {
|
||||
it("matches raw and prefixed LINE group keys for requireMention", () => {
|
||||
it("preserves candidate precedence and falls back to wildcard", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
line: {
|
||||
groups: {
|
||||
"room:r123": {
|
||||
same: {
|
||||
requireMention: false,
|
||||
},
|
||||
"group:g123": {
|
||||
"group:same": {
|
||||
requireMention: true,
|
||||
},
|
||||
"room:same": {
|
||||
requireMention: true,
|
||||
},
|
||||
"group:typed": {
|
||||
requireMention: false,
|
||||
},
|
||||
"room:typed": {
|
||||
requireMention: true,
|
||||
},
|
||||
"*": {
|
||||
requireMention: true,
|
||||
requireMention: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveLineGroupRequireMention({ cfg, groupId: "r123" })).toBe(false);
|
||||
expect(resolveLineGroupRequireMention({ cfg, groupId: "room:r123" })).toBe(false);
|
||||
expect(resolveLineGroupRequireMention({ cfg, groupId: "g123" })).toBe(false);
|
||||
expect(resolveLineGroupRequireMention({ cfg, groupId: "group:g123" })).toBe(false);
|
||||
expect(resolveLineGroupRequireMention({ cfg, groupId: "other" })).toBe(true);
|
||||
expect(resolveLineGroupRequireMention({ cfg, groupId: "same" })).toBe(false);
|
||||
expect(resolveLineGroupRequireMention({ cfg, groupId: "room:same" })).toBe(false);
|
||||
expect(resolveLineGroupRequireMention({ cfg, groupId: "group:same" })).toBe(false);
|
||||
expect(resolveLineGroupRequireMention({ cfg, groupId: "typed" })).toBe(false);
|
||||
expect(resolveLineGroupRequireMention({ cfg, groupId: "group:typed" })).toBe(false);
|
||||
expect(resolveLineGroupRequireMention({ cfg, groupId: "room:typed" })).toBe(true);
|
||||
expect(resolveLineGroupRequireMention({ cfg, groupId: "other" })).toBe(false);
|
||||
});
|
||||
|
||||
it("uses account-scoped prefixed LINE group config for requireMention", () => {
|
||||
|
||||
@@ -52,11 +52,10 @@ export function resolveLineGroupsConfig(
|
||||
}
|
||||
|
||||
export function resolveExactLineGroupConfigKey(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
groups: Record<string, unknown> | undefined;
|
||||
groupId?: string | null;
|
||||
}): string | undefined {
|
||||
const groups = resolveLineGroupsConfig(params.cfg, params.accountId);
|
||||
const { groups } = params;
|
||||
if (!groups) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
// Line plugin module implements group policy behavior.
|
||||
import { resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy";
|
||||
import {
|
||||
buildChannelGroupsScopeTree,
|
||||
resolveScopeRequireMention,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
import { resolveExactLineGroupConfigKey, type OpenClawConfig } from "./channel-api.js";
|
||||
|
||||
type LineGroupContext = {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
groupId?: string | null;
|
||||
};
|
||||
type LineGroupContext = { cfg: OpenClawConfig; accountId?: string | null; groupId?: string | null };
|
||||
|
||||
export function resolveLineGroupRequireMention(params: LineGroupContext): boolean {
|
||||
const exactGroupId = resolveExactLineGroupConfigKey({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
const tree = buildChannelGroupsScopeTree(params.cfg, "line", params.accountId);
|
||||
const matchedKey = resolveExactLineGroupConfigKey({
|
||||
groups: tree.scopes,
|
||||
groupId: params.groupId,
|
||||
});
|
||||
return resolveChannelGroupRequireMention({
|
||||
cfg: params.cfg,
|
||||
channel: "line",
|
||||
groupId: exactGroupId ?? params.groupId,
|
||||
accountId: params.accountId,
|
||||
return resolveScopeRequireMention({
|
||||
tree,
|
||||
path: matchedKey ? [matchedKey] : [],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,17 +15,24 @@ describe("resolveMattermostGroupRequireMention", () => {
|
||||
expect(requireMention).toBe(true);
|
||||
});
|
||||
|
||||
it("respects chatmode-derived account override", () => {
|
||||
it("lets groups config beat chatmode and chatmode beat the final default", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
mattermost: {
|
||||
chatmode: "onmessage",
|
||||
groups: {
|
||||
calls: { requireMention: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const requireMention = resolveMattermostGroupRequireMention({ cfg, accountId: "default" });
|
||||
expect(requireMention).toBe(false);
|
||||
expect(
|
||||
resolveMattermostGroupRequireMention({ cfg, accountId: "default", groupId: "calls" }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
resolveMattermostGroupRequireMention({ cfg, accountId: "default", groupId: "other" }),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("prefers an explicit runtime override when provided", () => {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Mattermost plugin module implements group mentions behavior.
|
||||
import { resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy";
|
||||
import {
|
||||
buildChannelGroupsScopeTree,
|
||||
resolveScopeRequireMention,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
import { resolveMattermostAccount } from "./mattermost/accounts.js";
|
||||
import type { ChannelGroupContext } from "./runtime-api.js";
|
||||
|
||||
@@ -10,15 +13,10 @@ export function resolveMattermostGroupRequireMention(
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
const requireMentionOverride =
|
||||
typeof params.requireMentionOverride === "boolean"
|
||||
? params.requireMentionOverride
|
||||
: account.requireMention;
|
||||
return resolveChannelGroupRequireMention({
|
||||
cfg: params.cfg,
|
||||
channel: "mattermost",
|
||||
groupId: params.groupId,
|
||||
accountId: params.accountId,
|
||||
requireMentionOverride,
|
||||
return resolveScopeRequireMention({
|
||||
tree: buildChannelGroupsScopeTree(params.cfg, "mattermost", params.accountId),
|
||||
path: params.groupId ? [params.groupId] : [],
|
||||
requireMentionOverride: params.requireMentionOverride ?? account.requireMention,
|
||||
overrideOrder: "after-config",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
buildChannelGroupsScopeTree,
|
||||
resolveScopeKeyCaseInsensitive,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
// Qqbot tests cover shared group tool policy behavior.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { describe, expect, it } from "vitest";
|
||||
@@ -5,19 +9,20 @@ import { qqbotPlugin } from "./channel.js";
|
||||
import { resolveQQBotGroupToolPolicy } from "./group-policy.js";
|
||||
|
||||
describe("qqbot group tool policy", () => {
|
||||
it("resolves canonical per-group tools config", () => {
|
||||
it("prefers an exact group key over a case-insensitive match", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
qqbot: {
|
||||
groups: {
|
||||
G1: { tools: { deny: ["*"] } },
|
||||
g1: { tools: { allow: ["case-insensitive"] } },
|
||||
G1: { tools: { deny: ["exact"] } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveQQBotGroupToolPolicy({ cfg, groupId: "G1" })).toStrictEqual({
|
||||
deny: ["*"],
|
||||
deny: ["exact"],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,7 +51,7 @@ describe("qqbot group tool policy", () => {
|
||||
).toStrictEqual({ deny: ["*"] });
|
||||
});
|
||||
|
||||
it("matches mixed-case group ids after session-key normalization", () => {
|
||||
it("uses a case-insensitive group key when no exact key exists", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
qqbot: {
|
||||
@@ -71,6 +76,24 @@ describe("qqbot group tool policy", () => {
|
||||
).toStrictEqual({ deny: ["*"] });
|
||||
});
|
||||
|
||||
it("keeps wildcard defaults out of case-insensitive scope matching", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
qqbot: {
|
||||
groups: {
|
||||
"*": { tools: { deny: ["default"] } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const tree = buildChannelGroupsScopeTree(cfg, "qqbot");
|
||||
|
||||
expect(resolveScopeKeyCaseInsensitive(tree, "*")).toBeUndefined();
|
||||
expect(resolveQQBotGroupToolPolicy({ cfg, groupId: "*" })).toStrictEqual({
|
||||
deny: ["default"],
|
||||
});
|
||||
});
|
||||
|
||||
it("registers the resolver on the channel plugin", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
// Qqbot plugin module implements group tool policy behavior.
|
||||
import type { ChannelGroupContext } from "openclaw/plugin-sdk/channel-contract";
|
||||
import {
|
||||
resolveChannelGroupToolsPolicy,
|
||||
buildChannelGroupsScopeTree,
|
||||
resolveScopeKeyCaseInsensitive,
|
||||
resolveScopeToolsPolicy,
|
||||
type GroupToolPolicyConfig,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
|
||||
export function resolveQQBotGroupToolPolicy(
|
||||
params: ChannelGroupContext,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
return resolveChannelGroupToolsPolicy({
|
||||
cfg: params.cfg,
|
||||
channel: "qqbot",
|
||||
groupId: params.groupId,
|
||||
groupIdCaseInsensitive: true,
|
||||
accountId: params.accountId,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
const tree = buildChannelGroupsScopeTree(params.cfg, "qqbot", params.accountId);
|
||||
const scopeKey = resolveScopeKeyCaseInsensitive(tree, params.groupId);
|
||||
return resolveScopeToolsPolicy({
|
||||
...params,
|
||||
tree,
|
||||
path: scopeKey ? [scopeKey] : [],
|
||||
messageProvider: "qqbot",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import type { OpenClawConfig } from "./runtime-api.js";
|
||||
|
||||
describe("whatsapp group policy", () => {
|
||||
it("uses generic channel group policy helpers", () => {
|
||||
it("resolves exact, wildcard, and unconfigured policies", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
@@ -33,5 +33,66 @@ describe("whatsapp group policy", () => {
|
||||
expect(resolveWhatsAppGroupToolPolicy({ cfg, groupId: "other@g.us" })).toEqual({
|
||||
allow: ["message.send"],
|
||||
});
|
||||
expect(resolveWhatsAppGroupRequireMention({ cfg: {}, groupId: "other@g.us" })).toBe(true);
|
||||
expect(resolveWhatsAppGroupToolPolicy({ cfg: {}, groupId: "other@g.us" })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses account groups and preserves the single-account empty fallback", () => {
|
||||
const overrideCfg = {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groups: { "1203630@g.us": { requireMention: false } },
|
||||
accounts: {
|
||||
work: { groups: { "1203630@g.us": { requireMention: true } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const fallbackCfg = {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groups: { "1203630@g.us": { requireMention: false } },
|
||||
accounts: { work: { groups: {} } },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
resolveWhatsAppGroupRequireMention({
|
||||
cfg: overrideCfg,
|
||||
accountId: "work",
|
||||
groupId: "1203630@g.us",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
resolveWhatsAppGroupRequireMention({
|
||||
cfg: fallbackCfg,
|
||||
accountId: "work",
|
||||
groupId: "1203630@g.us",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("prefers sender-scoped tools", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groups: {
|
||||
"1203630@g.us": {
|
||||
tools: { deny: ["exec"] },
|
||||
toolsBySender: { "channel:whatsapp:alice": { allow: ["message.send"] } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
resolveWhatsAppGroupToolPolicy({
|
||||
cfg,
|
||||
groupId: "1203630@g.us",
|
||||
senderId: "alice",
|
||||
}),
|
||||
).toEqual({ allow: ["message.send"] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Whatsapp plugin module implements group policy behavior.
|
||||
import {
|
||||
resolveChannelGroupRequireMention,
|
||||
resolveChannelGroupToolsPolicy,
|
||||
buildChannelGroupsScopeTree,
|
||||
resolveScopeRequireMention,
|
||||
resolveScopeToolsPolicy,
|
||||
type GroupToolPolicyConfig,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
@@ -16,26 +17,24 @@ type WhatsAppGroupContext = {
|
||||
senderE164?: string | null;
|
||||
};
|
||||
|
||||
function resolveScopePath(params: WhatsAppGroupContext) {
|
||||
return params.groupId ? [params.groupId] : [];
|
||||
}
|
||||
|
||||
export function resolveWhatsAppGroupRequireMention(params: WhatsAppGroupContext): boolean {
|
||||
return resolveChannelGroupRequireMention({
|
||||
cfg: params.cfg,
|
||||
channel: "whatsapp",
|
||||
groupId: params.groupId,
|
||||
accountId: params.accountId,
|
||||
return resolveScopeRequireMention({
|
||||
tree: buildChannelGroupsScopeTree(params.cfg, "whatsapp", params.accountId),
|
||||
path: resolveScopePath(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveWhatsAppGroupToolPolicy(
|
||||
params: WhatsAppGroupContext,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
return resolveChannelGroupToolsPolicy({
|
||||
cfg: params.cfg,
|
||||
channel: "whatsapp",
|
||||
groupId: params.groupId,
|
||||
accountId: params.accountId,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
return resolveScopeToolsPolicy({
|
||||
...params,
|
||||
tree: buildChannelGroupsScopeTree(params.cfg, "whatsapp", params.accountId),
|
||||
path: resolveScopePath(params),
|
||||
messageProvider: "whatsapp",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
|
||||
types: 6,
|
||||
"agent-config-primitives": 2,
|
||||
"command-auth": 81,
|
||||
compat: 152,
|
||||
compat: 160,
|
||||
"direct-dm": 9,
|
||||
"direct-dm-access": 5,
|
||||
discord: 48,
|
||||
@@ -159,7 +159,7 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
|
||||
"channel-pairing": 1,
|
||||
"conversation-runtime": 4,
|
||||
"channel-send-result": 1,
|
||||
"channel-policy": 8,
|
||||
"channel-policy": 15,
|
||||
"channel-route": 5,
|
||||
"session-store-runtime": 4,
|
||||
"session-transcript-runtime": 2,
|
||||
@@ -202,19 +202,22 @@ export function readPluginSdkSurfaceBudgets(env = process.env) {
|
||||
327,
|
||||
env,
|
||||
),
|
||||
// ScopeTree adds six channel-policy exports, mirrored by compat, including three functions.
|
||||
// Its flat channel-groups builder adds one function, also mirrored by compat.
|
||||
// Its case-insensitive scope-key resolver adds one function, also mirrored by compat.
|
||||
publicExports: readPluginSdkSurfaceBudgetEnv(
|
||||
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS",
|
||||
10649,
|
||||
10665,
|
||||
env,
|
||||
),
|
||||
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(
|
||||
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS",
|
||||
5361,
|
||||
5371,
|
||||
env,
|
||||
),
|
||||
publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv(
|
||||
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS",
|
||||
3283,
|
||||
3292,
|
||||
env,
|
||||
),
|
||||
publicWildcardReexports: readPluginSdkSurfaceBudgetEnv(
|
||||
|
||||
@@ -330,7 +330,7 @@ export function resolveToolsBySender(
|
||||
return matchToolsBySenderPolicy(compiled, params);
|
||||
}
|
||||
|
||||
function resolveChannelGroups(
|
||||
export function resolveChannelGroups(
|
||||
cfg: OpenClawConfig,
|
||||
channel: GroupPolicyChannel,
|
||||
accountId?: string | null,
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
// Verifies canonical group scope precedence and sender policy resolution.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "./config.js";
|
||||
import { resolveChannelGroupRequireMention, resolveToolsBySender } from "./group-policy.js";
|
||||
import {
|
||||
resolveScopeIntroHint,
|
||||
resolveScopeRequireMention,
|
||||
resolveScopeToolsPolicy,
|
||||
type ScopeTree,
|
||||
} from "./group-scope-tree.js";
|
||||
|
||||
describe("resolveScopeRequireMention", () => {
|
||||
const scalarCases: Array<{
|
||||
name: string;
|
||||
tree: ScopeTree;
|
||||
path: string[];
|
||||
expected: boolean;
|
||||
}> = [
|
||||
{
|
||||
name: "uses the most specific configured scope",
|
||||
tree: {
|
||||
defaults: { requireMention: false },
|
||||
scopes: {
|
||||
broad: { requireMention: false },
|
||||
narrow: { requireMention: true },
|
||||
},
|
||||
},
|
||||
path: ["broad", "narrow"],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "skips missing scope keys",
|
||||
tree: { scopes: { broad: { requireMention: false } } },
|
||||
path: ["broad", "missing"],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "uses defaults after path scopes",
|
||||
tree: { defaults: { requireMention: false }, scopes: { room: {} } },
|
||||
path: ["room"],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "defaults to requiring a mention",
|
||||
tree: { scopes: {} },
|
||||
path: ["missing"],
|
||||
expected: true,
|
||||
},
|
||||
];
|
||||
|
||||
it.each(scalarCases)("$name", ({ tree, path, expected }) => {
|
||||
expect(resolveScopeRequireMention({ tree, path })).toBe(expected);
|
||||
});
|
||||
|
||||
it("honors Telegram wildcard-topic precedence encoded by the path", () => {
|
||||
const tree: ScopeTree = {
|
||||
scopes: {
|
||||
"*#topic:7": { requireMention: false },
|
||||
"<chat>": { requireMention: true },
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
resolveScopeRequireMention({
|
||||
tree,
|
||||
path: ["*", "<chat>", "*#topic:7", "<chat>#topic:7"],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "no override",
|
||||
configured: false,
|
||||
override: undefined,
|
||||
overrideOrder: undefined,
|
||||
},
|
||||
{
|
||||
name: "before-config override",
|
||||
configured: false,
|
||||
override: true,
|
||||
overrideOrder: "before-config" as const,
|
||||
},
|
||||
{
|
||||
name: "after-config override",
|
||||
configured: false,
|
||||
override: true,
|
||||
overrideOrder: "after-config" as const,
|
||||
},
|
||||
{
|
||||
name: "after-config fallback override",
|
||||
configured: undefined,
|
||||
override: false,
|
||||
overrideOrder: "after-config" as const,
|
||||
},
|
||||
])("matches flat group-policy behavior: $name", ({ configured, override, overrideOrder }) => {
|
||||
const node = typeof configured === "boolean" ? { requireMention: configured } : {};
|
||||
const tree: ScopeTree = { scopes: { room: node } };
|
||||
const cfg = {
|
||||
channels: { whatsapp: { groups: { room: node } } },
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
resolveScopeRequireMention({
|
||||
tree,
|
||||
path: ["room"],
|
||||
requireMentionOverride: override,
|
||||
overrideOrder,
|
||||
}),
|
||||
).toBe(
|
||||
resolveChannelGroupRequireMention({
|
||||
cfg,
|
||||
channel: "whatsapp",
|
||||
groupId: "room",
|
||||
requireMentionOverride: override,
|
||||
overrideOrder,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults configured-but-unset scopes to no mention only when requested", () => {
|
||||
const tree: ScopeTree = { scopes: { configured: {} } };
|
||||
|
||||
expect(
|
||||
resolveScopeRequireMention({
|
||||
tree,
|
||||
path: ["configured"],
|
||||
configuredScopeDefaultsToNoMention: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
resolveScopeRequireMention({
|
||||
tree,
|
||||
path: ["missing"],
|
||||
configuredScopeDefaultsToNoMention: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveScopeToolsPolicy", () => {
|
||||
const cascadeCases: Array<{
|
||||
name: string;
|
||||
tree: ScopeTree;
|
||||
senderId: string;
|
||||
expected: { allow?: string[]; deny?: string[] };
|
||||
}> = [
|
||||
{
|
||||
name: "channel sender policy",
|
||||
tree: {
|
||||
scopes: {
|
||||
team: { tools: { deny: ["team"] } },
|
||||
channel: {
|
||||
toolsBySender: { "id:alice": { allow: ["channel-sender"] } },
|
||||
tools: { allow: ["channel"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
senderId: "alice",
|
||||
expected: { allow: ["channel-sender"] },
|
||||
},
|
||||
{
|
||||
name: "channel policy",
|
||||
tree: {
|
||||
scopes: {
|
||||
team: { tools: { deny: ["team"] } },
|
||||
channel: {
|
||||
toolsBySender: { "id:alice": { allow: ["channel-sender"] } },
|
||||
tools: { allow: ["channel"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
senderId: "bob",
|
||||
expected: { allow: ["channel"] },
|
||||
},
|
||||
{
|
||||
name: "team sender policy",
|
||||
tree: {
|
||||
scopes: {
|
||||
team: {
|
||||
toolsBySender: { "id:bob": { allow: ["team-sender"] } },
|
||||
tools: { deny: ["team"] },
|
||||
},
|
||||
channel: {},
|
||||
},
|
||||
},
|
||||
senderId: "bob",
|
||||
expected: { allow: ["team-sender"] },
|
||||
},
|
||||
{
|
||||
name: "team policy",
|
||||
tree: {
|
||||
scopes: {
|
||||
team: {
|
||||
toolsBySender: { "id:bob": { allow: ["team-sender"] } },
|
||||
tools: { deny: ["team"] },
|
||||
},
|
||||
channel: {},
|
||||
},
|
||||
},
|
||||
senderId: "carol",
|
||||
expected: { deny: ["team"] },
|
||||
},
|
||||
];
|
||||
|
||||
it.each(cascadeCases)(
|
||||
"resolves the MSTeams cascade through $name",
|
||||
({ tree, senderId, expected }) => {
|
||||
expect(resolveScopeToolsPolicy({ tree, path: ["team", "channel"], senderId })).toEqual(
|
||||
expected,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ name: "id", sender: { senderId: "user:alice" }, expected: { allow: ["id"] } },
|
||||
{
|
||||
name: "username",
|
||||
sender: { senderUsername: "@Alice" },
|
||||
expected: { allow: ["username"] },
|
||||
},
|
||||
{
|
||||
name: "channel",
|
||||
sender: { senderId: "user:alice", messageProvider: "discord" },
|
||||
expected: { allow: ["channel"] },
|
||||
},
|
||||
{
|
||||
name: "channel without provider",
|
||||
sender: { senderId: "user:alice" },
|
||||
expected: { allow: ["id"] },
|
||||
},
|
||||
])("matches resolveToolsBySender for typed $name keys", ({ sender, expected }) => {
|
||||
const toolsBySender = {
|
||||
"id:user:alice": { allow: ["id"] },
|
||||
"username:alice": { allow: ["username"] },
|
||||
"channel:discord:user:alice": { allow: ["channel"] },
|
||||
"*": { deny: ["fallback"] },
|
||||
};
|
||||
const tree: ScopeTree = { scopes: { room: { toolsBySender } } };
|
||||
|
||||
const directPolicy = resolveToolsBySender({ toolsBySender, ...sender });
|
||||
expect(resolveScopeToolsPolicy({ tree, path: ["room"], ...sender })).toEqual(directPolicy);
|
||||
expect(directPolicy).toEqual(expected);
|
||||
});
|
||||
|
||||
it("uses sender and plain policies from defaults after path scopes", () => {
|
||||
const senderTree: ScopeTree = {
|
||||
defaults: {
|
||||
toolsBySender: { "id:alice": { allow: ["default-sender"] } },
|
||||
tools: { deny: ["default"] },
|
||||
},
|
||||
scopes: { channel: {} },
|
||||
};
|
||||
|
||||
expect(
|
||||
resolveScopeToolsPolicy({ tree: senderTree, path: ["channel"], senderId: "alice" }),
|
||||
).toEqual({ allow: ["default-sender"] });
|
||||
expect(
|
||||
resolveScopeToolsPolicy({ tree: senderTree, path: ["channel"], senderId: "bob" }),
|
||||
).toEqual({ deny: ["default"] });
|
||||
expect(resolveScopeToolsPolicy({ tree: { scopes: {} }, path: [] })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps a narrower plain policy ahead of a broader sender match", () => {
|
||||
const tree: ScopeTree = {
|
||||
scopes: {
|
||||
team: { toolsBySender: { "id:alice": { allow: ["team-sender"] } } },
|
||||
channel: { tools: { deny: ["channel"] } },
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
resolveScopeToolsPolicy({
|
||||
tree,
|
||||
path: ["team", "channel"],
|
||||
senderId: "alice",
|
||||
}),
|
||||
).toEqual({ deny: ["channel"] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveScopeIntroHint", () => {
|
||||
it("uses the first defined hint from narrowest scope through defaults", () => {
|
||||
const tree: ScopeTree = {
|
||||
defaults: { introHint: "default" },
|
||||
scopes: {
|
||||
team: { introHint: "team" },
|
||||
channel: {},
|
||||
thread: { introHint: "thread" },
|
||||
},
|
||||
};
|
||||
|
||||
expect(resolveScopeIntroHint({ tree, path: ["team", "channel", "thread"] })).toBe("thread");
|
||||
expect(resolveScopeIntroHint({ tree, path: ["missing"] })).toBe("default");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
// Resolves canonical group policy scopes prepared by channel plugins.
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { ChannelId } from "../channels/plugins/channel-id.types.js";
|
||||
import { resolveChannelGroups, resolveToolsBySender } from "./group-policy.js";
|
||||
import type { OpenClawConfig } from "./types.openclaw.js";
|
||||
import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
|
||||
|
||||
export type ScopeNode = {
|
||||
requireMention?: boolean;
|
||||
tools?: GroupToolPolicyConfig;
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
introHint?: string;
|
||||
};
|
||||
|
||||
export type ScopeTree = {
|
||||
defaults?: ScopeNode;
|
||||
// Flat keys preserve channel-defined precedence: channels emit broad-to-narrow paths.
|
||||
// Nested trees cannot model Telegram, where a wildcard-group topic outranks
|
||||
// an exact-group scalar requireMention.
|
||||
scopes: Record<string, ScopeNode>;
|
||||
};
|
||||
|
||||
export type ScopePath = string[];
|
||||
|
||||
type ScopeToolPolicySender = Omit<Parameters<typeof resolveToolsBySender>[0], "toolsBySender">;
|
||||
|
||||
export function buildChannelGroupsScopeTree(
|
||||
cfg: OpenClawConfig,
|
||||
channel: ChannelId,
|
||||
accountId?: string | null,
|
||||
): ScopeTree {
|
||||
// 13+ channels share the flat `groups` config shape; richer channels such as
|
||||
// Discord, Telegram, and Microsoft Teams ship their own builders.
|
||||
const groups = resolveChannelGroups(cfg, channel, accountId) ?? {};
|
||||
// The wildcard config is the fallback node, not a matchable exact scope.
|
||||
const { "*": defaults, ...scopes } = groups;
|
||||
return { defaults, scopes };
|
||||
}
|
||||
|
||||
export function resolveScopeKeyCaseInsensitive(
|
||||
tree: ScopeTree,
|
||||
key: string | null | undefined,
|
||||
): string | undefined {
|
||||
if (!key) {
|
||||
return undefined;
|
||||
}
|
||||
// Exact scope identity wins; keys are matched untrimmed for parity with the
|
||||
// legacy resolver. Wildcard never matches: builders move it to `defaults`.
|
||||
if (Object.hasOwn(tree.scopes, key)) {
|
||||
return key;
|
||||
}
|
||||
const target = normalizeLowercaseStringOrEmpty(key);
|
||||
return Object.keys(tree.scopes).find(
|
||||
(scopeKey) => normalizeLowercaseStringOrEmpty(scopeKey) === target,
|
||||
);
|
||||
}
|
||||
|
||||
function resolveFromScopes<Value>(params: {
|
||||
tree: ScopeTree;
|
||||
path: ScopePath;
|
||||
resolveNode: (node: ScopeNode) => Value | undefined;
|
||||
}): Value | undefined {
|
||||
for (let index = params.path.length - 1; index >= 0; index -= 1) {
|
||||
const key = params.path[index];
|
||||
if (key === undefined || !Object.hasOwn(params.tree.scopes, key)) {
|
||||
continue;
|
||||
}
|
||||
const node = params.tree.scopes[key];
|
||||
if (!node) {
|
||||
continue;
|
||||
}
|
||||
const value = params.resolveNode(node);
|
||||
if (value !== undefined) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return params.tree.defaults ? params.resolveNode(params.tree.defaults) : undefined;
|
||||
}
|
||||
|
||||
export function resolveScopeRequireMention(params: {
|
||||
tree: ScopeTree;
|
||||
path: ScopePath;
|
||||
requireMentionOverride?: boolean;
|
||||
overrideOrder?: "before-config" | "after-config";
|
||||
configuredScopeDefaultsToNoMention?: boolean;
|
||||
}): boolean {
|
||||
// Runtime overrides stay in resolver parameters because channels derive them per message.
|
||||
const { requireMentionOverride, overrideOrder = "after-config" } = params;
|
||||
const configuredMention = resolveFromScopes({
|
||||
tree: params.tree,
|
||||
path: params.path,
|
||||
resolveNode: (node) => node.requireMention,
|
||||
});
|
||||
|
||||
if (overrideOrder === "before-config" && typeof requireMentionOverride === "boolean") {
|
||||
return requireMentionOverride;
|
||||
}
|
||||
if (typeof configuredMention === "boolean") {
|
||||
return configuredMention;
|
||||
}
|
||||
if (overrideOrder !== "before-config" && typeof requireMentionOverride === "boolean") {
|
||||
return requireMentionOverride;
|
||||
}
|
||||
if (
|
||||
params.configuredScopeDefaultsToNoMention &&
|
||||
params.path.some((key) => Object.hasOwn(params.tree.scopes, key))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function resolveScopeToolsPolicy(
|
||||
params: {
|
||||
tree: ScopeTree;
|
||||
path: ScopePath;
|
||||
} & ScopeToolPolicySender,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
return resolveFromScopes({
|
||||
tree: params.tree,
|
||||
path: params.path,
|
||||
resolveNode: (node) =>
|
||||
resolveToolsBySender({
|
||||
toolsBySender: node.toolsBySender,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
messageProvider: params.messageProvider,
|
||||
}) ?? node.tools,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveScopeIntroHint(params: {
|
||||
tree: ScopeTree;
|
||||
path: ScopePath;
|
||||
}): string | undefined {
|
||||
return resolveFromScopes({
|
||||
tree: params.tree,
|
||||
path: params.path,
|
||||
resolveNode: (node) => node.introHint,
|
||||
});
|
||||
}
|
||||
@@ -48,6 +48,16 @@ export {
|
||||
resolveToolsBySender,
|
||||
type ChannelGroupPolicy,
|
||||
} from "../config/group-policy.js";
|
||||
export {
|
||||
buildChannelGroupsScopeTree,
|
||||
resolveScopeIntroHint,
|
||||
resolveScopeKeyCaseInsensitive,
|
||||
resolveScopeRequireMention,
|
||||
resolveScopeToolsPolicy,
|
||||
type ScopeNode,
|
||||
type ScopePath,
|
||||
type ScopeTree,
|
||||
} from "../config/group-scope-tree.js";
|
||||
export {
|
||||
DM_GROUP_ACCESS_REASON,
|
||||
readStoreAllowFromForDmPolicy,
|
||||
|
||||
Reference in New Issue
Block a user