Bridge Protocol (Legacy TCP JSONL) - Archived Reference
Archived documentation for the legacy bridge protocol, covering TCP JSONL transport, pairing, and scoped RPC. Intended for developers maintaining old node clients or reviewing historical gateway security.
Read this when
- Implementing or updating gateway WS clients
- Debugging protocol mismatches or connect failures
- Regenerating protocol schema/models
Warning
The TCP bridge is gone. Newer OpenClaw releases no longer include the bridge listener, and the
bridge.*configuration keys have been dropped from the schema. This page exists only as an archived record. For all node/operator clients, rely on the Gateway protocol.
Why it existed
- Security boundary: only a narrow allowlist was exposed, not the entire gateway API.
- Pairing + node identity: the gateway handled node admission, tying it to a token unique to each node.
- Discovery UX: gateways could be found by nodes over LAN via Bonjour, or reached directly across a tailnet.
- Loopback WS: unless tunneled through SSH, the full WS control plane remained local.
Transport
- TCP transport, with one JSON object per line (JSONL).
- TLS optional (
bridge.tls.enabled: true). - Default port for the listener was
18790.
When TLS was on, discovery TXT records carried bridgeTls=1 and bridgeTlsSha256, the latter being a non-secret hint. Since Bonjour/mDNS TXT records are not authenticated, clients had no way to treat the advertised fingerprint as an authoritative pin without separate out-of-band checks.
Handshake and pairing
- The client transmits
hello, containing node metadata and a token if pairing already happened. - For unpaired nodes, the gateway answers with
error(NOT_PAIRED/UNAUTHORIZED). - Next, the client sends
pair-request. - Once approval is granted, the gateway responds with
pair-okandhello-ok.
Previously, hello-ok was used to return serverName; hosted plugin surfaces are now announced via pluginSurfaceUrls on the current Gateway protocol (Canvas/A2UI relies on pluginSurfaceUrls.canvas).
Frames
Client to gateway:
req/res: gateway RPC with scoped access (chat, sessions, config, health, voicewake, skills.bins).event: node signals covering voice transcript, agent request, chat subscribe, and exec lifecycle.
Gateway to client:
invoke/invoke-res: node commands (canvas.present,canvas.hide,canvas.navigate,camera.*,screen.record,location.get,sms.send).event: chat updates for sessions being subscribed to.ping/pong: keepalive traffic.
The allowlist enforcement was implemented in src/gateway/server-bridge.ts (now removed).
Exec lifecycle events
To surface completed system.run activity, nodes emitted exec.finished, which the gateway mapped to system events (legacy nodes could alternatively emit exec.started). A denied system.run attempt was marked by exec.denied as a terminal denial, with no system event queued and no agent work triggered.
Payload fields (all optional unless noted):
| Field | Notes |
|---|---|
sessionKey | Required. Agent session used for event correlation and, for exec.finished, system event delivery. |
runId | Unique exec id for grouping. |
command | Raw or formatted command string. |
exitCode, timedOut, output | Completion details (finished only). |
reason | Denial reason (denied only). |
Historical tailnet usage
- Bind the bridge to a tailnet IP by setting
bridge.bind: "tailnet"in~/.openclaw/openclaw.json(historical only;bridge.*is no longer accepted config). - Clients connected through a MagicDNS name or a tailnet IP.
- Bonjour cannot traverse networks; otherwise, wide-area DNS-SD or a manual host/port was necessary.
Versioning
The bridge acted as implicit v1, with no min/max negotiation. Current node/operator clients use the WebSocket Gateway protocol, which does negotiate a protocol version range.