Files
openclaw/docs/gateway/protocol.md
Peter Steinberger 912af0a56f fix(gateway): keep N-1 nodes manageable during upgrades (#101109)
* fix(gateway): allow N-1 node protocol maintenance

* docs: refresh generated map for node upgrade guide
2026-07-06 20:01:20 +01:00

924 lines
57 KiB
Markdown

---
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"
---
The Gateway WS protocol is the single control plane and node transport for
OpenClaw. Every client (CLI, web UI, macOS app, iOS/Android nodes, headless
nodes) connects over WebSocket and declares a **role** and **scope** at
handshake time.
## Transport and framing
- WebSocket, text frames, JSON payloads.
- First frame **must** be a `connect` request.
- Pre-connect frames are capped at 64 KiB (`MAX_PREAUTH_PAYLOAD_BYTES`). After
handshake, follow `hello-ok.policy.maxPayload` and
`hello-ok.policy.maxBufferedBytes`. With diagnostics enabled, oversized
inbound frames and slow outbound buffers emit `payload.large` events before
the gateway closes or drops the frame. These events carry `surface`, byte
sizes, limits, and a safe reason code, never message bodies, attachment
contents, raw frame bytes, tokens, cookies, or secrets.
Frame shapes:
- 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).
## Handshake
Gateway sends a pre-connect challenge:
```json
{
"type": "event",
"event": "connect.challenge",
"payload": { "nonce": "…", "ts": 1737264000000 }
}
```
Client replies with `connect`:
```json
{
"type": "req",
"id": "…",
"method": "connect",
"params": {
"minProtocol": 4,
"maxProtocol": 4,
"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 responds with `hello-ok`:
```json
{
"type": "res",
"id": "…",
"ok": true,
"payload": {
"type": "hello-ok",
"protocol": 4,
"server": { "version": "…", "connId": "…" },
"features": { "methods": ["…"], "events": ["…"] },
"snapshot": { "…": "…" },
"auth": {
"role": "operator",
"scopes": ["operator.read", "operator.write"]
},
"policy": {
"maxPayload": 26214400,
"maxBufferedBytes": 52428800,
"tickIntervalMs": 15000
}
}
}
```
`server`, `features`, `snapshot`, `policy`, and `auth` are all required by
`HelloOkSchema` (`packages/gateway-protocol/src/schema/frames.ts`). `auth`
reports the negotiated role/scopes even when no device token is issued (shape
above). `pluginSurfaceUrls` is optional and maps plugin surface names (e.g.
`canvas`) to scoped hosted URLs; it may expire, so nodes call
`node.pluginSurface.refresh` with `{ "surface": "canvas" }` for a fresh entry.
The deprecated `canvasHostUrl` / `canvasCapability` / `node.canvas.capability.refresh`
path is not supported; use plugin surfaces.
While the gateway is still finishing startup sidecars, `connect` can return a
retryable `UNAVAILABLE` error with `details.reason: "startup-sidecars"` and
`retryAfterMs`. Retry within your connection budget instead of treating it as
a terminal handshake failure.
When a device token is issued, `hello-ok.auth` adds it:
```json
{
"auth": {
"deviceToken": "…",
"role": "operator",
"scopes": ["operator.read", "operator.write"]
}
}
```
Built-in QR/setup-code bootstrap is a mobile handoff path. A successful
baseline setup-code connect returns a primary node token plus one bounded
operator token:
```json
{
"auth": {
"deviceToken": "…",
"role": "node",
"scopes": [],
"deviceTokens": [
{
"deviceToken": "…",
"role": "operator",
"scopes": ["operator.approvals", "operator.read", "operator.talk.secrets", "operator.write"]
}
]
}
}
```
This operator handoff is bounded on purpose: enough to start the mobile
operator loop and native setup, including `operator.talk.secrets` for Talk
config reads, but no pairing-mutation scopes and no `operator.admin`. Broader
pairing/admin access needs a separate approved pairing or token flow. Persist
`hello-ok.auth.deviceTokens` only when bootstrap auth ran over a trusted
transport (`wss://` or loopback/local pairing).
Trusted same-process backend clients (`client.id: "gateway-client"`,
`client.mode: "backend"`) may omit `device` on direct loopback connections when
authenticating with the shared gateway token/password. This path is reserved
for internal control-plane RPCs (e.g. subagent session updates) and avoids
stale CLI/device pairing baselines blocking local backend work. Remote,
browser-origin, node, and explicit device-token/device-identity clients still
go through normal pairing and scope-upgrade checks.
### Node connect example
```json
{
"type": "req",
"id": "…",
"method": "connect",
"params": {
"minProtocol": 4,
"maxProtocol": 4,
"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": "…"
}
}
}
```
Nodes declare capability claims at connect time:
- `caps`: high-level categories such as `camera`, `canvas`, `screen`,
`location`, `voice`, `talk`.
- `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.
## Roles and scopes
For the full operator scope model, approval-time checks, and shared-secret
semantics, see [Operator scopes](/gateway/operator-scopes).
Roles:
- `operator`: control-plane client (CLI/UI/automation).
- `node`: capability host (camera/screen/canvas/system.run).
Operator scopes (`src/gateway/operator-scopes.ts`), the full closed set:
- `operator.read`
- `operator.write`
- `operator.admin`
- `operator.approvals`
- `operator.pairing`
- `operator.talk.secrets`
`talk.config` with `includeSecrets: true` requires `operator.talk.secrets` (or
`operator.admin`). When secrets are included, read the active Talk provider
credential from `talk.resolved.config.apiKey`; `talk.providers.<id>.apiKey`
stays source-shaped and may be a SecretRef object or a redacted string.
Plugin-registered gateway RPC methods may request their own operator scope,
but these reserved core prefixes always resolve to `operator.admin`
(`src/shared/gateway-method-policy.ts`): `config.*`, `exec.approvals.*`,
`wizard.*`, `update.*`.
Method scope is only the first gate. Some slash commands reached through
`chat.send` apply stricter command-level checks: persistent `/config set` and
`/config unset` writes require `operator.admin` even for gateway clients that
already hold a lower operator scope.
`node.pair.approve` has an extra approval-time scope check on top of the base
method scope (`operator.pairing`), based on the pending request's declared
`commands` (`src/infra/node-pairing-authz.ts`):
| Declared commands | Required scopes |
| -------------------------------------------------------------- | ------------------------------------- |
| none | `operator.pairing` |
| non-exec commands | `operator.pairing` + `operator.write` |
| includes `system.run`, `system.run.prepare`, or `system.which` | `operator.pairing` + `operator.admin` |
## Presence
- `system-presence` returns entries keyed by device identity, including
`deviceId`, `roles`, and `scopes`, so UIs can show one row per device even
when it connects as both operator and node.
- `node.list` includes optional `lastSeenAtMs` and `lastSeenReason`. Connected
nodes report current connection time with reason `connect`; paired nodes can
also report durable background presence via a trusted node event.
### Node background alive event
Nodes call `node.event` with `event: "node.presence.alive"` to record that a
paired node was alive during a background wake, without marking it connected:
```json
{
"event": "node.presence.alive",
"payloadJSON": "{\"trigger\":\"silent_push\",\"sentAtMs\":1737264000000,\"displayName\":\"Peter's iPhone\",\"version\":\"2026.4.28\",\"platform\":\"iOS 18.4.0\",\"deviceFamily\":\"iPhone\",\"modelIdentifier\":\"iPhone17,1\",\"pushTransport\":\"relay\"}"
}
```
`trigger` is a closed enum: `background`, `silent_push`, `bg_app_refresh`,
`significant_location`, `manual`, `connect`. Unknown values normalize to
`background` (`src/shared/node-presence.ts`). The event only persists for
authenticated node device sessions; device-less or unpaired sessions return
`handled: false`.
Successful gateways return a structured result:
```json
{
"ok": true,
"event": "node.presence.alive",
"handled": true,
"reason": "persisted"
}
```
Older gateways may return only `{ "ok": true }` for `node.event`; treat that
as an acknowledged RPC, not durable presence persistence.
## Broadcast event scoping
Server-pushed broadcast events are scope-gated so pairing-scoped or node-only
sessions do not passively receive session content
(`src/gateway/server-broadcast.ts`):
- Chat, agent, and tool-result frames (streamed `agent` events, tool-result
events) require at least `operator.read`. Sessions without it skip these
frames entirely.
- Plugin-defined `plugin.*` broadcasts are gated to `operator.write` or
`operator.admin` by default; explicit entries such as
`plugin.approval.requested` / `plugin.approval.resolved` use
`operator.approvals` instead.
- Status/transport events (`heartbeat`, `presence`, `tick`, connect/disconnect
lifecycle) stay unrestricted so transport health is observable to every
authenticated session.
- Unknown broadcast event families are scope-gated by default (fail-closed)
unless a registered handler explicitly relaxes them.
Each client connection keeps its own per-client sequence number, so broadcasts
stay monotonically ordered on that socket even when different clients see
different scope-filtered subsets of the event stream.
## RPC method families
`hello-ok.features.methods` is a conservative discovery list built from
`src/gateway/server-methods-list.ts` plus loaded plugin/channel method
exports — it is not a generated dump of every method, and some methods (for
example `push.test`, `web.login.start`, `web.login.wait`, `sessions.usage`)
are intentionally excluded from discovery even though they are real, callable
methods. Treat this as feature discovery, not a full enumeration of
`src/gateway/server-methods/*.ts`.
<AccordionGroup>
<Accordion title="System and identity">
- `health` returns the cached or freshly probed gateway health snapshot.
- `diagnostics.stability` returns the recent bounded diagnostic stability recorder: event names, counts, byte sizes, memory readings, queue/session state, channel/plugin names, session ids. No chat text, webhook bodies, tool outputs, raw request/response bodies, tokens, cookies, or secrets. Requires `operator.read`.
- `status` returns the `/status`-style gateway summary; sensitive fields only for admin-scoped operator clients.
- `gateway.identity.get` returns the gateway device identity used by relay and pairing flows.
- `system-presence` returns the current presence snapshot for connected operator/node devices.
- `system-event` appends a system event and can update/broadcast presence context.
- `last-heartbeat` returns the latest persisted heartbeat event.
- `set-heartbeats` toggles heartbeat processing on the gateway.
</Accordion>
<Accordion title="Models and usage">
- `models.list` returns the runtime-allowed model catalog. See "`models.list` views" below.
- `usage.status` returns provider usage windows/remaining quota summaries.
- `usage.cost` returns aggregated cost usage summaries for a date range. Pass `agentId` for one agent, or `agentScope: "all"` to aggregate configured agents.
- `doctor.memory.status` returns vector-memory / cached embedding readiness for the active default agent workspace. Pass `{ "probe": true }` or `{ "deep": true }` only for an explicit live embedding provider ping. Pass `{ "agentId": "agent-id" }` to scope Dreaming store stats to one agent workspace; omitting it aggregates configured Dreaming workspaces.
- `doctor.memory.dreamDiary`, `doctor.memory.backfillDreamDiary`, `doctor.memory.resetDreamDiary`, `doctor.memory.resetGroundedShortTerm`, `doctor.memory.repairDreamingArtifacts`, and `doctor.memory.dedupeDreamDiary` accept optional `{ "agentId": "agent-id" }`; omitted, they operate on the configured default agent workspace.
- `doctor.memory.remHarness` returns a bounded, read-only REM harness preview for remote control-plane clients, including workspace paths, memory snippets, rendered grounded markdown, and deep promotion candidates. Requires `operator.read`.
- `sessions.usage` returns per-session usage summaries. Pass `agentId` for one agent, or `agentScope: "all"` to list configured agents together.
- `sessions.usage.timeseries` returns timeseries usage for one session.
- `sessions.usage.logs` returns usage log entries for one session.
</Accordion>
<Accordion title="Channels and login helpers">
- `channels.status` returns built-in + bundled channel/plugin status summaries.
- `channels.logout` logs out a specific channel/account where the channel supports it.
- `web.login.start` starts a QR/web login flow for the current QR-capable web channel provider.
- `web.login.wait` waits for that flow to complete and starts the channel on success.
- `push.test` sends a test APNs push to a registered iOS node.
- `voicewake.get` returns the stored wake-word triggers.
- `voicewake.set` updates wake-word triggers and broadcasts the change.
</Accordion>
<Accordion title="Messaging and logs">
- `send` is the direct outbound-delivery RPC for channel/account/thread-targeted sends outside the chat runner.
- `logs.tail` returns the configured gateway file-log tail with cursor/limit and max-byte controls.
</Accordion>
<Accordion title="Operator terminal">
- `terminal.open` starts a host PTY for an explicit `agentId` or the default agent and returns the resolved agent, working directory, shell, and confinement state.
- `terminal.input`, `terminal.resize`, and `terminal.close` operate only on sessions owned by the calling connection.
- `terminal.data` and `terminal.exit` events stream only to the connection that owns the session.
- Sessions whose connection drops are detached, not killed: they stay reattachable for `gateway.terminal.detachedSessionTimeoutSeconds` (default 300; `0` restores kill-on-disconnect) while recent output accumulates in a bounded server-side buffer.
- `terminal.list` returns attachable sessions; `terminal.attach` rebinds a live-or-detached session to the calling connection and returns the replay buffer (tmux-style take-over — a previous live owner receives `terminal.exit` with reason `detached`); `terminal.text` reads the buffer as plain text without attaching.
- Every terminal method requires `operator.admin`; `gateway.terminal.enabled` must be explicitly true. Fully sandboxed agents are refused, and an agent policy change closes existing and in-flight PTYs, detached ones included.
</Accordion>
<Accordion title="Talk and TTS">
- `talk.catalog` returns the read-only Talk provider catalog for speech, streaming transcription, and realtime voice: canonical provider ids, registry aliases, labels, configured state, an optional group-level `ready` result, exposed model/voice ids, canonical modes, transports, brain strategies, and realtime audio/capability flags, without returning provider secrets or mutating global config. Current gateways set `ready` after applying runtime provider selection; treat its absence as unverified on older gateways.
- `talk.config` returns the effective Talk config payload; `includeSecrets` requires `operator.talk.secrets` (or `operator.admin`).
- `talk.session.create` creates a gateway-owned Talk session for `realtime/gateway-relay`, `transcription/gateway-relay`, or `stt-tts/managed-room`. For `stt-tts/managed-room`, `operator.write` callers that pass `sessionKey` must also pass `spawnedBy` for scoped session-key visibility; unscoped `sessionKey` creation and `brain: "direct-tools"` require `operator.admin`.
- `talk.session.join` validates a managed-room session token, emits `session.ready` or `session.replaced` as needed, and returns room/session metadata plus recent Talk events, never the plaintext token or its hash.
- `talk.session.appendAudio` appends base64 PCM input audio to gateway-owned realtime relay and transcription sessions.
- `talk.session.startTurn`, `talk.session.endTurn`, and `talk.session.cancelTurn` drive managed-room turn lifecycle with stale-turn rejection before state clears.
- `talk.session.cancelOutput` stops assistant audio output, primarily for VAD-gated barge-in in gateway relay sessions.
- `talk.session.submitToolResult` completes a provider tool call emitted by a gateway-owned realtime relay session. Pass `options: { willContinue: true }` for interim tool output when a final result follows, or `options: { suppressResponse: true }` when the tool result should satisfy the provider call without starting another realtime response.
- `talk.session.steer` sends active-run voice control into a gateway-owned agent-backed Talk session: `{ sessionId, text, mode? }`, where `mode` is `status`, `steer`, `cancel`, or `followup`; omitted mode is classified from the spoken text.
- `talk.session.close` closes a gateway-owned relay, transcription, or managed-room session and emits terminal Talk events.
- `talk.mode` sets/broadcasts the current Talk mode state for WebChat/Control UI clients.
- `talk.client.create` creates a client-owned realtime provider session using `webrtc` or `provider-websocket` while the gateway owns config, credentials, instructions, and tool policy.
- `talk.client.toolCall` lets client-owned realtime transports forward provider tool calls to gateway policy. The first supported tool is `openclaw_agent_consult`; clients get a run id and wait for normal chat lifecycle events before submitting the provider-specific tool result.
- `talk.client.steer` sends active-run voice control for client-owned realtime transports. The gateway resolves the active embedded run from `sessionKey` and returns a structured accepted/rejected result instead of silently dropping steering.
- `talk.event` is the single Talk event channel for realtime, transcription, STT/TTS, managed-room, telephony, and meeting adapters.
- `talk.speak` synthesizes speech through the active Talk speech provider.
- `tts.status` returns TTS enabled state, active provider, fallback providers, and provider config state.
- `tts.providers` returns the visible TTS provider inventory.
- `tts.enable` and `tts.disable` toggle TTS prefs state.
- `tts.setProvider` updates the preferred TTS provider.
- `tts.convert` runs one-shot text-to-speech conversion.
- `tts.speak` (`operator.write`) renders non-empty `text` with the configured general TTS provider chain and returns one whole clip inline as `audioBase64`, plus `provider` and optional `outputFormat`, `mimeType`, and `fileExtension` metadata. Unlike `tts.convert`, it does not return a Gateway-local path; unlike `talk.speak`, it does not require a Talk provider. Text above `messages.tts.maxTextLength` returns `INVALID_REQUEST`; synthesis failures return `UNAVAILABLE`.
</Accordion>
<Accordion title="Secrets, config, update, and wizard">
- `secrets.reload` re-resolves active SecretRefs and swaps runtime secret state only on full success.
- `secrets.resolve` resolves command-target secret assignments for a specific command/target set.
- `config.get` returns the current config snapshot and hash.
- `config.set` writes a validated config payload.
- `config.patch` merges a partial config update. Destructive array replacement requires the affected path in `replacePaths`; nested arrays under array entries use `[]` paths such as `agents.list[].skills`.
- `config.apply` validates + replaces the full config payload.
- `config.schema` returns the live config schema payload used by Control UI and CLI tooling: schema, `uiHints`, version, generation metadata, plugin + channel schema metadata when loadable. It includes `title` / `description` metadata from the same labels/help text as the UI, including nested object, wildcard, array-item, and `anyOf` / `oneOf` / `allOf` composition branches when matching field documentation exists.
- `config.schema.lookup` returns a path-scoped lookup payload for one config path: normalized path, a shallow schema node, matched hint + `hintPath`, optional `reloadKind`, and immediate child summaries for UI/CLI drill-down. `reloadKind` is one of `restart`, `hot`, or `none` (`src/config/schema.ts`) and mirrors the gateway config reload planner for the requested path. Lookup schema nodes keep the user-facing docs and common validation fields (`title`, `description`, `type`, `enum`, `const`, `format`, `pattern`, numeric/string/array/object bounds, `additionalProperties`, `deprecated`, `readOnly`, `writeOnly`). Child summaries expose `key`, normalized `path`, `type`, `required`, `hasChildren`, optional `reloadKind`, plus the matched `hint` / `hintPath`.
- `update.run` runs the gateway update flow and schedules a restart only if the update succeeded; callers with a session can include `continuationMessage` so startup resumes one follow-up agent turn through the restart continuation queue. Package-manager updates and supervised git-checkout updates from the control plane use a detached managed-service handoff instead of replacing the package tree or mutating checkout/build output inside the live gateway. A started handoff returns `ok: true` with `result.reason: "managed-service-handoff-started"` and `handoff.status: "started"`; unavailable or failed handoffs return `ok: false` with `managed-service-handoff-unavailable` or `managed-service-handoff-failed`, plus `handoff.command` when a manual shell update is required. Unavailable means OpenClaw lacks a safe supervisor boundary or durable service identity, such as `OPENCLAW_SYSTEMD_UNIT` for systemd. During a started handoff, the restart sentinel may briefly report `stats.reason: "restart-health-pending"`; the continuation is delayed until the CLI verifies the restarted gateway and writes the final `ok` sentinel.
- `update.status` refreshes and returns the latest update restart sentinel, including the post-restart running version when available.
- `wizard.start`, `wizard.next`, `wizard.status`, and `wizard.cancel` expose the onboarding wizard over WS RPC.
</Accordion>
<Accordion title="Agent and workspace helpers">
- `agents.list` returns configured agent entries, including effective model and runtime metadata.
- `agents.create`, `agents.update`, and `agents.delete` manage agent records and workspace wiring.
- `agents.files.list`, `agents.files.get`, and `agents.files.set` manage the bootstrap workspace files exposed for an agent.
- `audit.list` returns a bounded metadata-only ledger of agent run and tool action events.
- `agents.workspace.list` and `agents.workspace.get` (`operator.read`) expose read-only, paginated browsing of an agent's workspace directory for clients in the trusted operator domain described in [Operator scopes](/gateway/operator-scopes). Requests accept workspace-relative paths only; reads stay confined to the realpathed workspace root (symlink and hardlink escapes rejected), size-capped, and limited to UTF-8 text plus common image types (base64). Responses do not expose the host workspace path. There are no write operations in this namespace.
- `tasks.list`, `tasks.get`, and `tasks.cancel` expose the gateway task ledger to SDK and operator clients. See [Task ledger RPCs](#task-ledger-rpcs) below.
- `artifacts.list`, `artifacts.get`, and `artifacts.download` expose transcript-derived artifact summaries and downloads for an explicit `sessionKey`, `runId`, or `taskId` scope. Run and task queries resolve the owning session server-side and only return transcript media with matching provenance; unsafe or local URL sources return unsupported downloads instead of fetching server-side.
- `environments.list` and `environments.status` expose read-only gateway-local and node environment discovery for SDK clients.
- `agent.identity.get` returns the effective assistant identity for an agent or session.
- `agent.wait` waits for a run to finish and returns the terminal snapshot when available.
</Accordion>
<Accordion title="Session control">
- `sessions.list` returns the current session index, including per-row `agentRuntime` metadata when an agent runtime backend is configured.
- `sessions.subscribe` and `sessions.unsubscribe` toggle session change event subscriptions for the current WS client.
- `sessions.messages.subscribe` and `sessions.messages.unsubscribe` toggle transcript/message event subscriptions for one session.
- `sessions.preview` returns bounded transcript previews for specific session keys.
- `sessions.describe` returns one gateway session row for an exact session key.
- `sessions.resolve` resolves or canonicalizes a session target.
- `sessions.create` creates a new session entry.
- `sessions.send` sends a message into an existing session.
- `sessions.steer` is the interrupt-and-steer variant for an active session.
- `sessions.abort` aborts active work for a session. Pass `key` plus optional `runId`, or `runId` alone for active runs the gateway can resolve to a session.
- `sessions.patch` updates session metadata/overrides and reports the resolved canonical model plus effective `agentRuntime`.
- `sessions.reset`, `sessions.delete`, and `sessions.compact` perform session maintenance.
- `sessions.get` returns the full stored session row.
- Chat execution still uses `chat.history`, `chat.send`, `chat.abort`, and `chat.inject`. `chat.history` is display-normalized for UI clients: inline directive tags are stripped from visible text, plain-text tool-call XML payloads (`<tool_call>...</tool_call>`, `<function_call>...</function_call>`, `<tool_calls>...</tool_calls>`, `<function_calls>...</function_calls>`, and truncated tool-call blocks) and leaked ASCII/full-width model control tokens are stripped, pure silent-token assistant rows (exact `NO_REPLY` / `no_reply`) are omitted, and oversized rows can be replaced with placeholders.
- `chat.message.get` is the additive bounded full-message reader for a single visible transcript entry. Pass `sessionKey`, optional `agentId` when session selection is agent-scoped, and a transcript `messageId` previously surfaced through `chat.history`; the gateway returns the same display-normalized projection without the lightweight history truncation cap when the stored entry is still available and not oversized.
- `chat.send` accepts one-turn `fastMode: "auto"` to use fast mode for model calls started before the auto cutoff, then start later retry, fallback, tool-result, or continuation calls without fast mode. The cutoff defaults to 60 seconds (`DEFAULT_FAST_MODE_AUTO_ON_SECONDS`) and can be configured per model with `agents.defaults.models["<provider>/<model>"].params.fastAutoOnSeconds`. A `chat.send` caller can pass one-turn `fastAutoOnSeconds` to override the cutoff for that request.
</Accordion>
<Accordion title="Device pairing and device tokens">
- `device.pair.list` returns pending and approved paired devices.
- `device.pair.setupCode` creates a mobile setup code and, by default, a PNG QR data URL. It requires `operator.admin` and is intentionally omitted from advertised discovery. The result includes `setupCode`, optional `qrDataUrl`, `gatewayUrl`, the non-secret `auth` label, and `urlSource`.
- `device.pair.approve`, `device.pair.reject`, and `device.pair.remove` manage device-pairing records.
- `device.token.rotate` rotates a paired device token within its approved role and caller scope bounds.
- `device.token.revoke` revokes a paired device token within its approved role and caller scope bounds.
The setup code embeds a short-lived bootstrap credential. Clients must not
log or persist it beyond the pairing flow.
</Accordion>
<Accordion title="Node pairing, invoke, and pending work">
- `node.pair.request`, `node.pair.list`, `node.pair.approve`, `node.pair.reject`, `node.pair.remove`, and `node.pair.verify` cover node pairing and bootstrap verification.
- `node.list` and `node.describe` return known/connected node state.
- `node.rename` updates a paired node label.
- `node.invoke` forwards a command to a connected node.
- `node.invoke.result` returns the result for an invoke request.
- `node.event` carries node-originated events back into the gateway.
- `node.pending.pull` and `node.pending.ack` are the connected-node queue APIs.
- `node.pending.enqueue` and `node.pending.drain` manage durable pending work for offline/disconnected nodes.
</Accordion>
<Accordion title="Approval families">
- `exec.approval.request`, `exec.approval.get`, `exec.approval.list`, and `exec.approval.resolve` cover one-shot exec approval requests plus pending approval lookup/replay.
- `exec.approval.waitDecision` waits on one pending exec approval and returns the final decision (or `null` on timeout).
- `exec.approvals.get` and `exec.approvals.set` manage gateway exec approval policy snapshots.
- `exec.approvals.node.get` and `exec.approvals.node.set` manage node-local exec approval policy via node relay commands.
- `plugin.approval.request`, `plugin.approval.list`, `plugin.approval.waitDecision`, and `plugin.approval.resolve` cover plugin-defined approval flows.
</Accordion>
<Accordion title="Automation, skills, and tools">
- Automation: `wake` schedules an immediate or next-heartbeat wake text injection; `cron.get`, `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`, `cron.run`, `cron.runs` manage scheduled work.
- `cron.run` remains an enqueue-style RPC for manual runs. Clients that need completion semantics should read the returned `runId` and poll `cron.runs`.
- `cron.runs` accepts an optional non-empty `runId` filter so clients can follow one queued manual run without racing against other history entries for the same job.
- Skills and tools: `commands.list`, `skills.*`, `tools.catalog`, `tools.effective`, `tools.invoke`. See [Operator helper methods](#operator-helper-methods) below.
</Accordion>
</AccordionGroup>
### Common event families
- `chat`: UI chat updates such as `chat.inject` and other transcript-only chat
events. In protocol v4, delta payloads carry `deltaText`; `message` remains
the cumulative assistant snapshot. Non-prefix replacements set
`replace=true` and use `deltaText` as the replacement text.
- `session.message`, `session.operation`, `session.tool`: transcript, in-flight
session operation, and event-stream updates for a subscribed session.
- `sessions.changed`: session index or metadata changed.
- `presence`: system presence snapshot updates.
- `tick`: periodic keepalive/liveness event.
- `health`: gateway health snapshot update.
- `heartbeat`: heartbeat event stream update.
- `cron`: cron run/job change event.
- `shutdown`: gateway shutdown notification.
- `node.pair.requested` / `node.pair.resolved`: node pairing lifecycle.
- `node.invoke.request`: node invoke request broadcast.
- `device.pair.requested` / `device.pair.resolved`: paired-device lifecycle.
- `voicewake.changed`: wake-word trigger config changed.
- `exec.approval.requested` / `exec.approval.resolved`: exec approval
lifecycle.
- `plugin.approval.requested` / `plugin.approval.resolved`: plugin approval
lifecycle.
### Node helper methods
Nodes may call `skills.bins` to fetch the current list of skill executables
for auto-allow checks.
## Audit ledger RPC
`audit.list` gives operator clients a stable newest-first view of agent run and
tool action metadata. It requires `operator.read`. Queries exclude records
older than 30 days, and the shared SQLite ledger is capped at 100,000 records.
Expired rows are deleted during Gateway startup, hourly maintenance, and later
writes.
- Params: optional exact `agentId`, `sessionKey`, or `runId`; optional `kind`
(`"agent_run"` or `"tool_action"`); optional `status` (`"started"`,
`"succeeded"`, `"failed"`, `"cancelled"`, `"timed_out"`, `"blocked"`, or
`"unknown"`); optional inclusive `after` / `before` Unix-millisecond bounds;
optional `limit` from `1` to `500`; and optional string `cursor` from the
preceding page.
- Result: `{ "events": AuditEvent[], "nextCursor"?: string }`.
Each event includes a stable event id, monotonic ledger sequence, source event
sequence, timestamp, actor, agent/session/run provenance, action, status, and a
normalized error code when applicable. Tool events may include tool call id and
tool name. The `redaction` field is always `"metadata_only"`: the ledger does
not store prompts, messages, tool arguments, tool results, command output, or
raw error text.
Recording is on by default and controlled by
[`audit.enabled`](/gateway/configuration-reference#audit); when disabled,
`audit.list` keeps serving records written earlier until they expire.
Use [`openclaw audit`](/cli/audit) for text queries and bounded JSON exports.
## Task ledger RPCs
Operator clients inspect and cancel gateway background task records through
the task ledger RPCs (`packages/gateway-protocol/src/schema/tasks.ts`). These
return sanitized task summaries, not raw runtime state.
- `tasks.list` requires `operator.read`.
- Params: optional `status` (`"queued"`, `"running"`, `"completed"`,
`"failed"`, `"cancelled"`, or `"timed_out"`) or an array of those statuses,
optional `agentId`, optional `sessionKey`, optional `limit` from `1` to
`500`, and optional string `cursor`.
- Result: `{ "tasks": TaskSummary[], "nextCursor"?: string }`.
- `tasks.get` requires `operator.read`.
- Params: `{ "taskId": string }`.
- Result: `{ "task": TaskSummary }`.
- Missing task ids return the gateway not-found error shape.
- `tasks.cancel` requires `operator.write`.
- Params: `{ "taskId": string, "reason"?: string }`.
- Result: `{ "found": boolean, "cancelled": boolean, "reason"?: string, "task"?: TaskSummary }`.
- `found` reports whether the ledger had a matching task. `cancelled`
reports whether the runtime accepted or recorded cancellation.
`TaskSummary` includes `id`, `status`, and optional metadata: `kind`,
`runtime`, `title`, `agentId`, `sessionKey`, `childSessionKey`, `ownerKey`,
`runId`, `taskId`, `flowId`, `parentTaskId`, `sourceId`, timestamps, progress,
terminal summary, and sanitized error text. `agentId` identifies the agent
executing the task; `sessionKey` and `ownerKey` preserve requester and control
context.
## Operator helper methods
- `commands.list` (`operator.read`) fetches the runtime command inventory for
an agent.
- `agentId` is optional; omit it to read the default agent workspace.
- `scope` controls which surface the primary `name` targets: `text` returns
the primary text command token without the leading `/`; `native` and the
default `both` path return provider-aware native names when available.
- `textAliases` carries exact slash aliases such as `/model` and `/m`.
- `nativeName` carries the provider-aware native command name when one
exists.
- `provider` is optional and only affects native naming plus native plugin
command availability.
- `includeArgs=false` omits serialized argument metadata from the response.
- `tools.catalog` (`operator.read`) fetches 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
- `tools.effective` (`operator.read`) fetches the runtime-effective tool
inventory for a session.
- `sessionKey` is required.
- The gateway derives trusted runtime context from the session server-side
instead of accepting caller-supplied auth or delivery context.
- The response is a session-scoped server-derived projection of the active
inventory, including core, plugin, channel, and already-discovered MCP
server tools.
- `tools.effective` is read-only for MCP: it may project a warm session MCP
catalog through the final tool policy, but does not create MCP runtimes,
connect transports, or issue `tools/list`. If no matching warm catalog
exists, the response may include a notice such as `mcp-not-yet-connected`,
`mcp-not-yet-listed`, or `mcp-stale-catalog`.
- Effective tool entries use `source="core"`, `source="plugin"`,
`source="channel"`, or `source="mcp"`.
- `tools.invoke` (`operator.write`) invokes one available tool through the
same gateway policy path as `/tools/invoke`.
- `name` is required. `args`, `sessionKey`, `agentId`, `confirm`, and
`idempotencyKey` are optional.
- If both `sessionKey` and `agentId` are present, the resolved session agent
must match `agentId`.
- Owner-only core wrappers such as `cron`, `gateway`, and `nodes` require
owner/admin identity (`operator.admin`) even though `tools.invoke` itself
is `operator.write`.
- The response is an SDK-facing envelope with `ok`, `toolName`, optional
`output`, and typed `error` fields. Approval or policy refusals return
`ok:false` in the payload rather than bypassing the gateway tool policy
pipeline.
- `skills.status` (`operator.read`) fetches the visible skill inventory for an
agent.
- `agentId` is optional; omit it to read the default agent workspace.
- The response includes eligibility, missing requirements, config checks,
and sanitized install options without exposing raw secret values.
- `skills.search` and `skills.detail` (`operator.read`) return ClawHub
discovery metadata.
- `skills.upload.begin`, `skills.upload.chunk`, and `skills.upload.commit`
(`operator.admin`) stage a private skill archive before installing it. This
is a separate admin upload path for trusted clients, not the normal ClawHub
skill install flow, and is disabled by default unless
`skills.install.allowUploadedArchives` is enabled.
- `skills.upload.begin({ kind: "skill-archive", slug, sizeBytes, sha256?, force?, idempotencyKey? })`
creates an upload bound to that slug and force value.
- `skills.upload.chunk({ uploadId, offset, dataBase64 })` appends bytes at
the exact decoded offset.
- `skills.upload.commit({ uploadId, sha256? })` verifies the final size and
SHA-256. Commit only finalizes the upload; it does not install the skill.
- Uploaded skill archives are zip archives containing a `SKILL.md` root. The
archive's internal directory name never selects the install target.
- `skills.install` (`operator.admin`) has three modes:
- ClawHub mode: `{ source: "clawhub", slug, version?, force? }` installs a
skill folder into the default agent workspace `skills/` directory.
- Upload mode: `{ source: "upload", uploadId, slug, force?, sha256?, timeoutMs? }`
installs a committed upload into the default agent workspace
`skills/<slug>` directory. The slug and force value must match the
original `skills.upload.begin` request. Rejected unless
`skills.install.allowUploadedArchives` is enabled; the setting does not
affect ClawHub installs.
- Gateway installer mode: `{ name, installId, timeoutMs? }` runs a declared
`metadata.openclaw.install` action on the gateway host. Older clients may
still send `dangerouslyForceUnsafeInstall`; this field is deprecated,
accepted only for protocol compatibility, and ignored. Use
`security.installPolicy` for operator-owned install decisions.
- `skills.update` (`operator.admin`) has two modes:
- ClawHub mode updates one tracked slug or all tracked ClawHub installs in
the default agent workspace.
- Config mode patches `skills.entries.<skillKey>` values such as `enabled`,
`apiKey`, and `env`.
### `models.list` views
`models.list` accepts an optional `view` parameter
(`src/agents/model-catalog-visibility.ts`):
- Omitted or `"default"`: if `agents.defaults.models` is configured, the
response is the allowed catalog, including dynamically discovered models
for `provider/*` entries. Otherwise the response is the full gateway
catalog.
- `"configured"`: picker-sized behavior. If `agents.defaults.models` is
configured, it still wins, including provider-scoped discovery for
`provider/*` entries. Without an allowlist, the response uses explicit
`models.providers.<provider>.models` entries, falling back to the full
catalog only when no configured model rows exist.
- `"all"`: full gateway catalog, bypassing `agents.defaults.models`. Use for
diagnostics/discovery UIs, not normal model pickers.
## 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`).
- For `host=node`, `exec.approval.request` must include `systemRunPlan`
(canonical `argv`/`cwd`/`rawCommand`/session metadata). Requests missing
`systemRunPlan` are rejected.
- After approval, forwarded `node.invoke system.run` calls reuse that
canonical `systemRunPlan` as the authoritative command/cwd/session context.
- If a caller mutates `command`, `rawCommand`, `cwd`, `agentId`, or
`sessionKey` between prepare and the final approved `system.run` forward,
the gateway rejects the run instead of trusting the mutated payload.
## Agent delivery fallback
- `agent` requests can include `deliver=true` to request outbound delivery.
- `bestEffortDeliver=false` (the default) keeps strict behavior: unresolved or
internal-only delivery targets return `INVALID_REQUEST`.
- `bestEffortDeliver=true` allows fallback to session-only execution when no
external deliverable route can be resolved (for example internal/webchat
sessions or ambiguous multi-channel configs).
- Final `agent` results may include `result.deliveryStatus` when delivery was
requested, using the same `sent`, `suppressed`, `partial_failed`, and
`failed` statuses documented for
[`openclaw agent --json --deliver`](/cli/agent#json-delivery-status).
## Versioning
- `PROTOCOL_VERSION`, `MIN_CLIENT_PROTOCOL_VERSION`,
`MIN_NODE_PROTOCOL_VERSION`, and `MIN_PROBE_PROTOCOL_VERSION` live in
`packages/gateway-protocol/src/version.ts`.
- Clients send `minProtocol` + `maxProtocol`. Operator and UI clients must
include the current protocol in that range; current clients and servers run
protocol v4.
- Authenticated clients with both `role: "node"` and `client.mode: "node"`
may use the N-1 node protocol (currently v3). Lightweight restart probes use
the same N-1 window. Device auth, pairing, scopes, command policy, and exec
approvals are unchanged by this compatibility window. Plugin-owned node
capabilities and commands are withheld until the node upgrades to the current
protocol because their hosted surfaces are not part of the N-1 contract.
- Schemas and models are generated from TypeBox definitions:
- `pnpm protocol:gen`
- `pnpm protocol:gen:swift`
- `pnpm protocol:check`
### Client constants
The reference client implementation lives in `packages/gateway-client/src/`
(OpenClaw wraps it via the thin `src/gateway/client.ts` facade). These
defaults are stable across protocol v4 and are the expected baseline for
third-party clients.
| Constant | Default | Source |
| ----------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `PROTOCOL_VERSION` | `4` | `packages/gateway-protocol/src/version.ts` |
| `MIN_CLIENT_PROTOCOL_VERSION` | `4` | `packages/gateway-protocol/src/version.ts` |
| `MIN_NODE_PROTOCOL_VERSION` | `3` | `packages/gateway-protocol/src/version.ts` |
| `MIN_PROBE_PROTOCOL_VERSION` | `3` | `packages/gateway-protocol/src/version.ts` |
| Request timeout (per RPC) | `30_000` ms | `packages/gateway-client/src/client.ts` (`requestTimeoutMs`) |
| Preauth / connect-challenge timeout | `15_000` ms | `packages/gateway-client/src/timeouts.ts` (`OPENCLAW_HANDSHAKE_TIMEOUT_MS` env can raise the paired server/client budget) |
| Initial reconnect backoff | `1_000` ms | `packages/gateway-client/src/client.ts` (`backoffMs`) |
| Max reconnect backoff | `30_000` ms | `packages/gateway-client/src/client.ts` (`scheduleReconnect`) |
| Fast-retry clamp after device-token close | `250` ms | `packages/gateway-client/src/client.ts` |
| Force-stop grace before `terminate()` | `250` ms | `FORCE_STOP_TERMINATE_GRACE_MS` |
| `stopAndWait()` default timeout | `1_000` ms | `STOP_AND_WAIT_TIMEOUT_MS` |
| Default tick interval (pre `hello-ok`) | `30_000` ms | `packages/gateway-client/src/client.ts` |
| Tick-timeout close | code `4000` when silence exceeds `tickIntervalMs * 2` | `packages/gateway-client/src/client.ts` |
| `MAX_PAYLOAD_BYTES` | `25 * 1024 * 1024` (25 MB) | `src/gateway/server-constants.ts` |
The server advertises the effective `policy.tickIntervalMs`,
`policy.maxPayload`, and `policy.maxBufferedBytes` in `hello-ok`; clients
should honor those values rather than the pre-handshake defaults.
## Auth
- Shared-secret gateway auth uses `connect.params.auth.token` or
`connect.params.auth.password`, depending on the configured
`gateway.auth.mode` (`"none" | "token" | "password" | "trusted-proxy"`).
- Identity-bearing modes such as Tailscale Serve (`gateway.auth.allowTailscale: true`)
or non-loopback `gateway.auth.mode: "trusted-proxy"` satisfy the connect
auth check from request headers instead of `connect.params.auth.*`.
- Private-ingress `gateway.auth.mode: "none"` skips shared-secret connect auth
entirely; do not expose that mode on public/untrusted ingress.
- After pairing, the gateway issues a device token scoped to the connection
role + scopes, returned in `hello-ok.auth.deviceToken`. Clients should
persist it after any successful connect.
- Reconnecting with that stored device token should also reuse the stored
approved scope set for that token. This preserves read/probe/status access
already granted and avoids silently collapsing reconnects to a narrower
implicit admin-only scope.
- Client-side connect auth assembly (`selectConnectAuth` in
`packages/gateway-client/src/client.ts`):
- `auth.password` is orthogonal and always forwarded when set.
- `auth.token` is populated in priority order: explicit shared token first,
then an explicit `deviceToken`, then a stored per-device token (keyed by
`deviceId` + `role`).
- `auth.bootstrapToken` is sent only when none of the above resolved
`auth.token`. A shared token or any resolved device token suppresses it.
- Auto-promotion of a stored device token on the one-shot
`AUTH_TOKEN_MISMATCH` retry is gated to trusted endpoints only: loopback,
or `wss://` with a pinned `tlsFingerprint`. Public `wss://` without pinning
does not qualify.
- Built-in setup-code bootstrap returns the primary node
`hello-ok.auth.deviceToken` plus a bounded operator token in
`hello-ok.auth.deviceTokens` for trusted mobile handoff. The operator token
includes `operator.talk.secrets` for native Talk configuration reads, but
excludes pairing-mutation scopes and `operator.admin`.
- While a non-baseline setup-code bootstrap waits for approval,
`PAIRING_REQUIRED` details include `recommendedNextStep: "wait_then_retry"`,
`retryable: true`, and `pauseReconnect: false`. Keep reconnecting with the
same bootstrap token until the request is approved or the token becomes
invalid.
- Persist `hello-ok.auth.deviceTokens` only when the connect used bootstrap
auth on a trusted transport such as `wss://` or loopback/local pairing.
- If a client supplies an explicit `deviceToken` or explicit `scopes`, that
caller-requested scope set remains authoritative; cached scopes are only
reused when the client is reusing the stored per-device token.
- Device tokens can be rotated/revoked via `device.token.rotate` and
`device.token.revoke` (requires `operator.pairing`). Rotating or revoking a
node or other non-operator role also requires `operator.admin`.
- `device.token.rotate` returns rotation metadata. It echoes the replacement
bearer token only for same-device calls already authenticated with that
device token, so token-only clients can persist their replacement before
reconnecting. Shared/admin rotations do not echo the bearer token.
- Token issuance, rotation, and revocation stay bounded to the approved role
set recorded in that device's pairing entry; token mutation cannot expand or
target a device role that pairing approval never granted.
- For paired-device token sessions, device management is self-scoped unless
the caller also has `operator.admin`: non-admin callers can manage only the
operator token for their own device entry. Node and other non-operator token
management is admin-only, even for the caller's own device.
- `device.token.rotate` and `device.token.revoke` also check the target
operator token scope set against the caller's current session scopes.
Non-admin callers cannot rotate or revoke a broader operator token than they
already hold.
- Auth failures include `error.details.code` plus recovery hints:
- `error.details.canRetryWithDeviceToken` (boolean)
- `error.details.recommendedNextStep`: one of `retry_with_device_token`,
`update_auth_configuration`, `update_auth_credentials`,
`wait_then_retry`, `review_auth_configuration`
(`packages/gateway-protocol/src/connect-error-details.ts`).
- Client behavior for `AUTH_TOKEN_MISMATCH`:
- Trusted clients may attempt one bounded retry with a cached per-device
token.
- If that retry fails, stop automatic reconnect loops and surface operator
action guidance.
- `AUTH_SCOPE_MISMATCH` means the device token was recognized but does not
cover the requested role/scopes. Do not present this as a bad token; prompt
the operator to re-pair or approve the narrower/broader scope contract.
## Device identity and 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.
- Pairing auto-approval is centered on direct local loopback connects.
- OpenClaw also has a narrow backend/container-local self-connect path for
trusted shared-secret helper flows.
- Same-host tailnet or LAN connects are still treated as remote for pairing
and require approval.
- WS clients normally include `device` identity during `connect` (operator +
node). The only device-less operator exceptions are explicit trust paths:
- `gateway.controlUi.allowInsecureAuth=true` for localhost-only insecure
HTTP compatibility.
- successful `gateway.auth.mode: "trusted-proxy"` operator Control UI auth.
- `gateway.controlUi.dangerouslyDisableDeviceAuth=true` (break-glass, severe
security downgrade).
- direct-loopback `gateway-client` backend RPCs on the reserved internal
helper path.
- Omitting device identity has scope consequences. When a device-less
operator connection is allowed through an explicit trust path, OpenClaw
still clears self-declared scopes to an empty set unless that path has a
named scope-preservation exception. Scope-gated methods then fail with
`missing scope`.
- `gateway.controlUi.dangerouslyDisableDeviceAuth=true` is a Control UI
break-glass scope-preservation path. It does not grant scopes to arbitrary
custom backend or CLI-shaped WebSocket clients.
- The reserved direct-loopback `gateway-client` backend helper path preserves
scopes only for internal local control-plane RPCs; custom backend IDs do
not receive this exception.
- 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`
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`
(`buildDeviceAuthPayloadV3` in `packages/gateway-client/src/device-auth.ts`),
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 and pinning
- TLS is supported for WS connections (`gateway.tls` config).
- Clients may optionally pin the gateway cert fingerprint via
`gateway.remote.tlsFingerprint` or CLI `--tls-fingerprint`.
## Scope
This protocol exposes the full gateway API: status, channels, models, chat,
agent, sessions, nodes, approvals, and more. The exact surface is defined by
the TypeBox schemas re-exported from `packages/gateway-protocol/src/schema.ts`.
## Related
- [Bridge protocol](/gateway/bridge-protocol)
- [Gateway runbook](/gateway)