On this page
On this page
Gateway
Gateway protocol handshake
Connect a client, read the Gateway response, and negotiate capabilities. For authentication setup, see Gateway authentication; for permissions, see Operator scopes.
Handshake
Gateway sends a pre-connect challenge:
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:
Clients may send small requests immediately after connect when their parameters
do not depend on hello-ok. The Gateway queues up to 15 following frames, each at
most 64 KiB, until hello delivery completes. It then dispatches them in receipt
order under the connection's current authorization; responses may complete out of
order. Authentication rejection closes the connection and discards queued frames.
Clients must retire pending requests with that connection and wait for an accepted
hello before enabling authenticated UI actions.
This queue is existing protocol behavior. Using it changes neither the wire schema nor protocol version 4; clients that wait for hello continue to work. Protocol changes remain additive first, with an explicit version decision required for an incompatible change.
Hello response
The default connect deadline is 30 seconds, allowing authentication and profile
preparation to finish on busy or slower hosts. It is a total deadline, not an idle
timeout: progress does not extend it. The client connect watchdog and server
preauth timeout share this default. Operators can override the server budget with
OPENCLAW_HANDSHAKE_TIMEOUT_MS; configure connecting clients with the same budget.
When the Gateway already holds a named canonical owner and its local-owner
binding, initial connections and reconnects reuse those profile facts. Committed
profile writes update the retained catalog; creating an owner, adopting a missing
name, or restoring an absent binding still uses the profile writer. Merged or
misbound owners require openclaw doctor --fix before reconnecting. Every
connection retains its authentication, device authorization, role, and visibility
checks.
server, features, snapshot, policy, and auth are all required by
HelloOkSchema (packages/gateway-protocol/src/schema/frames.ts). auth
reports the negotiated role and the current socket's effective authorization
scopes even when no device token is issued (shape above). deviceToken, when
present, is the primary reusable credential for the same device and role.
Optional hello fields:
| Field | Client use |
|---|---|
server.commit, server.builtAt |
Running Gateway build provenance, captured at startup and omitted when unavailable. Visible to authenticated members; the Control UI shows it in Settings → About, separately from frontend details. Diagnostic/update RPC scopes are unchanged. |
controlUiUrl |
Credential-free public Control UI origin/base path for shareable links, independent of tunnels or dev servers. Omitted when gateway.publicOrigin is unset or the UI is disabled; grants no access. |
snapshot.appliedConfigHash |
Active runtime's accepted config revision. Compare with config.get.configRevisionHash to detect unapplied saved changes. config.get.hash instead guards authored root/include-file writes. |
snapshot.controlUiIdentityUrl |
HTTPS dashboard sign-in URL for trusted-proxy/Tailscale Serve identity. Use normal HTTPS trust, not native TLS pins; never send native connection tokens/passwords. Refresh on reconnect and discard on disconnect. Serve-route removal/replacement closes affected connections with 1012. |
pluginSurfaceUrls |
Map of surface names to scoped, potentially expiring URLs. Renew with { surface: "canvas" } through node.pluginSurface.refresh (nodes) or plugin.surface.refresh (Control UI with operator.read, including write/admin). Stop automatic renewal on FORBIDDEN until reconnect. |
Clients with operator.admin also receive the running Gateway's authMode,
bindHost, and tailscaleMode. These include startup flags such as --bind
and --tailscale, which the config file can't show. Older Gateways omit
bindHost and tailscaleMode, so clients must not infer exposure from their
absence.
Attachment limits
policy.attachments is optional (older gateways omit it) and advertises
the decoded-size ceilings chat attachments face on chat.send, sessions.send,
and session-creation initial turns:
| 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) |
Validating before send:
- Check each file's decoded size against
maxImageBytesfor images andmaxBytesfor everything else. - Serialize the whole request and check its encoded size against
policy.maxPayload.policy.attachmentsis a per-attachment ceiling, never a promise the frame fits: attachments travel as base64, so a 20 MB file is about 26.7 MB on the wire and exceeds the default 25 MiB frame limit on its own. - Treat the server as authoritative for everything else. Accepted MIME types and per-message handling are deliberately not advertised because they depend on the entrypoint, the resolved model, and payload sniffing. The gateway can return a typed rejection, while text-only model runs can omit additional images after their offload cap and still complete the request.
- Re-read the values on every reconnect. They are a connection-time snapshot, so
a live
mediaMaxMbedit reaches existing connections only after they reconnect.
Connection lifecycle and credentials
openclaw.setup.verify checks active config/restart state before and after live
inference. It returns { ok: false, status: "unavailable", error } while saved
settings are unapplied, restart work remains, or the verified runtime changes.
Preserve the selected model and retry after application/restart. Standalone CLI
verification tests saved configuration without requiring a running Gateway.
Retired APIs: use plugin surfaces instead of canvasHostUrl, canvasCapability,
or node.canvas.capability.refresh; use sessions.companion.ask instead of
sessions.observer.ask.
For a complete client flow after a timeout or disconnect, see config-write reconciliation.
While the Gateway is preparing authentication and request state, connect can return a
retryable UNAVAILABLE error with details.reason: "startup-sidecars" and
retryAfterMs. Retry within your connection budget instead of treating it as
a terminal handshake failure. Once that state is ready, authenticated clients can
connect and read available sessions while background recovery continues. Writes
retain their startup and per-agent recovery gates; node enrollment retains its
startup-specific admission checks.
When a device token is issued, hello-ok.auth adds it:
Built-in QR/setup-code bootstrap is a mobile handoff path. A successful baseline setup-code connect returns a primary node token plus one bounded operator token:
This operator handoff is bounded on purpose: enough to start the mobile
operator loop and native setup, with operator.write satisfying Talk sessions
and operator.talk.secrets covering Talk config reads, but no pairing-mutation scopes
and no operator.admin. Broader
pairing/admin access needs a separate approved pairing or token flow. Persist
hello-ok.auth.deviceTokens only when bootstrap auth ran over a trusted
transport (wss:// or loopback/local pairing).
Trusted local backend clients (client.id: "gateway-client",
client.mode: "backend") may omit device on direct loopback connections when
authenticating with the shared gateway token/password. This path is reserved
for internal control-plane RPCs (e.g. subagent session updates) and avoids
stale CLI/device pairing baselines blocking local backend work. The exception
also applies when that backend supplies a signed device identity: it does not
create a pairing record, so an unpaired identity receives no device token.
Remote, browser-origin, node, and non-backend clients follow their normal pairing
and scope-upgrade policies. Device-token authentication still validates the
existing token's role and scopes before any local-backend pairing exception.
Worker role and closed protocol
A Gateway-owned launcher starts the worker process through
openclaw worker; it is not a manual registration command.
Workers use a closed protocol through either the public
/__openclaw__/worker WebSocket path on the main TLS endpoint or the dedicated
loopback ingress reached through the gateway-owned, host-key-pinned SSH tunnel.
The route selects worker mode before reading frames, so it never dispatches
general auth, node events, operator RPCs, or plugin methods. Public admission
shares the main per-client pre-auth budget and authentication rate limiter; its
wire errors collapse credential and environment details to
admission-rejected, while trusted gateway diagnostics retain the internal
reason. A strict connect verifies a hash-at-rest, short-lived credential bound
to the environment, bundle hash, owner epoch, RPC-set version, expiry, and one
nullable session; it separately checks the current version and feature set.
Success returns minimal worker-hello-ok; feature negotiation is independent of
the general protocol version. Frames stay under 64 KiB, except a negotiated
worker.inference.start frame may be up to 25 MiB. The closed allowlist contains
worker.heartbeat, worker.transcript.commit, worker.live-event,
worker.inference.start, and worker.inference.cancel.
Assistant messages in worker.transcript.commit may carry the live event's
itemId. The Gateway binds it to its generated commit key, removes it before
storage, and publishes the correlation only after the row commits. It is not
provider replay data. This worker-only field travels between the exact
builds admitted by the bundle, version, and feature checks above.
For an identity-audited attached run, the live turn capability can record the credential, build, owner-epoch, and placement checks as one enforced admission receipt. The receipt contains none of the credential, build hashes, tokens, environment id, or session id. Worker operation rows and placement state remain their authoritative owners; successful connection is not an action-success receipt.
Transcript commits use owner-epoch fencing, a gateway-owned session binding, base-leaf compare-and-swap, and durable sequence replay; the gateway generates transcript entry and parent IDs through the normal session writer. Ownership and expiry are rechecked on each RPC.
Server capabilities
hello-ok.features.capabilities advertises supported contracts, not permissions.
If a marker is absent, support is unknown: retain existing behavior rather than
infer support from the protocol version or method list. Examples:
chat-input-custody-v1: chat queue modes and exact input receipts; see Chat input reconciliation.chat-send-routing-contract: chat send routing; see Session control.control-ui-browser-focus: bundled Control UI focus routing; omitted for configured UI roots or a disabled Control UI.
Client capabilities
Declare optional capabilities in connect.params.caps. Older Gateways ignore
unsupported capability declarations.
| Capability | Effect |
|---|---|
hello-health-on-demand |
Sends snapshot.health: {} instead of cached health; required snapshot fields, version counters, health RPCs, and events remain. |
health-summary |
Omits agents and sessions from health events and cached hello health. Other health fields remain; the health RPC retains full diagnostics. |
hello-presence-self |
Sends only the connecting client's hello presence row. The presence event stream supplies the full roster, including the initial reset for presence-deltas clients. |
tool-events |
Accepts structured tool lifecycle events. |
inline-widgets |
Renders hosted inline widget tool results. |
no-question-input |
Cannot collect structured answers; turns omit ask_user and ask in ordinary text (used by the ACP bridge). |
ultrafast |
Accepts the explicit "ultrafast" value. Without it, session responses/events project it as Fast (true) for released decoders; stored selection and execution are unchanged. |
chat-only-assistant-text |
Renders assistant text from chat, omitting redundant assistant-text agent events. See event families. |
chat-queue-status |
Displays authoritative pending-input receipts instead of transient successful queue acknowledgments. Refusals, drops, and errors remain visible. |
Without the health/presence options, clients retain full cached hello health and
presence. The Control UI advertises hello-health-on-demand and
hello-presence-self; the presence stream avoids caching a roster across
connections. Keep the ultrafast legacy projection while released decoders need it.
Capabilities do not grant authorization. For tools that require client capabilities:
- The session records the operator declaration on each operator turn. Empty declarations or channel turns clear it; internal wakes and restart resumes retain it. Existing sessions acquire it on their next operator turn; reset or deletion removes it.
- Tool listing uses that session fact. Execution still checks current client
capabilities and, for attached-client tools, the bound recipient's connection.
For example,
screenrequires its requesting Control UI to remain connected.
Node connect example
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.
Request the least privilege needed; hello-ok.auth.scopes reports the effective
grants. Use the canonical scope table
and method-level checks.
In particular:
operator.writealso satisfiesoperator.talk; Talk secret reads requireoperator.talk.secretsor admin. Usetalk.resolved.config.apiKeyfor the resolved secret, not source-shapedtalk.providers.<id>.apiKey.- Reserved plugin RPC prefixes (
config.*,exec.approvals.*,wizard.*,update.*) require admin. Persistent/config setand/config unsetalso require admin even when reached through lower-scopedchat.send. - Pairing require additional scopes based on the declared commands.
- Gateway
fs.listDirneeds write for workspace-contained host browsing and admin whennodeIdis present. Pass returned paths verbatim, including whitespace.
Caps/commands/permissions (node)
Nodes declare capability claims at connect time:
caps: high-level capability categories such ascamera,canvas,screen,location,voice, andtalk.commands: command allowlist for invoke.permissions: granular toggles (e.g.screen.record,camera.capture).
The Gateway treats these as claims and enforces server-side allowlists.
Connected nodes can publish optional agent-visible plugin or MCP tool
descriptors with node.pluginTools.update after a successful connect or
reconnect. Headless node hosts restart to apply declarative MCP inventory
changes. This update method is the only publication path; plugin tool descriptors are not accepted in
connect params. Each descriptor must use a provider-safe tool name and name
a command in the node's current command allowlist. The Gateway trusts descriptor
metadata from the paired node, filters descriptors outside the approved command
surface, removes them when the node disconnects, and rejects operator attempts
to mutate another node's catalog. Set gateway.nodes.pluginTools.enabled: false
to ignore node-published descriptors.
Connected node hosts publish their complete skill replacement catalog with
node.skills.update. This node-role method is the only node skill publication
path; skills are not accepted in connect params. Each descriptor contains a
safe name, description, and bounded SKILL.md content. The Gateway parses that
content with the normal skills loader, includes it in agent skill snapshots
while the node is connected, and removes it on disconnect. Set
gateway.nodes.allowSkills: false to ignore node-published skills.