34 KiB
| summary | read_when | title | |||
|---|---|---|---|---|---|
| Gateway WebSocket protocol: handshake, frames, versioning |
|
Gateway protocol |
The Gateway WS protocol is the single control plane + node transport for OpenClaw. All clients (CLI, web UI, macOS app, iOS/Android nodes, headless nodes) connect over WebSocket and declare their role + scope at handshake time.
Transport
- WebSocket, text frames with JSON payloads.
- First frame must be a
connectrequest. - Pre-connect frames are capped at 64 KiB. After a successful handshake, clients
should follow the
hello-ok.policy.maxPayloadandhello-ok.policy.maxBufferedByteslimits. With diagnostics enabled, oversized inbound frames and slow outbound buffers emitpayload.largeevents before the gateway closes or drops the affected frame. These events keep sizes, limits, surfaces, and safe reason codes. They do not keep the message body, attachment contents, raw frame body, tokens, cookies, or secret values.
Handshake (connect)
Gateway → Client (pre-connect challenge):
{
"type": "event",
"event": "connect.challenge",
"payload": { "nonce": "…", "ts": 1737264000000 }
}
Client → Gateway:
{
"type": "req",
"id": "…",
"method": "connect",
"params": {
"minProtocol": 3,
"maxProtocol": 3,
"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 → Client:
{
"type": "res",
"id": "…",
"ok": true,
"payload": {
"type": "hello-ok",
"protocol": 3,
"server": { "version": "…", "connId": "…" },
"features": { "methods": ["…"], "events": ["…"] },
"snapshot": { "…": "…" },
"auth": {
"role": "operator",
"scopes": ["operator.read", "operator.write"]
},
"policy": {
"maxPayload": 26214400,
"maxBufferedBytes": 52428800,
"tickIntervalMs": 15000
}
}
}
server, features, snapshot, and policy are all required by the schema
(src/gateway/protocol/schema/frames.ts). auth is also required and reports
the negotiated role/scopes. canvasHostUrl is optional.
When no device token is issued, hello-ok.auth reports the negotiated
permissions without token fields:
{
"auth": {
"role": "operator",
"scopes": ["operator.read", "operator.write"]
}
}
Trusted same-process backend clients (client.id: "gateway-client",
client.mode: "backend") may omit device on direct loopback connections when
they authenticate with the shared gateway token/password. This path is reserved
for internal control-plane RPCs and keeps stale CLI/device pairing baselines from
blocking local backend work such as subagent session updates. Remote clients,
browser-origin clients, node clients, and explicit device-token/device-identity
clients still use the normal pairing and scope-upgrade checks.
When a device token is issued, hello-ok also includes:
{
"auth": {
"deviceToken": "…",
"role": "operator",
"scopes": ["operator.read", "operator.write"]
}
}
During trusted bootstrap handoff, hello-ok.auth may also include additional
bounded role entries in deviceTokens:
{
"auth": {
"deviceToken": "…",
"role": "node",
"scopes": [],
"deviceTokens": [
{
"deviceToken": "…",
"role": "operator",
"scopes": ["operator.approvals", "operator.read", "operator.talk.secrets", "operator.write"]
}
]
}
}
For the built-in node/operator bootstrap flow, the primary node token stays
scopes: [] and any handed-off operator token stays bounded to the bootstrap
operator allowlist (operator.approvals, operator.read,
operator.talk.secrets, operator.write). Bootstrap scope checks stay
role-prefixed: operator entries only satisfy operator requests, and non-operator
roles still need scopes under their own role prefix.
Node example
{
"type": "req",
"id": "…",
"method": "connect",
"params": {
"minProtocol": 3,
"maxProtocol": 3,
"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": "…"
}
}
}
Framing
- Request:
{type:"req", id, method, params} - Response:
{type:"res", id, ok, payload|error} - Event:
{type:"event", event, payload, seq?, stateVersion?}
Side-effecting methods require idempotency keys (see schema).
Roles + scopes
Roles
operator= control plane client (CLI/UI/automation).node= capability host (camera/screen/canvas/system.run).
Scopes (operator)
Common scopes:
operator.readoperator.writeoperator.adminoperator.approvalsoperator.pairingoperator.talk.secrets
talk.config with includeSecrets: true requires operator.talk.secrets
(or operator.admin).
Plugin-registered gateway RPC methods may request their own operator scope, but
reserved core admin prefixes (config.*, exec.approvals.*, wizard.*,
update.*) always resolve to operator.admin.
Method scope is only the first gate. Some slash commands reached through
chat.send apply stricter command-level checks on top. For example, persistent
/config set and /config unset writes require operator.admin.
node.pair.approve also has an extra approval-time scope check on top of the
base method scope:
- commandless requests:
operator.pairing - requests with non-exec node commands:
operator.pairing+operator.write - requests that include
system.run,system.run.prepare, orsystem.which:operator.pairing+operator.admin
Caps/commands/permissions (node)
Nodes declare capability claims at connect time:
caps: high-level capability categories.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.
Presence
system-presencereturns entries keyed by device identity.- Presence entries include
deviceId,roles, andscopesso UIs can show a single row per device even when it connects as both operator and node.
Broadcast event scoping
Server-pushed WebSocket broadcast events are scope-gated so that pairing-scoped or node-only sessions do not passively receive session content.
- Chat, agent, and tool-result frames (including streamed
agentevents and tool call results) require at leastoperator.read. Sessions withoutoperator.readskip these frames entirely. - Plugin-defined
plugin.*broadcasts are gated tooperator.writeoroperator.admin, depending on how the plugin registered them. - Status and transport events (
heartbeat,presence,tick, connect/disconnect lifecycle, etc.) remain unrestricted so transport health stays observable to every authenticated session. - Unknown broadcast event families are scope-gated by default (fail-closed) unless a registered handler explicitly relaxes them.
Each client connection keeps its own per-client sequence number so broadcasts preserve monotonic ordering on that socket even when different clients see different scope-filtered subsets of the event stream.
Common RPC method families
The public WS surface is broader than the handshake/auth examples above. This
is not a generated dump — hello-ok.features.methods is a conservative
discovery list built from src/gateway/server-methods-list.ts plus loaded
plugin/channel method exports. Treat it as feature discovery, not a full
enumeration of src/gateway/server-methods/*.ts.
Common event families
chat: UI chat updates such aschat.injectand other transcript-only chat events.session.messageandsession.tool: transcript/event-stream updates for a subscribed session.sessions.changed: session index or metadata changed.presence: system presence snapshot updates.tick: periodic keepalive / liveness event.health: gateway health snapshot update.heartbeat: heartbeat event stream update.cron: cron run/job change event.shutdown: gateway shutdown notification.node.pair.requested/node.pair.resolved: node pairing lifecycle.node.invoke.request: node invoke request broadcast.device.pair.requested/device.pair.resolved: paired-device lifecycle.voicewake.changed: wake-word trigger config changed.exec.approval.requested/exec.approval.resolved: exec approval lifecycle.plugin.approval.requested/plugin.approval.resolved: plugin approval lifecycle.
Node helper methods
- Nodes may call
skills.binsto fetch the current list of skill executables for auto-allow checks.
Operator helper methods
- Operators may call
commands.list(operator.read) to fetch the runtime command inventory for an agent.agentIdis optional; omit it to read the default agent workspace.scopecontrols which surface the primarynametargets:textreturns the primary text command token without the leading/nativeand the defaultbothpath return provider-aware native names when available
textAliasescarries exact slash aliases such as/modeland/m.nativeNamecarries the provider-aware native command name when one exists.provideris optional and only affects native naming plus native plugin command availability.includeArgs=falseomits serialized argument metadata from the response.
- Operators may call
tools.catalog(operator.read) to fetch the runtime tool catalog for an agent. The response includes grouped tools and provenance metadata:source:coreorpluginpluginId: plugin owner whensource="plugin"optional: whether a plugin tool is optional
- Operators may call
tools.effective(operator.read) to fetch the runtime-effective tool inventory for a session.sessionKeyis required.- The gateway derives trusted runtime context from the session server-side instead of accepting caller-supplied auth or delivery context.
- The response is session-scoped and reflects what the active conversation can use right now, including core, plugin, and channel tools.
- Operators may call
skills.status(operator.read) to fetch the visible skill inventory for an agent.agentIdis optional; omit it to read the default agent workspace.- The response includes eligibility, missing requirements, config checks, and sanitized install options without exposing raw secret values.
- Operators may call
skills.searchandskills.detail(operator.read) for ClawHub discovery metadata. - Operators may call
skills.install(operator.admin) in two modes:- ClawHub mode:
{ source: "clawhub", slug, version?, force? }installs a skill folder into the default agent workspaceskills/directory. - Gateway installer mode:
{ name, installId, dangerouslyForceUnsafeInstall?, timeoutMs? }runs a declaredmetadata.openclaw.installaction on the gateway host.
- ClawHub mode:
- Operators may call
skills.update(operator.admin) in two modes:- ClawHub mode updates one tracked slug or all tracked ClawHub installs in the default agent workspace.
- Config mode patches
skills.entries.<skillKey>values such asenabled,apiKey, andenv.
models.list views
models.list accepts an optional view parameter:
- Omitted or
"default": current runtime behavior. Ifagents.defaults.modelsis configured, the response is the allowed catalog; otherwise the response is the full Gateway catalog. "configured": picker-sized behavior. Ifagents.defaults.modelsis configured, it still wins. Otherwise the response uses explicitmodels.providers.*.modelsentries, falling back to the full catalog only when no configured model rows exist."all": full Gateway catalog, bypassingagents.defaults.models. Use this for diagnostics and discovery UIs, not normal model pickers.
Exec approvals
- When an exec request needs approval, the gateway broadcasts
exec.approval.requested. - Operator clients resolve by calling
exec.approval.resolve(requiresoperator.approvalsscope). - For
host=node,exec.approval.requestmust includesystemRunPlan(canonicalargv/cwd/rawCommand/session metadata). Requests missingsystemRunPlanare rejected. - After approval, forwarded
node.invoke system.runcalls reuse that canonicalsystemRunPlanas the authoritative command/cwd/session context. - If a caller mutates
command,rawCommand,cwd,agentId, orsessionKeybetween prepare and the final approvedsystem.runforward, the gateway rejects the run instead of trusting the mutated payload.
Agent delivery fallback
agentrequests can includedeliver=trueto request outbound delivery.bestEffortDeliver=falsekeeps strict behavior: unresolved or internal-only delivery targets returnINVALID_REQUEST.bestEffortDeliver=trueallows fallback to session-only execution when no external deliverable route can be resolved (for example internal/webchat sessions or ambiguous multi-channel configs).
Versioning
PROTOCOL_VERSIONlives insrc/gateway/protocol/schema/protocol-schemas.ts.- Clients send
minProtocol+maxProtocol; the server rejects mismatches. - Schemas + models are generated from TypeBox definitions:
pnpm protocol:genpnpm protocol:gen:swiftpnpm protocol:check
Client constants
The reference client in src/gateway/client.ts uses these defaults. Values are
stable across protocol v3 and are the expected baseline for third-party clients.
| Constant | Default | Source |
|---|---|---|
PROTOCOL_VERSION |
3 |
src/gateway/protocol/schema/protocol-schemas.ts |
| Request timeout (per RPC) | 30_000 ms |
src/gateway/client.ts (requestTimeoutMs) |
| Preauth / connect-challenge timeout | 10_000 ms |
src/gateway/handshake-timeouts.ts (clamp 250–10_000) |
| Initial reconnect backoff | 1_000 ms |
src/gateway/client.ts (backoffMs) |
| Max reconnect backoff | 30_000 ms |
src/gateway/client.ts (scheduleReconnect) |
| Fast-retry clamp after device-token close | 250 ms |
src/gateway/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 |
src/gateway/client.ts |
| Tick-timeout close | code 4000 when silence exceeds tickIntervalMs * 2 |
src/gateway/client.ts |
MAX_PAYLOAD_BYTES |
25 * 1024 * 1024 (25 MB) |
src/gateway/server-constants.ts |
The server advertises the effective policy.tickIntervalMs, policy.maxPayload,
and policy.maxBufferedBytes in hello-ok; clients should honor those values
rather than the pre-handshake defaults.
Auth
- Shared-secret gateway auth uses
connect.params.auth.tokenorconnect.params.auth.password, depending on the configured auth mode. - Identity-bearing modes such as Tailscale Serve
(
gateway.auth.allowTailscale: true) or non-loopbackgateway.auth.mode: "trusted-proxy"satisfy the connect auth check from request headers instead ofconnect.params.auth.*. - Private-ingress
gateway.auth.mode: "none"skips shared-secret connect auth entirely; do not expose that mode on public/untrusted ingress. - After pairing, the Gateway issues a device token scoped to the connection
role + scopes. It is returned in
hello-ok.auth.deviceTokenand should be persisted by the client for future connects. - Clients should persist the primary
hello-ok.auth.deviceTokenafter any successful connect. - Reconnecting with that stored device token should also reuse the stored approved scope set for that token. This preserves read/probe/status access that was already granted and avoids silently collapsing reconnects to a narrower implicit admin-only scope.
- Client-side connect auth assembly (
selectConnectAuthinsrc/gateway/client.ts):auth.passwordis orthogonal and is always forwarded when set.auth.tokenis populated in priority order: explicit shared token first, then an explicitdeviceToken, then a stored per-device token (keyed bydeviceId+role).auth.bootstrapTokenis sent only when none of the above resolved anauth.token. A shared token or any resolved device token suppresses it.- Auto-promotion of a stored device token on the one-shot
AUTH_TOKEN_MISMATCHretry is gated to trusted endpoints only — loopback, orwss://with a pinnedtlsFingerprint. Publicwss://without pinning does not qualify.
- Additional
hello-ok.auth.deviceTokensentries are bootstrap handoff tokens. Persist them only when the connect used bootstrap auth on a trusted transport such aswss://or loopback/local pairing. - If a client supplies an explicit
deviceTokenor explicitscopes, that caller-requested scope set remains authoritative; cached scopes are only reused when the client is reusing the stored per-device token. - Device tokens can be rotated/revoked via
device.token.rotateanddevice.token.revoke(requiresoperator.pairingscope). device.token.rotatereturns rotation metadata. It echoes the replacement bearer token only for same-device calls that are already authenticated with that device token, so token-only clients can persist their replacement before reconnecting. Shared/admin rotations do not echo the bearer token.- Token issuance, rotation, and revocation stay bounded to the approved role set recorded in that device's pairing entry; token mutation cannot expand or target a device role that pairing approval never granted.
- For paired-device token sessions, device management is self-scoped unless the
caller also has
operator.admin: non-admin callers can remove/revoke/rotate only their own device entry. device.token.rotateanddevice.token.revokealso check the target operator token scope set against the caller's current session scopes. Non-admin callers cannot rotate or revoke a broader operator token than they already hold.- Auth failures include
error.details.codeplus recovery hints:error.details.canRetryWithDeviceToken(boolean)error.details.recommendedNextStep(retry_with_device_token,update_auth_configuration,update_auth_credentials,wait_then_retry,review_auth_configuration)
- Client behavior for
AUTH_TOKEN_MISMATCH:- Trusted clients may attempt one bounded retry with a cached per-device token.
- If that retry fails, clients should stop automatic reconnect loops and surface operator action guidance.
Device identity + pairing
- Nodes should include a stable device identity (
device.id) derived from a keypair fingerprint. - Gateways issue tokens per device + role.
- Pairing approvals are required for new device IDs unless local auto-approval is enabled.
- Pairing auto-approval is centered on direct local loopback connects.
- OpenClaw also has a narrow backend/container-local self-connect path for trusted shared-secret helper flows.
- Same-host tailnet or LAN connects are still treated as remote for pairing and require approval.
- WS clients normally include
deviceidentity duringconnect(operator + node). The only device-less operator exceptions are explicit trust paths:gateway.controlUi.allowInsecureAuth=truefor localhost-only insecure HTTP compatibility.- successful
gateway.auth.mode: "trusted-proxy"operator Control UI auth. gateway.controlUi.dangerouslyDisableDeviceAuth=true(break-glass, severe security downgrade).- direct-loopback
gateway-clientbackend RPCs authenticated with the shared gateway token/password.
- All connections must sign the server-provided
connect.challengenonce.
Device auth migration diagnostics
For legacy clients that still use pre-challenge signing behavior, connect now returns
DEVICE_AUTH_* detail codes under error.details.code with a stable error.details.reason.
Common migration failures:
| Message | details.code | details.reason | Meaning |
|---|---|---|---|
device nonce required |
DEVICE_AUTH_NONCE_REQUIRED |
device-nonce-missing |
Client omitted device.nonce (or sent blank). |
device nonce mismatch |
DEVICE_AUTH_NONCE_MISMATCH |
device-nonce-mismatch |
Client signed with a stale/wrong nonce. |
device signature invalid |
DEVICE_AUTH_SIGNATURE_INVALID |
device-signature |
Signature payload does not match v2 payload. |
device signature expired |
DEVICE_AUTH_SIGNATURE_EXPIRED |
device-signature-stale |
Signed timestamp is outside allowed skew. |
device identity mismatch |
DEVICE_AUTH_DEVICE_ID_MISMATCH |
device-id-mismatch |
device.id does not match public key fingerprint. |
device public key invalid |
DEVICE_AUTH_PUBLIC_KEY_INVALID |
device-public-key |
Public key format/canonicalization failed. |
Migration target:
- Always wait for
connect.challenge. - Sign the v2 payload that includes the server nonce.
- Send the same nonce in
connect.params.device.nonce. - Preferred signature payload is
v3, which bindsplatformanddeviceFamilyin addition to device/client/role/scopes/token/nonce fields. - Legacy
v2signatures remain accepted for compatibility, but paired-device metadata pinning still controls command policy on reconnect.
TLS + pinning
- TLS is supported for WS connections.
- Clients may optionally pin the gateway cert fingerprint (see
gateway.tlsconfig plusgateway.remote.tlsFingerprintor CLI--tls-fingerprint).
Scope
This protocol exposes the full gateway API (status, channels, models, chat,
agent, sessions, nodes, approvals, etc.). The exact surface is defined by the
TypeBox schemas in src/gateway/protocol/schema.ts.