Version: 1.0-draft
Status: Draft — subject to change before implementation
The Bridge Protocol allows external platform adapters written in any programming language to connect to cc-connect at runtime via WebSocket. This eliminates the requirement to write Go code and recompile the binary for every new platform integration.
┌──────────────────────────────────────────────────────┐
│ cc-connect │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────────┐ │
│ │ Telegram │ │ Feishu │ │ BridgePlatform │ │
│ │ (native) │ │ (native) │ │ (WebSocket) │ │
│ └─────┬──────┘ └─────┬──────┘ └───────┬────────┘ │
│ │ │ │ │
│ └──────────────┴────────────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ Engine │ │
│ └───────────┘ │
└──────────────────────────────────────────────────────┘
│ WebSocket
┌──────────┴───────────┐
│ │
┌──────────┴──────┐ ┌───────────┴─────┐
│ Python Adapter │ │ Node.js Adapter │
│ (WeChat, Line…) │ │ (Custom Chat…) │
└─────────────────┘ └─────────────────┘
The BridgePlatform is a built-in platform inside cc-connect that:
- Exposes a WebSocket endpoint for external adapters to connect.
- Translates WebSocket messages into
core.Platforminterface calls. - Routes engine replies back to the adapter over the same WebSocket connection.
ws://<host>:<port>/bridge/ws
The port and path are configured in config.toml:
[bridge]
enabled = true
port = 9810
path = "/bridge/ws" # optional, default "/bridge/ws"
token = "your-secret" # required for authenticationThe adapter must authenticate on connection using one of:
| Method | Example |
|---|---|
| Query parameter | ws://host:9810/bridge/ws?token=your-secret |
| Header | Authorization: Bearer your-secret |
| Header | X-Bridge-Token: your-secret |
Unauthenticated connections are rejected with HTTP 401.
Adapter cc-connect
│ │
│──── WebSocket Connect ──────────→│ (with token)
│ │
│──── register ──────────────────→│ (declare platform name & capabilities)
│←─── register_ack ──────────────│ (confirm or reject)
│ │
│←──→ message / reply exchange ──→│ (bidirectional)
│ │
│──── ping ──────────────────────→│ (keepalive, every 30s recommended)
│←─── pong ──────────────────────│
│ │
│──── close ─────────────────────→│ (graceful disconnect)
All messages are JSON objects with a required type field. The protocol uses newline-delimited JSON over WebSocket text frames (one JSON object per frame).
Must be the first message after connection. Declares the adapter identity and capabilities.
{
"type": "register",
"platform": "wechat",
"capabilities": ["text", "image", "file", "audio", "card", "buttons", "typing", "update_message", "preview"],
"metadata": {
"version": "1.0.0",
"description": "WeChat Official Account adapter"
}
}Fields:
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | yes | "register" |
platform |
string | yes | Unique platform name (lowercase, alphanumeric + hyphens). Used in session keys. |
capabilities |
string[] | yes | List of supported capabilities (see Capabilities). |
metadata |
object | no | Free-form metadata for logging/debugging. |
Delivers an incoming user message to the engine.
{
"type": "message",
"msg_id": "msg-001",
"session_key": "wechat:user123:user123",
"user_id": "user123",
"user_name": "Alice",
"content": "Hello, what can you do?",
"reply_ctx": "conv-abc-123",
"images": [],
"files": [],
"audio": null
}Fields:
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | yes | "message" |
msg_id |
string | yes | Platform-specific message ID for tracing. |
session_key |
string | yes | Unique session identifier. Format: {platform}:{scope}:{user}. The adapter defines how to compose this. |
user_id |
string | yes | User identifier on the platform. |
user_name |
string | no | Display name. |
content |
string | yes | Text content. |
reply_ctx |
string | yes | Opaque context string the adapter needs to route replies back. cc-connect echoes this in every reply. |
images |
Image[] | no | Attached images (see Image Object). |
files |
File[] | no | Attached files (see File Object). |
audio |
Audio | no | Voice message (see Audio Object). |
User clicked a button or selected an option on a card.
{
"type": "card_action",
"session_key": "wechat:user123:user123",
"action": "cmd:/new",
"reply_ctx": "conv-abc-123"
}Fields:
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | yes | "card_action" |
session_key |
string | yes | Session that triggered the action. |
action |
string | yes | The callback value from the button (e.g., "cmd:/new", "nav:/model", "act:/heartbeat pause"). |
reply_ctx |
string | yes | Reply context for routing the response. |
Acknowledges a preview start and returns a handle for subsequent updates.
{
"type": "preview_ack",
"ref_id": "preview-req-001",
"preview_handle": "platform-msg-id-789"
}Keepalive. cc-connect responds with pong.
{
"type": "ping",
"ts": 1710000000000
}Confirms or rejects registration.
{
"type": "register_ack",
"ok": true,
"error": ""
}A complete reply message to send to the user.
{
"type": "reply",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"content": "I can help you with coding tasks!",
"format": "text"
}Fields:
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | yes | "reply" |
session_key |
string | yes | Target session. |
reply_ctx |
string | yes | Echoed from the original message. |
content |
string | yes | Reply text content. |
format |
string | no | "text" (default) or "markdown". |
Streaming delta for real-time typing preview. Only sent if the adapter declared "preview" capability.
{
"type": "reply_stream",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"delta": "partial content...",
"full_text": "accumulated full text so far...",
"preview_handle": "platform-msg-id-789",
"done": false
}| Field | Type | Description |
|---|---|---|
delta |
string | New text since last stream message. |
full_text |
string | Full accumulated text. Adapters can use this for "replace entire message" updates. |
preview_handle |
string | Handle returned by preview_ack. Empty on first stream message. |
done |
bool | true on the final stream message. |
Requests the adapter to create an initial preview message (for streaming).
{
"type": "preview_start",
"ref_id": "preview-req-001",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"content": "Thinking..."
}The adapter should send the message and respond with preview_ack containing the platform message ID.
Requests the adapter to edit an existing message in-place. Used for streaming preview updates.
{
"type": "update_message",
"session_key": "wechat:user123:user123",
"preview_handle": "platform-msg-id-789",
"content": "Updated text content..."
}Requests the adapter to delete a message (e.g., cleaning up preview messages).
{
"type": "delete_message",
"session_key": "wechat:user123:user123",
"preview_handle": "platform-msg-id-789"
}Send a structured card to the user. Only sent if the adapter declared "card" capability; otherwise cc-connect falls back to reply with card.RenderText().
{
"type": "card",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"card": {
"header": {
"title": "Model Selection",
"color": "blue"
},
"elements": [
{
"type": "markdown",
"content": "Choose a model:"
},
{
"type": "actions",
"buttons": [
{"text": "GPT-4", "btn_type": "primary", "value": "cmd:/model switch gpt-4"},
{"text": "Claude", "btn_type": "default", "value": "cmd:/model switch claude"}
],
"layout": "row"
},
{
"type": "divider"
},
{
"type": "note",
"text": "Current: gpt-4"
}
]
}
}See Card Schema for the full card element reference.
Send a message with inline buttons. Only sent if the adapter declared "buttons" capability.
{
"type": "buttons",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"content": "Allow tool execution: bash(rm -rf /tmp/old)?",
"buttons": [
[
{"text": "✅ Allow", "data": "perm:req-123:allow"},
{"text": "❌ Deny", "data": "perm:req-123:deny"}
]
]
}buttons is a 2D array: each inner array is one row.
Requests the adapter to show a typing indicator.
{
"type": "typing_start",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123"
}Requests the adapter to hide the typing indicator.
{
"type": "typing_stop",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123"
}Send a voice/audio message. Only sent if the adapter declared "audio" capability.
{
"type": "audio",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"data": "<base64-encoded-audio>",
"format": "mp3"
}Send an image to the user. Only sent if the adapter declared "image" capability.
{
"type": "image",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"data": "<base64-encoded-image>",
"mime_type": "image/png",
"file_name": "screenshot.png"
}Send a file to the user. Only sent if the adapter declared "file" capability.
{
"type": "file",
"session_key": "wechat:user123:user123",
"reply_ctx": "conv-abc-123",
"data": "<base64-encoded-file>",
"mime_type": "application/pdf",
"file_name": "report.pdf"
}Response to ping.
{
"type": "pong",
"ts": 1710000000000
}Notify the adapter of a server-side error.
{
"type": "error",
"code": "session_not_found",
"message": "No active session for the given key"
}| Capability | Description | Enables |
|---|---|---|
text |
Basic text messaging (required) | message, reply |
image |
Sending/receiving images | message.images, image reply |
file |
Sending/receiving files | message.files, file reply |
audio |
Sending/receiving voice messages | message.audio, audio reply |
card |
Structured rich card rendering | card reply |
buttons |
Inline clickable buttons | buttons reply, card_action |
typing |
Typing indicator | typing_start, typing_stop |
update_message |
Edit existing messages | update_message |
preview |
Streaming preview (requires update_message) |
preview_start, reply_stream |
delete_message |
Delete messages | delete_message |
reconstruct_reply |
Can reconstruct reply context from session_key | Enables cron/heartbeat messages |
If a capability is not declared, cc-connect will automatically degrade:
- No
card→ cards are rendered as plain text viaRenderText(). - No
buttons→ buttons are omitted or rendered as text hints. - No
preview→ streaming is disabled; only the final reply is sent. - No
typing→ typing indicators are skipped.
{
"mime_type": "image/png",
"data": "<base64-encoded>",
"file_name": "screenshot.png"
}{
"mime_type": "application/pdf",
"data": "<base64-encoded>",
"file_name": "report.pdf"
}{
"mime_type": "audio/ogg",
"data": "<base64-encoded>",
"format": "ogg",
"duration": 5
}A card consists of an optional header and a list of elements:
{
"header": {
"title": "Card Title",
"color": "blue"
},
"elements": [ ... ]
}Supported colors: blue, green, red, orange, purple, grey, turquoise, violet, indigo, wathet, yellow, carmine.
Markdown
{"type": "markdown", "content": "**Bold** and _italic_"}Divider
{"type": "divider"}Actions (Button Row)
{
"type": "actions",
"buttons": [
{"text": "Click Me", "btn_type": "primary", "value": "cmd:/do-something"}
],
"layout": "row"
}btn_type: "primary", "default", "danger".
layout: "row" (default), "equal_columns".
List Item (Description + Button)
{
"type": "list_item",
"text": "GPT-4 — Most capable model",
"btn_text": "Select",
"btn_type": "primary",
"btn_value": "cmd:/model switch gpt-4"
}Select (Dropdown)
{
"type": "select",
"placeholder": "Choose a model",
"options": [
{"text": "GPT-4", "value": "cmd:/model switch gpt-4"},
{"text": "Claude", "value": "cmd:/model switch claude"}
],
"init_value": "cmd:/model switch gpt-4"
}Note (Footnote)
{
"type": "note",
"text": "Tip: use /help to see all commands",
"tag": "optional-machine-tag"
}Session keys follow the pattern:
{platform}:{scope}:{user_id}
- platform: The
platformname from registration (e.g.,wechat). - scope: A grouping scope — could be a group/channel ID, or the same as
user_idfor 1-on-1 chats. - user_id: The unique user identifier.
Examples:
wechat:user123:user123— personal DMwechat:group456:user123— user in a group chatmatrix:room789:alice— Matrix room
The adapter is responsible for constructing consistent session keys.
In addition to the WebSocket protocol for real-time messaging, the Bridge Server exposes HTTP REST endpoints on the same port for session management. This allows adapters to list, create, switch, and delete sessions without requiring the separate Management API.
The same token used for WebSocket connections applies to REST endpoints:
| Method | Example |
|---|---|
| Header | Authorization: Bearer your-secret |
| Query param | ?token=your-secret |
All responses use the same envelope as the Management API:
{"ok": true, "data": { ... }}
{"ok": false, "error": "message"}All endpoints are relative to the Bridge Server base URL (e.g., http://localhost:9810).
Lists sessions for a given session key prefix (typically platform:chatId).
Query parameters:
| Param | Type | Required | Description |
|---|---|---|---|
session_key |
string | yes | The session key to list sessions for (e.g., wechat:user123:user123). |
Response:
{
"ok": true,
"data": {
"sessions": [
{
"id": "s1",
"name": "default",
"history_count": 12
},
{
"id": "s2",
"name": "work",
"history_count": 5
}
],
"active_session_id": "s1"
}
}Creates a new named session.
Request body:
{
"session_key": "wechat:user123:user123",
"name": "work"
}| Field | Type | Required | Description |
|---|---|---|---|
session_key |
string | yes | Session key for the user. |
name |
string | no | Human-readable session name. Defaults to "default". |
Response:
{
"ok": true,
"data": {
"id": "s3",
"name": "work",
"message": "session created"
}
}Returns session detail with message history.
Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
session_key |
string | (required) | Session key to identify the project context. |
history_limit |
int | 50 | Max history entries to return. |
Response:
{
"ok": true,
"data": {
"id": "s1",
"name": "default",
"history": [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi! How can I help?"}
]
}
}Deletes a session and its history.
Query parameters:
| Param | Type | Required | Description |
|---|---|---|---|
session_key |
string | yes | Session key to identify the project context. |
Response:
{
"ok": true,
"data": {
"message": "session deleted"
}
}Switches the active session for a session key.
Request body:
{
"session_key": "wechat:user123:user123",
"target": "s2"
}| Field | Type | Required | Description |
|---|---|---|---|
session_key |
string | yes | Session key. |
target |
string | yes | Session ID or name to switch to. |
Response:
{
"ok": true,
"data": {
"message": "session switched",
"active_session_id": "s2"
}
}If the WebSocket connection drops, the adapter should:
- Wait with exponential backoff (starting at 1s, max 60s).
- Reconnect and send a new
registermessage. - Resume normal operation — cc-connect maintains session state independently of the connection.
Messages within a single WebSocket connection are ordered. cc-connect processes adapter messages sequentially per session key.
- Ping interval: Adapters should send
pingat least every 30 seconds. - Connection timeout: cc-connect closes idle connections after 90 seconds without a ping.
- Reply timeout: If an agent takes too long, cc-connect may send an error reply. The adapter does not need to handle this specially.
[bridge]
enabled = true
port = 9810
token = "a-strong-random-secret"
# Optional: restrict which adapters can connect (by platform name).
# Default: allow all registered adapters.
# allow_platforms = ["wechat", "matrix"]No per-adapter project configuration is needed — adapters are associated with the default project or specify a project field in the register message to bind to a specific project.
When building an adapter, follow these guidelines:
- Keep it stateless — the adapter should be a thin translation layer. All session state lives in cc-connect.
- Handle reconnection — network failures are normal. Implement exponential backoff.
- Declare capabilities honestly — only declare capabilities your platform actually supports.
- Use
reply_ctxfaithfully — always echo back thereply_ctxfrom the original message. - Base64 for binary data — images, files, and audio are transferred as base64-encoded strings.
- Log errors, don't crash — if you receive an unknown message type, log it and continue.
import asyncio
import json
import websockets
async def main():
uri = "ws://localhost:9810/bridge/ws?token=your-secret"
async with websockets.connect(uri) as ws:
# 1. Register
await ws.send(json.dumps({
"type": "register",
"platform": "my-chat",
"capabilities": ["text", "buttons"]
}))
ack = json.loads(await ws.recv())
assert ack["ok"], f"Registration failed: {ack['error']}"
# 2. Start message loop
async def recv_loop():
async for raw in ws:
msg = json.loads(raw)
if msg["type"] == "reply":
send_to_chat_platform(msg["reply_ctx"], msg["content"])
elif msg["type"] == "buttons":
send_buttons_to_chat(msg["reply_ctx"], msg["content"], msg["buttons"])
# ... handle other types
async def send_loop():
while True:
chat_msg = await get_next_chat_message()
await ws.send(json.dumps({
"type": "message",
"msg_id": chat_msg.id,
"session_key": f"my-chat:{chat_msg.user_id}:{chat_msg.user_id}",
"user_id": chat_msg.user_id,
"user_name": chat_msg.user_name,
"content": chat_msg.text,
"reply_ctx": chat_msg.conversation_id
}))
await asyncio.gather(recv_loop(), send_loop())
asyncio.run(main())The protocol version is declared in the register message via metadata.protocol_version. The current version is 1. cc-connect will reject connections with incompatible versions and respond with a register_ack containing an error.
{
"type": "register",
"platform": "my-chat",
"capabilities": ["text"],
"metadata": {
"protocol_version": 1
}
}