Skip to content

feat(channels): Signal messenger support via signal-cli daemon - #268

Closed
ibhagwan wants to merge 1 commit into
NousResearch:mainfrom
ibhagwan:feat/signal-channel-support
Closed

feat(channels): Signal messenger support via signal-cli daemon#268
ibhagwan wants to merge 1 commit into
NousResearch:mainfrom
ibhagwan:feat/signal-channel-support

Conversation

@ibhagwan

@ibhagwan ibhagwan commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

Implements Signal platform adapter using signal-cli daemon HTTP/JSON-RPC interface.

Features:

  • Continuous SSE streaming with exponential backoff reconnection (2s→60s)
  • Typing indicators sent every 8 seconds while processing messages
  • Attachment upload/download with 100MB size validation
  • DM pairing integration (reuses existing pairing system)
  • Group message filtering (disabled by default for security)
  • Allowlist checking for E.164 numbers and UUIDs
  • Phone number redaction in all debug logs (+1555****4567)
  • Health check monitor for stale SSE connections
  • Enhanced session context: shows E164 + UUID for DMs, name + ID for groups

Signal support in send_message tool:

  • Added 'signal' platform to platform_map
  • Added attachments array parameter for sending files
  • Added _send_signal() function using JSON-RPC
  • Updated tool schema with Signal examples

Authorization fixes:

  • Added SIGNAL_ALLOWED_USERS and SIGNAL_GROUP_ALLOWED_USERS support
  • Fixed group allowlist checking in run.py _is_user_authorized()
  • Signal users in group allowlist now correctly authorized

Phone redaction:

  • Added _SIGNAL_PHONE_RE regex pattern for E164 numbers
  • Added redact function matching signal.py _redact_phone() logic
  • Applied to RedactingFormatter for all gateway logs

Session context improvements:

  • Added user_id_alt field for Signal UUIDs and chat_id_alt for group IDs
  • Updated description property to show E164 + UUID for DMs (e.g., 'DM with John Smith (+15551234567/uuid:abc...)')
  • Updated description to show group name + ID (e.g., 'group: Supreme Leader (ID: abc123)')
  • Updated build_source() in base adapter to accept new optional fields

Configuration (environment variables):

  • SIGNAL_HTTP_URL, SIGNAL_ACCOUNT (required)
  • SIGNAL_ALLOWED_USERS, SIGNAL_GROUP_ALLOWED_USERS
  • SIGNAL_DM_POLICY (pairing|allowlist|open)
  • SIGNAL_GROUP_POLICY (disabled|allowlist|open)
  • SIGNAL_DEBUG for verbose logging

Files:

  • gateway/platforms/signal.py: NEW - Complete adapter (934 lines)
  • gateway/run.py: SignalAdapter factory, group allowlist, SIGNAL_DEBUG
  • gateway/config.py: Platform.SIGNAL enum, env overrides
  • gateway/session.py: Added user_id_alt and chat_id_alt fields, enhanced description
  • gateway/platforms/base.py: Added user_id_alt and chat_id_alt params to build_source
  • tools/send_message_tool.py: Signal platform + attachments support
  • agent/redact.py: Signal phone number redaction
  • hermes_cli/config.py, setup.py: Signal config metadata
  • tests/gateway/test_signal.py: NEW - Unit tests
  • tests/agent/test_redact.py: Added Signal redaction tests

Implements Signal platform adapter using signal-cli daemon HTTP/JSON-RPC interface.

Features:
- Continuous SSE streaming with exponential backoff reconnection (2s→60s)
- Typing indicators sent every 8 seconds while processing messages
- Attachment upload/download with 100MB size validation
- DM pairing integration (reuses existing pairing system)
- Group message filtering (disabled by default for security)
- Allowlist checking for E.164 numbers and UUIDs
- Phone number redaction in all debug logs (+1555****4567)
- Health check monitor for stale SSE connections
- Enhanced session context: shows E164 + UUID for DMs, name + ID for groups

Signal support in send_message tool:
- Added 'signal' platform to platform_map
- Added attachments array parameter for sending files
- Added _send_signal() function using JSON-RPC
- Updated tool schema with Signal examples

