Why it existed
- Security boundary: exposed a small allowlist instead of the full gateway API surface.
- Pairing + node identity: node admission was owned by the gateway and tied to a per-node token.
- Discovery UX: nodes could discover gateways via Bonjour on LAN, or connect directly over a tailnet.
- Loopback WS: the full WS control plane stayed local unless tunneled via SSH.
Transport
- TCP, one JSON object per line (JSONL).
- Optional TLS (
bridge.tls.enabled: true). - Default listener port was
18790.
bridgeTls=1 plus bridgeTlsSha256 as a non-secret hint. Bonjour/mDNS TXT records are unauthenticated; clients could not treat the advertised fingerprint as an authoritative pin without other out-of-band verification.
Handshake and pairing
- Client sends
hellowith node metadata plus token (if already paired). - If not paired, gateway replies
error(NOT_PAIRED/UNAUTHORIZED). - Client sends
pair-request. - Gateway waits for approval, then sends
pair-okandhello-ok.
hello-ok used to return serverName; hosted plugin surfaces are now advertised through pluginSurfaceUrls on the current Gateway protocol (Canvas/A2UI uses pluginSurfaceUrls.canvas).
Frames
Client to gateway:req/res: scoped gateway RPC (chat, sessions, config, health, voicewake, skills.bins).event: node signals (voice transcript, agent request, chat subscribe, exec lifecycle).
invoke/invoke-res: node commands (canvas.*,camera.*,screen.record,location.get,sms.send).event: chat updates for subscribed sessions.ping/pong: keepalive.
src/gateway/server-bridge.ts (removed).
Exec lifecycle events
Nodes emittedexec.finished to surface completed system.run activity, mapped to system events by the gateway (legacy nodes could also emit exec.started). exec.denied marked a denied system.run attempt as a terminal denial without enqueuing a system event or waking agent work.
Payload fields (all optional unless noted):
| Field | Notes |
|---|---|
sessionKey | Required. Agent session 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:
bridge.bind: "tailnet"in~/.openclaw/openclaw.json(historical only;bridge.*is no longer valid config). - Clients connected via MagicDNS name or tailnet IP.
- Bonjour does not cross networks; wide-area DNS-SD or a manual host/port was required otherwise.