molecule-core/docs/agent-runtime/social-channels.md
Hongming Wang 479a027e4b chore: open-source restructure — rename dirs, remove internal files, scrub secrets
Renames:
- platform/ → workspace-server/ (Go module path stays as "platform" for
  external dep compat — will update after plugin module republish)
- workspace-template/ → workspace/

Removed (moved to separate repos or deleted):
- PLAN.md — internal roadmap (move to private project board)
- HANDOFF.md, AGENTS.md — one-time internal session docs
- .claude/ — gitignored entirely (local agent config)
- infra/cloudflare-worker/ → Molecule-AI/molecule-tenant-proxy
- org-templates/molecule-dev/ → standalone template repo
- .mcp-eval/ → molecule-mcp-server repo
- test-results/ — ephemeral, gitignored

Security scrubbing:
- Cloudflare account/zone/KV IDs → placeholders
- Real EC2 IPs → <EC2_IP> in all docs
- CF token prefix, Neon project ID, Fly app names → redacted
- Langfuse dev credentials → parameterized
- Personal runner username/machine name → generic

Community files:
- CONTRIBUTING.md — build, test, branch conventions
- CODE_OF_CONDUCT.md — Contributor Covenant 2.1

All Dockerfiles, CI workflows, docker-compose, railway.toml, render.yaml,
README, CLAUDE.md updated for new directory names.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 00:24:44 -07:00

8.5 KiB

Social Channels

Connect AI agent workspaces to social platforms (Telegram, Slack, Discord) so users can talk to agents from anywhere. Built on a pluggable adapter pattern — one channel per workspace, multiple chats per channel.

Architecture

Telegram/Slack/Discord
    ↓ webhook or long-polling
Platform: ChannelAdapter.ParseWebhook() / StartPolling()
    ↓ allowlist check + Redis history lookup
ProxyA2ARequest(ctx, workspaceID, body, "channel:<type>", true)
    ↓ agent processes (existing A2A flow)
Reply text extracted from response
    ↓ ChannelAdapter.SendMessage()
Social chat ← reply (with typing indicator while waiting)

The channel:<type> caller prefix bypasses workspace hierarchy access checks (same pattern as webhook: and system:).

Adapters

Type Status Library
telegram Implemented go-telegram-bot-api/v5
slack Planned
discord Planned
whatsapp Planned

To add a new adapter: implement ChannelAdapter in workspace-server/internal/channels/, register in registry.go. Everything else (CRUD API, Canvas UI, MCP tools) works automatically.

Telegram Setup

1. Create the bot

  1. Talk to @BotFather on Telegram → /newbot
  2. Save the token (looks like 1234567890:ABCdefGHIjklMNOpqrSTUvwxYZ)

By default, Telegram bots in groups only see commands and @mentions. To let your bot see all group messages:

  • @BotFather → /mybots → select your bot → Bot SettingsGroup PrivacyTurn off
  • Then re-add the bot to the group (privacy changes don't apply to existing memberships)

The Discover endpoint reports can_read_all_group_messages and surfaces a warning if privacy is on.

3. Connect via Canvas

  1. Open the workspace in Canvas → Channels tab → + Connect
  2. Paste the bot token
  3. Add the bot to your group(s) and send a message, OR send /start to it in DMs
  4. Click Detect Chats → select the chats from the checklist
  5. (Optional) Add Allowed Users for an allowlist
  6. Connect Channel

4. Or connect via API

curl -X POST http://localhost:8080/workspaces/:id/channels \
  -H 'Content-Type: application/json' \
  -d '{
    "channel_type": "telegram",
    "config": {
      "bot_token": "1234567890:ABC...",
      "chat_id": "-100123, -100456"
    },
    "allowed_users": ["telegram_user_id_1"]
  }'

Multi-chat IDs

A single channel entry serves multiple chats — chat_id is comma-separated:

config:
  chat_id: "-100123, -100456, -100789"

The bot listens for messages from any of these chats and uses the same workspace agent for all of them. Outbound messages (e.g. agent-initiated notifications) are sent to all configured chats.

Allowlist

Per-channel allowlist of user IDs (or chat IDs for groups). Empty = allow everyone.

{ "allowed_users": ["123456789", "987654321"] }

When non-empty, messages from users not in the list are silently dropped (logged but no error).

Bot Commands

The bot registers these commands via setMyCommands, so they appear in Telegram's command autocomplete:

Command Behavior
/start Reply "Connected to Molecule AI agent". Skipped if forwarded to agent.
/help List all commands.
/reset Clear conversation history (Redis key).
/cancel Best-effort acknowledgment (no actual cancel plumbing yet).

