docs: expand TypeBox protocol guide
This commit is contained in:
@@ -5,20 +5,242 @@ read_when:
|
|||||||
---
|
---
|
||||||
# TypeBox as protocol source of truth
|
# TypeBox as protocol source of truth
|
||||||
|
|
||||||
Last updated: 2026-01-08
|
Last updated: 2026-01-10
|
||||||
|
|
||||||
TypeBox schemas define the Gateway control plane (connect/req/res/event frames and
|
TypeBox is a TypeScript-first schema library. We use it to define the **Gateway
|
||||||
payloads). All generated artifacts must come from these schemas.
|
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:
|
||||||
|
|
||||||
|
| 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` | WebChat uses these |
|
||||||
|
| Sessions | `sessions.list`, `sessions.patch`, `sessions.delete` | session admin |
|
||||||
|
| Nodes | `node.list`, `node.invoke`, `node.pair.*` | bridge + node actions |
|
||||||
|
| Events | `tick`, `presence`, `agent`, `chat`, `health`, `shutdown` | server push |
|
||||||
|
|
||||||
|
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`
|
||||||
|
- Generated Swift models: `apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift`
|
||||||
|
|
||||||
## Current pipeline
|
## Current pipeline
|
||||||
|
|
||||||
- `pnpm protocol:gen`
|
- `pnpm protocol:gen`
|
||||||
- writes the JSON Schema output (draft‑07)
|
- writes JSON Schema (draft‑07) to `dist/protocol.schema.json`
|
||||||
- `pnpm protocol:gen:swift`
|
- `pnpm protocol:gen:swift`
|
||||||
- generates Swift gateway models
|
- generates Swift gateway models
|
||||||
- `pnpm protocol:check`
|
- `pnpm protocol:check`
|
||||||
- runs both generators and verifies the output is committed
|
- runs both generators and verifies the output is committed
|
||||||
|
|
||||||
|
## 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": {
|
||||||
|
"name": "macos",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"platform": "macos 15.1",
|
||||||
|
"mode": "app",
|
||||||
|
"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"] },
|
||||||
|
"snapshot": { "presence": [], "health": {}, "stateVersion": { "presence": 0, "health": 0 }, "uptimeMs": 0 },
|
||||||
|
"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", () => {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: "req",
|
||||||
|
id: "c1",
|
||||||
|
method: "connect",
|
||||||
|
params: {
|
||||||
|
minProtocol: 2,
|
||||||
|
maxProtocol: 2,
|
||||||
|
client: {
|
||||||
|
name: "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 `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>;
|
||||||
|
```
|
||||||
|
|
||||||
|
2) **Validation**
|
||||||
|
|
||||||
|
In `src/gateway/protocol/index.ts`, export an AJV validator:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const validateSystemEchoParams =
|
||||||
|
ajv.compile<SystemEchoParams>(SystemEchoParamsSchema);
|
||||||
|
```
|
||||||
|
|
||||||
|
3) **Server behavior**
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
4) **Regenerate**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm protocol:check
|
||||||
|
```
|
||||||
|
|
||||||
|
5) **Tests + docs**
|
||||||
|
|
||||||
|
Add a server test in `src/gateway/server.*.test.ts` and note the method in docs.
|
||||||
|
|
||||||
## Swift codegen behavior
|
## Swift codegen behavior
|
||||||
|
|
||||||
The Swift generator emits:
|
The Swift generator emits:
|
||||||
@@ -29,6 +251,27 @@ The Swift generator emits:
|
|||||||
|
|
||||||
Unknown frame types are preserved as raw payloads for forward compatibility.
|
Unknown frame types are preserved as raw payloads for forward compatibility.
|
||||||
|
|
||||||
|
## 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`).
|
||||||
|
|
||||||
|
## Live schema JSON
|
||||||
|
|
||||||
|
Generated JSON Schema is in the repo at `dist/protocol.schema.json`. The
|
||||||
|
published raw file is typically available at:
|
||||||
|
|
||||||
|
- https://raw.githubusercontent.com/clawdbot/clawdbot/main/dist/protocol.schema.json
|
||||||
|
|
||||||
## When you change schemas
|
## When you change schemas
|
||||||
|
|
||||||
1) Update the TypeBox schemas.
|
1) Update the TypeBox schemas.
|
||||||
|
|||||||
Reference in New Issue
Block a user