--- 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 title: "Gateway Protocol" --- # Gateway protocol (WebSocket) The Gateway WS protocol is the **single control plane + node transport** for OpenClaw. All clients (CLI, web UI, macOS app, iOS/Android nodes, headless nodes) connect over WebSocket and declare their **role** + **scope** at handshake time. ## Transport - WebSocket, text frames with JSON payloads. - First frame **must** be a `connect` request. ## Handshake (connect) Gateway → Client (pre-connect challenge): ```json { "type": "event", "event": "connect.challenge", "payload": { "nonce": "…", "ts": 1737264000000 } } ``` Client → Gateway: ```json { "type": "req", "id": "…", "method": "connect", "params": { "minProtocol": 3, "maxProtocol": 3, "client": { "id": "cli", "version": "1.2.3", "platform": "macos", "mode": "operator" }, "role": "operator", "scopes": ["operator.read", "operator.write"], "caps": [], "commands": [], "permissions": {}, "auth": { "token": "…" }, "locale": "en-US", "userAgent": "openclaw-cli/1.2.3", "device": { "id": "device_fingerprint", "publicKey": "…", "signature": "…", "signedAt": 1737264000000, "nonce": "…" } } } ``` Gateway → Client: ```json { "type": "res", "id": "…", "ok": true, "payload": { "type": "hello-ok", "protocol": 3, "policy": { "tickIntervalMs": 15000 } } } ``` When a device token is issued, `hello-ok` also includes: ```json { "auth": { "deviceToken": "…", "role": "operator", "scopes": ["operator.read", "operator.write"] } } ``` ### 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", "userAgent": "openclaw-ios/1.2.3", "device": { "id": "device_fingerprint", "publicKey": "…", "signature": "…", "signedAt": 1737264000000, "nonce": "…" } } } ``` ## Framing - **Request**: `{type:"req", id, method, params}` - **Response**: `{type:"res", id, ok, payload|error}` - **Event**: `{type:"event", event, payload, seq?, stateVersion?}` Side-effecting methods require **idempotency keys** (see schema). ## Roles + scopes ### Roles - `operator` = control plane client (CLI/UI/automation). - `node` = capability host (camera/screen/canvas/system.run). ### Scopes (operator) Common scopes: - `operator.read` - `operator.write` - `operator.admin` - `operator.approvals` - `operator.pairing` 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`. ### Caps/commands/permissions (node) Nodes declare capability claims at connect time: - `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. ## 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**. ### Node helper methods - Nodes may call `skills.bins` to fetch the current list of skill executables for auto-allow checks. ### 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 ## 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). - For `host=node`, `exec.approval.request` must include `systemRunPlan` (canonical `argv`/`cwd`/`rawCommand`/session metadata). Requests missing `systemRunPlan` are rejected. ## 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 - If `OPENCLAW_GATEWAY_TOKEN` (or `--token`) is set, `connect.params.auth.token` must match or the socket is closed. - 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). ## 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. - **Local** connects include loopback and the gateway host’s own tailnet address (so same‑host tailnet binds can still auto‑approve). - All WS clients must include `device` identity during `connect` (operator + node). Control UI can omit it **only** when `gateway.controlUi.dangerouslyDisableDeviceAuth` is enabled for break-glass use. - All connections must sign the server-provided `connect.challenge` nonce. ### 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`. - 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. ## TLS + pinning - TLS is supported for WS connections. - Clients may optionally pin the gateway cert fingerprint (see `gateway.tls` config plus `gateway.remote.tlsFingerprint` or CLI `--tls-fingerprint`). ## Scope This protocol exposes the **full gateway API** (status, channels, models, chat, agent, sessions, nodes, approvals, etc.). The exact surface is defined by the TypeBox schemas in `src/gateway/protocol/schema.ts`.