Authorization fixes:
- Added SIGNAL_ALLOWED_USERS and SIGNAL_GROUP_ALLOWED_USERS support
- Fixed group allowlist checking in run.py _is_user_authorized()
- Signal users in group allowlist now correctly authorized

Phone redaction:
- Added _SIGNAL_PHONE_RE regex pattern for E164 numbers
- Added redact function matching signal.py _redact_phone() logic
- Applied to RedactingFormatter for all gateway logs

Session context improvements:
- Added user_id_alt field for Signal UUIDs and chat_id_alt for group IDs
- Updated description property to show E164 + UUID for DMs (e.g., 'DM with John Smith (+15551234567/uuid:abc...)')
- Updated description to show group name + ID (e.g., 'group: Supreme Leader (ID: abc123)')
- Updated build_source() in base adapter to accept new optional fields

Configuration (environment variables):
- SIGNAL_HTTP_URL, SIGNAL_ACCOUNT (required)
- SIGNAL_ALLOWED_USERS, SIGNAL_GROUP_ALLOWED_USERS
- SIGNAL_DM_POLICY (pairing|allowlist|open)
- SIGNAL_GROUP_POLICY (disabled|allowlist|open)
- SIGNAL_DEBUG for verbose logging

Files:
- gateway/platforms/signal.py: NEW - Complete adapter (934 lines)
- gateway/run.py: SignalAdapter factory, group allowlist, SIGNAL_DEBUG
- gateway/config.py: Platform.SIGNAL enum, env overrides
- gateway/session.py: Added user_id_alt and chat_id_alt fields, enhanced description
- gateway/platforms/base.py: Added user_id_alt and chat_id_alt params to build_source
- tools/send_message_tool.py: Signal platform + attachments support
- agent/redact.py: Signal phone number redaction
- hermes_cli/config.py, setup.py: Signal config metadata
- tests/gateway/test_signal.py: NEW - Unit tests
- tests/agent/test_redact.py: Added Signal redaction tests
@teknium1

teknium1 commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

Thanks for this substantial contribution, @ibhagwan! This is a really well-built adapter — solid architecture, good security defaults (pairing for DMs, groups disabled), SSE reconnection with exponential backoff, and clean integration with the existing platform patterns. Great work.

We're going to hold off on merging for now since we want to be able to test a new platform adapter end-to-end before landing it, but we've done a thorough review and want to share our findings so you can address them in the meantime:

Bugs

1. Timestamp read from wrong dict (Medium)
Around line 529 in signal.py:

timestamp = envelope.get("timestamp", 0)

Should be envelope_data.get("timestamp", 0) — all other message fields are read from envelope_data (the inner dict extracted from envelope["envelope"]). As-is, timestamps always fall back to datetime.now().

2. Inconsistent HTTP library in send_message_tool.py (Medium)
_send_signal() uses aiohttp, but the Signal adapter itself uses httpx throughout. This introduces an unnecessary extra dependency and inconsistency. Should use httpx to match.

3. Account phone number logged without redaction (Low-Medium)
A few log lines expose the raw account phone number:

  • Line ~422: f"[Signal] Config: http_url={self.http_url}, account={self.account}"
  • Line ~520: SSE URL includes raw account in the URL string
  • Line ~522: f"[Signal] SSE listener starting for account: {self.account}"
  • Line ~1014: f"[Signal] Account: {self.account}"

Most other log lines correctly use self._redact_phone() — these were just missed.

4. __import__ hack in send_message_tool.py (Low)

"id": f"send_{int(__import__('time').time() * 1000)}",

Should just import time at the top of the file.

5. Wildcard allowlist check (Low)

if self.allowed_users == ["*"]:

This only matches if the list is exactly ["*"]. If someone configures SIGNAL_ALLOWED_USERS=*,+1234567890, the wildcard won't be detected. Should be "*" in self.allowed_users instead.

6. Async tests require pytest-asyncio (Low)
3 tests in test_signal.py use @pytest.mark.asyncio but pytest-asyncio isn't in test dependencies, so they fail. Either add it to pyproject.toml test deps or rewrite those tests as synchronous.

What we verified (for when we revisit)

