Skip to content

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

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

Activity

  1. patrick-chinchill commented on May 28, 2026

    @patrick-chinchill
    CollaboratorAuthor

    Shipped in #86 (feat(slack): Socket Mode transport, merged d4632ff), part of the 0.4.27 wave.

    Implementation covers the issue's acceptance criteria:

    • ✅ SlackAdapterConfig(mode="socket", app_token="xapp-...") connects and receives events
    • ✅ initialize() / disconnect() lifecycle with outer reconnect loop (1s → 30s exp backoff, 250ms shutdown poll) layered on top of slack_sdk.socket_mode.aiohttp.SocketModeClient
    • ✅ All existing handler registrations fire under socket mode via the same dispatch paths (_route_socket_event → events_api / interactive / slash_commands handlers)
    • ✅ 24 regression tests in tests/test_slack_socket_mode.py covering config validation, lifecycle, reconnect, ContextVar isolation, forwarded-events receiver, ack protocol
    • ✅ Forwarded-events receiver in handle_webhook (x-slack-socket-token, hmac.compare_digest) for the serverless variant where a separate long-lived process runs the socket and POSTs to a stateless handler

    Two divergences from upstream documented in docs/UPSTREAM_SYNC.md:

    • Outer reconnect loop on top of SocketModeClient (defense against connect() raising before inner loop owns the lifecycle)
    • startSocketModeListener (Vercel-specific serverless cron glue) not ported, but the receiver side IS ported

    New optional extra: chat-sdk-python[slack-socket].


    Generated by Claude Code

  2. added a commit that references this issue on May 28, 2026
    c43ea05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions