# 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](#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`](../../../packages/types/src/channel/channel.dto.ts), [`channel-adapter.ts`](../../../packages/types/src/channel/channel-adapter.ts)                                                                         | DTOs, ports, lifecycle health, and delivery error codes exported by `@mosaicstack/types`.                                                                  |
| Discord translation   | [`plugins/discord/src/index.ts`](../../../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`](../../../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`](../../../apps/gateway/src/chat/chat.gateway.ts), [`chat.gateway-auth.ts`](../../../apps/gateway/src/chat/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`](../../../apps/gateway/src/plugin/plugin.interface.ts), [`plugin.module.ts`](../../../apps/gateway/src/plugin/plugin.module.ts), [`plugin.service.ts`](../../../apps/gateway/src/plugin/plugin.service.ts) | Lifecycle-only `IChannelPlugin[]` hosting. This is not a universal `OfficialChannelAdapter` registry.                                                      |
| Telegram              | [`plugins/telegram/src/index.ts`](../../../plugins/telegram/src/index.ts), [`package.json`](../../../plugins/telegram/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`](../architecture/channel-protocol.md).

## Shared contract

`@mosaicstack/types` currently exports the channel types through [`packages/types/src/channel/index.ts`](../../../packages/types/src/channel/index.ts) and [`packages/types/src/index.ts`](../../../packages/types/src/index.ts).

The lifecycle and port seams are:

```typescript
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:

- `ChannelMessageDto` for normalized native messages and JSON-safe metadata;
- `ChannelAttachmentDto` for bounded external attachment references;
- `ChannelAuthorizedPrincipalDto` for the already-authorized channel actor and `viewer`/`operator`/`admin` role;
- `ChannelBindingDto` for configuration-owned workspace/channel/logical-agent binding;
- `ChannelResponseTargetDto` for a channel and optional thread;
- `ChannelConversationRouteDto` for binding, logical agent, stable conversation ID, authorization channel, and response target;
- `ChannelIngressDto` for correlation, native message ID, operation, principal, message, and route; and
- `ChannelEgressDto` for 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:

```text
<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:

1. ignores bot-authored and non-guild messages;
2. resolves a thread's actual parent text channel, while leaving normal category parents out of authorization;
3. applies default-deny guild, parent-channel, and user allowlists;
4. resolves configuration-owned `instanceId`/`agentConfigId` bindings and paired-user roles before thread creation or dispatch;
5. applies message and mention-thread limits before side effects;
6. creates/reuses a mention thread or preserves an existing thread target; and
7. maps the event to a `ChannelIngressDto` when an `ingressPort` dependency 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`](../../../apps/gateway/src/plugin/plugin.module.ts), so it uses the established compatibility path:

1. `start()` connects to `${DISCORD_GATEWAY_URL}/chat` with `auth.discordServiceToken`.
2. Normal sends emit a signed `message` envelope; approval and stop emit `discord:approve` and `discord:stop` envelopes.
3. The HMAC-SHA-256 signature covers the ordered Discord payload using `DISCORD_SERVICE_TOKEN`.
4. The gateway verifies the service token/signature, allowlists, binding/operation role, stable conversation ID, attachment bounds, and replay key before dispatch.
5. The gateway selects the binding's trusted `agentConfigId` and 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.
6. Agent output currently returns as raw Socket.IO `agent:start`, `agent:text`, and `agent:end` events. 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:

1. Normalize native messages to the shared DTOs and preserve native message ID, correlation ID, channel/thread identity, attachments, and response target.
2. Enforce native guild/room/channel/user/pairing/role policy before thread/room creation or gateway dispatch.
3. Select the logical agent from trusted configuration; never from a caller-controlled provider, model, harness, or route field.
4. Keep the route stable when the gateway changes runtime provider or harness.
5. Validate egress route and destination before sending; bound chunks, retries, and attachment metadata.
6. Return sanitized errors and expose non-throwing lifecycle health for ordinary disconnected state.
7. 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_TOKEN` and `TELEGRAM_GATEWAY_URL` through the gateway plugin factory, whose URL default is `http://localhost:14242`;
- accepts text messages only and ignores attachment-only messages;
- maps each Telegram `chat.id` to `telegram-<chatId>`;
- emits a raw `{ conversationId, content, role: "user" }` object rather than `ChannelIngressDto`;
- 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:

```bash
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`](../../../packages/types/src/channel/channel.dto.ts) — shared DTOs, operations, route fields, and metadata types.
- [`packages/types/src/channel/channel-adapter.ts`](../../../packages/types/src/channel/channel-adapter.ts) — adapter lifecycle, ingress/egress ports, and delivery errors.
- [`plugins/discord/src/index.ts`](../../../plugins/discord/src/index.ts) — native translation, auth ordering, compatibility envelope, route-safe egress, retry, and health.
- [`plugins/discord/src/index.test.ts`](../../../plugins/discord/src/index.test.ts) — focused Discord contract and behavior tests.
- [`apps/gateway/src/chat/chat.gateway.ts`](../../../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`](../../../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`](../../../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`](../../../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`](../../../apps/gateway/src/plugin/plugin.interface.ts), [`plugin.module.ts`](../../../apps/gateway/src/plugin/plugin.module.ts), and [`plugin.service.ts`](../../../apps/gateway/src/plugin/plugin.service.ts) — lifecycle-only host registry.
- [`plugins/telegram/src/index.ts`](../../../plugins/telegram/src/index.ts) and [`plugins/telegram/package.json`](../../../plugins/telegram/package.json) — raw Telegram behavior and no-test script.
- [Canonical channel protocol architecture](../architecture/channel-protocol.md) — shared contract and explicit current/draft boundary.
- [Discord ingress security](../../ADMIN-GUIDE/security/discord-ingress.md) — administrator-facing config and auth claims.
- [Discord conversations](../../USER-GUIDE/workflows/discord-conversations.md) — user-facing routing behavior.
- [Developer Guide](../README.md)
