From 61429230b202dbf2773a50c31f6cc4318f9420b0 Mon Sep 17 00:00:00 2001 From: Alex Zaytsev <32521398+unisone@users.noreply.github.com> Date: Fri, 13 Mar 2026 08:14:30 -0400 Subject: [PATCH] fix(signal): add groups config to Signal channel schema (#27199) Merged via squash. Prepared head SHA: 4ba4a39ddf10eaa3d6ad3f2975c088547f5373e5 Co-authored-by: unisone <32521398+unisone@users.noreply.github.com> Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com> Reviewed-by: @altaywtf --- CHANGELOG.md | 1 + docs/channels/signal.md | 4 ++ src/config/types.signal.ts | 9 +++ src/config/zod-schema.providers-core.ts | 11 ++++ src/config/zod-schema.signal-groups.test.ts | 65 +++++++++++++++++++++ 5 files changed, 90 insertions(+) create mode 100644 src/config/zod-schema.signal-groups.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c189efda09..afdf2c745e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai - Config/validation: accept documented `agents.list[].params` per-agent overrides in strict config validation so `openclaw config validate` no longer rejects runtime-supported `cacheRetention`, `temperature`, and `maxTokens` settings. (#41171) Thanks @atian8179. - Android/onboarding QR scan: switch setup QR scanning to Google Code Scanner so onboarding uses a more reliable scanner instead of the legacy embedded ZXing flow. (#45021) Thanks @obviyus. - Config/web fetch: restore runtime validation for documented `tools.web.fetch.readability` and `tools.web.fetch.firecrawl` settings so valid web fetch configs no longer fail with unrecognized-key errors. (#42583) Thanks @stim64045-spec. +- Signal/config validation: add `channels.signal.groups` schema support so per-group `requireMention`, `tools`, and `toolsBySender` overrides no longer get rejected during config validation. (#27199) Thanks @unisone. ## 2026.3.12 diff --git a/docs/channels/signal.md b/docs/channels/signal.md index b216af120ce..cfc050b6e75 100644 --- a/docs/channels/signal.md +++ b/docs/channels/signal.md @@ -195,6 +195,8 @@ Groups: - `channels.signal.groupPolicy = open | allowlist | disabled`. - `channels.signal.groupAllowFrom` controls who can trigger in groups when `allowlist` is set. +- `channels.signal.groups["" | "*"]` can override group behavior with `requireMention`, `tools`, and `toolsBySender`. +- Use `channels.signal.accounts..groups` for per-account overrides in multi-account setups. - Runtime note: if `channels.signal` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set). ## How it works (behavior) @@ -312,6 +314,8 @@ Provider options: - `channels.signal.allowFrom`: DM allowlist (E.164 or `uuid:`). `open` requires `"*"`. Signal has no usernames; use phone/UUID ids. - `channels.signal.groupPolicy`: `open | allowlist | disabled` (default: allowlist). - `channels.signal.groupAllowFrom`: group sender allowlist. +- `channels.signal.groups`: per-group overrides keyed by Signal group id (or `"*"`). Supported fields: `requireMention`, `tools`, `toolsBySender`. +- `channels.signal.accounts..groups`: per-account version of `channels.signal.groups` for multi-account setups. - `channels.signal.historyLimit`: max group messages to include as context (0 disables). - `channels.signal.dmHistoryLimit`: DM history limit in user turns. Per-user overrides: `channels.signal.dms[""].historyLimit`. - `channels.signal.textChunkLimit`: outbound chunk size (chars). diff --git a/src/config/types.signal.ts b/src/config/types.signal.ts index 1f3d5180b92..bd33a64cf51 100644 --- a/src/config/types.signal.ts +++ b/src/config/types.signal.ts @@ -1,8 +1,15 @@ import type { CommonChannelMessagingConfig } from "./types.channel-messaging-common.js"; +import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js"; export type SignalReactionNotificationMode = "off" | "own" | "all" | "allowlist"; export type SignalReactionLevel = "off" | "ack" | "minimal" | "extensive"; +export type SignalGroupConfig = { + requireMention?: boolean; + tools?: GroupToolPolicyConfig; + toolsBySender?: GroupToolPolicyBySenderConfig; +}; + export type SignalAccountConfig = CommonChannelMessagingConfig & { /** Optional explicit E.164 account for signal-cli. */ account?: string; @@ -24,6 +31,8 @@ export type SignalAccountConfig = CommonChannelMessagingConfig & { ignoreAttachments?: boolean; ignoreStories?: boolean; sendReadReceipts?: boolean; + /** Per-group overrides keyed by Signal group id (or "*"). */ + groups?: Record; /** Outbound text chunk size (chars). Default: 4000. */ textChunkLimit?: number; /** Reaction notification mode (off|own|all|allowlist). Default: own. */ diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 2b2fccee310..47f76614dd8 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -971,6 +971,16 @@ export const SlackConfigSchema = SlackAccountSchema.safeExtend({ validateSlackSigningSecretRequirements(value, ctx); }); +const SignalGroupEntrySchema = z + .object({ + requireMention: z.boolean().optional(), + tools: ToolPolicySchema, + toolsBySender: ToolPolicyBySenderSchema, + }) + .strict(); + +const SignalGroupsSchema = z.record(z.string(), SignalGroupEntrySchema.optional()).optional(); + export const SignalAccountSchemaBase = z .object({ name: z.string().optional(), @@ -995,6 +1005,7 @@ export const SignalAccountSchemaBase = z defaultTo: z.string().optional(), groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), groupPolicy: GroupPolicySchema.optional().default("allowlist"), + groups: SignalGroupsSchema, historyLimit: z.number().int().min(0).optional(), dmHistoryLimit: z.number().int().min(0).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(), diff --git a/src/config/zod-schema.signal-groups.test.ts b/src/config/zod-schema.signal-groups.test.ts new file mode 100644 index 00000000000..2dcd1ac0676 --- /dev/null +++ b/src/config/zod-schema.signal-groups.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; +import { validateConfigObject } from "./config.js"; + +describe("signal groups schema", () => { + it("accepts top-level Signal groups overrides", () => { + const res = validateConfigObject({ + channels: { + signal: { + groups: { + "*": { + requireMention: false, + }, + "+1234567890": { + requireMention: true, + }, + }, + }, + }, + }); + + expect(res.ok).toBe(true); + }); + + it("accepts per-account Signal groups overrides", () => { + const res = validateConfigObject({ + channels: { + signal: { + accounts: { + primary: { + groups: { + "*": { + requireMention: false, + }, + }, + }, + }, + }, + }, + }); + + expect(res.ok).toBe(true); + }); + + it("rejects unknown keys in Signal groups entries", () => { + const res = validateConfigObject({ + channels: { + signal: { + groups: { + "*": { + requireMention: false, + nope: true, + }, + }, + }, + }, + }); + + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.issues.some((issue) => issue.path.startsWith("channels.signal.groups"))).toBe( + true, + ); + } + }); +});