Conversation History

Last 10 messages per chat stored in Redis at channel:telegram:{chat_id}:history with 24h TTL. Sent in A2A metadata.history so the agent has context. Same shape as Canvas chat history.

Webhook Mode

Currently, channels run in long-polling mode by default. Webhook mode is implemented but requires:

  1. Public URL pointing to POST /webhooks/telegram on your platform
  2. Manual setWebhook call to Telegram with the URL + a secret_token
  3. Storing the same secret_token in channel_config.webhook_secret

The platform verifies the X-Telegram-Bot-Api-Secret-Token header on every webhook request.

Channels can be defined in org.yaml so they're auto-created when the org is deployed. Config values support ${VAR} expansion from .env files.

workspaces:
  - name: PM
    files_dir: pm
    channels:
      - type: telegram
        config:
          bot_token: ${TELEGRAM_BOT_TOKEN}
          chat_id: ${TELEGRAM_CHAT_ID}
        allowed_users: []
        enabled: true

The vars are resolved from (in order): pm/.env → org root .env → platform process env. If any required var is unresolved, the channel is skipped with a clear log message and the skip reason is surfaced in the import response (channels_skipped field).

The platform calls adapter.ValidateConfig() upfront so unknown channel types or invalid configs fail fast. Insert is idempotent (ON CONFLICT DO UPDATE) so re-importing the same org refreshes the channel config.

Hot Reload

CRUD operations on /workspaces/:id/channels (POST, PATCH, DELETE) trigger manager.Reload(). Active polling goroutines are diffed against the desired DB state — new channels start, removed/disabled ones stop. No platform restart required.

The Discover endpoint also pauses any pollers using the same bot token to avoid Telegram's "only one getUpdates per bot" 409 Conflict, then resumes them after.

Database

Migration 016_workspace_channels.sql:

CREATE TABLE workspace_channels (
    id              UUID PRIMARY KEY,
    workspace_id    UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
    channel_type    TEXT NOT NULL,          -- 'telegram', 'slack', etc.
    channel_config  JSONB NOT NULL,         -- adapter-specific (bot_token, chat_id, ...)
    enabled         BOOLEAN DEFAULT true,
    allowed_users   JSONB DEFAULT '[]',
    last_message_at TIMESTAMPTZ,
    message_count   INTEGER DEFAULT 0,
    ...
);

API Endpoints

Method Path Description
GET /channels/adapters List available platforms
POST /channels/discover Detect chats for a bot token
GET /workspaces/:id/channels List channels (bot_token masked)
POST /workspaces/:id/channels Create channel (validates config)
PATCH /workspaces/:id/channels/:channelId Update config/enabled/allowlist
DELETE /workspaces/:id/channels/:channelId Remove channel
POST /workspaces/:id/channels/:channelId/send Outbound message
POST /workspaces/:id/channels/:channelId/test Send test message
POST /webhooks/:type Incoming webhook receiver

MCP Tools

list_channel_adapters()                                              // list platforms
list_channels({ workspace_id })                                      // list channels
add_channel({ workspace_id, channel_type, config, allowed_users })   // hot reload
update_channel({ workspace_id, channel_id, config, enabled, allowed_users })
remove_channel({ workspace_id, channel_id })
send_channel_message({ workspace_id, channel_id, text })             // outbound
test_channel({ workspace_id, channel_id })                           // test connection

Telegram-Specific Implementation Notes

  • Bot instance cache (sync.RWMutex) avoids getMe API call on every send.
  • 4096-char message splitting at paragraph/line/word boundaries (Telegram's hard limit).
  • sendChatAction("typing") goroutine re-sends every 4s during agent calls so the user sees "typing..." for the entire wait.
  • Markdown → plain text fallback if the formatting fails (ParseMode = "Markdown" then retry without).
  • my_chat_member event handling — when the bot is added to a chat, it auto-greets with the chat ID (no /start required).
  • Typed error handling: 401 invalidates the bot cache; 403 returns a forbidden error; 429 honors RetryAfter.
  • Token format validation via regex (^\d+:[A-Za-z0-9_-]{30,}$) before any API call.

Files

File Purpose
workspace-server/internal/channels/adapter.go ChannelAdapter interface
workspace-server/internal/channels/registry.go Adapter registry
workspace-server/internal/channels/telegram.go Telegram implementation
workspace-server/internal/channels/manager.go Orchestrator with hot reload
workspace-server/internal/handlers/channels.go REST API + webhook
workspace-server/migrations/016_workspace_channels.sql DB schema
canvas/src/components/tabs/ChannelsTab.tsx Canvas UI
mcp-server/src/index.ts 7 MCP tools