Foundation Relay (channel adapters)
- Area: Foundation
- Path:
services/foundation/relay - Kind: Go module (interface + scaffolded adapters)
- Status: scaffold complete — interface + 8 adapter skeletons; wire protocols pending (tracked under KRELAY-–KRELAY- + KRELAY- Telegram migration)
Role in the stack
The Kode Relay server in services/ai/kode/platform/relay speaks one WebSocket protocol to Kode frontends (CLI, TUI, mobile, desktop, web). To extend the same persistent-session model to third-party messaging channels — Slack, Discord, WhatsApp, Signal, Matrix, Microsoft Teams, iMessage, Google Chat — the relay needs a uniform bridge layer.
services/foundation/relay is that layer. It is intentionally AI-free and lives under services/foundation/ (not services/ai/) because channel I/O is reusable infrastructure: the same Slack adapter could serve a non-AI bot, an alerting daemon, or a CRM webhook proxy. Per RFC-003, code consumed by multiple sectors via API/import sits in services/foundation/.
The module exports a single contract — adapter.Adapter — that every channel implementation satisfies. The Kode Relay server (the AI-specific consumer) imports concrete adapters and routes traffic; each adapter only knows its own platform.
Primary couplings
| Module | Direction | Why |
|---|---|---|
services/ai/kode/platform/relay |
imports relay | Routes Kode traffic across channels |
services/foundation/relay/adapter |
exported | Shared interface + types + errors |
Each adapters/<channel>/ |
imports adapter |
Compile-time var _ adapter.Adapter = (*Adapter)(nil) |
No outbound dependencies on other Koder modules — adapters speak only to their respective third-party APIs (Slack Bolt, Discord Gateway, WhatsApp Cloud API, etc.).
Interface
type Adapter interface {
Name() string
Start(ctx context.Context) error
Stop(ctx context.Context) error
Send(ctx context.Context, msg OutboundMessage) (MessageID, error)
Subscribe(ctx context.Context) (<-chan InboundMessage, error)
}Plus shared types: OutboundMessage, InboundMessage, Body, File, Identity, MessageID. Sentinel errors: ErrNotImplemented, ErrInvalidConfig, ErrNotStarted.
Status (2026-04-30)
| Adapter | Scaffold | Config validation | Wire protocol |
|---|---|---|---|
slack |
yes | BotToken + Socket/HTTP credentials + WebhookListenAddr (HTTP) + APIURL (test override) | Socket Mode (KRELAY-) + HTTP Events mode (KRELAY-) + files upload (KRELAY-) shipped 2026-04-30; URL-pull file mode deferred |
discord |
yes | BotToken; ApplicationID + Intents optional + APIURL (test override) | Gateway + REST shipped (KRELAY-, 2026-04-30); slash commands + file uploads deferred |
whatsapp |
yes | AccessToken + PhoneNumberID + VerifyToken + WebhookSecret + APIURL | Cloud API + webhook listener shipped (KRELAY-, 2026-04-30); media upload + interactive deferred |
telegram |
not yet | — | live in services/ai/kode/platform/kode-bridge (legacy); migration is KRELAY- |
signal |
yes | RestEndpoint + PhoneNumber | pending |
matrix |
yes | HomeserverURL + UserID + AccessToken | pending |
teams |
yes | AppID + AppPassword + TenantID | pending |
imessage |
yes | ServerURL + Password (BlueBubbles) | pending |
gchat |
yes | CredentialsPath + ProjectID + PubsubSubscription | pending |
All 8 scaffolded adapters compile, satisfy the interface (asserted at compile time), validate their configs, and return ErrNotImplemented for the wire-protocol surfaces.
Sub-tickets opened from KRELAY-
The original epic was decomposed in 2026-04-30 into 8 actionable sub-tickets (KRELAY-–KRELAY-) so each line of work — wire protocol, migration, inbox routing, CLI, deploy, credentials docs — can land independently:
| Ticket | Scope |
|---|---|
| KRELAY- | Slack wire protocol (slack-go/slack) |
| KRELAY- | Discord wire protocol (bwmarrin/discordgo) |
| KRELAY- | WhatsApp wire protocol (Cloud API; hand-rolled HTTP) |
| KRELAY- | Telegram migration: kode-bridge → adapters/telegram/ |
| KRELAY- | Multi-channel inbox routing in Kode TUI |
| KRELAY- | kode relay status CLI command |
| KRELAY- | Deploy Slack + Discord in koder-relay LXC on s.r1 |
| KRELAY- | Per-channel credentials docs in meta/context/credentials/ |
Why the legacy Telegram bridge stays put for now
services/ai/kode/platform/kode-bridge was the original Telegram implementation, written before this adapter framework existed. It ships in production today. Migrating it into adapters/telegram/ under the new contract is a separate work item (tracked in ticket projects/koder-stack/backlog/pending/024-kode-relay-multi-canal-adapters.md) and intentionally not part of this scaffold.
Reporter (KRELAY-, 2026-04-30)
services/foundation/relay/reporter/ aggregates lifecycle + health signal across the configured adapter pool. The flagship consumer is kode relay status, but the library is presentation-agnostic so a Prometheus exporter, dashboard, or alerting daemon can reuse the snapshot shape.
r := reporter.New(slackAdapter, discordAdapter, whatsappAdapter)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
for _, status := range r.Snapshot(ctx) {
fmt.Println(status.Name, status.State, status.HealthError)
}AdapterStatus.State is one of running | degraded | not_started | no_health_signal. The last bucket covers adapters that don't implement the optional adapter.HealthChecker interface — they get reported but with no liveness conclusion, so operators can tell scaffolds-without-wire apart from genuinely stopped channels.
Three adapters implement HealthChecker today (slack, discord, whatsapp — the wired-up trio); the remaining five report no_health_signal until each lands its wire protocol.
A standalone binary lives at cmd/kode-relay/. kode-relay
status (text + --json) probes every adapter and exits non-zero if any reported degraded. Integration into the main Kode CLI (kode relay status) is tracked under KRELAY- — the services/ai/kode/app lock blocked it during KRELAY-'s session.
Tests
Each adapter ships ~7 unit tests covering config rejection paths, valid construction, and behavioural assertions for the wire protocol (or ErrNotImplemented while scaffolded). The reporter package adds 9 tests covering every State bucket plus context-deadline propagation. Total: 10 packages (1 contract + 8 adapters + reporter), all green under go test -race
-count=1 ./... from the module root. The standalone cmd/kode-relay binary is smoke-tested manually (no automated test until the conf loader lands and loadAdapters() returns a non-empty slice).