{ "title": "Gateway protocol (WebSocket)", "content": "The Gateway WS protocol is the **single control plane + node transport** for\nOpenClaw. All clients (CLI, web UI, macOS app, iOS/Android nodes, headless\nnodes) connect over WebSocket and declare their **role** + **scope** at\nhandshake time.\n\n* WebSocket, text frames with JSON payloads.\n* First frame **must** be a `connect` request.\n\n## Handshake (connect)\n\nGateway → Client (pre-connect challenge):\n\nWhen a device token is issued, `hello-ok` also includes:\n\n* **Request**: `{type:\"req\", id, method, params}`\n* **Response**: `{type:\"res\", id, ok, payload|error}`\n* **Event**: `{type:\"event\", event, payload, seq?, stateVersion?}`\n\nSide-effecting methods require **idempotency keys** (see schema).\n\n* `operator` = control plane client (CLI/UI/automation).\n* `node` = capability host (camera/screen/canvas/system.run).\n\n### Scopes (operator)\n\n* `operator.read`\n* `operator.write`\n* `operator.admin`\n* `operator.approvals`\n* `operator.pairing`\n\n### Caps/commands/permissions (node)\n\nNodes declare capability claims at connect time:\n\n* `caps`: high-level capability categories.\n* `commands`: command allowlist for invoke.\n* `permissions`: granular toggles (e.g. `screen.record`, `camera.capture`).\n\nThe Gateway treats these as **claims** and enforces server-side allowlists.\n\n* `system-presence` returns entries keyed by device identity.\n* Presence entries include `deviceId`, `roles`, and `scopes` so UIs can show a single row per device\n even when it connects as both **operator** and **node**.\n\n### Node helper methods\n\n* Nodes may call `skills.bins` to fetch the current list of skill executables\n for auto-allow checks.\n\n* When an exec request needs approval, the gateway broadcasts `exec.approval.requested`.\n* Operator clients resolve by calling `exec.approval.resolve` (requires `operator.approvals` scope).\n\n* `PROTOCOL_VERSION` lives in `src/gateway/protocol/schema.ts`.\n* Clients send `minProtocol` + `maxProtocol`; the server rejects mismatches.\n* Schemas + models are generated from TypeBox definitions:\n * `pnpm protocol:gen`\n * `pnpm protocol:gen:swift`\n * `pnpm protocol:check`\n\n* If `OPENCLAW_GATEWAY_TOKEN` (or `--token`) is set, `connect.params.auth.token`\n must match or the socket is closed.\n* After pairing, the Gateway issues a **device token** scoped to the connection\n role + scopes. It is returned in `hello-ok.auth.deviceToken` and should be\n persisted by the client for future connects.\n* Device tokens can be rotated/revoked via `device.token.rotate` and\n `device.token.revoke` (requires `operator.pairing` scope).\n\n## Device identity + pairing\n\n* Nodes should include a stable device identity (`device.id`) derived from a\n keypair fingerprint.\n* Gateways issue tokens per device + role.\n* Pairing approvals are required for new device IDs unless local auto-approval\n is enabled.\n* **Local** connects include loopback and the gateway host’s own tailnet address\n (so same‑host tailnet binds can still auto‑approve).\n* All WS clients must include `device` identity during `connect` (operator + node).\n Control UI can omit it **only** when `gateway.controlUi.allowInsecureAuth` is enabled\n (or `gateway.controlUi.dangerouslyDisableDeviceAuth` for break-glass use).\n* Non-local connections must sign the server-provided `connect.challenge` nonce.\n\n* TLS is supported for WS connections.\n* Clients may optionally pin the gateway cert fingerprint (see `gateway.tls`\n config plus `gateway.remote.tlsFingerprint` or CLI `--tls-fingerprint`).\n\nThis protocol exposes the **full gateway API** (status, channels, models, chat,\nagent, sessions, nodes, approvals, etc.). The exact surface is defined by the\nTypeBox schemas in `src/gateway/protocol/schema.ts`.", "code_samples": [ { "code": "Client → Gateway:", "language": "unknown" }, { "code": "Gateway → Client:", "language": "unknown" }, { "code": "When a device token is issued, `hello-ok` also includes:", "language": "unknown" }, { "code": "### Node example", "language": "unknown" } ], "headings": [ { "level": "h2", "text": "Transport", "id": "transport" }, { "level": "h2", "text": "Handshake (connect)", "id": "handshake-(connect)" }, { "level": "h3", "text": "Node example", "id": "node-example" }, { "level": "h2", "text": "Framing", "id": "framing" }, { "level": "h2", "text": "Roles + scopes", "id": "roles-+-scopes" }, { "level": "h3", "text": "Roles", "id": "roles" }, { "level": "h3", "text": "Scopes (operator)", "id": "scopes-(operator)" }, { "level": "h3", "text": "Caps/commands/permissions (node)", "id": "caps/commands/permissions-(node)" }, { "level": "h2", "text": "Presence", "id": "presence" }, { "level": "h3", "text": "Node helper methods", "id": "node-helper-methods" }, { "level": "h2", "text": "Exec approvals", "id": "exec-approvals" }, { "level": "h2", "text": "Versioning", "id": "versioning" }, { "level": "h2", "text": "Auth", "id": "auth" }, { "level": "h2", "text": "Device identity + pairing", "id": "device-identity-+-pairing" }, { "level": "h2", "text": "TLS + pinning", "id": "tls-+-pinning" }, { "level": "h2", "text": "Scope", "id": "scope" } ], "url": "llms-txt#gateway-protocol-(websocket)", "links": [] }