* feat(gateway): advertise chat attachment limits on hello-ok Clients had no way to learn the gateway attachment ceilings, so external clients hardcoded guesses that drifted from server enforcement. Publish the two unconditional decoded-size ceilings on hello-ok policy.attachments from one shared resolver so advertised values cannot drift from the parser. MIME acceptance and per-message counts stay server-side: they depend on the entrypoint, the resolved model, and payload sniffing, so they cannot be stated once per connection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 17d6c355-8948-4b48-a936-e08b1c8806ef * feat(gateway): advertise chat attachment limits on hello-ok --------- Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Copilot-Session: 17d6c355-8948-4b48-a936-e08b1c8806ef
12 KiB
summary, read_when, title
| summary | read_when | title | |||
|---|---|---|---|---|---|
| Build a third-party operator or WebChat client for the Gateway WebSocket protocol |
|
Building a Gateway client |
Use the published Gateway packages to build operator dashboards, WebChat clients, and other third-party applications. This guide covers the client lifecycle around the wire contract: authentication, capabilities, reconnect recovery, history, subscriptions, and version upgrades.
For frame shapes, the handshake, errors, and the complete method surface, read the Gateway protocol specification.
Install the packages
npm install @openclaw/gateway-client @openclaw/gateway-protocol
@openclaw/gateway-protocolprovides schemas, runtime validators, TypeScript types, client identity and capability registries, structured error readers, and protocol version constants. Its npm tarball also includes the generatedprotocol.schema.jsonmachine-readable contract.@openclaw/gateway-clientis the reference connection implementation. Import the package root for the Node client and@openclaw/gateway-client/browserfor the browser-safe protocol, device-auth, and reconnect helpers.
The Node entry owns its WebSocket transport. A browser host supplies a WebSocket adapter plus persistent storage and signing callbacks for the device identity and device token.
Choose scopes and pair the device
A full interactive chat client that also renders approval prompts should request
role: "operator" with these scopes:
| Scope | Use it for |
|---|---|
operator.read |
chat.history, sessions.list, sessions.subscribe, model status, and read-only events |
operator.write |
chat.send and ordinary session mutations |
operator.approvals |
Listing, displaying, and resolving exec or plugin approvals |
Add operator.questions only if the client handles interactive questions,
operator.pairing only if it manages paired devices or nodes, and
operator.admin only for administrative operations such as config.patch.
The operator scopes reference
defines the complete method and approval-time rules.
Do not create a per-client bearer token by hand-editing openclaw.json. Configure
the Gateway's shared bootstrap authentication with openclaw configure --section gateway or the openclaw onboard --gateway-auth ... options, then let device
pairing mint the client token:
- Persist an Ed25519 device identity in the client.
- Wait for
connect.challenge, use itstsas the device proof'ssignedAt, sign the challenge-bound device payload, and sendconnectwith the requested operator role, scopes, and the shared Gateway token or password for bootstrap authentication. A received WebSocket challenge without a non-negative integertsis invalid. Clients that explicitly support Gateways from beforeconnect.challengeexisted may use local time only on their no-challenge path. - If the Gateway returns structured
PAIRING_REQUIREDdetails, show the request ID and pause or retry according toerror.details.recommendedNextStep. - On the Gateway host, review the request with
openclaw devices list, then approve that exact current request withopenclaw devices approve <requestId>. - Reconnect and persist
hello-ok.auth.deviceTokenwith the negotiated role and scopes. Use that device token for later connections.
Scope or role upgrades create a new pending pairing request. Token rotation cannot expand the approved pairing contract. See the Devices CLI for approval, rotation, and revocation commands.
Advertise client capabilities
connect.params.caps describes optional behavior the client can consume. It does
not grant authorization. Import names from GATEWAY_CLIENT_CAPS instead of
duplicating string literals:
import { GATEWAY_CLIENT_CAPS } from "@openclaw/gateway-protocol/client-info";
const caps = [GATEWAY_CLIENT_CAPS.TOOL_EVENTS];
The current registry contains approvals, exec-approvals, inline-widgets,
run-tool-bindings, session-scoped-events, plugin-approvals,
task-suggestions, terminal-offset-seq, tool-events, and ui-commands.
Advertise only capabilities the client actually implements.
Capability-gated agent tools are a separate use of the same declaration. If an agent tool requires a client capability, the Gateway omits that tool unless the originating client advertised every required capability.
Validate attachments before sending
Attachment limits are operator-tunable, so do not hardcode them. Read
hello-ok.policy.attachments and validate locally before uploading:
const attachments = hello.policy.attachments;
if (attachments) {
const ceiling = isImage ? attachments.maxImageBytes : attachments.maxBytes;
if (file.byteLength > ceiling) rejectLocally();
}
Both values are decoded per-attachment ceilings. Still check the serialized
request against policy.maxPayload: attachments travel as base64, so a file near
maxBytes can exceed the frame limit on its own. Older gateways omit
policy.attachments; when it is absent, send and handle the server outcome.
Accepted MIME types and per-message handling are not advertised because they
depend on the entrypoint and the resolved model. 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. The values are a connection-time
snapshot, so re-read them on every reconnect.
Recover state after reconnect
Treat every successful reconnect as a new projection over durable history and current in-memory run state:
- Re-establish
sessions.subscribeand the selected session'ssessions.messages.subscribesubscription. - Call
chat.historyfor the selectedsessionKeyand replace local persisted rows with the returnedmessagesprojection. - If
inFlightRunis present, adopt itsrunId, bufferedtext, and optionalplan. Adopt the run even whentextis empty. - Read
sessionInfo.hasActiveRunandsessionInfo.activeRunIds. Prefer exact membership inactiveRunIdswhen deciding whether a retained run still owns the streaming UI. A truehasActiveRunwith no listed ID can represent another active runtime projection. - Reconcile subsequent
agentevents bypayload.runIdandpayload.seq. Maintain the highest accepted sequence independently for each run, ignore an already-seen or lower sequence, and treat a forward gap as a reason to reload authoritative history.
The outer event frame also has an optional seq, which orders events on the
current WebSocket connection. It resets with a new connection. The seq inside
an agent event payload is assigned per run and orders that run's lifecycle,
assistant, plan, tool, and other stream events.
Render generated image artifacts
Assistant-generated images arrive as canonical type: "image" content blocks.
Managed blocks include a stable artifactId, a Gateway-relative url, MIME
type, dimensions, size, and accessible alt text. Keep that reference in the
transcript cache; do not persist downloaded bytes or temporary download URLs.
Resolve the image through the authenticated WebSocket connection:
- Call
artifacts.downloadwith the currentsessionKey, optionalagentId, and the block'sartifactId. - Use the returned short-lived
urlbeforeexpiresAt. The URL is scoped to that exact transcript-backed artifact and does not contain a reusable Gateway or device credential. - Fetch it from the Gateway origin using the same TLS pin and reverse-proxy headers as the active connection. Validate the response as an image and enforce a 12 MiB source limit plus a bounded decoded thumbnail.
- If the URL expires, repeat
artifacts.downloadonce. Reconnect or route changes cancel the old load rather than retargeting it to another Gateway.
Older image blocks without artifactId remain displayable by existing Control
UI clients, but native clients should show a readable attachment fallback rather
than forward a shared owner credential.
Use history metadata and stable anchors
Rows returned by chat.history can carry an __openclaw metadata envelope:
idis the transcript entry identity. Use it for anchored history requests, but not as a unique display-row key.seqis the positive transcript-record sequence. One stored record can project into more than one display row, so keep siblings with the sameidand sequence together.kindidentifies synthetic rows. A compaction boundary useskind: "compaction"and may includetokensBeforeandtokensAfterwhen a matching checkpoint recorded those metrics.
Page backward with the response's hasMore and nextOffset values. Numeric
offsets describe the current transcript projection, so do not persist them as
long-lived bookmarks across reset or compaction. Persist __openclaw.id instead.
To restore around a known row, call chat.history with messageId and the
sessionId that returned it. The Gateway can resolve that anchor from reset
archive history; anchored responses intentionally omit numeric paging metadata.
Subscribe instead of polling usage
Load the initial catalog with sessions.list, then call sessions.subscribe once
per connection. Merge sessions.changed events by sessionKey. Session change
payloads can carry live inputTokens, outputTokens, totalTokens,
totalTokensFresh, contextTokens, estimatedCostUsd, response-usage settings,
and active-run state.
Some change notifications are only invalidation signals. If an event omits the
row fields your view needs, refresh sessions.list. Do not poll usage.cost or
sessions.usage to keep a live session list current; reserve those methods for
on-demand aggregate or detailed reports.
Backfill exec approvals
A client with operator.approvals should install its event listener as soon as
hello-ok completes, then call exec.approval.list to backfill requests that
predate the connection. Reconcile the list and live
exec.approval.requested / exec.approval.resolved events by approval ID so a
transition racing the list request is neither lost nor resurrected.
Track protocol versions
The current wire version is 4. General operator and WebChat clients must
negotiate the exact current version with minProtocol: 4 and maxProtocol: 4.
Only authenticated node clients and lightweight probes have the N-1 acceptance
window, currently protocol 3 through 4.
Protocol changes are additive first. protocol.schema.json includes since
release-vintage metadata and required scope metadata for core methods, but a wire
version bump is still an explicit breaking event for third-party clients. Pin the
package versions you test, upgrade the client and Gateway together when the wire
version changes, and review the
OpenClaw changelog
before each upgrade.