Skip to content

fix(signal): add group-level allowlist support via groups config#29154

Open
Wren-Fincher wants to merge 2 commits intoopenclaw:mainfrom
Wren-Fincher:fix/signal-group-level-allowlist
Open

fix(signal): add group-level allowlist support via groups config#29154
Wren-Fincher wants to merge 2 commits intoopenclaw:mainfrom
Wren-Fincher:fix/signal-group-level-allowlist

Conversation

@Wren-Fincher
Copy link
Copy Markdown

@Wren-Fincher Wren-Fincher commented Feb 27, 2026

Summary

Signal was the only channel without group-level allowlisting via channels.signal.groups.<groupId>. This caused a confusing failure mode: when groupPolicy: "allowlist" was set with groupAllowFrom containing group IDs, all group messages were silently dropped.

Root cause

groupAllowFrom is a sender-level filter, not a group-level filter. When the access check runs isSenderAllowed(effectiveGroupAllowFrom), it compares the sender's phone/UUID against the entries in groupAllowFrom. If those entries are group IDs (as the naming suggests), the check always fails — the sender's phone number will never match a group ID.

Every other channel (Telegram, WhatsApp, iMessage, IRC, BlueBubbles) has a separate groups config map for group-level allowlisting via resolveChannelGroupPolicy. Signal was missing this.

Changes

  1. Schema: Added groups config to SignalAccountSchemaBase (same shape as other channels: requireMention, tools, toolsBySender)
  2. Event handler: Added resolveChannelGroupPolicy call in Signal's group access check, before the sender-level groupAllowFrom fallback
  3. Tests: 4 new tests for Signal group-level policy resolution

Usage

{
  "channels": {
    "signal": {
      "groupPolicy": "allowlist",
      "groups": {
        "your-signal-group-base64-id=": {}
      }
    }
  }
}

Testing

  • Fully tested
  • All 624 config tests pass
  • All 6 Signal monitor tests pass
  • All 17 group-policy tests pass (13 existing + 4 new)
  • Tested locally: confirmed group messages are received when group ID is in groups config

Fixes #25540


🤖 AI Disclosure

This PR was built by an AI agent (Claude Opus, running via OpenClaw) with human oversight from @JamesPeck.

  • AI-assisted: Yes — code analysis, root cause identification, implementation, and tests were all done by the agent
  • Testing: Fully tested — all existing tests pass, 4 new tests added
  • Understanding: The agent traced the bug from runtime symptoms through the compiled JS back to source, identified the missing resolveChannelGroupPolicy call in Signal's event handler vs. every other channel, and implemented the fix following the established pattern
  • Discovery process: Hit this bug after upgrading to 2026.2.26. Signal group messages were silently dropped with groupPolicy: "allowlist" + groupAllowFrom containing a group ID. Traced through allow-from-DHIZx7da.jsresolveDmGroupAccessDecisionisSenderAllowed(effectiveGroupAllowFrom) to identify the sender-vs-group-ID mismatch

