Gateway
Gateway protocol
The Gateway WS protocol is the single control plane and node transport for OpenClaw. Operator and node clients (CLI, web UI, macOS app, iOS/Android nodes, headless nodes) connect over WebSocket and declare a role and scope at handshake time.
Transport and framing
- WebSocket, text frames, JSON payloads.
- First frame must be a
connectrequest. - Pre-connect frames are capped at 64 KiB (
MAX_PREAUTH_PAYLOAD_BYTES). After handshake, followhello-ok.policy.maxPayloadandhello-ok.policy.maxBufferedBytes. With diagnostics enabled, oversized inbound frames and slow outbound buffers emitpayload.largeevents before the gateway closes or drops the frame. These events carrysurface, 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:
{ "type": "event", "event": "connect.challenge", "payload": { "nonce": "…", "ts": 1737264000000 }}Client replies 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": "…" } }}Gateway responds 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 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:
{ "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:
{ "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.
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 auth, node events, operator RPCs, or plugin methods. A strict connect
verifies a hash-at-rest, short-lived credential bound to the environment, bundle
hash, owner epoch, RPC-set version, expiry, and one nullable session; it
separately checks the current version and feature set. Success returns minimal
worker-hello-ok; feature negotiation is independent of the general protocol
version. Frames stay under 64 KiB. The closed allowlist contains
worker.heartbeat and worker.transcript.commit for ordered semantic message batches. 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 may advertise optional capabilities in 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 ascamera,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.
Roles:
operator: control-plane client (CLI/UI/automation).node: capability host (camera/screen/canvas/system.run).worker: cloud execution host on the dedicated, closed worker protocol.
Operator scopes (src/gateway/operator-scopes.ts), the full closed set:
operator.readoperator.writeoperator.adminoperator.approvalsoperator.pairingoperator.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 |
Caps/commands/permissions (node)
Nodes declare capability claims at connect time:
caps: high-level capability categories such ascamera,canvas,screen,location,voice, andtalk.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.
Connected nodes can publish optional agent-visible plugin or MCP tool
descriptors with node.pluginTools.update after a successful connect or
reconnect. Headless node hosts restart to apply declarative MCP inventory
changes. This update method is the only publication path; plugin tool descriptors are not accepted in
connect params. Each descriptor must use a provider-safe tool name and name
a command 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 node-published descriptors.
Connected node hosts publish their complete skill replacement catalog with
node.skills.update. This node-role method is the only node skill publication
path; skills are not accepted in connect params. Each descriptor contains a
safe name, description, and bounded SKILL.md content. The Gateway parses that
content with the normal skills loader, includes it in agent skill snapshots
while the node is connected, and removes it on disconnect. Set
gateway.nodes.skills.enabled: false to ignore node-published skills.
Presence
system-presencereturns entries keyed by device identity, includingdeviceId,roles, andscopes, so UIs can show one row per device even when it connects as both operator and node.node.listincludes optionallastSeenAtMsandlastSeenReason. Connected nodes report current connection time with reasonconnect; 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:
{ "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:
{ "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
agentevents, tool-result events) require at leastoperator.read. Sessions without it skip these frames entirely. - Plugin-defined
plugin.*broadcasts are gated tooperator.writeoroperator.adminby default; explicit entries such asplugin.approval.requested/plugin.approval.resolveduseoperator.approvalsinstead. - 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.
System and identity
healthreturns the cached or freshly probed gateway health snapshot.diagnostics.stabilityreturns 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. Requiresoperator.read.statusreturns the/status-style gateway summary; sensitive fields only for admin-scoped operator clients.gateway.identity.getreturns the gateway device identity used by relay and pairing flows.system-presencereturns the current presence snapshot for connected operator/node devices.system-eventappends a system event and can update/broadcast presence context.last-heartbeatreturns the latest persisted heartbeat event.set-heartbeatstoggles heartbeat processing on the gateway.gateway.suspend.preparecreates a short cooperative-suspension lease only when tracked Gateway work is idle.gateway.suspend.statuschecks that lease, andgateway.suspend.resumereleases it after thaw or an aborted host operation.
Models and usage
models.listreturns the runtime-allowed model catalog. See "models.listviews" below.usage.statusreturns provider usage windows/remaining quota summaries.usage.costreturns aggregated cost usage summaries for a date range. PassagentIdfor one agent, oragentScope: "all"to aggregate configured agents.doctor.memory.statusreturns 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, anddoctor.memory.dedupeDreamDiaryaccept optional{ "agentId": "agent-id" }; omitted, they operate on the configured default agent workspace.doctor.memory.remHarnessreturns a bounded, read-only REM harness preview for remote control-plane clients, including workspace paths, memory snippets, rendered grounded markdown, and deep promotion candidates. Requiresoperator.read.sessions.usagereturns per-session usage summaries. PassagentIdfor one agent, oragentScope: "all"to list configured agents together. Both usage methods acceptmode: "specific"with an IANAtimeZonefor DST-aware calendar-day boundaries and buckets.utcOffsetremains supported for older clients and as a fallback when the Gateway runtime does not recognize the requested zone.sessions.usage.timeseriesreturns timeseries usage for one session.sessions.usage.logsreturns usage log entries for one session.
Channels and login helpers
channels.statusreturns built-in + bundled channel/plugin status summaries.channels.logoutlogs out a specific channel/account where the channel supports it.web.login.startstarts a QR/web login flow for the current QR-capable web channel provider.web.login.waitwaits for that flow to complete and starts the channel on success.push.testsends a test APNs push to a registered iOS node.voicewake.getreturns the stored wake-word triggers.voicewake.setupdates wake-word triggers and broadcasts the change.
Plugin management
plugins.list(operator.read) returns the installed plugin inventory plus locally curated official picks, diagnostics, and whether the current install mode allows mutations.plugins.search(operator.read) searches installable ClawHub code-plugin and bundle-plugin families. Pass non-emptyqueryand optionallimitfrom 1 to 100.plugins.install(operator.admin) installs either an official catalog entry with{ source: "official", pluginId }or a ClawHub package with{ source: "clawhub", packageName, version?, acknowledgeClawHubRisk? }. ClawHub installs preserve Gateway trust, integrity, and install-policy checks. Successful installs require a Gateway restart.plugins.setEnabled(operator.admin) changes one installed plugin's enabled policy with{ pluginId, enabled }. The response includes the updated catalog entry, restart metadata, and any slot-selection warnings.plugins.uninstall(operator.admin) removes one externally installed plugin with{ pluginId }: config references, the install record, and managed files. Bundled plugins cannot be uninstalled, only disabled. The response lists the removal actions and always requires a Gateway restart.
Messaging and logs
sendis the direct outbound-delivery RPC for channel/account/thread-targeted sends outside the chat runner.logs.tailreturns the configured gateway file-log tail with cursor/limit and max-byte controls.
Operator terminal
terminal.openstarts a host PTY for an explicitagentIdor the default agent and returns the resolved agent, working directory, shell, and confinement state.terminal.input,terminal.resize, andterminal.closeoperate only on sessions owned by the calling connection.terminal.dataandterminal.exitevents 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;0restores kill-on-disconnect) while recent output accumulates in a bounded server-side buffer. terminal.listreturns attachable sessions;terminal.attachrebinds a live-or-detached session to the calling connection and returns the replay buffer (tmux-style take-over — a previous live owner receivesterminal.exitwith reasondetached);terminal.textreads the buffer as plain text without attaching.- Every terminal method requires
operator.admin;gateway.terminal.enabledmust be explicitly true. Fully sandboxed agents are refused, and an agent policy change closes existing and in-flight PTYs, detached ones included.
Talk and TTS
talk.catalogreturns the read-only Talk provider catalog for speech, streaming transcription, and realtime voice: canonical provider ids, registry aliases, labels, configured state, an optional group-levelreadyresult, exposed model/voice ids, canonical modes, transports, brain strategies, and realtime audio/capability flags, without returning provider secrets or mutating global config. Current gateways setreadyafter applying runtime provider selection; treat its absence as unverified on older gateways.talk.configreturns the effective Talk config payload;includeSecretsrequiresoperator.talk.secrets(oroperator.admin).talk.session.createcreates a gateway-owned Talk session forrealtime/gateway-relay,transcription/gateway-relay, orstt-tts/managed-room. Forstt-tts/managed-room,operator.writecallers that passsessionKeymust also passspawnedByfor scoped session-key visibility; unscopedsessionKeycreation andbrain: "direct-tools"requireoperator.admin.talk.session.joinvalidates a managed-room session token, emitssession.readyorsession.replacedas needed, and returns room/session metadata plus recent Talk events, never the plaintext token or its hash.talk.session.appendAudioappends base64 PCM input audio to gateway-owned realtime relay and transcription sessions.talk.session.startTurn,talk.session.endTurn, andtalk.session.cancelTurndrive managed-room turn lifecycle with stale-turn rejection before state clears.talk.session.cancelOutputstops assistant audio output, primarily for VAD-gated barge-in in gateway relay sessions.talk.session.submitToolResultcompletes a provider tool call emitted by a gateway-owned realtime relay session. The request waits for any asynchronous completion signal exposed by the provider bridge; failed submissions keep the linked run active and do not emit a successful tool-result event. Passoptions: { willContinue: true }for interim tool output oroptions: { suppressResponse: true }when the provider bridge advertises suppression support and the result should not start another response.talk.session.steersends active-run voice control into a gateway-owned agent-backed Talk session:{ sessionId, text, mode? }, wheremodeisstatus,steer,cancel, orfollowup; omitted mode is classified from the spoken text.talk.session.closecloses a gateway-owned relay, transcription, or managed-room session and emits terminal Talk events.talk.modesets/broadcasts the current Talk mode state for WebChat/Control UI clients.talk.client.createcreates a client-owned realtime provider session usingwebrtcorprovider-websocketwhile the gateway owns config, credentials, instructions, and tool policy.talk.client.toolCalllets client-owned realtime transports forward provider tool calls to gateway policy. The first supported tool isopenclaw_agent_consult; clients get a run id and wait for normal chat lifecycle events before submitting the provider-specific tool result.talk.client.steersends active-run voice control for client-owned realtime transports. The gateway resolves the active embedded run fromsessionKeyand returns a structured accepted/rejected result instead of silently dropping steering.talk.eventis the single Talk event channel for realtime, transcription, STT/TTS, managed-room, telephony, and meeting adapters.talk.speaksynthesizes speech through the active Talk speech provider.tts.statusreturns TTS enabled state, active provider, fallback providers, and provider config state.tts.providersreturns the visible TTS provider inventory.tts.enableandtts.disabletoggle TTS prefs state.tts.setProviderupdates the preferred TTS provider.tts.convertruns one-shot text-to-speech conversion.tts.speak(operator.write) renders non-emptytextwith the configured general TTS provider chain and returns one whole clip inline asaudioBase64, plusproviderand optionaloutputFormat,mimeType, andfileExtensionmetadata. Unliketts.convert, it does not return a Gateway-local path; unliketalk.speak, it does not require a Talk provider. Text abovemessages.tts.maxTextLengthreturnsINVALID_REQUEST; synthesis failures returnUNAVAILABLE.
Secrets, config, update, and wizard
secrets.reloadre-resolves active SecretRefs and swaps runtime secret state only on full success.secrets.resolveresolves command-target secret assignments for a specific command/target set.config.getreturns the current config snapshot and hash.config.setwrites a validated config payload.config.patchmerges a partial config update. Destructive array replacement requires the affected path inreplacePaths; nested arrays under array entries use[]paths such asagents.list[].skills.config.applyvalidates + replaces the full config payload.config.schemareturns the live config schema payload used by Control UI and CLI tooling: schema,uiHints, version, generation metadata, plugin + channel schema metadata when loadable. It includestitle/descriptionmetadata from the same labels/help text as the UI, including nested object, wildcard, array-item, andanyOf/oneOf/allOfcomposition branches when matching field documentation exists.config.schema.lookupreturns a path-scoped lookup payload for one config path: normalized path, a shallow schema node, matched hint +hintPath, optionalreloadKind, and immediate child summaries for UI/CLI drill-down.reloadKindis one ofrestart,hot, ornone(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 exposekey, normalizedpath,type,required,hasChildren, optionalreloadKind, plus the matchedhint/hintPath.update.runruns the gateway update flow and schedules a restart only if the update succeeded; callers with a session can includecontinuationMessageso 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 returnsok: truewithresult.reason: "managed-service-handoff-started"andhandoff.status: "started"; unavailable or failed handoffs returnok: falsewithmanaged-service-handoff-unavailableormanaged-service-handoff-failed, plushandoff.commandwhen a manual shell update is required. Unavailable means OpenClaw lacks a safe supervisor boundary or durable service identity, such asOPENCLAW_SYSTEMD_UNITfor systemd. During a started handoff, the restart sentinel may briefly reportstats.reason: "restart-health-pending"; the continuation is delayed until the CLI verifies the restarted gateway and writes the finaloksentinel.update.statusrefreshes and returns the latest update restart sentinel, including the post-restart running version when available.wizard.start,wizard.next,wizard.status, andwizard.cancelexpose the onboarding wizard over WS RPC.
Agent and workspace helpers
agents.listreturns configured agent entries, including effective model and runtime metadata.agents.create,agents.update, andagents.deletemanage agent records and workspace wiring.agents.files.list,agents.files.get, andagents.files.setmanage the bootstrap workspace files exposed for an agent.audit.activity.listreturns the versioned metadata-only activity ledger;audit.listremains the compatibility-safe run/tool RPC.agents.workspace.listandagents.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. 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, andtasks.cancelexpose the gateway task ledger to SDK and operator clients. See Task ledger RPCs below.artifacts.list,artifacts.get, andartifacts.downloadexpose transcript-derived artifact summaries and downloads for an explicitsessionKey,runId, ortaskIdscope. 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.listandenvironments.statuspreserve gateway-local and node environment discovery. Configured cloud workers and durable records left by earlier profiles addworkermetadata withproviderId, optionalleaseId,state,ageMs, optionalidleMs, andattachedSessionIds. Worker lifecycle states arerequested,provisioning,bootstrapping,ready,attached,idle,draining,destroying,destroyed,failed, andorphaned.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 requireoperator.admin, are control-plane writes, and return the same environment summary shape used by status responses.agent.identity.getreturns the effective assistant identity for an agent or session.agent.waitwaits for a run to finish and returns the terminal snapshot when available.
Session control
sessions.listreturns the current session index, including per-rowagentRuntimemetadata when an agent runtime backend is configured.sessions.subscribeandsessions.unsubscribetoggle session change event subscriptions for the current WS client.sessions.messages.subscribeandsessions.messages.unsubscribetoggle transcript/message event subscriptions for one session.sessions.previewreturns bounded transcript previews for specific session keys.sessions.describereturns one gateway session row for an exact session key.sessions.resolveresolves or canonicalizes a session target.sessions.createcreates a new session entry.worktree: trueprovisions a managed worktree; optionalworktreeBaseRef/worktreeNameselect the base ref and branch name, andexecNode(operator.admin) binds session exec to a node host. The created worktree is echoed in the result and persisted on the session row (worktree: { id, branch, repoRoot }).sessions.groups.list,sessions.groups.put,sessions.groups.rename, andsessions.groups.deletemanage the gateway-owned custom session group catalog (names + display order). Membership stays on each session'scategoryfield; rename and delete update member sessions server-side.sessions.sendsends a message into an existing session.sessions.steeris the interrupt-and-steer variant for an active session.sessions.abortaborts active work for a session. Passkeyplus optionalrunId, orrunIdalone for active runs the gateway can resolve to a session.sessions.patchupdates session metadata/overrides and reports the resolved canonical model plus effectiveagentRuntime.sessions.reset,sessions.delete, andsessions.compactperform session maintenance.sessions.getreturns the full stored session row.- Chat execution still uses
chat.history,chat.send,chat.abort, andchat.inject.chat.historyis 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 (exactNO_REPLY/no_reply) are omitted, and oversized rows can be replaced with placeholders. chat.message.getis the additive bounded full-message reader for a single visible transcript entry. PasssessionKey, optionalagentIdwhen session selection is agent-scoped, and a transcriptmessageIdpreviously surfaced throughchat.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.toolTitlesreturns short purpose titles for tool calls rendered in the Control UI (batched, max 24 items with bounded inputs). The feature is opt-in viagateway.controlUi.toolTitles(default off); disabled gateways answer{ titles: {}, disabled: true }with no model call so clients stop asking. When enabled, titles use standard utility-model routing: an explicitly configuredutilityModel(an operator decision that, like all utility tasks, may send bounded task content to the chosen provider), else the session provider's declared small-model default so no new egress destination appears implicitly; an emptyutilityModeldisables them entirely. Titles never fall back to the primary model. Results cache in the per-agent state database keyed by tool name + input, so repeated views never re-bill the same calls.chat.sendaccepts one-turnfastMode: "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 withagents.defaults.models["<provider>/<model>"].params.fastAutoOnSeconds. Achat.sendcaller can pass one-turnfastAutoOnSecondsto override the cutoff for that request.
Device pairing and device tokens
device.pair.listreturns pending and approved paired devices.device.pair.setupCodecreates a mobile setup code and, by default, a PNG QR data URL. It requiresoperator.adminand is intentionally omitted from advertised discovery. The result includessetupCode, optionalqrDataUrl,gatewayUrl, the non-secretauthlabel, andurlSource.device.pair.approve,device.pair.reject, anddevice.pair.removemanage device-pairing records.device.pair.renameassigns an operator label ({ deviceId, label }) that is preferred over the client-reported display name and survives device repair or re-approval.device.token.rotaterotates a paired device token within its approved role and caller scope bounds.device.token.revokerevokes 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.
Node pairing, invoke, and pending work
node.pair.list,node.pair.approve,node.pair.reject, andnode.pair.removecover node capability approvals.node.pair.requestandnode.pair.verifywere removed in 2026.7 together with the standalone node pairing store; pending requests are created by the Gateway during node connects.node.listandnode.describereturn known/connected node state.node.renameupdates a paired node label.node.invokeforwards a command to a connected node.node.invoke.resultreturns the result for an invoke request.mcp.tools.call.v1is the headless node-host command for calling a configured node-local MCP tool. It is carried throughnode.invoke, requires the node to declare the command, and remains subject to pairing approval andgateway.nodes.denyCommands.node.eventcarries node-originated events back into the gateway.node.pluginTools.updateis the only publication path for replacing the connected node's agent-visible plugin/MCP tool descriptors;connectparams do not carry them.node.pending.pullandnode.pending.ackare the connected-node queue APIs.node.pending.enqueueandnode.pending.drainmanage durable pending work for offline/disconnected nodes.
Approval families
approval.getandapproval.resolveare the kind-agnostic durable approval methods (scopeoperator.approvals).approval.getreturns a sanitized pending or retained terminal projection with a stableurlPath;approval.resolveaccepts the canonical approval id, an explicitkind, and a decision, applies first-answer-wins resolution, and always returns the recorded canonical result.exec.approval.request,exec.approval.get,exec.approval.list, andexec.approval.resolvecover one-shot exec approval requests plus pending approval lookup/replay. They are protocol-boundary adapters over the same durable approval registry.exec.approval.waitDecisionwaits on one pending exec approval and returns the final decision (ornullon timeout).exec.approvals.getandexec.approvals.setmanage gateway exec approval policy snapshots.exec.approvals.node.getandexec.approvals.node.setmanage node-local exec approval policy via node relay commands.plugin.approval.request,plugin.approval.list,plugin.approval.waitDecision, andplugin.approval.resolvecover plugin-defined approval flows.
Automation, skills, and tools
- Automation:
wakeschedules an immediate or next-heartbeat wake text injection;cron.get,cron.list,cron.status,cron.add,cron.update,cron.remove,cron.run,cron.runsmanage scheduled work. cron.runremains an enqueue-style RPC for manual runs. Clients that need completion semantics should read the returnedrunIdand pollcron.runs.cron.runsaccepts an optional non-emptyrunIdfilter 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: UI chat updates such aschat.injectand other transcript-only chat events. In protocol v4, delta payloads carrydeltaText;messageremains the cumulative assistant snapshot. Non-prefix replacements setreplace=trueand usedeltaTextas 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.activity.list gives operator clients a stable newest-first view of agent
run, tool action, and opt-in message lifecycle 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. See
Audit history for the data model and privacy semantics.
- Params: optional exact
agentId,sessionKey, orrunId; optionalkind("agent_run","tool_action", or"message"); optionalstatus("started","succeeded","failed","cancelled","timed_out","blocked", or"unknown"); optional messagedirection("inbound"or"outbound") and exactchannel; optional inclusiveafter/beforeUnix-millisecond bounds; optionallimitfrom1to500; and optional stringcursorfrom the preceding page. - Result:
{ "events": AuditActivityEventV1[], "nextCursor"?: string }.
The named V1 result union has separate agent-run, tool-action, inbound-message,
and outbound-message schemas. The eventType discriminator is respectively
agent_run, tool_action, inbound_message, or outbound_message; kind and
message direction remain available for filtering and display. Every event has
integer schemaVersion: 1. Message identity references use the exact
hmac-sha256:v1:<32 hex key id>:<64 hex digest> format; a channel-sender actor
id uses the same format.
All variants require eventType, schemaVersion, eventId, sequence,
sourceSequence, occurredAt, kind, action, status, actor, and
redaction. Variant fields are:
eventType |
Required fields | Optional fields |
|---|---|---|
agent_run |
agentId, runId; kind: "agent_run" |
sessionKey, sessionId, errorCode |
tool_action |
agentId, runId; kind: "tool_action" |
sessionKey, sessionId, toolCallId, toolName, errorCode |
inbound_message |
direction: "inbound", channel, conversationKind, outcome |
agentId, runId, durationMs, resultCount, identity references, reasonCode, errorCode |
outbound_message |
direction: "outbound", channel, conversationKind, outcome |
agentId, runId, durationMs, resultCount, identity references, reasonCode, deliveryKind, failureStage, errorCode |
The closed message enums are:
conversationKind:direct,group,channel, orunknown.- Inbound
outcome:completed,skipped, orfailed; optionalreasonCode: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, oracp_dispatch_aborted. - Outbound
outcome:sent,suppressed,failed, orunknown; optionalreasonCode:cancelled_by_message_sending_hook,cancelled_by_reply_payload_sending_hook,empty_after_message_sending_hook,empty_after_reply_payload_sending_hook, orno_visible_payload. An adapter that returns no platform identity isunknown, because the external side effect cannot be disproved. deliveryKind:text,media, orother;failureStage:platform_send,queue, orunknown.
Terminal fields are correlated, not independently optional:
| Variant | Terminal mapping |
|---|---|
| Agent run | started has no errorCode; each non-success finished status requires its matching run_* code. |
| Tool action | started and succeeded have no errorCode; each other finished status requires its matching tool_* code. |
| Inbound message | succeeded = completed; blocked = skipped; failed = failed plus message_processing_failed. reasonCode, when present, must belong to that terminal family. |
| Outbound message | succeeded = sent; blocked = suppressed plus reasonCode; failed = failed plus errorCode and failureStage; unknown = unknown plus failureStage. |
Each activity event includes a stable event id, monotonic ledger sequence,
source event sequence, timestamp, actor, action, status, integer
schemaVersion: 1, and redaction: "metadata_only". Run and tool records
require agent and run provenance and may include session provenance. Message
records may include agent and run ids, but intentionally never include
sessionKey or sessionId; the sessionKey query filter therefore applies to
run and tool rows only. Tool events may include tool call id and tool name.
Message records use message.inbound.processed or
message.outbound.finished and add 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 aid
correlation but are not anonymization: the state database contains their key,
while RPC and CLI exports do not. The ledger does not store prompts, message
bodies, tool arguments, tool results, command output, or raw error text.
Run/tool sessionKey values remain raw correlation metadata and can 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.listrequiresoperator.read.- Params: optional
status("queued","running","completed","failed","cancelled", or"timed_out") or an array of those statuses, optionalagentId, optionalsessionKey, optionallimitfrom1to500, and optional stringcursor. - Result:
{ "tasks": TaskSummary[], "nextCursor"?: string }.
- Params: optional
tasks.getrequiresoperator.read.- Params:
{ "taskId": string }. - Result:
{ "task": TaskSummary }. - Missing task ids return the gateway not-found error shape.
- Params:
tasks.cancelrequiresoperator.write.- Params:
{ "taskId": string, "reason"?: string }. - Result:
{ "found": boolean, "cancelled": boolean, "reason"?: string, "task"?: TaskSummary }. foundreports whether the ledger had a matching task.cancelledreports whether the runtime accepted or recorded cancellation.
- Params:
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.agentIdis optional; omit it to read the default agent workspace.scopecontrols which surface the primarynametargets:textreturns the primary text command token without the leading/;nativeand the defaultbothpath return provider-aware native names when available.textAliasescarries exact slash aliases such as/modeland/m.nativeNamecarries the provider-aware native command name when one exists.provideris optional and only affects native naming plus native plugin command availability.includeArgs=falseomits 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:coreorpluginpluginId: plugin owner whensource="plugin"optional: whether a plugin tool is optional
tools.effective(operator.read) fetches the runtime-effective tool inventory for a session.sessionKeyis 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.effectiveis 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 issuetools/list. If no matching warm catalog exists, the response may include a notice such asmcp-not-yet-connected,mcp-not-yet-listed, ormcp-stale-catalog.- Effective tool entries use
source="core",source="plugin",source="channel", orsource="mcp".
tools.invoke(operator.write) invokes one available tool through the same gateway policy path as/tools/invoke.nameis required.args,sessionKey,agentId,confirm, andidempotencyKeyare optional.- If both
sessionKeyandagentIdare present, the resolved session agent must matchagentId. - Owner-only core wrappers such as
cron,gateway, andnodesrequire owner/admin identity (operator.admin) even thoughtools.invokeitself isoperator.write. - The response is an SDK-facing envelope with
ok,toolName, optionaloutput, and typederrorfields. Approval or policy refusals returnok:falsein the payload rather than bypassing the gateway tool policy pipeline.
skills.status(operator.read) fetches the visible skill inventory for an agent.agentIdis 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.searchandskills.detail(operator.read) return ClawHub discovery metadata.skills.upload.begin,skills.upload.chunk, andskills.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 unlessskills.install.allowUploadedArchivesis 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.mdroot. 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 workspaceskills/directory. - Upload mode:
{ source: "upload", uploadId, slug, force?, sha256?, timeoutMs? }installs a committed upload into the default agent workspaceskills/<slug>directory. The slug and force value must match the originalskills.upload.beginrequest. Rejected unlessskills.install.allowUploadedArchivesis enabled; the setting does not affect ClawHub installs. - Gateway installer mode:
{ name, installId, timeoutMs? }runs a declaredmetadata.openclaw.installaction on the gateway host. Older clients may still senddangerouslyForceUnsafeInstall; this field is deprecated, accepted only for protocol compatibility, and ignored. Usesecurity.installPolicyfor operator-owned install decisions.
- ClawHub mode:
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 asenabled,apiKey, andenv.
models.list views
models.list accepts an optional view parameter
(src/agents/model-catalog-visibility.ts):
- Omitted or
"default": ifagents.defaults.modelsis configured, the response is the allowed catalog, including dynamically discovered models forprovider/*entries. Otherwise the response is the full gateway catalog. "configured": picker-sized behavior. Ifagents.defaults.modelsis configured, it still wins, including provider-scoped discovery forprovider/*entries. Without an allowlist, the response uses explicitmodels.providers.<provider>.modelsentries, falling back to the full catalog only when no configured model rows exist."provider-config": source-authoredmodels.providers.*.modelsinventory, independent of picker allowlists. Rows include public model capabilities and route-aware availability, but omit provider endpoints, auth material, and runtime request configuration."all": full gateway catalog, bypassingagents.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(requiresoperator.approvals). - For
host=node,exec.approval.requestmust includesystemRunPlan(canonicalargv/cwd/rawCommand/session metadata). Requests missingsystemRunPlanare rejected. - After approval, forwarded
node.invoke system.runcalls reuse that canonicalsystemRunPlanas the authoritative command/cwd/session context. - If a caller mutates
command,rawCommand,cwd,agentId, orsessionKeybetween prepare and the final approvedsystem.runforward, the gateway rejects the run instead of trusting the mutated payload.
Agent delivery fallback
agentrequests can includedeliver=trueto request outbound delivery.bestEffortDeliver=false(the default) keeps strict behavior: unresolved or internal-only delivery targets returnINVALID_REQUEST.bestEffortDeliver=trueallows fallback to session-only execution when no external deliverable route can be resolved (for example internal/webchat sessions or ambiguous multi-channel configs).- Final
agentresults may includeresult.deliveryStatuswhen delivery was requested, using the samesent,suppressed,partial_failed, andfailedstatuses documented foropenclaw agent --json --deliver.
Versioning
PROTOCOL_VERSION,MIN_CLIENT_PROTOCOL_VERSION,MIN_NODE_PROTOCOL_VERSION, andMIN_PROBE_PROTOCOL_VERSIONlive inpackages/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"andclient.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:genpnpm protocol:gen:swiftpnpm 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.
The reference client lets finite requests own their configured deadline when
every pending request has one. An expectFinal request without a finite
timeoutMs, any request with timeoutMs: null, or a mix of finite and
unbounded requests keeps the tick watchdog active. If inbound events and
responses remain silent past the tick-timeout threshold, the client closes the
socket with code 4000, rejects every pending request, and reconnects. It does
not replay rejected requests after reconnecting.
Auth
- Shared-secret gateway auth uses
connect.params.auth.tokenorconnect.params.auth.password, depending on the configuredgateway.auth.mode("none" | "token" | "password" | "trusted-proxy"). - Identity-bearing modes such as Tailscale Serve (
gateway.auth.allowTailscale: true) or non-loopbackgateway.auth.mode: "trusted-proxy"satisfy the connect auth check from request headers instead ofconnect.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 (
selectConnectAuthinpackages/gateway-client/src/client.ts):auth.passwordis orthogonal and always forwarded when set.auth.tokenis populated in priority order: explicit shared token first, then an explicitdeviceToken, then a stored per-device token (keyed bydeviceId+role).auth.bootstrapTokenis sent only when none of the above resolvedauth.token. A shared token or any resolved device token suppresses it.- Auto-promotion of a stored device token on the one-shot
AUTH_TOKEN_MISMATCHretry is gated to trusted endpoints only: loopback, orwss://with a pinnedtlsFingerprint. Publicwss://without pinning does not qualify.
- Built-in setup-code bootstrap returns the primary node
hello-ok.auth.deviceTokenplus a bounded operator token inhello-ok.auth.deviceTokensfor trusted mobile handoff. The operator token includesoperator.talk.secretsfor native Talk configuration reads, but excludes pairing-mutation scopes andoperator.admin. - While a non-baseline setup-code bootstrap waits for approval,
PAIRING_REQUIREDdetails includerecommendedNextStep: "wait_then_retry",retryable: true, andpauseReconnect: false. Keep reconnecting with the same bootstrap token until the request is approved or the token becomes invalid. - Persist
hello-ok.auth.deviceTokensonly when the connect used bootstrap auth on a trusted transport such aswss://or loopback/local pairing. - If a client supplies an explicit
deviceTokenor explicitscopes, 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.rotateanddevice.token.revoke(requiresoperator.pairing). Rotating or revoking a node or other non-operator role also requiresoperator.admin. device.token.rotatereturns 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.rotateanddevice.token.revokealso 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.codeplus recovery hints:error.details.canRetryWithDeviceToken(boolean)error.details.recommendedNextStep: one ofretry_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_MISMATCHmeans 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
deviceidentity duringconnect(operator + node). The only device-less operator exceptions are explicit trust paths:gateway.controlUi.allowInsecureAuth=truefor 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-clientbackend 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=trueis 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-clientbackend 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.challengenonce.
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(buildDeviceAuthPayloadV3inpackages/gateway-client/src/device-auth.ts), which bindsplatformanddeviceFamilyin addition to device/client/role/scopes/token/nonce fields. - Legacy
v2signatures 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.tlsconfig). - Clients may optionally pin the gateway cert fingerprint via
gateway.remote.tlsFingerprintor 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.