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`
2026-03-07 19:38:40 +00:00
Method scope is only the first gate. Some slash commands reached through
`chat.send` apply stricter command-level checks on top. For example, persistent
`/config set` and `/config unset` writes require `operator.admin` .
2026-01-19 08:54:21 +00:00
### 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-03-10 17:05:57 -05:00
- Auth failures include `error.details.code` plus recovery hints:
- `error.details.canRetryWithDeviceToken` (boolean)
- `error.details.recommendedNextStep` (`retry_with_device_token` , `update_auth_configuration` , `update_auth_credentials` , `wait_then_retry` , `review_auth_configuration` )
- Client behavior for `AUTH_TOKEN_MISMATCH` :
- Trusted clients may attempt one bounded retry with a cached per-device token.
- If that retry fails, clients should stop automatic reconnect loops and surface operator action guidance.
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-03-10 17:05:57 -05:00
Control UI can omit it only in these modes:
- `gateway.controlUi.allowInsecureAuth=true` for localhost-only insecure HTTP compatibility.
- `gateway.controlUi.dangerouslyDisableDeviceAuth=true` (break-glass, severe security downgrade).
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` .