17 KiB
Channel adapters
Status: Current shared channel types plus the Discord reference/compatibility implementation. A shared gateway adapter registry, Telegram parity, and Matrix channel integration remain unimplemented or unproven.
Last verified: 2026-08-10 against the source and focused tests listed in Evidence.
Audience: Developers implementing or reviewing channel integrations.
The gateway remains the policy and runtime boundary. Channel code translates native events, applies its native admission checks, and delivers normalized ingress/egress; it must not choose a provider, harness, process, or native runtime session on behalf of the gateway.
Current source boundaries
| Boundary | Current authority | Current claim |
|---|---|---|
| Shared contract | packages/types/src/channel/channel.dto.ts, channel-adapter.ts |
DTOs, ports, lifecycle health, and delivery error codes exported by @mosaicstack/types. |
| Discord translation | plugins/discord/src/index.ts |
First adapter implementing the shared lifecycle/egress seam, with an optional direct ingress port and a tested Socket.IO compatibility path. |
| Discord behavior | plugins/discord/src/index.test.ts |
Current allowlist, pairing, role, thread, route, attachment, replay-envelope, egress, retry, and health behavior. |
| Gateway compatibility | apps/gateway/src/chat/chat.gateway.ts, chat.gateway-auth.ts |
/chat Socket.IO service/session authentication, signed Discord envelope validation, trusted binding selection, raw chat dispatch, and raw stream egress. |
| Host registry | apps/gateway/src/plugin/plugin.interface.ts, plugin.module.ts, plugin.service.ts |
Lifecycle-only IChannelPlugin[] hosting. This is not a universal OfficialChannelAdapter registry. |
| Telegram | plugins/telegram/src/index.ts, package.json |
Raw legacy Telegraf/Socket.IO behavior only; no shared-contract or authenticated gateway parity. |
| Matrix | No current channel adapter boundary in the cited implementation | Matrix-related fleet/runtime code is not evidence of a gateway channel adapter. |
Executable source and tests outrank older architecture or quarantine pages. The canonical architecture summary is architecture/channel-protocol.md.
Shared contract
@mosaicstack/types currently exports the channel types through packages/types/src/channel/index.ts and packages/types/src/index.ts.
The lifecycle and port seams are:
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>;
}
The current DTO boundary includes:
ChannelMessageDtofor normalized native messages and JSON-safe metadata;ChannelAttachmentDtofor bounded external attachment references;ChannelAuthorizedPrincipalDtofor the already-authorized channel actor andviewer/operator/adminrole;ChannelBindingDtofor configuration-owned workspace/channel/logical-agent binding;ChannelResponseTargetDtofor a channel and optional thread;ChannelConversationRouteDtofor binding, logical agent, stable conversation ID, authorization channel, and response target;ChannelIngressDtofor correlation, native message ID, operation, principal, message, and route; andChannelEgressDtofor correlation, normalized output, and route.
Current operations are message.send, approval.create, and session.stop. Current delivery errors are invalid_route, destination_unavailable, and delivery_failed; there is no shared revoked-auth error code or executable protocol-version contract in this surface.
Stable route rule
The Discord adapter derives:
<logical-agent-id>:discord:<response-channel-id>
The binding address is derived from the configured guild, parent channel, and logical-agent instance. The route intentionally omits runtime provider, harness, model, process, and native runtime-session identifiers. Gateway provider/runtime layers own those identities; durable-session ownership applies only after explicit enrollment and is not implied by the external route.
This is a route-integrity rule, not proof that the gateway has a universal channel session API. A future adapter must derive its route from trusted configuration and native channel/thread identity; it must not accept a caller-selected logical agent or runtime target.
Current Discord implementation
Native ingress
DiscordPlugin currently:
- ignores bot-authored and non-guild messages;
- resolves a thread's actual parent text channel, while leaving normal category parents out of authorization;
- applies default-deny guild, parent-channel, and user allowlists;
- resolves configuration-owned
instanceId/agentConfigIdbindings and paired-user roles before thread creation or dispatch; - applies message and mention-thread limits before side effects;
- creates/reuses a mention thread or preserves an existing thread target; and
- maps the event to a
ChannelIngressDtowhen aningressPortdependency is supplied.
The normalized message uses channelName: "discord", the response target as channelId, senderKind: "user", markdown for non-empty text, image/file for attachment-only input, mapped attachments, and metadata containing the native channel message ID and guild ID. It does not currently claim a universal metadata shape for mentions, embeds, channel type, or replies.
Socket.IO compatibility path
The gateway-hosted plugin is currently constructed without a direct ChannelIngressPort in plugin.module.ts, so it uses the established compatibility path:
start()connects to${DISCORD_GATEWAY_URL}/chatwithauth.discordServiceToken.- Normal sends emit a signed
messageenvelope; approval and stop emitdiscord:approveanddiscord:stopenvelopes. - The HMAC-SHA-256 signature covers the ordered Discord payload using
DISCORD_SERVICE_TOKEN. - The gateway verifies the service token/signature, allowlists, binding/operation role, stable conversation ID, attachment bounds, and replay key before dispatch.
- The gateway selects the binding's trusted
agentConfigIdand verifies its name equals the logical-agent instance. It attempts normal conversation binding/persistence before dispatch, but the external route is not a UUID and no route-to-UUID mapping currently proves durable ordinary-chat history. - Agent output currently returns as raw Socket.IO
agent:start,agent:text, andagent:endevents. The plugin buffers the text and calls its typed Discord egress at stream end.
This compatibility path carries enough normalized identity to preserve current security and routing behavior, but it is not a gateway-produced ChannelIngressDto/ChannelEgressDto flow through a shared host registry. The gateway can continue live dispatch after persistence failure; adapter authors must not claim durable continuity until external routes map to UUID conversations and fresh-message restart tests pass.
Egress and health
DiscordPlugin.send() is a typed ChannelEgressPort implementation. It validates the route and message alignment before destination lookup, sends at a 1,900-character boundary, retries transient 429/5xx/network failures up to three times, uses one deterministic enforced nonce per correlation/chunk, and does not retry permanent failures. It reports connected, degraded, or disconnected from Discord client readiness and gateway socket connectivity.
The host wrapper currently exposes only name, start, stop, and optional project provisioning. It does not expose health(), inject the direct ingress port, produce ChannelEgressDto values from the gateway, or provide healthAll/getAdapter semantics.
Adapter authoring boundary
For a future official adapter, preserve these current architectural boundaries:
- Normalize native messages to the shared DTOs and preserve native message ID, correlation ID, channel/thread identity, attachments, and response target.
- Enforce native guild/room/channel/user/pairing/role policy before thread/room creation or gateway dispatch.
- Select the logical agent from trusted configuration; never from a caller-controlled provider, model, harness, or route field.
- Keep the route stable when the gateway changes runtime provider or harness.
- Validate egress route and destination before sending; bound chunks, retries, and attachment metadata.
- Return sanitized errors and expose non-throwing lifecycle health for ordinary disconnected state.
- Add happy-path and failure-path tests for authorization ordering, replay/idempotency, route integrity, native side effects, egress, reconnect, and health.
These are implementation constraints for future work, not evidence that the missing shared registry already exists.
Parity status
Telegram: raw legacy adapter, not shared parity
The current Telegram source:
- launches Telegraf and a Socket.IO client;
- reads
TELEGRAM_BOT_TOKENandTELEGRAM_GATEWAY_URLthrough the gateway plugin factory, whose URL default ishttp://localhost:14242; - accepts text messages only and ignores attachment-only messages;
- maps each Telegram
chat.idtotelegram-<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-safe egress, or health method; and
- has no package test file in this checkout; its script is
vitest run --passWithNoTests.
Its Socket.IO connection does not send the Discord service token or a BetterAuth session. A configured TELEGRAM_BOT_TOKEN therefore must not be described as an authenticated official channel. Shared Telegram parity requires a separate implementation and focused security/contract tests.
Matrix: no current channel adapter
No current gateway adapter, shared-port wiring, channel binding, identity resolver, persistence boundary, authentication path, or focused channel test establishes Matrix as a Mosaic channel. Matrix code elsewhere in the repository belongs to other transport/runtime work and must not be promoted into channel-adapter instructions without a separate contract and evidence.
Shared registry: draft/unimplemented
The current PLUGIN_REGISTRY is an array of IChannelPlugin lifecycle wrappers. It is not a registry of OfficialChannelAdapter instances and does not inject ChannelIngressPort/ChannelEgressPort, aggregate adapter health, or remove the gateway's Discord-specific auth/envelope/approval/stop/replay branches.
A future registry must specify binding and credential ownership, ingress/egress injection, health/error semantics, compatibility with the existing Socket.IO clients, and tests proving that adapters cannot bypass gateway authorization or route validation before it can be documented as current architecture.
Safe verification commands
The focused package commands used by the current Discord evidence are:
pnpm --filter @mosaicstack/types build
pnpm --filter @mosaicstack/types typecheck
pnpm --filter @mosaicstack/discord-plugin typecheck
pnpm --filter @mosaicstack/discord-plugin lint
pnpm --filter @mosaicstack/discord-plugin test
cd apps/gateway && pnpm exec vitest run \
src/plugin/discord-ingress.security.spec.ts \
src/chat/chat.gateway-redaction.spec.ts \
src/__tests__/integration/tess-cross-surface.integration.test.ts
These commands do not require starting Gateway, Discord, Telegram, Matrix, a queue, or a database. The Discord package test includes its configured coverage thresholds; the gateway command is a focused Vitest run rather than a claim of full repository integration coverage.
Evidence
packages/types/src/channel/channel.dto.ts— shared DTOs, operations, route fields, and metadata types.packages/types/src/channel/channel-adapter.ts— adapter lifecycle, ingress/egress ports, and delivery errors.plugins/discord/src/index.ts— native translation, auth ordering, compatibility envelope, route-safe egress, retry, and health.plugins/discord/src/index.test.ts— focused Discord contract and behavior tests.apps/gateway/src/chat/chat.gateway.ts— service/session auth, signed envelope validation, replay, trusted agent selection, and raw stream events.apps/gateway/src/chat/chat.gateway-auth.ts— timing-safe Discord service-token and BetterAuth session checks.apps/gateway/src/plugin/discord-ingress.security.spec.ts— gateway security and privileged-operation tests.apps/gateway/src/__tests__/integration/tess-cross-surface.integration.test.ts— control-flow test with explicit durable-session pre-enrollment; not ordinary Discord persistence evidence.apps/gateway/src/plugin/plugin.interface.ts,plugin.module.ts, andplugin.service.ts— lifecycle-only host registry.plugins/telegram/src/index.tsandplugins/telegram/package.json— raw Telegram behavior and no-test script.- Canonical channel protocol architecture — shared contract and explicit current/draft boundary.
- Discord ingress security — administrator-facing config and auth claims.
- Discord conversations — user-facing routing behavior.
- Developer Guide