@openclaw/matrix 2026.2.1 → 2026.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -1
- package/package.json +2 -2
- package/src/channel.ts +3 -7
- package/src/config-schema.ts +1 -0
- package/src/matrix/monitor/allowlist.test.ts +45 -0
- package/src/matrix/monitor/allowlist.ts +56 -15
- package/src/matrix/monitor/handler.ts +14 -19
- package/src/matrix/monitor/index.ts +91 -44
- package/src/matrix/monitor/rooms.test.ts +39 -0
- package/src/matrix/monitor/rooms.ts +0 -1
- package/src/onboarding.ts +24 -17
- package/src/resolve-targets.test.ts +48 -0
- package/src/resolve-targets.ts +48 -2
- package/src/types.ts +7 -5
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/matrix",
|
|
3
|
-
"version": "2026.2.
|
|
3
|
+
"version": "2026.2.3",
|
|
4
4
|
"description": "OpenClaw Matrix channel plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
|
|
8
8
|
"@vector-im/matrix-bot-sdk": "0.8.0-element.3",
|
|
9
9
|
"markdown-it": "14.1.0",
|
|
10
|
-
"music-metadata": "^11.11.
|
|
10
|
+
"music-metadata": "^11.11.2",
|
|
11
11
|
"zod": "^4.3.6"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
package/src/channel.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
type ResolvedMatrixAccount,
|
|
25
25
|
} from "./matrix/accounts.js";
|
|
26
26
|
import { resolveMatrixAuth } from "./matrix/client.js";
|
|
27
|
-
import {
|
|
27
|
+
import { normalizeMatrixAllowList, normalizeMatrixUserId } from "./matrix/monitor/allowlist.js";
|
|
28
28
|
import { probeMatrix } from "./matrix/probe.js";
|
|
29
29
|
import { sendMessageMatrix } from "./matrix/send.js";
|
|
30
30
|
import { matrixOnboardingAdapter } from "./onboarding.js";
|
|
@@ -144,7 +144,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
|
|
144
144
|
}),
|
|
145
145
|
resolveAllowFrom: ({ cfg }) =>
|
|
146
146
|
((cfg as CoreConfig).channels?.matrix?.dm?.allowFrom ?? []).map((entry) => String(entry)),
|
|
147
|
-
formatAllowFrom: ({ allowFrom }) =>
|
|
147
|
+
formatAllowFrom: ({ allowFrom }) => normalizeMatrixAllowList(allowFrom),
|
|
148
148
|
},
|
|
149
149
|
security: {
|
|
150
150
|
resolveDmPolicy: ({ account }) => ({
|
|
@@ -153,11 +153,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
|
|
153
153
|
policyPath: "channels.matrix.dm.policy",
|
|
154
154
|
allowFromPath: "channels.matrix.dm.allowFrom",
|
|
155
155
|
approveHint: formatPairingApproveHint("matrix"),
|
|
156
|
-
normalizeEntry: (raw) =>
|
|
157
|
-
raw
|
|
158
|
-
.replace(/^matrix:/i, "")
|
|
159
|
-
.trim()
|
|
160
|
-
.toLowerCase(),
|
|
156
|
+
normalizeEntry: (raw) => normalizeMatrixUserId(raw),
|
|
161
157
|
}),
|
|
162
158
|
collectWarnings: ({ account, cfg }) => {
|
|
163
159
|
const defaultGroupPolicy = (cfg as CoreConfig).channels?.defaults?.groupPolicy;
|
package/src/config-schema.ts
CHANGED
|
@@ -51,6 +51,7 @@ export const MatrixConfigSchema = z.object({
|
|
|
51
51
|
threadReplies: z.enum(["off", "inbound", "always"]).optional(),
|
|
52
52
|
textChunkLimit: z.number().optional(),
|
|
53
53
|
chunkMode: z.enum(["length", "newline"]).optional(),
|
|
54
|
+
responsePrefix: z.string().optional(),
|
|
54
55
|
mediaMaxMb: z.number().optional(),
|
|
55
56
|
autoJoin: z.enum(["always", "allowlist", "off"]).optional(),
|
|
56
57
|
autoJoinAllowlist: z.array(allowFromEntry).optional(),
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { normalizeMatrixAllowList, resolveMatrixAllowListMatch } from "./allowlist.js";
|
|
3
|
+
|
|
4
|
+
describe("resolveMatrixAllowListMatch", () => {
|
|
5
|
+
it("matches full user IDs and prefixes", () => {
|
|
6
|
+
const userId = "@Alice:Example.org";
|
|
7
|
+
const direct = resolveMatrixAllowListMatch({
|
|
8
|
+
allowList: normalizeMatrixAllowList(["@alice:example.org"]),
|
|
9
|
+
userId,
|
|
10
|
+
});
|
|
11
|
+
expect(direct.allowed).toBe(true);
|
|
12
|
+
expect(direct.matchSource).toBe("id");
|
|
13
|
+
|
|
14
|
+
const prefixedMatrix = resolveMatrixAllowListMatch({
|
|
15
|
+
allowList: normalizeMatrixAllowList(["matrix:@alice:example.org"]),
|
|
16
|
+
userId,
|
|
17
|
+
});
|
|
18
|
+
expect(prefixedMatrix.allowed).toBe(true);
|
|
19
|
+
expect(prefixedMatrix.matchSource).toBe("prefixed-id");
|
|
20
|
+
|
|
21
|
+
const prefixedUser = resolveMatrixAllowListMatch({
|
|
22
|
+
allowList: normalizeMatrixAllowList(["user:@alice:example.org"]),
|
|
23
|
+
userId,
|
|
24
|
+
});
|
|
25
|
+
expect(prefixedUser.allowed).toBe(true);
|
|
26
|
+
expect(prefixedUser.matchSource).toBe("prefixed-user");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("ignores display names and localparts", () => {
|
|
30
|
+
const match = resolveMatrixAllowListMatch({
|
|
31
|
+
allowList: normalizeMatrixAllowList(["alice", "Alice"]),
|
|
32
|
+
userId: "@alice:example.org",
|
|
33
|
+
});
|
|
34
|
+
expect(match.allowed).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("matches wildcard", () => {
|
|
38
|
+
const match = resolveMatrixAllowListMatch({
|
|
39
|
+
allowList: normalizeMatrixAllowList(["*"]),
|
|
40
|
+
userId: "@alice:example.org",
|
|
41
|
+
});
|
|
42
|
+
expect(match.allowed).toBe(true);
|
|
43
|
+
expect(match.matchSource).toBe("wildcard");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -4,22 +4,71 @@ function normalizeAllowList(list?: Array<string | number>) {
|
|
|
4
4
|
return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
function normalizeMatrixUser(raw?: string | null): string {
|
|
8
|
+
const value = (raw ?? "").trim();
|
|
9
|
+
if (!value) {
|
|
10
|
+
return "";
|
|
11
|
+
}
|
|
12
|
+
if (!value.startsWith("@") || !value.includes(":")) {
|
|
13
|
+
return value.toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
const withoutAt = value.slice(1);
|
|
16
|
+
const splitIndex = withoutAt.indexOf(":");
|
|
17
|
+
if (splitIndex === -1) {
|
|
18
|
+
return value.toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
const localpart = withoutAt.slice(0, splitIndex).toLowerCase();
|
|
21
|
+
const server = withoutAt.slice(splitIndex + 1).toLowerCase();
|
|
22
|
+
if (!server) {
|
|
23
|
+
return value.toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
return `@${localpart}:${server.toLowerCase()}`;
|
|
9
26
|
}
|
|
10
27
|
|
|
11
|
-
function
|
|
12
|
-
|
|
28
|
+
export function normalizeMatrixUserId(raw?: string | null): string {
|
|
29
|
+
const trimmed = (raw ?? "").trim();
|
|
30
|
+
if (!trimmed) {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
const lowered = trimmed.toLowerCase();
|
|
34
|
+
if (lowered.startsWith("matrix:")) {
|
|
35
|
+
return normalizeMatrixUser(trimmed.slice("matrix:".length));
|
|
36
|
+
}
|
|
37
|
+
if (lowered.startsWith("user:")) {
|
|
38
|
+
return normalizeMatrixUser(trimmed.slice("user:".length));
|
|
39
|
+
}
|
|
40
|
+
return normalizeMatrixUser(trimmed);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeMatrixAllowListEntry(raw: string): string {
|
|
44
|
+
const trimmed = raw.trim();
|
|
45
|
+
if (!trimmed) {
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
if (trimmed === "*") {
|
|
49
|
+
return trimmed;
|
|
50
|
+
}
|
|
51
|
+
const lowered = trimmed.toLowerCase();
|
|
52
|
+
if (lowered.startsWith("matrix:")) {
|
|
53
|
+
return `matrix:${normalizeMatrixUser(trimmed.slice("matrix:".length))}`;
|
|
54
|
+
}
|
|
55
|
+
if (lowered.startsWith("user:")) {
|
|
56
|
+
return `user:${normalizeMatrixUser(trimmed.slice("user:".length))}`;
|
|
57
|
+
}
|
|
58
|
+
return normalizeMatrixUser(trimmed);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function normalizeMatrixAllowList(list?: Array<string | number>) {
|
|
62
|
+
return normalizeAllowList(list).map((entry) => normalizeMatrixAllowListEntry(entry));
|
|
13
63
|
}
|
|
14
64
|
|
|
15
65
|
export type MatrixAllowListMatch = AllowlistMatch<
|
|
16
|
-
"wildcard" | "id" | "prefixed-id" | "prefixed-user"
|
|
66
|
+
"wildcard" | "id" | "prefixed-id" | "prefixed-user"
|
|
17
67
|
>;
|
|
18
68
|
|
|
19
69
|
export function resolveMatrixAllowListMatch(params: {
|
|
20
70
|
allowList: string[];
|
|
21
71
|
userId?: string;
|
|
22
|
-
userName?: string;
|
|
23
72
|
}): MatrixAllowListMatch {
|
|
24
73
|
const allowList = params.allowList;
|
|
25
74
|
if (allowList.length === 0) {
|
|
@@ -29,14 +78,10 @@ export function resolveMatrixAllowListMatch(params: {
|
|
|
29
78
|
return { allowed: true, matchKey: "*", matchSource: "wildcard" };
|
|
30
79
|
}
|
|
31
80
|
const userId = normalizeMatrixUser(params.userId);
|
|
32
|
-
const userName = normalizeMatrixUser(params.userName);
|
|
33
|
-
const localPart = userId.startsWith("@") ? (userId.slice(1).split(":")[0] ?? "") : "";
|
|
34
81
|
const candidates: Array<{ value?: string; source: MatrixAllowListMatch["matchSource"] }> = [
|
|
35
82
|
{ value: userId, source: "id" },
|
|
36
83
|
{ value: userId ? `matrix:${userId}` : "", source: "prefixed-id" },
|
|
37
84
|
{ value: userId ? `user:${userId}` : "", source: "prefixed-user" },
|
|
38
|
-
{ value: userName, source: "name" },
|
|
39
|
-
{ value: localPart, source: "localpart" },
|
|
40
85
|
];
|
|
41
86
|
for (const candidate of candidates) {
|
|
42
87
|
if (!candidate.value) {
|
|
@@ -53,10 +98,6 @@ export function resolveMatrixAllowListMatch(params: {
|
|
|
53
98
|
return { allowed: false };
|
|
54
99
|
}
|
|
55
100
|
|
|
56
|
-
export function resolveMatrixAllowListMatches(params: {
|
|
57
|
-
allowList: string[];
|
|
58
|
-
userId?: string;
|
|
59
|
-
userName?: string;
|
|
60
|
-
}) {
|
|
101
|
+
export function resolveMatrixAllowListMatches(params: { allowList: string[]; userId?: string }) {
|
|
61
102
|
return resolveMatrixAllowListMatch(params).allowed;
|
|
62
103
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { LocationMessageEventContent, MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
createReplyPrefixOptions,
|
|
4
4
|
createTypingCallbacks,
|
|
5
5
|
formatAllowlistMatchMeta,
|
|
6
6
|
logInboundDrop,
|
|
@@ -23,9 +23,9 @@ import {
|
|
|
23
23
|
sendTypingMatrix,
|
|
24
24
|
} from "../send.js";
|
|
25
25
|
import {
|
|
26
|
+
normalizeMatrixAllowList,
|
|
26
27
|
resolveMatrixAllowListMatch,
|
|
27
28
|
resolveMatrixAllowListMatches,
|
|
28
|
-
normalizeAllowListLower,
|
|
29
29
|
} from "./allowlist.js";
|
|
30
30
|
import { resolveMatrixLocation, type MatrixLocationPayload } from "./location.js";
|
|
31
31
|
import { downloadMatrixMedia } from "./media.js";
|
|
@@ -236,12 +236,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
236
236
|
const storeAllowFrom = await core.channel.pairing
|
|
237
237
|
.readAllowFromStore("matrix")
|
|
238
238
|
.catch(() => []);
|
|
239
|
-
const effectiveAllowFrom =
|
|
239
|
+
const effectiveAllowFrom = normalizeMatrixAllowList([...allowFrom, ...storeAllowFrom]);
|
|
240
240
|
const groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? [];
|
|
241
|
-
const effectiveGroupAllowFrom =
|
|
242
|
-
...groupAllowFrom,
|
|
243
|
-
...storeAllowFrom,
|
|
244
|
-
]);
|
|
241
|
+
const effectiveGroupAllowFrom = normalizeMatrixAllowList(groupAllowFrom);
|
|
245
242
|
const groupAllowConfigured = effectiveGroupAllowFrom.length > 0;
|
|
246
243
|
|
|
247
244
|
if (isDirectMessage) {
|
|
@@ -252,7 +249,6 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
252
249
|
const allowMatch = resolveMatrixAllowListMatch({
|
|
253
250
|
allowList: effectiveAllowFrom,
|
|
254
251
|
userId: senderId,
|
|
255
|
-
userName: senderName,
|
|
256
252
|
});
|
|
257
253
|
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
|
|
258
254
|
if (!allowMatch.allowed) {
|
|
@@ -297,9 +293,8 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
297
293
|
const roomUsers = roomConfig?.users ?? [];
|
|
298
294
|
if (isRoom && roomUsers.length > 0) {
|
|
299
295
|
const userMatch = resolveMatrixAllowListMatch({
|
|
300
|
-
allowList:
|
|
296
|
+
allowList: normalizeMatrixAllowList(roomUsers),
|
|
301
297
|
userId: senderId,
|
|
302
|
-
userName: senderName,
|
|
303
298
|
});
|
|
304
299
|
if (!userMatch.allowed) {
|
|
305
300
|
logVerboseMessage(
|
|
@@ -314,7 +309,6 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
314
309
|
const groupAllowMatch = resolveMatrixAllowListMatch({
|
|
315
310
|
allowList: effectiveGroupAllowFrom,
|
|
316
311
|
userId: senderId,
|
|
317
|
-
userName: senderName,
|
|
318
312
|
});
|
|
319
313
|
if (!groupAllowMatch.allowed) {
|
|
320
314
|
logVerboseMessage(
|
|
@@ -387,21 +381,18 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
387
381
|
const senderAllowedForCommands = resolveMatrixAllowListMatches({
|
|
388
382
|
allowList: effectiveAllowFrom,
|
|
389
383
|
userId: senderId,
|
|
390
|
-
userName: senderName,
|
|
391
384
|
});
|
|
392
385
|
const senderAllowedForGroup = groupAllowConfigured
|
|
393
386
|
? resolveMatrixAllowListMatches({
|
|
394
387
|
allowList: effectiveGroupAllowFrom,
|
|
395
388
|
userId: senderId,
|
|
396
|
-
userName: senderName,
|
|
397
389
|
})
|
|
398
390
|
: false;
|
|
399
391
|
const senderAllowedForRoomUsers =
|
|
400
392
|
isRoom && roomUsers.length > 0
|
|
401
393
|
? resolveMatrixAllowListMatches({
|
|
402
|
-
allowList:
|
|
394
|
+
allowList: normalizeMatrixAllowList(roomUsers),
|
|
403
395
|
userId: senderId,
|
|
404
|
-
userName: senderName,
|
|
405
396
|
})
|
|
406
397
|
: false;
|
|
407
398
|
const hasControlCommandInMessage = core.channel.text.hasControlCommand(bodyText, cfg);
|
|
@@ -588,7 +579,12 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
588
579
|
channel: "matrix",
|
|
589
580
|
accountId: route.accountId,
|
|
590
581
|
});
|
|
591
|
-
const
|
|
582
|
+
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
|
|
583
|
+
cfg,
|
|
584
|
+
agentId: route.agentId,
|
|
585
|
+
channel: "matrix",
|
|
586
|
+
accountId: route.accountId,
|
|
587
|
+
});
|
|
592
588
|
const typingCallbacks = createTypingCallbacks({
|
|
593
589
|
start: () => sendTypingMatrix(roomId, true, undefined, client),
|
|
594
590
|
stop: () => sendTypingMatrix(roomId, false, undefined, client),
|
|
@@ -613,8 +609,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
613
609
|
});
|
|
614
610
|
const { dispatcher, replyOptions, markDispatchIdle } =
|
|
615
611
|
core.channel.reply.createReplyDispatcherWithTyping({
|
|
616
|
-
|
|
617
|
-
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
612
|
+
...prefixOptions,
|
|
618
613
|
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
|
|
619
614
|
deliver: async (payload) => {
|
|
620
615
|
await deliverMatrixReplies({
|
|
@@ -644,7 +639,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
644
639
|
replyOptions: {
|
|
645
640
|
...replyOptions,
|
|
646
641
|
skillFilter: roomConfig?.skills,
|
|
647
|
-
onModelSelected
|
|
642
|
+
onModelSelected,
|
|
648
643
|
},
|
|
649
644
|
});
|
|
650
645
|
markDispatchIdle();
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
resolveSharedMatrixClient,
|
|
11
11
|
stopSharedClient,
|
|
12
12
|
} from "../client.js";
|
|
13
|
+
import { normalizeMatrixUserId } from "./allowlist.js";
|
|
13
14
|
import { registerMatrixAutoJoin } from "./auto-join.js";
|
|
14
15
|
import { createDirectRoomTracker } from "./direct.js";
|
|
15
16
|
import { registerMatrixMonitorEvents } from "./events.js";
|
|
@@ -68,68 +69,94 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
68
69
|
.replace(/^(room|channel):/i, "")
|
|
69
70
|
.trim();
|
|
70
71
|
const isMatrixUserId = (value: string) => value.startsWith("@") && value.includes(":");
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
const resolveUserAllowlist = async (
|
|
73
|
+
label: string,
|
|
74
|
+
list?: Array<string | number>,
|
|
75
|
+
): Promise<string[]> => {
|
|
76
|
+
let allowList = list ?? [];
|
|
77
|
+
if (allowList.length === 0) {
|
|
78
|
+
return allowList;
|
|
79
|
+
}
|
|
80
|
+
const entries = allowList
|
|
78
81
|
.map((entry) => normalizeUserEntry(String(entry)))
|
|
79
82
|
.filter((entry) => entry && entry !== "*");
|
|
80
|
-
if (entries.length
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
if (entries.length === 0) {
|
|
84
|
+
return allowList;
|
|
85
|
+
}
|
|
86
|
+
const mapping: string[] = [];
|
|
87
|
+
const unresolved: string[] = [];
|
|
88
|
+
const additions: string[] = [];
|
|
89
|
+
const pending: string[] = [];
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
if (isMatrixUserId(entry)) {
|
|
92
|
+
additions.push(normalizeMatrixUserId(entry));
|
|
93
|
+
continue;
|
|
91
94
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
95
|
+
pending.push(entry);
|
|
96
|
+
}
|
|
97
|
+
if (pending.length > 0) {
|
|
98
|
+
const resolved = await resolveMatrixTargets({
|
|
99
|
+
cfg,
|
|
100
|
+
inputs: pending,
|
|
101
|
+
kind: "user",
|
|
102
|
+
runtime,
|
|
103
|
+
});
|
|
104
|
+
for (const entry of resolved) {
|
|
105
|
+
if (entry.resolved && entry.id) {
|
|
106
|
+
const normalizedId = normalizeMatrixUserId(entry.id);
|
|
107
|
+
additions.push(normalizedId);
|
|
108
|
+
mapping.push(`${entry.input}→${normalizedId}`);
|
|
109
|
+
} else {
|
|
110
|
+
unresolved.push(entry.input);
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
|
-
allowFrom = mergeAllowlist({ existing: allowFrom, additions });
|
|
109
|
-
summarizeMapping("matrix users", mapping, unresolved, runtime);
|
|
110
113
|
}
|
|
111
|
-
|
|
114
|
+
allowList = mergeAllowlist({ existing: allowList, additions });
|
|
115
|
+
summarizeMapping(label, mapping, unresolved, runtime);
|
|
116
|
+
if (unresolved.length > 0) {
|
|
117
|
+
runtime.log?.(
|
|
118
|
+
`${label} entries must be full Matrix IDs (example: @user:server). Unresolved entries are ignored.`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return allowList;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const allowlistOnly = cfg.channels?.matrix?.allowlistOnly === true;
|
|
125
|
+
let allowFrom = cfg.channels?.matrix?.dm?.allowFrom ?? [];
|
|
126
|
+
let groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? [];
|
|
127
|
+
let roomsConfig = cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms;
|
|
128
|
+
|
|
129
|
+
allowFrom = await resolveUserAllowlist("matrix dm allowlist", allowFrom);
|
|
130
|
+
groupAllowFrom = await resolveUserAllowlist("matrix group allowlist", groupAllowFrom);
|
|
112
131
|
|
|
113
132
|
if (roomsConfig && Object.keys(roomsConfig).length > 0) {
|
|
114
|
-
const entries = Object.keys(roomsConfig).filter((key) => key !== "*");
|
|
115
133
|
const mapping: string[] = [];
|
|
116
134
|
const unresolved: string[] = [];
|
|
117
|
-
const nextRooms
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
const nextRooms: Record<string, (typeof roomsConfig)[string]> = {};
|
|
136
|
+
if (roomsConfig["*"]) {
|
|
137
|
+
nextRooms["*"] = roomsConfig["*"];
|
|
138
|
+
}
|
|
139
|
+
const pending: Array<{ input: string; query: string; config: (typeof roomsConfig)[string] }> =
|
|
140
|
+
[];
|
|
141
|
+
for (const [entry, roomConfig] of Object.entries(roomsConfig)) {
|
|
142
|
+
if (entry === "*") {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
120
145
|
const trimmed = entry.trim();
|
|
121
146
|
if (!trimmed) {
|
|
122
147
|
continue;
|
|
123
148
|
}
|
|
124
149
|
const cleaned = normalizeRoomEntry(trimmed);
|
|
125
|
-
if (cleaned.startsWith("!") && cleaned.includes(":")) {
|
|
150
|
+
if ((cleaned.startsWith("!") || cleaned.startsWith("#")) && cleaned.includes(":")) {
|
|
126
151
|
if (!nextRooms[cleaned]) {
|
|
127
|
-
nextRooms[cleaned] =
|
|
152
|
+
nextRooms[cleaned] = roomConfig;
|
|
153
|
+
}
|
|
154
|
+
if (cleaned !== entry) {
|
|
155
|
+
mapping.push(`${entry}→${cleaned}`);
|
|
128
156
|
}
|
|
129
|
-
mapping.push(`${entry}→${cleaned}`);
|
|
130
157
|
continue;
|
|
131
158
|
}
|
|
132
|
-
pending.push({ input: entry, query: trimmed });
|
|
159
|
+
pending.push({ input: entry, query: trimmed, config: roomConfig });
|
|
133
160
|
}
|
|
134
161
|
if (pending.length > 0) {
|
|
135
162
|
const resolved = await resolveMatrixTargets({
|
|
@@ -145,7 +172,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
145
172
|
}
|
|
146
173
|
if (entry.resolved && entry.id) {
|
|
147
174
|
if (!nextRooms[entry.id]) {
|
|
148
|
-
nextRooms[entry.id] =
|
|
175
|
+
nextRooms[entry.id] = source.config;
|
|
149
176
|
}
|
|
150
177
|
mapping.push(`${source.input}→${entry.id}`);
|
|
151
178
|
} else {
|
|
@@ -155,6 +182,25 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
155
182
|
}
|
|
156
183
|
roomsConfig = nextRooms;
|
|
157
184
|
summarizeMapping("matrix rooms", mapping, unresolved, runtime);
|
|
185
|
+
if (unresolved.length > 0) {
|
|
186
|
+
runtime.log?.(
|
|
187
|
+
"matrix rooms must be room IDs or aliases (example: !room:server or #alias:server). Unresolved entries are ignored.",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (roomsConfig && Object.keys(roomsConfig).length > 0) {
|
|
192
|
+
const nextRooms = { ...roomsConfig };
|
|
193
|
+
for (const [roomKey, roomConfig] of Object.entries(roomsConfig)) {
|
|
194
|
+
const users = roomConfig?.users ?? [];
|
|
195
|
+
if (users.length === 0) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const resolvedUsers = await resolveUserAllowlist(`matrix room users (${roomKey})`, users);
|
|
199
|
+
if (resolvedUsers !== users) {
|
|
200
|
+
nextRooms[roomKey] = { ...roomConfig, users: resolvedUsers };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
roomsConfig = nextRooms;
|
|
158
204
|
}
|
|
159
205
|
|
|
160
206
|
cfg = {
|
|
@@ -167,6 +213,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
167
213
|
...cfg.channels?.matrix?.dm,
|
|
168
214
|
allowFrom,
|
|
169
215
|
},
|
|
216
|
+
...(groupAllowFrom.length > 0 ? { groupAllowFrom } : {}),
|
|
170
217
|
...(roomsConfig ? { groups: roomsConfig } : {}),
|
|
171
218
|
},
|
|
172
219
|
},
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolveMatrixRoomConfig } from "./rooms.js";
|
|
3
|
+
|
|
4
|
+
describe("resolveMatrixRoomConfig", () => {
|
|
5
|
+
it("matches room IDs and aliases, not names", () => {
|
|
6
|
+
const rooms = {
|
|
7
|
+
"!room:example.org": { allow: true },
|
|
8
|
+
"#alias:example.org": { allow: true },
|
|
9
|
+
"Project Room": { allow: true },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const byId = resolveMatrixRoomConfig({
|
|
13
|
+
rooms,
|
|
14
|
+
roomId: "!room:example.org",
|
|
15
|
+
aliases: [],
|
|
16
|
+
name: "Project Room",
|
|
17
|
+
});
|
|
18
|
+
expect(byId.allowed).toBe(true);
|
|
19
|
+
expect(byId.matchKey).toBe("!room:example.org");
|
|
20
|
+
|
|
21
|
+
const byAlias = resolveMatrixRoomConfig({
|
|
22
|
+
rooms,
|
|
23
|
+
roomId: "!other:example.org",
|
|
24
|
+
aliases: ["#alias:example.org"],
|
|
25
|
+
name: "Other Room",
|
|
26
|
+
});
|
|
27
|
+
expect(byAlias.allowed).toBe(true);
|
|
28
|
+
expect(byAlias.matchKey).toBe("#alias:example.org");
|
|
29
|
+
|
|
30
|
+
const byName = resolveMatrixRoomConfig({
|
|
31
|
+
rooms: { "Project Room": { allow: true } },
|
|
32
|
+
roomId: "!different:example.org",
|
|
33
|
+
aliases: [],
|
|
34
|
+
name: "Project Room",
|
|
35
|
+
});
|
|
36
|
+
expect(byName.allowed).toBe(false);
|
|
37
|
+
expect(byName.config).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
});
|
package/src/onboarding.ts
CHANGED
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
} from "openclaw/plugin-sdk";
|
|
9
9
|
import type { CoreConfig, DmPolicy } from "./types.js";
|
|
10
10
|
import { listMatrixDirectoryGroupsLive } from "./directory-live.js";
|
|
11
|
-
import { listMatrixDirectoryPeersLive } from "./directory-live.js";
|
|
12
11
|
import { resolveMatrixAccount } from "./matrix/accounts.js";
|
|
13
12
|
import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js";
|
|
13
|
+
import { resolveMatrixTargets } from "./resolve-targets.js";
|
|
14
14
|
|
|
15
15
|
const channel = "matrix" as const;
|
|
16
16
|
|
|
@@ -65,14 +65,16 @@ async function promptMatrixAllowFrom(params: {
|
|
|
65
65
|
|
|
66
66
|
while (true) {
|
|
67
67
|
const entry = await prompter.text({
|
|
68
|
-
message: "Matrix allowFrom (
|
|
68
|
+
message: "Matrix allowFrom (full @user:server; display name only if unique)",
|
|
69
69
|
placeholder: "@user:server",
|
|
70
70
|
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
|
71
71
|
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
72
72
|
});
|
|
73
73
|
const parts = parseInput(String(entry));
|
|
74
74
|
const resolvedIds: string[] = [];
|
|
75
|
-
|
|
75
|
+
const pending: string[] = [];
|
|
76
|
+
const unresolved: string[] = [];
|
|
77
|
+
const unresolvedNotes: string[] = [];
|
|
76
78
|
|
|
77
79
|
for (const part of parts) {
|
|
78
80
|
if (isFullUserId(part)) {
|
|
@@ -83,28 +85,33 @@ async function promptMatrixAllowFrom(params: {
|
|
|
83
85
|
unresolved.push(part);
|
|
84
86
|
continue;
|
|
85
87
|
}
|
|
86
|
-
|
|
88
|
+
pending.push(part);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (pending.length > 0) {
|
|
92
|
+
const results = await resolveMatrixTargets({
|
|
87
93
|
cfg,
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
inputs: pending,
|
|
95
|
+
kind: "user",
|
|
90
96
|
}).catch(() => []);
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
)
|
|
97
|
+
for (const result of results) {
|
|
98
|
+
if (result?.resolved && result.id) {
|
|
99
|
+
resolvedIds.push(result.id);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (result?.input) {
|
|
103
|
+
unresolved.push(result.input);
|
|
104
|
+
if (result.note) {
|
|
105
|
+
unresolvedNotes.push(`${result.input}: ${result.note}`);
|
|
106
|
+
}
|
|
99
107
|
}
|
|
100
|
-
} else {
|
|
101
|
-
unresolved.push(part);
|
|
102
108
|
}
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
if (unresolved.length > 0) {
|
|
112
|
+
const details = unresolvedNotes.length > 0 ? unresolvedNotes : unresolved;
|
|
106
113
|
await prompter.note(
|
|
107
|
-
`Could not resolve
|
|
114
|
+
`Could not resolve:\n${details.join("\n")}\nUse full @user:server IDs.`,
|
|
108
115
|
"Matrix allowlist",
|
|
109
116
|
);
|
|
110
117
|
continue;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk";
|
|
2
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
3
|
+
import { listMatrixDirectoryPeersLive } from "./directory-live.js";
|
|
4
|
+
import { resolveMatrixTargets } from "./resolve-targets.js";
|
|
5
|
+
|
|
6
|
+
vi.mock("./directory-live.js", () => ({
|
|
7
|
+
listMatrixDirectoryPeersLive: vi.fn(),
|
|
8
|
+
listMatrixDirectoryGroupsLive: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe("resolveMatrixTargets (users)", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.mocked(listMatrixDirectoryPeersLive).mockReset();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("resolves exact unique display name matches", async () => {
|
|
17
|
+
const matches: ChannelDirectoryEntry[] = [
|
|
18
|
+
{ kind: "user", id: "@alice:example.org", name: "Alice" },
|
|
19
|
+
];
|
|
20
|
+
vi.mocked(listMatrixDirectoryPeersLive).mockResolvedValue(matches);
|
|
21
|
+
|
|
22
|
+
const [result] = await resolveMatrixTargets({
|
|
23
|
+
cfg: {},
|
|
24
|
+
inputs: ["Alice"],
|
|
25
|
+
kind: "user",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(result?.resolved).toBe(true);
|
|
29
|
+
expect(result?.id).toBe("@alice:example.org");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("does not resolve ambiguous or non-exact matches", async () => {
|
|
33
|
+
const matches: ChannelDirectoryEntry[] = [
|
|
34
|
+
{ kind: "user", id: "@alice:example.org", name: "Alice" },
|
|
35
|
+
{ kind: "user", id: "@alice:evil.example", name: "Alice" },
|
|
36
|
+
];
|
|
37
|
+
vi.mocked(listMatrixDirectoryPeersLive).mockResolvedValue(matches);
|
|
38
|
+
|
|
39
|
+
const [result] = await resolveMatrixTargets({
|
|
40
|
+
cfg: {},
|
|
41
|
+
inputs: ["Alice"],
|
|
42
|
+
kind: "user",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result?.resolved).toBe(false);
|
|
46
|
+
expect(result?.note).toMatch(/use full Matrix ID/i);
|
|
47
|
+
});
|
|
48
|
+
});
|
package/src/resolve-targets.ts
CHANGED
|
@@ -28,6 +28,52 @@ function pickBestGroupMatch(
|
|
|
28
28
|
return matches[0];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function pickBestUserMatch(
|
|
32
|
+
matches: ChannelDirectoryEntry[],
|
|
33
|
+
query: string,
|
|
34
|
+
): ChannelDirectoryEntry | undefined {
|
|
35
|
+
if (matches.length === 0) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const normalized = query.trim().toLowerCase();
|
|
39
|
+
if (!normalized) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const exact = matches.filter((match) => {
|
|
43
|
+
const id = match.id.trim().toLowerCase();
|
|
44
|
+
const name = match.name?.trim().toLowerCase();
|
|
45
|
+
const handle = match.handle?.trim().toLowerCase();
|
|
46
|
+
return normalized === id || normalized === name || normalized === handle;
|
|
47
|
+
});
|
|
48
|
+
if (exact.length === 1) {
|
|
49
|
+
return exact[0];
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function describeUserMatchFailure(matches: ChannelDirectoryEntry[], query: string): string {
|
|
55
|
+
if (matches.length === 0) {
|
|
56
|
+
return "no matches";
|
|
57
|
+
}
|
|
58
|
+
const normalized = query.trim().toLowerCase();
|
|
59
|
+
if (!normalized) {
|
|
60
|
+
return "empty input";
|
|
61
|
+
}
|
|
62
|
+
const exact = matches.filter((match) => {
|
|
63
|
+
const id = match.id.trim().toLowerCase();
|
|
64
|
+
const name = match.name?.trim().toLowerCase();
|
|
65
|
+
const handle = match.handle?.trim().toLowerCase();
|
|
66
|
+
return normalized === id || normalized === name || normalized === handle;
|
|
67
|
+
});
|
|
68
|
+
if (exact.length === 0) {
|
|
69
|
+
return "no exact match; use full Matrix ID";
|
|
70
|
+
}
|
|
71
|
+
if (exact.length > 1) {
|
|
72
|
+
return "multiple exact matches; use full Matrix ID";
|
|
73
|
+
}
|
|
74
|
+
return "no exact match; use full Matrix ID";
|
|
75
|
+
}
|
|
76
|
+
|
|
31
77
|
export async function resolveMatrixTargets(params: {
|
|
32
78
|
cfg: unknown;
|
|
33
79
|
inputs: string[];
|
|
@@ -52,13 +98,13 @@ export async function resolveMatrixTargets(params: {
|
|
|
52
98
|
query: trimmed,
|
|
53
99
|
limit: 5,
|
|
54
100
|
});
|
|
55
|
-
const best = matches
|
|
101
|
+
const best = pickBestUserMatch(matches, trimmed);
|
|
56
102
|
results.push({
|
|
57
103
|
input,
|
|
58
104
|
resolved: Boolean(best?.id),
|
|
59
105
|
id: best?.id,
|
|
60
106
|
name: best?.name,
|
|
61
|
-
note:
|
|
107
|
+
note: best ? undefined : describeUserMatchFailure(matches, trimmed),
|
|
62
108
|
});
|
|
63
109
|
} catch (err) {
|
|
64
110
|
params.runtime?.error?.(`matrix resolve failed: ${String(err)}`);
|
package/src/types.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type MatrixDmConfig = {
|
|
|
7
7
|
enabled?: boolean;
|
|
8
8
|
/** Direct message access policy (default: pairing). */
|
|
9
9
|
policy?: DmPolicy;
|
|
10
|
-
/** Allowlist for DM senders (matrix user IDs
|
|
10
|
+
/** Allowlist for DM senders (matrix user IDs or "*"). */
|
|
11
11
|
allowFrom?: Array<string | number>;
|
|
12
12
|
};
|
|
13
13
|
|
|
@@ -22,7 +22,7 @@ export type MatrixRoomConfig = {
|
|
|
22
22
|
tools?: { allow?: string[]; deny?: string[] };
|
|
23
23
|
/** If true, reply without mention requirements. */
|
|
24
24
|
autoReply?: boolean;
|
|
25
|
-
/** Optional allowlist for room senders (user IDs
|
|
25
|
+
/** Optional allowlist for room senders (matrix user IDs). */
|
|
26
26
|
users?: Array<string | number>;
|
|
27
27
|
/** Optional skill filter for this room. */
|
|
28
28
|
skills?: string[];
|
|
@@ -61,7 +61,7 @@ export type MatrixConfig = {
|
|
|
61
61
|
allowlistOnly?: boolean;
|
|
62
62
|
/** Group message policy (default: allowlist). */
|
|
63
63
|
groupPolicy?: GroupPolicy;
|
|
64
|
-
/** Allowlist for group senders (user IDs
|
|
64
|
+
/** Allowlist for group senders (matrix user IDs). */
|
|
65
65
|
groupAllowFrom?: Array<string | number>;
|
|
66
66
|
/** Control reply threading when reply tags are present (off|first|all). */
|
|
67
67
|
replyToMode?: ReplyToMode;
|
|
@@ -71,6 +71,8 @@ export type MatrixConfig = {
|
|
|
71
71
|
textChunkLimit?: number;
|
|
72
72
|
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
|
|
73
73
|
chunkMode?: "length" | "newline";
|
|
74
|
+
/** Outbound response prefix override for this channel/account. */
|
|
75
|
+
responsePrefix?: string;
|
|
74
76
|
/** Max outbound media size in MB. */
|
|
75
77
|
mediaMaxMb?: number;
|
|
76
78
|
/** Auto-join invites (always|allowlist|off). Default: always. */
|
|
@@ -79,9 +81,9 @@ export type MatrixConfig = {
|
|
|
79
81
|
autoJoinAllowlist?: Array<string | number>;
|
|
80
82
|
/** Direct message policy + allowlist overrides. */
|
|
81
83
|
dm?: MatrixDmConfig;
|
|
82
|
-
/** Room config allowlist keyed by room ID
|
|
84
|
+
/** Room config allowlist keyed by room ID or alias (names resolved to IDs when possible). */
|
|
83
85
|
groups?: Record<string, MatrixRoomConfig>;
|
|
84
|
-
/** Room config allowlist keyed by room ID
|
|
86
|
+
/** Room config allowlist keyed by room ID or alias. Legacy; use groups. */
|
|
85
87
|
rooms?: Record<string, MatrixRoomConfig>;
|
|
86
88
|
/** Per-action tool gating (default: true for all). */
|
|
87
89
|
actions?: MatrixActionConfig;
|