Matrix: add allowBots bot-to-bot policy

This commit is contained in:
Gustavo Madeira Santana 2026-03-19 21:39:31 -04:00
parent de9f2dc227
commit ab97cc3f11
No known key found for this signature in database
15 changed files with 463 additions and 2 deletions

View File

@ -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.

View File

@ -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(),

View File

@ -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",
]);
});
});

View File

@ -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;

View File

@ -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",

View File

@ -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;

View File

@ -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",

View File

@ -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,

View File

@ -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",

View File

@ -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,

View File

@ -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). */

View File

@ -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({

View File

@ -342,6 +342,7 @@ const SINGLE_ACCOUNT_KEYS_TO_MOVE_BY_CHANNEL: Record<string, ReadonlySet<string>
"initialSyncLimit",
"encryption",
"allowlistOnly",
"allowBots",
"replyToMode",
"threadReplies",
"textChunkLimit",

View File

@ -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":

View File

@ -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",