docs: add channel connector pairing design
This commit is contained in:
@ -0,0 +1,384 @@
|
||||
# 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
|
||||
-> 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.
|
||||
|
||||
## 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/OpenClaw-style setup uses a local plugin installer 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, launches connector processes, 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.
|
||||
|
||||
`ExternalConnectorProcess` is an optional local process for platforms whose SDK or login behavior is better isolated outside the Python backend. It talks to Beaver through a narrow control and message protocol.
|
||||
|
||||
## 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 external adapter
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## 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 an external connector process.
|
||||
|
||||
Recommended first implementation:
|
||||
|
||||
- connector kind: `weixin`
|
||||
- setup mode: local plugin command plus QR login.
|
||||
- external process: required.
|
||||
- runtime adapter: external bridge adapter that receives normalized events from the connector.
|
||||
|
||||
Required setup checks:
|
||||
|
||||
- local connector installed.
|
||||
- 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.
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user