Skip to content

feat(channels): add Signal channel via signal-cli HTTP daemon#63

Merged
DonPrus merged 23 commits intonullclaw:mainfrom
ibhagwan:main
Feb 23, 2026
Merged

feat(channels): add Signal channel via signal-cli HTTP daemon#63
DonPrus merged 23 commits intonullclaw:mainfrom
ibhagwan:main

Conversation

@ibhagwan
Copy link
Contributor

Implement Signal messaging channel that connects to a signal-cli daemon running in HTTP mode (signal-cli --http ). Follows the vtable-based channel pattern established by existing channels.

Architecture:

  • SSE event stream for receiving messages at /api/v1/events
  • JSON-RPC for sending messages/typing indicators at /api/v1/rpc
  • curl subprocess for all HTTP (consistent with other channels)
  • Manual JSON construction via json_util helpers

Features:

  • Full envelope processing (data messages, group messages)
  • Allowlist-based access control for users and groups (empty allowlist = deny all, secure by default)
  • Story message filtering (ignore_stories, default true)
  • Attachment placeholder support (ignore_attachments, default true)
  • Wildcard (*) support in allowlists
  • UUID normalization (uuid: prefix) in allowlists
  • E.164 phone number validation
  • Trailing slash stripping from base URL
  • Message splitting at 4096 char limit
  • Best-effort typing indicators
  • Health check via /api/v1/check endpoint

Config (accounts wrapper pattern):
channels.signal.accounts..http_url (required)
channels.signal.accounts..account (required, E.164 phone)
channels.signal.accounts..allowed_users
channels.signal.accounts..allowed_groups
channels.signal.accounts..ignore_attachments (default: true)
channels.signal.accounts..ignore_stories (default: true)

Files:

  • src/channels/signal.zig: channel implementation + tests
  • src/config_types.zig: SignalConfig struct + ChannelsConfig field
  • src/config_parse.zig: JSON config parsing for Signal channel
  • src/channels/root.zig: sub-module registration

Closes #58

@ibhagwan
Copy link
Contributor Author

Let me know quarter you think @DonPrus, I’m thinking to change the config option to match telegram and option names in Openclaw, so instead of allowed_users -> allowed_from, etc.

@ibhagwan
Copy link
Contributor Author

Also, I’m not familiar with the codebase yet but if the model implemented this in the most efficient way ( currently) there may be some work to be done in the channel abstraction layer as there are way too many changes needed in files outside the channel folder.

For example, onboarding, status, the main loop - all should be automatically propagated after a channel is added (vs the current hardcoded approach).

@ibhagwan
Copy link
Contributor Author

Ty @DonPrus, fantastic changes

@ibhagwan
Copy link
Contributor Author

@DonPrus, f0c43cf

allowed_groups→group_allow_from

