Skip to main content
A channel is the bridge between an external messaging platform and Curia’s internal message bus. Every channel — CLI, HTTP, Email, Signal — follows the same pattern: receive platform messages, normalize them into bus events, and deliver outbound responses back through the platform API. Channels implement a small interface and are tracked in the channel registry, so they can be installed, enabled, and credentialed like skills.

The Channel interface

A channel is a class implementing this interface:
interface Channel {
  readonly name: string;          // 'email' | 'signal' | 'http' | 'cli' | 'my-channel'
  readonly isToggleable: boolean; // false only for http and cli (always-on safeguard)
  start(): Promise<void>;         // connect, begin listening, subscribe to outbound
  stop(): Promise<void>;          // graceful, idempotent teardown
}
There is no send() method on the interface. Inside start(), the channel publishes inbound messages and subscribes to outbound ones; outbound delivery for email and Signal is routed through the shared outbound gateway, while simple channels (like CLI) write directly.

Implementation steps

1

Implement the Channel interface

Create a new directory under src/channels/<name>/ and a class that implements Channel (from src/channels/channel.ts):
src/channels/my-channel/
  my-channel-adapter.ts
Register on the bus at the channel layer — the hard security boundary limits a channel to publishing inbound.message and subscribing to outbound.message. It cannot invoke skills, read memory, or trigger agent tasks.
2

Add a catalog descriptor

Add a ChannelDescriptor to src/channels/catalog.ts so the registry knows about your channel and what credentials it needs:
{
  name: 'my-channel',
  description: 'My platform integration',
  isToggleable: true,
  credentialFields: [
    { key: 'api_token', label: 'API token', secret: true },
  ],
  requiredSecretKeys: ['api_token'],
}
Each credential is stored in the vault under channel.<name>.<field> — here, channel.my-channel.api_token. The registry will not let the channel be enabled until every requiredSecretKeys entry resolves.
3

Handle inbound messages

When your platform delivers a message, normalize it into an inbound.message event and publish it on the bus:
bus.publish({
  type: 'inbound.message',
  payload: {
    channel: 'my-channel',
    channelTrust: 'medium',
    conversationId: 'my-channel:<unique-conversation-id>',
    sender: {
      displayName: 'Sender Name',
      channelIdentity: { type: 'my-channel', value: 'sender-id' },
    },
    text: messageContent,
  },
});
The channelTrust field declares your channel’s trust level. Choose based on the identity guarantees your platform provides:
Trust levelWhen to use
highStrong identity verification (local access, E2E encryption with phone verification)
mediumToken-based or OAuth authentication
lowWeak identity guarantees (email addresses can be spoofed)
4

Handle outbound messages

In start(), subscribe to outbound.message at the channel layer and deliver messages addressed to your channel:
bus.subscribe('outbound.message', 'channel', async (event) => {
  if (event.payload.channelId !== 'my-channel') return;
  await myPlatformApi.sendMessage(
    event.payload.conversationId,
    event.payload.content,
  );
});
Filter by channelId so you only handle messages destined for your channel.
5

Add configuration and wire it up

Add any non-secret settings (polling intervals, endpoints) to config/default.yaml; credentials go in the vault via the catalog, not in YAML. Then add your channel to the bootstrap sequence in src/index.ts so it starts with the rest of the system.

What the dispatch layer handles for you

You don’t need to implement any of these in your adapter — they’re handled by the dispatch layer after your inbound.message is published:
  • Contact resolution — the dispatcher resolves the sender to a contact record
  • Trust scoring — your channelTrust value is combined with contact confidence and injection risk
  • Rate limiting — global and per-sender limits are enforced
  • Prompt injection scanning — inbound text is checked against detection patterns
  • PII redaction — outbound messages are redacted based on channel-specific policies

Testing

Write tests for your adapter covering:
  • Inbound message normalization (platform format → bus event)
  • Outbound message delivery (bus event → platform API call)
  • Authentication and connection handling
  • Error recovery (platform API failures, connection drops)
Integration tests should use a real bus instance to verify that your events flow through the full dispatch pipeline correctly.

How channels work

Channel routing, trust levels, and the dispatch pipeline in detail.

Architecture

How channel adapters fit into the five-layer bus architecture.