Skip to content

Commit b9c4815

Browse files
authored
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
1 parent 8ac4a52 commit b9c4815

19 files changed

Lines changed: 806 additions & 115 deletions
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
d43b631bf84fdef65d44514e34f77dc97de14a3e5016d728e59585a1c47d1b2b plugin-sdk-api-baseline.json
2-
5c272b15c12c2f7ff0756c57f33706662702564882b5c9fbdd46c58cf8dcf314 plugin-sdk-api-baseline.jsonl
1+
2828a432f8c8fcec39fed6a2f612907aef001e1e5bb7bc295aa3f6d05aa053e5 plugin-sdk-api-baseline.json
2+
c847fbaf67e4159a9305f8f985b00696d3d830a84082ef54df222ef627ea22a5 plugin-sdk-api-baseline.jsonl
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Googlechat tests cover group policy plugin behavior.
2+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
3+
import { describe, expect, it } from "vitest";
4+
import { resolveGoogleChatGroupRequireMention } from "./group-policy.js";
5+
6+
describe("googlechat group policy", () => {
7+
it("resolves exact, wildcard, and unconfigured mention policies", () => {
8+
const cfg = {
9+
channels: {
10+
googlechat: {
11+
groups: {
12+
"spaces/exact": { requireMention: false },
13+
"*": { requireMention: true },
14+
},
15+
},
16+
},
17+
} as OpenClawConfig;
18+
19+
expect(resolveGoogleChatGroupRequireMention({ cfg, groupId: "spaces/exact" })).toBe(false);
20+
expect(resolveGoogleChatGroupRequireMention({ cfg, groupId: "spaces/other" })).toBe(true);
21+
expect(resolveGoogleChatGroupRequireMention({ cfg: {}, groupId: "spaces/other" })).toBe(true);
22+
});
23+
24+
it("uses account groups instead of root groups", () => {
25+
const cfg = {
26+
channels: {
27+
googlechat: {
28+
groups: { "spaces/exact": { requireMention: false } },
29+
accounts: {
30+
work: { groups: { "spaces/exact": { requireMention: true } } },
31+
},
32+
},
33+
},
34+
} as OpenClawConfig;
35+
36+
expect(
37+
resolveGoogleChatGroupRequireMention({ cfg, accountId: "work", groupId: "spaces/exact" }),
38+
).toBe(true);
39+
});
40+
41+
it("falls back to root groups for one account with an empty groups map", () => {
42+
const cfg = {
43+
channels: {
44+
googlechat: {
45+
groups: { "spaces/exact": { requireMention: false } },
46+
accounts: { work: { groups: {} } },
47+
},
48+
},
49+
} as OpenClawConfig;
50+
51+
expect(
52+
resolveGoogleChatGroupRequireMention({ cfg, accountId: "work", groupId: "spaces/exact" }),
53+
).toBe(false);
54+
});
55+
});
Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
// Googlechat plugin module implements group policy behavior.
2-
import { resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy";
1+
import {
2+
buildChannelGroupsScopeTree,
3+
resolveScopeRequireMention,
4+
} from "openclaw/plugin-sdk/channel-policy";
35
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
46

5-
type GoogleChatGroupContext = {
6-
cfg: OpenClawConfig;
7-
accountId?: string | null;
8-
groupId?: string | null;
9-
};
7+
type GroupContext = { cfg: OpenClawConfig; accountId?: string | null; groupId?: string | null };
8+
function resolveScopePath(params: GroupContext) {
9+
return params.groupId ? [params.groupId] : [];
10+
}
1011

11-
export function resolveGoogleChatGroupRequireMention(params: GoogleChatGroupContext): boolean {
12-
return resolveChannelGroupRequireMention({
13-
cfg: params.cfg,
14-
channel: "googlechat",
15-
groupId: params.groupId,
16-
accountId: params.accountId,
12+
export function resolveGoogleChatGroupRequireMention(params: GroupContext): boolean {
13+
return resolveScopeRequireMention({
14+
tree: buildChannelGroupsScopeTree(params.cfg, "googlechat", params.accountId),
15+
path: resolveScopePath(params),
1716
});
1817
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Imessage tests cover group policy plugin behavior.
2+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
3+
import { describe, expect, it } from "vitest";
4+
import {
5+
resolveIMessageGroupRequireMention,
6+
resolveIMessageGroupToolPolicy,
7+
} from "./group-policy.js";
8+
9+
describe("imessage group policy", () => {
10+
it("resolves exact, wildcard, and unconfigured policies", () => {
11+
const cfg = {
12+
channels: {
13+
imessage: {
14+
groups: {
15+
exact: { requireMention: false, tools: { deny: ["exec"] } },
16+
"*": { requireMention: true, tools: { allow: ["message.send"] } },
17+
},
18+
},
19+
},
20+
} as OpenClawConfig;
21+
22+
expect(resolveIMessageGroupRequireMention({ cfg, groupId: "exact" })).toBe(false);
23+
expect(resolveIMessageGroupRequireMention({ cfg, groupId: "other" })).toBe(true);
24+
expect(resolveIMessageGroupToolPolicy({ cfg, groupId: "exact" })).toEqual({
25+
deny: ["exec"],
26+
});
27+
expect(resolveIMessageGroupToolPolicy({ cfg, groupId: "other" })).toEqual({
28+
allow: ["message.send"],
29+
});
30+
expect(resolveIMessageGroupRequireMention({ cfg: {}, groupId: "other" })).toBe(true);
31+
expect(resolveIMessageGroupToolPolicy({ cfg: {}, groupId: "other" })).toBeUndefined();
32+
});
33+
34+
it("uses account groups and preserves the single-account empty fallback", () => {
35+
const overrideCfg = {
36+
channels: {
37+
imessage: {
38+
groups: { exact: { requireMention: false } },
39+
accounts: { work: { groups: { exact: { requireMention: true } } } },
40+
},
41+
},
42+
} as OpenClawConfig;
43+
const fallbackCfg = {
44+
channels: {
45+
imessage: {
46+
groups: { exact: { requireMention: false } },
47+
accounts: { work: { groups: {} } },
48+
},
49+
},
50+
} as OpenClawConfig;
51+
52+
expect(
53+
resolveIMessageGroupRequireMention({
54+
cfg: overrideCfg,
55+
accountId: "work",
56+
groupId: "exact",
57+
}),
58+
).toBe(true);
59+
expect(
60+
resolveIMessageGroupRequireMention({
61+
cfg: fallbackCfg,
62+
accountId: "work",
63+
groupId: "exact",
64+
}),
65+
).toBe(false);
66+
});
67+
68+
it("prefers sender-scoped tools", () => {
69+
const cfg = {
70+
channels: {
71+
imessage: {
72+
groups: {
73+
exact: {
74+
tools: { deny: ["exec"] },
75+
toolsBySender: { "channel:imessage:alice": { allow: ["message.send"] } },
76+
},
77+
},
78+
},
79+
},
80+
} as OpenClawConfig;
81+
82+
expect(resolveIMessageGroupToolPolicy({ cfg, groupId: "exact", senderId: "alice" })).toEqual({
83+
allow: ["message.send"],
84+
});
85+
});
86+
});
Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// Imessage plugin module implements group policy behavior.
22
import {
3-
resolveChannelGroupRequireMention,
4-
resolveChannelGroupToolsPolicy,
3+
buildChannelGroupsScopeTree,
4+
resolveScopeRequireMention,
5+
resolveScopeToolsPolicy,
56
type GroupToolPolicyConfig,
67
} from "openclaw/plugin-sdk/channel-policy";
78
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
@@ -16,26 +17,24 @@ type IMessageGroupContext = {
1617
senderE164?: string | null;
1718
};
1819

20+
function resolveScopePath(params: IMessageGroupContext) {
21+
return params.groupId ? [params.groupId] : [];
22+
}
23+
1924
export function resolveIMessageGroupRequireMention(params: IMessageGroupContext): boolean {
20-
return resolveChannelGroupRequireMention({
21-
cfg: params.cfg,
22-
channel: "imessage",
23-
groupId: params.groupId,
24-
accountId: params.accountId,
25+
return resolveScopeRequireMention({
26+
tree: buildChannelGroupsScopeTree(params.cfg, "imessage", params.accountId),
27+
path: resolveScopePath(params),
2528
});
2629
}
2730

2831
export function resolveIMessageGroupToolPolicy(
2932
params: IMessageGroupContext,
3033
): GroupToolPolicyConfig | undefined {
31-
return resolveChannelGroupToolsPolicy({
32-
cfg: params.cfg,
33-
channel: "imessage",
34-
groupId: params.groupId,
35-
accountId: params.accountId,
36-
senderId: params.senderId,
37-
senderName: params.senderName,
38-
senderUsername: params.senderUsername,
39-
senderE164: params.senderE164,
34+
return resolveScopeToolsPolicy({
35+
...params,
36+
tree: buildChannelGroupsScopeTree(params.cfg, "imessage", params.accountId),
37+
path: resolveScopePath(params),
38+
messageProvider: "imessage",
4039
});
4140
}

extensions/line/src/group-keys.test.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,40 +62,57 @@ describe("account-scoped LINE groups", () => {
6262
expect(resolveLineGroupsConfig(cfg, "work")).toEqual({
6363
"group:g1": { requireMention: false },
6464
});
65-
expect(resolveExactLineGroupConfigKey({ cfg, accountId: "work", groupId: "g1" })).toBe(
66-
"group:g1",
67-
);
68-
expect(resolveExactLineGroupConfigKey({ cfg, accountId: "default", groupId: "g1" })).toBe(
69-
undefined,
70-
);
65+
expect(
66+
resolveExactLineGroupConfigKey({
67+
groups: resolveLineGroupsConfig(cfg, "work"),
68+
groupId: "g1",
69+
}),
70+
).toBe("group:g1");
71+
expect(
72+
resolveExactLineGroupConfigKey({
73+
groups: resolveLineGroupsConfig(cfg, "default"),
74+
groupId: "g1",
75+
}),
76+
).toBe(undefined);
7177
});
7278
});
7379

7480
describe("line group policy", () => {
75-
it("matches raw and prefixed LINE group keys for requireMention", () => {
81+
it("preserves candidate precedence and falls back to wildcard", () => {
7682
const cfg = {
7783
channels: {
7884
line: {
7985
groups: {
80-
"room:r123": {
86+
same: {
8187
requireMention: false,
8288
},
83-
"group:g123": {
89+
"group:same": {
90+
requireMention: true,
91+
},
92+
"room:same": {
93+
requireMention: true,
94+
},
95+
"group:typed": {
8496
requireMention: false,
8597
},
86-
"*": {
98+
"room:typed": {
8799
requireMention: true,
88100
},
101+
"*": {
102+
requireMention: false,
103+
},
89104
},
90105
},
91106
},
92107
} as OpenClawConfig;
93108

94-
expect(resolveLineGroupRequireMention({ cfg, groupId: "r123" })).toBe(false);
95-
expect(resolveLineGroupRequireMention({ cfg, groupId: "room:r123" })).toBe(false);
96-
expect(resolveLineGroupRequireMention({ cfg, groupId: "g123" })).toBe(false);
97-
expect(resolveLineGroupRequireMention({ cfg, groupId: "group:g123" })).toBe(false);
98-
expect(resolveLineGroupRequireMention({ cfg, groupId: "other" })).toBe(true);
109+
expect(resolveLineGroupRequireMention({ cfg, groupId: "same" })).toBe(false);
110+
expect(resolveLineGroupRequireMention({ cfg, groupId: "room:same" })).toBe(false);
111+
expect(resolveLineGroupRequireMention({ cfg, groupId: "group:same" })).toBe(false);
112+
expect(resolveLineGroupRequireMention({ cfg, groupId: "typed" })).toBe(false);
113+
expect(resolveLineGroupRequireMention({ cfg, groupId: "group:typed" })).toBe(false);
114+
expect(resolveLineGroupRequireMention({ cfg, groupId: "room:typed" })).toBe(true);
115+
expect(resolveLineGroupRequireMention({ cfg, groupId: "other" })).toBe(false);
99116
});
100117

101118
it("uses account-scoped prefixed LINE group config for requireMention", () => {

extensions/line/src/group-keys.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,10 @@ export function resolveLineGroupsConfig(
5252
}
5353

5454
export function resolveExactLineGroupConfigKey(params: {
55-
cfg: OpenClawConfig;
56-
accountId?: string | null;
55+
groups: Record<string, unknown> | undefined;
5756
groupId?: string | null;
5857
}): string | undefined {
59-
const groups = resolveLineGroupsConfig(params.cfg, params.accountId);
58+
const { groups } = params;
6059
if (!groups) {
6160
return undefined;
6261
}
Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
// Line plugin module implements group policy behavior.
2-
import { resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy";
2+
import {
3+
buildChannelGroupsScopeTree,
4+
resolveScopeRequireMention,
5+
} from "openclaw/plugin-sdk/channel-policy";
36
import { resolveExactLineGroupConfigKey, type OpenClawConfig } from "./channel-api.js";
47

5-
type LineGroupContext = {
6-
cfg: OpenClawConfig;
7-
accountId?: string | null;
8-
groupId?: string | null;
9-
};
8+
type LineGroupContext = { cfg: OpenClawConfig; accountId?: string | null; groupId?: string | null };
109

1110
export function resolveLineGroupRequireMention(params: LineGroupContext): boolean {
12-
const exactGroupId = resolveExactLineGroupConfigKey({
13-
cfg: params.cfg,
14-
accountId: params.accountId,
11+
const tree = buildChannelGroupsScopeTree(params.cfg, "line", params.accountId);
12+
const matchedKey = resolveExactLineGroupConfigKey({
13+
groups: tree.scopes,
1514
groupId: params.groupId,
1615
});
17-
return resolveChannelGroupRequireMention({
18-
cfg: params.cfg,
19-
channel: "line",
20-
groupId: exactGroupId ?? params.groupId,
21-
accountId: params.accountId,
16+
return resolveScopeRequireMention({
17+
tree,
18+
path: matchedKey ? [matchedKey] : [],
2219
});
2320
}

extensions/mattermost/src/group-mentions.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,24 @@ describe("resolveMattermostGroupRequireMention", () => {
1515
expect(requireMention).toBe(true);
1616
});
1717

18-
it("respects chatmode-derived account override", () => {
18+
it("lets groups config beat chatmode and chatmode beat the final default", () => {
1919
const cfg: OpenClawConfig = {
2020
channels: {
2121
mattermost: {
2222
chatmode: "onmessage",
23+
groups: {
24+
calls: { requireMention: true },
25+
},
2326
},
2427
},
2528
};
2629

27-
const requireMention = resolveMattermostGroupRequireMention({ cfg, accountId: "default" });
28-
expect(requireMention).toBe(false);
30+
expect(
31+
resolveMattermostGroupRequireMention({ cfg, accountId: "default", groupId: "calls" }),
32+
).toBe(true);
33+
expect(
34+
resolveMattermostGroupRequireMention({ cfg, accountId: "default", groupId: "other" }),
35+
).toBe(false);
2936
});
3037

3138
it("prefers an explicit runtime override when provided", () => {

0 commit comments

Comments
 (0)