Full diff reviewed — all 17 files, 1856 additions checked. Delegated deep-dive analysis that:

  • Compared signal.py adapter against existing Telegram/WhatsApp/Discord adapters for pattern consistency
  • Verified all shared file changes (run.py, session.py, config.py, base.py, send_message_tool.py, redact.py) are backward-compatible
  • Checked security model: auth flow, allowlists, phone redaction, group policy defaults
  • Ran test suite — 1666 passed, 3 async Signal tests failed (pytest-asyncio missing, not code bugs)
  • Audited SessionSource additions (user_id_alt, chat_id_alt) — optional params with None defaults, safe
  • Confirmed extract_images regex update for file:// URLs is correct and doesn't break http/https
  • Verified get_connected_platforms() fix in config.py correctly handles Signal's non-token auth and also fixes WhatsApp case
  • Checked pairing system integration — reuses existing pairing flow, returns existing pending code instead of rate-limiting

What looked great

  • Follows BasePlatformAdapter correctly (connect/disconnect/send/send_typing/send_image/get_chat_info/build_source)
  • SSE streaming with exponential backoff (2s→60s) + health monitoring + _force_reconnect() via response close
  • Default security: DM policy=pairing, groups=disabled
  • Attachment handling: 100MB size validation, magic-byte extension detection, base class cache utilities
  • Phone redaction in agent/redact.py with _SIGNAL_PHONE_RE regex
  • Config/CLI integration complete: setup wizard, status display, env overrides all wired
  • Backward-compatible changes throughout shared files

Next steps

  • Set up signal-cli test instance for end-to-end verification
  • Contributor addresses the 6 issues above
  • Re-review and merge

teknium1 added a commit that referenced this pull request Mar 9, 2026
Complete Signal adapter using signal-cli daemon HTTP API.
Based on PR #268 by ibhagwan, rebuilt on current main with bug fixes.

Architecture:
- SSE streaming for inbound messages with exponential backoff (2s→60s)
- JSON-RPC 2.0 for outbound (send, typing, attachments, contacts)
- Health monitor detects stale SSE connections (120s threshold)
- Phone number redaction in all logs and global redact.py

Features:
- DM and group message support with separate access policies
- DM policies: pairing (default), allowlist, open
- Group policies: disabled (default), allowlist, open
- Attachment download with magic-byte type detection
- Typing indicators (8s refresh interval)
- 100MB attachment size limit, 8000 char message limit
- E.164 phone + UUID allowlist support

Integration:
- Platform.SIGNAL enum in gateway/config.py
- Signal in _is_user_authorized() allowlist maps (gateway/run.py)
- Adapter factory in _create_adapter() (gateway/run.py)
- user_id_alt/chat_id_alt fields in SessionSource for UUIDs
- send_message tool support via httpx JSON-RPC (not aiohttp)
- Interactive setup wizard in 'hermes gateway setup'
- Connectivity testing during setup (pings /api/v1/check)
- signal-cli detection and install guidance

Bug fixes from PR #268:
- Timestamp reads from envelope_data (not outer wrapper)
- Uses httpx consistently (not aiohttp in send_message tool)
- SIGNAL_DEBUG scoped to signal logger (not root)
- extract_images regex NOT modified (preserves group numbering)
- pairing.py NOT modified (no cross-platform side effects)
- No dual authorization (adapter defers to run.py for user auth)
- Wildcard uses set membership ('*' in set, not list equality)
- .zip default for PK magic bytes (not .docx)

No new Python dependencies — uses httpx (already core).
External requirement: signal-cli daemon (user-installed).

Tests: 30 new tests covering config, init, helpers, session source,
phone redaction, authorization, and send_message integration.

Co-authored-by: ibhagwan <ibhagwan@users.noreply.github.com>
@teknium1

teknium1 commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

Thanks for the excellent work @ibhagwan! Your Signal adapter implementation was the foundation for what we merged in commit 24f549a.

