Files
openclaw/docs/gateway/protocol.md
2026-03-07 19:38:49 +00:00

261 lines
8.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 hosts own tailnet address
(so samehost tailnet binds can still autoapprove).
- 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`.