26 KiB
Channel protocol architecture
Status: Current shared type contract and Discord compatibility baseline. The shared gateway registry, Telegram parity, Matrix integration, identity-linking, and multi-surface multiplexing described below are draft or unimplemented.
Audience: Developers maintaining
@mosaicstack/types, channel plugins, the gateway chat/plugin boundaries, or future official adapters.Last verified: 2026-08-10 against the source and focused tests listed in Evidence.
Authority: Executable source and tests are authoritative for current behavior. This page explains the boundary; it is not a runtime registry, an API contract, a requirements document, or proof that every channel uses the shared DTOs.
Reading this page
This page intentionally separates three states:
- Current — implemented in the repository and supported by the cited tests.
- Compatibility — an existing wire path that preserves current behavior but does not yet mean that the shared channel ports are wired through the gateway.
- Draft — a design direction or follow-up work item. Draft sections have no implementation authority and must not be used as instructions for operating Telegram, Matrix, identity linking, or cross-surface fanout.
The migration from docs/_old_structure/architecture/channel-protocol.md is a documentation correction. It does not add adapters, change gateway behavior, change authentication, or create database objects.
Authority and evidence boundaries
| Boundary | Current authority | What this page may claim |
|---|---|---|
| Shared channel types and ports | channel.dto.ts, channel-adapter.ts, and their exports |
The TypeScript shapes and method signatures that are currently published from @mosaicstack/types. |
| Discord native behavior | plugins/discord/src/index.ts and index.test.ts |
The Discord allowlist, pairing, role, thread, ingress, egress, retry, and health behavior covered by source and tests. |
| Discord gateway compatibility | chat.gateway.ts, chat.gateway-auth.ts, and the focused gateway tests |
The signed Socket.IO service path, gateway validation, raw chat events, and current session dispatch behavior. |
| Plugin hosting | plugin.interface.ts, plugin.module.ts, and plugin.service.ts |
The lifecycle registry that exists today. It is not evidence of a shared OfficialChannelAdapter registry. |
| Telegram | plugins/telegram/src/index.ts and package.json |
The raw legacy behavior that exists. It is not evidence of shared-contract parity or a working authenticated gateway integration. |
| Matrix, identity linking, and multiplexing | No matching current implementation and test boundary was found for the old page's designs | These topics remain explicitly draft/unimplemented here. |
The current source boundaries also distinguish two identities:
- A channel route carries a configuration-owned logical agent and response destination.
- The gateway chooses the durable session, provider, model, and runtime session internally.
A route is therefore not a claim that a channel adapter owns or exposes a harness, provider, model, process, or native runtime-session identity.
Current shared contract
The channel types are exported through packages/types/src/channel/index.ts and packages/types/src/index.ts. They define a transport-neutral vocabulary, but TypeScript interfaces alone do not prove that every producer or consumer uses that vocabulary.
DTOs
The current DTO surface is:
| Type | Current shape and boundary |
|---|---|
ChannelMetadataValue |
JSON-safe strings, numbers, booleans, null, arrays, and nested objects. |
ChannelAttachmentDto |
id, name, mimeType, url, and optional sizeBytes. |
ChannelMessageDto |
id, channelName, channelId, senderId, senderKind, content, contentKind, timestamp, and metadata; threadId, replyToId, and attachments are optional. |
ChannelAuthorizedPrincipalDto |
Native channelUserId, a viewer/operator/admin role, and an optional mosaicUserId for privileged gateway policy. |
ChannelBindingDto |
Configuration-owned bindingId, channelName, workspaceId, channelId, logicalAgentId, and paired principals. Credentials are intentionally absent. |
ChannelResponseTargetDto |
channelId and an optional threadId. |
ChannelConversationRouteDto |
bindingId, logicalAgentId, conversationId, channelName, authorizationChannelId, and responseTarget. |
ChannelIngressDto |
correlationId, nativeMessageId, an operation, an authorized principal, a normalized message, and a stable route. |
ChannelEgressDto |
correlationId, a normalized message, and the stable route. |
ChannelAdapterHealthDto |
status of connected, degraded, or disconnected, with optional detail. |
The available operations are message.send, approval.create, and session.stop. The available sender kinds are user, agent, and system; content kinds are text, markdown, code, image, and file.
Lifecycle and ports
The shared adapter file currently defines these seams:
interface OfficialChannelAdapter {
readonly name: string;
start(): Promise<void>;
stop(): Promise<void>;
health(): Promise<ChannelAdapterHealthDto>;
}
interface ChannelIngressPort {
receive(ingress: ChannelIngressDto): Promise<void>;
}
interface ChannelEgressPort {
send(egress: ChannelEgressDto): Promise<void>;
}
ChannelDeliveryError currently has only these codes: invalid_route, destination_unavailable, and delivery_failed. The type surface does not define a revoked-auth error code or an executable protocol version 1.0.0.
Stable route rule
ChannelConversationRouteDto deliberately omits provider, harness, model, process, and native runtime-session fields. The Discord implementation derives its current conversation address as:
<logical-agent-id>:discord:<response-channel-id>
and derives its binding address from the configured guild, parent channel, and logical-agent instance. The gateway validates the expected Discord conversation address before dispatch. This is a route-integrity rule, not a claim that the shared DTO is already the gateway's universal session API.
What is and is not wired today
The Discord class implements both OfficialChannelAdapter and ChannelEgressPort, and accepts an optional ChannelIngressPort dependency. The direct ingress seam is exercised by the Discord tests. However, the gateway host currently registers IChannelPlugin objects with only name, start, stop, and optional project provisioning. Its PLUGIN_REGISTRY is an array of those lifecycle wrappers; it does not expose health(), ChannelRegistry.healthAll(), or shared port wiring.
The gateway's current output path is also still Socket.IO event streaming (agent:start, agent:text, and agent:end). No gateway service in the cited implementation produces a ChannelEgressDto for a registered adapter. The shared ports are therefore current contracts and a tested Discord seam, not a completed gateway-wide adapter architecture.
Current Discord compatibility path
Discord is the current reference implementation for the shared contract and the compatibility path. Its behavior is split between native Discord translation in the plugin and gateway-side validation/dispatch.
Native ingress and authorization
For an inbound Discord message, the plugin currently:
- Ignores bot-authored messages and messages without a guild.
- Uses the configured parent text channel as the authorization channel only when the message is in a thread. A normal channel's category parent is not substituted for the channel itself.
- Applies default-deny guild, channel, and user allowlists.
- Resolves a configuration-owned binding and paired user role before creating a thread or dispatching to the gateway.
viewercannot send turns; approval and stop are admin operations. - Applies per-user/channel message and mention-thread rate limits before Discord thread creation or gateway dispatch.
- Builds a stable route from the configured logical-agent instance and the response channel/thread.
- Normalizes the authorized turn to
ChannelIngressDtowhen a directingressPortdependency is supplied.
The normalized ChannelMessageDto currently includes:
channelName: "discord";- the response channel as
channelId; - the Discord author as
senderIdandsenderKind: "user"; markdownfor non-empty text, orimage/filefor attachment-only input;- attachments mapped to
ChannelAttachmentDto; and metadatacontainingchannelMessageIdandguildId.
The implementation does not currently populate channelType, mentions, embeds, or replyToId in that normalized metadata. The old page's broader Discord metadata table must not be treated as current behavior.
Direct shared ingress versus compatibility envelope
When a direct port is present, the plugin calls ChannelIngressPort.receive() with the complete normalized ingress DTO. In the current gateway-hosted path, the plugin instead signs a compatibility envelope and emits one of these Socket.IO events:
| Shared operation | Compatibility event | Current envelope boundary |
|---|---|---|
message.send |
message |
Correlation ID, native Discord message ID, guild/channel/user IDs, conversation ID, content, optional thread ID, and attachments. |
approval.create |
discord:approve |
The same signed Discord identity and route fields, carrying the approval command. |
session.stop |
discord:stop |
The same signed Discord identity and route fields, carrying the stop command. |
The signature is HMAC-SHA-256 over the ordered envelope payload using the injected Discord service token. The token is used for service authentication and is not part of the protocol payload.
Gateway validation and dispatch
The gateway exposes the /chat Socket.IO namespace. A Discord connection authenticates with discordServiceToken; ordinary clients use a BetterAuth session. For Discord service messages, the gateway:
- Verifies the signed envelope with
DISCORD_SERVICE_TOKEN. - Re-applies the configured guild, channel, and user allowlists.
- Resolves the configured binding and operation role.
- Checks that the conversation ID matches the bound logical-agent instance and channel/thread.
- Rejects a repeated native Discord message ID through the bounded replay protector.
- Reconstructs a gateway
ChatSocketMessageDtocontaining the conversation ID, content, and validated attachments. - Uses the configured Discord service principal/tenant for ordinary chat dispatch and the paired
mosaicUserIdfor privileged approval/stop policy where required. - Selects the trusted
agentConfigIdfrom the binding and verifies that the provisioned agent name matches the binding's logical-agent instance.
This path is intentionally described as compatibility: the gateway receives a signed Discord envelope and reconstructs chat input; it does not currently receive a complete ChannelIngressDto from the host registry.
Thread and conversation behavior
| Inbound case | Current route and side effect |
|---|---|
| Authorized untagged message in a parent channel | Uses the parent channel as the response target; no thread is created. |
| Authorized bot mention in a parent channel | Creates a public thread, or reuses the thread already attached to that message, and routes the response to that thread. |
| Authorized follow-up in an existing thread | Authorizes against the configured parent channel and keeps the existing thread; it does not create a nested thread. |
/approve or /stop <approval> |
Uses the current parent/thread durable session and does not create a new topic. |
| Requested thread creation fails | Does not dispatch the message, because the requested response target cannot be honored. |
Discord egress and health
DiscordPlugin.send() is a typed egress implementation, even though the current gateway does not wire it as a universal ChannelEgressPort. It:
- rejects a forged or route-misaligned conversation before looking up a Discord destination;
- rejects a missing destination with
destination_unavailable; - chunks text at a Discord-safe 1,900-character boundary;
- retries transient rate-limit, server, and network failures up to three attempts;
- derives one stable nonce per correlation/chunk and sends with Discord's enforced nonce option; and
- does not retry permanent delivery failures.
The plugin reports connected, degraded, or disconnected from Discord client readiness and gateway socket connectivity. The health method is tested without exposing provider or runtime state. Outbound delivery uses Discord's native send operation; code-content wrapping from the former page is not implemented.
Agent output reaches the plugin through the current agent:start/agent:text/agent:end Socket.IO events. The plugin buffers the text by conversation ID and calls its typed Discord egress method when the stream ends.
Draft: shared adapter registry and gateway wiring
Status: Draft / unimplemented.
The gateway does have a startup IChannelPlugin[] registry, but that registry is a lifecycle host for the current Discord and Telegram wrappers. It does not register OfficialChannelAdapter instances, inject ChannelIngressPort and ChannelEgressPort through a common gateway service, expose adapter health, or implement a dynamic ChannelRegistry with getAdapter, listAdapters, and healthAll semantics.
The former page's claim that adapters are already registered uniformly, or that new adapters can be added without channel-specific gateway branches, is not current. The gateway still has Discord-specific authentication, envelope, approval, stop, replay, and binding branches.
A future implementation may define a registry and host lifecycle, but that work must first specify:
- ownership and injection of ingress and egress ports;
- health and failure semantics;
- binding and credential loading boundaries;
- compatibility behavior for existing Socket.IO clients; and
- tests proving that an adapter cannot bypass gateway authorization or route validation.
Until then, this section is design context only.
Draft: Telegram shared-contract parity
Status: Raw legacy adapter exists; shared protocol parity and authenticated gateway participation are unimplemented/unproven.
The current Telegram plugin is not an OfficialChannelAdapter implementation. Its source currently:
- launches a Telegraf bot and a Socket.IO client;
- accepts only messages with a text field and ignores attachment-only messages;
- maps each Telegram chat ID to
telegram-<chatId>; - emits a raw
{ conversationId, content, role: "user" }object rather thanChannelIngressDto; - has no shared DTO import, channel binding, principal/role policy, native message ID, attachment mapping, route validation, or health method; and
- sends plain
sendMessageresponses in chunks, without the former page's claimed MarkdownV2, photo, or document handling.
The Telegram Socket.IO client does not provide the Discord service token or a BetterAuth session in its connection options. The source therefore does not establish participation in the gateway's current authenticated connection path. The package's test script uses --passWithNoTests, and no package test file is present in this checkout.
Future Telegram parity is draft work. It would need an explicit identity/authentication boundary, shared ingress normalization, binding and operation policy, route-safe egress, health reporting, and focused tests before this page could describe Telegram as an official shared-contract adapter.
Draft: Matrix integration
Status: Draft / unimplemented in the channel protocol.
No current gateway adapter, shared-port wiring, channel binding, identity resolver, room/conversation persistence boundary, or focused channel tests were found for the Matrix design described by the former page. The old Conduit choice, appservice registration, room and Space mappings, ghost users, encryption defaults, retention jobs, and agent-room behavior are therefore proposals, not current system behavior.
Those details must not be copied into implementation instructions or treated as deployment requirements. A future Matrix effort must independently decide and implement its homeserver/appservice boundary, authentication, route mapping, persistence, authorization, delivery, and tests.
Draft: channel identity linking
Status: Draft / unimplemented.
The shared contract carries an already-authorized ChannelAuthorizedPrincipalDto; it does not implement a generic channel-identity database or linking flow. Discord currently uses configuration-owned allowlists and pairings. A pairing may include a mosaicUserId for privileged operations, while ordinary Discord chat dispatch uses the configured service principal and tenant in the gateway.
No current evidence establishes the former page's proposed channel_identities table, OAuth/deep-link flow, anonymous-principal behavior, persistent Matrix session, or revocation endpoint. Those are not implied by the optional mosaicUserId field and must remain planned work until schema, auth, gateway, and adapter implementations exist together.
Draft: multi-surface conversation multiplexing
Status: Partial raw chat-session support exists; the proposed channel-protocol fanout architecture is unimplemented.
The gateway currently tracks client/conversation sessions and emits raw typed Socket.IO chat events. Focused gateway tests verify isolation of concurrent conversation streams sharing one socket. The chat event contract is ChatMessagePayload plus agent:* events, not ChannelMessageDto fanout.
There is no evidence in the cited current path for the former page's complete ConversationService plus Valkey pub/sub topology, canonical cross-surface ChannelMessageDto persistence, or Matrix fanout. Concurrent stream isolation must not be presented as multi-surface channel multiplexing.
A future multiplexing design must define canonical message ownership, subscription and fanout boundaries, replay/ordering behavior, conflict semantics, and per-surface authorization before it can become architecture guidance.
Current limitations and version boundary
The following are intentionally not claimed as current protocol policy:
- A semantic protocol version of
1.0.0.packages/types/package.jsoncurrently reports package version0.0.2, whilepackages/types/src/index.tsexportsVERSION = "0.0.0"; no migration machinery is present in the cited channel code. - A generic revoked-auth
ChannelDeliveryErrorcode. The current union contains onlyinvalid_route,destination_unavailable, anddelivery_failed. - Structured log records with a universal
{ channel, event, ... }schema. Current plugin logs are string-prefixed. - Universal adapter health monitoring by the gateway host. Discord exposes health; the
IChannelPluginhost does not. - Complete
ChannelEgressDtodelivery from the gateway to every channel. Discord's current gateway egress remains Socket.IO stream events followed by plugin-side delivery.
These limitations are evidence boundaries, not requests to change implementation in this documentation migration.
Evidence
Current-contract evidence:
packages/types/src/channel/channel.dto.ts— DTOs, enums, route fields, and metadata shape.packages/types/src/channel/channel-adapter.ts— adapter lifecycle, ingress/egress ports, and delivery errors.packages/types/src/channel/index.tsandpackages/types/src/index.ts— export surface and current packageVERSION.
Discord evidence:
plugins/discord/src/index.ts— native translation, authorization, thread routing, signed compatibility envelope, typed ingress/egress seam, retry, and health.plugins/discord/src/index.test.ts— direct ingress, routing/thread behavior, authorization ordering, attachments, route-safe egress, retries, chunking, and health.apps/gateway/src/chat/chat.gateway.ts—/chatnamespace, service/session authentication, signed-envelope reconstruction, trusted binding selection, and raw stream egress.apps/gateway/src/chat/chat.gateway-auth.ts— Discord service-token and BetterAuth session validation.apps/gateway/src/plugin/discord-ingress.security.spec.ts— signature, allowlist, replay, binding, attachment, approval, stop, and logical-agent checks.apps/gateway/src/__tests__/integration/tess-cross-surface.integration.test.ts— focused Discord-to-durable-session integration.
Hosting and compatibility evidence:
apps/gateway/src/plugin/plugin.interface.ts,plugin.module.ts, andplugin.service.ts— current lifecycle-only plugin host.packages/types/src/chat/events.ts— raw Socket.IO chat event contracts.apps/gateway/src/chat/chat.gateway-redaction.spec.ts— current per-client/per-conversation stream isolation evidence.
Telegram evidence:
plugins/telegram/src/index.ts— current raw Telegraf and Socket.IO behavior.plugins/telegram/package.json— package scripts, including--passWithNoTests.