Channel Configuration

Configuration for all 45 channel adapters, per-channel behavior overrides, and sidecar channel integration.


[channels]

All in-process channel adapters are configured under [channels.<name>]. Each channel is Option<T> -- omitting the section disables the adapter entirely. Including the section header (even empty) enables it with default values.

Universal channel fields: Every channel adapter supports the following common fields in addition to its own specific fields:

FieldTypeDefaultDescription
default_agentstring or nullnullAgent name to route messages to by default.
account_idstring or nullnullUnique identifier for this bot instance. Used for multi-bot routing via [[bindings]] match rules.
overridesobject(defaults)Per-channel behavior overrides. See Channel Overrides.

Webhook security

Channels that receive inbound traffic over HTTP enforce a uniform contract:

OutcomeStatus
Required header missing or malformed (X-Line-Signature, Authorization: HMAC …, DingTalk timestamp/sign)400 Bad Request
Header present but signature mismatched, replayed, or expired401 Unauthorized

HMAC verification covers the raw wire bytes (not bytes round-tripped through serde_json::Value), so the signing key set in the platform portal must match the env var configured below. Comparison runs in constant time.

Channels with a per-secret env var (Teams security_token_env) fall back to "skip verification + log a startup warning" when the env var is unset — this preserves backwards compatibility but should never be left unset in production.

Outbound webhook URLs (callback_url on [channels.webhook]) are run through an SSRF guard at adapter construction: any private (10/8, 172.16/12, 192.168/16), CGN (100.64/10), loopback (127/8, ::1), link-local (169.254/16, fe80::/10), unique-local (fc00::/7), multicast, or cloud-metadata (169.254.169.254, metadata.google.internal, metadata.azure.com) target — including IPv6-bracket forms like [::], [::ffff:127.0.0.1], NAT64 [64:ff9b::7f00:1], and trailing-dot FQDNs — is rejected with a startup error.

Upgrading from earlier versions

If you are upgrading from a release before this contract was introduced:

  1. Operating Teams? Copy the outgoing-webhook security token (base64) from the Teams portal and export it as TEAMS_SECURITY_TOKEN (or whatever you set security_token_env to). Without it, inbound webhooks still work but are unauthenticated — a warning is logged once at startup.
  2. Operating LINE / DingTalk? No new env var to set, but probes/health-checks that hit the webhook path without the platform's signature header now return 400/401. Real platform traffic always carries a signature, so genuine inbound is unaffected; only direct probes break.
  3. Using [channels.webhook] with callback_url pointing at a private address? The most common case is a local dev setup with callback_url = "http://127.0.0.1/...". The adapter now refuses to start with that. Switch to a public tunnel (e.g. ngrok, cloudflared) or omit callback_url entirely if you don't need outbound delivery.

If you operate none of the channels above, no action is required — your existing config keeps working unchanged.

Telegram

Telegram is provided by the Python sidecar adapter (librefang.sidecar.adapters.telegram). The in-process [channels.telegram] config block was removed in #5241. Declare Telegram as a [[sidecar_channels]] entry instead:

[[sidecar_channels]]
name = "telegram"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.telegram"]
channel_type = "telegram"
[sidecar_channels.env]
TELEGRAM_BOT_TOKEN = "..."
# ALLOWED_USERS = "111,@alice"

See [[sidecar_channels]] below for all fields. The sidecar preserves the same runtime semantics: TELEGRAM_BOT_TOKEN env var, optional ALLOWED_USERS allowlist, channel_type = "telegram", and RBAC mapping via channel_role_mapping.telegram.

