Messaging Channels
Wayland Core ships 10 messaging channel adapters. Each implements a uniform Channel trait and runs on its own tokio task. All channels fan inbound events into a single broadcast stream that the agent loop subscribes to, and the agent can send to any registered channel through the send_message tool.
The adapters are organized across three crate layers:
wcore-channels: theChanneltrait,ChannelEventenum,ChannelConfigschema, andChannelManagerwcore-channel-<platform>(10 crates): one per platformwcore-channels-registry: factory dispatch table andauto_register_from_user_config
The 10 adapters
Section titled “The 10 adapters”| Platform | Inbound | Outbound | Notes |
|---|---|---|---|
| Slack | Events API webhook (HMAC-SHA256 verified) | chat.postMessage | |
| Discord | Gateway v10 WebSocket (MESSAGE_CREATE) | REST POST /api/v10/channels/{id}/messages | Privileged MESSAGE_CONTENT intent required |
| Telegram | getUpdates long-poll background task | sendMessage | |
| Signal | JSON-RPC receive from signal-cli subprocess | JSON-RPC send over stdin | Requires signal-cli on PATH |
Meta webhook (X-Hub-Signature-256 verified) | Meta Cloud API POST /{phone_number_id}/messages | ||
| SMS | Twilio webhook (HMAC-SHA1 verified) | Twilio REST POST Accounts/{sid}/Messages.json | |
IMAP polling with UID cursor (spawn_blocking) | SMTP via lettre (rustls-tls) | IMAP section is optional; omit for outbound-only | |
| Matrix | REST poll | Matrix REST | |
| MS Teams | Send-only at this version | Bot Framework Connector REST, OAuth2 client-credentials | See caveat below |
| iMessage | Polls ~/Library/Messages/chat.db (read-only SQLite) | osascript AppleScript subprocess | macOS only; see caveat below |
:::caution MS Teams is send-only
wcore-channel-msteams does not yet receive inbound messages. Inbound webhook receipt is deferred to v0.8.3. Do not rely on inbound Teams events at this version.
:::
:::caution iMessage is macOS-only
The "imessage" factory is compiled only when target_os = "macos". On Linux and Windows, the registry returns None for any config with platform = "imessage" and logs a skip at boot. iMessage also requires Full Disk Access (to read chat.db) and macOS Automation TCC consent (for osascript).
:::
Config files
Section titled “Config files”Each channel is configured with one TOML file at ~/.wayland/channels/<name>.toml. The file stem must match the name field inside the file. Credentials are never stored in config files; credential_handle fields are keys passed to the OS keychain (or whichever CredentialsStore backend is active).
At minimum, every file needs name, platform, and an [options] table.
name = "my-channel"platform = "<platform-string>"enabled = true # default true; set false to keep the file without registering
[options]# platform-specific keys herename = "acme-slack"platform = "slack"
[options]workspace_name = "acme"credential_handle_bot_token = "slack.acme.bot_token"credential_handle_signing_secret = "slack.acme.signing_secret"# optional:# default_channel_id = "C0123456789"# max_retry_attempts = 5Required keys: workspace_name, credential_handle_bot_token, credential_handle_signing_secret.
Discord
Section titled “Discord”name = "acme-discord"platform = "discord"
[options]credential_handle = "discord.acme.bot_token"# optional:# allowed_channel_ids = ["111222333444555666"]# intents = 33792 # default: GUILD_MESSAGES | MESSAGE_CONTENTThe MESSAGE_CONTENT intent (bitmask 33792) must be enabled in the Discord developer portal for the bot to receive message text.
Telegram
Section titled “Telegram”name = "acme-tg"platform = "telegram"
[options]credential_handle = "telegram.acme.bot_token"# optional:# allowed_chat_ids = ["-100123456789"]# long_poll_timeout_secs = 30 # default 30, max 120# parse_mode = "MarkdownV2" # MarkdownV2 | HTML | MarkdownSignal
Section titled “Signal”name = "acme-signal"platform = "signal"
[options]account = "+15551234567"# optional:# signal_cli_path = "/usr/local/bin/signal-cli" # default: finds signal-cli on PATH# send_timeout_secs = 10Signal credentials live in signal-cli’s own state directory, not in the engine credential store. The _credentials argument is intentionally ignored by this adapter.
name = "acme-whatsapp"platform = "whatsapp"
[options]workspace_name = "acme"phone_number_id = "1234567890"credential_handle_access_token = "whatsapp.acme.access_token"credential_handle_app_secret = "whatsapp.acme.app_secret"# optional:# default_recipient = "+15551234567"# graph_version = "v18.0"# max_retry_attempts = 5The credential_handle_app_secret is used for X-Hub-Signature-256 webhook verification.
SMS (Twilio)
Section titled “SMS (Twilio)”name = "acme-sms"platform = "sms"
[options]from_number = "+15550001111"credential_handle_account_sid = "sms.acme.account_sid"credential_handle_auth_token = "sms.acme.auth_token"# optional:# max_retry_attempts = 5Inbound messages arrive via Twilio webhook; the adapter verifies the HMAC-SHA1 signature.
name = "acme-email"platform = "email"
[options]from_address = "bot@acme.com"
[options.smtp]host = "smtp.acme.com"# port = 587 # defaultuser_credential_handle = "email.acme.smtp_user"password_credential_handle = "email.acme.smtp_pass"
# omit [options.imap] entirely for outbound-only[options.imap]host = "imap.acme.com"# port = 993 # defaultuser_credential_handle = "email.acme.imap_user"password_credential_handle = "email.acme.imap_pass"# mailbox = "INBOX" # default# poll_interval_secs = 60 # default# allowed_senders = ["trusted@example.com"]The allowed_senders filter compares against the bare address in the From: header. The From: header is not an authenticated principal; SMTP does not bind the envelope sender to the connecting party, and no SPF/DKIM/DMARC verification is performed here. Treat allowed_senders as a delivery-side filter, not as authentication.
Matrix
Section titled “Matrix”name = "acme-matrix"platform = "matrix"
[options]homeserver_url = "https://matrix.org"user_id = "@wayland-bot:matrix.org"credential_handle_access_token = "matrix.acme.token"MS Teams
Section titled “MS Teams”name = "acme-teams"platform = "msteams"
[options]credential_handle_app_id = "msteams.acme.app_id"credential_handle_app_password = "msteams.acme.app_password"# optional:# service_url = "https://smba.trafficmanager.net/amer/" # default: Americas endpointFor other regions, set service_url to the appropriate Bot Framework Connector endpoint (e.g. https://smba.trafficmanager.net/emea/).
iMessage
Section titled “iMessage”name = "acme-imessage"platform = "imessage"
[options]# all fields optional# poll_interval_ms = 2000 # default 2000, clamped to [500, 60000]# allowed_handles = ["+15551234567", "user@example.com"]No credential_handle fields are needed. Access is controlled by macOS TCC permissions: Full Disk Access for reading chat.db, and Automation consent for sending via osascript.
Auto-registration at boot
Section titled “Auto-registration at boot”wcore-channels-registry::auto_register_from_user_config scans ~/.wayland/channels/*.toml at engine boot (bootstrap.rs:1880-1900). Files are processed in sorted filename order so registration order is deterministic. For each file:
- The file stem must equal the
namefield inside the file; mismatches are logged and skipped. - Files with
enabled = falseare skipped without error. - Unknown
platformvalues are logged atwarnand skipped. - Parse failures and construction errors are logged at
warnand skipped.
A single bad config file cannot prevent the other channels from registering; the function always returns Ok(count) where count is the number successfully registered.
$WAYLAND_HOME overrides the base directory; the channels directory is always <WAYLAND_HOME>/channels/.
The send_message tool
Section titled “The send_message tool”The agent can send a message to any registered channel using the send_message tool (wcore-tools/src/send_message.rs, tool name "send_message"). The tool takes a platform name, a target identifier (channel name, phone number, chat ID, etc.), and a text payload.
The tool is wired to a MessageTransport at construction time, which in turn calls ChannelManager::send_to. If no transport is wired (for example in a one-shot CLI invocation without a channel session), the tool returns a structured error rather than silently no-oping.
Example: ask the agent to send a Slack notification:
Send "Build passed" to the acme-slack channel.The agent will invoke send_message with platform = "slack" and the channel name from your registered config.
Webhook signature verification
Section titled “Webhook signature verification”Three adapters perform HMAC verification on inbound webhooks before accepting any event:
| Adapter | Algorithm | Header |
|---|---|---|
| Slack | HMAC-SHA256 | X-Slack-Signature + X-Slack-Request-Timestamp (5-minute replay window) |
| HMAC-SHA256 | X-Hub-Signature-256 | |
| SMS (Twilio) | HMAC-SHA1 | Twilio X-Twilio-Signature |
Requests that fail signature verification are rejected before any event is enqueued. The signing secrets are resolved from the credential store at start() time, not stored in the TOML config.
Operating the gateway
Section titled “Operating the gateway”Running channels at scale is covered separately. See Channel Gateway Operations for the exactly-once delivery ledger, the single-owner inbound polling lease, and how to name, acknowledge, and re-send an abandoned delivery.