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ía | Ejemplos | Notas |
|---|---|---|
| Core | connect, health, status | connect debe ser primero |
| Mensajería | send, poll, agent, agent.wait | efectos secundarios necesitan idempotencyKey |
| Chat | chat.history, chat.send, chat.abort, chat.inject | WebChat los usa |
| Sesiones | sessions.list, sessions.patch, sessions.delete | administración de sesiones |
| Nodos | node.list, node.invoke, node.pair.* | acciones de Gateway WS + nodo |
| Eventos | tick, presence, agent, chat, health, shutdown | push 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
- escribe JSON Schema (draft-07) en
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
connectcuyos params coincidan conConnectParams. - Lado cliente: el cliente JS valida los frames de evento y respuesta antes de usarlos.
- Superficie de métodos: el Gateway anuncia los
methodsyeventssoportados enhello-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 }.
- 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>;
- Validación
En src/gateway/protocol/index.ts, exporta un validador AJV:
export const validateSystemEchoParams = ajv.compile<SystemEchoParams>(SystemEchoParamsSchema);
- 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.
- Regenerar
pnpm protocol:check
- 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
GatewayFramecon casosreq,res,eventyunknown - Structs/enums de payload fuertemente tipados
- Valores de
ErrorCodeyGATEWAY_PROTOCOL_VERSION
Los tipos de frame desconocidos se preservan como payloads crudos para compatibilidad futura.
Versionado + compatibilidad
PROTOCOL_VERSIONreside ensrc/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: falsepara payloads estrictos. NonEmptyStringes el valor por defecto para IDs y nombres de método/evento.- El
GatewayFramede nivel superior usa un discriminador entype. - Los métodos con efectos secundarios generalmente requieren un
idempotencyKeyen params (ejemplo:send,poll,agent,chat.send). agentaceptainternalEventsopcionales 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
- Actualiza los esquemas TypeBox.
- Ejecuta
pnpm protocol:check. - Commitea el esquema regenerado + los modelos Swift.