TypeBox Schemas: Gateway Protocol Source of Truth
Learn how TypeBox defines the Gateway WebSocket protocol for OpenClaw, powering validation, JSON Schema export, and Swift codegen. Essential for developers working on protocol clients or server internals.
Read this when
- Updating protocol schemas or codegen
TypeBox serves as a schema library built around TypeScript. OpenClaw relies on it to define the Gateway WebSocket protocol, covering the handshake, request/response patterns, and server events. These schemas power runtime validation through AJV, JSON Schema export, and Swift codegen for the macOS application. A single source of truth drives all downstream generation.
For broader protocol context, refer to Gateway architecture.
Mental model (30 seconds)
Gateway WS messages come in three frame types:
- Request:
{ type: "req", id, method, params } - Response:
{ type: "res", id, ok, payload | error } - Event:
{ type: "event", event, payload, seq?, stateVersion? }
A connect request is mandatory as the initial frame. Subsequently, clients invoke methods like health, send, and chat.send, and subscribe to events such as presence, tick, and agent.
Here is a minimal connection flow:
Client Gateway
|---- req:connect -------->|
|<---- res:hello-ok --------|
|<---- event:tick ----------|
|---- req:health ---------->|
|<---- res:health ----------|
A summary of typical methods and events:
| Category | Examples | Notes |
|---|---|---|
| Core | connect, health, status | connect must be first |
| Messaging | send, agent, agent.wait, system-event, logs.tail | side-effecting methods need idempotencyKey |
| Chat | chat.history, chat.send, chat.abort | WebChat uses these |
| Sessions | sessions.list, sessions.patch, sessions.delete | session admin |
| Automation | wake, cron.list, cron.run, cron.runs | wake and cron control |
| Nodes | node.list, node.invoke, node.pair.* | Gateway WS plus node actions |
| Events | tick, presence, agent, chat, health, shutdown | server push |
The canonical discovery inventory, as advertised, resides in src/gateway/server-methods-list.ts (listGatewayMethods, GATEWAY_EVENTS).
Where the schemas live
- Source barrels: the authoritative domain-module list is held by
packages/gateway-protocol/src/schema-modules.ts, whereas the publicschema.tswrapper additionally surfacesProtocolSchemas. - Generator registry: ordered
protocol-schema-fragment-*.tsfiles associate stable names with the canonical TypeBox objects from their owning modules.protocol-schemas.tsmerges these fragments in a predetermined sequence and rejects any duplicate keys. - Runtime validators (AJV):
packages/gateway-protocol/src/index.ts - Advertised feature/discovery registry:
src/gateway/server-methods-list.ts - Server handshake and method dispatch:
src/gateway/server-core-runtime.ts - Node client:
src/gateway/client.ts - Generated JSON Schema:
dist/protocol.schema.json(produced at build time, not checked in) - Generated Swift models:
apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift
Current pipeline
pnpm protocol:genwrites JSON Schema (draft-07) todist/protocol.schema.json.pnpm protocol:gen:swiftgenerates the Swift gateway models.pnpm protocol:check:swiftverifies the committed Swift models without rewriting them.pnpm protocol:gen:kotlingenerates the Android protocol models and constants.pnpm protocol:checkchecks the registry structure, runs all three generators, and verifies the committed Swift and Kotlin output (the JSON Schema output is a gitignored build artifact).
When a gateway schema affects native clients, run pnpm protocol:gen:swift, review the generated diff, then run pnpm protocol:check:swift. Commit the schema and GatewayModels.swift update together. Stable decoding behavior belongs in the focused GatewayModelsCompatibilityTests.swift regressions rather than in handwritten model copies.
How the schemas are used at runtime
- Server side: every inbound frame is validated with AJV. The handshake only accepts a
connectrequest whose params matchConnectParams. - Client side: the JS client validates event and response frames before using them.
- Feature discovery: the Gateway sends a conservative
features.methodsandfeatures.eventslist inhello-ok, fromlistGatewayMethods()andGATEWAY_EVENTS. - That discovery list is not a generated dump of every callable helper in
coreGatewayHandlers; some helper RPCs are implemented insrc/gateway/server-methods/*.tswithout being enumerated in the advertised feature list.
Example frames
Connect (first message):
{
"type": "req",
"id": "c1",
"method": "connect",
"params": {
"minProtocol": 3,
"maxProtocol": 4,
"client": {
"id": "openclaw-macos",
"displayName": "macos",
"version": "1.0.0",
"platform": "macos 15.1",
"mode": "ui",
"instanceId": "A1B2"
}
}
}
Hello-ok response:
{
"type": "res",
"id": "c1",
"ok": true,
"payload": {
"type": "hello-ok",
"protocol": 4,
"server": { "version": "dev", "connId": "ws-1" },
"features": { "methods": ["health"], "events": ["tick"] },
"snapshot": {
"presence": [],
"health": {},
"stateVersion": { "presence": 0, "health": 0 },
"uptimeMs": 0
},
"auth": { "role": "operator", "scopes": ["operator.read"] },
"policy": { "maxPayload": 1048576, "maxBufferedBytes": 1048576, "tickIntervalMs": 30000 }
}
}
Request and response:
{ "type": "req", "id": "r1", "method": "health" }
{ "type": "res", "id": "r1", "ok": true, "payload": { "ok": true } }
Event:
{ "type": "event", "event": "tick", "payload": { "ts": 1730000000 }, "seq": 12 }
Minimal client (Node.js)
Smallest useful flow: connect + health.
import { WebSocket } from "ws";
const ws = new WebSocket("ws://127.0.0.1:18789");
ws.on("open", () => {
ws.send(
JSON.stringify({
type: "req",
id: "c1",
method: "connect",
params: {
minProtocol: 4,
maxProtocol: 4,
client: {
id: "cli",
displayName: "example",
version: "dev",
platform: "node",
mode: "cli",
},
},
}),
);
});
ws.on("message", (data) => {
const msg = JSON.parse(String(data));
if (msg.type === "res" && msg.id === "c1" && msg.ok) {
ws.send(JSON.stringify({ type: "req", id: "h1", method: "health" }));
}
if (msg.type === "res" && msg.id === "h1") {
console.log("health:", msg.payload);
ws.close();
}
});
Worked example: add a method end-to-end
Example: add a new system.echo request that returns { ok: true, text }.
- Schema (source of truth)
Add to packages/gateway-protocol/src/schema/system-info.ts (or the closest matching feature module):
export const SystemEchoParamsSchema = Type.Object(
{ text: NonEmptyString },
{ additionalProperties: false },
);
export const SystemEchoResultSchema = Type.Object(
{ ok: Type.Boolean(), text: NonEmptyString },
{ additionalProperties: false },
);
Add both entries to the closest semantic packages/gateway-protocol/src/schema/protocol-schema-fragment-*.ts file. Import the owner module as a namespace when that fragment does not already use it, then map the stable registry names to the canonical schema objects:
import * as system from "./system.js";
export const OperationsProtocolSchemas = {
// Existing entries stay in their current order.
// ...
SystemEchoParams: system.SystemEchoParamsSchema,
SystemEchoResult: system.SystemEchoResultSchema,
} as const;
Do not sort fragment keys or move existing entries: native code generation follows registry insertion order. protocol-schemas.ts owns the deliberate fragment order and should change only when introducing a new semantic fragment.
export type SystemEchoParams = Static<typeof SystemEchoParamsSchema>;
export type SystemEchoResult = Static<typeof SystemEchoResultSchema>;
- Validation
In packages/gateway-protocol/src/index.ts, export an AJV validator:
export const validateSystemEchoParams = ajv.compile<SystemEchoParams>(SystemEchoParamsSchema);
- Server behavior
Add a handler in src/gateway/server-methods/system.ts:
export const systemHandlers: GatewayRequestHandlers = {
"system.echo": ({ params, respond }) => {
const text = String(params.text ?? "");
respond(true, { ok: true, text });
},
};
Register it in src/gateway/server-methods.ts (already merges systemHandlers), then add "system.echo" to the listGatewayMethods input in src/gateway/server-methods-list.ts.
If the method is callable by operator or node clients, also classify it in src/gateway/method-scopes.ts so scope enforcement and hello-ok feature advertising stay aligned.
- Regenerate
pnpm protocol:check
- Tests and docs
Add a server test in src/gateway/server.*.test.ts and note the method in docs.
Swift codegen behavior
The Swift generator emits:
- a
GatewayFrameenum withreq,res,event, andunknowncases - strongly typed payload structs/enums
ErrorCodevalues,GATEWAY_PROTOCOL_VERSION, andGATEWAY_MIN_PROTOCOL_VERSION
Unknown frame types are preserved as raw payloads for forward compatibility.
Versioning and compatibility
PROTOCOL_VERSIONis located inpackages/gateway-protocol/src/version.ts, with4as its current value.- The server receives
minProtocolandmaxProtocolfrom clients; any range that excludes its present protocol is rejected. - Unknown frame types are preserved by the Swift models so that older clients remain unaffected.
Schema patterns and conventions
- Strict payloads are handled through
additionalProperties: falsein most objects. - For IDs and method/event names,
NonEmptyString(Type.String({ minLength: 1 })) serves as the default. - A discriminator on
typeis applied to the top-levelGatewayFrame. - When side effects are involved, methods typically demand an
idempotencyKeyin their params, as seen insend,poll,agent, andchat.send. - Optional
internalEventscan be supplied toagentfor orchestration context generated at runtime, such as subagent or cron task completion handoff; consider this internal API surface.
Live schema JSON
The generated JSON Schema is a build artifact and is not stored in the repository. During the package rollout, the current beta schema can be accessed at:
When you change schemas
- Within the owning
packages/gateway-protocol/src/schema/*.tsmodule, modify the TypeBox schemas and add them to the nearestprotocol-schema-fragment-*.tsfile, keeping existing keys in their current order. - Register the method or event in
src/gateway/server-methods-list.ts. - If the new RPC requires operator or node scope classification, update
src/gateway/method-scopes.tsaccordingly. - Execute
pnpm protocol:check. - Commit the regenerated Swift models.