docs/gateway/protocol.md
The Gateway WS protocol is the single control plane and node transport for OpenClaw. Every client (CLI, web UI, macOS app, iOS/Android nodes, headless nodes) connects over WebSocket and declares a role and scope at handshake time.
connect request.MAX_PREAUTH_PAYLOAD_BYTES). After
handshake, follow hello-ok.policy.maxPayload and
hello-ok.policy.maxBufferedBytes. With diagnostics enabled, oversized
inbound frames and slow outbound buffers emit payload.large events before
the gateway closes or drops the frame. These events carry surface, byte
sizes, limits, and a safe reason code, never message bodies, attachment
contents, raw frame bytes, tokens, cookies, or secrets.Frame shapes:
{type:"req", id, method, params}{type:"res", id, ok, payload|error}{type:"event", event, payload, seq?, stateVersion?}Side-effecting methods require idempotency keys (see schema).
Gateway sends a pre-connect challenge:
{
"type": "event",
"event": "connect.challenge",
"payload": { "nonce": "…", "ts": 1737264000000 }
}
Client replies with connect:
{
"type": "req",
"id": "…",
"method": "connect",
"params": {
"minProtocol": 4,
"maxProtocol": 4,
"client": {
"id": "cli",
"version": "1.2.3",
"platform": "macos",
"mode": "operator"
},
"role": "operator",
"scopes": ["operator.read", "operator.write"],
"caps": [],
"commands": [],
"permissions": {},
"auth": { "token": "…" },
"locale": "en-US",
"userAgent": "openclaw-cli/1.2.3",
"device": {
"id": "device_fingerprint",
"publicKey": "…",
"signature": "…",
"signedAt": 1737264000000,
"nonce": "…"
}
}
}
Gateway responds with hello-ok:
{
"type": "res",
"id": "…",
"ok": true,
"payload": {
"type": "hello-ok",
"protocol": 4,
"server": { "version": "…", "connId": "…" },
"features": { "methods": ["…"], "events": ["…"] },
"snapshot": { "…": "…" },
"auth": {
"role": "operator",
"scopes": ["operator.read", "operator.write"]
},
"policy": {
"maxPayload": 26214400,
"maxBufferedBytes": 52428800,
"tickIntervalMs": 15000
}
}
}
server, features, snapshot, policy, and auth are all required by
HelloOkSchema (packages/gateway-protocol/src/schema/frames.ts). auth
reports the negotiated role/scopes even when no device token is issued (shape
above). pluginSurfaceUrls is optional and maps plugin surface names (e.g.
canvas) to scoped hosted URLs; it may expire, so nodes call
node.pluginSurface.refresh with { "surface": "canvas" } for a fresh entry.
The deprecated canvasHostUrl / canvasCapability / node.canvas.capability.refresh
path is not supported; use plugin surfaces.
While the gateway is still finishing startup sidecars, connect can return a
retryable UNAVAILABLE error with details.reason: "startup-sidecars" and
retryAfterMs. Retry within your connection budget instead of treating it as
a terminal handshake failure.
When a device token is issued, hello-ok.auth adds it:
{
"auth": {
"deviceToken": "…",
"role": "operator",
"scopes": ["operator.read", "operator.write"]
}
}
Built-in QR/setup-code bootstrap is a mobile handoff path. A successful baseline setup-code connect returns a primary node token plus one bounded operator token:
{
"auth": {
"deviceToken": "…",
"role": "node",
"scopes": [],
"deviceTokens": [
{
"deviceToken": "…",
"role": "operator",
"scopes": ["operator.approvals", "operator.read", "operator.talk.secrets", "operator.write"]
}
]
}
}
This operator handoff is bounded on purpose: enough to start the mobile
operator loop and native setup, including operator.talk.secrets for Talk
config reads, but no pairing-mutation scopes and no operator.admin. Broader
pairing/admin access needs a separate approved pairing or token flow. Persist
hello-ok.auth.deviceTokens only when bootstrap auth ran over a trusted
transport (wss:// or loopback/local pairing).
Trusted same-process backend clients (client.id: "gateway-client",
client.mode: "backend") may omit device on direct loopback connections when
authenticating with the shared gateway token/password. This path is reserved
for internal control-plane RPCs (e.g. subagent session updates) and avoids
stale CLI/device pairing baselines blocking local backend work. Remote,
browser-origin, node, and explicit device-token/device-identity clients still
go through normal pairing and scope-upgrade checks.
{
"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 as camera, 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.
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).Operator scopes (src/gateway/operator-scopes.ts), the full closed set:
operator.readoperator.writeoperator.adminoperator.approvalsoperator.pairingoperator.talk.secretstalk.config with includeSecrets: true requires operator.talk.secrets (or
operator.admin). When secrets are included, read the active Talk provider
credential from talk.resolved.config.apiKey; talk.providers.<id>.apiKey
stays source-shaped and may be a SecretRef object or a redacted string.
Plugin-registered gateway RPC methods may request their own operator scope,
but these reserved core prefixes always resolve to operator.admin
(src/shared/gateway-method-policy.ts): config.*, exec.approvals.*,
wizard.*, update.*.
Method scope is only the first gate. Some slash commands reached through
chat.send apply stricter command-level checks: persistent /config set and
/config unset writes require operator.admin even for gateway clients that
already hold a lower operator scope.
node.pair.approve has an extra approval-time scope check on top of the base
method scope (operator.pairing), based on the pending request's declared
commands (src/infra/node-pairing-authz.ts):
| Declared commands | Required scopes |
|---|---|
| none | operator.pairing |
| non-exec commands | operator.pairing + operator.write |
includes system.run, system.run.prepare, or system.which | operator.pairing + operator.admin |
system-presence returns entries keyed by device identity, including
deviceId, roles, and scopes, so UIs can show one row per device even
when it connects as both operator and node.node.list includes optional lastSeenAtMs and lastSeenReason. Connected
nodes report current connection time with reason connect; paired nodes can
also report durable background presence via a trusted node event.Nodes call node.event with event: "node.presence.alive" to record that a
paired node was alive during a background wake, without marking it connected:
{
"event": "node.presence.alive",
"payloadJSON": "{\"trigger\":\"silent_push\",\"sentAtMs\":1737264000000,\"displayName\":\"Peter's iPhone\",\"version\":\"2026.4.28\",\"platform\":\"iOS 18.4.0\",\"deviceFamily\":\"iPhone\",\"modelIdentifier\":\"iPhone17,1\",\"pushTransport\":\"relay\"}"
}
trigger is a closed enum: background, silent_push, bg_app_refresh,
significant_location, manual, connect. Unknown values normalize to
background (src/shared/node-presence.ts). The event only persists for
authenticated node device sessions; device-less or unpaired sessions return
handled: false.
Successful gateways return a structured result:
{
"ok": true,
"event": "node.presence.alive",
"handled": true,
"reason": "persisted"
}
Older gateways may return only { "ok": true } for node.event; treat that
as an acknowledged RPC, not durable presence persistence.
Server-pushed broadcast events are scope-gated so pairing-scoped or node-only
sessions do not passively receive session content
(src/gateway/server-broadcast.ts):
agent events, tool-result
events) require at least operator.read. Sessions without it skip these
frames entirely.plugin.* broadcasts are gated to operator.write or
operator.admin by default; explicit entries such as
plugin.approval.requested / plugin.approval.resolved use
operator.approvals instead.heartbeat, presence, tick, connect/disconnect
lifecycle) stay unrestricted so transport health is observable to every
authenticated session.Each client connection keeps its own per-client sequence number, so broadcasts stay monotonically ordered on that socket even when different clients see different scope-filtered subsets of the event stream.
hello-ok.features.methods is a conservative discovery list built from
src/gateway/server-methods-list.ts plus loaded plugin/channel method
exports — it is not a generated dump of every method, and some methods (for
example push.test, web.login.start, web.login.wait, sessions.usage)
are intentionally excluded from discovery even though they are real, callable
methods. Treat this as feature discovery, not a full enumeration of
src/gateway/server-methods/*.ts.
The setup code embeds a short-lived bootstrap credential. Clients must not
log or persist it beyond the pairing flow.
chat: UI chat updates such as chat.inject and other transcript-only chat
events. In protocol v4, delta payloads carry deltaText; message remains
the cumulative assistant snapshot. Non-prefix replacements set
replace=true and use deltaText as the replacement text.session.message, session.operation, session.tool: transcript, in-flight
session operation, and event-stream updates for a subscribed session.sessions.changed: session index or metadata changed.presence: system presence snapshot updates.tick: periodic keepalive/liveness event.health: gateway health snapshot update.heartbeat: heartbeat event stream update.cron: cron run/job change event.shutdown: gateway shutdown notification.node.pair.requested / node.pair.resolved: node pairing lifecycle.node.invoke.request: node invoke request broadcast.device.pair.requested / device.pair.resolved: paired-device lifecycle.voicewake.changed: wake-word trigger config changed.exec.approval.requested / exec.approval.resolved: exec approval
lifecycle.plugin.approval.requested / plugin.approval.resolved: plugin approval
lifecycle.Nodes may call skills.bins to fetch the current list of skill executables
for auto-allow checks.
audit.list gives operator clients a stable newest-first view of agent run and
tool action 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.
agentId, sessionKey, or runId; optional kind
("agent_run" or "tool_action"); optional status ("started",
"succeeded", "failed", "cancelled", "timed_out", "blocked", or
"unknown"); optional inclusive after / before Unix-millisecond bounds;
optional limit from 1 to 500; and optional string cursor from the
preceding page.{ "events": AuditEvent[], "nextCursor"?: string }.Each event includes a stable event id, monotonic ledger sequence, source event
sequence, timestamp, actor, agent/session/run provenance, action, status, and a
normalized error code when applicable. Tool events may include tool call id and
tool name. The redaction field is always "metadata_only": the ledger does
not store prompts, messages, tool arguments, tool results, command output, or
raw error text.
Recording is on by default and controlled by
audit.enabled; when disabled,
audit.list keeps serving records written earlier until they expire.
Use openclaw audit for text queries and bounded JSON exports.
Operator clients inspect and cancel gateway background task records through
the task ledger RPCs (packages/gateway-protocol/src/schema/tasks.ts). These
return sanitized task summaries, not raw runtime state.
tasks.list requires operator.read.
status ("queued", "running", "completed",
"failed", "cancelled", or "timed_out") or an array of those statuses,
optional agentId, optional sessionKey, optional limit from 1 to
500, and optional string cursor.{ "tasks": TaskSummary[], "nextCursor"?: string }.tasks.get requires operator.read.
{ "taskId": string }.{ "task": TaskSummary }.tasks.cancel requires operator.write.
{ "taskId": string, "reason"?: string }.{ "found": boolean, "cancelled": boolean, "reason"?: string, "task"?: TaskSummary }.found reports whether the ledger had a matching task. cancelled
reports whether the runtime accepted or recorded cancellation.TaskSummary includes id, status, and optional metadata: kind,
runtime, title, agentId, sessionKey, childSessionKey, ownerKey,
runId, taskId, flowId, parentTaskId, sourceId, timestamps, progress,
terminal summary, and sanitized error text. agentId identifies the agent
executing the task; sessionKey and ownerKey preserve requester and control
context.
commands.list (operator.read) fetches the runtime command inventory for
an agent.
agentId is optional; omit it to read the default agent workspace.scope controls which surface the primary name targets: text returns
the primary text command token without the leading /; native and the
default both path return provider-aware native names when available.textAliases carries exact slash aliases such as /model and /m.nativeName carries the provider-aware native command name when one
exists.provider is optional and only affects native naming plus native plugin
command availability.includeArgs=false omits serialized argument metadata from the response.tools.catalog (operator.read) fetches the runtime tool catalog for an
agent. The response includes grouped tools and provenance metadata:
source: core or pluginpluginId: plugin owner when source="plugin"optional: whether a plugin tool is optionaltools.effective (operator.read) fetches the runtime-effective tool
inventory for a session.
sessionKey is required.tools.effective is read-only for MCP: it may project a warm session MCP
catalog through the final tool policy, but does not create MCP runtimes,
connect transports, or issue tools/list. If no matching warm catalog
exists, the response may include a notice such as mcp-not-yet-connected,
mcp-not-yet-listed, or mcp-stale-catalog.source="core", source="plugin",
source="channel", or source="mcp".tools.invoke (operator.write) invokes one available tool through the
same gateway policy path as /tools/invoke.
name is required. args, sessionKey, agentId, confirm, and
idempotencyKey are optional.sessionKey and agentId are present, the resolved session agent
must match agentId.cron, gateway, and nodes require
owner/admin identity (operator.admin) even though tools.invoke itself
is operator.write.ok, toolName, optional
output, and typed error fields. Approval or policy refusals return
ok:false in the payload rather than bypassing the gateway tool policy
pipeline.skills.status (operator.read) fetches the visible skill inventory for an
agent.
agentId is optional; omit it to read the default agent workspace.skills.search and skills.detail (operator.read) return ClawHub
discovery metadata.skills.upload.begin, skills.upload.chunk, and skills.upload.commit
(operator.admin) stage a private skill archive before installing it. This
is a separate admin upload path for trusted clients, not the normal ClawHub
skill install flow, and is disabled by default unless
skills.install.allowUploadedArchives is enabled.
skills.upload.begin({ kind: "skill-archive", slug, sizeBytes, sha256?, force?, idempotencyKey? })
creates an upload bound to that slug and force value.skills.upload.chunk({ uploadId, offset, dataBase64 }) appends bytes at
the exact decoded offset.skills.upload.commit({ uploadId, sha256? }) verifies the final size and
SHA-256. Commit only finalizes the upload; it does not install the skill.SKILL.md root. The
archive's internal directory name never selects the install target.skills.install (operator.admin) has three modes:
{ source: "clawhub", slug, version?, force? } installs a
skill folder into the default agent workspace skills/ directory.{ source: "upload", uploadId, slug, force?, sha256?, timeoutMs? }
installs a committed upload into the default agent workspace
skills/<slug> directory. The slug and force value must match the
original skills.upload.begin request. Rejected unless
skills.install.allowUploadedArchives is enabled; the setting does not
affect ClawHub installs.{ name, installId, timeoutMs? } runs a declared
metadata.openclaw.install action on the gateway host. Older clients may
still send dangerouslyForceUnsafeInstall; this field is deprecated,
accepted only for protocol compatibility, and ignored. Use
security.installPolicy for operator-owned install decisions.skills.update (operator.admin) has two modes:
skills.entries.<skillKey> values such as enabled,
apiKey, and env.models.list viewsmodels.list accepts an optional view parameter
(src/agents/model-catalog-visibility.ts):
"default": if agents.defaults.models is configured, the
response is the allowed catalog, including dynamically discovered models
for provider/* entries. Otherwise the response is the full gateway
catalog."configured": picker-sized behavior. If agents.defaults.models is
configured, it still wins, including provider-scoped discovery for
provider/* entries. Without an allowlist, the response uses explicit
models.providers.<provider>.models entries, falling back to the full
catalog only when no configured model rows exist."all": full gateway catalog, bypassing agents.defaults.models. Use for
diagnostics/discovery UIs, not normal model pickers.exec.approval.requested.exec.approval.resolve (requires
operator.approvals).host=node, exec.approval.request must include systemRunPlan
(canonical argv/cwd/rawCommand/session metadata). Requests missing
systemRunPlan are rejected.node.invoke system.run calls reuse that
canonical systemRunPlan as the authoritative command/cwd/session context.command, rawCommand, cwd, agentId, or
sessionKey between prepare and the final approved system.run forward,
the gateway rejects the run instead of trusting the mutated payload.agent requests can include deliver=true to request outbound delivery.bestEffortDeliver=false (the default) keeps strict behavior: unresolved or
internal-only delivery targets return INVALID_REQUEST.bestEffortDeliver=true allows fallback to session-only execution when no
external deliverable route can be resolved (for example internal/webchat
sessions or ambiguous multi-channel configs).agent results may include result.deliveryStatus when delivery was
requested, using the same sent, suppressed, partial_failed, and
failed statuses documented for
openclaw agent --json --deliver.PROTOCOL_VERSION, MIN_CLIENT_PROTOCOL_VERSION,
MIN_NODE_PROTOCOL_VERSION, and MIN_PROBE_PROTOCOL_VERSION live in
packages/gateway-protocol/src/version.ts.minProtocol + maxProtocol. Operator and UI clients must
include the current protocol in that range; current clients and servers run
protocol v4.role: "node" and client.mode: "node"
may use the N-1 node protocol (currently v3). Lightweight restart probes use
the same N-1 window. Device auth, pairing, scopes, command policy, and exec
approvals are unchanged by this compatibility window. Plugin-owned node
capabilities and commands are withheld until the node upgrades to the current
protocol because their hosted surfaces are not part of the N-1 contract.pnpm protocol:genpnpm protocol:gen:swiftpnpm protocol:checkThe reference client implementation lives in packages/gateway-client/src/
(OpenClaw wraps it via the thin src/gateway/client.ts facade). These
defaults are stable across protocol v4 and are the expected baseline for
third-party clients.
| Constant | Default | Source |
|---|---|---|
PROTOCOL_VERSION | 4 | packages/gateway-protocol/src/version.ts |
MIN_CLIENT_PROTOCOL_VERSION | 4 | packages/gateway-protocol/src/version.ts |
MIN_NODE_PROTOCOL_VERSION | 3 | packages/gateway-protocol/src/version.ts |
MIN_PROBE_PROTOCOL_VERSION | 3 | packages/gateway-protocol/src/version.ts |
| Request timeout (per RPC) | 30_000 ms | packages/gateway-client/src/client.ts (requestTimeoutMs) |
| Preauth / connect-challenge timeout | 15_000 ms | packages/gateway-client/src/timeouts.ts (OPENCLAW_HANDSHAKE_TIMEOUT_MS env can raise the paired server/client budget) |
| Initial reconnect backoff | 1_000 ms | packages/gateway-client/src/client.ts (backoffMs) |
| Max reconnect backoff | 30_000 ms | packages/gateway-client/src/client.ts (scheduleReconnect) |
| Fast-retry clamp after device-token close | 250 ms | packages/gateway-client/src/client.ts |
Force-stop grace before terminate() | 250 ms | FORCE_STOP_TERMINATE_GRACE_MS |
stopAndWait() default timeout | 1_000 ms | STOP_AND_WAIT_TIMEOUT_MS |
Default tick interval (pre hello-ok) | 30_000 ms | packages/gateway-client/src/client.ts |
| Tick-timeout close | code 4000 when silence exceeds tickIntervalMs * 2 | packages/gateway-client/src/client.ts |
MAX_PAYLOAD_BYTES | 25 * 1024 * 1024 (25 MB) | src/gateway/server-constants.ts |
The server advertises the effective policy.tickIntervalMs,
policy.maxPayload, and policy.maxBufferedBytes in hello-ok; clients
should honor those values rather than the pre-handshake defaults.
connect.params.auth.token or
connect.params.auth.password, depending on the configured
gateway.auth.mode ("none" | "token" | "password" | "trusted-proxy").gateway.auth.allowTailscale: true)
or non-loopback gateway.auth.mode: "trusted-proxy" satisfy the connect
auth check from request headers instead of connect.params.auth.*.gateway.auth.mode: "none" skips shared-secret connect auth
entirely; do not expose that mode on public/untrusted ingress.hello-ok.auth.deviceToken. Clients should
persist it after any successful connect.selectConnectAuth in
packages/gateway-client/src/client.ts):
auth.password is orthogonal and always forwarded when set.auth.token is populated in priority order: explicit shared token first,
then an explicit deviceToken, then a stored per-device token (keyed by
deviceId + role).auth.bootstrapToken is sent only when none of the above resolved
auth.token. A shared token or any resolved device token suppresses it.AUTH_TOKEN_MISMATCH retry is gated to trusted endpoints only: loopback,
or wss:// with a pinned tlsFingerprint. Public wss:// without pinning
does not qualify.hello-ok.auth.deviceToken plus a bounded operator token in
hello-ok.auth.deviceTokens for trusted mobile handoff. The operator token
includes operator.talk.secrets for native Talk configuration reads, but
excludes pairing-mutation scopes and operator.admin.PAIRING_REQUIRED details include recommendedNextStep: "wait_then_retry",
retryable: true, and pauseReconnect: false. Keep reconnecting with the
same bootstrap token until the request is approved or the token becomes
invalid.hello-ok.auth.deviceTokens only when the connect used bootstrap
auth on a trusted transport such as wss:// or loopback/local pairing.deviceToken or explicit scopes, that
caller-requested scope set remains authoritative; cached scopes are only
reused when the client is reusing the stored per-device token.device.token.rotate and
device.token.revoke (requires operator.pairing). Rotating or revoking a
node or other non-operator role also requires operator.admin.device.token.rotate returns rotation metadata. It echoes the replacement
bearer token only for same-device calls already authenticated with that
device token, so token-only clients can persist their replacement before
reconnecting. Shared/admin rotations do not echo the bearer token.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.rotate and device.token.revoke also 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.error.details.code plus recovery hints:
error.details.canRetryWithDeviceToken (boolean)error.details.recommendedNextStep: one of retry_with_device_token,
update_auth_configuration, update_auth_credentials,
wait_then_retry, review_auth_configuration
(packages/gateway-protocol/src/connect-error-details.ts).AUTH_TOKEN_MISMATCH:
AUTH_SCOPE_MISMATCH means the device token was recognized but does not
cover the requested role/scopes. Do not present this as a bad token; prompt
the operator to re-pair or approve the narrower/broader scope contract.device.id) derived from a
keypair fingerprint.device identity during connect (operator +
node). The only device-less operator exceptions are explicit trust paths:
gateway.controlUi.allowInsecureAuth=true for localhost-only insecure
HTTP compatibility.gateway.auth.mode: "trusted-proxy" operator Control UI auth.gateway.controlUi.dangerouslyDisableDeviceAuth=true (break-glass, severe
security downgrade).gateway-client backend RPCs on the reserved internal
helper path.missing scope.gateway.controlUi.dangerouslyDisableDeviceAuth=true is a Control UI
break-glass scope-preservation path. It does not grant scopes to arbitrary
custom backend or CLI-shaped WebSocket clients.gateway-client backend helper path preserves
scopes only for internal local control-plane RPCs; custom backend IDs do
not receive this exception.connect.challenge nonce.For legacy clients that still use pre-challenge signing behavior, connect
returns DEVICE_AUTH_* detail codes under error.details.code with a stable
error.details.reason.
Common migration failures:
| Message | details.code | details.reason | Meaning |
|---|---|---|---|
device nonce required | DEVICE_AUTH_NONCE_REQUIRED | device-nonce-missing | Client omitted device.nonce (or sent blank). |
device nonce mismatch | DEVICE_AUTH_NONCE_MISMATCH | device-nonce-mismatch | Client signed with a stale/wrong nonce. |
device signature invalid | DEVICE_AUTH_SIGNATURE_INVALID | device-signature | Signature payload does not match v2 payload. |
device signature expired | DEVICE_AUTH_SIGNATURE_EXPIRED | device-signature-stale | Signed timestamp is outside allowed skew. |
device identity mismatch | DEVICE_AUTH_DEVICE_ID_MISMATCH | device-id-mismatch | device.id does not match public key fingerprint. |
device public key invalid | DEVICE_AUTH_PUBLIC_KEY_INVALID | device-public-key | Public key format/canonicalization failed. |
Migration target:
connect.challenge.connect.params.device.nonce.v3
(buildDeviceAuthPayloadV3 in packages/gateway-client/src/device-auth.ts),
which binds platform and deviceFamily in addition to
device/client/role/scopes/token/nonce fields.v2 signatures remain accepted for compatibility, but paired-device
metadata pinning still controls command policy on reconnect.gateway.tls config).gateway.remote.tlsFingerprint or CLI --tls-fingerprint.This protocol exposes the full gateway API: status, channels, models, chat,
agent, sessions, nodes, approvals, and more. The exact surface is defined by
the TypeBox schemas re-exported from packages/gateway-protocol/src/schema.ts.