2025-12-09 17:51:05 +00:00
|
|
|
|
---
|
|
|
|
|
|
summary: "TypeBox schemas as the single source of truth for the gateway protocol"
|
|
|
|
|
|
read_when:
|
|
|
|
|
|
- Updating protocol schemas or codegen
|
2026-01-31 16:04:03 -05:00
|
|
|
|
title: "TypeBox"
|
2025-12-09 17:51:05 +00:00
|
|
|
|
---
|
2026-01-31 21:13:13 +09:00
|
|
|
|
|
2026-01-08 23:06:56 +01:00
|
|
|
|
# TypeBox as protocol source of truth
|
2025-12-09 15:18:34 +01:00
|
|
|
|
|
2026-01-10 17:38:34 +01:00
|
|
|
|
Last updated: 2026-01-10
|
2025-12-09 15:18:34 +01:00
|
|
|
|
|
2026-01-10 17:38:34 +01:00
|
|
|
|
TypeBox is a TypeScript-first schema library. We use it to define the **Gateway
|
|
|
|
|
|
WebSocket protocol** (handshake, request/response, server events). Those schemas
|
|
|
|
|
|
drive **runtime validation**, **JSON Schema export**, and **Swift codegen** for
|
|
|
|
|
|
the macOS app. One source of truth; everything else is generated.
|
|
|
|
|
|
|
|
|
|
|
|
If you want the higher-level protocol context, start with
|
|
|
|
|
|
[Gateway architecture](/concepts/architecture).
|
|
|
|
|
|
|
|
|
|
|
|
## Mental model (30 seconds)
|
|
|
|
|
|
|
|
|
|
|
|
Every Gateway WS message is one of three frames:
|
|
|
|
|
|
|
|
|
|
|
|
- **Request**: `{ type: "req", id, method, params }`
|
|
|
|
|
|
- **Response**: `{ type: "res", id, ok, payload | error }`
|
|
|
|
|
|
- **Event**: `{ type: "event", event, payload, seq?, stateVersion? }`
|
|
|
|
|
|
|
|
|
|
|
|
The first frame **must** be a `connect` request. After that, clients can call
|
|
|
|
|
|
methods (e.g. `health`, `send`, `chat.send`) and subscribe to events (e.g.
|
|
|
|
|
|
`presence`, `tick`, `agent`).
|
|
|
|
|
|
|
|
|
|
|
|
Connection flow (minimal):
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
Client Gateway
|
|
|
|
|
|
|---- req:connect -------->|
|
|
|
|
|
|
|<---- res:hello-ok --------|
|
|
|
|
|
|
|<---- event:tick ----------|
|
|
|
|
|
|
|---- req:health ---------->|
|
|
|
|
|
|
|<---- res:health ----------|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Common methods + events:
|
|
|
|
|
|
|
2026-01-31 21:13:13 +09:00
|
|
|
|
| Category | Examples | Notes |
|
|
|
|
|
|
| --------- | --------------------------------------------------------- | ---------------------------------- |
|
|
|
|
|
|
| Core | `connect`, `health`, `status` | `connect` must be first |
|
|
|
|
|
|
| Messaging | `send`, `poll`, `agent`, `agent.wait` | side-effects need `idempotencyKey` |
|
|
|
|
|
|
| Chat | `chat.history`, `chat.send`, `chat.abort`, `chat.inject` | WebChat uses these |
|
|
|
|
|
|
| Sessions | `sessions.list`, `sessions.patch`, `sessions.delete` | session admin |
|
|
|
|
|
|
| Nodes | `node.list`, `node.invoke`, `node.pair.*` | Gateway WS + node actions |
|
|
|
|
|
|
| Events | `tick`, `presence`, `agent`, `chat`, `health`, `shutdown` | server push |
|
2026-01-10 17:38:34 +01:00
|
|
|
|
|
|
|
|
|
|
Authoritative list lives in `src/gateway/server.ts` (`METHODS`, `EVENTS`).
|
|
|
|
|
|
|
|
|
|
|
|
## Where the schemas live
|
|
|
|
|
|
|
|
|
|
|
|
- Source: `src/gateway/protocol/schema.ts`
|
|
|
|
|
|
- Runtime validators (AJV): `src/gateway/protocol/index.ts`
|
|
|
|
|
|
- Server handshake + method dispatch: `src/gateway/server.ts`
|
|
|
|
|
|
- Node client: `src/gateway/client.ts`
|
|
|
|
|
|
- Generated JSON Schema: `dist/protocol.schema.json`
|
2026-01-30 03:15:10 +01:00
|
|
|
|
- Generated Swift models: `apps/macos/Sources/OpenClawProtocol/GatewayModels.swift`
|
2025-12-09 15:18:34 +01:00
|
|
|
|
|
|
|
|
|
|
## Current pipeline
|
|
|
|
|
|
|
2026-01-08 23:06:56 +01:00
|
|
|
|
- `pnpm protocol:gen`
|
2026-01-10 17:38:34 +01:00
|
|
|
|
- writes JSON Schema (draft‑07) to `dist/protocol.schema.json`
|
2026-01-08 23:06:56 +01:00
|
|
|
|
- `pnpm protocol:gen:swift`
|
|
|
|
|
|
- generates Swift gateway models
|
|
|
|
|
|
- `pnpm protocol:check`
|
|
|
|
|
|
- runs both generators and verifies the output is committed
|
2025-12-09 15:18:34 +01:00
|
|
|
|
|
2026-01-10 17:38:34 +01:00
|
|
|
|
## 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.
|
|
|
|
|
|
- **Method surface**: the Gateway advertises the supported `methods` and
|
|
|
|
|
|
`events` in `hello-ok`.
|
|
|
|
|
|
|
|
|
|
|
|
## Example frames
|
|
|
|
|
|
|
|
|
|
|
|
Connect (first message):
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"type": "req",
|
|
|
|
|
|
"id": "c1",
|
|
|
|
|
|
"method": "connect",
|
|
|
|
|
|
"params": {
|
|
|
|
|
|
"minProtocol": 2,
|
|
|
|
|
|
"maxProtocol": 2,
|
|
|
|
|
|
"client": {
|
2026-01-30 03:15:10 +01:00
|
|
|
|
"id": "openclaw-macos",
|
2026-01-11 11:45:25 +00:00
|
|
|
|
"displayName": "macos",
|
2026-01-10 17:38:34 +01:00
|
|
|
|
"version": "1.0.0",
|
|
|
|
|
|
"platform": "macos 15.1",
|
2026-01-11 11:45:25 +00:00
|
|
|
|
"mode": "ui",
|
2026-01-10 17:38:34 +01:00
|
|
|
|
"instanceId": "A1B2"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Hello-ok response:
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"type": "res",
|
|
|
|
|
|
"id": "c1",
|
|
|
|
|
|
"ok": true,
|
|
|
|
|
|
"payload": {
|
|
|
|
|
|
"type": "hello-ok",
|
|
|
|
|
|
"protocol": 2,
|
|
|
|
|
|
"server": { "version": "dev", "connId": "ws-1" },
|
|
|
|
|
|
"features": { "methods": ["health"], "events": ["tick"] },
|
2026-01-31 21:13:13 +09:00
|
|
|
|
"snapshot": {
|
|
|
|
|
|
"presence": [],
|
|
|
|
|
|
"health": {},
|
|
|
|
|
|
"stateVersion": { "presence": 0, "health": 0 },
|
|
|
|
|
|
"uptimeMs": 0
|
|
|
|
|
|
},
|
2026-01-10 17:38:34 +01:00
|
|
|
|
"policy": { "maxPayload": 1048576, "maxBufferedBytes": 1048576, "tickIntervalMs": 30000 }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Request + response:
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{ "type": "req", "id": "r1", "method": "health" }
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{ "type": "res", "id": "r1", "ok": true, "payload": { "ok": true } }
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Event:
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{ "type": "event", "event": "tick", "payload": { "ts": 1730000000 }, "seq": 12 }
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Minimal client (Node.js)
|
|
|
|
|
|
|
|
|
|
|
|
Smallest useful flow: connect + health.
|
|
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
|
import { WebSocket } from "ws";
|
|
|
|
|
|
|
|
|
|
|
|
const ws = new WebSocket("ws://127.0.0.1:18789");
|
|
|
|
|
|
|
|
|
|
|
|
ws.on("open", () => {
|
2026-01-31 21:13:13 +09:00
|
|
|
|
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",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
2026-01-10 17:38:34 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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 }`.
|
|
|
|
|
|
|
2026-01-31 21:13:13 +09:00
|
|
|
|
1. **Schema (source of truth)**
|
2026-01-10 17:38:34 +01:00
|
|
|
|
|
|
|
|
|
|
Add to `src/gateway/protocol/schema.ts`:
|
|
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
|
export const SystemEchoParamsSchema = Type.Object(
|
|
|
|
|
|
{ text: NonEmptyString },
|
|
|
|
|
|
{ additionalProperties: false },
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
export const SystemEchoResultSchema = Type.Object(
|
|
|
|
|
|
{ ok: Type.Boolean(), text: NonEmptyString },
|
|
|
|
|
|
{ additionalProperties: false },
|
|
|
|
|
|
);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Add both to `ProtocolSchemas` and export types:
|
|
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
|
SystemEchoParams: SystemEchoParamsSchema,
|
|
|
|
|
|
SystemEchoResult: SystemEchoResultSchema,
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
|
export type SystemEchoParams = Static<typeof SystemEchoParamsSchema>;
|
|
|
|
|
|
export type SystemEchoResult = Static<typeof SystemEchoResultSchema>;
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-02-06 10:00:08 -05:00
|
|
|
|
2. **Validation**
|
2026-01-10 17:38:34 +01:00
|
|
|
|
|
|
|
|
|
|
In `src/gateway/protocol/index.ts`, export an AJV validator:
|
|
|
|
|
|
|
|
|
|
|
|
```ts
|
2026-01-31 21:13:13 +09:00
|
|
|
|
export const validateSystemEchoParams = ajv.compile<SystemEchoParams>(SystemEchoParamsSchema);
|
2026-01-10 17:38:34 +01:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-02-06 10:00:08 -05:00
|
|
|
|
3. **Server behavior**
|
2026-01-10 17:38:34 +01:00
|
|
|
|
|
|
|
|
|
|
Add a handler in `src/gateway/server-methods/system.ts`:
|
|
|
|
|
|
|
|
|
|
|
|
```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 `METHODS` in `src/gateway/server.ts`.
|
|
|
|
|
|
|
2026-02-06 10:00:08 -05:00
|
|
|
|
4. **Regenerate**
|
2026-01-10 17:38:34 +01:00
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
pnpm protocol:check
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-02-06 10:00:08 -05:00
|
|
|
|
5. **Tests + docs**
|
2026-01-10 17:38:34 +01:00
|
|
|
|
|
|
|
|
|
|
Add a server test in `src/gateway/server.*.test.ts` and note the method in docs.
|
|
|
|
|
|
|
2026-01-08 23:06:56 +01:00
|
|
|
|
## Swift codegen behavior
|
2025-12-09 15:18:34 +01:00
|
|
|
|
|
2026-01-08 23:06:56 +01:00
|
|
|
|
The Swift generator emits:
|
2025-12-09 15:18:34 +01:00
|
|
|
|
|
2026-01-08 23:06:56 +01:00
|
|
|
|
- `GatewayFrame` enum with `req`, `res`, `event`, and `unknown` cases
|
|
|
|
|
|
- Strongly typed payload structs/enums
|
|
|
|
|
|
- `ErrorCode` values and `GATEWAY_PROTOCOL_VERSION`
|
2025-12-09 15:18:34 +01:00
|
|
|
|
|
2026-01-08 23:06:56 +01:00
|
|
|
|
Unknown frame types are preserved as raw payloads for forward compatibility.
|
2025-12-09 15:18:34 +01:00
|
|
|
|
|
2026-01-10 17:38:34 +01:00
|
|
|
|
## Versioning + compatibility
|
|
|
|
|
|
|
|
|
|
|
|
- `PROTOCOL_VERSION` lives in `src/gateway/protocol/schema.ts`.
|
|
|
|
|
|
- Clients send `minProtocol` + `maxProtocol`; the server rejects mismatches.
|
|
|
|
|
|
- The Swift models keep unknown frame types to avoid breaking older clients.
|
|
|
|
|
|
|
|
|
|
|
|
## Schema patterns and conventions
|
|
|
|
|
|
|
|
|
|
|
|
- Most objects use `additionalProperties: false` for strict payloads.
|
|
|
|
|
|
- `NonEmptyString` is the default for IDs and method/event names.
|
|
|
|
|
|
- The top-level `GatewayFrame` uses a **discriminator** on `type`.
|
|
|
|
|
|
- Methods with side effects usually require an `idempotencyKey` in params
|
|
|
|
|
|
(example: `send`, `poll`, `agent`, `chat.send`).
|
2026-03-01 23:11:08 +00:00
|
|
|
|
- `agent` accepts optional `internalEvents` for runtime-generated orchestration context
|
|
|
|
|
|
(for example subagent/cron task completion handoff); treat this as internal API surface.
|
2026-01-10 17:38:34 +01:00
|
|
|
|
|
|
|
|
|
|
## Live schema JSON
|
|
|
|
|
|
|
|
|
|
|
|
Generated JSON Schema is in the repo at `dist/protocol.schema.json`. The
|
|
|
|
|
|
published raw file is typically available at:
|
|
|
|
|
|
|
2026-02-06 10:08:59 -05:00
|
|
|
|
- [https://raw.githubusercontent.com/openclaw/openclaw/main/dist/protocol.schema.json](https://raw.githubusercontent.com/openclaw/openclaw/main/dist/protocol.schema.json)
|
2026-01-10 17:38:34 +01:00
|
|
|
|
|
2026-01-08 23:06:56 +01:00
|
|
|
|
## When you change schemas
|
2025-12-09 15:18:34 +01:00
|
|
|
|
|
2026-01-31 21:13:13 +09:00
|
|
|
|
1. Update the TypeBox schemas.
|
|
|
|
|
|
2. Run `pnpm protocol:check`.
|
|
|
|
|
|
3. Commit the regenerated schema + Swift models.
|