TypeBox como fuente de verdad del protocolo

Última actualización: 2026-01-10

TypeBox es una librería de esquemas TypeScript-first. La usamos para definir el protocolo WebSocket del Gateway (handshake, request/response, eventos del servidor). Esos esquemas impulsan la validación en runtime, la exportación de JSON Schema y el codegen de Swift para la app de macOS. Una fuente de verdad; todo lo demás se genera.

Si quieres el contexto de protocolo de más alto nivel, comienza con Gateway architecture.

Modelo mental (30 segundos)

Cada mensaje WS del Gateway es uno de tres frames:

  • Request: { type: "req", id, method, params }
  • Response: { type: "res", id, ok, payload | error }
  • Event: { type: "event", event, payload, seq?, stateVersion? }

El primer frame debe ser una solicitud connect. Después de eso, los clientes pueden llamar métodos (por ejemplo, health, send, chat.send) y suscribirse a eventos (por ejemplo, presence, tick, agent).

Flujo de conexión (mínimo):

Client                    Gateway
  |---- req:connect -------->|
  |<---- res:hello-ok --------|
  |<---- event:tick ----------|
  |---- req:health ---------->|
  |<---- res:health ----------|

Métodos + eventos comunes:

CategoríaEjemplosNotas
Coreconnect, health, statusconnect debe ser primero
Mensajeríasend, poll, agent, agent.waitefectos secundarios necesitan idempotencyKey
Chatchat.history, chat.send, chat.abort, chat.injectWebChat los usa
Sesionessessions.list, sessions.patch, sessions.deleteadministración de sesiones
Nodosnode.list, node.invoke, node.pair.*acciones de Gateway WS + nodo
Eventostick, presence, agent, chat, health, shutdownpush del servidor

La lista autoritativa está en src/gateway/server.ts (METHODS, EVENTS).

Dónde residen los esquemas

  • Fuente: src/gateway/protocol/schema.ts
  • Validadores en runtime (AJV): src/gateway/protocol/index.ts
  • Handshake del servidor + dispatch de métodos: src/gateway/server.ts
  • Cliente de nodo: src/gateway/client.ts
  • JSON Schema generado: dist/protocol.schema.json
  • Modelos Swift generados: apps/macos/Sources/OpenClawProtocol/GatewayModels.swift

Pipeline actual

  • pnpm protocol:gen
    • escribe JSON Schema (draft-07) en dist/protocol.schema.json
  • pnpm protocol:gen:swift
    • genera modelos Swift del gateway
  • pnpm protocol:check
    • ejecuta ambos generadores y verifica que la salida esté commiteada

Cómo se usan los esquemas en runtime

  • Lado servidor: cada frame entrante se valida con AJV. El handshake solo acepta una solicitud connect cuyos params coincidan con ConnectParams.
  • Lado cliente: el cliente JS valida los frames de evento y respuesta antes de usarlos.
  • Superficie de métodos: el Gateway anuncia los methods y events soportados en hello-ok.

Frames de ejemplo

Connect (primer mensaje):

{
  "type": "req",
  "id": "c1",
  "method": "connect",
  "params": {
    "minProtocol": 2,
    "maxProtocol": 2,
    "client": {
      "id": "openclaw-macos",
      "displayName": "macos",
      "version": "1.0.0",
      "platform": "macos 15.1",
      "mode": "ui",
      "instanceId": "A1B2"
    }
  }
}

Respuesta hello-ok:

{
  "type": "res",
  "id": "c1",
  "ok": true,
  "payload": {
    "type": "hello-ok",
    "protocol": 2,
    "server": { "version": "dev", "connId": "ws-1" },
    "features": { "methods": ["health"], "events": ["tick"] },
    "snapshot": {
      "presence": [],
      "health": {},
      "stateVersion": { "presence": 0, "health": 0 },
      "uptimeMs": 0
    },
    "policy": { "maxPayload": 1048576, "maxBufferedBytes": 1048576, "tickIntervalMs": 30000 }
  }
}

Request + 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 }

Cliente mínimo (Node.js)

Flujo útil más pequeño: 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: 3,
        maxProtocol: 3,
        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();
  }
});

Ejemplo paso a paso: agregar un método de extremo a extremo

Ejemplo: agregar un nuevo request system.echo que devuelve { ok: true, text }.

  1. Schema (fuente de verdad)

Agrega en src/gateway/protocol/schema.ts:

export const SystemEchoParamsSchema = Type.Object(
  { text: NonEmptyString },
  { additionalProperties: false },
);

export const SystemEchoResultSchema = Type.Object(
  { ok: Type.Boolean(), text: NonEmptyString },
  { additionalProperties: false },
);

Agrega ambos a ProtocolSchemas y exporta tipos:

  SystemEchoParams: SystemEchoParamsSchema,
  SystemEchoResult: SystemEchoResultSchema,
export type SystemEchoParams = Static<typeof SystemEchoParamsSchema>;
export type SystemEchoResult = Static<typeof SystemEchoResultSchema>;
  1. Validación

En src/gateway/protocol/index.ts, exporta un validador AJV:

export const validateSystemEchoParams = ajv.compile<SystemEchoParams>(SystemEchoParamsSchema);
  1. Comportamiento del servidor

Agrega un handler en src/gateway/server-methods/system.ts:

export const systemHandlers: GatewayRequestHandlers = {
  "system.echo": ({ params, respond }) => {
    const text = String(params.text ?? "");
    respond(true, { ok: true, text });
  },
};

Regístralo en src/gateway/server-methods.ts (ya fusiona systemHandlers), luego agrega "system.echo" a METHODS en src/gateway/server.ts.

  1. Regenerar
pnpm protocol:check
  1. Tests + docs

Agrega un test del servidor en src/gateway/server.*.test.ts y documenta el método en los docs.

Comportamiento del codegen de Swift

El generador de Swift emite:

  • Enum GatewayFrame con casos req, res, event y unknown
  • Structs/enums de payload fuertemente tipados
  • Valores de ErrorCode y GATEWAY_PROTOCOL_VERSION

Los tipos de frame desconocidos se preservan como payloads crudos para compatibilidad futura.

Versionado + compatibilidad

  • PROTOCOL_VERSION reside en src/gateway/protocol/schema.ts.
  • Los clientes envían minProtocol + maxProtocol; el servidor rechaza incompatibilidades.
  • Los modelos Swift mantienen los tipos de frame desconocidos para no romper clientes anteriores.

Patrones y convenciones de esquemas

  • La mayoría de objetos usan additionalProperties: false para payloads estrictos.
  • NonEmptyString es el valor por defecto para IDs y nombres de método/evento.
  • El GatewayFrame de nivel superior usa un discriminador en type.
  • Los métodos con efectos secundarios generalmente requieren un idempotencyKey en params (ejemplo: send, poll, agent, chat.send).
  • agent acepta internalEvents opcionales para contexto de orquestación generado en runtime (por ejemplo, handoff de completación de subagente/cron); trata esto como superficie de API interna.

JSON Schema en vivo

El JSON Schema generado está en el repo en dist/protocol.schema.json. El archivo crudo publicado normalmente está disponible en:

Cuando cambias los esquemas

  1. Actualiza los esquemas TypeBox.
  2. Ejecuta pnpm protocol:check.
  3. Commitea el esquema regenerado + los modelos Swift.