I’m not sure about this, if I am to judge this by the openclaw config conventions group_allow_from are user accounts (E164 numbers) which are allowed to talk within configured/permitted groups, the original code (perhaps changes now) uses allowed_groups to limit which groups the agent participates in (unless the group is in this list or the list is * the agent will ignore all messages in the group.

Judging by the chosen name I believe you want to match this behavior to openclaw, in which case we need to modify the code.

I’m thinking it’s still valuable to decide which groups the agent is listening to at all and on top to have group_allow_from which is the “allowlist” of groups only.

@ibhagwan
Copy link
Contributor Author

9812db5, FYI, the DebugAllocator was segfaulting due to a race condition on debug build when running the daemon.

ibhagwan and others added 11 commits February 22, 2026 22:55
Implement Signal messaging channel that connects to a signal-cli daemon
running in HTTP mode (signal-cli --http <port>). Follows the vtable-based
channel pattern established by existing channels.

Architecture:
- SSE event stream for receiving messages at /api/v1/events
- JSON-RPC for sending messages/typing indicators at /api/v1/rpc
- curl subprocess for all HTTP (consistent with other channels)
- Manual JSON construction via json_util helpers

Features:
- Full envelope processing (data messages, group messages)
- Allowlist-based access control for users and groups
  (empty allowlist = deny all, secure by default)
- Story message filtering (ignore_stories, default true)
- Attachment placeholder support (ignore_attachments, default true)
- Wildcard (*) support in allowlists
- UUID normalization (uuid: prefix) in allowlists
- E.164 phone number validation
- Trailing slash stripping from base URL
- Message splitting at 4096 char limit
- Best-effort typing indicators
- Health check via /api/v1/check endpoint

Config (accounts wrapper pattern):
  channels.signal.accounts.<id>.http_url     (required)
  channels.signal.accounts.<id>.account      (required, E.164 phone)
  channels.signal.accounts.<id>.allowed_users
  channels.signal.accounts.<id>.allowed_groups
  channels.signal.accounts.<id>.ignore_attachments (default: true)
  channels.signal.accounts.<id>.ignore_stories     (default: true)

Files:
- src/channels/signal.zig: channel implementation + tests
- src/config_types.zig: SignalConfig struct + ChannelsConfig field
- src/config_parse.zig: JSON config parsing for Signal channel
- src/channels/root.zig: sub-module registration
…dd agent routing

- Fix story message parsing panic in Signal (else-if guard on .object access)
- Add Signal to has_channel check in doctor.zig
- Use msg.deinit() instead of manual field-by-field free in Signal loop
- Unify config naming: allowed_users→allow_from, allowed_groups→group_allow_from,
  allowed_senders→allow_from, allowlist→allow_from/group_allow_from across all channels
- Add allow_from field to Line and OneBot configs
- Register Email, Line, QQ, OneBot, MaixCam in central ChannelsConfig with JSON parsing
- Add 5 new channels to status, onboard, doctor, and main channel listings
- Create agent_routing.zig: 7-tier OpenClaw-compatible binding resolution (29 tests)
- Fix WhatsApp to use case-insensitive phone matching (isAllowed vs isAllowedExact)
- Fix Matrix vtableSend to respect target room parameter
Expands native image parsing to Discord, WhatsApp, and Signal by extracting attachments and substituting them with [IMAGE:path] tag for multimodal agent workflows.
…l safety, access control

- Eliminate duplicate config structs in email/line/qq/onebot/maixcam (import from config_types.zig)
- Make LineConfig access_token/channel_secret required, add allow_from to QQConfig
- Add missing channels to status.zig, doctor.zig, daemon.zig, main.zig (channel doctor/add)
- Signal SSE parser: add JSON tag checks before .object dereference (crash fix)
- Signal: log errors instead of silently masking as empty poll
- Signal: accumulate multiline SSE data: events correctly
- Signal: group_allow_from now checks sender in groups (matching OpenClaw), not group ID
- Signal: session keys include group context to prevent cross-chat leaks
- Signal: change ignore_attachments/ignore_stories defaults to false (match references)
- Signal: implement SIGNAL_HTTP_URL/SIGNAL_ACCOUNT env overrides
- Enforce allow_from in OneBot (handleEvent) and Line (parseAndFilterEvents)
- Discord: add initFromConfig() to pass allow_from/mention_only/intents
- channel start: accept explicit channel name arg, default Telegram-first
- Implement granular group policies (allowlist, policy string) for Telegram, WhatsApp, and iMessage.
- Align Discord configuration keys (mention_only -> require_mention) with parent spec.
- Fix Signal group message validation bypass.
- Resolve config parsing test memory leaks and failing upstream stream chunks.
…iring

- Add ChannelManager (src/channel_manager.zig) — central orchestrator for all
  channel lifecycle (init, start, supervise, stop), replacing ~250 lines of
  hardcoded Telegram/Signal logic in daemon.zig
- JSON safety guards across 5 channel files (line, onebot, qq, whatsapp, signal)
  to prevent crashes on malformed webhook payloads
- Signal error propagation: SSE poll failures now bubble up instead of being
  silently swallowed; curlGetSSE gains --fail flag for HTTP error detection
- QQ allow_from: parse config + enforce allowlist in handleMessageCreate
- compatible.zig: preserve multimodal content_parts when merging system messages
- Gateway expansion: add /line and /lark webhook routes with signature
  verification and event parsing
- CLI expansion: `channel start` now supports discord, qq, onebot in addition
  to telegram and signal
- Agent routing: wire resolveRoute into Telegram/Signal polling loops;
  add agent_bindings field to Config
- Fix websocket.zig for Zig 0.15 Io.Reader API (readVec instead of read)
- Multi-account warning in config parser
- Extract serializeContentPart helper to avoid duplication
…stener parity

- Add agent routing resolution to all channel loops (Telegram, Discord, Signal, WhatsApp, etc.)
- Expand ChannelManager with multi-account support, listener wiring, per-channel routing
- Gateway: add channel-level routing, session management, health endpoints
- Config: parse accounts map, agent routing bindings, channel-specific configs
- Signal: group peer ID extraction, attachment fetch improvements
- All channels: consistent session key resolution through agent routing
The daemon command was crashing in Debug mode due to a thread-safety issue
when loading config inside the gateway thread. The fix involves two changes:

1. Always use smp_allocator instead of DebugAllocator in main(). DebugAllocator
   is not thread-safe and causes memory corruption when used across threads.

2. Pass the already-loaded config from daemon.run() to gateway.run() instead
   of having gateway.run() load its own config in a separate thread. This
   ensures config is loaded in the main thread where it's safe, and avoids
   redundant config loading.

The gateway.run() function now accepts an optional config_ptr parameter for
backward compatibility with direct invocations.

Fixes: Debug mode segfault at config_parse.zig:95 in parseJson
…us wiring

Phase 1 — structural fixes: media param on Channel.send vtable (all 16
channels), dedup channel instances between ChannelManager and channel_loop,
gateway-loop supervision, typing indicators and error replies for bus
channels, session eviction for bus channels.

Phase 2 — session config and routing: SessionConfig with DmScope and
IdentityLink types, buildSessionKeyWithScope (4 DM scope modes),
normalizeId for agent/account IDs, resolveLinkedPeerId, main_session_key
on ResolvedRoute, thread session keys.

Phase 3 — multi-account support: ChannelsConfig fields changed from
optional to slices for 7 channel types (telegram, discord, slack, signal,
qq, onebot, maixcam), Primary() helpers, getAllAccounts with alphabetical
sort, per-account Entry.account_id in ChannelManager, ~90 callers updated.

Phase 4 — webhook to bus wiring: event_bus on GatewayState, gateway.run
accepts optional bus, webhook endpoints (telegram, whatsapp, line, lark)
publish to bus in daemon mode, standalone in-request mode preserved.

30 files changed, +897/-279, 3155 tests passing.
…mat parity

- Fix routing bug: bindings with peer+guild now require ALL constraints
  to match (allConstraintsMatch), not just the tier-specific field
- Fix gateway.zig compile error on Linux (|*cfg| → |cfg|)
- Add camelCase + dash-format support for session config parsing
  (dmScope, idleMinutes, identityLinks, per-peer/per-channel-peer)
- Add map-format identity_links parsing ({"alice": ["telegram:111"]})
- 34 new agent_routing tests: route priority, dmScope, identity links,
  session key continuity, normalizeId, thread keys
- 18 new config tests: multi-account edge cases, session config parsing
  with both snake_case and camelCase formats
On Windows, std.net.Stream.Handle is ws2_32.SOCKET (opaque pointer),
not i32. Use std.net.Stream.Handle alias and INVALID_SOCKET sentinel
for cross-platform socket storage. Close via closesocket() on Windows.
@DonPrus
Copy link
Contributor

DonPrus commented Feb 23, 2026

@ibhagwan, thanks, I will double check every thing tomorrow and than I will be ready for merge

- Add channel_catalog.zig: centralized channel metadata, DRY doctor/onboard/status/main
- Fix Codex provider: always include `instructions` field (API requires it)
- Add 19 new tests: discord/qq bus routing, telegram/line/lark session keys,
  signal/qq/onebot/maixcam multi-account, single-account selection, channel manager wiring
- Refactor channel listing to use catalog loop instead of per-channel conditionals
… safety checks

- Refactor collectConfiguredChannels to comptime-iterate ChannelsConfig fields
- Add initFromConfig() to all channel types for uniform construction
- Add account_id to OutboundMessage and ChannelRegistry entries
- Add resolveRouteWithSession() for dm_scope-aware routing
- Add ListenerMode to channel_catalog, requiresRuntime(), findById()
- Deduplicate polling sources (same bot_token/http_url+account)
- Add setBus() to DiscordChannel, safety checks for root_val != .object
- Refactor config_parse.zig, simplify channel parsing
- Update README: 3,230+ tests, 17 channels
@ibhagwan
Copy link
Contributor Author

@DonPrus, when building debug mode with simply zig build I get a panic: unreachable code as soon as the first message arrives (consistently), I tracked down to sqllite.sig implStore, I’m still working on it unsure why this happens and if related to the signal code, this doesn’t happen with ReleaseSmall.

Per my understanding it’s because ReleaseSmall doesn’t panic for unreachable but the behavior will be undefined and can lead to some nasty voodoo bugs.

I don’t have any other channels configured, can you check with another channel (or signal if you have) and lmk if you get the panic too in a debug build?

I’ve been working on it for a while now suspecting the memory is being corrupted somewhere else causing the vtable call to panic.

@DonPrus DonPrus marked this pull request as ready for review February 23, 2026 15:19
@DonPrus
Copy link
Contributor

DonPrus commented Feb 23, 2026

@ibhagwan, I added some tests, but couldnt reproduce it now, could you try it again?

@ibhagwan
Copy link
Contributor Author

@ibhagwan, I added some tests, but couldnt reproduce it now, could you try it again?

Pulled latest changes, still crashing on the first signal message in debug mode, it's very consistent for me but the source of the memory corruption is ellusive, I'm still debugging it will keep you posted.

Have you tried with signal channel or other channels only? I'm suspecting something in the newly added signal channel code.

@ibhagwan
Copy link
Contributor Author

Found the issue @DonPrus, it was only happening on memory.backend=markdown due to workspace_dir ref being corrupted, testing and will commit the fix in a sec.

MarkdownMemory stored a borrowed slice to workspace_dir, but that slice
pointed to a temporary string allocated in initChannelRuntime. After
initChannelRuntime returned, the temporary was freed, but MarkdownMemory
continued using the dangling pointer. When that freed memory was reused
for the session key, workspace_dir appeared corrupted with session data.

The crash manifested as 'panic: reached unreachable code' when
implStore tried to use the corrupted path containing the session key.

Fix:
- MarkdownMemory.init now duplicates the workspace_dir string
- MarkdownMemory.deinit now frees the duplicated string

This ensures the workspace_dir remains valid for the lifetime of
the MarkdownMemory instance.
@ibhagwan
Copy link
Contributor Author

@DonPrus, 2539cdb

@DonPrus
Copy link
Contributor

DonPrus commented Feb 23, 2026

Looks good for me, lets wait checks and I will merge it.

@ibhagwan
Copy link
Contributor Author

Looks good for me, lets wait checks and I will merge it.

Amazign work tysm @DonPrus!

@DonPrus DonPrus merged commit 115b988 into nullclaw:main Feb 23, 2026
3 checks passed
devin-ai-integration bot referenced this pull request in mccoysc/nullclaw Mar 7, 2026
…gistry) (CodeRabbit #63)

Move the builtin_tools cleanup errdefer ABOVE the allocator.create call
so that if the registry allocation OOMs, all tool structs from allTools()
are properly freed instead of leaked.

Co-Authored-By: mccoy <hbzgzr@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Add Signal channel support via signal-cli --http JSON-RPC daemon

2 participants