Gateway WebSocket Protocol: Handshake, Frames, Versioning
Learn the OpenClaw Gateway WebSocket protocol, including handshake, frames, and versioning. Essential for developers building clients or embedding the Gateway.
Read this when
- Implementing or updating gateway WS clients
- Debugging protocol mismatches or connect failures
- Regenerating protocol schema/models
The Gateway WS protocol serves as 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.
npm packages
These packages ship with OpenClaw release trains. During the initial rollout, npm may return E404 until the first package-bearing release is published.
@openclaw/gateway-protocolpublishes the schemas, validators, TypeScript types, lightweight frame and error helpers, and version constants. Its tarball includes the generatedprotocol.schema.jsonmachine-readable contract.@openclaw/gateway-clientpublishes the reference Node client and a browser-safe entry at@openclaw/gateway-client/browser.
For application lifecycle guidance, see Building a Gateway client. For apps that supervise the Gateway as a child process, see Embedding OpenClaw.
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, traceparent?} - Response:
{type:"res", id, ok, payload|error} - Event:
{type:"event", event, payload, seq?, stateVersion?}
After authentication, a client may include a W3C traceparent string on each
request frame. The Gateway continues a valid value as a child trace context for
that request. Missing or syntactically malformed values within the
128-character field limit keep the default fresh request trace and do not fail
the RPC; longer values make the request frame invalid. The initial connect
request never establishes trace context for later frames. Use a separate
traceparent for each logical request on a long-lived connection; do not treat
the WebSocket itself as one trace.
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).
Gateway-controlled WebRTC Talk
talk.client.create accepts the additive capability
gateway-control-v1. It is currently available only for OpenAI GA Realtime
sessions with resolvable Platform API-key authentication. A successful result
includes clientControl: { owner: "gateway" }, a 60-second single-use Gateway
broker token in clientSecret, and the relative
offerUrl: "/plugins/openai/realtime/calls".
The client sends only application/sdp to that route with the broker token. It
must not create a provider control data channel. The Gateway creates the call,
attaches the provider sideband before returning the answer SDP, and owns tool,
transcript, steering, cancellation, and close lifecycle. Clients that omit the
capability retain the existing browser session behavior. A Gateway or
configured authentication path that cannot provide the requested owner returns
UNAVAILABLE; it never downgrades the request to client-owned control.
Handshake
Gateway sends a pre-connect challenge:
{
"type": "event",
"event": "connect.challenge",
"payload": { "nonce": "…", "ts": 1737264000000 }
}
Device-auth clients use the challenge ts as connect.params.device.signedAt.
For WebSocket challenges, ts must be a non-negative integer. Clients that
explicitly support Gateways from before connect.challenge existed may use local
time only when no challenge arrives; a received challenge with an absent or
malformed ts is invalid.
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 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,
"attachments": { "maxBytes": 20971520, "maxImageBytes": 6291456 }
}
}
}
For HelloOkSchema (packages/gateway-protocol/src/schema/frames.ts), the fields server, features, snapshot, policy, and auth are all mandatory. When no device token gets issued, auth still indicates the agreed-upon role and the effective authorization scopes for the current socket, following the shape shown above. The primary reusable credential for that same device and role, when it exists, is deviceToken. Optional and absent from older gateways, policy.attachments announces the decoded-size limits that chat attachments face on chat.send, sessions.send, and the initial turns of session creation:
| Field | Meaning |
|---|---|
maxBytes | Largest decoded size accepted for a single attachment (agents.defaults.mediaMaxMb, default 20 MB) |
maxImageBytes | Largest decoded size accepted for a single image: min(maxBytes, 6 MB agent-hydration cap) |
Pre-send validation steps:
- Compare every file's decoded size against
maxImageBytesfor images andmaxBytesfor all other content. - Serialize the complete request and verify its encoded size against
policy.maxPayload. Thatpolicy.attachmentsvalue caps each attachment individually, not the whole frame: since attachments travel as base64, a 20 MB file becomes roughly 26.7 MB on the wire, already exceeding the default 25 MiB frame limit by itself. - Let the server be the final authority on everything else. Advertised MIME types and per-message behavior are intentionally withheld, as they hinge on the entrypoint, the resolved model, and payload sniffing. A typed rejection may come from the gateway, while text-only model runs can drop extra images after their offload cap and still finish the request.
- Fetch these values again after every reconnect. They represent a connection-time snapshot, so a live
mediaMaxMbedit only affects existing connections once those connections reconnect.
pluginSurfaceUrls is optional, mapping plugin surface names (such as canvas) to scoped hosted URLs; since it can expire, nodes call node.pluginSurface.refresh with { "surface": "canvas" } to obtain a fresh entry. The legacy canvasHostUrl / canvasCapability / node.canvas.capability.refresh route is unsupported; plugin surfaces are the replacement. sessions.observer.ask has been removed in favor of sessions.companion.ask. The optional appliedConfigHash in the snapshot is the resolved source-config revision the active Gateway runtime accepts. Clients can compare it against config.get.configRevisionHash to see if a newer saved config still requires a restart. config.get.hash stays the raw root-file revision used by config write conflict guards.
While startup sidecars are still being finalized, connect may return a retryable UNAVAILABLE error carrying 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 appends it:
{
"auth": {
"deviceToken": "…",
"role": "operator",
"scopes": ["operator.read"]
}
}
The built-in QR/setup-code bootstrap serves as a mobile handoff path. A successful baseline setup-code connect yields 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 intentionally limited: it suffices to launch the mobile operator loop and native setup, with operator.write covering Talk sessions and operator.talk.secrets handling Talk config reads, but no pairing-mutation scopes and no operator.admin. Broader pairing or admin access demands 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") can skip device on direct loopback connections when authenticating with the shared gateway token/password. This path is reserved for internal control-plane RPCs (for example, subagent session updates) and prevents stale CLI/device pairing baselines from blocking local backend work. Remote, browser-origin, node, and explicit device-token/device-identity clients still undergo normal pairing and scope-upgrade checks.
Worker role and closed protocol
Workers communicate through a restricted protocol, either over the public /__openclaw__/worker WebSocket path on the main TLS endpoint or via the loopback ingress that the gateway-owned, host-key-pinned SSH tunnel exposes. Before any frames are read, the route picks the worker mode, so general authentication, node events, operator RPCs, and plugin methods are never dispatched. Public admission draws from the same per-client pre-auth budget and authentication rate limiter as the main path; its wire errors reduce credential and environment details to admission-rejected, whereas trusted gateway diagnostics keep the internal reason intact. A strict connect validates a short-lived credential stored as a hash-at-rest and tied to the environment, along with the bundle hash, owner epoch, RPC-set version, expiry, and one nullable session; the current version and feature set get checked separately. A successful result yields minimal worker-hello-ok; feature negotiation operates independently of the general protocol version. Frames are capped at 64 KiB, except a negotiated worker.inference.start frame, which can reach 25 MiB. The closed allowlist consists of worker.heartbeat, worker.transcript.commit, worker.live-event, worker.inference.start, and worker.inference.cancel.
For an attached run under identity audit, the live turn capability can log the credential, build, owner-epoch, and placement checks as one enforced admission receipt. That receipt carries none of the credential, build hashes, tokens, environment id, or session id. Worker operation rows and placement state remain the authoritative owners; a successful connection does not count as an action-success receipt.
Transcript commits rely on owner-epoch fencing, a session binding owned by the gateway, base-leaf compare-and-swap, and durable sequence replay; the gateway produces transcript entry and parent IDs through the standard session writer. Each RPC triggers a fresh check of ownership and expiry.
Client capabilities
Operator clients can 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 lack Gateway client capabilities, so capability-gated tools stay unavailable even when tool policy explicitly permits 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.talkoperator.talk.secrets
operator.write continues to satisfy operator.talk for compatibility with
existing clients. Voice-device setup can issue the narrower Talk grant without
general Gateway write access.
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 performs an additional scope check at approval time, layered on top of the base method scope (operator.pairing), which draws on 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, browser.proxy.upload.v1, fs.listDir, or system.execApprovals.get/set | operator.pairing + operator.admin |
Within this table, fs.listDir refers to the node command that node.invoke relays. For the top-level Gateway fs.listDir RPC, operator.write is needed to browse the host's workspace, and operator.admin becomes necessary when nodeId is present.
Caps/commands/permissions (node)
At connection time, nodes declare their capability claims:
caps: broad capability groupings likecamera,canvas,screen,location,voice, andtalk.commands: the command allowlist used for invoke operations.permissions: fine-grained switches, for instancescreen.record,camera.capture.
The Gateway views these as claims and applies server-side allowlists for enforcement. After a successful connect or reconnect, connected nodes may publish optional agent-visible plugin or MCP tool descriptors using node.pluginTools.update. Headless node hosts restart to apply declarative MCP inventory changes. This update method serves as the sole publication route; plugin tool descriptors are not permitted in connect params. Every descriptor must employ a provider-safe tool name and specify a command that exists in the node's current command allowlist. The Gateway trusts descriptor metadata coming from the paired node, filters out descriptors that fall outside the approved command surface, removes them upon node disconnect, and blocks operator attempts to alter another node's catalog. To disregard node-published descriptors, set gateway.nodes.pluginTools.enabled: false.
Connected node hosts publish their full skill replacement catalog via node.skills.update. This node-role method is the only way nodes publish skills; connect params do not accept skills. Each descriptor carries a safe name, a description, and bounded SKILL.md content. The Gateway processes that content with the standard skills loader, incorporates it into agent skill snapshots while the node stays connected, and drops it when the node disconnects. Setting gateway.nodes.allowSkills: false makes the Gateway ignore node-published skills.
Presence
system-presencedelivers entries keyed by device identity, coveringdeviceId,roles, andscopes, which lets UIs render a single row per device even when it connects in both operator and node roles.node.listoptionally includeslastSeenAtMsandlastSeenReason. Connected nodes report current connection time with reasonconnect; paired nodes can additionally report durable background presence through a trusted node event.
Native macOS nodes can also transmit authenticated node.presence.activity events with bounded input idle time. The Gateway calculates activity timestamps using its own clock, surfaces the most recent connected Mac via node.list and node.describe, and sends node.presence updates to read-scoped clients. When the user disables activity sharing, the app sends { "action": "clear" }; the Gateway clears timestamps only for that specific authenticated node connection. Gateways that predate this acknowledged action treat it as unhandled, so the Mac node reconnects once and lets disconnect cleanup remove the old connection state. For selection, privacy, model context, and notification-routing details, see Active computer presence.
Node background alive event
Nodes use node.event together with event: "node.presence.alive" to log that a paired node was reachable during a background wake, without setting it to 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. Any unrecognized value gets normalized to
background (src/shared/node-presence.ts). The event is stored only for authenticated node device sessions; sessions without a device or pairing return
handled: false.
Gateways that succeed produce a structured result:
{
"ok": true,
"event": "node.presence.alive",
"handled": true,
"reason": "persisted"
}
Legacy gateways might respond with just { "ok": true } for node.event; interpret that as an acknowledged RPC, not as durable presence storage.
Broadcast event scoping
Broadcast events pushed from the server are filtered by scope, 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) demand at leastoperator.read. Sessions lacking it get none of these frames. - Plugin-defined
plugin.*broadcasts default tooperator.writeoroperator.admin; explicit entries likeplugin.approval.requested/plugin.approval.resolvedrely onoperator.approvalsinstead. - Status/transport events (
heartbeat,presence,tick, connect/disconnect lifecycle) remain open so every authenticated session can observe transport health. - Unknown broadcast event families are scope-gated by default (fail-closed) unless a registered handler explicitly opens them up.
Each client connection tracks 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 assembled from
src/gateway/server-methods-list.ts plus loaded plugin/channel method
exports, not a generated dump of every method, and some methods (for
example push.test, web.login.start, web.login.wait, sessions.usage)
are deliberately left out of 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.restart.preflightis a deprecated, read-only compatibility preview of restart-specific active work. It does not close admission, create a suspension lease, or provide the atomic full-work fence ofgateway.suspend.prepare; new restart flows should callgateway.restart.request.gateway.suspend.preparecreates a short cooperative-suspension lease only when tracked Gateway work is idle. While prepared, authenticated WebSocket connects remain available, but onlygateway.suspend.*and an exact targeted non-safegateway.restart.requestmay run; safe and untargeted restarts remain fenced.gateway.suspend.statuschecks the lease, andgateway.suspend.resumereleases it after thaw or an aborted host operation.
Models and usage
models.listprovides the model catalog that the runtime permits. Refer to "models.listviews" further down.usage.statusgives summaries of provider usage windows and remaining quota.usage.costsupplies aggregated cost usage summaries across a date range. UseagentIdfor a single agent, oragentScope: "all"to combine configured agents.doctor.memory.statusreports vector-memory and cached embedding readiness for the active default agent workspace. Pass{ "probe": true }or{ "deep": true }solely for an explicit live embedding provider ping. Include{ "agentId": "agent-id" }to restrict Dreaming store stats to one agent workspace; if omitted, configured Dreaming workspaces are aggregated.doctor.memory.dreamDiary,doctor.memory.backfillDreamDiary,doctor.memory.resetDreamDiary,doctor.memory.resetGroundedShortTerm,doctor.memory.repairDreamingArtifacts, anddoctor.memory.dedupeDreamDiaryall take an optional{ "agentId": "agent-id" }; when left out, they target the configured default agent workspace.sessions.usagedelivers per-session usage summaries. PassagentIdfor one agent, oragentScope: "all"to present configured agents together. Both usage methods acceptmode: "specific"with an IANAtimeZonefor DST-aware calendar-day boundaries and buckets.utcOffsetstays supported for older clients and as a fallback when the Gateway runtime does not recognize the requested zone.sessions.usage.timeseriesprovides timeseries usage for one session.sessions.usage.logsreturns usage log entries for one session.
Channels and login helpers
channels.statusreturns status summaries for built-in and bundled channel/plugin components.channels.logoutlogs out a specific channel/account where the channel supports it.web.login.startinitiates a QR/web login flow for the current QR-capable web channel provider.web.login.waitwaits for that flow to finish 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, acknowledgeInstallPolicyWarning? }or a ClawHub package with{ source: "clawhub", packageName, version?, acknowledgeClawHubRisk?, acknowledgeInstallPolicyWarning? }. When install policy returnswarn, the errordetailsincludeinstallPolicyCode: "install_policy_warning_acknowledgement_required", the target, reason, and optional findings. After review, retrying the same action withacknowledgeInstallPolicyWarning: trueapproves every warning in that install invocation; each warning is freshly evaluated before installation continues.blockand policy failures remain terminal. ClawHub installs preserve Gateway trust and integrity 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
- When
terminal.openis invoked, a host PTY gets started for either a specifiedagentIdor the default agent, and the response includes the resolved agent, working directory, shell, and confinement state. If you passsessionKey, the PTY is tied to that exact agent session, and the calling connection becomes its initial viewer; without it, an operator terminal owned by the connection is created. - Both
terminal.inputandterminal.resizetarget sessions that belong to the calling connection, as well as agent-owned sessions where that connection is listed as an attached viewer. Withterminal.close, a connection-owned session is terminated, but for an established agent-owned session, only the calling viewer gets detached. When a Control UI terminal is new and session-bound, closing or disconnecting the initiating viewer discards the PTY until either the browser or the exact-session agent adopts it first through an authorized operation. - A single base64 file, capped at 16 MiB, is accepted by
terminal.upload, which stages it in a private temporary directory lasting 24 hours on the session's Gateway or paired-node host, then returns the absolute path. That path must still be pasted or otherwise used by the caller; the RPC never writes terminal input or runs a command. - Events from
terminal.dataandterminal.exitare streamed to both the connection owner and any attached viewers. When a task-owned agent terminal's authoritative task reaches a terminal state, that terminal closes, while ordinary conversation-owned agent terminals stay persistent. - If a connection-owned session loses its connection, it is detached rather than killed: it remains reattachable for
gateway.terminal.detachedSessionTimeoutSeconds(default 300;0restores kill-on-disconnect), and recent output is kept in a bounded server-side buffer. Established agent-owned sessions also survive viewer disconnect. - Attachable sessions are returned by
terminal.list. The replay buffer comes fromterminal.attach, which either rebinds a connection-owned session (a tmux-style take-over, where a previous live owner getsterminal.exitwith reasondetached) or adds the connection as a viewer of an agent-owned session. - Every terminal method demands
operator.admin;gateway.terminal.enableddefaults to on and rejects every method when set tofalse. Fully sandboxed agents are refused, and changing an agent policy closes existing and in-flight PTYs, including detached ones.
Talk and TTS
talk.catalogprovides the read-only catalog of Talk providers for speech, streaming transcription, and realtime voice. It covers 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. Provider secrets are never returned, and global config is left untouched. Current gateways setreadyafter runtime provider selection is applied; if it is missing, treat the gateway as unverified on older versions.talk.configdelivers the effective Talk config payload;includeSecretsneedsoperator.talk.secrets(oroperator.admin).talk.session.create(operator.talk) establishes a gateway-owned Talk session forrealtime/gateway-relay,transcription/gateway-relay, orstt-tts/managed-room. Forstt-tts/managed-room, non-admin callers supplyingsessionKeymust also supplyspawnedByto get scoped session-key visibility; unscopedsessionKeycreation andbrain: "direct-tools"demandoperator.admin.talk.session.appendAudioadds base64 PCM input audio to gateway-owned realtime relay and transcription sessions.talk.session.cancelOutputhalts assistant audio output, mainly for VAD-gated barge-in in gateway relay sessions. Send the currenttalk.event.turnId; the outcome isapplied,stale, oridle.talk.session.submitToolResultfinalizes a provider tool call from a gateway-owned realtime relay session. The request waits for any asynchronous completion signal from the provider bridge; failed submissions keep the linked run active and do not emit a successful tool-result event. Useoptions: { willContinue: true }for interim tool output oroptions: { suppressResponse: true }when the provider bridge advertises suppression support and the result should not trigger another response.talk.session.steerpushes active-run voice control into a gateway-owned agent-backed Talk session:{ sessionId, text, mode? }, wheremodeisstatus,steer,cancel, orfollowup; if omitted, the mode is inferred from the spoken text.talk.session.closeterminates a gateway-owned relay, transcription, or managed-room session and emits terminal Talk events.talk.modesets or broadcasts the current Talk mode state for WebChat/Control UI clients.talk.client.createcreates or resumes a client-owned realtime provider session usingwebrtcorprovider-websocket, while the gateway holds credentials, instructions, tool policy, and the returnedvoiceSessionId. Clients passsessionKeyand reusevoiceSessionIdwhen swapping the provider transport during a single call. Clients negotiatinggateway-control-v1keep WebRTC media direct but shift the provider control channel and tool lifecycle to the Gateway.talk.client.transcriptappends one finalized{ role, text }item to the normal agent session. The requiredentryIdis idempotent withinvoiceSessionId; retries do not duplicate transcript messages.talk.client.closecloses the logical voice session after pending transcript writes. Closing is idempotent and may deliver a mutation-only call digest to the session's last non-WebChat channel.talk.client.toolCallallows client-owned realtime transports to forward provider tool calls to gateway policy. The first supported tool isopenclaw_agent_consult; clients receive a run id and 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 exact final execution action and the next consult supplies theconfirmationId; policy or hook rewrites require confirmation again.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.setProviderrefreshes the preferred TTS provider setting.tts.convertexecutes a single-pass text-to-speech operation.tts.speak(operator.write) processes any non-emptytextthrough the standard TTS provider chain and delivers a single inline audio clip asaudioBase64, along withproviderand, when applicable,outputFormat,mimeType, andfileExtensionmetadata. In contrast totts.convert, no Gateway-local file path is produced; unliketalk.speak, a Talk provider is not needed. Input exceedingtts.maxTextLengthyieldsINVALID_REQUEST; if synthesis fails,UNAVAILABLEis returned.
Secrets, config, update, and wizard
secrets.reloadre-evaluates active SecretRefs and publishes owner-aware runtime state atomically. When eligible owners fail, the degradation can be published as cold or stale usingwarningCount; strict or unmapped failures cause the reload to be rejected, keeping the active snapshot intact.secrets.resolveresolves secret assignments tied to a given command/target combination.secrets.store.list(operator.admin) delivers team-scoped metadata and values exclusively forkind: "env"records.kind: "secret"records come back in a separate result format without a value field, and no reveal operation exists for them.secrets.store.setandsecrets.store.delete(operator.admin) either create/update or soft-delete a single team-scoped record. Once the write succeeds, the Gateway refreshes the active secrets runtime only if the name appears in astoreSecretRef within the active source config.config.getprovides the current on-disk config snapshot, the raw root-filehash, the resolvedconfigRevisionHash, and an optionalappliedConfigHashtied to the resolved revision that the active Gateway runtime has accepted.config.setcommits a validated config payload.config.patchapplies a partial config update. For destructive array replacement, the affected path must be listed inreplacePaths; nested arrays under array entries rely on[]paths, for exampleagents.entries.*.skills.config.applyvalidates and swaps in the complete config payload.config.schemasupplies the live config schema payload for Control UI and CLI tooling, covering schema,uiHints, version, generation metadata, and plugin plus channel schema metadata when those can be loaded. It also includestitle/descriptionmetadata drawn from the same labels and help text as the UI, including nested object, wildcard, array-item, andanyOf/oneOf/allOfcomposition branches wherever matching field documentation exists.config.schema.lookupreturns a path-scoped lookup payload for one config path, containing the normalized path, a shallow schema node, the matched hint plushintPath, an 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 preserve user-facing docs and standard 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, and the matchedhint/hintPath.update.runtriggers the gateway update process and only schedules a restart when the update is successful; session holders may passcontinuationMessageso that startup picks up one extra agent turn via the restart continuation queue. Updates from package managers and supervised git-checkout updates directed by the control plane rely on a detached managed-service handoff rather than swapping the package tree or altering checkout/build output within the active gateway. A handoff that has started yieldsok: truealong withresult.reason: "managed-service-handoff-started"andhandoff.status: "started". A second simultaneousupdate.runprocessed by the same Gateway instance yieldsok: falsewithresult.reason: "managed-service-handoff-already-running"andhandoff.status: "already-running"; its continuation is rejected, so the caller can retry once the ongoing update finishes. Standalone CLI updaters and replacement Gateway processes fall outside this process-local safeguard. Handoffs that are unavailable or unsuccessful returnok: falsewith eithermanaged-service-handoff-unavailableormanaged-service-handoff-failed, and additionallyhandoff.commandwhen a manual shell update is needed. Unavailable indicates OpenClaw lacks a secure supervisor boundary or durable service identity, for instanceOPENCLAW_SYSTEMD_UNITfor systemd. During a started handoff, the restart sentinel might briefly showstats.reason: "restart-health-pending"; the continuation waits until the CLI confirms the restarted gateway and records the finaloksentinel.update.statusrefreshes and returns the most recent update restart sentinel, including the post-restart running version when it exists.wizard.start,wizard.next,wizard.status, andwizard.cancelmake the onboarding wizard available through WS RPC.
Agent and workspace helpers
agents.listreturns agent entries visible to the gateway, with effective model/runtime metadata and optional semantickind(agentorsystem). Entries that have recorded creation provenance also carrycreatedVia(operator,agent, orclaw), nullablecreatorAgentId, and millisecondcreatedAt; entries lacking provenance omit these fields. Clients announcing theagent-kindhandshake capability get the full typed roster; those without it retain the legacy selector-safe roster that excludes system rows. Kind-aware clients filter outsystemrows from standard selectors but keep them in diagnostic views. Older v4 gateways may return rows withoutkind.agents.create,agents.update, andagents.deletehandle agent record management and workspace configuration.agents.files.list,agents.files.get, andagents.files.setmanage the bootstrap workspace files exposed for an agent.audit.activity.listprovides the versioned metadata-only activity ledger;audit.run.inspectlocates execution ids or inspects a single execution identity context;audit.liststays the compatibility-safe run/tool RPC.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), size-capped, and limited to UTF-8 text plus common image types (base64). Responses never reveal the host workspace path. This namespace contains no write operations.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.status(operator.read) stay accessible even without cloud-worker profiles, preserving discovery of gateway-local and node environments. Node environments carry the durablesessionHostidentity, which keeps a known offline host visible; however, the current connected inventory takes precedence over that history. When identity is missing, the value is false. The exact bounded{ total, available }worker slots exist only during live operation and are omitted when offline. Configured cloud workers and durable records from earlier profiles contributeworkermetadata, includingproviderId, optionalleaseId,state,ageMs, optionalidleMs, andattachedSessionIds. Worker lifecycle states encompassrequested,provisioning,bootstrapping,ready,attached,idle,draining,destroying,destroyed,failed, andorphaned. A connected node might also includeworkerBundle: { status: "installed", version }orworkerBundle: { status: "missing" }. This optional observation is limited to the reconnect scope and confirms validation of a single Gateway-retained bundle; it does not grant launch authority. The public result never reveals the bundle hash, Gateway namespace, node filesystem path, receipt, or protocol-feature specifics.environments.create({ profileId, idempotencyKey }) creates a worker using a configured plugin provider profile; retrying with the same key reuses the durable operation.environments.destroy({ environmentId }) asks for idempotent removal of a durable worker environment. Both demandoperator.admin, act as control-plane writes, and produce the same environment summary format as status responses.worker.desktop.observe({ environmentId, control? },operator.admin) launches or reuses the environment's desktop forward, returning{ transport, wsPath, expiresAtMs, control, vncPassword? }.wsPathholds a one-time 60-second token for the Gateway's desktop observer WebSocket; reconnecting requires a new observe call. Environments with an observable desktop advertiseworker.desktop: trueinenvironments.list. This method is advertised only when thecloudWorkers.desktoplab is enabled. Refer to Cloud workers.agent.identity.getprovides the effective assistant identity for an agent or session.agent.waitblocks until a run completes and returns the terminal snapshot if available.
Session control
sessions.listprovides the present session index, which includes per-rowagentRuntimemetadata when an agent runtime backend is set up. The authoritative aggregate fact for direct-session activity ishasActiveRun. When projected,activeRunIdsrepresents the full exact active set; an empty array indicates that the session is idle. If aggregate activity is true but the field is missing, another runtime owner is active, yet its specific identities cannot be determined. A missing field in a snapshot means identities are unavailable. For incremental events, omission signals no change,nullacts as the event-only tombstone that resets cached exact IDs to unavailable, and an array replaces the cache. Clients correlate only exact IDs they own locally or that came from requests, history, or events, and they never treat the first list entry as an owner. When cloud-worker placement is enabled or durable recovery state exists, session rows additionally include a closedplacementstate (local,requested,provisioning,syncing,starting,active,draining,reconciling,reclaimed, orfailed) along with state-specific environment, owner-epoch, workspace, bundle, ACK-cursor, or recovery fields. Active placements may carry an advisorydiskSpacesample withstatus(ok,warning, orcritical),availableBytes,totalBytes, andobservedAtMs. An active paired-device placement also includesrunner: { kind: "device", status: "available" | "offline", deviceId? };deviceIdidentifies the paired device hosting the placement (the chosen host forautoDevicedispatch), and non-device placements leave this field out. This availability is process-current, derived from the exact active environment binding and reconnect-scoped node-runner proof, and it starts offline after a Gateway restart until that runner reconnects. Inventory changes triggersessions.changedso clients refresh the canonical row. Rows carry ownership projections: write-oncecreatedActor, the mutableowner(actor plusassignedBy/assignedAt), a boundedparticipantslist (owner excluded, up to 4 actors), and the fullparticipantCount; actor display labels and avatars are resolved from current profiles and agent identities at read time. PasscreatorIdto filter by immutablecreatedActor.id; passownerIdto filter by the current assignable owner, falling back tocreatedActorwhen no owner is assigned. The completeownersfacet is independent of pagination and remains unfiltered by either query, so clients can render the full owner picker. Authenticated callers can passinvolvingMe: trueto keep only sessions the caller owns or has prompted, evaluated against the full participant history (profile-backed human participants only).sessions.subscribeturns on session change events for the current WebSocket client. The subscription ends when that client disconnects.sessions.messages.subscribeandsessions.messages.unsubscribeswitch on transcript/message event subscriptions for one session. PassincludeApprovals: trueto also receive sanitizedsession.approvallifecycle events for approvals whose persisted audience includes that exact session and whose reviewer binding authorizes the subscribing client. The subscribe response then includes a bounded pendingapprovalReplay; it is authoritative whentruncatedis false. The opt-in is per subscribe call, not sticky: re-subscribing to the same session withoutincludeApprovals: trueremoves an existing approval subscription. In addition to normal session-read authority, this opt-in requiresoperator.admin, oroperator.approvalson a paired device.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 by key, raw session ID, label, or Control UI short ID. Ambiguous short IDs return a bounded candidate list as a successful RPC result.sessions.createestablishes a fresh session record. When supplied,model,contextWindow, andthinkingLevelpersist the starting model, the advertised context-window preference, and reasoning overrides as a single atomic operation; an optionalcategoryattaches the session to a custom group, creating that group on its first use.worktree: truesets up a managed worktree, withworktreeBaseRef/worktreeNameoptionally picking the base reference and branch name, whileexecNode(operator.admin) ties session execution to a specific node host. In the absence ofworktreeName, OpenClaw generates a human-readable name from the session label or the title of the first generated message, defaulting to a crustacean-themed name if neither exists; if another owner, a local branch, or an unmanaged path already claims that name, a numeric suffix is appended. The resulting worktree appears in the response and is stored on the session row viaworktree: { id, branch, repoRoot }. Should the entry be created but its nested initialchat.sendbe refused, the successful response carriesrunStarted: falseandrunError; clients can retain the prompt and retry using the returned session key. When a caller providesparentSessionKeyalong withemitCommandHooks: true, it must also specify the lifecycle outcome for a separate child:succeedsParent: trueterminates the parent withsession_end, whereasfalseleaves the parent running and emits only the child'ssession_start. Leaving outsucceedsParentkeeps the old parent-rollover behavior for existing clients. This disposition depends on both parent linkage and command hooks; a fork cannot outlive its parent. Reset-in-place for main sessions stays unchanged because no distinct child is generated. New rows get write-once creation provenance (createdVia,createdActor,createdAt) from the trusted creation seam; adopting an existing key never re-stamps it. For human profile actors,createdActor.labelis derived from the current user profile at projection time and is never saved on the session entry, so profile renames cause no drift. Session rows additionally carryparentSessionKey(navigation parent, persisted),controlOwnerSessionKey(runtime controller when active),forkSource(exact source key plus transcript generation for forks), andpreviousSessionId(prior transcript generation under the same key).sessions.dispatchrelocates an authorized local OpenClaw session, one with a live, registry-owned managed worktree, to a paired device or a designated cloud profile. Use{ key, deviceId, agentId? }for a specific device,{ key, autoDevice: true, agentId? }for automatic paired-device selection,{ key, profileId, machineClass?, agentId? }for a specific profile, or{ key, agentId? }to resolve the managed worktree's normalized origin incloudWorkers.projectProfiles. These target modes are mutually exclusive, and explicit targets override project-profile lookup. Automatic selection sorts worker-slot runtimes by available slots, then by device ID; runtimes without worker slots fall back to device ID order. If a candidate becomes ineligible during dispatch, up to three ranked candidates are attempted; other errors are not retried. Explicit and automatic device dispatch demandoperator.write; explicit-profile and project-profile dispatch demandoperator.admin. A missing origin, an unmatched mapping, or a mapping to an unconfigured profile yields a typedINVALID_REQUESTwithout provisioning or fallback. Malformed parameters use the write scope before schema validation. A missing cloud profile hides only cloud targets; eligible paired-device dispatch stays available. Dispatch closes local turn admission before draining active work and returns only after placement reachesactiveworker ownership. Arbitrary plain directories cannot be dispatched; after admission, the workspace transport may use manifest mirroring if the managed worktree's Git metadata later becomes unavailable. SSH fallback candidates rotate only for idempotent probes, content-addressed transfers, receipt/lock-guarded artifact installation, convergent managed-worktree mirroring, and tunnel reconnects. Ambiguous unguarded stateful commands fail closed and are not replayed. Dispatch is one-way; worker-to-local pull-back is not part of this RPC.sessions.reclaim(operator.write) safely halts a session placement by key. It waits for an in-flight dispatch, drains admitted work, reconciles active workspace changes, and retries pending failed-environment teardown through the placement owner. Callers never need raw environment-destroy authority.sessions.moveshifts an authorized active session to the Gateway, a paired device, or a configured profile. Gateway and device targets requireoperator.write; profile targets requireoperator.admin; malformed targets use the write scope before schema validation. The caller provides the exact observed generation, environment, and owner epoch; session authorization and those source facts are revalidated before the move commits. Ordinary moves always reconcile the source. Only a Gateway target may addabandonSource: true, and only when the exact source is a currently offline paired-device placement. That durable decision force-fences and destroys the remote owner, skips remote workspace reconciliation, and continues from the last Gateway-synced state without replay; unsynced files and in-flight work may be lost. Available, unknown, profile, and other-worker sources reject explicit abandonment.sessions.groups.list,sessions.groups.put,sessions.groups.rename, andsessions.groups.deletehandle the gateway-owned custom session group catalog (names + display order). The read-scoped list result is intentionally path-free.sessions.groups.defaultsandsessions.groups.updaterequireoperator.writeand read or replace one custom group's optional working-directory and worktree defaults. Non-admin callers can save only directories inside a configured agent workspace; other absolute Gateway paths requireoperator.admin. Membership stays on each session'scategoryfield; rename and delete update member sessions server-side.sessions.senddelivers a message into an existing session.sessions.steeris a deprecated alias forchat.sendwithqueueMode: "interrupt"; removal follows the protocol deprecation policy.- Calling
sessions.abortterminates any in-flight work tied to a session. You can passkeytogether with the optionalrunId, or justrunIdwhen the gateway can map an active run to a session. AddingrunIdconfines the cancellation to that specific run. For a key-only non-global request, settingclearQueued: truealso clears the followup and lane queues that belong to the session. Callers who leave outclearQueuedkeep those queues intact. Using the literalglobalkey applies the existing agent-qualifiedchat.abortownership rules, and no non-global followup or lane cleanup is attempted. - Session metadata and overrides are refreshed by
sessions.patch, which also reports the resolved canonical model and the effectiveagentRuntime. Only an id listed in the selected model'scontextWindowsarray is accepted bycontextWindow;nullbrings backcontextWindowDefault. To modify session organization fields or the per-sessionmodeloverride,operator.writeis necessary, while thinking, fast, verbose, trace, reasoning, and other privileged overrides demandoperator.admin. A default agent configuration can only be persisted through an admin model selection. Archive and restore patches need the caller-observedsessionIdfromsessions.listorsessions.describeasexpectedSessionId; if it is missing or altered, the operation fails without creating or modifying a replacement. Whenarchived: trueis set, the Gateway shields agent main sessions (includingglobalwhen global scope is active) and theunknownsentinel; for any other real session, it first blocks new admissions, cancels all active, pending, queued, reply, embedded, and worker work tied to that session, and waits for admission and runtime terminal-persistence drains to finish before applyingarchivedAt. A failure during cancellation, draining, or persistence yields retryableUNAVAILABLEand leaves the session unarchived. Per target,sessions.patchManycarriesexpectedSessionId, prepares archive targets in input order within the same batch lifecycle fence, and returns ordered per-target results. Spawn lineage (spawnedBy,spawnedWorkspaceDir,spawnedCwd,spawnDepth,subagentRole,subagentControlScope) can no longer be patched publicly; trusted creation paths write those details once, and any request still including them gets rejected. - The session's mutable owner is reassigned by
sessions.assignOwner(operator.write) to a person or a configured agent ({ key, owner: { type, id } }). An identified caller is required (an authenticated profile or a trusted agent identity), authorization is based on session visibility, andassignedBy/assignedAtare recorded in the row'sownerfield. The write-oncecreatedActorand creator-anchored sharing authority remain unchanged; refer to Multi-user mode. - Session upkeep is handled by
sessions.reset,sessions.delete, andsessions.compact. - The complete stored session row is provided by
sessions.get. - Chat execution continues to rely on
chat.history,chat.send,chat.abort, andchat.inject. ItssessionInfoapplies the same combinedhasActiveRunand optional exact-completionactiveRunIdsbehavior assessions.list. For UI clients,chat.historyis normalized for display: inline directive tags get stripped from the 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) plus leaked ASCII or full-width model control tokens are removed, pure silent-token assistant rows (exactlyNO_REPLYorno_reply) are left out, and oversized rows may be swapped for placeholders. Tail responses may carry an opaquedeltaCursor. Supply it back ascursortochat.historyorchat.startuprather thanoffsetormessageId. A successful catch-up yields{ kind: "delta", messages, deltaCursor, sessionInfo }; feed eachmessagesentry through the same reducer used for a livesession.messagepayload.{ kind: "reset" }signals that the cursor is invalid, stale, tied to another session, crossed a reset or compaction, or lagging too far; pull a normal tail page instead. Catch-up never delivers a partial page or continuation: exceeding 200 raw events or the 1 MB payload limit falls back to a tail fetch. chat.message.getserves as the additive bounded full-message reader for one visible transcript entry. ProvidesessionKey, optionalagentIdwhen session selection is agent-scoped, and a transcriptmessageIdpreviously exposed viachat.history; the gateway returns the same display-normalized projection without the lightweight history truncation cap, provided the stored entry remains available and is not oversized.chat.toolTitlesgenerates 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(off by default); disabled gateways answer{ titles: {}, disabled: true }without a model call so clients stop requesting. When enabled, titles follow standard utility-model routing: an explicitly configuredutilityModel(an operator choice that, like all utility tasks, may send bounded task content to the chosen provider), otherwise the session provider's declared small-model default so no new egress destination appears implicitly; an emptyutilityModelturns them off entirely. Titles never fall back to the primary model. Results are cached in the per-agent state database keyed by tool name plus input, so repeated views never re-bill the same calls.chat.sendaccepts a one-turnfastMode: "auto"to use fast mode for model calls initiated 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 set per model viaagents.defaults.models["<provider>/<model>"].params.fastAutoOnSeconds. Achat.sendcaller can pass a one-turnfastAutoOnSecondsto override the cutoff for that request. PassqueueMode(steer,followup,collect, orinterrupt) to override the stored queue mode for this request only; explicit Control UI steer actions usequeueMode: "steer". Interrupt mode captures and aborts the session's current admitted turn, waits for that exact owner to settle, then starts the new turn; an idle session starts normally. A steer send targets the selected session's current state: the Gateway atomically injects the message into that session's direct active run, or starts a new turn when the session is idle. Activity in descendant subagent sessions never makes the selected session busy for this decision.expectedLeafEntryIdis an independent transcript-branch compare-and-swap for non-steer interactive sends: pass the displayed branch leaf (or deliberatenullfor an authoritative empty transcript) and the send rejects withdetails.reason: "active-leaf-changed"if another client switched transcript branches first; steer sends ignore it.
Device pairing and device tokens
device.pair.listfetches both pending and approved paired devices.device.pair.setupCodegenerates a mobile setup code and, unless configured otherwise, a PNG QR data URL. It depends onoperator.adminand is deliberately excluded from advertised discovery. Modern gateways ship with an opaque non-secretsetupId, authoritativeexpiresAtMs,setupCode, optionalqrDataUrl,gatewayUrl, the non-secretauthlabel,urlSource, and the assignedaccesstier (full,limited, ornode). Legacy protocol-v4 gateways lacksetupIdandexpiresAtMs, so separately shipped clients must treat those lifecycle fields as optional. ThesetupIdoperates independently of the bootstrap credential and never appears inside the setup code.device.pair.setupStatusaligns a single setup credential previously issued by the caller ({ setupId }). It needsoperator.admin, stays out of advertised discovery, and yields either{ completion }once the credential-bearing response completes or{ deliveryUncertain }if the bearer was retired but response delivery remained unconfirmed. Both outcomes carry the same non-secret payload as their matching events. With both fields missing, the gateway retains no pending result for thatsetupId.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 endures device repair or re-approval.device.token.rotaterotates a paired device token within its approved role and caller scope limits.device.token.revokeinvalidates a paired device token within its approved role and caller scope limits.
The setup code carries a short-lived bootstrap credential. Clients must not log or store it beyond the pairing process.
Pairing-scoped clients get device.pair.setup.completed only once the
exact setup handoff has delivered its credentials. Its payload is
{ setupId, deviceId, deviceName?, access, ts }; it never contains the
bootstrap credential or token-derived identifiers.
If the response closes before delivery is confirmed, the gateway keeps
the bearer retired and emits device.pair.setup.deliveryUncertain instead
of success. The presenting client should give the operator a way to inspect
or remove the paired device and issue a fresh setup code.
The gateway logs an uncertain outcome when it consumes the bearer, then
upgrades it to completion only after response delivery finishes. Operator
event frames are best effort and drop for slow subscribers rather than
closing their socket. A client that displayed a setup code must therefore
call device.pair.setupStatus before presenting the code as expired.
Outcomes are retained past the credential's own expiry.
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 dropped in 2026.7 along with the standalone node pairing store; pending requests are generated by the Gateway during node connects.node.listandnode.describereport known/connected node state.node.renamechanges a paired node label.node.invokesends a command to a connected node.node.invoke.resultprovides the result for an invoke request.mcp.tools.call.v1is the headless node-host command for invoking a configured node-local MCP tool. It travels throughnode.invoke, demands the node to declare the command, and stays subject to pairing approval andgateway.nodes.commands.deny.node.eventroutes node-originated events back into the gateway.node.pluginTools.updateis the sole publication path for updating 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.historyreturns the most recent terminal approvals first, keeping 30 days of history for exec, plugin, and system-agent requests (scopeoperator.approvals). Cursor pagination is supported, along with an optional kind filter; pending approvals do not appear as history rows.approval.getandapproval.resolveserve as the kind-agnostic durable approval methods (scopeoperator.approvals).approval.getprovides a sanitized projection of either pending or retained terminal states, featuring a stableurlPath;approval.resolvetakes the canonical approval id, an explicitkind, and a decision, applies first-answer-wins resolution, and consistently returns the recorded canonical outcome.exec.approval.request,exec.approval.get,exec.approval.list, andexec.approval.resolvehandle one-shot exec approval requests and pending approval lookup/replay. These act as protocol-boundary adapters over the same durable approval registry.exec.approval.waitDecisionblocks on a single pending exec approval and yields the final decision (ornullif a timeout occurs).exec.approvals.getandexec.approvals.sethandle 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 approval flows defined by plugins.
Control UI commands
ui.commandenables anoperator.writecaller to transmit typed layout and navigation commands to connected Control UI clients that declare theui-commandscapability.- These commands include pane split/close/focus, sidebar visibility, terminal/browser panel visibility and docking, and session navigation.
- Protocol v1 deliberately broadcasts to every connected capable Control UI. When no such client is present, the request fails with
UNAVAILABLErather than falsely indicating a layout change.
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.runstays an enqueue-style RPC for manual runs. Clients requiring completion semantics should read the returnedrunIdand pollcron.runs.cron.runsaccepts an optional non-emptyrunIdfilter, letting clients track a single queued manual run without conflicting with 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, includingchat.injectand other transcript-only chat events. In protocol v4, delta payloads containdeltaText;messageremains the cumulative assistant snapshot. Non-prefix replacements setreplace=trueand employdeltaTextas the replacement text.session.message,session.operation,session.tool: transcript, in-flight session operation, and event-stream updates for a subscribed session.session.approval: sanitized pending and terminal approval truth for an explicitly opted-in exact-session subscriber. Child approvals use the persisted ancestor audience; events never mutate transcripts or wake agents.session.observer: safe live session headline and status digest. A model-authored preamble can update the headline immediately; utility-model assessments replace it later when available. Web, iOS, and Android use the same run-scoped digest. Clients show its headline or inspector link only while the digest's exactrunIdis present inactiveRunIds.sessions.changed: session index or metadata changed. Active-run fields use the same aggregate and complete-exact semantics assessions.list;activeRunIds: nullclears cached exact identities to unavailable, omission leaves the cache unchanged, and an array replaces it.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 approval lifecycle.device.pair.setup.completed: exact setup-code handoff completion, scoped tooperator.pairing.device.pair.setup.deliveryUncertain: replay-safe setup-code retirement whose credential response delivery could not be confirmed, scoped tooperator.pairing.voicewake.changed: wake-word trigger config changed.config.changed: a config write persisted (payload carries the config path, the new snapshot hash, and a timestamp, never config content). Operator-read scoped; clients refresh viaconfig.get.skills.changed: connectivity, the skill catalog, config, or eligibility changed after the gateway invalidated its skills snapshot. The payload'sreasoniswatch,watch-targets,manual,remote-node,config-change, orworkshop. Operator-read scoped; clients refresh viaskills.status.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, inbound-message, and terminal outbound-message 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.
- Parameters: either exact
agentId,sessionKey, orrunId; optionallykind("agent_run","tool_action", or"message"); optionallystatus("started","succeeded","failed","cancelled","timed_out","blocked", or"unknown"); optionally a messagedirection("inbound"or"outbound") and exactchannel; optionally inclusiveafter/beforeUnix-millisecond limits; optionallylimitranging from1to500; and optionally a stringcursortaken from the prior page. - Output:
{ "events": AuditActivityEventV1[], "nextCursor"?: string }.
The V1 result union, which is named, splits into separate schemas for agent-run, tool-action, inbound-message,
and outbound-message. The eventType discriminator maps to
agent_run, tool_action, inbound_message, or outbound_message in that order; kind and
message direction stay accessible for filtering and display purposes. Each event carries an
integer schemaVersion: 1. References to message identity adopt the exact
hmac-sha256:v1:<32 hex key id>:<64 hex digest> format; a channel-sender actor
id follows the same format.
Every variant needs eventType, schemaVersion, eventId, sequence,
sourceSequence, occurredAt, kind, action, status, actor, and
redaction. The variant fields are:
eventType | Mandatory fields | Non-mandatory 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 enums for closed messages are listed here:
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 lacks a errorCode; every non-success finished status must be paired with its corresponding run_* code. |
| Tool action | started and succeeded do not include a errorCode; all other finished statuses need their matching tool_* code. |
| Inbound message | succeeded maps to completed; blocked maps to skipped; failed maps to failed plus message_processing_failed. When reasonCode appears, it must be from that terminal family. |
| Outbound message | succeeded maps to sent; blocked maps to suppressed plus reasonCode; failed maps to failed plus errorCode and failureStage; unknown maps 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
need agent and run provenance, and session provenance is optional. Message
records can include agent and run ids, but they deliberately omit
sessionKey and sessionId; as a result, the sessionKey query filter only
affects run and tool rows. Tool events may include a tool call id and a tool name.
The activity ledger returns message.inbound.processed and
message.outbound.finished records, adding direction, channel, conversation kind,
normalized outcome, and optional delivery kind, failure stage, duration,
result count, reason code, and installation-local keyed
account, conversation, message, and target pseudonyms. These pseudonyms help
with correlation but do not provide anonymization: the state database holds their key,
whereas RPC and CLI exports do not. The ledger stores no prompts, message
bodies, tool arguments, tool results, command output, or raw error text.
Run and tool sessionKey values stay as raw correlation metadata and can embed
platform account or peer ids; message records leave out session keys.
For inbound rows, durationMs measures core dispatch through its terminal, and
resultCount tallies finalized queued tool, block, and reply payloads. For
outbound rows, durationMs covers delivery ownership from acknowledgement
through dead letter or reconciliation, including queued wait time, and resultCount
counts identified physical platform sends. When deliveryKind is present, it
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 and terminal outcomes. Outbound coverage writes
replay-safe queue and platform-start records to a lazy owner-native companion
and one terminal activity row per original logical reply payload that reaches
shared durable delivery; run inspection merges those sources. Chunking and
adapter fan-out are aggregated in terminal resultCount. Ambiguous sends reach
a terminal only after acknowledgement, dead
letter, or reconciliation. Plugin-local and direct-send paths that bypass those
shared boundaries are not yet covered. The bounded process-owned async queue is
best-effort and may drop records on saturation, terminal persistence failure,
or shutdown timeout, so this surface is not a lossless compliance archive.
Recording is enabled by default and managed via
logging.audit.enabled. Message
recording has its own control, logging.audit.messages, and defaults to
"off". When
recording is off, audit.activity.list continues serving records written
earlier until they expire.
audit.run.inspect also demands operator.read. Its closed request chooses
exactly one executionId for exact inspection or one runId for bounded
execution discovery. A single run match resolves directly; multiple matches yield
an explicit ambiguous result with at most 50 candidates and require exact
execution selection. Decision pages hold at most 100 receipts. Execution
identity collection is separately off by default and needs
logging.audit.executionIdentity: true plus an enabled audit ledger after
Gateway restart. Missing best-effort evidence never proves that a run did not
occur.
For a selected run, decision receipts combine terminal outbound activity with
owner-native queued and platform_started progress. Progress is
attribution-only, resides in the lazy companion store, and is not part of the
audit.activity.list result schema.
The shipped audit.list request, result, and AuditEvent schemas stay
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.
- For
tasks.list,operator.readis a prerequisite.- Params: an optional
status(which can be"queued","running","completed","failed","cancelled", or"timed_out", or an array holding those statuses), an optionalagentId, an optionalsessionKey, an optionallimitspanning1through500, and an optional stringcursor. - What comes back:
{ "tasks": TaskSummary[], "nextCursor"?: string }.
- Params: an optional
tasks.getdepends onoperator.read.- Params:
{ "taskId": string }. - What comes back:
{ "task": TaskSummary }. - When task ids are absent, the gateway responds with its not-found error shape.
- Params:
tasks.canceldepends onoperator.write.- Params:
{ "taskId": string, "reason"?: string }. - What comes back:
{ "found": boolean, "cancelled": boolean, "reason"?: string, "task"?: TaskSummary }. - Whether the ledger held a matching task is indicated by
found. Whether cancellation was accepted or recorded by the runtime is indicated bycancelled.
- Params:
Within TaskSummary you will find 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. The agent responsible for running
the task is named by agentId; requester and control context are carried by
sessionKey and ownerKey.
Operator helper methods
commands.list(operator.read) retrieves the agent's runtime command list.agentIdis not required; leave it out to access the default agent workspace.scopedetermines which surface the mainnameis aimed at:textgives the primary text command token without the/prefix;nativeand the standardbothpath return provider-aware native names when those are available.textAliasesholds exact slash aliases like/modeland/m.nativeNameholds the provider-aware native command name if one is present.provideris optional and impacts only native naming and native plugin command availability.includeArgs=falseremoves serialized argument metadata from the response.
tools.catalog(operator.read) retrieves the agent's runtime tool catalog. The response contains grouped tools and provenance details:source: eithercoreorpluginpluginId: plugin owner whensource="plugin"optional: indicates whether a plugin tool is optional
tools.effective(operator.read) retrieves the runtime-effective tool inventory for a session.sessionKeyis mandatory.- The gateway derives trusted runtime context from the session on the server side, rather than using caller-supplied auth or delivery context.
- The response is a session-scoped, server-derived view of the active inventory, covering core, plugin, channel, and already-discovered MCP server tools.
tools.effectiveis read-only for MCP: it can project a warm session MCP catalog through the final tool policy, but it does not create MCP runtimes, connect transports, or sendtools/list. If no matching warm catalog is available, the response may include 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 as/tools/invoke.nameis required.args,sessionKey,agentId,confirm, andidempotencyKeyare optional.- When both
sessionKeyandagentIdare provided, the resolved session agent must matchagentId. - Owner-only core wrappers like
cron,gateway, andnodesdemand 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 instead of skipping the gateway tool policy pipeline.
skills.status(operator.read) retrieves the visible skill inventory for an agent.agentIdis not required. Leave it out to access the default agent workspace.- Eligibility, missing requirements, config checks, and sanitized install options are all part of the response. Raw secret values are never exposed.
- ClawHub discovery metadata is returned by
skills.searchandskills.detail(operator.read). - Before installation, a private skill archive is staged through
skills.upload.begin,skills.upload.chunk, andskills.upload.commit(operator.admin). This admin-only upload path serves trusted clients and is separate from the standard ClawHub skill install process. Unlessskills.install.allowUploadedArchivesis turned on, it stays disabled by default.- An upload tied to that slug and force value is created by
skills.upload.begin({ kind: "skill-archive", slug, sizeBytes, sha256?, force?, idempotencyKey? }). - Bytes are appended by
skills.upload.chunk({ uploadId, offset, dataBase64 })at the precise decoded offset. - Final size and SHA-256 are checked by
skills.upload.commit({ uploadId, sha256? }). The upload is only finalized by commit; the skill is not installed at that point. - Uploaded skill archives are zips with a
SKILL.mdroot. The internal directory name inside the archive never decides the install target.
- An upload tied to that slug and force value is created by
- Three modes are available for
skills.install(operator.admin):- ClawHub mode: a skill folder gets installed into the
skills/directory of the default agent workspace by{ source: "clawhub", slug, version?, force? }. - Upload mode: a committed upload is installed into the
skills/<slug>directory of the default agent workspace by{ source: "upload", uploadId, slug, force?, sha256?, timeoutMs? }. The slug and force value must match those from the originalskills.upload.beginrequest. This is rejected unlessskills.install.allowUploadedArchivesis enabled; that setting has no effect on ClawHub installs. - Gateway installer mode: a declared
metadata.openclaw.installaction is executed on the gateway host by{ name, installId, timeoutMs? }. Older clients might still senddangerouslyForceUnsafeInstall; that field is deprecated, accepted only for protocol compatibility, and otherwise ignored. For operator-owned install decisions, usesecurity.installPolicy.
- ClawHub mode: a skill folder gets installed into the
- Two modes exist for
skills.update(operator.admin):- ClawHub mode refreshes one tracked slug or every tracked ClawHub install in the default agent workspace. Updates that would overwrite a skill directory whose installed files no longer match the recorded install digests are refused; the per-skill failure in
details.resultsincludescode: "force_required". To replace such a skill regardless, retry with the optionalforce: trueparameter. - Config mode patches
skills.entries.<skillKey>values likeenabled,apiKey, andenv.
- ClawHub mode refreshes one tracked slug or every tracked ClawHub install in the default agent workspace. Updates that would overwrite a skill directory whose installed files no longer match the recorded install digests are refused; the per-skill failure in
models.list views
An optional view parameter (src/agents/model-catalog-visibility.ts) is accepted by models.list:
- Omitted or
"default": whenagents.defaults.modelPolicy.allowis configured, the response is the allowed catalog, with dynamically discovered models included forprovider/*entries. Otherwise the full gateway catalog is returned. "configured": picker-sized behavior. Ifagents.defaults.modelPolicy.allowis configured, it takes precedence, including provider-scoped discovery forprovider/*entries. With no allowlist, explicitmodels.providers.<provider>.modelsentries are used in the response, and the full catalog is the fallback only when no configured model rows exist."provider-config": source-authoredmodels.providers.*.modelsinventory, unaffected by picker allowlists. Public model capabilities and route-aware availability appear in the rows, but provider endpoints, auth material, and runtime request configuration are left out."all": the full gateway catalog, withagents.defaults.modelPolicy.allowbypassed. This is meant for diagnostics or discovery UIs, not regular model pickers.
Two optional controls tell automatic reads apart from operator-requested discovery:
- For that runtime generation,
preparedOnly: truereuses the current prepared catalog or a completed catalog without starting provider discovery. Control UI startup and polling rely on this mode. - When the selected view calls for discovery,
refresh: trueswaps in a completed full catalog. Concurrent refreshes share a single build; a failed refresh keeps the previous completed catalog available and sends the failure back to the caller.
Since one forbids discovery while the other asks for it, preparedOnly: true and refresh: true cannot be used together.
Exec approvals
- When approval is required for an exec request, the gateway emits
exec.approval.requested. - Operator clients resolve it through
exec.approval.resolve, which needsoperator.approvals. - For
host=node,exec.approval.requesthas to carrysystemRunPlan(the canonicalargv/cwd/rawCommand/session metadata). Any request lackingsystemRunPlangets turned down. - Once approved, forwarded
node.invoke system.runcalls reuse that same canonicalsystemRunPlanas the authoritative command/cwd/session context. - Should a caller alter
command,rawCommand,cwd,agentId, orsessionKeybetween prepare and the final approvedsystem.runforward, the gateway rejects the run rather than trusting the modified payload.
Agent delivery fallback
agentrequests may carrydeliver=trueto ask for outbound delivery.bestEffortDeliver=false(the default) enforces strict behavior: unresolved or internal-only delivery targets yieldINVALID_REQUEST.bestEffortDeliver=truepermits fallback to session-only execution when no external deliverable route can be found (for instance internal/webchat sessions or ambiguous multi-channel configs).- Final
agentresults may containresult.deliveryStatuswhen delivery was requested, employing the samesent,suppressed,partial_failed, andfailedstatuses described foropenclaw agent --json --deliver.
Versioning
PROTOCOL_VERSION,MIN_CLIENT_PROTOCOL_VERSION,MIN_NODE_PROTOCOL_VERSION, andMIN_PROBE_PROTOCOL_VERSIONreside inpackages/gateway-protocol/src/version.ts.- Clients transmit
minProtocol+maxProtocol. Operator and UI clients must include the current protocol in that range; current clients and servers run protocol v4. - Authenticated clients holding both
role: "node"andclient.mode: "node"can 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 unaffected by this compatibility window. Plugin-owned node capabilities and commands stay withheld until the node upgrades to the current protocol because their hosted surfaces fall outside the N-1 contract. - Schemas and models derive from TypeBox definitions:
pnpm protocol:genpnpm protocol:gen:swiftpnpm protocol:check
Client constants
The reference client implementation sits in packages/gateway-client/src/
(OpenClaw wraps it via the slim src/gateway/client.ts facade). These
defaults remain stable across protocol v4 and serve as 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 |
| Chat attachment ceiling | agents.defaults.mediaMaxMb, default 20 MB decoded | src/gateway/chat-attachment-policy.ts |
| Chat attachment image ceiling | min(attachment ceiling, 6 MB) | src/gateway/chat-attachment-policy.ts, packages/media-core/src/constants.ts |
The effective policy.tickIntervalMs, policy.maxPayload, policy.maxBufferedBytes, and policy.attachments are announced by the server through hello-ok; clients should rely on those advertised values instead of the pre-handshake defaults or any hardcoded attachment limits.
When every pending request carries its own deadline, the reference client lets those finite requests govern their configured timeout. The tick watchdog stays engaged if an expectFinal request lacks a finite timeoutMs, if any request sets timeoutMs: null, or when finite and unbounded requests are mixed together. Should no inbound events or responses arrive before the tick-timeout threshold, the client terminates the socket with code 4000, fails all pending requests, and re-establishes the connection. Rejected requests are never retried after the reconnect.
Auth
- Depending on the configured
gateway.auth.mode("none" | "token" | "password" | "trusted-proxy"), shared-secret gateway authentication relies on eitherconnect.params.auth.tokenorconnect.params.auth.password. - Connect auth checks are satisfied by identity-bearing modes like Tailscale Serve (
gateway.auth.allowTailscale: true) or non-loopbackgateway.auth.mode: "trusted-proxy", which pull from request headers rather thanconnect.params.auth.*. - With private-ingress
gateway.auth.mode: "none", shared-secret connect auth is bypassed entirely; that mode should never be exposed on public or untrusted ingress. - Once pairing completes, the gateway hands out a device token tied to the connection role and approved grant, delivered in
hello-ok.auth.deviceToken. After a successful connect, clients should save it viahello-ok.auth.scopeswhenever the token is fresh or differs from what's stored. - The live authority for the current socket is
hello-ok.auth.scopes, and it aligns with the scopes enforced by RPC dispatch. - When
hello-ok.auth.deviceTokenprecisely matches the token already recorded for the same gateway, device, client, and role, keep that record's stored scopes rather than swapping in a narrower live scope set. A token that's newly issued or rotated useshello-ok.auth.scopes; its approved grant matches that connection at issuance time. - Reconnecting with that stored device token should likewise reuse the stored approved scope set for it. This keeps read/probe/status access intact and prevents reconnects from silently dropping to a narrower implicit admin-only scope.
- Client-side connect auth assembly (
selectConnectAuthinpackages/gateway-client/src/client.ts):auth.passwordoperates independently and is always forwarded when present.auth.tokengets filled by priority: an explicit shared token comes first, then an explicitdeviceToken, then a stored per-device token (keyed bydeviceId+role).auth.bootstrapTokenis only transmitted when none of the above resolved toauth.token. A shared token or any resolved device token keeps it from being sent.- Auto-promotion of a stored device token on the one-shot
AUTH_TOKEN_MISMATCHretry is limited to trusted endpoints: loopback, orwss://with a pinnedtlsFingerprint. Publicwss://without pinning doesn't qualify.
- The built-in setup-code bootstrap hands back the primary node
hello-ok.auth.deviceTokenplus a bounded operator token inhello-ok.auth.deviceTokensfor trusted mobile handoff. That operator token carriesoperator.talk.secretsfor native Talk configuration reads, but leaves out pairing-mutation scopes andoperator.admin. hello-ok.auth.deviceTokensholds only extra bootstrap-handoff tokens. Don't treat it as metadata for the primarydeviceTokenreconnect record.- While a non-baseline setup-code bootstrap awaits approval,
PAIRING_REQUIREDdetails includerecommendedNextStep: "wait_then_retry",retryable: true, andpauseReconnect: false. Keep reconnecting with the same bootstrap token until the request gets approved or the token goes invalid. - Persist
hello-ok.auth.deviceTokensonly when the connect used bootstrap auth over a trusted transport likewss://or loopback/local pairing. - If a client passes an explicit
deviceTokenor explicitscopes, that caller-requested scope set stays authoritative for the live connection and shows up inhello-ok.auth.scopes; cached token-grant scopes are reused only when the client is reusing the stored per-device token. - Rotation or revocation of device tokens happens through
device.token.rotateanddevice.token.revoke(which requiresoperator.pairing). Rotating or revoking a node or other non-operator role also demandsoperator.admin. - Rotation metadata comes back from
device.token.rotate. It echoes the replacement bearer token only for same-device calls already authenticated with that device token, letting token-only clients persist their replacement before reconnecting. Shared/admin rotations don't echo the bearer token. - Token issuance, rotation, and revocation stay confined to the approved role set in that device's pairing entry; token mutation can't 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 holds
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 verify the target operator token scope set against the caller's current session scopes. Non-admin callers can't rotate or revoke a broader operator token than they already possess.- 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, halt automatic reconnect loops and present operator action guidance.
AUTH_SCOPE_MISMATCHindicates the device token was recognized but doesn't cover the requested role/scopes. Don't frame this as a bad token; guide the operator to re-pair or approve the narrower/broader scope contract.
Device identity and pairing
- Every node should present a stable device identifier (
device.id) generated from a keypair fingerprint. - Per-device and per-role tokens are issued by gateways.
- New device IDs demand pairing approval unless local auto-approval is turned on.
- Pairing auto-approval applies only to direct local loopback connections.
- OpenClaw additionally provides a restricted backend/container-local self-connect route for trusted shared-secret helper flows.
- LAN or same-host tailnet connections are still classified as remote for pairing purposes and still require approval.
- WS clients typically send
deviceidentity duringconnect(operator plus node). The sole device-less operator cases are explicit trust paths:- a successful
gateway.auth.mode: "trusted-proxy"operator Control UI authentication. - direct-loopback
gateway-clientbackend RPCs on the reserved internal helper path.
- a successful
- Leaving out device identity carries scope implications. When a device-less operator connection is admitted via an explicit trust path, OpenClaw still resets self-declared scopes to an empty set unless that path carries a named scope-preservation exception. Methods gated by scope then fail with
missing scope. - The reserved direct-loopback
gateway-clientbackend helper path retains scopes only for internal local control-plane RPCs; custom backend IDs get no such exception. - Every connection must sign the server-supplied
connect.challengenonce.
Device auth migration diagnostics
For older clients relying on pre-challenge signing behavior, connect produces DEVICE_AUTH_* detail codes under error.details.code with a consistent error.details.reason.
Typical migration failures:
| Message | details.code | details.reason | Meaning |
|---|---|---|---|
device nonce required | DEVICE_AUTH_NONCE_REQUIRED | device-nonce-missing | Client left out device.nonce (or sent an empty value). |
device nonce mismatch | DEVICE_AUTH_NONCE_MISMATCH | device-nonce-mismatch | Client signed using an outdated or incorrect nonce. |
device signature invalid | DEVICE_AUTH_SIGNATURE_INVALID | device-signature | Signature payload fails to match the v2 payload. |
device signature expired | DEVICE_AUTH_SIGNATURE_EXPIRED | device-signature-stale | Signed timestamp falls outside the permitted skew. |
device identity mismatch | DEVICE_AUTH_DEVICE_ID_MISMATCH | device-id-mismatch | device.id does not correspond to the public key fingerprint. |
device public key invalid | DEVICE_AUTH_PUBLIC_KEY_INVALID | device-public-key | Public key format or canonicalization could not be processed. |
Migration target:
- Always wait for
connect.challenge. - Employ
connect.challenge.payload.tsasconnect.params.device.signedAt. - Sign the v2 payload that incorporates the server nonce.
- Echo the same nonce in
connect.params.device.nonce. - The preferred signature payload is
v3(buildDeviceAuthPayloadV3inpackages/gateway-client/src/device-auth.ts), which also bindsplatformanddeviceFamilyalongside device/client/role/scopes/token/nonce fields. - Legacy
v2signatures remain accepted for compatibility, yet paired-device metadata pinning still governs command policy upon reconnect.
TLS and pinning
- TLS works for WS connections (
gateway.tlsconfig). - Clients can optionally pin the gateway cert fingerprint through
gateway.remote.tlsFingerprintor CLI--tls-fingerprint.
Scope
This protocol exposes the complete 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.