Files
deer-flow/backend/docs/IM_CHANNEL_CONNECTIONS.md
T
Nan Gao 0966131b31 fix(channels): require bound identity for user-owned IM messages (#3578)
* fix(channels): require bound identity for user-owned IM messages

* make format

* docs: document bound identity channel config

* refactor: reuse channel connection config

* refactor _requires_bound_identity()

* refactor from_app_config()

* make format

* fix: reject unbound channel chats before semaphore

* security enhancement

* make format

* fix: enforce bound-identity admission at command entry point

The bound-identity gate only ran for non-command messages in
_handle_message() and as a fallback inside _handle_chat(). Commands had
no equivalent boundary, so an unbound platform user could send /new and
reach _create_thread() directly, creating an unowned Gateway thread and
empty checkpoint. Info commands (/status, /models, /memory) likewise
leaked Gateway state to unbound users.

Add the same _requires_bound_identity() check at the top of
_handle_command(), rejecting via _reject_unbound_channel_message() before
any thread creation or Gateway query. The gate is a no-op in legacy
open-bot mode (require_bound_identity=False) and auth-disabled mode.
Provider-level binding flows (/connect, /start) are consumed by the
provider adapter before reaching the manager, so they are unaffected.

Tests:
- unbound auth-enabled /new is rejected before threads.create
- bound auth-enabled /new still creates the thread

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(channels): carry workspace fallback decision on inbound messages

* fix(channels): recheck bound identity by normalized workspace

* fix(channels): avoid duplicate bound identity checks

* fix(channels): preserve verified routing for bound identity rejects

* fix(channels): clarify bound identity upgrade failures

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-06-16 23:04:39 +08:00

5.0 KiB

IM Channel Connections

DeerFlow supports user-owned IM channel bindings for Telegram, Slack, Discord, Feishu/Lark, DingTalk, WeChat, and WeCom. The feature reuses the existing channels.* runtime configuration, so it works in local and private deployments with the same outbound transports already supported by DeerFlow.

No public IP, OAuth callback URL, or provider webhook is required in this implementation.

Configuration

Configure the actual IM bots under the existing channels block:

channels:
  telegram:
    enabled: true
    bot_token: $TELEGRAM_BOT_TOKEN

  slack:
    enabled: true
    bot_token: $SLACK_BOT_TOKEN
    app_token: $SLACK_APP_TOKEN

  discord:
    enabled: true
    bot_token: $DISCORD_BOT_TOKEN

  feishu:
    enabled: true
    app_id: $FEISHU_APP_ID
    app_secret: $FEISHU_APP_SECRET

  dingtalk:
    enabled: true
    client_id: $DINGTALK_CLIENT_ID
    client_secret: $DINGTALK_CLIENT_SECRET

  wechat:
    enabled: true
    bot_token: $WECHAT_BOT_TOKEN

  wecom:
    enabled: true
    bot_id: $WECOM_BOT_ID
    bot_secret: $WECOM_BOT_SECRET

Then enable user bindings in channel_connections:

channel_connections:
  enabled: true
  # Auth-enabled deployments require ordinary IM messages to come from a
  # connected DeerFlow user by default. Set this to false only for legacy
  # operator-owned/open-bot deployments that intentionally route unbound
  # platform users to platform-ID user buckets.
  require_bound_identity: true

  telegram:
    enabled: true
    bot_username: $TELEGRAM_BOT_USERNAME

  slack:
    enabled: true

  discord:
    enabled: true

  feishu:
    enabled: true

  dingtalk:
    enabled: true

  wechat:
    enabled: true

  wecom:
    enabled: true

channel_connections does not duplicate provider secrets. It only controls the browser-facing connect UI and stores per-user binding records. Telegram needs bot_username only so the frontend can open a deep link.

When channel_connections.enabled and require_bound_identity are true, auth-enabled deployments reject ordinary unbound IM messages before creating a DeerFlow thread or run. Users must connect the channel from DeerFlow Settings first. Auth-disabled local mode still routes channel messages to the auth-disabled default user, and legacy open-bot behavior can be restored explicitly with require_bound_identity: false.

Upgrade note: existing auth-enabled deployments that already have channel_connections.enabled: true will start rejecting ordinary unbound IM messages after this field is introduced because require_bound_identity defaults to true. Legacy operator-owned/open-bot deployments that intentionally allow unbound platform users to create DeerFlow runs should set require_bound_identity: false before upgrading and restart the service.

Connect Flow

Telegram:

  • The frontend creates a short one-time code.
  • The Connect button opens https://t.me/<bot_username>?start=<code>.
  • The existing Telegram long-polling worker receives /start <code> and binds that Telegram chat/user to the current DeerFlow user.

Slack:

  • The frontend creates a short one-time code.
  • The UI shows Send /connect <code> to the DeerFlow Slack bot.
  • The existing Slack Socket Mode worker receives the message and binds the Slack user/team to the current DeerFlow user.

Discord:

  • The frontend creates a short one-time code.
  • The UI shows Send /connect <code> to the DeerFlow Discord bot.
  • The existing Discord Gateway worker receives the message and binds the Discord user/guild to the current DeerFlow user.

Feishu/Lark, DingTalk, WeChat, and WeCom:

  • The frontend creates a short one-time code.
  • The UI shows Send /connect <code> to the DeerFlow <Provider> bot.
  • The already-running long-connection or polling worker receives the message and binds the platform user/workspace identity to the current DeerFlow user.

Codes use 128 bits of randomness, expire after 10 minutes, and are single-use.

Runtime Model

Connection records live in SQL tables under deerflow.persistence.channel_connections:

  • channel_connections: owner user, provider identity, workspace/guild/team, status, metadata.
  • channel_oauth_states: one-time connect codes and Telegram deep-link state.
  • channel_conversations: connection-scoped IM conversation to DeerFlow thread mapping.
  • channel_credentials: reserved for future provider-token flows, not used by the local/private binding flow.

Incoming messages that resolve to a connection carry connection_id, owner_user_id, and workspace_id. ChannelManager uses owner_user_id as the DeerFlow run user id and preserves the raw platform user id as channel_user_id.

Security Notes

  • Browser APIs remain authenticated and CSRF-protected.
  • Connect codes are 128-bit random, short-lived, and single-use.
  • Provider bot tokens remain in channels.* and are never returned to the browser.
  • Stored per-connection credentials are encrypted. If stored credential material cannot be decrypted, DeerFlow treats it as unavailable instead of using corrupt secrets.
  • This implementation does not add public provider callback or webhook routes.