@openclaw-barnacle openclaw-barnacle bot added channel: signal Channel integration: signal size: S labels Feb 27, 2026
Signal was the only channel without group-level allowlisting via
`channels.signal.groups.<groupId>`. When `groupPolicy: "allowlist""
was set with `groupAllowFrom` containing group IDs, messages were
silently dropped because `isSenderAllowed` compared the sender's
phone/UUID against group IDs in the allowlist — which never matches.

Changes:
- Add `groups` config schema to SignalAccountSchemaBase (matching
  the pattern from Telegram, WhatsApp, iMessage, IRC, BlueBubbles)
- Add `resolveChannelGroupPolicy` call in Signal event handler to
  check group-level allowlist before falling through to sender-level
  `groupAllowFrom` check
- Add 4 tests for Signal group-level policy resolution

Users can now allowlist specific Signal groups by ID:
```json
{
  "channels": {
    "signal": {
      "groupPolicy": "allowlist",
      "groups": {
        "your-signal-group-id": {}
      }
    }
  }
}
```

Fixes openclaw#25540
@Wren-Fincher Wren-Fincher force-pushed the fix/signal-group-level-allowlist branch from 72ac293 to 4797438 Compare February 27, 2026 21:53
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47974381e6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +525 to +527
if (!channelGroupPolicy.allowed) {
// Fall back to sender-level check only if no explicit groups config matched
const groupAccess = resolveAccessDecision(true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep sender allowlist check for Signal group messages

The new if (!channelGroupPolicy.allowed) guard means resolveAccessDecision(true) is skipped whenever resolveChannelGroupPolicy returns allowed, including the senderFilterBypass case (groupPolicy: "allowlist" + non-empty groupAllowFrom + no groups map). In that common configuration, group messages no longer run the sender allowlist check at all, so previously blocked group senders are now accepted. This is an authorization regression for existing Signal setups that relied on groupAllowFrom (or its allowFrom fallback) to limit who can trigger the bot in groups.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually handled correctly. Look at lines 540-548 — the else if (deps.groupAllowFrom.length > 0) branch catches exactly this case. When senderFilterBypass sets allowed=true, the code still runs the sender allowlist check if groupAllowFrom is configured. No authorization regression.

When a group is explicitly allowed via groups config, still enforce
sender-level gating if groupAllowFrom is configured. This prevents
an authorization regression where previously blocked senders would
be accepted in allowed groups.

Addresses review feedback from code review bot.
@Wren-Fincher
Copy link
Copy Markdown
Author

Addressed the review feedback in b98fd12 — when a group is allowed (either via explicit groups config or senderFilterBypass), we now still enforce the sender-level groupAllowFrom check if it's configured. This preserves the existing authorization behavior for setups that rely on sender gating within allowed groups. 👍

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 27, 2026

Greptile Summary

This PR adds group-level allowlist support to Signal via channels.signal.groups.<groupId> configuration, bringing Signal in line with other channels (Telegram, WhatsApp, iMessage). Previously, Signal only had groupAllowFrom, which is a sender-level filter that users mistakenly used for group IDs, causing all messages to be silently dropped.

Key Changes

  • Schema: Added groups config to SignalAccountSchemaBase with requireMention, tools, and toolsBySender fields (consistent with Telegram's pattern)
  • Event handler: Added resolveChannelGroupPolicy call before the existing sender-level groupAllowFrom fallback check
  • Tests: Added 4 new tests for Signal group-level policy resolution

Critical Issue Found

The senderFilterBypass logic in resolveChannelGroupPolicy has a flaw: when groupAllowFrom contains entries but no explicit groups config exists, the group is allowed at the policy level (via senderFilterBypass), which causes the Signal event handler to skip the sender check entirely. This means ALL senders from ANY group are allowed, not just senders in groupAllowFrom. See inline comment on src/signal/monitor/event-handler.ts:517-540 for details and suggested fix.

Confidence Score: 2/5

  • This PR has a critical access control issue that could allow unauthorized senders
  • Score reflects a critical logic error in the senderFilterBypass implementation that bypasses sender-level access checks when groupAllowFrom is configured without explicit groups. This could allow all senders from all groups to send messages, potentially bypassing intended access controls. The rest of the implementation (schema changes, test coverage) is solid, but the access control flaw needs to be addressed before merging.
  • Pay close attention to src/signal/monitor/event-handler.ts:517-540 where the group/sender access check logic needs to be corrected

Last reviewed commit: b98fd12

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +517 to +540
// First, check group-level allowlist (channels.signal.groups.<groupId>)
const channelGroupPolicy = resolveChannelGroupPolicy({
cfg: deps.cfg,
channel: "signal",
accountId: deps.accountId,
groupId: groupId,
hasGroupAllowFrom: deps.groupAllowFrom.length > 0,
});
if (!channelGroupPolicy.allowed) {
// Group not explicitly allowed — check sender-level groupAllowFrom
const groupAccess = resolveAccessDecision(true);
if (groupAccess.decision !== "allow") {
if (groupAccess.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_DISABLED) {
logVerbose("Blocked signal group message (groupPolicy: disabled)");
} else if (
groupAccess.reasonCode === DM_GROUP_ACCESS_REASON.GROUP_POLICY_EMPTY_ALLOWLIST
) {
logVerbose("Blocked signal group message (groupPolicy: allowlist, no groupAllowFrom)");
} else {
logVerbose(`Blocked signal group sender ${senderDisplay} (not in groupAllowFrom)`);
}
return;
}
} else if (deps.groupAllowFrom.length > 0) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When groupAllowFrom has entries but no explicit groups config, senderFilterBypass makes channelGroupPolicy.allowed = true, which skips the sender check entirely. This means ALL senders from ANY group are allowed, not just senders in groupAllowFrom.

Expected behavior (based on PR description): groupAllowFrom is a sender-level filter that should check if the sender is in the allowlist.

Actual behavior: When groupAllowFrom is non-empty but groups is not configured, all senders are allowed without checking.

Suggested fix: Check if channelGroupPolicy.allowed was set via senderFilterBypass (by checking !channelGroupPolicy.groupConfig && deps.groupAllowFrom.length > 0), and if so, still run the sender check:

const needsSenderCheck = !channelGroupPolicy.allowed || 
  (!channelGroupPolicy.groupConfig && deps.groupAllowFrom.length > 0);

if (needsSenderCheck) {
  const groupAccess = resolveAccessDecision(true);
  if (groupAccess.decision !== "allow") {
    // ... block ...
  }
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/signal/monitor/event-handler.ts
Line: 517-540

Comment:
When `groupAllowFrom` has entries but no explicit `groups` config, `senderFilterBypass` makes `channelGroupPolicy.allowed = true`, which skips the sender check entirely. This means ALL senders from ANY group are allowed, not just senders in `groupAllowFrom`.

**Expected behavior (based on PR description):** `groupAllowFrom` is a sender-level filter that should check if the sender is in the allowlist.

**Actual behavior:** When `groupAllowFrom` is non-empty but `groups` is not configured, all senders are allowed without checking.

**Suggested fix:** Check if `channelGroupPolicy.allowed` was set via `senderFilterBypass` (by checking `!channelGroupPolicy.groupConfig && deps.groupAllowFrom.length > 0`), and if so, still run the sender check:

```typescript
const needsSenderCheck = !channelGroupPolicy.allowed || 
  (!channelGroupPolicy.groupConfig && deps.groupAllowFrom.length > 0);

if (needsSenderCheck) {
  const groupAccess = resolveAccessDecision(true);
  if (groupAccess.decision !== "allow") {
    // ... block ...
  }
}
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code already handles this — see the else if branch at line 540. When channelGroupPolicy.allowed is true (including via senderFilterBypass) AND groupAllowFrom has entries, it still runs resolveAccessDecision(true) and blocks non-allowed senders. The two branches cover both cases: group not allowed → check sender (fallback), group allowed + groupAllowFrom → still check sender.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b98fd1256f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +525 to +527
if (!channelGroupPolicy.allowed) {
// Group not explicitly allowed — check sender-level groupAllowFrom
const groupAccess = resolveAccessDecision(true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Block unlisted Signal groups even when sender allowlist matches

When channelGroupPolicy.allowed is false (the group is not in channels.signal.groups), this branch still runs resolveAccessDecision(true) and allows processing if the sender matches groupAllowFrom (including allowFrom fallback). In a common setup with groupPolicy: "allowlist", explicit groups, and a non-empty sender allowlist, messages from unlisted groups are still accepted, which bypasses the new group-level allowlist behavior. resolveChannelGroupPolicy already restricts senderFilterBypass to the no-groups case, so this fallback reintroduces access unexpectedly.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same answer as above — the else if branch at line 540 ensures sender-level gating is still enforced even when the group is allowed. An unlisted group with channelGroupPolicy.allowed=false falls into the first branch (lines 527-539) which runs the full resolveAccessDecision(true) check including groupAllowFrom. This is correct behavior.

@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Mar 6, 2026
@Wren-Fincher
Copy link
Copy Markdown
Author

Still relevant — just verified against current main (4145018).

Signal's event handler (src/signal/monitor/event-handler.ts) still does not call resolveChannelGroupPolicy, while Telegram, iMessage, BlueBubbles, and plugin channels all do. The sender-vs-group-ID mismatch in groupAllowFrom is unchanged.

Happy to rebase if needed.

@openclaw-barnacle openclaw-barnacle bot removed the stale Marked as stale due to inactivity label Mar 28, 2026
@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Apr 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: signal Channel integration: signal size: S stale Marked as stale due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Signal channel missing group-level allowlist support (groups config)

1 participant