Skip to content

Slack adapter: add Socket Mode transport (webhook-only today) #68

@patrick-chinchill

Description

@patrick-chinchill

Missing transport — webhook-only deployment forces public ingress.

Current state

SlackAdapter in src/chat_sdk/adapters/slack/adapter.py dispatches exclusively via HTTP webhooks. disconnect() is a no-op (adapter.py:375) because there is no persistent connection to tear down. Consumers running behind a firewall, on a laptop, or in a CI tunnel need an external tunneling service (ngrok, Cloudflare Tunnel, etc.) to receive events — or they can't develop locally at all.

Slack offers Socket Mode as a first-class alternative: the bot opens an outbound WebSocket to wss://wss-primary.slack.com/link/ and receives the same events_api, interactive, and slash_commands envelopes that would otherwise be POSTed to a webhook. No public URL required.

slack_sdk.socket_mode.aiohttp.SocketModeClient ships in slack_sdk>=3.x (already our transitive dep via slack_sdk.web.async_client.AsyncWebClient). Wiring is mechanical: open the socket, parse each envelope, dispatch through the existing _handle_events_api_event / _handle_interactive_payload / _handle_slash_command paths, ack the envelope.

Why it matters

  • Local dev: today every contributor needs a tunnel. Socket Mode removes that friction.
  • Private bots: enterprise deployments that don't want to expose a public webhook URL.
  • Rapid prototyping: CI-run integration tests can't hit a webhook without infrastructure; they can hit a socket.

Design

Add a mode field to SlackAdapterConfig:

mode: Literal["webhook", "socket"] = "webhook"
app_level_token: str | None = None  # required when mode == "socket"

On initialize(), if mode == "socket":

  1. Construct SocketModeClient(app_token=app_level_token, web_client=AsyncWebClient(...)).
  2. Register an envelope handler that routes to the existing dispatch paths in adapter.py (the same ones handle_webhook uses after parsing).
  3. Call await client.connect().
  4. On disconnect(), await client.disconnect().

The existing handle_webhook path stays untouched; the socket path is a parallel entry point to the same dispatchers.

Signature verification (crypto.py) is webhook-only and should skip on the socket path — Slack already authenticated the socket via the app-level token.

Acceptance

  • SlackAdapterConfig(mode="socket", app_level_token=...) connects and receives events
  • initialize() / disconnect() lifecycle manages the socket cleanly (reconnect on drop, clean shutdown on signal)
  • All existing handler registrations (on_message, on_mention, on_action, on_slash_command, modal submissions) fire under socket mode with equivalent payloads
  • Regression test: mock SocketModeClient, feed an envelope per event type, assert dispatch
  • docs/ARCHITECTURE.md gets a short section on webhook-vs-socket deployment choice
  • Example in CONTRIBUTING.md for local dev with Socket Mode

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions