2026-01-12 04:44:14 +00:00
---
summary: "Gateway WebSocket protocol: handshake, frames, versioning"
read_when:
- Implementing or updating gateway WS clients
- Debugging protocol mismatches or connect failures
- Regenerating protocol schema/models
2026-01-31 16:04:03 -05:00
title: "Gateway Protocol"
2026-01-12 04:44:14 +00:00
---
# Gateway protocol (WebSocket)
2026-01-19 08:54:21 +00:00
The Gateway WS protocol is the **single control plane + node transport ** for
2026-01-30 03:15:10 +01:00
OpenClaw. All clients (CLI, web UI, macOS app, iOS/Android nodes, headless
2026-01-19 08:54:21 +00:00
nodes) connect over WebSocket and declare their **role ** + **scope ** at
handshake time.
2026-01-12 04:44:14 +00:00
## Transport
- WebSocket, text frames with JSON payloads.
- First frame **must ** be a `connect` request.
## Handshake (connect)
2026-01-20 11:15:10 +00:00
Gateway → Client (pre-connect challenge):
``` json
{
"type" : "event" ,
"event" : "connect.challenge" ,
"payload" : { "nonce" : "…" , "ts" : 1737264000000 }
}
```
2026-01-12 04:44:14 +00:00
Client → Gateway:
``` json
{
"type" : "req" ,
"id" : "…" ,
"method" : "connect" ,
"params" : {
"minProtocol" : 3 ,
"maxProtocol" : 3 ,
"client" : {
"id" : "cli" ,
"version" : "1.2.3" ,
"platform" : "macos" ,
"mode" : "operator"
} ,
2026-01-19 08:54:21 +00:00
"role" : "operator" ,
"scopes" : [ "operator.read" , "operator.write" ] ,
2026-01-12 04:44:14 +00:00
"caps" : [ ] ,
2026-01-19 08:54:21 +00:00
"commands" : [ ] ,
"permissions" : { } ,
2026-01-12 04:44:14 +00:00
"auth" : { "token" : "…" } ,
"locale" : "en-US" ,
2026-01-30 03:15:10 +01:00
"userAgent" : "openclaw-cli/1.2.3" ,
2026-01-20 11:15:10 +00:00
"device" : {
"id" : "device_fingerprint" ,
"publicKey" : "…" ,
"signature" : "…" ,
"signedAt" : 1737264000000 ,
"nonce" : "…"
}
2026-01-12 04:44:14 +00:00
}
}
```
Gateway → Client:
``` json
{
"type" : "res" ,
"id" : "…" ,
"ok" : true ,
"payload" : { "type" : "hello-ok" , "protocol" : 3 , "policy" : { "tickIntervalMs" : 15000 } }
}
```
2026-01-20 10:29:13 +00:00
When a device token is issued, `hello-ok` also includes:
``` json
{
"auth" : {
"deviceToken" : "…" ,
"role" : "operator" ,
"scopes" : [ "operator.read" , "operator.write" ]
}
}
```
2026-01-19 08:54:21 +00:00
### Node example
``` json
{
"type" : "req" ,
"id" : "…" ,
"method" : "connect" ,
"params" : {
"minProtocol" : 3 ,
"maxProtocol" : 3 ,
"client" : {
"id" : "ios-node" ,
"version" : "1.2.3" ,
"platform" : "ios" ,
"mode" : "node"
} ,
"role" : "node" ,
"scopes" : [ ] ,
"caps" : [ "camera" , "canvas" , "screen" , "location" , "voice" ] ,
"commands" : [ "camera.snap" , "canvas.navigate" , "screen.record" , "location.get" ] ,
"permissions" : { "camera.capture" : true , "screen.record" : false } ,
"auth" : { "token" : "…" } ,
"locale" : "en-US" ,
2026-01-30 03:15:10 +01:00
"userAgent" : "openclaw-ios/1.2.3" ,
2026-01-19 08:54:21 +00:00
"device" : {
"id" : "device_fingerprint" ,
"publicKey" : "…" ,
"signature" : "…" ,
2026-01-20 11:15:10 +00:00
"signedAt" : 1737264000000 ,
"nonce" : "…"
2026-01-19 08:54:21 +00:00
}
}
}
```
2026-01-12 04:44:14 +00:00
## Framing
2026-01-31 21:13:13 +09:00
- **Request**: `{type:"req", id, method, params}`
- **Response**: `{type:"res", id, ok, payload|error}`
2026-01-12 04:44:14 +00:00
- **Event**: `{type:"event", event, payload, seq?, stateVersion?}`
Side-effecting methods require **idempotency keys ** (see schema).
2026-01-19 08:54:21 +00:00
## Roles + scopes
### Roles
2026-01-31 21:13:13 +09:00
2026-01-19 08:54:21 +00:00
- `operator` = control plane client (CLI/UI/automation).
- `node` = capability host (camera/screen/canvas/system.run).
### Scopes (operator)
2026-01-31 21:13:13 +09:00
2026-01-19 08:54:21 +00:00
Common scopes:
2026-01-31 21:13:13 +09:00
2026-01-19 08:54:21 +00:00
- `operator.read`
- `operator.write`
- `operator.admin`
- `operator.approvals`
- `operator.pairing`
### Caps/commands/permissions (node)
2026-01-31 21:13:13 +09:00
2026-01-19 08:54:21 +00:00
Nodes declare capability claims at connect time:
2026-01-31 21:13:13 +09:00
2026-01-19 08:54:21 +00:00
- `caps` : high-level capability categories.
- `commands` : command allowlist for invoke.
- `permissions` : granular toggles (e.g. `screen.record` , `camera.capture` ).
The Gateway treats these as **claims ** and enforces server-side allowlists.
2026-01-20 12:20:20 +00:00
## Presence
- `system-presence` returns entries keyed by device identity.
- Presence entries include `deviceId` , `roles` , and `scopes` so UIs can show a single row per device
even when it connects as both **operator ** and **node ** .
2026-01-20 09:23:56 +00:00
### Node helper methods
- Nodes may call `skills.bins` to fetch the current list of skill executables
for auto-allow checks.
2026-02-22 23:55:59 -06:00
### Operator helper methods
- Operators may call `tools.catalog` (`operator.read` ) to fetch the runtime tool catalog for an
agent. The response includes grouped tools and provenance metadata:
- `source` : `core` or `plugin`
- `pluginId` : plugin owner when `source="plugin"`
- `optional` : whether a plugin tool is optional
2026-01-20 12:20:20 +00:00
## Exec approvals
- When an exec request needs approval, the gateway broadcasts `exec.approval.requested` .
- Operator clients resolve by calling `exec.approval.resolve` (requires `operator.approvals` scope).
2026-03-02 01:12:47 +00:00
- For `host=node` , `exec.approval.request` must include `systemRunPlan` (canonical `argv` /`cwd` /`rawCommand` /session metadata). Requests missing `systemRunPlan` are rejected.
2026-01-20 12:20:20 +00:00
2026-01-12 04:44:14 +00:00
## Versioning
- `PROTOCOL_VERSION` lives in `src/gateway/protocol/schema.ts` .
- Clients send `minProtocol` + `maxProtocol` ; the server rejects mismatches.
- Schemas + models are generated from TypeBox definitions:
- `pnpm protocol:gen`
- `pnpm protocol:gen:swift`
- `pnpm protocol:check`
## Auth
2026-01-30 03:15:10 +01:00
- If `OPENCLAW_GATEWAY_TOKEN` (or `--token` ) is set, `connect.params.auth.token`
2026-01-12 04:44:14 +00:00
must match or the socket is closed.
2026-01-20 10:29:13 +00:00
- After pairing, the Gateway issues a **device token ** scoped to the connection
role + scopes. It is returned in `hello-ok.auth.deviceToken` and should be
persisted by the client for future connects.
- Device tokens can be rotated/revoked via `device.token.rotate` and
`device.token.revoke` (requires `operator.pairing` scope).
2026-01-12 04:44:14 +00:00
2026-01-19 08:54:21 +00:00
## Device identity + pairing
- Nodes should include a stable device identity (`device.id` ) derived from a
keypair fingerprint.
- Gateways issue tokens per device + role.
- Pairing approvals are required for new device IDs unless local auto-approval
is enabled.
2026-01-21 00:14:06 +00:00
- **Local** connects include loopback and the gateway host’ s own tailnet address
(so same‑ host tailnet binds can still auto‑ approve).
2026-01-20 09:23:56 +00:00
- All WS clients must include `device` identity during `connect` (operator + node).
2026-02-21 12:55:18 +01:00
Control UI can omit it **only ** when `gateway.controlUi.dangerouslyDisableDeviceAuth`
is enabled for break-glass use.
2026-02-22 09:26:49 +01:00
- All connections must sign the server-provided `connect.challenge` nonce.
2026-02-27 00:05:43 -05:00
### Device auth migration diagnostics
For legacy clients that still use pre-challenge signing behavior, `connect` now returns
`DEVICE_AUTH_*` detail codes under `error.details.code` with a stable `error.details.reason` .
Common migration failures:
| Message | details.code | details.reason | Meaning |
| --------------------------- | -------------------------------- | ------------------------ | -------------------------------------------------- |
| `device nonce required` | `DEVICE_AUTH_NONCE_REQUIRED` | `device-nonce-missing` | Client omitted `device.nonce` (or sent blank). |
| `device nonce mismatch` | `DEVICE_AUTH_NONCE_MISMATCH` | `device-nonce-mismatch` | Client signed with a stale/wrong nonce. |
| `device signature invalid` | `DEVICE_AUTH_SIGNATURE_INVALID` | `device-signature` | Signature payload does not match v2 payload. |
| `device signature expired` | `DEVICE_AUTH_SIGNATURE_EXPIRED` | `device-signature-stale` | Signed timestamp is outside allowed skew. |
| `device identity mismatch` | `DEVICE_AUTH_DEVICE_ID_MISMATCH` | `device-id-mismatch` | `device.id` does not match public key fingerprint. |
| `device public key invalid` | `DEVICE_AUTH_PUBLIC_KEY_INVALID` | `device-public-key` | Public key format/canonicalization failed. |
Migration target:
- Always wait for `connect.challenge` .
- Sign the v2 payload that includes the server nonce.
- Send the same nonce in `connect.params.device.nonce` .
2026-02-26 14:10:00 +01:00
- Preferred signature payload is `v3` , which binds `platform` and `deviceFamily`
in addition to device/client/role/scopes/token/nonce fields.
- Legacy `v2` signatures remain accepted for compatibility, but paired-device
metadata pinning still controls command policy on reconnect.
2026-01-19 08:54:21 +00:00
## TLS + pinning
- TLS is supported for WS connections.
- Clients may optionally pin the gateway cert fingerprint (see `gateway.tls`
2026-01-20 12:20:20 +00:00
config plus `gateway.remote.tlsFingerprint` or CLI `--tls-fingerprint` ).
2026-01-19 08:54:21 +00:00
2026-01-12 04:44:14 +00:00
## Scope
2026-01-19 08:54:21 +00:00
This protocol exposes the **full gateway API ** (status, channels, models, chat,
agent, sessions, nodes, approvals, etc.). The exact surface is defined by the
2026-01-12 04:44:14 +00:00
TypeBox schemas in `src/gateway/protocol/schema.ts` .