Matrix: add allowBots bot-to-bot policy
This commit is contained in:
parent
de9f2dc227
commit
ab97cc3f11
@ -164,6 +164,35 @@ This is a practical baseline config with DM pairing, room allowlist, and E2EE en
|
||||
|
||||
## E2EE setup
|
||||
|
||||
## Bot to bot rooms
|
||||
|
||||
By default, Matrix messages from other configured OpenClaw Matrix accounts are ignored.
|
||||
|
||||
Use `allowBots` when you intentionally want inter-agent Matrix traffic:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
matrix: {
|
||||
allowBots: "mentions", // true | "mentions"
|
||||
groups: {
|
||||
"!roomid:example.org": {
|
||||
requireMention: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- `allowBots: true` accepts messages from other configured Matrix bot accounts in allowed rooms and DMs.
|
||||
- `allowBots: "mentions"` accepts those messages only when they visibly mention this bot in rooms. DMs are still allowed.
|
||||
- `groups.<room>.allowBots` overrides the account-level setting for one room.
|
||||
- OpenClaw still ignores messages from the same Matrix user ID to avoid self-reply loops.
|
||||
- Matrix does not expose a native bot flag here; OpenClaw treats "bot-authored" as "sent by another configured Matrix account on this OpenClaw gateway".
|
||||
|
||||
Use strict room allowlists and mention requirements when enabling bot-to-bot traffic in shared rooms.
|
||||
|
||||
Enable encryption:
|
||||
|
||||
```json5
|
||||
@ -580,6 +609,7 @@ Live directory lookup uses the logged-in Matrix account:
|
||||
- `name`: optional label for the account.
|
||||
- `defaultAccount`: preferred account ID when multiple Matrix accounts are configured.
|
||||
- `homeserver`: homeserver URL, for example `https://matrix.example.org`.
|
||||
- `allowPrivateNetwork`: allow this Matrix account to connect to private/internal homeservers. Enable this when the homeserver resolves to `localhost`, a LAN/Tailscale IP, or an internal host such as `matrix-synapse`.
|
||||
- `userId`: full Matrix user ID, for example `@bot:example.org`.
|
||||
- `accessToken`: access token for token-based auth.
|
||||
- `password`: password for password-based login.
|
||||
|
||||
@ -34,6 +34,7 @@ const matrixRoomSchema = z
|
||||
enabled: z.boolean().optional(),
|
||||
allow: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
allowBots: z.union([z.boolean(), z.literal("mentions")]).optional(),
|
||||
tools: ToolPolicySchema,
|
||||
autoReply: z.boolean().optional(),
|
||||
users: AllowFromListSchema,
|
||||
@ -49,6 +50,7 @@ export const MatrixConfigSchema = z.object({
|
||||
accounts: z.record(z.string(), z.unknown()).optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
homeserver: z.string().optional(),
|
||||
allowPrivateNetwork: z.boolean().optional(),
|
||||
userId: z.string().optional(),
|
||||
accessToken: z.string().optional(),
|
||||
password: buildSecretInputSchema().optional(),
|
||||
@ -58,6 +60,7 @@ export const MatrixConfigSchema = z.object({
|
||||
initialSyncLimit: z.number().optional(),
|
||||
encryption: z.boolean().optional(),
|
||||
allowlistOnly: z.boolean().optional(),
|
||||
allowBots: z.union([z.boolean(), z.literal("mentions")]).optional(),
|
||||
groupPolicy: GroupPolicySchema.optional(),
|
||||
replyToMode: z.enum(["off", "first", "all"]).optional(),
|
||||
threadReplies: z.enum(["off", "inbound", "always"]).optional(),
|
||||
|
||||
@ -3,12 +3,21 @@ import { getMatrixScopedEnvVarNames } from "../env-vars.js";
|
||||
import type { CoreConfig } from "../types.js";
|
||||
import {
|
||||
listMatrixAccountIds,
|
||||
resolveConfiguredMatrixBotUserIds,
|
||||
resolveDefaultMatrixAccountId,
|
||||
resolveMatrixAccount,
|
||||
} from "./accounts.js";
|
||||
import type { MatrixStoredCredentials } from "./credentials-read.js";
|
||||
|
||||
const loadMatrixCredentialsMock = vi.hoisted(() =>
|
||||
vi.fn<(env?: NodeJS.ProcessEnv, accountId?: string | null) => MatrixStoredCredentials | null>(
|
||||
() => null,
|
||||
),
|
||||
);
|
||||
|
||||
vi.mock("./credentials-read.js", () => ({
|
||||
loadMatrixCredentials: () => null,
|
||||
loadMatrixCredentials: (env?: NodeJS.ProcessEnv, accountId?: string | null) =>
|
||||
loadMatrixCredentialsMock(env, accountId),
|
||||
credentialsMatchConfig: () => false,
|
||||
}));
|
||||
|
||||
@ -28,6 +37,7 @@ describe("resolveMatrixAccount", () => {
|
||||
let prevEnv: Record<string, string | undefined> = {};
|
||||
|
||||
beforeEach(() => {
|
||||
loadMatrixCredentialsMock.mockReset().mockReturnValue(null);
|
||||
prevEnv = {};
|
||||
for (const key of envKeys) {
|
||||
prevEnv[key] = process.env[key];
|
||||
@ -195,4 +205,66 @@ describe("resolveMatrixAccount", () => {
|
||||
|
||||
expect(resolveDefaultMatrixAccountId(cfg)).toBe("default");
|
||||
});
|
||||
|
||||
it("collects other configured Matrix account user ids for bot detection", () => {
|
||||
const cfg: CoreConfig = {
|
||||
channels: {
|
||||
matrix: {
|
||||
userId: "@main:example.org",
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "main-token",
|
||||
accounts: {
|
||||
ops: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@ops:example.org",
|
||||
accessToken: "ops-token",
|
||||
},
|
||||
alerts: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@alerts:example.org",
|
||||
accessToken: "alerts-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
Array.from(resolveConfiguredMatrixBotUserIds({ cfg, accountId: "ops" })).toSorted(),
|
||||
).toEqual(["@alerts:example.org", "@main:example.org"]);
|
||||
});
|
||||
|
||||
it("falls back to stored credentials when an access-token-only account omits userId", () => {
|
||||
loadMatrixCredentialsMock.mockImplementation(
|
||||
(env?: NodeJS.ProcessEnv, accountId?: string | null) =>
|
||||
accountId === "ops"
|
||||
? {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@ops:example.org",
|
||||
accessToken: "ops-token",
|
||||
createdAt: "2026-03-19T00:00:00.000Z",
|
||||
}
|
||||
: null,
|
||||
);
|
||||
|
||||
const cfg: CoreConfig = {
|
||||
channels: {
|
||||
matrix: {
|
||||
userId: "@main:example.org",
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "main-token",
|
||||
accounts: {
|
||||
ops: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "ops-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(Array.from(resolveConfiguredMatrixBotUserIds({ cfg, accountId: "default" }))).toEqual([
|
||||
"@ops:example.org",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -38,6 +38,31 @@ export type ResolvedMatrixAccount = {
|
||||
config: MatrixConfig;
|
||||
};
|
||||
|
||||
function resolveMatrixAccountUserId(params: {
|
||||
cfg: CoreConfig;
|
||||
accountId: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): string | null {
|
||||
const env = params.env ?? process.env;
|
||||
const resolved = resolveMatrixConfigForAccount(params.cfg, params.accountId, env);
|
||||
const configuredUserId = resolved.userId.trim();
|
||||
if (configuredUserId) {
|
||||
return configuredUserId;
|
||||
}
|
||||
|
||||
const stored = loadMatrixCredentials(env, params.accountId);
|
||||
if (!stored) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.homeserver && stored.homeserver !== resolved.homeserver) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.accessToken && stored.accessToken !== resolved.accessToken) {
|
||||
return null;
|
||||
}
|
||||
return stored.userId.trim() || null;
|
||||
}
|
||||
|
||||
export function listMatrixAccountIds(cfg: CoreConfig): string[] {
|
||||
const ids = resolveConfiguredMatrixAccountIds(cfg, process.env);
|
||||
return ids.length > 0 ? ids : [DEFAULT_ACCOUNT_ID];
|
||||
@ -47,6 +72,39 @@ export function resolveDefaultMatrixAccountId(cfg: CoreConfig): string {
|
||||
return normalizeAccountId(resolveMatrixDefaultOrOnlyAccountId(cfg));
|
||||
}
|
||||
|
||||
export function resolveConfiguredMatrixBotUserIds(params: {
|
||||
cfg: CoreConfig;
|
||||
accountId?: string | null;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Set<string> {
|
||||
const env = params.env ?? process.env;
|
||||
const currentAccountId = normalizeAccountId(params.accountId);
|
||||
const accountIds = new Set(resolveConfiguredMatrixAccountIds(params.cfg, env));
|
||||
if (resolveMatrixAccount({ cfg: params.cfg, accountId: DEFAULT_ACCOUNT_ID }).configured) {
|
||||
accountIds.add(DEFAULT_ACCOUNT_ID);
|
||||
}
|
||||
const ids = new Set<string>();
|
||||
|
||||
for (const accountId of accountIds) {
|
||||
if (normalizeAccountId(accountId) === currentAccountId) {
|
||||
continue;
|
||||
}
|
||||
if (!resolveMatrixAccount({ cfg: params.cfg, accountId }).configured) {
|
||||
continue;
|
||||
}
|
||||
const userId = resolveMatrixAccountUserId({
|
||||
cfg: params.cfg,
|
||||
accountId,
|
||||
env,
|
||||
});
|
||||
if (userId) {
|
||||
ids.add(userId);
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
export function resolveMatrixAccount(params: {
|
||||
cfg: CoreConfig;
|
||||
accountId?: string | null;
|
||||
|
||||
@ -55,6 +55,31 @@ describe("updateMatrixAccountConfig", () => {
|
||||
expect(updated.channels?.["matrix"]?.accounts?.default?.userId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("stores and clears Matrix allowBots and allowPrivateNetwork settings", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
accounts: {
|
||||
default: {
|
||||
allowBots: true,
|
||||
allowPrivateNetwork: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
|
||||
const updated = updateMatrixAccountConfig(cfg, "default", {
|
||||
allowBots: "mentions",
|
||||
allowPrivateNetwork: null,
|
||||
});
|
||||
|
||||
expect(updated.channels?.["matrix"]?.accounts?.default).toMatchObject({
|
||||
allowBots: "mentions",
|
||||
});
|
||||
expect(updated.channels?.["matrix"]?.accounts?.default?.allowPrivateNetwork).toBeUndefined();
|
||||
});
|
||||
|
||||
it("normalizes account id and defaults account enabled=true", () => {
|
||||
const updated = updateMatrixAccountConfig({} as CoreConfig, "Main Bot", {
|
||||
name: "Main Bot",
|
||||
|
||||
@ -7,6 +7,7 @@ export type MatrixAccountPatch = {
|
||||
name?: string | null;
|
||||
enabled?: boolean;
|
||||
homeserver?: string | null;
|
||||
allowPrivateNetwork?: boolean | null;
|
||||
userId?: string | null;
|
||||
accessToken?: string | null;
|
||||
password?: string | null;
|
||||
@ -15,6 +16,7 @@ export type MatrixAccountPatch = {
|
||||
avatarUrl?: string | null;
|
||||
encryption?: boolean | null;
|
||||
initialSyncLimit?: number | null;
|
||||
allowBots?: MatrixConfig["allowBots"] | null;
|
||||
dm?: MatrixConfig["dm"] | null;
|
||||
groupPolicy?: MatrixConfig["groupPolicy"] | null;
|
||||
groupAllowFrom?: MatrixConfig["groupAllowFrom"] | null;
|
||||
@ -144,6 +146,14 @@ export function updateMatrixAccountConfig(
|
||||
applyNullableStringField(nextAccount, "deviceName", patch.deviceName);
|
||||
applyNullableStringField(nextAccount, "avatarUrl", patch.avatarUrl);
|
||||
|
||||
if (patch.allowPrivateNetwork !== undefined) {
|
||||
if (patch.allowPrivateNetwork === null) {
|
||||
delete nextAccount.allowPrivateNetwork;
|
||||
} else {
|
||||
nextAccount.allowPrivateNetwork = patch.allowPrivateNetwork;
|
||||
}
|
||||
}
|
||||
|
||||
if (patch.initialSyncLimit !== undefined) {
|
||||
if (patch.initialSyncLimit === null) {
|
||||
delete nextAccount.initialSyncLimit;
|
||||
@ -159,6 +169,13 @@ export function updateMatrixAccountConfig(
|
||||
nextAccount.encryption = patch.encryption;
|
||||
}
|
||||
}
|
||||
if (patch.allowBots !== undefined) {
|
||||
if (patch.allowBots === null) {
|
||||
delete nextAccount.allowBots;
|
||||
} else {
|
||||
nextAccount.allowBots = patch.allowBots;
|
||||
}
|
||||
}
|
||||
if (patch.dm !== undefined) {
|
||||
if (patch.dm === null) {
|
||||
delete nextAccount.dm;
|
||||
|
||||
@ -24,6 +24,8 @@ type MatrixHandlerTestHarnessOptions = {
|
||||
allowFrom?: string[];
|
||||
groupAllowFrom?: string[];
|
||||
roomsConfig?: Record<string, MatrixRoomConfig>;
|
||||
accountAllowBots?: boolean | "mentions";
|
||||
configuredBotUserIds?: Set<string>;
|
||||
mentionRegexes?: MatrixMonitorHandlerParams["mentionRegexes"];
|
||||
groupPolicy?: "open" | "allowlist" | "disabled";
|
||||
replyToMode?: ReplyToMode;
|
||||
@ -164,6 +166,8 @@ export function createMatrixHandlerTestHarness(
|
||||
allowFrom: options.allowFrom ?? [],
|
||||
groupAllowFrom: options.groupAllowFrom ?? [],
|
||||
roomsConfig: options.roomsConfig,
|
||||
accountAllowBots: options.accountAllowBots,
|
||||
configuredBotUserIds: options.configuredBotUserIds,
|
||||
mentionRegexes: options.mentionRegexes ?? [],
|
||||
groupPolicy: options.groupPolicy ?? "open",
|
||||
replyToMode: options.replyToMode ?? "off",
|
||||
|
||||
@ -260,6 +260,172 @@ describe("matrix monitor handler pairing account scope", () => {
|
||||
expect(enqueueSystemEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("drops room messages from configured Matrix bot accounts when allowBots is off", async () => {
|
||||
const { handler, resolveAgentRoute, recordInboundSession } = createMatrixHandlerTestHarness({
|
||||
isDirectMessage: false,
|
||||
configuredBotUserIds: new Set(["@ops:example.org"]),
|
||||
roomsConfig: {
|
||||
"!room:example.org": { requireMention: false },
|
||||
},
|
||||
getMemberDisplayName: async () => "ops-bot",
|
||||
});
|
||||
|
||||
await handler(
|
||||
"!room:example.org",
|
||||
createMatrixTextMessageEvent({
|
||||
eventId: "$bot-off",
|
||||
sender: "@ops:example.org",
|
||||
body: "hello from bot",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(resolveAgentRoute).not.toHaveBeenCalled();
|
||||
expect(recordInboundSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts room messages from configured Matrix bot accounts when allowBots is true", async () => {
|
||||
const { handler, resolveAgentRoute, recordInboundSession } = createMatrixHandlerTestHarness({
|
||||
isDirectMessage: false,
|
||||
accountAllowBots: true,
|
||||
configuredBotUserIds: new Set(["@ops:example.org"]),
|
||||
roomsConfig: {
|
||||
"!room:example.org": { requireMention: false },
|
||||
},
|
||||
getMemberDisplayName: async () => "ops-bot",
|
||||
});
|
||||
|
||||
await handler(
|
||||
"!room:example.org",
|
||||
createMatrixTextMessageEvent({
|
||||
eventId: "$bot-on",
|
||||
sender: "@ops:example.org",
|
||||
body: "hello from bot",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(resolveAgentRoute).toHaveBeenCalled();
|
||||
expect(recordInboundSession).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not treat unconfigured Matrix users as bots when allowBots is off", async () => {
|
||||
const { handler, resolveAgentRoute, recordInboundSession } = createMatrixHandlerTestHarness({
|
||||
isDirectMessage: false,
|
||||
configuredBotUserIds: new Set(["@ops:example.org"]),
|
||||
roomsConfig: {
|
||||
"!room:example.org": { requireMention: false },
|
||||
},
|
||||
getMemberDisplayName: async () => "human",
|
||||
});
|
||||
|
||||
await handler(
|
||||
"!room:example.org",
|
||||
createMatrixTextMessageEvent({
|
||||
eventId: "$non-bot",
|
||||
sender: "@alice:example.org",
|
||||
body: "hello from human",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(resolveAgentRoute).toHaveBeenCalled();
|
||||
expect(recordInboundSession).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('drops configured Matrix bot room messages without a mention when allowBots="mentions"', async () => {
|
||||
const { handler, resolveAgentRoute, recordInboundSession } = createMatrixHandlerTestHarness({
|
||||
isDirectMessage: false,
|
||||
accountAllowBots: "mentions",
|
||||
configuredBotUserIds: new Set(["@ops:example.org"]),
|
||||
roomsConfig: {
|
||||
"!room:example.org": { requireMention: false },
|
||||
},
|
||||
mentionRegexes: [/@bot/i],
|
||||
getMemberDisplayName: async () => "ops-bot",
|
||||
});
|
||||
|
||||
await handler(
|
||||
"!room:example.org",
|
||||
createMatrixTextMessageEvent({
|
||||
eventId: "$bot-mentions-off",
|
||||
sender: "@ops:example.org",
|
||||
body: "hello from bot",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(resolveAgentRoute).not.toHaveBeenCalled();
|
||||
expect(recordInboundSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accepts configured Matrix bot room messages with a mention when allowBots="mentions"', async () => {
|
||||
const { handler, resolveAgentRoute, recordInboundSession } = createMatrixHandlerTestHarness({
|
||||
isDirectMessage: false,
|
||||
accountAllowBots: "mentions",
|
||||
configuredBotUserIds: new Set(["@ops:example.org"]),
|
||||
roomsConfig: {
|
||||
"!room:example.org": { requireMention: false },
|
||||
},
|
||||
mentionRegexes: [/@bot/i],
|
||||
getMemberDisplayName: async () => "ops-bot",
|
||||
});
|
||||
|
||||
await handler(
|
||||
"!room:example.org",
|
||||
createMatrixTextMessageEvent({
|
||||
eventId: "$bot-mentions-on",
|
||||
sender: "@ops:example.org",
|
||||
body: "hello @bot",
|
||||
mentions: { user_ids: ["@bot:example.org"] },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(resolveAgentRoute).toHaveBeenCalled();
|
||||
expect(recordInboundSession).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accepts configured Matrix bot DMs without a mention when allowBots="mentions"', async () => {
|
||||
const { handler, resolveAgentRoute, recordInboundSession } = createMatrixHandlerTestHarness({
|
||||
isDirectMessage: true,
|
||||
accountAllowBots: "mentions",
|
||||
configuredBotUserIds: new Set(["@ops:example.org"]),
|
||||
getMemberDisplayName: async () => "ops-bot",
|
||||
});
|
||||
|
||||
await handler(
|
||||
"!dm:example.org",
|
||||
createMatrixTextMessageEvent({
|
||||
eventId: "$bot-dm-mentions",
|
||||
sender: "@ops:example.org",
|
||||
body: "hello from dm bot",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(resolveAgentRoute).toHaveBeenCalled();
|
||||
expect(recordInboundSession).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("lets room-level allowBots override a permissive account default", async () => {
|
||||
const { handler, resolveAgentRoute, recordInboundSession } = createMatrixHandlerTestHarness({
|
||||
isDirectMessage: false,
|
||||
accountAllowBots: true,
|
||||
configuredBotUserIds: new Set(["@ops:example.org"]),
|
||||
roomsConfig: {
|
||||
"!room:example.org": { requireMention: false, allowBots: false },
|
||||
},
|
||||
getMemberDisplayName: async () => "ops-bot",
|
||||
});
|
||||
|
||||
await handler(
|
||||
"!room:example.org",
|
||||
createMatrixTextMessageEvent({
|
||||
eventId: "$bot-room-override",
|
||||
sender: "@ops:example.org",
|
||||
body: "hello from bot",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(resolveAgentRoute).not.toHaveBeenCalled();
|
||||
expect(recordInboundSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("drops forged metadata-only mentions before agent routing", async () => {
|
||||
const { handler, recordInboundSession, resolveAgentRoute } = createMatrixHandlerTestHarness({
|
||||
isDirectMessage: false,
|
||||
|
||||
@ -46,6 +46,7 @@ import { isMatrixVerificationRoomMessage } from "./verification-utils.js";
|
||||
const ALLOW_FROM_STORE_CACHE_TTL_MS = 30_000;
|
||||
const PAIRING_REPLY_COOLDOWN_MS = 5 * 60_000;
|
||||
const MAX_TRACKED_PAIRING_REPLY_SENDERS = 512;
|
||||
type MatrixAllowBotsMode = "off" | "mentions" | "all";
|
||||
|
||||
export type MatrixMonitorHandlerParams = {
|
||||
client: MatrixClient;
|
||||
@ -58,6 +59,8 @@ export type MatrixMonitorHandlerParams = {
|
||||
allowFrom: string[];
|
||||
groupAllowFrom?: string[];
|
||||
roomsConfig?: Record<string, MatrixRoomConfig>;
|
||||
accountAllowBots?: boolean | "mentions";
|
||||
configuredBotUserIds?: ReadonlySet<string>;
|
||||
mentionRegexes: ReturnType<PluginRuntime["channel"]["mentions"]["buildMentionRegexes"]>;
|
||||
groupPolicy: "open" | "allowlist" | "disabled";
|
||||
replyToMode: ReplyToMode;
|
||||
@ -125,6 +128,16 @@ function resolveMatrixInboundBodyText(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function resolveMatrixAllowBotsMode(value?: boolean | "mentions"): MatrixAllowBotsMode {
|
||||
if (value === true) {
|
||||
return "all";
|
||||
}
|
||||
if (value === "mentions") {
|
||||
return "mentions";
|
||||
}
|
||||
return "off";
|
||||
}
|
||||
|
||||
export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParams) {
|
||||
const {
|
||||
client,
|
||||
@ -137,6 +150,8 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
allowFrom,
|
||||
groupAllowFrom = [],
|
||||
roomsConfig,
|
||||
accountAllowBots,
|
||||
configuredBotUserIds = new Set<string>(),
|
||||
mentionRegexes,
|
||||
groupPolicy,
|
||||
replyToMode,
|
||||
@ -305,12 +320,21 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
})
|
||||
: undefined;
|
||||
const roomConfig = roomConfigInfo?.config;
|
||||
const allowBotsMode = resolveMatrixAllowBotsMode(roomConfig?.allowBots ?? accountAllowBots);
|
||||
const isConfiguredBotSender = configuredBotUserIds.has(senderId);
|
||||
const roomMatchMeta = roomConfigInfo
|
||||
? `matchKey=${roomConfigInfo.matchKey ?? "none"} matchSource=${
|
||||
roomConfigInfo.matchSource ?? "none"
|
||||
}`
|
||||
: "matchKey=none matchSource=none";
|
||||
|
||||
if (isConfiguredBotSender && allowBotsMode === "off") {
|
||||
logVerboseMessage(
|
||||
`matrix: drop configured bot sender=${senderId} (allowBots=false${isDirectMessage ? "" : `, ${roomMatchMeta}`})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRoom && roomConfig && !roomConfigInfo?.allowed) {
|
||||
logVerboseMessage(`matrix: room disabled room=${roomId} (${roomMatchMeta})`);
|
||||
return;
|
||||
@ -476,6 +500,17 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
text: mentionPrecheckText,
|
||||
mentionRegexes,
|
||||
});
|
||||
if (
|
||||
isConfiguredBotSender &&
|
||||
allowBotsMode === "mentions" &&
|
||||
!isDirectMessage &&
|
||||
!wasMentioned
|
||||
) {
|
||||
logVerboseMessage(
|
||||
`matrix: drop configured bot sender=${senderId} (allowBots=mentions, missing mention, ${roomMatchMeta})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
|
||||
cfg,
|
||||
surface: "matrix",
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
} from "../../runtime-api.js";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
import type { CoreConfig, ReplyToMode } from "../../types.js";
|
||||
import { resolveMatrixAccount } from "../accounts.js";
|
||||
import { resolveConfiguredMatrixBotUserIds, resolveMatrixAccount } from "../accounts.js";
|
||||
import { setActiveMatrixClient } from "../active-client.js";
|
||||
import {
|
||||
isBunRuntime,
|
||||
@ -80,10 +80,15 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
const accountConfig = account.config;
|
||||
|
||||
const allowlistOnly = accountConfig.allowlistOnly === true;
|
||||
const accountAllowBots = accountConfig.allowBots;
|
||||
let allowFrom: string[] = (accountConfig.dm?.allowFrom ?? []).map(String);
|
||||
let groupAllowFrom: string[] = (accountConfig.groupAllowFrom ?? []).map(String);
|
||||
let roomsConfig = accountConfig.groups ?? accountConfig.rooms;
|
||||
let needsRoomAliasesForConfig = false;
|
||||
const configuredBotUserIds = resolveConfiguredMatrixBotUserIds({
|
||||
cfg,
|
||||
accountId: effectiveAccountId,
|
||||
});
|
||||
|
||||
({ allowFrom, groupAllowFrom, roomsConfig } = await resolveMatrixMonitorConfig({
|
||||
cfg,
|
||||
@ -201,6 +206,8 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
allowFrom,
|
||||
groupAllowFrom,
|
||||
roomsConfig,
|
||||
accountAllowBots,
|
||||
configuredBotUserIds,
|
||||
mentionRegexes,
|
||||
groupPolicy,
|
||||
replyToMode,
|
||||
|
||||
@ -19,6 +19,11 @@ export type MatrixRoomConfig = {
|
||||
allow?: boolean;
|
||||
/** Require mentioning the bot to trigger replies. */
|
||||
requireMention?: boolean;
|
||||
/**
|
||||
* Allow messages from other configured Matrix bot accounts.
|
||||
* true accepts all configured bot senders; "mentions" requires they mention this bot.
|
||||
*/
|
||||
allowBots?: boolean | "mentions";
|
||||
/** Optional tool policy overrides for this room. */
|
||||
tools?: { allow?: string[]; deny?: string[] };
|
||||
/** If true, reply without mention requirements. */
|
||||
@ -63,6 +68,8 @@ export type MatrixConfig = {
|
||||
defaultAccount?: string;
|
||||
/** Matrix homeserver URL (https://matrix.example.org). */
|
||||
homeserver?: string;
|
||||
/** Allow Matrix homeserver traffic to private/internal hosts. */
|
||||
allowPrivateNetwork?: boolean;
|
||||
/** Matrix user id (@user:server). */
|
||||
userId?: string;
|
||||
/** Matrix access token. */
|
||||
@ -81,6 +88,11 @@ export type MatrixConfig = {
|
||||
encryption?: boolean;
|
||||
/** If true, enforce allowlists for groups + DMs regardless of policy. */
|
||||
allowlistOnly?: boolean;
|
||||
/**
|
||||
* Allow messages from other configured Matrix bot accounts.
|
||||
* true accepts all configured bot senders; "mentions" requires they mention this bot.
|
||||
*/
|
||||
allowBots?: boolean | "mentions";
|
||||
/** Group message policy (default: allowlist). */
|
||||
groupPolicy?: GroupPolicy;
|
||||
/** Allowlist for group senders (matrix user IDs). */
|
||||
|
||||
@ -165,6 +165,34 @@ describe("createPatchedAccountSetupAdapter", () => {
|
||||
});
|
||||
|
||||
describe("moveSingleAccountChannelSectionToDefaultAccount", () => {
|
||||
it("moves Matrix allowBots into the promoted default account", () => {
|
||||
const next = moveSingleAccountChannelSectionToDefaultAccount({
|
||||
cfg: asConfig({
|
||||
channels: {
|
||||
matrix: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
allowBots: "mentions",
|
||||
},
|
||||
},
|
||||
}),
|
||||
channelKey: "matrix",
|
||||
});
|
||||
|
||||
expect(next.channels?.matrix).toMatchObject({
|
||||
accounts: {
|
||||
default: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
allowBots: "mentions",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(next.channels?.matrix?.allowBots).toBeUndefined();
|
||||
});
|
||||
|
||||
it("promotes legacy Matrix keys into the sole named account when defaultAccount is unset", () => {
|
||||
const next = moveSingleAccountChannelSectionToDefaultAccount({
|
||||
cfg: asConfig({
|
||||
|
||||
@ -342,6 +342,7 @@ const SINGLE_ACCOUNT_KEYS_TO_MOVE_BY_CHANNEL: Record<string, ReadonlySet<string>
|
||||
"initialSyncLimit",
|
||||
"encryption",
|
||||
"allowlistOnly",
|
||||
"allowBots",
|
||||
"replyToMode",
|
||||
"threadReplies",
|
||||
"textChunkLimit",
|
||||
|
||||
@ -729,6 +729,8 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
auth: "Authentication profile root used for multi-profile provider credentials and cooldown-based failover ordering. Keep profiles minimal and explicit so automatic failover behavior stays auditable.",
|
||||
"channels.slack.allowBots":
|
||||
"Allow bot-authored messages to trigger Slack replies (default: false).",
|
||||
"channels.matrix.allowBots":
|
||||
'Allow messages from other configured Matrix bot accounts to trigger replies (default: false). Set "mentions" to only accept bot messages that visibly mention this bot.',
|
||||
"channels.slack.thread.historyScope":
|
||||
'Scope for Slack thread history context ("thread" isolates per thread; "channel" reuses channel history).',
|
||||
"channels.slack.thread.inheritParent":
|
||||
|
||||
@ -807,6 +807,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"channels.slack.commands.nativeSkills": "Slack Native Skill Commands",
|
||||
"channels.slack.allowBots": "Slack Allow Bot Messages",
|
||||
"channels.discord.allowBots": "Discord Allow Bot Messages",
|
||||
"channels.matrix.allowBots": "Matrix Allow Bot Messages",
|
||||
"channels.discord.token": "Discord Bot Token",
|
||||
"channels.slack.botToken": "Slack Bot Token",
|
||||
"channels.slack.appToken": "Slack App Token",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user