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:

CategoryExamplesNotes
Coreconnect, health, statusconnect must be first
Messagingsend, agent, agent.wait, system-event, logs.tailside-effecting methods need idempotencyKey
Chatchat.history, chat.send, chat.abortWebChat uses these
Sessionssessions.list, sessions.patch, sessions.deletesession admin
Automationwake, cron.list, cron.run, cron.runswake and cron control
Nodesnode.list, node.invoke, node.pair.*Gateway WS plus node actions
Eventstick, presence, agent, chat, health, shutdownserver 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 public schema.ts wrapper additionally surfaces ProtocolSchemas.
  • Generator registry: ordered protocol-schema-fragment-*.ts files associate stable names with the canonical TypeBox objects from their owning modules. protocol-schemas.ts merges 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:gen writes JSON Schema (draft-07) to dist/protocol.schema.json.
  • pnpm protocol:gen:swift generates the Swift gateway models.
  • pnpm protocol:check:swift verifies the committed Swift models without rewriting them.
  • pnpm protocol:gen:kotlin generates the Android protocol models and constants.
  • pnpm protocol:check checks 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 connect request whose params match ConnectParams.
  • Client side: the JS client validates event and response frames before using them.
  • Feature discovery: the Gateway sends a conservative features.methods and features.events list in hello-ok, from listGatewayMethods() and GATEWAY_EVENTS.
  • That discovery list is not a generated dump of every callable helper in coreGatewayHandlers; some helper RPCs are implemented in src/gateway/server-methods/*.ts without 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 }.

  1. 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>;
  1. Validation

In packages/gateway-protocol/src/index.ts, export an AJV validator:

export const validateSystemEchoParams = ajv.compile<SystemEchoParams>(SystemEchoParamsSchema);
  1. 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.

  1. Regenerate
pnpm protocol:check
  1. 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 GatewayFrame enum with req, res, event, and unknown cases
  • strongly typed payload structs/enums
  • ErrorCode values, GATEWAY_PROTOCOL_VERSION, and GATEWAY_MIN_PROTOCOL_VERSION

Unknown frame types are preserved as raw payloads for forward compatibility.

Versioning and compatibility

  • PROTOCOL_VERSION is located in packages/gateway-protocol/src/version.ts, with 4 as its current value.
  • The server receives minProtocol and maxProtocol from 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: false in most objects.
  • For IDs and method/event names, NonEmptyString (Type.String({ minLength: 1 })) serves as the default.
  • A discriminator on type is applied to the top-level GatewayFrame.
  • When side effects are involved, methods typically demand an idempotencyKey in their params, as seen in send, poll, agent, and chat.send.
  • Optional internalEvents can be supplied to agent for 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

  1. Within the owning packages/gateway-protocol/src/schema/*.ts module, modify the TypeBox schemas and add them to the nearest protocol-schema-fragment-*.ts file, keeping existing keys in their current order.
  2. Register the method or event in src/gateway/server-methods-list.ts.
  3. If the new RPC requires operator or node scope classification, update src/gateway/method-scopes.ts accordingly.
  4. Execute pnpm protocol:check.
  5. Commit the regenerated Swift models.
1,393 words · updated Aug 13, 2026