Skip to content

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: the Channel trait, ChannelEvent enum, ChannelConfig schema, and ChannelManager
  • wcore-channel-<platform> (10 crates): one per platform
  • wcore-channels-registry: factory dispatch table and auto_register_from_user_config
PlatformInboundOutboundNotes
SlackEvents API webhook (HMAC-SHA256 verified)chat.postMessage
DiscordGateway v10 WebSocket (MESSAGE_CREATE)REST POST /api/v10/channels/{id}/messagesPrivileged MESSAGE_CONTENT intent required
TelegramgetUpdates long-poll background tasksendMessage
SignalJSON-RPC receive from signal-cli subprocessJSON-RPC send over stdinRequires signal-cli on PATH
WhatsAppMeta webhook (X-Hub-Signature-256 verified)Meta Cloud API POST /{phone_number_id}/messages
SMSTwilio webhook (HMAC-SHA1 verified)Twilio REST POST Accounts/{sid}/Messages.json
EmailIMAP polling with UID cursor (spawn_blocking)SMTP via lettre (rustls-tls)IMAP section is optional; omit for outbound-only
MatrixREST pollMatrix REST
MS TeamsSend-only at this versionBot Framework Connector REST, OAuth2 client-credentialsSee caveat below
iMessagePolls ~/Library/Messages/chat.db (read-only SQLite)osascript AppleScript subprocessmacOS 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). :::

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 here
name = "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 = 5

Required keys: workspace_name, credential_handle_bot_token, credential_handle_signing_secret.

name = "acme-discord"
platform = "discord"
[options]
credential_handle = "discord.acme.bot_token"
# optional:
# allowed_channel_ids = ["111222333444555666"]
# intents = 33792 # default: GUILD_MESSAGES | MESSAGE_CONTENT

The MESSAGE_CONTENT intent (bitmask 33792) must be enabled in the Discord developer portal for the bot to receive message text.

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 | Markdown
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 = 10

Signal 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 = 5

The credential_handle_app_secret is used for X-Hub-Signature-256 webhook verification.

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 = 5

Inbound 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 # default
user_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 # default
user_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.

name = "acme-matrix"
platform = "matrix"
[options]
homeserver_url = "https://matrix.org"
user_id = "@wayland-bot:matrix.org"
credential_handle_access_token = "matrix.acme.token"
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 endpoint

For other regions, set service_url to the appropriate Bot Framework Connector endpoint (e.g. https://smba.trafficmanager.net/emea/).

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.

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 name field inside the file; mismatches are logged and skipped.
  • Files with enabled = false are skipped without error.
  • Unknown platform values are logged at warn and skipped.
  • Parse failures and construction errors are logged at warn and 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 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.

Three adapters perform HMAC verification on inbound webhooks before accepting any event:

AdapterAlgorithmHeader
SlackHMAC-SHA256X-Slack-Signature + X-Slack-Request-Timestamp (5-minute replay window)
WhatsAppHMAC-SHA256X-Hub-Signature-256
SMS (Twilio)HMAC-SHA1Twilio 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.