Skip to content

feat(gateway): multi-workspace Socket Mode support for Slack adapter - #6686

Closed
jordanhubbard wants to merge 1 commit into
NousResearch:mainfrom
jordanhubbard:feature/multi-workspace-socket-mode
Closed

jordanhubbard wants to merge 1 commit into
NousResearch:mainfrom
jordanhubbard:feature/multi-workspace-socket-mode

Conversation

@jordanhubbard

@jordanhubbard jordanhubbard commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Transforms the Slack adapter from single-workspace Socket Mode to multi-workspace Socket Mode, enabling an agent to receive and respond to events from multiple Slack workspaces simultaneously via independent Socket Mode connections.

Also incorporates robustness improvements from PR #3928 (graceful token degradation + persisted channel routing).

Currently, Hermes supports sending to multiple workspaces via comma-separated `SLACK_BOT_TOKEN` values, but can only receive events from a single workspace (the one with `SLACK_APP_TOKEN`). This PR makes it N:N — receive from N, send to N.

Related Issue

N/A — feature request from production multi-workspace deployments.

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)

Changes Made

`gateway/platforms/slack.py`

Multi-workspace Socket Mode (core feature):

  • New `_load_accounts()` reads `~/.hermes/slack_accounts.json` for multi-workspace configs, each with its own `bot_token` + `app_token` pair for independent Socket Mode connections. Falls back to `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` env vars for full backward compatibility.
  • Extracted `_register_app_handlers(app)` to register Bolt event/command/action handlers identically on each per-account `AsyncApp` instance (message events, app_mention, reactions, slash commands, approval buttons, assistant thread lifecycle).
  • Refactored `connect()` to iterate accounts, creating an independent `AsyncApp` + `AsyncSocketModeHandler` per account with separate token locks.
  • Refactored `disconnect()` to close all handlers and release all token locks.
  • `self._app` retained as primary app reference for backward compatibility.

Robustness improvements (from PR #3928):

  • Graceful token degradation: `auth_test()` failures warn and skip the account instead of aborting `connect()`; only fails if zero accounts authenticate. Handles revoked/expired tokens in multi-token setups.
  • Persisted channel→team routing via `~/.hermes/slack_channel_teams.json`: `_record_channel_team()` replaces all direct `_channel_team[]` writes and persists on every new mapping. Loaded at `connect()` time so routing survives gateway restarts (eliminates post-restart `channel_not_found` on secondary workspaces).
  • Metadata-aware client selection: `_get_client(chat_id, metadata)` uses explicit `team_id` from metadata before falling back to learned routing. All send/upload methods (`send`, `send_typing`, `_upload_file`, `send_image`, `send_video`, `send_document`) pass `metadata` through.

State changes:

  • `self._handler` → `self._handlers` (Dict[name, handler])
  • `self._socket_mode_task` → `self._socket_mode_tasks` (Dict[name, task])
  • `self._token_lock_identity` → `self._token_lock_identities` (list)
  • New: `self._apps` (Dict[name, AsyncApp])

`docs/slack_accounts.example.json` (new)

Example configuration file showing the multi-workspace JSON format.

`docs/plans/2026-04-08-multi-workspace-socket-mode.md` (new)

Design document covering architecture, configuration, backward compatibility, and edge cases.

How to Test

Single workspace (backward compat — no changes needed)

  1. Set `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` in `~/.hermes/.env`
  2. Start gateway: `hermes gateway`
  3. Verify agent connects and responds to messages normally

Multi-workspace

  1. Create `~/.hermes/slack_accounts.json`:
    ```json
    [
    {
    "name": "workspace-one",
    "bot_token": "xoxb-...",
    "app_token": "xapp-...",
    "signing_secret": "..."
    },
    {
    "name": "workspace-two",
    "bot_token": "xoxb-...",
    "app_token": "xapp-...",
    "signing_secret": "..."
    }
    ]
    ```
  2. Start gateway: `hermes gateway`
  3. Verify logs show both workspaces connecting
  4. Send messages in both workspaces — agent should receive and respond in both

Robustness

  • Revoke one token while others are valid → gateway continues with the valid ones
  • Restart gateway after receiving messages → sends to previously-seen channels still route to the correct workspace

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this feature (no unrelated commits)
  • I've run `pytest tests/ -q` and all tests pass
  • I've tested on my platform: Ubuntu 24.04, macOS (Apple Silicon)
  • Tested in production with 2 agents × 2 workspaces

Documentation & Housekeeping

  • Added example config (`docs/slack_accounts.example.json`)
  • Added design document (`docs/plans/`)
  • Cross-platform: uses `pathlib.Path` for config resolution, no OS-specific code

Relation to other PRs

This PR supersedes the approach in PR #3928 (single Socket Mode + fixes) by solving the root cause: true N:N Socket Mode connections. It also incorporates the robustness patterns from #3928 (graceful token degradation, persisted routing, metadata-aware client selection).

Transforms the Slack adapter from single-workspace to N:N Socket Mode,
incorporating robustness improvements from PR #3928.

## Multi-workspace Socket Mode (core feature)
- New _load_accounts() reads ~/.hermes/slack_accounts.json; each entry
  gets its own AsyncApp + AsyncSocketModeHandler (true parallel receive)
- Falls back to SLACK_BOT_TOKEN + SLACK_APP_TOKEN env vars (backward compat)
- Extracted _register_app_handlers(app) to register all Bolt handlers
  identically on each per-account app instance
- Per-app-token scoped locks; self._app retained as primary fallback
- State: _handlers Dict[name→handler], _socket_mode_tasks Dict[name→task],
  _apps Dict[name→AsyncApp], _token_lock_identities list

## Robustness (from PR #3928)
- Graceful token degradation: auth_test() failures warn and skip the
  account rather than aborting connect(); fails only if zero accounts work
- Persisted channel→team routing via slack_channel_teams.json: routing
  survives gateway restarts, eliminating post-restart channel_not_found
- _record_channel_team() replaces direct _channel_team[] writes and
  persists on every new mapping
- Metadata-aware client selection: _get_client(chat_id, metadata) resolves
  team_id from metadata before falling back to learned routing; all
  send/upload methods pass metadata through

## New tests
- test_connect_skips_stale_saved_tokens: revoked token doesn't block startup
- test_send_uses_persisted_channel_team_route: routing loaded from disk
- test_records_channel_route_on_inbound_message: inbound writes to disk

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jordanhubbard

Copy link
Copy Markdown
Contributor Author

Superseded by #13837, which rebases this feature onto current upstream/main (conflict-free) and consolidates it with Signal reactions/editing, agent budget reset after compression, and a Ctrl+D readline fix. Closing this PR in favor of the consolidated one.

@alt-glitch alt-glitch added type/feature New feature or request P2 Medium — degraded but workaround exists platform/slack Slack app adapter comp/gateway Gateway runner, session dispatch, delivery labels Apr 22, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

Superseded by #13837 which rebases this work and adds Signal reactions, budget reset, and Ctrl+D fix.

1 similar comment
@alt-glitch

Copy link
Copy Markdown
Contributor

Superseded by #13837 which rebases this work and adds Signal reactions, budget reset, and Ctrl+D fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/slack Slack app adapter type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants