# Channel Connectors And Pairing Design

Date: 2026-06-02

## Goal

Add a first-class connection layer above Beaver's channel runtime so users can connect messaging platforms through plugin, QR, OAuth, token, or app-credential flows instead of editing static channel JSON by hand.

This design reframes platform channels as two cooperating layers:

```text
ChannelConnector
-> install / auth / QR / OAuth / credential validation / login state
-> ChannelConnectionStore
-> ChannelRuntime
-> ChannelAdapter or ExternalConnectorChannel
-> MessageBus
-> AgentService
```

The existing `ChannelRuntime`, `MessageBus`, `ChannelManager`, and `ChannelAdapter` contracts remain the message routing core. The new connector layer owns user-visible setup and connection lifecycle. For platforms backed by predeclared sidecar services, Beaver should expose the sidecar to the runtime as an `ExternalConnectorChannel` rather than a Beaver-owned platform protocol adapter.

## Why This Is Required

The current channel design assumes a channel is already configured before the backend starts. That is enough for local development and simple webhook/token channels, but it does not match real platform onboarding:

- Feishu/Lark now has a Channel SDK pattern that packages bot channel setup, WebSocket or webhook transport, event handling, and replies around an installed app identity.
- Weixin personal-account setup uses a docker-compose predeclared sidecar connector plus QR login and persistent login state.
- Terminal devices need pairing or device registration; a raw `peer_id` connect frame is not enough for a real deployment.
- Even simple token platforms such as Telegram need a UI flow for token entry, validation, status, revoke, and restart.

So Beaver needs a connection lifecycle layer. Adapters should not be responsible for prompting the user, installing packages, storing long-lived credentials, or deciding whether an unknown device is allowed to bind.

## Non-Goals

- Do not replace `ChannelRuntime`, `MessageBus`, `ChannelManager`, or `AgentService`.
- Do not make every connector a Node sidecar. Node sidecars are allowed when the official or practical SDK path requires them.
- Do not implement every channel in this phase.
- Do not build a plugin marketplace in this phase.
- Do not store platform secrets in plain channel config when a credential store is available.
- Do not let external connector code call `AgentService` directly.

## Core Terms

`ChannelConnection` is the user-visible connection instance. Examples: "Weixin personal account", "Lark workspace bot", "Telegram main bot", "Desk terminal".

`ChannelConnector` is the setup and lifecycle controller for one platform family. It starts pairing sessions, validates credentials, checks preconfigured connector endpoints when needed, handles reconnects, and emits runtime channel config.

`ChannelAdapter` is the message transport adapter used by `ChannelRuntime`. It receives normalized inbound messages and sends outbound replies. It does not own onboarding.

`ExternalConnectorChannel` is the runtime channel object used when a platform protocol lives outside the Python backend. It implements the same `start()`, `stop()`, and `send()` contract as an adapter, but its `send()` method calls an external connector HTTP API and inbound messages enter Beaver through a connector bridge endpoint.

`ExternalConnectorProcess` is an optional preconfigured service for platforms whose SDK or login behavior is better isolated outside the Python backend. For Weixin, this process is a docker-compose predeclared sidecar service. Beaver must not dynamically create containers or require Docker socket access.

## Data Model

Add a durable connection store under the backend workspace:

```python
@dataclass
class ChannelConnection:
    connection_id: str
    owner_user_id: str | None
    channel_id: str
    kind: str
    mode: str
    display_name: str
    account_id: str
    status: str
    auth_type: str
    credentials_ref: str | None
    connector_ref: str | None
    pairing_session_id: str | None
    runtime_config: dict[str, Any]
    capabilities: list[str]
    created_at: str
    updated_at: str
    last_seen_at: str | None
    last_error: str | None
```

`status` values:

- `draft`: setup has started but no credentials are usable.
- `pairing`: waiting for QR scan, OAuth callback, device approval, or token validation.
- `connected`: credentials are valid and the runtime channel can start.
- `running`: the runtime adapter or external connector is active.
- `degraded`: partially working, for example inbound works but media upload failed.
- `error`: connection cannot start or authenticate.
- `revoked`: user or platform revoked the connection.

Credential material should live behind `credentials_ref`, not inline in `ChannelConnection`. For the first local implementation, the reference may point to an encrypted file or a restricted JSON store. The interface should still look like a credential vault so AuthZ or a real secret backend can replace it later.

## Connector Contract

Every connector implements a setup contract:

```python
class ChannelConnector(Protocol):
    kind: str

    async def start_pairing(request: StartPairingRequest) -> PairingSession
    async def complete_pairing(event: PairingEvent) -> ChannelConnection
    async def validate(connection_id: str) -> ValidationResult
    async def materialize_runtime(connection_id: str) -> ChannelRuntimeSpec
    async def revoke(connection_id: str) -> None
```

`materialize_runtime()` returns the adapter-ready config:

```python
@dataclass
class ChannelRuntimeSpec:
    channel_id: str
    kind: str
    mode: str
    account_id: str
    display_name: str
    config: dict[str, Any]
    secrets_ref: str | None
    external_endpoint: str | None
```

The runtime may still internally use `ChannelConfig`, but the source of truth becomes `ChannelConnectionStore`, not only static `BeaverConfig.channels`.

## Control APIs

Add backend APIs for the connection UI:

```text
GET    /api/channel-connectors
GET    /api/channel-connections
POST   /api/channel-connections
GET    /api/channel-connections/{connection_id}
POST   /api/channel-connections/{connection_id}/pairing/start
POST   /api/channel-connections/{connection_id}/pairing/complete
POST   /api/channel-connections/{connection_id}/validate
POST   /api/channel-connections/{connection_id}/start
POST   /api/channel-connections/{connection_id}/stop
POST   /api/channel-connections/{connection_id}/revoke
GET    /api/channel-connections/{connection_id}/events
```

The existing `/api/channels` status endpoint can keep reporting runtime adapter status, but the UI should prefer `/api/channel-connections` for setup state.

## UI Flow

The status page becomes a channel connection page:

```text
Add Channel
-> choose platform
-> connector-specific setup form
-> QR/OAuth/token/app credential validation
-> connection status
-> start runtime channel
-> test message or platform health check
```

The UI must distinguish:

- setup state: pairing, credential validation, revoked.
- runtime state: adapter running, disconnected, outbound failed.
- platform state: QR expired, app not installed, permission missing, token invalid.

This avoids the current problem where all failures collapse into adapter startup errors.

## External Connector Process

Some channels should run through an external process:

```text
ExternalConnectorProcess
-> Beaver connector control API
-> local Unix/TCP/WebSocket bridge
-> ChannelRuntime ExternalConnectorChannel
```

The external process must not receive permanent backend admin credentials through QR codes or copied commands. It should receive a short-lived pairing token with a narrow scope:

```text
scope: channel:pair
kind: weixin
expires_in: 10 minutes
one_time: true
```

After pairing, Beaver stores the resulting connection credentials and gives the connector a renewable connection token scoped to that connection only. For docker-compose sidecars, that token is passed through the connector HTTP API or service configuration agreed for that sidecar; Beaver does not create or restart the sidecar container.

## Per-Channel Assessment

### Feishu / Lark

Feishu/Lark should be a first-class connector, not only a static adapter.

Recommended first implementation:

- connector kind: `feishu`
- setup fields: domain, app id, app secret, connection mode.
- default mode: WebSocket long connection.
- optional mode: webhook.
- runtime adapter: may be Python if coverage is sufficient, or an external Node connector when using official Channel SDK behavior.

Required setup checks:

- app credentials are present.
- bot/event permissions are configured.
- event subscription mode is valid.
- bot identity can be resolved.
- a test direct message or event subscription health check can run when available.

The connector should expose both "manual app credential setup" and future "install from app template" paths. The manual path is enough for the first Beaver release.

### Weixin

Weixin should use a docker-compose predeclared sidecar connector.

Recommended first implementation:

- connector kind: `weixin`
- setup mode: Beaver calls the sidecar HTTP API to start QR login and poll pairing state.
- external process: required, predeclared in docker-compose, and never dynamically created by Beaver.
- runtime channel: `ExternalConnectorChannel`.

Required setup checks:

- sidecar base URL is configured.
- sidecar health endpoint responds.
- connector version is compatible with Beaver.
- QR session is pending, scanned, confirmed, expired, or failed.
- login state is stored behind `credentials_ref`.
- connector heartbeat is visible.

The sidecar owns Weixin protocol handling, QR login, inbound receive, outbound send, and login-state persistence. Beaver owns connector setup state, bridge API validation, message normalization boundaries, runtime dedupe, and outbound HTTP calls to the sidecar `/send` API.

The agreed runtime flow is:

```text
Weixin sidecar connector
-> Beaver connector bridge endpoint
-> ChannelRuntime.accept_inbound()
-> MessageBus
-> AgentService

AgentService
-> MessageBus outbound
-> ExternalConnectorChannel.send()
-> Weixin sidecar connector /send
```

Group delivery remains best-effort. The connector must surface group capability separately from direct message capability.

### Telegram

Telegram can be implemented as an internal connector plus internal adapter.

Recommended first implementation:

- setup mode: bot token entry.
- validation: call Telegram `getMe`.
- runtime mode: polling by default, webhook optional.
- no external process required.

The UI still treats it as a connector so users can add, validate, revoke, and restart it without editing JSON.

### QQBot

QQBot should start as an internal connector with official gateway credentials.

Recommended first implementation:

- setup fields: app id, client secret, intents or permission hints.
- runtime mode: WebSocket gateway.
- validation: token exchange or gateway auth dry run when available.

If SDK/runtime behavior later becomes easier outside Python, this connector can move to an external process without changing the runtime message contract.

### Terminal

Terminal should move from raw `peer_id` to pairing.

Recommended first implementation:

- UI creates a terminal pairing session.
- Beaver displays a command or QR/setup code.
- device connects with one-time pairing token.
- Beaver binds a stable device identity to a `ChannelConnection`.
- subsequent WebSocket `connect` frames authenticate as the bound device.

The message protocol can keep `connect`, `connected`, `message`, `ack`, and assistant `message`, but production connections must include an authenticated device token.

## Message Flow After Pairing

Once a connection is paired, the message path stays unchanged:

```text
platform or device
-> connector transport
-> ChannelAdapter
-> ChannelRuntime.accept_inbound()
-> MessageBus.inbound
-> AgentService.handle_inbound_message()
-> MessageBus.outbound
-> ChannelManager.dispatch_outbound()
-> ChannelAdapter.send()
-> connector transport
-> platform or device
```

This is intentionally conservative. Pairing changes how a channel becomes trusted and running; it does not change the agent loop.

## Access Control

Connection setup requires a Beaver user or backend owner identity. The connector layer decides who may create, view, revoke, or start a connection.

Inbound platform messages still use adapter-level policy:

- `open`: accept platform scope.
- `allowlist`: accept only known users/groups.
- `disabled`: ignore that scope.

The important change is that allowlists belong to the connection settings, not ad hoc adapter config only.

## Error Handling

Pairing errors:

- expired pairing token.
- QR not scanned before timeout.
- OAuth callback state mismatch.
- platform permission missing.
- credentials validation failed.

Runtime errors:

- adapter startup failed.
- connector process unavailable.
- heartbeat missed.
- inbound normalization failed.
- outbound delivery failed.

Each event should be recorded against `connection_id` and, when available, `channel_id` and `session_id`.

## Security Requirements

- Pairing tokens are short-lived, one-time, and scoped to one connector kind.
- QR codes never embed permanent backend credentials.
- External connector processes do not receive broad backend admin tokens.
- Revoking a connection invalidates connector tokens and stops the runtime channel.
- Stored platform credentials are referenced by `credentials_ref`.
- Event logs must not include raw secrets, tokens, QR payloads, or full platform credential responses.

## Relationship To Existing Channel Specs

The terminal WebSocket spec remains valid as a development transport spec, but production terminal setup must add pairing.

The chat platform adapter spec remains valid as a runtime adapter spec, but these statements should be revised before implementation:

- "Do not introduce a Node sidecar as the default channel architecture" should become "Use internal adapters by default, but allow external connector processes where platform SDK or login state requires them."
- "Pairing is out of scope for this phase" should become "Pairing is owned by the connector layer; adapters assume a materialized connection."
- Static `BeaverConfig.channels` should become a development override and backward-compatible import path, not the only source of runtime channels.

## Rollout

Implement in this order:

1. `ChannelConnectionStore`, connector registry, and connection status APIs.
2. Telegram connector as the simplest token-based setup path.
3. Terminal pairing to remove raw unauthenticated `peer_id` usage.
4. Feishu/Lark connector with WebSocket long-connection mode and credential validation.
5. Weixin external connector bridge with QR pairing.
6. QQBot connector after the common credential and gateway patterns are stable.

This order proves the common connector lifecycle with a low-risk token channel before adding QR and external process complexity.

## Testing

Add unit tests for:

- connection store create/update/revoke.
- pairing token expiry and one-time use.
- connector registry dispatch by kind.
- materializing runtime specs from connections.
- secret redaction in events.
- adapter runtime still receiving normalized `InboundMessage`.

Add integration-style tests with fake connectors for:

- successful token setup.
- QR expired and QR completed.
- external connector heartbeat loss.
- revoke stops runtime dispatch.

Live platform tests remain manual or gated behind explicit environment variables.

## Acceptance Criteria

- A user can add a channel connection without editing backend JSON.
- Beaver can show setup state separately from runtime adapter state.
- Telegram can validate a bot token and materialize a runtime channel.
- Terminal can bind through a one-time pairing flow.
- Feishu/Lark design allows official SDK or Node connector use when needed.
- Weixin design requires an external connector and QR login state.
- Existing channel runtime message flow remains bus-first and adapter-mediated.
