{
  "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": []
}