We rebuilt it on current main (the branch had diverged significantly in 4 shared files) and fixed the bugs identified during review:

  • Timestamp reads from envelope_data (not outer wrapper)
  • Uses httpx consistently (send_message tool was using aiohttp)
  • SIGNAL_DEBUG scoped to signal logger only (not root)
  • extract_images regex NOT modified (preserves group numbering for all platforms)
  • pairing.py NOT modified (no cross-platform side effects)
  • Authorization delegated to run.py (no dual-auth layer)
  • Wildcard uses set membership, .zip for PK magic bytes

Your architecture design — SSE streaming + JSON-RPC + health monitoring — was solid and we kept it intact. You're credited as co-author in the commit.

Closing this PR since the work is merged. Thank you! 🎉

@ibhagwan

ibhagwan commented Mar 9, 2026

Copy link
Copy Markdown
Contributor Author

Tysm, that's fantastic news @teknium1!

angelburgosrosado pushed a commit to angelburgosrosado/hermes-agent that referenced this pull request Apr 27, 2026
Complete Signal adapter using signal-cli daemon HTTP API.
Based on PR NousResearch#268 by ibhagwan, rebuilt on current main with bug fixes.

Architecture:
- SSE streaming for inbound messages with exponential backoff (2s→60s)
- JSON-RPC 2.0 for outbound (send, typing, attachments, contacts)
- Health monitor detects stale SSE connections (120s threshold)
- Phone number redaction in all logs and global redact.py

Features:
- DM and group message support with separate access policies
- DM policies: pairing (default), allowlist, open
- Group policies: disabled (default), allowlist, open
- Attachment download with magic-byte type detection
- Typing indicators (8s refresh interval)
- 100MB attachment size limit, 8000 char message limit
- E.164 phone + UUID allowlist support

Integration:
- Platform.SIGNAL enum in gateway/config.py
- Signal in _is_user_authorized() allowlist maps (gateway/run.py)
- Adapter factory in _create_adapter() (gateway/run.py)
- user_id_alt/chat_id_alt fields in SessionSource for UUIDs
- send_message tool support via httpx JSON-RPC (not aiohttp)
- Interactive setup wizard in 'hermes gateway setup'
- Connectivity testing during setup (pings /api/v1/check)
- signal-cli detection and install guidance

Bug fixes from PR NousResearch#268:
- Timestamp reads from envelope_data (not outer wrapper)
- Uses httpx consistently (not aiohttp in send_message tool)
- SIGNAL_DEBUG scoped to signal logger (not root)
- extract_images regex NOT modified (preserves group numbering)
- pairing.py NOT modified (no cross-platform side effects)
- No dual authorization (adapter defers to run.py for user auth)
- Wildcard uses set membership ('*' in set, not list equality)
- .zip default for PK magic bytes (not .docx)

No new Python dependencies — uses httpx (already core).
External requirement: signal-cli daemon (user-installed).

Tests: 30 new tests covering config, init, helpers, session source,
phone redaction, authorization, and send_message integration.

Co-authored-by: ibhagwan <ibhagwan@users.noreply.github.com>
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
Complete Signal adapter using signal-cli daemon HTTP API.
Based on PR NousResearch#268 by ibhagwan, rebuilt on current main with bug fixes.

Architecture:
- SSE streaming for inbound messages with exponential backoff (2s→60s)
- JSON-RPC 2.0 for outbound (send, typing, attachments, contacts)
- Health monitor detects stale SSE connections (120s threshold)
- Phone number redaction in all logs and global redact.py

Features:
- DM and group message support with separate access policies
- DM policies: pairing (default), allowlist, open
- Group policies: disabled (default), allowlist, open
- Attachment download with magic-byte type detection
- Typing indicators (8s refresh interval)
- 100MB attachment size limit, 8000 char message limit
- E.164 phone + UUID allowlist support

Integration:
- Platform.SIGNAL enum in gateway/config.py
- Signal in _is_user_authorized() allowlist maps (gateway/run.py)
- Adapter factory in _create_adapter() (gateway/run.py)
- user_id_alt/chat_id_alt fields in SessionSource for UUIDs
- send_message tool support via httpx JSON-RPC (not aiohttp)
- Interactive setup wizard in 'hermes gateway setup'
- Connectivity testing during setup (pings /api/v1/check)
- signal-cli detection and install guidance

