arizuko › components › channel adapters
A channel adapter is a small daemon that speaks one upstream platform’s protocol on one side and arizuko’s HTTP contract on the other. Telegram on the left, gated on the right.
Inbound: the adapter receives a message from the platform, signs the request with CHANNEL_SECRET, and POSTs it to gated. Outbound: gated calls back to the adapter’s POST /v1/send with the agent’s reply, and the adapter pushes it to the platform.
One adapter per platform. One container per adapter. Each runs on LISTEN_ADDR=:8080 inside its container; Docker networking keeps the ports collision-free.
Every adapter binds to the same Go interface in core/types.go:
Channel — Name, Connect, Send, SendFile, SendVoice, Owns, Typing, Disconnect. The minimum every adapter implements.Socializer — Post, Like, Dislike, Forward, Quote, Repost, Edit, Delete. Implemented where the platform has the verb; sentinel error otherwise.Suggester / Namer — pane controls. Suggested-prompt buttons and conversation title. Implemented today by slakd; others can opt in.HistoryFetcher — optional. Pull older messages from the platform API when the agent asks for context.Every adapter serves the same HTTP surface:
GET /health — 200 when connected to the platform, 503 when the link is down (QR unscanned, stream dropped, token expired).POST /v1/send — gated hands the adapter an outbound row to deliver.POST /v1/connect / POST /v1/disconnect — lifecycle for the upstream session.platform (Slack, Telegram, …)
| platform-native protocol
v
<channel adapter> (LISTEN_ADDR=:8080, own container)
| POST /v1/messages (signed by CHANNEL_SECRET)
v
gated
| POST /v1/send (callback to adapter)
v
<channel adapter>
| platform-native protocol
v
platform
The trust contract is symmetric: gated verifies inbound HMACs, the adapter verifies outbound HMACs. Neither trusts a request without a valid CHANNEL_SECRET signature. See auth.
| name | platform | language | notes |
|---|---|---|---|
| slakd | Slack | Go | Assistant pane — suggested prompts and conversation title; reactions map to likes. |
| teled | Telegram | Go | Long-poll bot API, file uploads, native voice messages. |
| discd | Discord | Go | Gateway over websocket, ActionRow buttons, channel threads. |
| mastd | Mastodon | Go | ActivityPub via API token, replies and boosts. |
| bskyd | Bluesky | Go | AT Protocol, post threading. |
| reditd | Go | Comments, posts, crossposts; native dislike via downvote. | |
| emaid | Go | IMAP receive, SMTP send, threading by Message-ID. | |
| linkd | Go | Messaging API, connection-scoped DMs. | |
| whapd | TypeScript | Baileys client, QR pairing, voice notes. | |
| twitd | X / Twitter | TypeScript | Mentions and DMs. |
Capability gaps are honest: an adapter without a platform verb returns chanlib.ErrUnsupported. The agent gets a structured response and picks another action instead of silently dropping the call.
Per-platform quirks stay inside the adapter. Slack threads, Discord ActionRows, WhatsApp QR pairing, IMAP folder watching — none of it leaks into gated. The DB sees a uniform messages row no matter where the message came from.
Output rendering follows the same one-renderer rule. The agent picks a Claude Code output style per channel: slack emits Slack mrkdwn (*bold*, _italic_, <url|text>); other channels use CommonMark. The runner selects the style when it spawns the container (container/runner.go) so the adapter receives text already shaped for the surface.
Language choice follows the upstream SDK. Most platforms have a maintained Go client, so the adapter is Go. WhatsApp and X have richer TypeScript clients (Baileys, twitter-api-v2), so those adapters run on Node. The HTTP contract is identical either way.
Two artifacts, no edits to proxyd or compose:
Channel; implement Socializer for the verbs the platform exposes. Serve the standard HTTP surface on LISTEN_ADDR=:8080.template/services/<daemon>.toml with the compose env block and a [[proxyd_route]] entry. The next arizuko run picks it up.Spec: specs/5/35-proxyd-standalone.md. Extension points: EXTENDING.md.
An inbound channel and an outbound channel do not have to be the same daemon. A message can arrive on email and the reply can go out on Slack — the route table decides. See howto/asymmetric-channels.
core/types.go — Channel, Socializer, Suggester, Namer, HistoryFetcher interfaces.chanlib/ and chanreg/ — shared HTTP plumbing every adapter mounts.