Rust alternative (since #5831). A first-party Rust port of the Telegram sidecar ships at sdk/rust/librefang-sidecar-telegram/ — pick it when you do not want a Python runtime on the host, when per-respawn startup latency matters, or when the ~3 MB stripped binary saves enough image weight to justify it. The wire shape and access-control semantics are byte-equivalent with the Python adapter; the supervisor cannot tell them apart. Since #5936 the binary ships inside the platform release tarballs, so librefang update installs it to ~/.librefang/bin/librefang-sidecar-telegram — no manual cargo build required. Configure with the bare command = "librefang-sidecar-telegram" and the daemon auto-resolves the bundled binary (it checks its own executable directory, then ~/.librefang/bin/, then PATH); the same TELEGRAM_BOT_TOKEN / ALLOWED_USERS env applies. A development build that has not been installed via librefang update still uses an explicit command = "/abs/path/to/target/release/librefang-sidecar-telegram". See Rust Telegram sidecar adapter for the full reference.

Discord (sidecar)

Discord migrated to an out-of-process sidecar in v2026.5; declare it as a [[sidecar_channels]] block. The sidecar preserves runtime semantics of the removed in-process adapter: DISCORD_BOT_TOKEN env var, gateway intents, allowed_guilds / allowed_users filters, and RBAC mapping via channel_role_mapping.discord.

[[sidecar_channels]]
name = "discord"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.discord"]
channel_type = "discord"

[sidecar_channels.env]
DISCORD_BOT_TOKEN = "..."
# DISCORD_ALLOWED_GUILDS = "123,456"
# DISCORD_ALLOWED_USERS = "789"
# DISCORD_INTENTS = "37376"
# DISCORD_IGNORE_BOTS = "true"
# DISCORD_MENTION_PATTERNS = "hey bot,!ask"
# DISCORD_ACCOUNT_ID = "guild-42"

See [[sidecar_channels]] below for all supervisor fields (restart, backoff, ready timeout, message buffer).

Slack (sidecar)

Slack migrated to an out-of-process sidecar in v2026.5; declare it as a [[sidecar_channels]] block. The sidecar preserves the runtime semantics of the removed in-process adapter: Socket Mode WebSocket + Web API, SLACK_ALLOWED_CHANNELS filter (DMs exempt), thread-reply context, Block Kit interactive callbacks, eyes / check reactions, and multi-bot routing via SLACK_ACCOUNT_ID.

[[sidecar_channels]]
name = "slack"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.slack"]
channel_type = "slack"

[sidecar_channels.env]
SLACK_APP_TOKEN = "xapp-..."
SLACK_BOT_TOKEN = "xoxb-..."
# SLACK_ALLOWED_CHANNELS = "C0123,C0456"
# SLACK_UNFURL_LINKS = "false"
# SLACK_FORCE_FLAT_REPLIES = "false"
# SLACK_REACTIONS = "true"
# SLACK_ACCOUNT_ID = "workspace-prod"

See [[sidecar_channels]] below for all supervisor fields (restart, backoff, ready timeout, message buffer).

WhatsApp (sidecar)

WhatsApp migrated from the in-process Rust adapter to a Python sidecar (librefang.sidecar.adapters.whatsapp, stdlib-only). The in-process [channels.whatsapp] block is no longer recognised. The sidecar covers both modes — Meta Cloud API and the legacy Web/QR (Baileys) gateway.

Cloud API mode (recommended — production):

[[sidecar_channels]]
name = "whatsapp"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.whatsapp"]
channel_type = "whatsapp"
[sidecar_channels.env]
WHATSAPP_PHONE_NUMBER_ID = "123456789012345"
WHATSAPP_WEBHOOK_PORT = "8460"
# WHATSAPP_WEBHOOK_PATH = "/webhook"
# WHATSAPP_ALLOWED_USERS = "+15551111,+15552222"
# WHATSAPP_ACCOUNT_ID = "production"
# WHATSAPP_DM_POLICY = "respond"          # respond / allowed_only / ignore
# WHATSAPP_GROUP_POLICY = "all"           # all / mention_only / commands_only / ignore

WHATSAPP_ACCESS_TOKEN (Cloud API access token), WHATSAPP_VERIFY_TOKEN (for Meta's webhook subscription handshake), and WHATSAPP_APP_SECRET (HMAC-SHA256 key for inbound X-Hub-Signature-256) belong in ~/.librefang/secrets.env. The sidecar runs its own webhook server on WHATSAPP_WEBHOOK_PORT (default 8460) — Meta's Webhook subscription URL becomes https://<host>:<WHATSAPP_WEBHOOK_PORT><WHATSAPP_WEBHOOK_PATH>.

The in-process Rust adapter's start() (whatsapp.rs:454-483) was a TODO stub that logged "webhook ready" but never parsed inbound activities — the sidecar implements the real handler, so Cloud API inbound now works end-to-end without a separate forwarder.

Web/QR mode (legacy — personal accounts, no Meta dev account needed):

[[sidecar_channels]]
name = "whatsapp"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.whatsapp"]
channel_type = "whatsapp"
[sidecar_channels.env]
WHATSAPP_GATEWAY_URL = "http://localhost:3009"
# WHATSAPP_ALLOWED_USERS = "+15551111,+15552222"
# WHATSAPP_ACCOUNT_ID = "personal"

The Baileys gateway (@librefang/whatsapp-gateway, Node.js, runs at WHATSAPP_GATEWAY_URL) must be started separately — the kernel no longer embeds or auto-spawns it. Inbound messages flow directly from the gateway to LibreFang's REST API (POST /api/agents/{id}/message), bypassing the sidecar; outbound replies route through the sidecar to {gateway_url}/message/send.

Signal (sidecar)

Signal migrated from an in-process Rust adapter to an out-of-process Python sidecar (librefang.sidecar.adapters.signal). The sidecar talks to a separately-run signal-cli-rest-api container. An existing [channels.signal] block is no longer recognised — re-declare as [[sidecar_channels]]:

[[sidecar_channels]]
name = "signal"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.signal"]
channel_type = "signal"
[sidecar_channels.env]
SIGNAL_API_URL = "https://signal-cli.example.com"
SIGNAL_NUMBER  = "+15555550100"
# SIGNAL_ALLOWED_USERS = "+15555550199,+15555550200"  # optional
# SIGNAL_ACCOUNT_ID = "prod-bot"                       # optional
# SIGNAL_POLL_INTERVAL_SECS = "2"                      # optional
# SIGNAL_ALLOW_LOCAL = "1"                             # opt-in SSRF bypass for localhost

The sidecar enforces the same SSRF guard as the Rust adapter — SIGNAL_API_URL must resolve to a public address unless SIGNAL_ALLOW_LOCAL=1 is set. Optional SIGNAL_API_KEY belongs in ~/.librefang/secrets.env.

Matrix (sidecar)

Matrix migrated from an in-process Rust adapter to an out-of-process Python sidecar (librefang.sidecar.adapters.matrix). The sidecar polls /sync on the configured homeserver via the Client-Server API. An existing [channels.matrix] block is no longer recognised — re-declare as [[sidecar_channels]]:

[[sidecar_channels]]
name = "matrix"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.matrix"]
channel_type = "matrix"
[sidecar_channels.env]
MATRIX_HOMESERVER_URL = "https://matrix.org"
MATRIX_USER_ID = "@librefang:matrix.org"
# MATRIX_ALLOWED_ROOMS = "!abc:matrix.org,!def:matrix.org"  # optional
# MATRIX_ACCOUNT_ID = "prod-bot"                             # optional
# MATRIX_MAX_UPLOAD_BYTES = "52428800"                       # optional, default 50 MiB

Secret via ~/.librefang/secrets.env: MATRIX_ACCESS_TOKEN (the bot's access token from the homeserver — same shape Element uses).

Email (IMAP + SMTP) (sidecar)

Email is provided by the Python sidecar adapter (librefang.sidecar.adapters.email). The in-process [channels.email] config block was removed in the sidecar migration. Declare email as a [[sidecar_channels]] entry instead:

[[sidecar_channels]]
name = "email"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.email"]
channel_type = "email"
[sidecar_channels.env]
EMAIL_IMAP_HOST = "imap.gmail.com"
EMAIL_SMTP_HOST = "smtp.gmail.com"
EMAIL_USERNAME = "bot@example.com"
# EMAIL_IMAP_PORT = "993"
# EMAIL_SMTP_PORT = "587"
# EMAIL_POLL_INTERVAL_SECS = "30"      # IMAP polling interval
# EMAIL_FOLDERS = "INBOX"              # comma-separated
# EMAIL_ALLOWED_SENDERS = ""           # exact addr or @domain, csv
# EMAIL_ACCOUNT_ID = "prod"            # multi-bot routing key
# EMAIL_TLS_ROOT_CA_PATH = ""          # additional CA bundle (PEM)
# EMAIL_TLS_ACCEPT_INVALID_CERTS = ""  # last-resort dev escape hatch

EMAIL_PASSWORD (and the optional per-protocol overrides EMAIL_IMAP_PASSWORD / EMAIL_SMTP_PASSWORD) go into ~/.librefang/secrets.env — the dashboard's Channels page writes them there when you fill the form. The sidecar polls IMAP with UID SEARCH UNSEEN UNKEYWORD Librefang-Quarantine (falls back to plain UNSEEN when the server rejects the keyword), retries LOGINAUTHENTICATE PLAIN for servers like Lark that only advertise SASL PLAIN, and threads outbound replies via In-Reply-To + References from a per-sender cache.

Microsoft Teams (sidecar)

Teams migrated from the in-process Rust adapter to an out-of-process Python sidecar (librefang.sidecar.adapters.teams). The in-process [channels.teams] config block is no longer recognised — re-declare as [[sidecar_channels]]:

[[sidecar_channels]]
name = "teams"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.teams"]
channel_type = "teams"
[sidecar_channels.env]
TEAMS_APP_ID = "00000000-0000-0000-0000-000000000000"
TEAMS_WEBHOOK_PORT = "8459"
# TEAMS_WEBHOOK_PATH = "/webhook"
# TEAMS_ALLOWED_TENANTS = "tenant-a,tenant-b"     # empty = all
# TEAMS_ACCOUNT_ID = "production"

TEAMS_APP_PASSWORD (Bot Framework client secret) and TEAMS_SECURITY_TOKEN (base64 outgoing-webhook token from the Teams portal — gates HMAC-SHA256 verification of every inbound) go into ~/.librefang/secrets.env. The sidecar runs its own webhook server on TEAMS_WEBHOOK_PORT (default 8459) — the public URL operators register in the Azure Bot Channel configuration changes from https://<host>/channels/teams/webhook to https://<host>:<TEAMS_WEBHOOK_PORT><TEAMS_WEBHOOK_PATH>.

Mattermost (sidecar)

Mattermost migrated from an in-process Rust adapter to an out-of-process Python sidecar (librefang.sidecar.adapters.mattermost). An existing [channels.mattermost] block is no longer recognised — re-declare as [[sidecar_channels]]:

[[sidecar_channels]]
name = "mattermost"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.mattermost"]
channel_type = "mattermost"
[sidecar_channels.env]
MATTERMOST_SERVER_URL = "https://mattermost.example.com"
# MATTERMOST_ALLOWED_CHANNELS = "ch-id-1,ch-id-2"   # optional
# MATTERMOST_ACCOUNT_ID = "team-prod"               # optional

MATTERMOST_TOKEN belongs in ~/.librefang/secrets.env.

Google Chat

Google Chat is provided by the Python sidecar adapter (librefang.sidecar.adapters.google_chat). The in-process [channels.google_chat] config block was removed in the sidecar migration. Declare Google Chat as a [[sidecar_channels]] entry instead:

[[sidecar_channels]]
name = "google_chat"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.google_chat"]
channel_type = "google_chat"
[sidecar_channels.env]
GOOGLE_CHAT_WEBHOOK_PORT = "8090"
# GOOGLE_CHAT_SPACE_IDS = "spaces/AAAA,spaces/BBBB"   # optional, empty = all spaces
# GOOGLE_CHAT_ACCOUNT_ID = "prod"                     # optional, multi-bot routing
# GOOGLE_CHAT_API_BASE = "https://chat.googleapis.com/v1"  # testing override

GOOGLE_CHAT_SERVICE_ACCOUNT_JSON (the full JSON blob — not a path) belongs in ~/.librefang/secrets.env. The Google Cloud Console Bot configuration messaging endpoint should point at https://<host>:<GOOGLE_CHAT_WEBHOOK_PORT>/webhook.

Twitch

Twitch is provided by the Python sidecar adapter (librefang.sidecar.adapters.twitch). The in-process [channels.twitch] config block was removed in the sidecar migration. Declare Twitch as a [[sidecar_channels]] entry instead:

[[sidecar_channels]]
name = "twitch"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.twitch"]
channel_type = "twitch"
[sidecar_channels.env]
TWITCH_NICK = "librefang-bot"
TWITCH_CHANNELS = "channel1,channel2"      # comma-separated, no '#'
# TWITCH_ACCOUNT_ID = "prod"                # optional, multi-bot routing
# TWITCH_RATE_LIMIT_MSGS = "20"             # 20/30s for unmodded; 100 if bot is mod
# TWITCH_RATE_LIMIT_SECS = "30"

TWITCH_OAUTH_TOKEN belongs in ~/.librefang/secrets.env. The sidecar defaults to TLS on irc.chat.twitch.tv:6697; plaintext is reachable only via TWITCH_PLAINTEXT=1 for local mock listeners.

Rocket.Chat

Rocket.Chat is provided by the Python sidecar adapter (librefang.sidecar.adapters.rocketchat). The in-process [channels.rocketchat] block was removed in the sidecar migration. Declare Rocket.Chat as a [[sidecar_channels]] entry instead:

[[sidecar_channels]]
name = "rocketchat"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.rocketchat"]
channel_type = "rocketchat"
[sidecar_channels.env]
ROCKETCHAT_SERVER_URL = "https://chat.example.com"
ROCKETCHAT_USER_ID = "abc123"
# ROCKETCHAT_CHANNELS = "GENERAL,room2"        # optional; empty = all joined
# ROCKETCHAT_ACCOUNT_ID = "prod"                # optional, multi-bot routing

ROCKETCHAT_TOKEN belongs in ~/.librefang/secrets.env — the dashboard's Channels page writes it there when you fill the Rocket.Chat form.

Zulip

Zulip is provided by the Python sidecar adapter (librefang.sidecar.adapters.zulip). The in-process [channels.zulip] config block was removed in the sidecar migration. Declare Zulip as a [[sidecar_channels]] entry instead — see the configuration reference's "Zulip" section above for the full block.

LINE

LINE is provided by the Python sidecar adapter (librefang.sidecar.adapters.line). The in-process [channels.line] config block was removed in the sidecar migration. Declare LINE as a [[sidecar_channels]] entry instead — the sidecar runs its own HTTP webhook server (no longer mounted on the LibreFang API port), so the URL you register at the LINE Developers Console is now https://<your-sidecar-host>:<LINE_WEBHOOK_PORT><LINE_WEBHOOK_PATH>:

[[sidecar_channels]]
name = "line"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.line"]
channel_type = "line"
[sidecar_channels.env]
LINE_WEBHOOK_PORT = "9090"
# LINE_WEBHOOK_PATH = "/webhook"   # override the default path
# LINE_ACCOUNT_ID = "production"   # optional, multi-bot routing key

Set both LINE_CHANNEL_SECRET (used to verify the inbound X-Line-Signature HMAC-SHA256 over the raw request body) and LINE_CHANNEL_ACCESS_TOKEN (Bearer auth on outbound POST /v2/bot/message/push) in ~/.librefang/secrets.env.

Reddit

Reddit is provided by the Python sidecar adapter (librefang.sidecar.adapters.reddit). The in-process [channels.reddit] config block was removed in the sidecar migration. Declare Reddit as a [[sidecar_channels]] entry instead:

[[sidecar_channels]]
name = "reddit"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.reddit"]
channel_type = "reddit"
[sidecar_channels.env]
REDDIT_CLIENT_ID = "abc123"
REDDIT_USERNAME = "librefang-bot"
REDDIT_SUBREDDITS = "rust,programming"   # comma-separated
# REDDIT_ACCOUNT_ID = "prod"              # optional, multi-bot routing key
# REDDIT_USER_AGENT = "myorg-bot/1.0 (by /u/me)"   # override default UA

REDDIT_CLIENT_SECRET and REDDIT_PASSWORD belong in ~/.librefang/secrets.env — the dashboard's Channels page writes them there when you fill the Reddit form.

Mastodon

Mastodon is provided by the Python sidecar adapter (librefang.sidecar.adapters.mastodon). The in-process [channels.mastodon] config block was removed in the sidecar migration. Declare Mastodon as a [[sidecar_channels]] entry instead:

[[sidecar_channels]]
name = "mastodon"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.mastodon"]
channel_type = "mastodon"
[sidecar_channels.env]
MASTODON_INSTANCE_URL = "https://mastodon.social"
# MASTODON_VISIBILITY = "unlisted"          # public | unlisted | private | direct
# MASTODON_MAX_MESSAGE_LEN = "500"          # raise for higher-limit instances
# MASTODON_ACCOUNT_ID = "prod"              # optional, multi-bot routing key

MASTODON_ACCESS_TOKEN (OAuth bearer) goes into ~/.librefang/secrets.env — the dashboard's Channels page writes it there when you fill the Mastodon form. The sidecar inherits SSE-with-polling-fallback semantics from the removed in-process adapter.

Bluesky

Bluesky is provided by the Python sidecar adapter (librefang.sidecar.adapters.bluesky). The in-process [channels.bluesky] config block was removed in the sidecar migration. Declare Bluesky as a [[sidecar_channels]] entry instead:

[[sidecar_channels]]
name = "bluesky"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.bluesky"]
channel_type = "bluesky"
[sidecar_channels.env]
BLUESKY_IDENTIFIER = "mybot.bsky.social"
# BLUESKY_SERVICE_URL = "https://bsky.social"   # custom PDS
# BLUESKY_ACCOUNT_ID = "prod"                   # optional, multi-bot routing key

BLUESKY_APP_PASSWORD goes into ~/.librefang/secrets.env — the dashboard's Channels page writes it there when you fill the Bluesky form. The sidecar polls app.bsky.notification.listNotifications every 5 s and threads outbound replies back to the originating notification via an in-memory LRU cache (capacity 200).

Feishu / Lark (sidecar)

Feishu / Lark migrated to an out-of-process sidecar; declare it as a [[sidecar_channels]] block instead of [channels.feishu].

[[sidecar_channels]]
name = "feishu"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.feishu"]
channel_type = "feishu"
[sidecar_channels.env]
FEISHU_APP_ID = "cli_abc123"
# FEISHU_REGION = "cn"                       # cn (default) or intl (Lark)
# FEISHU_RECEIVE_MODE = "websocket"          # websocket (default) or webhook
# FEISHU_WEBHOOK_PORT = "8453"               # webhook mode only
# FEISHU_VERIFICATION_TOKEN = "..."          # webhook mode only
# FEISHU_ENCRYPT_KEY = "..."                 # webhook mode only (AES-256-CBC payloads)
# FEISHU_ACCOUNT_ID = "prod"                 # optional, multi-bot routing key

FEISHU_APP_SECRET goes into ~/.librefang/secrets.env — the dashboard's Channels page writes it there when you fill the Feishu form. The default websocket mode opens a long-lived connection to Feishu's event gateway (no public IP required). webhook mode listens on FEISHU_WEBHOOK_PORT and supports encrypted payloads (AES-256-CBC + PKCS7) when FEISHU_ENCRYPT_KEY is configured.

Nextcloud Talk

Nextcloud Talk is provided by the Python sidecar adapter (librefang.sidecar.adapters.nextcloud). The in-process [channels.nextcloud] block was removed in the sidecar migration. Declare Nextcloud as a [[sidecar_channels]] entry instead:

[[sidecar_channels]]
name = "nextcloud"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.nextcloud"]
channel_type = "nextcloud"
[sidecar_channels.env]
NEXTCLOUD_SERVER_URL = "https://cloud.example.com"
# NEXTCLOUD_ROOMS = "abc123,def456"           # optional; empty = all joined
# NEXTCLOUD_ACCOUNT_ID = "prod"               # optional, multi-bot routing

NEXTCLOUD_TOKEN belongs in ~/.librefang/secrets.env — the dashboard's Channels page writes it there when you fill the Nextcloud form. Outbound replies thread via Talk's replyTo parameter (improvement over the former Rust adapter, which never sent replyTo).

Webex (sidecar)

Webex migrated to an out-of-process sidecar in v2026.5; declare it as a [[sidecar_channels]] block instead of [channels.webex].

[[sidecar_channels]]
name = "webex"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.webex"]
channel_type = "webex"

[sidecar_channels.env]
# WEBEX_ALLOWED_ROOMS = "Y2lz...A,Y2lz...B"  # optional, empty = all rooms
# WEBEX_ACCOUNT_ID = "org-prod"              # optional, multi-bot routing

WEBEX_BOT_TOKEN belongs in ~/.librefang/secrets.env — the dashboard's Channels page writes it there when you fill the Webex form. Outbound replies thread via Webex's parentId parameter (improvement over the former Rust adapter, which never sent parentId).

DingTalk (sidecar)

DingTalk is provided by the Python sidecar adapter (librefang.sidecar.adapters.dingtalk), stream mode only. The in-process [channels.dingtalk] config block was removed in the sidecar migration. Declare DingTalk as a [[sidecar_channels]] entry instead. The legacy in-process webhook mode (HTTP callback + HMAC-SHA256) is NOT ported — operators who relied on webhook mode must re-create the robot with stream subscription enabled in the DingTalk admin console.

[[sidecar_channels]]
name = "dingtalk"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.dingtalk"]
channel_type = "dingtalk"
[sidecar_channels.env]
DINGTALK_APP_KEY = "dingxxx..."
# DINGTALK_ACCOUNT_ID = "prod-bot"     # optional, multi-bot routing key
# DINGTALK_ROBOT_CODE = ""             # optional, defaults to app_key

DINGTALK_APP_SECRET goes into ~/.librefang/secrets.env — the dashboard's Channels page writes it there when you fill the DingTalk form. The sidecar opens a WebSocket to the DingTalk Stream Gateway via two-step endpoint discovery (POST /v1.0/gateway/connections/open), ACKs every CALLBACK frame inline, and posts replies to the per-message sessionWebhook URL delivered with each inbound event.

ntfy

ntfy is provided by the Python sidecar adapter (librefang.sidecar.adapters.ntfy). The in-process [channels.ntfy] config block was removed in the sidecar migration. Declare ntfy as a [[sidecar_channels]] entry instead:

[[sidecar_channels]]
name = "ntfy"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.ntfy"]
channel_type = "ntfy"
[sidecar_channels.env]
NTFY_SERVER_URL = "https://ntfy.sh"
NTFY_TOPIC = "my-agent-topic"
# NTFY_ACCOUNT_ID = "prod"                  # optional, multi-bot routing

NTFY_TOKEN (auth, optional for public topics) belongs in ~/.librefang/secrets.env.

Gotify

Gotify is provided by the Python sidecar adapter (librefang.sidecar.adapters.gotify). The in-process [channels.gotify] config block was removed in the sidecar migration. Declare Gotify as a [[sidecar_channels]] entry instead:

[[sidecar_channels]]
name = "gotify"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.gotify"]
channel_type = "gotify"
[sidecar_channels.env]
GOTIFY_SERVER_URL = "https://gotify.example.com"
# GOTIFY_ACCOUNT_ID = "prod"    # optional, multi-bot routing key

GOTIFY_APP_TOKEN (used for outbound publish) and GOTIFY_CLIENT_TOKEN (used for inbound WebSocket subscribe) go into ~/.librefang/secrets.env — the dashboard's Channels page writes them there automatically when you fill the Gotify form.

Webhook (sidecar)

Webhook migrated from the in-process Rust adapter to a Python sidecar (librefang.sidecar.adapters.webhook, stdlib-only). The in-process [channels.webhook] block is no longer recognised. Same HMAC-SHA256 + timestamp + SSRF-guard semantics, plus inbound dedupe and 429-retry that the Rust adapter didn't have.

[[sidecar_channels]]
name = "webhook"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.webhook"]
channel_type = "webhook"
[sidecar_channels.env]
WEBHOOK_LISTEN_PORT = "8461"
# WEBHOOK_LISTEN_PATH = "/webhook"
# WEBHOOK_CALLBACK_URL = "https://example.com/incoming"
# WEBHOOK_DELIVER_ONLY = "1"
# WEBHOOK_DELIVER = "telegram"

WEBHOOK_SECRET (the HMAC-SHA256 signing secret) belongs in ~/.librefang/secrets.env. Sidecar binds its own listener on WEBHOOK_LISTEN_PORT (default 8461) at WEBHOOK_LISTEN_PATH (default /webhook). The same SSRF guard from the Rust adapter (private / loopback / link-local / multicast / cloud-metadata / 100.64-CGN / IPv4-mapped-IPv6 all rejected, plus reserved hostnames like localhost., kubernetes.default.svc.cluster.local) runs at sidecar startup AND on every outbound POST (defence-in-depth — a config reload that swaps the URL to a private host doesn't leak the secret to localhost).

When WEBHOOK_DELIVER_ONLY=1 the sidecar tags inbound with __deliver_only__ / __deliver_target__ metadata; the kernel's bridge layer reads those to forward the message body straight to the named channel without invoking an LLM. WEBHOOK_DELIVER is required when WEBHOOK_DELIVER_ONLY=1 — fail-closed at sidecar startup if missing (the Rust validator only warned at runtime).

QQ Bot (sidecar)

QQ migrated from an in-process Rust adapter to an out-of-process Python sidecar (librefang.sidecar.adapters.qq). The sidecar talks to the QQ Open Platform via WebSocket (gateway) + REST (token + outbound). An existing [channels.qq] block is no longer recognised — re-declare as [[sidecar_channels]]:

[[sidecar_channels]]
name = "qq"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.qq"]
channel_type = "qq"
[sidecar_channels.env]
QQ_APP_ID = "your-app-id"
# QQ_ALLOWED_USERS = "openid-1,openid-2"   # optional
# QQ_ACCOUNT_ID = "prod-bot"               # optional
# QQ_INTENTS = "1073746435"                # optional bitmask override

Secret via ~/.librefang/secrets.env: QQ_APP_SECRET (the bot's clientSecret from the QQ Open Platform console).

WeCom (sidecar)

WeCom (formerly WeChat Work / Enterprise WeChat) migrated from an in-process Rust adapter to an out-of-process Python sidecar (librefang.sidecar.adapters.wecom). The sidecar connects via WebSocket to wss://openws.work.weixin.qq.com using the intelligent-bot Bot ID and Secret. The legacy callback mode (HTTP webhook + AES-CBC-256 inbound decryption) is no longer supported — Python's stdlib has no AES, and the sidecar SDK is stdlib-only by policy. Operators who relied on callback mode must switch the bot to WebSocket mode in the WeCom admin console.

An existing [channels.wecom] block is no longer recognised — re-declare as [[sidecar_channels]]:

[[sidecar_channels]]
name = "wecom"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.wecom"]
channel_type = "wecom"

[sidecar_channels.env]
WECOM_BOT_ID = "aibxxxxxxx"
# WECOM_ALLOWED_USERS = "alice,bob"   # optional CSV; empty = all
# WECOM_ACCOUNT_ID    = "prod-bot"    # optional multi-bot routing

Secret via ~/.librefang/secrets.env: WECOM_BOT_SECRET (the bot secret from the WeCom admin console).

WeChat (sidecar)

WeChat (personal account via iLink) is provided by the Python sidecar adapter (librefang.sidecar.adapters.wechat). The in-process [channels.wechat] config block was removed in the sidecar migration. Declare WeChat as a [[sidecar_channels]] entry instead:

[[sidecar_channels]]
name = "wechat"
command = "python3"
args = ["-m", "librefang.sidecar.adapters.wechat"]
channel_type = "wechat"
[sidecar_channels.env]
# WECHAT_BOT_TOKEN = ""            # leave blank to trigger QR login
# WECHAT_ALLOWED_USERS = ""        # comma-separated hash@im.wechat
# WECHAT_ACCOUNT_ID = "prod"       # optional, multi-bot routing

On first start the sidecar runs the QR login flow against ilinkai.weixin.qq.com and logs the QR code string at INFO; scan it from the WeChat app to confirm. The returned bot token can be pasted into WECHAT_BOT_TOKEN to skip the QR step on subsequent restarts (the dashboard's Channels page writes it to ~/.librefang/secrets.env for you).


Channel Overrides (legacy — removed)

The [channels.<name>.overrides] sub-table is no longer recognised. It lived on the in-process ChannelsConfig which was emptied in the sidecar migration (ChannelsConfig now only carries file_download_* / file_upload_max_bytes global file-transfer fields). The section below documents the pre-migration behaviour for historical context and migration mapping.

Migration mapping — equivalent knobs in the sidecar model:

Old overrideSidecar equivalent
model, system_promptSet on the agent (agent.toml) rather than per-channel — sidecars route every inbound to the same agent.
dm_policy, group_policyPass as *_DM_POLICY / *_GROUP_POLICY env vars on the [sidecar_channels.env] block (Telegram / Discord / Slack / WhatsApp / Teams sidecars all read these).
rate_limit_per_minute, rate_limit_per_userSidecars rate-limit themselves to platform limits; for tighter caps, wrap the sidecar in a reverse proxy.
output_formatNot exposed on [[sidecar_channels]]. Agents that need a specific output format should flip it on the agent side.
disable_commands, allowed_commands, blocked_commandsSet on the agent's manifest (agent.toml) under the command-policy section — applies uniformly across channels.
threading, usage_footer, typing_modeSidecar defaults (threading where the platform supports it; no usage footer; instant typing). Not configurable per-channel today.

The remainder of this section describes the deleted override shape — keep for reference; do not copy-paste into a current config.toml.

Channel Overrides (historical reference)

Every channel adapter (pre-migration) supported an [channels.<name>.overrides] sub-table that customized agent behavior per-channel.

[channels.discord.overrides]
model = "claude-haiku-4-5-20251001"
system_prompt = "You are a concise Discord assistant."
dm_policy = "respond"
group_policy = "mention_only"
rate_limit_per_minute = 0
rate_limit_per_user = 10
threading = true
output_format = "markdown"
usage_footer = "tokens"
typing_mode = "instant"
disable_commands = false
allowed_commands = []
blocked_commands = []
FieldTypeDefaultDescription
modelstring or nullnullModel override for this channel. Uses the agent's default model when null.
system_promptstring or nullnullSystem prompt override for this channel.
dm_policystring"respond"How the bot handles direct messages. See below.
group_policystring"mention_only"How the bot handles group messages. See below.
rate_limit_per_minuteu320Global rate limit for this channel (messages per minute). 0 = unlimited.
rate_limit_per_useru320Maximum messages per user per minute. 0 = unlimited.
threadingboolfalseEnable thread replies (where supported by the platform).
output_formatstring or nullnullOverride output formatting. See below.
usage_footerstring or nullnullOverride usage footer mode for this channel. Values: off, tokens, cost, full.
typing_modestring or nullnullTyping indicator behavior. See below. Defaults to instant.
disable_commandsboolfalseDisable all built-in slash commands. See Command Policy.
allowed_commandslist of strings[]Whitelist of command names (no leading /). When non-empty, only these commands work.
blocked_commandslist of strings[]Blacklist of command names (no leading /). Applied when allowed_commands is empty.

dm_policy values:

ValueDescription
respondRespond to all direct messages (default).
allowed_onlyOnly respond to DMs from users in the allowed list.
ignoreIgnore all direct messages.

group_policy values:

ValueDescription
allRespond to all messages in group chats.
mention_onlyOnly respond when the bot is @mentioned (default).
commands_onlyOnly respond to slash commands.
ignoreIgnore all group messages.

output_format values:

ValueDescription
markdownStandard Markdown (default).
telegram_htmlTelegram HTML subset (<b>, <i>, <code>, etc.).
slack_mrkdwnSlack mrkdwn format (*bold*, _italic_, `code`).
plain_textNo formatting markup.

typing_mode values:

ValueDescription
instantSend typing indicator immediately on message receipt (default).
messageSend typing indicator only when the first text delta arrives from the LLM.
thinkingSend typing indicator only during LLM reasoning/thinking phase.
neverNever send typing indicators.

Command Policy

Every channel ships with a set of built-in slash commands (/agent, /new, /reboot, /model, /usage, etc.) that let users switch agents, fork into a fresh session, inspect usage, and trigger workflows. On public-facing bots (customer support, community assistants), exposing these to arbitrary users is a security hole — anyone can type /agent admin to switch to an internal agent, /new to fork off into a new session (the prior conversation stays resumable on the channel), or /model to change the model.

The three command-policy fields gate the built-in commands per channel. Precedence: disable_commands > allowed_commands (whitelist) > blocked_commands (blacklist).

Blocked commands are forwarded to the agent as plain text — they are not rejected with an error. So a public bot with disable_commands = true will respond conversationally when a user types /agent admin, and the user never learns that any command system exists.

Recipe: locked-down public bot (no commands at all)

[channels.discord.overrides]
disable_commands = true

Recipe: public bot that keeps platform UX commands

Keep only the safe built-in commands:

[channels.discord.overrides]
allowed_commands = ["start", "help"]

Recipe: admin bot that just blocks the dangerous ones

For an operator-facing bot (with an allowed_users gate), you may still want to block commands that change costly state:

[channels.discord.overrides]
blocked_commands = ["agent", "new", "reboot", "model", "stop"]

Command names are the bare tokens used internally — either "agent" or "/agent" in TOML works, the leading slash is stripped on match. The full list of built-in commands is in /help.


[[sidecar_channels]]

Sidecar channel adapters allow external processes (written in any language) to act as channel adapters. Communication uses newline-delimited JSON over stdin/stdout.

[[sidecar_channels]]
name = "my-custom-channel"
command = "python3"
args = ["adapters/my_adapter.py"]
channel_type = "custom_platform"
[sidecar_channels.env]
MY_API_TOKEN = "secret"
FieldTypeDefaultDescription
namestringrequiredDisplay name for this adapter.
commandstringrequiredExecutable to run (e.g., "python3", "/usr/local/bin/my-adapter").
argslist of strings[]Arguments to pass to the command.
envmap of string to string{}Extra environment variables to pass to the subprocess.
channel_typestring or nullnullChannel type identifier. Defaults to Custom(<name>) if null.