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.
@openclaw/gateway-protocolprovides schemas, validators, TypeScript types, lightweight frame and error helpers, and version constants. Its tarball contains the generatedprotocol.schema.jsonmachine-readable contract.@openclaw/gateway-clientoffers the reference Node client and a browser-safe entry at@openclaw/gateway-client/browser.
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
connectrequest. - Pre-connect frames are limited to 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?}
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 }.requiredScopesis the complete known scope set for the requested operation. The legacymissing 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 ascamera,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.readoperator.writeoperator.adminoperator.approvalsoperator.pairingoperator.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 commands | Required scopes |
|---|---|
| none | operator.pairing |
| ordinary commands | operator.pairing + operator.write |
includes system.run, system.run.prepare, system.which, browser.proxy, fs.listDir, or system.execApprovals.get/set | operator.pairing + operator.admin |
Caps/commands/permissions (node)
At connection time, nodes declare their capability claims:
caps: high-level capability categories such ascamera,canvas,screen,location,voice, andtalk.commands: command allowlist for invoke.permissions: granular toggles (for examplescreen.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-presencereturns entries keyed by device identity, containingdeviceId,roles, andscopes, so UIs can display one row per device even when that device connects as both an operator and a node.node.listincludes optionallastSeenAtMsandlastSeenReason. Connected nodes report the current connection time with reasonconnect; 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
agentevents, tool-result events) require at leastoperator.read. Sessions lacking 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 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
healthprovides the gateway health status, either from a cache or by performing a fresh probe.diagnostics.stabilitygives 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. Theoperator.readsetting is required.statusdelivers a gateway summary in the/statusstyle, with sensitive fields only accessible to operator clients that have admin scope.gateway.identity.getreturns the gateway device identity utilized in relay and pairing flows.system-presenceprovides the current presence snapshot for connected operator and node devices.system-eventadds a system event and can update or broadcast presence context.last-heartbeatretrieves the most recent heartbeat event that has been persisted.set-heartbeatstoggles the processing of heartbeats on the gateway.gateway.suspend.preparecreates a short cooperative suspension lease only when tracked Gateway work is idle. The lease is checked bygateway.suspend.statusand released bygateway.suspend.resumeafter a thaw or an aborted host operation.
Models and usage
models.listreturns the model catalog permitted at runtime. Refer to the "models.listviews" section below.usage.statusprovides summaries of provider usage windows and remaining quota.usage.costreturns aggregated cost usage summaries for a specified date range. UseagentIdfor a single agent, oragentScope: "all"to aggregate across configured agents.doctor.memory.statusreturns 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, anddoctor.memory.dedupeDreamDiaryall accept an optional{ "agentId": "agent-id" }parameter. If omitted, they operate on the configured default agent workspace.doctor.memory.remHarnessreturns 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. Theoperator.readrequirement must be met.sessions.usagereturns per session usage summaries. PassagentIdfor one agent, oragentScope: "all"to list all 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 data for a single session.sessions.usage.logsreturns usage log entries for a single session.
Channels and login helpers
channels.statusreturns status summaries for built in and bundled channels and plugins.channels.logoutlogs out a specific channel or account when the channel supports that action.web.login.startinitiates a QR or web login flow for the current QR capable web channel provider.web.login.waitwaits for that flow to complete and starts the channel upon success.push.testsends a test APNs push notification 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) 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-emptyqueryand an optionallimitranging 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
sendserves as the direct outbound-delivery RPC for sending messages to channels, accounts, or threads outside the chat runner.logs.tailretrieves the configured gateway file-log tail with cursor and limit controls as well as a maximum byte constraint.
Operator terminal
terminal.openlaunches a host PTY for a specifiedagentIdor the default agent and returns the resolved agent, working directory, shell, and confinement status.terminal.input,terminal.resize, andterminal.closefunction exclusively on sessions owned by the calling connection.terminal.uploadaccepts 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.dataandterminal.exitevents 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;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, including detached ones.
Talk and TTS
talk.catalogretrieves 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-levelreadyresult, 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 setreadyonce runtime provider selection is applied; if it is absent, treat the gateway as unverified on older versions.talk.configprovides the active Talk configuration payload;includeSecretsdemandsoperator.talk.secrets(oroperator.admin).talk.session.createestablishes a Talk session owned by the gateway forrealtime/gateway-relay,transcription/gateway-relay, orstt-tts/managed-room. Forstt-tts/managed-room, callers usingoperator.writewho supplysessionKeymust also includespawnedByto limit session-key visibility to the current scope; unscopedsessionKeycreation andbrain: "direct-tools"requireoperator.admin.talk.session.joinvalidates a session token for a managed room, emittingsession.readyorsession.replacedwhen appropriate. It returns room and session metadata along with recent Talk events, but never exposes the plaintext token or its hash.talk.session.appendAudioadds base64-encoded PCM input audio to realtime relay and transcription sessions that the gateway owns.talk.session.startTurn,talk.session.endTurn, andtalk.session.cancelTurnmanage the turn lifecycle in a managed room, rejecting stale turns before clearing state.talk.session.cancelOutputhalts assistant audio output, primarily used for VAD-gated barge-in within gateway relay sessions.talk.session.submitToolResultfinalizes 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. Supplyoptions: { willContinue: true }for interim tool output, oroptions: { suppressResponse: true }when the provider bridge supports suppression and the result should not trigger another response.talk.session.steersends voice control for an active run into a gateway-owned agent-backed Talk session:{ sessionId, text, mode? }, wheremodeisstatus,steer,cancel, orfollowup; if no mode is specified, it is inferred from the spoken text.talk.session.closeterminates a relay, transcription, or managed-room session owned by the gateway and emits terminal Talk events.talk.modesets or broadcasts the current Talk mode state for WebChat and Control UI clients.talk.client.createcreates or resumes a client-owned realtime provider session usingwebrtcorprovider-websocket, while the gateway retains ownership of credentials, instructions, tool policy, and the returnedvoiceSessionId. Clients providesessionKeyand reusevoiceSessionIdwhen swapping the provider transport during a single call.talk.client.transcriptappends a finalized{ role, text }item to the standard agent session. The requiredentryIdis idempotent withinvoiceSessionId; retries will not duplicate transcript messages.talk.client.closecloses 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.toolCallallows client-owned realtime transports to forward provider tool calls to the gateway policy. The first supported tool isopenclaw_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 returnVOICE_CONFIRMATION_REQUIRED:<id>until a later finalized user utterance explicitly confirms that action, and the next consult provides theconfirmationId.talk.client.steersends voice control for an active run on client-owned realtime transports. The gateway resolves the active embedded run fromsessionKeyand returns a structured accepted or rejected result, rather than silently discarding the steering command.talk.eventserves as the single Talk event channel for realtime, transcription, STT/TTS, managed-room, telephony, and meeting adapters.talk.speakgenerates speech through the currently active Talk speech provider.tts.statusreturns TTS enabled status, the active provider, fallback providers, and provider configuration state.tts.providersreturns the visible inventory of TTS providers.- The TTS preferences state is toggled by
tts.enableandtts.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-emptytextis rendered using the configured general TTS provider chain. The result is a single complete clip returned inline asaudioBase64, accompanied byproviderand optional metadata fieldsoutputFormat,mimeType, andfileExtension. In contrast totts.convert, no Gateway-local path is provided; unliketalk.speak, a Talk provider is not mandatory. When text exceedstts.maxTextLength,INVALID_REQUESTis returned; if synthesis fails,UNAVAILABLEis returned instead.
Secrets, config, update, and wizard
secrets.reloadre-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 usingwarningCount; strict or unmapped failures block the reload and keep the active snapshot intact.secrets.resolveresolves secret assignments for commands and targets within a particular command/target set.config.getprovides the current config snapshot stored on disk, the raw root-filehash, the resolvedconfigRevisionHash, and optionallyappliedConfigHashfor the resolved revision accepted by the active Gateway runtime.config.setcommits a validated configuration payload.config.patchintegrates a partial config update. For destructive array replacement, the affected path must be listed inreplacePaths; nested arrays within array entries use[]paths likeagents.entries.*.skills.config.applyvalidates and replaces the entire configuration payload.config.schemadelivers 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 includestitle/descriptionmetadata from the same labels and help text as the UI, covering 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 plushintPath, optionalreloadKind, and immediate child summaries for UI/CLI drill-down.reloadKindis one ofrestart,hot, ornone(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 exposekey, normalizedpath,type,required,hasChildren, optionalreloadKind, plus the matchedhint/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,continuationMessagecan 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 returnsok: truealong withresult.reason: "managed-service-handoff-started"andhandoff.status: "started". If a second concurrentupdate.runis handled by the same Gateway process, it returnsok: falsewithresult.reason: "managed-service-handoff-already-running"andhandoff.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: falseis returned withmanaged-service-handoff-unavailableormanaged-service-handoff-failed, andhandoff.commandis added when a manual shell update is required. Unavailable means OpenClaw lacks a safe supervisor boundary or durable service identity, for instanceOPENCLAW_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. - 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, andwizard.cancel.
Agent and workspace helpers
-
agents.listretrieves agent entries visible to the gateway, containing effective model and runtime metadata along with optional semantickind(agentorsystem). Clients that advertise theagent-kindhandshake capability get the full typed roster; those without it receive the legacy selector-safe roster that omits system rows. Kind-aware clients excludesystemrows from regular selectors but keep them available in diagnostic views. Older v4 gateways might return rows lackingkind. -
agents.create,agents.update, andagents.deletehandle agent records and workspace configuration. -
agents.files.list,agents.files.get, andagents.files.setmanage the bootstrap workspace files made available for an agent. -
audit.activity.listprovides the versioned activity ledger limited to metadata;audit.listremains the compatibility-safe RPC for runs and tools. -
agents.workspace.listandagents.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, andtasks.cancelexpose the gateway task ledger to SDK and operator clients. See Task ledger RPCs below. -
artifacts.list,artifacts.get, andartifacts.downloadprovide transcript-derived artifact summaries and downloads for a specificsessionKey,runId, ortaskIdscope. 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.listandenvironments.statusmaintain gateway-local and node environment discovery. Configured cloud workers and durable records from 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 provides the terminal snapshot when available.
Session control
sessions.listprovides the current session index, which includesagentRuntimemetadata 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 closedplacementstate (local,requested,provisioning,syncing,starting,active,draining,reconciling,reclaimed, orfailed) 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.subscribeandsessions.unsubscribe. - Transcript and message event subscriptions for a single session are toggled using
sessions.messages.subscribeandsessions.messages.unsubscribe. IncludeincludeApprovals: trueto also receive sanitizedsession.approvallifecycle 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 pendingapprovalReplay, which is authoritative only whentruncatedis false. This opt-in applies per subscribe call and is not sticky: re-subscribing to the same session withoutincludeApprovals: trueremoves any existing approval subscription. Beyond standard session-read authority, this opt-in requiresoperator.admin, oroperator.approvalson a paired device. - For specific session keys,
sessions.previewreturns 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. OptionalmodelandthinkingLevelvalues atomically persist the initial model and reasoning overrides. A managed worktree is provisioned byworktree: true, with optionalworktreeBaseRefandworktreeNameselecting the base reference and branch name, andexecNode(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 initialchat.sendis rejected, the successful result includesrunStarted: falseandrunError; clients can keep the prompt and retry against the returned session key. A caller passingparentSessionKeywithemitCommandHooks: trueshould also declare the lifecycle disposition of a distinct child:succeedsParent: trueends the parent withsession_end, whilefalsekeeps the parent active and emits only the child'ssession_start. OmittingsucceedsParentpreserves 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.labelis 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 includeparentSessionKey(navigation parent, persisted),controlOwnerSessionKey(runtime controller when live),forkSource(exact source key and transcript generation for forks), andpreviousSessionId(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 reachesactiveworker 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, andsessions.groups.deletehandle the catalog of custom session groups owned by the gateway, including their names and display ordering. The membership data lives in each session'scategoryfield; renaming or deleting a group updates the member sessions on the server side.sessions.senddispatches a message into an already existing session.sessions.steerprovides the interrupt-and-steer variant for a session that is currently active.sessions.abortstops any active work in a session. You can providekeytogether with the optionalrunId, or justrunIdfor active runs the gateway can map to a session. IncludingrunIdlimits the cancellation to that specific run. SettingclearQueued: trueon a key-only non-global request also removes the followup and lane queues belonging to that session. Existing callers that leave outclearQueuedkeep those queues intact. Using the literalglobalkey preserves the current agent-qualifiedchat.abortownership behavior and does not clean up non-global followup or lane data.sessions.patchmodifies session metadata and overrides, then returns the resolved canonical model together with the effectiveagentRuntime. 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, andsessions.compactcarry out session maintenance tasks.sessions.getretrieves the complete stored session row.- Chat execution continues to use
chat.history,chat.send,chat.abort, andchat.inject.chat.historyis 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 (exactNO_REPLY/no_reply) are excluded, and oversized rows may be replaced with placeholders. chat.message.getis the additive bounded full-message reader for a single visible transcript entry. SupplysessionKey, optionallyagentIdwhen session selection is scoped to an agent, and a transcriptmessageIdthat was previously surfaced throughchat.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.toolTitlesreturns short purpose titles for tool calls shown in the Control UI (batched, up to 24 items with bounded inputs). This feature is opt-in throughgateway.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 configuredutilityModel(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 emptyutilityModeldisables 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.sendreceives a one-turnfastMode: "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 usingagents.defaults.models["<provider>/<model>"].params.fastAutoOnSeconds. Achat.sendcaller may supply one-turnfastAutoOnSecondsto override the cutoff for that specific request. To override the stored queue mode for only this request, providequeueMode(steer,followup,collect, orinterrupt); explicit Control UI steer actions rely onqueueMode: "steer". Interactive clients can supplyexpectedLeafEntryIdwith the active transcript-branch leaf they are displaying, ornullfor an authoritative empty transcript; the Gateway rejects the send withdetails.reason: "active-leaf-changed"if another client has already switched branches.
Device pairing and device tokens
device.pair.listreturns both pending and approved paired devices.device.pair.setupCodegenerates a mobile setup code and, by default, a PNG QR data URL. It requiresoperator.adminand is deliberately excluded from advertised discovery. The response containssetupCode, optionalqrDataUrl,gatewayUrl, the non-secretauthlabel, andurlSource.device.pair.approve,device.pair.reject, anddevice.pair.removehandle device-pairing records.device.pair.renamesets an operator label ({ deviceId, label }) that takes precedence over the client-reported display name and persists through device repair or re-approval.device.token.rotaterotates a paired device token within the bounds of its approved role and caller scope.device.token.revokerevokes 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, andnode.pair.removeaddress node capability approvals.node.pair.requestandnode.pair.verifywere removed in 2026.7 along with the standalone node pairing store; pending requests are generated by the Gateway during node connections.node.listandnode.describereport known or connected node state.node.renamemodifies a paired node label.node.invokesends a command to a connected node.node.invoke.resultprovides the result for an invoke request.mcp.tools.call.v1serves as the headless node-host command for invoking a configured node-local MCP tool. It travels throughnode.invoke, requires the node to declare the command, and remains subject to pairing approval andgateway.nodes.commands.deny.node.eventrelays node-originated events back into the gateway.node.pluginTools.updateis the only publication path for updating the connected node's agent-visible plugin or MCP tool descriptors;connectparameters do not carry them.node.pending.pullandnode.pending.ackare the connected-node queue APIs.node.pending.enqueueandnode.pending.drainmanage durable pending work for offline or disconnected nodes.
Approval families
approval.historyreturns terminal approvals for exec, plugin, and system-agent requests (scopeoperator.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.getandapproval.resolveare the kind-agnostic durable approval methods (scopeoperator.approvals).approval.getreturns a sanitized projection of a pending or retained terminal approval with a stableurlPath;approval.resolvetakes 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.resolvehandle 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.waitDecisionwaits for 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 through node relay commands.plugin.approval.request,plugin.approval.list,plugin.approval.waitDecision, andplugin.approval.resolvecover plugin-defined approval flows.
Control UI commands
ui.commandallows anoperator.writecaller to send typed layout and navigation commands to connected Control UI clients that advertise theui-commandscapability.- 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
UNAVAILABLErather than pretending the layout changed.
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 needing 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: Chat interface updates from the UI, includingchat.injectand other transcript only chat events. With protocol v4, delta payloads containdeltaText;messageholds the cumulative assistant snapshot. When replacements aren't prefix based,replace=truegets set anddeltaTextserves 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 usingconfig.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, orrunId(optional); an optionalkind(which can be"agent_run","tool_action", or"message"); an optionalstatus(selectable from"started","succeeded","failed","cancelled","timed_out","blocked", or"unknown"); an optional messagedirection(either"inbound"or"outbound") along with an exactchannel; optional inclusive Unix-millisecond limits viaafterandbefore; an optionallimitranging from1to500; and an optional stringcursortaken 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:
eventType | Mandatory 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 consist of:
conversationKind:direct,group,channel, orunknown.- For inbound traffic,
outcome:completed,skipped, orfailed; thereasonCodefield is not required and acceptsduplicate,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. - For outbound traffic,
outcome:sent,suppressed,failed, orunknown; thereasonCodefield is optional and can becancelled_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 does not supply a platform identity isunknown, since the external side effect cannot be refuted. deliveryKind:text,media, orother;failureStage:platform_send,queue, orunknown.
Terminal fields are correlated rather than independently optional.
| Variant | Terminal mapping |
|---|---|
| Agent run | started lacks errorCode; any finished status that is not a success demands the corresponding run_* code. |
| Tool action | started together with succeeded do not carry errorCode; every other finished status requires its matching tool_* code. |
| Inbound message | succeeded 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 message | succeeded 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.listdepends onoperator.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 optionalagentId, an optionalsessionKey, an optionallimitranging from1to500, and an optional stringcursor. - The return value is
{ "tasks": TaskSummary[], "nextCursor"?: string }.
- Its parameters consist of an optional
tasks.getneedsoperator.read.- Parameters:
{ "taskId": string }. - Returns
{ "task": TaskSummary }. - When task IDs are absent, the gateway responds with the not-found error shape.
- Parameters:
tasks.cancelis built onoperator.write.- Parameters:
{ "taskId": string, "reason"?: string }. - Returns
{ "found": boolean, "cancelled": boolean, "reason"?: string, "task"?: TaskSummary }. foundindicates whether the ledger contained a matching task.cancelledshows whether the runtime accepted or logged the cancellation.
- Parameters:
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.agentIdis not required; leave it out to read the default agent workspace.scopedetermines which surface the mainnamepoints at:textgives back the primary text command token without the leading/;nativeand the defaultbothpath return provider-aware native names when those are present.textAliasesholds exact slash aliases like/modeland/m.nativeNameholds the provider-aware native command name if one is defined.provideris not required and only influences native naming and whether native plugin commands are available.includeArgs=falseremoves 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: eithercoreorpluginpluginId: the plugin owner whensource="plugin"optional: whether a plugin tool is optional
tools.effective(operator.read) retrieves the runtime-effective tool inventory for a specific session.sessionKeyis 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.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 is found, the response may contain a notice likemcp-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) calls one available tool through the same gateway policy path used by/tools/invoke.nameis required.args,sessionKey,agentId,confirm, andidempotencyKeyare optional.- When both
sessionKeyandagentIdare supplied, the resolved session agent must matchagentId. - Owner-only core wrappers such as
cron,gateway, andnodesrequire an owner or admin identity (operator.admin) even thoughtools.invokeitself isoperator.write. - The response is an SDK-facing envelope containing
ok,toolName, optionaloutput, and typederrorfields. Approval or policy denials returnok:falsein the payload instead of bypassing the gateway tool policy pipeline.
skills.status(operator.read) retrieves the visible skill inventory for an agent.agentIdis 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.searchandskills.detail(operator.read) retrieve ClawHub discovery metadata.skills.upload.begin,skills.upload.chunk, andskills.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 unlessskills.install.allowUploadedArchivesis 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.mdroot. 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 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; this setting does not impact ClawHub installs. - Gateway installer mode:
{ name, installId, timeoutMs? }executes a declaredmetadata.openclaw.installaction on the gateway host. Older clients may still senddangerouslyForceUnsafeInstall; this field is deprecated, accepted only for protocol compatibility, and disregarded. 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 likeenabled,apiKey, andenv.
models.list views
models.list takes an optional view parameter (src/agents/model-catalog-visibility.ts):
- Omitted or
"default": whenagents.defaults.modelPolicy.allowis set, the response is the allowed catalog, including dynamically discovered models forprovider/*entries. Otherwise, the full gateway catalog is returned. "configured": picker-sized behavior. Ifagents.defaults.modelPolicy.allowis configured, it still takes precedence, 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 exclude provider endpoints, auth material, and runtime request configuration."all": full gateway catalog, bypassingagents.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.
| 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 (GATEWAY_RECONNECT_POLICY) |
| Max reconnect backoff | 30_000 ms | packages/gateway-client/src/client.ts (GATEWAY_RECONNECT_POLICY) |
| 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 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.tokenorconnect.params.auth.password, determined by thegateway.auth.modesetting ("none" | "token" | "password" | "trusted-proxy"). - Identity-based modes like Tailscale Serve (
gateway.auth.allowTailscale: true) or non-loopbackgateway.auth.mode: "trusted-proxy"fulfill the connect auth requirement by reading request headers instead of usingconnect.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 (
selectConnectAuthinsidepackages/gateway-client/src/client.ts):auth.passwordis independent and always forwarded when present.auth.tokenis filled in priority order: an explicit shared token first, then an explicitdeviceToken, then a stored per-device token (keyed bydeviceIdandrole).auth.bootstrapTokenis sent only when none of the previous options resolved toauth.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_MISMATCHretry is limited to trusted endpoints: loopback, orwss://with a pinnedtlsFingerprint. Publicwss://without pinning does not qualify.
- The built-in setup-code bootstrap process returns the primary node
hello-ok.auth.deviceTokenalong with a restricted operator token inhello-ok.auth.deviceTokensfor trusted mobile handoff. This 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, the
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. - Only persist
hello-ok.auth.deviceTokenswhen the connect used bootstrap auth over a trusted transport likewss://or loopback/local pairing. - If a client provides an explicit
deviceTokenor explicitscopes, 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.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 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.rotateanddevice.token.revokealso 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.codealong with 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 using a cached per-device token.
- If that retry fails, stop automatic reconnect loops and display operator action guidance.
AUTH_SCOPE_MISMATCHmeans 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 includedeviceidentity. The only device-less operator exceptions are explicit trust paths:- successful
gateway.auth.mode: "trusted-proxy"operator Control UI authentication. - direct-loopback
gateway-clientbackend RPCs on the reserved internal helper path.
- successful
- 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-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 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:
| 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. - The 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.