Bug fixes from PR NousResearch#268:
- Timestamp reads from envelope_data (not outer wrapper)
- Uses httpx consistently (not aiohttp in send_message tool)
- SIGNAL_DEBUG scoped to signal logger (not root)
- extract_images regex NOT modified (preserves group numbering)
- pairing.py NOT modified (no cross-platform side effects)
- No dual authorization (adapter defers to run.py for user auth)
- Wildcard uses set membership ('*' in set, not list equality)
- .zip default for PK magic bytes (not .docx)

No new Python dependencies — uses httpx (already core).
External requirement: signal-cli daemon (user-installed).

Tests: 30 new tests covering config, init, helpers, session source,
phone redaction, authorization, and send_message integration.

Co-authored-by: ibhagwan <ibhagwan@users.noreply.github.com>
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
Complete Signal adapter using signal-cli daemon HTTP API.
Based on PR NousResearch#268 by ibhagwan, rebuilt on current main with bug fixes.

Architecture:
- SSE streaming for inbound messages with exponential backoff (2s→60s)
- JSON-RPC 2.0 for outbound (send, typing, attachments, contacts)
- Health monitor detects stale SSE connections (120s threshold)
- Phone number redaction in all logs and global redact.py

Features:
- DM and group message support with separate access policies
- DM policies: pairing (default), allowlist, open
- Group policies: disabled (default), allowlist, open
- Attachment download with magic-byte type detection
- Typing indicators (8s refresh interval)
- 100MB attachment size limit, 8000 char message limit
- E.164 phone + UUID allowlist support

Integration:
- Platform.SIGNAL enum in gateway/config.py
- Signal in _is_user_authorized() allowlist maps (gateway/run.py)
- Adapter factory in _create_adapter() (gateway/run.py)
- user_id_alt/chat_id_alt fields in SessionSource for UUIDs
- send_message tool support via httpx JSON-RPC (not aiohttp)
- Interactive setup wizard in 'hermes gateway setup'
- Connectivity testing during setup (pings /api/v1/check)
- signal-cli detection and install guidance

Bug fixes from PR NousResearch#268:
- Timestamp reads from envelope_data (not outer wrapper)
- Uses httpx consistently (not aiohttp in send_message tool)
- SIGNAL_DEBUG scoped to signal logger (not root)
- extract_images regex NOT modified (preserves group numbering)
- pairing.py NOT modified (no cross-platform side effects)
- No dual authorization (adapter defers to run.py for user auth)
- Wildcard uses set membership ('*' in set, not list equality)
- .zip default for PK magic bytes (not .docx)

No new Python dependencies — uses httpx (already core).
External requirement: signal-cli daemon (user-installed).

Tests: 30 new tests covering config, init, helpers, session source,
phone redaction, authorization, and send_message integration.

Co-authored-by: ibhagwan <ibhagwan@users.noreply.github.com>
ethenotethan added a commit to researchoors/hermes-agent that referenced this pull request Jul 27, 2026
…37)

Adds the `artifact.session.spawn` built-in intent handler. Instead of
executing anything inline, it creates a session through the standard
session runtime and returns the live `session_id` in the invoke result,
so the client can click through into real-time introspection of the run.

This is the gateway half of the intent-as-session flow the native client
(hermes-native NousResearch#268) is already wired to consume: on a `succeeded` result
carrying `session_id`, the success row becomes a link into that session.

Security invariants preserved:
- The initial task is composed server-side from the binding's
  author-declared `session_prompt` template plus the entity resolved out
  of the pinned content. The raw client `entity_ref` is a lookup key only,
  never spliced into the instruction (§0.2). An unresolved ref fails
  closed rather than spawning a session at an attacker-controlled string.
- Confirmation still runs in the invocation engine before the handler, so
  a destructive binding creates the session only after the user confirms.
- The lone dependency on the server module (in-process `_methods`
  dispatch) is isolated in `_spawn_session` so it's stubbable in tests and
  doesn't leak the server surface into the intent engine.

5 new tests (session id flows back, task seeded from stored entity not raw
ref, unresolved-entity fail-closed, artifact-scoped spawn, destructive
confirm gate). Docs updated with the new built-in and result shape.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.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.

2 participants