Gateway WebSocket Protocol: Handshake, Frames, and Versioning

This page describes the Gateway WebSocket protocol used as the control plane and node transport in OpenClaw. It covers handshake roles and scopes, frame formats, and versioning for operator and node clients.

Read this when

  • Implementing or updating gateway WS clients
  • Debugging protocol mismatches or connect failures
  • Regenerating protocol schema/models

The Gateway WS protocol acts as the single control plane and node transport within OpenClaw. Operator and node clients (CLI, web UI, macOS app, iOS/Android nodes, headless nodes) establish a WebSocket connection and declare a role and scope during the handshake.

npm packages

These packages ship as part of OpenClaw release trains. During the initial rollout, npm may return E404 until the first package-bearing release becomes available.

For application lifecycle guidance, refer to Building a Gateway client. For apps that manage the Gateway as a child process, see Embedding OpenClaw.

Transport and framing

  • WebSocket, text frames, JSON payloads.
  • The first frame must be a connect request.
  • Pre-connect frames are limited to 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?}

Response errors use { code, message, details?, retryable?, retryAfterMs? }. Clients should branch on code and details.code; message remains human-readable and can change except where a compatibility note says otherwise. Method-level authorization failures use top-level code: "FORBIDDEN" with structured missing-scope details:

  • Missing scope: { code: "MISSING_SCOPE", missingScope, requiredScopes }. requiredScopes is the complete known scope set for the requested operation. The legacy missing scope: <scope> message is retained for older clients.

Clients should read details first and use the legacy message only as a compatibility fallback. readMissingScopeError and readMissingScopeErrorDetails are exported from @openclaw/gateway-protocol/gateway-error-details; the browser-safe gateway client re-exports them from @openclaw/gateway-client/browser.

The schemas are exported as GatewayErrorDetailsSchema, MissingScopeErrorDetailsSchema from @openclaw/gateway-protocol/schema. HTTP scope failures mirror the MISSING_SCOPE object under error.details and use HTTP status 403.

Side-effecting methods require idempotency keys (see schema).

Handshake

The Gateway sends a pre-connect challenge:

{
  "type": "event",
  "event": "connect.challenge",
  "payload": { "nonce": "…", "ts": 1737264000000 }
}

The client responds with connect:

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

The Gateway replies with hello-ok:

