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-bridgeadapters/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).