Bridge Platform Protocol Specification

April 23, 2026 · View on GitHub

Version: 1.0-draft
Status: Draft — subject to change before implementation

Overview

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.

Architecture

┌──────────────────────────────────────────────────────┐
│                    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:

  1. Exposes a WebSocket endpoint for external adapters to connect.
  2. Translates WebSocket messages into core.Platform interface calls.
  3. Routes engine replies back to the adapter over the same WebSocket connection.

Connection

Endpoint

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 authentication

Authentication

The adapter must authenticate on connection using one of:

MethodExample
Query parameterws://host:9810/bridge/ws?token=your-secret
HeaderAuthorization: Bearer your-secret
HeaderX-Bridge-Token: your-secret

Unauthenticated connections are rejected with HTTP 401.

Connection Lifecycle

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)

Message Protocol

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).

Adapter → cc-connect

register

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:

FieldTypeRequiredDescription
typestringyes"register"
platformstringyesUnique platform name (lowercase, alphanumeric + hyphens). Used in session keys.
capabilitiesstring[]yesList of supported capabilities (see Capabilities).
metadataobjectnoFree-form metadata for logging/debugging.

message

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:

FieldTypeRequiredDescription
typestringyes"message"
msg_idstringyesPlatform-specific message ID for tracing.
session_keystringyesUnique session identifier. Format: {platform}:{scope}:{user}. The adapter defines how to compose this.
user_idstringyesUser identifier on the platform.
user_namestringnoDisplay name.
contentstringyesText content.
reply_ctxstringyesOpaque context string the adapter needs to route replies back. cc-connect echoes this in every reply.
imagesImage[]noAttached images (see Image Object).
filesFile[]noAttached files (see File Object).
audioAudionoVoice message (see Audio Object).

card_action

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:

FieldTypeRequiredDescription
typestringyes"card_action"
session_keystringyesSession that triggered the action.
actionstringyesThe callback value from the button (e.g., "cmd:/new", "nav:/model", "act:/heartbeat pause").
reply_ctxstringyesReply context for routing the response.

preview_ack

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"
}

ping

Keepalive. cc-connect responds with pong.

{
  "type": "ping",
  "ts": 1710000000000
}

cc-connect → Adapter

register_ack

Confirms or rejects registration.

{
  "type": "register_ack",
  "ok": true,
  "error": ""
}

reply

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:

FieldTypeRequiredDescription
typestringyes"reply"
session_keystringyesTarget session.
reply_ctxstringyesEchoed from the original message.
contentstringyesReply text content.
formatstringno"text" (default) or "markdown".

reply_stream

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
}
FieldTypeDescription
deltastringNew text since last stream message.
full_textstringFull accumulated text. Adapters can use this for "replace entire message" updates.
preview_handlestringHandle returned by preview_ack. Empty on first stream message.
donebooltrue on the final stream message.

preview_start

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.

update_message

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..."
}

delete_message

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"
}

card

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.

buttons

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.

typing_start

Requests the adapter to show a typing indicator.

{
  "type": "typing_start",
  "session_key": "wechat:user123:user123",
  "reply_ctx": "conv-abc-123"
}

typing_stop

Requests the adapter to hide the typing indicator.

{
  "type": "typing_stop",
  "session_key": "wechat:user123:user123",
  "reply_ctx": "conv-abc-123"
}

audio

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"
}

image

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"
}

file

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"
}

pong

Response to ping.

{
  "type": "pong",
  "ts": 1710000000000
}

error

Notify the adapter of a server-side error.

{
  "type": "error",
  "code": "session_not_found",
  "message": "No active session for the given key"
}

Data Schemas

Capabilities

CapabilityDescriptionEnables
textBasic text messaging (required)message, reply
imageSending/receiving imagesmessage.images, image reply
fileSending/receiving filesmessage.files, file reply
audioSending/receiving voice messagesmessage.audio, audio reply
cardStructured rich card renderingcard reply
buttonsInline clickable buttonsbuttons reply, card_action
typingTyping indicatortyping_start, typing_stop
update_messageEdit existing messagesupdate_message
previewStreaming preview (requires update_message)preview_start, reply_stream
delete_messageDelete messagesdelete_message
reconstruct_replyCan reconstruct reply context from session_keyEnables cron/heartbeat messages

If a capability is not declared, cc-connect will automatically degrade:

  • No card → cards are rendered as plain text via RenderText().
  • 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.

Image Object

{
  "mime_type": "image/png",
  "data": "<base64-encoded>",
  "file_name": "screenshot.png"
}

File Object

{
  "mime_type": "application/pdf",
  "data": "<base64-encoded>",
  "file_name": "report.pdf"
}

Audio Object

{
  "mime_type": "audio/ogg",
  "data": "<base64-encoded>",
  "format": "ogg",
  "duration": 5
}

Card Schema

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.

Element Types

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 Key Format

Session keys follow the pattern:

{platform}:{scope}:{user_id}
  • platform: The platform name from registration (e.g., wechat).
  • scope: A grouping scope — could be a group/channel ID, or the same as user_id for 1-on-1 chats.
  • user_id: The unique user identifier.

Examples:

  • wechat:user123:user123 — personal DM
  • wechat:group456:user123 — user in a group chat
  • matrix:room789:alice — Matrix room

The adapter is responsible for constructing consistent session keys.


Session Management REST API

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.

Authentication

The same token used for WebSocket connections applies to REST endpoints:

MethodExample
HeaderAuthorization: Bearer your-secret
Query param?token=your-secret

Response Format

All responses use the same envelope as the Management API:

{"ok": true, "data": { ... }}
{"ok": false, "error": "message"}

Endpoints

All endpoints are relative to the Bridge Server base URL (e.g., http://localhost:9810).

GET /bridge/sessions

Lists sessions for a given session key prefix (typically platform:chatId).

Query parameters:

ParamTypeRequiredDescription
session_keystringyesThe 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"
  }
}

POST /bridge/sessions

Creates a new named session.

Request body:

{
  "session_key": "wechat:user123:user123",
  "name": "work"
}
FieldTypeRequiredDescription
session_keystringyesSession key for the user.
namestringnoHuman-readable session name. Defaults to "default".

Response:

{
  "ok": true,
  "data": {
    "id": "s3",
    "name": "work",
    "message": "session created"
  }
}

GET /bridge/sessions/{id}

Returns session detail with message history.

Query parameters:

ParamTypeDefaultDescription
session_keystring(required)Session key to identify the project context.
history_limitint50Max 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?"}
    ]
  }
}

DELETE /bridge/sessions/{id}

Deletes a session and its history.

Query parameters:

ParamTypeRequiredDescription
session_keystringyesSession key to identify the project context.

Response:

{
  "ok": true,
  "data": {
    "message": "session deleted"
  }
}

POST /bridge/sessions/switch

Switches the active session for a session key.

Request body:

{
  "session_key": "wechat:user123:user123",
  "target": "s2"
}
FieldTypeRequiredDescription
session_keystringyesSession key.
targetstringyesSession ID or name to switch to.

Response:

{
  "ok": true,
  "data": {
    "message": "session switched",
    "active_session_id": "s2"
  }
}

Error Handling

Reconnection

If the WebSocket connection drops, the adapter should:

  1. Wait with exponential backoff (starting at 1s, max 60s).
  2. Reconnect and send a new register message.
  3. Resume normal operation — cc-connect maintains session state independently of the connection.

Message Ordering

Messages within a single WebSocket connection are ordered. cc-connect processes adapter messages sequentially per session key.

Timeouts

  • Ping interval: Adapters should send ping at 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.

Configuration Example

[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.


SDK Guidelines

When building an adapter, follow these guidelines:

  1. Keep it stateless — the adapter should be a thin translation layer. All session state lives in cc-connect.
  2. Handle reconnection — network failures are normal. Implement exponential backoff.
  3. Declare capabilities honestly — only declare capabilities your platform actually supports.
  4. Use reply_ctx faithfully — always echo back the reply_ctx from the original message.
  5. Base64 for binary data — images, files, and audio are transferred as base64-encoded strings.
  6. Log errors, don't crash — if you receive an unknown message type, log it and continue.

Minimal Adapter Example (Python pseudocode)

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())

Versioning

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
  }
}