{
  "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 must all be provided to HelloOkSchema (packages/gateway-protocol/src/schema/frames.ts). Even when no device token is issued, auth still indicates the negotiated role and scopes (see the shape above). The optional pluginSurfaceUrls maps plugin surface names (for instance, canvas) to scoped hosted URLs; because it can expire, nodes invoke node.pluginSurface.refresh with { "surface": "canvas" } to obtain a current entry. The older canvasHostUrl / canvasCapability / node.canvas.capability.refresh path is deprecated and unsupported; switch to plugin surfaces instead. Within the snapshot, the optional appliedConfigHash holds the resolved source-config revision that the active Gateway runtime accepted. Clients can compare it against config.get.configRevisionHash to see if a newer saved config still requires a restart. config.get.hash stays as the raw root-file revision used for config write conflict guards.

While the gateway is still completing startup sidecars, connect may return a retryable UNAVAILABLE error that includes details.reason: "startup-sidecars" and retryAfterMs. Retry within your connection budget rather than treating it as a terminal handshake failure.

When a device token is issued, hello-ok.auth includes it:

{
  "auth": {
    "deviceToken": "…",
    "role": "operator",
    "scopes": ["operator.read", "operator.write"]
  }
}

Built-in QR and setup-code bootstrap is a mobile handoff path. A successful baseline setup-code connection yields a primary node token along with one bounded operator token:

{
  "auth": {
    "deviceToken": "…",
    "role": "node",
    "scopes": [],
    "deviceTokens": [
      {
        "deviceToken": "…",
        "role": "operator",
        "scopes": ["operator.approvals", "operator.read", "operator.talk.secrets", "operator.write"]
      }
    ]
  }
}

This operator handoff is intentionally bounded: sufficient to start the mobile operator loop and native setup, including operator.talk.secrets for Talk config reads, but without any pairing-mutation scopes or operator.admin. Broader pairing and admin access requires a separate approved pairing or token flow. Only persist hello-ok.auth.deviceTokens when bootstrap authentication used a trusted transport (wss:// or loopback or local pairing).

Trusted same-process backend clients (client.id: "gateway-client", client.mode: "backend") may skip device on direct loopback connections when authenticating with the shared gateway token or password. This path is reserved for internal control-plane RPCs (such as subagent session updates) and prevents stale CLI or device pairing baselines from blocking local backend work. Remote, browser-origin, node, and explicit device-token or device-identity clients still follow normal pairing and scope-upgrade checks.

Worker role and closed protocol

Cloud workers use a dedicated loopback ingress through the gateway-owned, host-key-pinned SSH tunnel. It accepts only worker identity and never dispatches general authentication, node events, operator RPCs, or plugin methods. A strict connect validates a hash-at-rest, short-lived credential tied to the environment, bundle hash, owner epoch, RPC-set version, expiry, and one nullable session; it separately checks the current version and feature set. On success it returns minimal worker-hello-ok; feature negotiation is independent of the general protocol version. Frames are kept under 64 KiB, except a negotiated worker.inference.start frame that can be up to 25 MiB. The closed allowlist consists of worker.heartbeat, worker.transcript.commit, worker.live-event, worker.inference.start, and worker.inference.cancel.

Transcript commits use owner-epoch fencing, a gateway-owned session binding, base-leaf compare-and-swap, and durable sequence replay; the gateway generates transcript entry and parent IDs through the normal session writer. Ownership and expiry are rechecked on each RPC.

Client capabilities

Operator clients can advertise optional capabilities inside connect.params.caps:

  • tool-events: accepts structured tool lifecycle events.
  • inline-widgets: can render hosted inline widget tool results.

Client capabilities describe the connected client, not authorization. Agent tools may declare required capabilities; the Gateway omits those tools unless every requirement appears in the originating client's caps. Channel-originated runs have no Gateway client capabilities, so capability-gated tools are unavailable even when tool policy explicitly allows them.

Node connect example

{
  "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 (for example, screen.record, camera.capture).

The gateway treats these as claims and enforces server-side allowlists.

Roles and scopes

For complete details on the operator scope model, approval-time validations, and shared-secret behavior, refer to Operator scopes.

Roles:

  • operator: client for the control plane (CLI, UI, or automation).
  • node: capability host (camera, screen, canvas, system.run).
  • worker: cloud execution host operating over the dedicated, closed worker protocol.

Operator scopes (src/gateway/operator-scopes.ts), the exhaustive set:

  • operator.read
  • operator.write
  • operator.admin
  • operator.approvals
  • operator.pairing
  • operator.talk.secrets

Using talk.config together with includeSecrets: true demands operator.talk.secrets (or operator.admin). When secrets are involved, the active Talk provider credential is read from talk.resolved.config.apiKey; talk.providers.<id>.apiKey retains its source shape and can be a SecretRef object or a redacted string.

Gateway RPC methods registered by plugins can 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 acts only as the first gate. Some slash commands accessed through chat.send enforce stricter command-level checks: persistent /config set and /config unset writes require operator.admin even for gateway clients that already possess a lower operator scope.

node.pair.approve applies an extra approval-time scope check on top of the base method scope (operator.pairing), determined by the pending request's declared commands (src/infra/node-pairing-authz.ts):

Declared commandsRequired scopes
noneoperator.pairing
ordinary commandsoperator.pairing + operator.write
includes system.run, system.run.prepare, system.which, browser.proxy, fs.listDir, or system.execApprovals.get/setoperator.pairing + operator.admin

Caps/commands/permissions (node)

At connection time, nodes declare their capability claims:

  • caps: high-level capability categories such as camera, canvas, screen, location, voice, and talk.
  • commands: command allowlist for invoke.
  • permissions: granular toggles (for example screen.record, camera.capture).

The Gateway treats these as claims and enforces allowlists on the server side. Connected nodes may publish optional agent-visible plugin or MCP tool descriptors using node.pluginTools.update after a successful connect or reconnect. Headless node hosts restart to apply declarative MCP inventory changes. This update mechanism is the only publication route; plugin tool descriptors are not accepted in connect params. Each descriptor must use a provider-safe tool name and name a command present in the node's current command allowlist. The Gateway trusts descriptor metadata from the paired node, filters descriptors outside the approved command surface, removes them when the node disconnects, and rejects operator attempts to mutate another node's catalog. Set gateway.nodes.pluginTools.enabled: false to ignore descriptors published by the node.

Every connected node host publishes its full skill replacement catalog using node.skills.update. This role-based mechanism is the sole way nodes publish skills; the Gateway does not accept skills inside connect parameters. Each descriptor carries a safe name, a description, and bounded SKILL.md content. The Gateway parses that content with the standard skills loader, incorporates it into agent skill snapshots while the node stays connected, and removes it upon disconnection. Set gateway.nodes.allowSkills: false to disregard skills published by nodes.

Presence

  • system-presence returns entries keyed by device identity, containing deviceId, roles, and scopes, so UIs can display one row per device even when that device connects as both an operator and a node.
  • node.list includes optional lastSeenAtMs and lastSeenReason. Connected nodes report the current connection time with reason connect; paired nodes can also report durable background presence through a trusted node event.

Native macOS nodes can additionally send authenticated node.presence.activity events with a bounded input idle time. The Gateway derives activity timestamps using its own clock, exposes the most recent connected Mac through node.list and node.describe, and pushes node.presence updates to read-scoped clients. The app sends { "action": "clear" } when the user disables activity sharing; the Gateway clears timestamps only for that specific authenticated node connection. Gateways that predate this acknowledged action return it as unhandled, so the Mac node reconnects once and lets disconnect cleanup remove the old connection state. See Active computer presence for selection, privacy, model context, and notification-routing behavior.

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 as connected:

{
  "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 persists only for authenticated node device sessions; sessions without a device or that are unpaired return handled: false.

Successful gateways return a structured result:

{
  "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 as durable presence persistence.

Broadcast event scoping

Server-pushed broadcast events are scope-gated so that 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 lacking 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 and transport events (heartbeat, presence, tick, connect/disconnect lifecycle) remain unrestricted so transport health is visible 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 maintains 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 and 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.

System and identity

  • health provides the gateway health status, either from a cache or by performing a fresh probe.
  • diagnostics.stability gives access to the recent bounded diagnostic stability recorder, which includes event names, counts, byte sizes, memory readings, queue and session state, channel and plugin names, and session IDs. It excludes chat text, webhook bodies, tool outputs, raw request and response bodies, tokens, cookies, and secrets. The operator.read setting is required.
  • status delivers a gateway summary in the /status style, with sensitive fields only accessible to operator clients that have admin scope.
  • gateway.identity.get returns the gateway device identity utilized in relay and pairing flows.
  • system-presence provides the current presence snapshot for connected operator and node devices.
  • system-event adds a system event and can update or broadcast presence context.
  • last-heartbeat retrieves the most recent heartbeat event that has been persisted.
  • set-heartbeats toggles the processing of heartbeats on the gateway.
  • gateway.suspend.prepare creates a short cooperative suspension lease only when tracked Gateway work is idle. The lease is checked by gateway.suspend.status and released by gateway.suspend.resume after a thaw or an aborted host operation.

Models and usage

  • models.list returns the model catalog permitted at runtime. Refer to the "models.list views" section below.
  • usage.status provides summaries of provider usage windows and remaining quota.
  • usage.cost returns aggregated cost usage summaries for a specified date range. Use agentId for a single agent, or agentScope: "all" to aggregate across configured agents.
  • doctor.memory.status returns vector memory and cached embedding readiness for the active default agent workspace. Pass { "probe": true } or { "deep": true } only for an explicit live embedding provider ping. Use { "agentId": "agent-id" } to scope Dreaming store statistics to one agent workspace; omitting it aggregates statistics for all configured Dreaming workspaces.
  • doctor.memory.dreamDiary, doctor.memory.backfillDreamDiary, doctor.memory.resetDreamDiary, doctor.memory.resetGroundedShortTerm, doctor.memory.repairDreamingArtifacts, and doctor.memory.dedupeDreamDiary all accept an optional { "agentId": "agent-id" } parameter. If omitted, they operate on the configured default agent workspace.
  • doctor.memory.remHarness returns a bounded, read-only REM harness preview intended for remote control plane clients. This includes workspace paths, memory snippets, rendered grounded markdown, and deep promotion candidates. The operator.read requirement must be met.
  • sessions.usage returns per session usage summaries. Pass agentId for one agent, or agentScope: "all" to list all configured agents together. Both usage methods accept mode: "specific" with an IANA timeZone for DST aware calendar day boundaries and buckets. utcOffset remains supported for older clients and as a fallback when the Gateway runtime does not recognize the requested zone.
  • sessions.usage.timeseries returns timeseries usage data for a single session.
  • sessions.usage.logs returns usage log entries for a single session.

Channels and login helpers

  • channels.status returns status summaries for built in and bundled channels and plugins.
  • channels.logout logs out a specific channel or account when the channel supports that action.
  • web.login.start initiates a QR or web login flow for the current QR capable web channel provider.
  • web.login.wait waits for that flow to complete and starts the channel upon success.
  • push.test sends a test APNs push notification to a registered iOS node.
  • voicewake.get returns the stored wake word triggers.
  • voicewake.set updates wake word triggers and broadcasts the change.

Plugin management

  • plugins.list (operator.read) provides a list of installed plugins alongside locally curated official recommendations, diagnostic information, and an indicator of whether the current installation mode permits modifications.
  • plugins.search (operator.read) looks through installable ClawHub code-plugin and bundle-plugin families. Provide a non-empty query and an optional limit ranging from 1 to 100.
  • plugins.install (operator.admin) sets up either an official catalog entry using { source: "official", pluginId } or a ClawHub package with { source: "clawhub", packageName, version?, acknowledgeClawHubRisk? }. ClawHub installations maintain Gateway trust, integrity, and install-policy verification. A Gateway restart is needed for a successful installation.
  • plugins.setEnabled (operator.admin) modifies the enabled policy of one installed plugin through { pluginId, enabled }. The reply contains the updated catalog entry, restart metadata, and any warnings about slot selection.
  • plugins.uninstall (operator.admin) deletes one externally installed plugin via { pluginId }: configuration references, the installation record, and managed files. Bundled plugins cannot be removed, only turned off. The reply lists the removal steps and always demands a Gateway restart.

Messaging and logs

  • send serves as the direct outbound-delivery RPC for sending messages to channels, accounts, or threads outside the chat runner.
  • logs.tail retrieves the configured gateway file-log tail with cursor and limit controls as well as a maximum byte constraint.

Operator terminal

  • terminal.open launches a host PTY for a specified agentId or the default agent and returns the resolved agent, working directory, shell, and confinement status.
  • terminal.input, terminal.resize, and terminal.close function exclusively on sessions owned by the calling connection.
  • terminal.upload accepts a single base64 file up to 16 MiB, places it in a private 24-hour temporary directory on the session's Gateway or paired-node host, and provides the absolute path. The caller must still paste or otherwise use that path; the RPC never writes terminal input or runs a command.
  • terminal.data and terminal.exit events stream only to the connection that owns the session.
  • Sessions whose connection drops are detached, not terminated: they remain 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, including detached ones.

Talk and TTS

  • talk.catalog retrieves the read-only catalog of Talk providers for speech, streaming transcription, and realtime voice. This includes canonical provider IDs, registry aliases, labels, configured state, an optional group-level ready result, exposed model and voice IDs, canonical modes, transports, brain strategies, and realtime audio or capability flags. Provider secrets are not returned, and global configuration remains unchanged. Current gateways set ready once runtime provider selection is applied; if it is absent, treat the gateway as unverified on older versions.
  • talk.config provides the active Talk configuration payload; includeSecrets demands operator.talk.secrets (or operator.admin).
  • talk.session.create establishes a Talk session owned by the gateway for realtime/gateway-relay, transcription/gateway-relay, or stt-tts/managed-room. For stt-tts/managed-room, callers using operator.write who supply sessionKey must also include spawnedBy to limit session-key visibility to the current scope; unscoped sessionKey creation and brain: "direct-tools" require operator.admin.
  • talk.session.join validates a session token for a managed room, emitting session.ready or session.replaced when appropriate. It returns room and session metadata along with recent Talk events, but never exposes the plaintext token or its hash.
  • talk.session.appendAudio adds base64-encoded PCM input audio to realtime relay and transcription sessions that the gateway owns.
  • talk.session.startTurn, talk.session.endTurn, and talk.session.cancelTurn manage the turn lifecycle in a managed room, rejecting stale turns before clearing state.
  • talk.session.cancelOutput halts assistant audio output, primarily used for VAD-gated barge-in within gateway relay sessions.
  • talk.session.submitToolResult finalizes a provider tool call generated by a gateway-owned realtime relay session. The request waits for any asynchronous completion signal from the provider bridge; if submission fails, the linked run remains active and no successful tool-result event is emitted. Supply options: { willContinue: true } for interim tool output, or options: { suppressResponse: true } when the provider bridge supports suppression and the result should not trigger another response.
  • talk.session.steer sends voice control for an active run into a gateway-owned agent-backed Talk session: { sessionId, text, mode? }, where mode is status, steer, cancel, or followup; if no mode is specified, it is inferred from the spoken text.
  • talk.session.close terminates a relay, transcription, or managed-room session owned by the gateway and emits terminal Talk events.
  • talk.mode sets or broadcasts the current Talk mode state for WebChat and Control UI clients.
  • talk.client.create creates or resumes a client-owned realtime provider session using webrtc or provider-websocket, while the gateway retains ownership of credentials, instructions, tool policy, and the returned voiceSessionId. Clients provide sessionKey and reuse voiceSessionId when swapping the provider transport during a single call.
  • talk.client.transcript appends a finalized { role, text } item to the standard agent session. The required entryId is idempotent within voiceSessionId; retries will not duplicate transcript messages.
  • talk.client.close closes the logical voice session after pending transcript writes are complete. This operation is idempotent and may send a mutation-only call digest to the last non-WebChat channel associated with the session.
  • talk.client.toolCall allows client-owned realtime transports to forward provider tool calls to the gateway policy. The first supported tool is openclaw_agent_consult; clients receive a run ID and must wait for normal chat lifecycle events before submitting the provider-specific tool result. Voice-bound high-impact actions return VOICE_CONFIRMATION_REQUIRED:<id> until a later finalized user utterance explicitly confirms that action, and the next consult provides the confirmationId.
  • talk.client.steer sends voice control for an active run on client-owned realtime transports. The gateway resolves the active embedded run from sessionKey and returns a structured accepted or rejected result, rather than silently discarding the steering command.
  • talk.event serves as the single Talk event channel for realtime, transcription, STT/TTS, managed-room, telephony, and meeting adapters.
  • talk.speak generates speech through the currently active Talk speech provider.
  • tts.status returns TTS enabled status, the active provider, fallback providers, and provider configuration state.
  • tts.providers returns the visible inventory of TTS providers.
  • The TTS preferences state is toggled by tts.enable and tts.disable.
  • Which TTS provider is preferred gets updated through tts.setProvider.
  • A one-shot text-to-speech conversion is executed by tts.convert.
  • With tts.speak (operator.write), any non-empty text is rendered using the configured general TTS provider chain. The result is a single complete clip returned inline as audioBase64, accompanied by provider and optional metadata fields outputFormat, mimeType, and fileExtension. In contrast to tts.convert, no Gateway-local path is provided; unlike talk.speak, a Talk provider is not mandatory. When text exceeds tts.maxTextLength, INVALID_REQUEST is returned; if synthesis fails, UNAVAILABLE is returned instead.

Secrets, config, update, and wizard

  • secrets.reload re-evaluates active SecretRefs and atomically publishes runtime state that is owner-aware. When owner failures are eligible, they may publish as cold or stale degradation using warningCount; strict or unmapped failures block the reload and keep the active snapshot intact.
  • secrets.resolve resolves secret assignments for commands and targets within a particular command/target set.
  • config.get provides the current config snapshot stored on disk, the raw root-file hash, the resolved configRevisionHash, and optionally appliedConfigHash for the resolved revision accepted by the active Gateway runtime.
  • config.set commits a validated configuration payload.
  • config.patch integrates a partial config update. For destructive array replacement, the affected path must be listed in replacePaths; nested arrays within array entries use [] paths like agents.entries.*.skills.
  • config.apply validates and replaces the entire configuration payload.
  • config.schema delivers the live config schema payload used by Control UI and CLI tools: schema, uiHints, version, generation metadata, plugin and channel schema metadata when loadable. It includes title / description metadata from the same labels and help text as the UI, covering 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 plus 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 matches the gateway config reload planner for the requested path. Lookup schema nodes retain user-facing documentation 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.
  • The gateway update flow is executed by update.run, which only schedules a restart when the update completes successfully. For callers holding a session, continuationMessage can be included so that 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 rather than replacing the package tree or mutating checkout and build output inside the live gateway. A started handoff returns ok: true along with result.reason: "managed-service-handoff-started" and handoff.status: "started". If a second concurrent update.run is handled by the same Gateway process, it returns ok: false with result.reason: "managed-service-handoff-already-running" and handoff.status: "already-running"; its continuation is not accepted, so the caller may retry once the active update finishes. Standalone CLI updaters and replacement Gateway processes are not covered by this process-local guard. When a handoff is unavailable or fails, ok: false is returned with managed-service-handoff-unavailable or managed-service-handoff-failed, and handoff.command is added when a manual shell update is required. Unavailable means OpenClaw lacks a safe supervisor boundary or durable service identity, for instance 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.
  • The latest update restart sentinel, including the post-restart running version when available, is refreshed and returned by update.status.
  • The onboarding wizard is exposed over WS RPC through wizard.start, wizard.next, wizard.status, and wizard.cancel.

Agent and workspace helpers

  • agents.list retrieves agent entries visible to the gateway, containing effective model and runtime metadata along with optional semantic kind (agent or system). Clients that advertise the agent-kind handshake capability get the full typed roster; those without it receive the legacy selector-safe roster that omits system rows. Kind-aware clients exclude system rows from regular selectors but keep them available in diagnostic views. Older v4 gateways might return rows lacking kind.

  • agents.create, agents.update, and agents.delete handle agent records and workspace configuration.

  • agents.files.list, agents.files.get, and agents.files.set manage the bootstrap workspace files made available for an agent.

  • audit.activity.list provides the versioned activity ledger limited to metadata; audit.list remains the compatibility-safe RPC for runs and tools.

  • agents.workspace.list and agents.workspace.get (operator.read) offer read-only, paginated browsing of an agent's workspace directory for clients within the trusted operator domain described in Operator scopes. Requests accept only workspace-relative paths; reads are confined to the realpathed workspace root (symlink and hardlink escapes are rejected), capped by size, and restricted to UTF-8 text plus common image types (base64). The host workspace path is never exposed in responses. No write operations exist in this namespace.

  • tasks.list, tasks.get, and tasks.cancel expose the gateway task ledger to SDK and operator clients. See Task ledger RPCs below.

  • artifacts.list, artifacts.get, and artifacts.download provide transcript-derived artifact summaries and downloads for a specific sessionKey, runId, or taskId scope. Run and task queries resolve the owning session server-side and return only transcript media with matching provenance; unsafe or local URL sources return unsupported downloads rather than performing server-side fetches.

  • environments.list and environments.status maintain gateway-local and node environment discovery. Configured cloud workers and durable records from earlier profiles add worker metadata with providerId, optional leaseId, state, ageMs, optional idleMs, and attachedSessionIds. Worker lifecycle states are requested, provisioning, bootstrapping, ready, attached, idle, draining, destroying, destroyed, failed, and orphaned.

  • environments.create ({ profileId, idempotencyKey }) provisions a worker from a configured plugin provider profile; retries with the same key reuse the durable operation. environments.destroy ({ environmentId }) requests idempotent teardown of a durable worker environment. Both require operator.admin, are control-plane writes, and return the same environment summary shape used by status responses.

  • agent.identity.get returns the effective assistant identity for an agent or session.

  • agent.wait waits for a run to finish and provides the terminal snapshot when available.

Session control

  • sessions.list provides the current session index, which includes agentRuntime metadata on a per-row basis when an agent runtime backend is configured. If cloud-worker placement is active or durable recovery state exists, session rows additionally contain a closed placement state (local, requested, provisioning, syncing, starting, active, draining, reconciling, reclaimed, or failed) along with fields specific to that state, such as environment, owner-epoch, workspace, bundle, ACK-cursor, or recovery details.
  • Subscriptions to session change events for the current WebSocket client are toggled by sessions.subscribe and sessions.unsubscribe.
  • Transcript and message event subscriptions for a single session are toggled using sessions.messages.subscribe and sessions.messages.unsubscribe. Include includeApprovals: true to also receive sanitized session.approval lifecycle events for approvals where the persisted audience includes that exact session and the reviewer binding authorizes the subscribing client. The subscribe response then contains a bounded pending approvalReplay, which is authoritative only when truncated is false. This opt-in applies per subscribe call and is not sticky: re-subscribing to the same session without includeApprovals: true removes any existing approval subscription. Beyond standard session-read authority, this opt-in requires operator.admin, or operator.approvals on a paired device.
  • For specific session keys, sessions.preview returns bounded transcript previews.
  • An exact session key yields one gateway session row through sessions.describe.
  • A session target is resolved or canonicalized by sessions.resolve.
  • A new session entry is created by sessions.create. Optional model and thinkingLevel values atomically persist the initial model and reasoning overrides. A managed worktree is provisioned by worktree: true, with optional worktreeBaseRef and worktreeName selecting the base reference and branch name, and execNode (operator.admin) binding session execution to a node host. The created worktree is echoed in the result and stored on the session row (worktree: { id, branch, repoRoot }). If the entry is created but its nested initial chat.send is rejected, the successful result includes runStarted: false and runError; clients can keep the prompt and retry against the returned session key. A caller passing parentSessionKey with emitCommandHooks: true should also declare the lifecycle disposition of a distinct child: succeedsParent: true ends the parent with session_end, while false keeps the parent active and emits only the child's session_start. Omitting succeedsParent preserves the legacy parent-rollover behavior for existing clients. The disposition requires both parent linkage and command hooks; a fork cannot succeed its parent. Main-session reset-in-place behavior remains unchanged because no distinct child is created. New rows receive write-once creation provenance stamps (createdVia, createdActor, createdAt) from the trusted creation seam; adopting an existing key never restamps it. For human profile actors, createdActor.label is resolved from the current user profile when the row is projected and is never stored on the session entry, so profile renames do not drift. Session rows also include parentSessionKey (navigation parent, persisted), controlOwnerSessionKey (runtime controller when live), forkSource (exact source key and transcript generation for forks), and previousSessionId (prior transcript generation under the same key).
  • An existing local OpenClaw session with a session-owned managed worktree is moved to a configured cloud-worker profile by sessions.dispatch (operator.admin). Pass { key, profileId, agentId? }. This method is absent when no worker profile is configured, closes local turn admission before draining active work, and returns only after placement reaches active worker ownership. Dispatch is one-way; worker-to-local pull-back is not part of this RPC.
  • sessions.groups.list, sessions.groups.put, sessions.groups.rename, and sessions.groups.delete handle the catalog of custom session groups owned by the gateway, including their names and display ordering. The membership data lives in each session's category field; renaming or deleting a group updates the member sessions on the server side.
  • sessions.send dispatches a message into an already existing session.
  • sessions.steer provides the interrupt-and-steer variant for a session that is currently active.
  • sessions.abort stops any active work in a session. You can provide key together with the optional runId, or just runId for active runs the gateway can map to a session. Including runId limits the cancellation to that specific run. Setting clearQueued: true on a key-only non-global request also removes the followup and lane queues belonging to that session. Existing callers that leave out clearQueued keep those queues intact. Using the literal global key preserves the current agent-qualified chat.abort ownership behavior and does not clean up non-global followup or lane data.
  • sessions.patch modifies session metadata and overrides, then returns the resolved canonical model together with the effective agentRuntime. Spawn lineage fields (spawnedBy, spawnedWorkspaceDir, spawnedCwd, spawnDepth, subagentRole, subagentControlScope) can no longer be patched publicly; those values are written exactly once by trusted creation paths, and any request that still includes them is refused.
  • sessions.reset, sessions.delete, and sessions.compact carry out session maintenance tasks.
  • sessions.get retrieves the complete stored session row.
  • Chat execution continues to use chat.history, chat.send, chat.abort, and chat.inject. chat.history is display-normalized for UI clients: inline directive tags are removed 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 or full-width model control tokens are stripped, pure silent-token assistant rows (exact NO_REPLY / no_reply) are excluded, and oversized rows may be replaced with placeholders.
  • chat.message.get is the additive bounded full-message reader for a single visible transcript entry. Supply sessionKey, optionally agentId when session selection is scoped to an agent, and a transcript messageId that was previously surfaced through chat.history; the gateway returns the same display-normalized projection without the lightweight history truncation cap, provided the stored entry is still available and not oversized.
  • chat.toolTitles returns short purpose titles for tool calls shown in the Control UI (batched, up to 24 items with bounded inputs). This feature is opt-in through gateway.controlUi.toolTitles (disabled by default); gateways that have it turned off answer { titles: {}, disabled: true } with no model call, so clients stop asking. When enabled, titles follow standard utility-model routing: either an explicitly configured utilityModel (an operator choice that, like all utility tasks, may send bounded task content to the selected provider), or the session provider's declared small-model default so no new egress destination appears implicitly; an empty utilityModel disables them entirely. Titles never fall back to the primary model. Results are cached in the per-agent state database, keyed by tool name and input, so repeated views never re-bill the same calls.
  • When chat.send receives a one-turn fastMode: "auto", it enables fast mode for model calls that begin before the automatic cutoff, then handles subsequent retry, fallback, tool-result, or continuation calls without fast mode. By default, the cutoff is 60 seconds (DEFAULT_FAST_MODE_AUTO_ON_SECONDS), and you can adjust it per model using agents.defaults.models["<provider>/<model>"].params.fastAutoOnSeconds. A chat.send caller may supply one-turn fastAutoOnSeconds to override the cutoff for that specific request. To override the stored queue mode for only this request, provide queueMode (steer, followup, collect, or interrupt); explicit Control UI steer actions rely on queueMode: "steer". Interactive clients can supply expectedLeafEntryId with the active transcript-branch leaf they are displaying, or null for an authoritative empty transcript; the Gateway rejects the send with details.reason: "active-leaf-changed" if another client has already switched branches.

Device pairing and device tokens

  • device.pair.list returns both pending and approved paired devices.
  • device.pair.setupCode generates a mobile setup code and, by default, a PNG QR data URL. It requires operator.admin and is deliberately excluded from advertised discovery. The response contains setupCode, optional qrDataUrl, gatewayUrl, the non-secret auth label, and urlSource.
  • device.pair.approve, device.pair.reject, and device.pair.remove handle device-pairing records.
  • device.pair.rename sets an operator label ({ deviceId, label }) that takes precedence over the client-reported display name and persists through device repair or re-approval.
  • device.token.rotate rotates a paired device token within the bounds of its approved role and caller scope.
  • device.token.revoke revokes a paired device token within the bounds of its approved role and caller scope.

The setup code embeds a short-lived bootstrap credential. Clients must not log or store it beyond the pairing flow.

Node pairing, invoke, and pending work

  • node.pair.list, node.pair.approve, node.pair.reject, and node.pair.remove address node capability approvals. node.pair.request and node.pair.verify were removed in 2026.7 along with the standalone node pairing store; pending requests are generated by the Gateway during node connections.
  • node.list and node.describe report known or connected node state.
  • node.rename modifies a paired node label.
  • node.invoke sends a command to a connected node.
  • node.invoke.result provides the result for an invoke request.
  • mcp.tools.call.v1 serves as the headless node-host command for invoking a configured node-local MCP tool. It travels through node.invoke, requires the node to declare the command, and remains subject to pairing approval and gateway.nodes.commands.deny.
  • node.event relays node-originated events back into the gateway.
  • node.pluginTools.update is the only publication path for updating the connected node's agent-visible plugin or MCP tool descriptors; connect parameters do not carry them.
  • 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 or disconnected nodes.

Approval families

  • approval.history returns terminal approvals for exec, plugin, and system-agent requests (scope operator.approvals), ordered newest first and kept for 30 days. It supports cursor pagination and an optional kind filter; pending approvals are not included in history rows.
  • approval.get and approval.resolve are the kind-agnostic durable approval methods (scope operator.approvals). approval.get returns a sanitized projection of a pending or retained terminal approval with a stable urlPath; approval.resolve takes the canonical approval id, an explicit kind, and a decision, applies first-answer-wins resolution, and always returns the recorded canonical result.
  • exec.approval.request, exec.approval.get, exec.approval.list, and exec.approval.resolve handle one-shot exec approval requests as well as pending approval lookup and replay. These are protocol-boundary adapters over the same durable approval registry.
  • exec.approval.waitDecision waits for 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 through node relay commands.
  • plugin.approval.request, plugin.approval.list, plugin.approval.waitDecision, and plugin.approval.resolve cover plugin-defined approval flows.

Control UI commands

  • ui.command allows an operator.write caller to send typed layout and navigation commands to connected Control UI clients that advertise the ui-commands capability.
  • Commands include pane split, close, and focus, sidebar visibility, terminal and browser panel visibility and dock, and session navigation.
  • Protocol v1 intentionally broadcasts to every connected capable Control UI. If none is connected, the request fails with UNAVAILABLE rather than pretending the layout changed.

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 needing 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 below.

Common event families

  • chat: Chat interface updates from the UI, including chat.inject and other transcript only chat events. With protocol v4, delta payloads contain deltaText; message holds the cumulative assistant snapshot. When replacements aren't prefix based, replace=true gets set and deltaText serves as the replacement text.
  • session.message, session.operation, session.tool: Updates for a subscribed session covering the transcript, an in flight session operation, and the event stream.
  • session.approval: Cleaned up approval state for both pending and terminal approvals, provided to an exact session subscriber who has explicitly opted in. Child approvals rely on the stored ancestor audience; events never modify transcripts or activate agents.
  • sessions.changed: Session index or metadata was modified.
  • presence: System presence snapshot has been refreshed.
  • tick: A periodic keepalive or liveness event.
  • health: Gateway health snapshot has been updated.
  • heartbeat: Heartbeat event stream has been refreshed.
  • cron: A cron run or job change event.
  • shutdown: Notification that the gateway is shutting down.
  • node.pair.requested / node.pair.resolved: The node pairing lifecycle.
  • node.invoke.request: A broadcast requesting a node invoke.
  • device.pair.requested / device.pair.resolved: The lifecycle for a paired device.
  • voicewake.changed: Wake word trigger configuration was changed.
  • config.changed: A config write was persisted (the payload includes the config path, the new snapshot hash, and a timestamp, but never the config content). Scoped to operator reads; clients should refresh using config.get.
  • exec.approval.requested / exec.approval.resolved: The exec approval lifecycle.
  • plugin.approval.requested / plugin.approval.resolved: The plugin approval lifecycle.

Node helper methods

To retrieve the current list of skill executables for auto allow checks, nodes can call skills.bins.

Audit ledger RPC

audit.activity.list provides operator clients with a stable, newest first view of agent run, tool action, and opt in message lifecycle metadata. This requires operator.read. Queries exclude records older than 30 days, and the shared SQLite ledger is limited to 100,000 records. Expired rows are removed during Gateway startup, hourly maintenance, and subsequent writes. Refer to Audit history for the data model and privacy details.

  • Acceptable parameters: one exact value from agentId, sessionKey, or runId (optional); an optional kind (which can be "agent_run", "tool_action", or "message"); an optional status (selectable from "started", "succeeded", "failed", "cancelled", "timed_out", "blocked", or "unknown"); an optional message direction (either "inbound" or "outbound") along with an exact channel; optional inclusive Unix-millisecond limits via after and before; an optional limit ranging from 1 to 500; and an optional string cursor taken from the prior page.
  • Return value: { "events": AuditActivityEventV1[], "nextCursor"?: string }.

The V1 result union, identified by name, defines separate schemas for agent-run, tool-action, inbound-message, and outbound-message entries. The eventType discriminator uses agent_run, tool_action, inbound_message, or outbound_message respectively; kind and message direction stay available for filtering and display. Each event includes an integer schemaVersion: 1. Message identity references follow the exact hmac-sha256:v1:<32 hex key id>:<64 hex digest> format; a channel-sender actor id uses that same format.

Every variant requires eventType, schemaVersion, eventId, sequence, sourceSequence, occurredAt, kind, action, status, actor, and redaction. The fields specific to each variant are:

eventTypeMandatory fieldsOptional fields
agent_runagentId, runId; kind: "agent_run"sessionKey, sessionId, errorCode
tool_actionagentId, runId; kind: "tool_action"sessionKey, sessionId, toolCallId, toolName, errorCode
inbound_messagedirection: "inbound", channel, conversationKind, outcomeagentId, runId, durationMs, resultCount, identity references, reasonCode, errorCode
outbound_messagedirection: "outbound", channel, conversationKind, outcomeagentId, runId, durationMs, resultCount, identity references, reasonCode, deliveryKind, failureStage, errorCode

The closed message enums consist of:

  • conversationKind: direct, group, channel, or unknown.
  • For inbound traffic, outcome: completed, skipped, or failed; the reasonCode field is not required and accepts duplicate, reply_operation_active, reply_operation_aborted, fast_abort, plugin_bound_handled, plugin_bound_unavailable, plugin_bound_declined, plugin_bound_error, before_dispatch_handled, acp_dispatch_completed, acp_dispatch_failed, acp_dispatch_empty, or acp_dispatch_aborted.
  • For outbound traffic, outcome: sent, suppressed, failed, or unknown; the reasonCode field is optional and can be cancelled_by_message_sending_hook, cancelled_by_reply_payload_sending_hook, empty_after_message_sending_hook, empty_after_reply_payload_sending_hook, or no_visible_payload. An adapter that does not supply a platform identity is unknown, since the external side effect cannot be refuted.
  • deliveryKind: text, media, or other; failureStage: platform_send, queue, or unknown.

Terminal fields are correlated rather than independently optional.

VariantTerminal mapping
Agent runstarted lacks errorCode; any finished status that is not a success demands the corresponding run_* code.
Tool actionstarted together with succeeded do not carry errorCode; every other finished status requires its matching tool_* code.
Inbound messagesucceeded maps to completed; blocked maps to skipped; failed maps to failed combined with message_processing_failed. If reasonCode appears, it must fall within that terminal family.
Outbound messagesucceeded corresponds to sent; blocked corresponds to suppressed plus reasonCode; failed corresponds to failed together with errorCode and failureStage; unknown corresponds to unknown plus failureStage.

Every activity event carries a stable event id, a monotonic ledger sequence, a source event sequence, a timestamp, an actor, an action, a status, an integer schemaVersion: 1, and redaction: "metadata_only". Run and tool records require agent and run provenance and may optionally include session provenance. Message records may carry agent and run ids but deliberately never include sessionKey or sessionId; consequently the sessionKey query filter applies solely to run and tool rows. Tool events may optionally carry a tool call id and a tool name.

Message records employ message.inbound.processed or message.outbound.finished and include direction, channel, conversation kind, normalized outcome, and optional delivery kind, failure stage, duration, result count, reason code, and installation-local keyed account/conversation/message/target pseudonyms. These pseudonyms assist correlation but do not constitute anonymization: the state database retains their key, while RPC and CLI exports do not. The ledger does not hold prompts, message bodies, tool arguments, tool results, command output, or raw error text. Run/tool sessionKey values remain raw correlation metadata and may embed platform account or peer ids; message records omit session keys.

For inbound rows, durationMs measures core dispatch through its terminal and resultCount counts finalized queued tool, block, and reply payloads. For outbound rows, durationMs spans delivery ownership through acknowledgement, dead letter, or reconciliation (including queued wait time), and resultCount counts identified physical platform sends. deliveryKind, when present, describes the effective payload after hooks and rendering; suppressed or crash-ambiguous rows omit it.

Current message coverage includes accepted inbound messages that reach core dispatch, including core duplicate/terminal outcomes. Outbound coverage writes one terminal row per original logical reply payload that reaches shared durable delivery; chunking and adapter fan-out are aggregated in resultCount. Queued retryable or ambiguous sends are recorded only after acknowledgement, dead letter, or reconciliation. Plugin-local and direct-send paths that bypass those shared boundaries are not yet covered. The bounded worker queue is best-effort and may drop records on failure or saturation, so this surface is not a lossless compliance archive.

Recording is on by default and controlled by audit.enabled. Message recording is separately controlled by audit.messages and defaults to "off". When recording is disabled, audit.activity.list keeps serving records written earlier until they expire.

The shipped audit.list request, result, and AuditEvent schemas remain unchanged and return only agent-run and tool-action records. New operator clients should call audit.activity.list when the Gateway advertises it. Older Gateways may report either unknown method: audit.activity.list or, because authorization preceded method lookup in shipped versions, missing scope: operator.admin to a read-scoped request. Treat the latter as method absence only when the method was not advertised. A client may then retry audit.list only when its filters do not require message kind, direction, or channel support.

Use openclaw 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 depends on operator.read.
    • Its parameters consist of an optional status (which can be "queued", "running", "completed", "failed", "cancelled", or "timed_out", or an array of those statuses), an optional agentId, an optional sessionKey, an optional limit ranging from 1 to 500, and an optional string cursor.
    • The return value is { "tasks": TaskSummary[], "nextCursor"?: string }.
  • tasks.get needs operator.read.
    • Parameters: { "taskId": string }.
    • Returns { "task": TaskSummary }.
    • When task IDs are absent, the gateway responds with the not-found error shape.
  • tasks.cancel is built on operator.write.
    • Parameters: { "taskId": string, "reason"?: string }.
    • Returns { "found": boolean, "cancelled": boolean, "reason"?: string, "task"?: TaskSummary }.
    • found indicates whether the ledger contained a matching task. cancelled shows whether the runtime accepted or logged the cancellation.

TaskSummary is composed of id, status, and optional metadata fields: kind, runtime, title, agentId, sessionKey, childSessionKey, ownerKey, runId, taskId, flowId, parentTaskId, sourceId, timestamps, progress, a terminal summary, and sanitized error text. agentId specifies the agent running the task; sessionKey and ownerKey preserve the requester and control context.

Operator helper methods

  • commands.list (operator.read) retrieves the runtime command list for a given agent.
    • agentId is not required; leave it out to read the default agent workspace.
    • scope determines which surface the main name points at: text gives back the primary text command token without the leading /; native and the default both path return provider-aware native names when those are present.
    • textAliases holds exact slash aliases like /model and /m.
    • nativeName holds the provider-aware native command name if one is defined.
    • provider is not required and only influences native naming and whether native plugin commands are available.
    • includeArgs=false removes serialized argument metadata from the reply.
  • tools.catalog (operator.read) obtains the runtime tool catalog for an agent. The reply contains grouped tools and provenance information:
    • source: either core or plugin
    • pluginId: the plugin owner when source="plugin"
    • optional: whether a plugin tool is optional
  • tools.effective (operator.read) retrieves the runtime-effective tool inventory for a specific session.
    • sessionKey is mandatory.
    • The gateway derives trusted runtime context server-side from the session rather than accepting caller-provided auth or delivery context.
    • The response is a session-scoped server-derived view of the active inventory, covering core, plugin, channel, and previously 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 is found, the response may contain a notice like 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) calls one available tool through the same gateway policy path used by /tools/invoke.
    • name is required. args, sessionKey, agentId, confirm, and idempotencyKey are optional.
    • When both sessionKey and agentId are supplied, the resolved session agent must match agentId.
    • Owner-only core wrappers such as cron, gateway, and nodes require an owner or admin identity (operator.admin) even though tools.invoke itself is operator.write.
    • The response is an SDK-facing envelope containing ok, toolName, optional output, and typed error fields. Approval or policy denials return ok:false in the payload instead of bypassing the gateway tool policy pipeline.
  • skills.status (operator.read) retrieves the visible skill inventory for an agent.
  • agentId is not required; leaving it out reads from the default agent workspace.
  • The response provides eligibility details, missing requirements, configuration checks, and sanitized install options with raw secret values hidden.
  • skills.search and skills.detail (operator.read) retrieve ClawHub discovery metadata.
  • skills.upload.begin, skills.upload.chunk, and skills.upload.commit (operator.admin) stage a private skill archive before installation. This serves as a separate admin upload path for trusted clients, distinct from the normal ClawHub skill install process, and is turned off by default unless skills.install.allowUploadedArchives is active.
    • skills.upload.begin({ kind: "skill-archive", slug, sizeBytes, sha256?, force?, idempotencyKey? }) creates an upload linked to that slug and force value.
    • skills.upload.chunk({ uploadId, offset, dataBase64 }) adds bytes at the precise decoded offset.
    • skills.upload.commit({ uploadId, sha256? }) checks the final size and SHA-256. Commit only finishes the upload; it does not perform the skill installation.
    • Uploaded skill archives are zip files with a SKILL.md root. The archive's internal directory name never determines the install target.
  • skills.install (operator.admin) operates in three modes:
    • ClawHub mode: { source: "clawhub", slug, version?, force? } places 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; this setting does not impact ClawHub installs.
    • Gateway installer mode: { name, installId, timeoutMs? } executes 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 disregarded. 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 like enabled, apiKey, and env.

models.list views

models.list takes an optional view parameter (src/agents/model-catalog-visibility.ts):

  • Omitted or "default": when agents.defaults.modelPolicy.allow is set, the response is the allowed catalog, including dynamically discovered models for provider/* entries. Otherwise, the full gateway catalog is returned.
  • "configured": picker-sized behavior. If agents.defaults.modelPolicy.allow is configured, it still takes precedence, 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.
  • "provider-config": source-authored models.providers.*.models inventory, independent of picker allowlists. Rows include public model capabilities and route-aware availability, but exclude provider endpoints, auth material, and runtime request configuration.
  • "all": full gateway catalog, bypassing agents.defaults.modelPolicy.allow. Use for diagnostics or discovery UIs, not normal model pickers.

Exec approvals

When an exec request requires approval, the gateway transmits exec.approval.requested.

Operator clients resolve this by invoking exec.approval.resolve, which demands operator.approvals.

For host=node, the exec.approval.request payload must contain systemRunPlan (the canonical argv/cwd/rawCommand and session metadata). Any request lacking systemRunPlan is refused.

Once approved, forwarded node.invoke system.run calls reuse that same canonical systemRunPlan as the authoritative command, working directory, and session context.

If a caller modifies command, rawCommand, cwd, agentId, or sessionKey between the prepare step and the final approved system.run forward, the gateway rejects the run rather than trusting the altered payload.

Agent delivery fallback

agent requests may carry deliver=true to ask for outbound delivery.

bestEffortDeliver=false, which is the default, enforces strict behavior: unresolved or internal-only delivery targets produce INVALID_REQUEST.

bestEffortDeliver=true permits a fallback to session-only execution when no external deliverable route can be determined (for instance, internal or webchat sessions, or ambiguous multi-channel configurations).

Final agent results may include result.deliveryStatus when delivery was requested, employing the same sent, suppressed, partial_failed, and failed statuses documented for openclaw agent --json --deliver.

Versioning

PROTOCOL_VERSION, MIN_CLIENT_PROTOCOL_VERSION, MIN_NODE_PROTOCOL_VERSION, and MIN_PROBE_PROTOCOL_VERSION reside in packages/gateway-protocol/src/version.ts.

Clients transmit minProtocol plus maxProtocol. Operator and UI clients must include the current protocol within that range; current clients and servers operate protocol v4.

Authenticated clients possessing both role: "node" and client.mode: "node" can use the N-1 node protocol, currently v3. Lightweight restart probes also use the same N-1 window. Device authentication, pairing, scopes, command policy, and exec approvals are unaffected by this compatibility window. Plugin-owned node capabilities and commands are withheld until the node upgrades to the current protocol, since 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 is located in packages/gateway-client/src/ (OpenClaw accesses it through the thin src/gateway/client.ts facade). These defaults remain stable across protocol v4 and represent the expected baseline for third-party clients.

ConstantDefaultSource
PROTOCOL_VERSION4packages/gateway-protocol/src/version.ts
MIN_CLIENT_PROTOCOL_VERSION4packages/gateway-protocol/src/version.ts
MIN_NODE_PROTOCOL_VERSION3packages/gateway-protocol/src/version.ts
MIN_PROBE_PROTOCOL_VERSION3packages/gateway-protocol/src/version.ts
Request timeout (per RPC)30_000 mspackages/gateway-client/src/client.ts (requestTimeoutMs)
Preauth / connect-challenge timeout15_000 mspackages/gateway-client/src/timeouts.ts (OPENCLAW_HANDSHAKE_TIMEOUT_MS env can raise the paired server/client budget)
Initial reconnect backoff1_000 mspackages/gateway-client/src/client.ts (GATEWAY_RECONNECT_POLICY)
Max reconnect backoff30_000 mspackages/gateway-client/src/client.ts (GATEWAY_RECONNECT_POLICY)
Fast-retry clamp after device-token close250 mspackages/gateway-client/src/client.ts
Force-stop grace before terminate()250 msFORCE_STOP_TERMINATE_GRACE_MS
stopAndWait() default timeout1_000 msSTOP_AND_WAIT_TIMEOUT_MS
Default tick interval (pre hello-ok)30_000 mspackages/gateway-client/src/client.ts
Tick-timeout closecode 4000 when silence exceeds tickIntervalMs * 2packages/gateway-client/src/client.ts
MAX_PAYLOAD_BYTES25 * 1024 * 1024 (25 MB)src/gateway/server-constants.ts

The server announces the active policy.tickIntervalMs, policy.maxPayload, and policy.maxBufferedBytes inside hello-ok; clients should follow those values instead of the defaults used before the handshake.

In the reference client, when every pending request carries a finite deadline, those requests own their configured timeout. An expectFinal request that lacks a finite timeoutMs, any request using timeoutMs: null, or a combination of finite and unbounded requests leaves the tick watchdog running. If no inbound events or responses arrive before the tick-timeout limit expires, the client terminates the connection with code 4000, fails all outstanding requests, and reconnects. Failed requests are not retried after the new connection is established.

Auth

  • Gateway authentication using a shared secret relies on connect.params.auth.token or connect.params.auth.password, determined by the gateway.auth.mode setting ("none" | "token" | "password" | "trusted-proxy").
  • Identity-based modes like Tailscale Serve (gateway.auth.allowTailscale: true) or non-loopback gateway.auth.mode: "trusted-proxy" fulfill the connect auth requirement by reading request headers instead of using connect.params.auth.*.
  • Private ingress gateway.auth.mode: "none" bypasses shared-secret connect authentication entirely; do not expose this mode on public or untrusted ingress points.
  • After pairing completes, the gateway generates a device token tied to the connection role and scopes, delivered in hello-ok.auth.deviceToken. Clients should save this token after any successful connect.
  • When reconnecting with that saved device token, the previously approved scope set for that token should also be reused. This maintains read, probe, and status access already granted and prevents reconnects from silently falling back to a narrower implicit admin-only scope.
  • Client-side connect auth assembly (selectConnectAuth inside packages/gateway-client/src/client.ts):
    • auth.password is independent and always forwarded when present.
    • auth.token is filled in priority order: an explicit shared token first, then an explicit deviceToken, then a stored per-device token (keyed by deviceId and role).
    • auth.bootstrapToken is sent only when none of the previous options resolved to auth.token. A shared token or any resolved device token prevents it from being sent.
    • Auto-promotion of a stored device token during the one-shot AUTH_TOKEN_MISMATCH retry is limited to trusted endpoints: loopback, or wss:// with a pinned tlsFingerprint. Public wss:// without pinning does not qualify.
  • The built-in setup-code bootstrap process returns the primary node hello-ok.auth.deviceToken along with a restricted operator token in hello-ok.auth.deviceTokens for trusted mobile handoff. This 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, the 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.
  • Only persist hello-ok.auth.deviceTokens when the connect used bootstrap auth over a trusted transport like wss:// or loopback/local pairing.
  • If a client provides an explicit deviceToken or explicit scopes, that caller-requested scope set takes precedence; cached scopes are only used when the client reuses the stored per-device token.
  • Device tokens can be rotated or revoked through 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 save their replacement before reconnecting. Shared and admin rotations do not echo the bearer token.
  • Token issuance, rotation, and revocation remain limited to the approved role set recorded in that device's pairing entry; token mutations 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 only manage 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 verify the target operator token scope set against the caller's current session scopes. Non-admin callers cannot rotate or revoke an operator token broader than what they currently hold.
  • Auth failures include error.details.code along with 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 using a cached per-device token.
    • If that retry fails, stop automatic reconnect loops and display operator action guidance.
  • AUTH_SCOPE_MISMATCH means the device token was recognized but does not cover the requested role or scopes. Do not treat this as a bad token; instead prompt the operator to re-pair or approve the narrower or broader scope contract.

Device identity and pairing

  • A stable device identity (device.id) derived from a keypair fingerprint should be included by nodes.
  • Tokens are issued per device and role by gateways.
  • Pairing approvals are mandatory for new device IDs unless local auto-approval is enabled.
  • Local direct loopback connections are the focus of pairing auto-approval.
  • OpenClaw provides an additional, restricted backend/container-local self-connect path for trusted shared-secret helper flows.
  • Connections from the same-host tailnet or LAN are still considered remote for pairing and require approval.
  • During connect (operator plus node), WS clients typically include device identity. The only device-less operator exceptions are explicit trust paths:
    • successful gateway.auth.mode: "trusted-proxy" operator Control UI authentication.
    • 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 permitted through an explicit trust path, OpenClaw still resets self-declared scopes to an empty set unless that path has a named scope-preservation exception. Scope-gated methods then fail with missing scope.
  • 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 still using pre-challenge signing behavior, connect returns DEVICE_AUTH_* detail codes under error.details.code with a stable error.details.reason.

Common migration failures:

Messagedetails.codedetails.reasonMeaning
device nonce requiredDEVICE_AUTH_NONCE_REQUIREDdevice-nonce-missingClient omitted device.nonce (or sent blank).
device nonce mismatchDEVICE_AUTH_NONCE_MISMATCHdevice-nonce-mismatchClient signed with a stale/wrong nonce.
device signature invalidDEVICE_AUTH_SIGNATURE_INVALIDdevice-signatureSignature payload does not match v2 payload.
device signature expiredDEVICE_AUTH_SIGNATURE_EXPIREDdevice-signature-staleSigned timestamp is outside allowed skew.
device identity mismatchDEVICE_AUTH_DEVICE_ID_MISMATCHdevice-id-mismatchdevice.id does not match public key fingerprint.
device public key invalidDEVICE_AUTH_PUBLIC_KEY_INVALIDdevice-public-keyPublic 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.
  • The 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.