diff --git a/docs/typebox.md b/docs/typebox.md new file mode 100644 index 000000000..a51abc433 --- /dev/null +++ b/docs/typebox.md @@ -0,0 +1,37 @@ +# TypeBox as Protocol Source of Truth + +Last updated: 2025-12-09 + +We use TypeBox schemas in `src/gateway/protocol/schema.ts` as the single source of truth for the Gateway control plane (hello/req/res/event frames and payloads). All derived artifacts should be generated from these schemas, not edited by hand. + +## Current pipeline + +- **TypeBox → JSON Schema**: `pnpm protocol:gen` writes `dist/protocol.schema.json` (draft-07) and runs AJV in the server tests. +- **TypeBox → Swift (quicktype)**: `pnpm protocol:gen` currently also generates `apps/macos/Sources/ClawdisProtocol/Protocol.swift` via quicktype. This produces a single struct with many optionals and is not ideal for strong typing. + +## Problem + +- Quicktype flattens `oneOf`/`discriminator` into an all-optional struct, so Swift loses exhaustiveness and safety for `GatewayFrame`. + +## Preferred plan (next step) + +- Add a small, custom Swift generator driven directly by the TypeBox schemas: + - Emit a sealed `enum GatewayFrame: Codable { case hello(Hello), helloOk(HelloOk), helloError(...), req(RequestFrame), res(ResponseFrame), event(EventFrame) }`. + - Emit strongly typed payload structs/enums (`Hello`, `HelloOk`, `HelloError`, `RequestFrame`, `ResponseFrame`, `EventFrame`, `PresenceEntry`, `Snapshot`, `StateVersion`, `ErrorShape`, `AgentEvent`, `TickEvent`, `ShutdownEvent`, `SendParams`, `AgentParams`, `ErrorCode`, `PROTOCOL_VERSION`). + - Custom `init(from:)` / `encode(to:)` enforces the `type` discriminator and can include an `unknown` case for forward compatibility. + - Wire a new script (e.g., `pnpm protocol:gen:swift`) into `protocol:check` so CI fails if the generated Swift is stale. + +Why this path: +- Single source of truth stays TypeBox; no new IDL to maintain. +- Predictable, strongly typed Swift (no optional soup). +- Small deterministic codegen (~150–200 LOC script) we control. + +## Alternative (if we want off-the-shelf codegen) + +- Wrap the existing JSON Schema into an OpenAPI 3.1 doc (auto-generated) and use **swift-openapi-generator** or **openapi-generator swift5**. More moving parts, but also yields enums with discriminator support. Keep this as a fallback if we don’t want a custom emitter. + +## Action items + +- Implement `protocol:gen:swift` that reads the TypeBox schemas and emits the sealed Swift enum + payload structs. +- Update `protocol:check` to include the Swift generator output in the diff check. +- Remove quicktype output once the custom generator is in place (or keep it for docs only). diff --git a/src/cli/program.ts b/src/cli/program.ts index a8a67c894..1ee188885 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -5,7 +5,6 @@ import { healthCommand } from "../commands/health.js"; import { sendCommand } from "../commands/send.js"; import { sessionsCommand } from "../commands/sessions.js"; import { statusCommand } from "../commands/status.js"; -import { loadConfig } from "../config/config.js"; import { callGateway, randomIdempotencyKey } from "../gateway/call.js"; import { startGatewayServer } from "../gateway/server.js"; import { danger, info, setVerbose } from "../globals.js"; @@ -13,11 +12,8 @@ import { loginWeb, logoutWeb } from "../provider-web.js"; import { runRpcLoop } from "../rpc/loop.js"; import { defaultRuntime } from "../runtime.js"; import { VERSION } from "../version.js"; -import { - ensureWebChatServerFromConfig, - startWebChatServer, -} from "../webchat/server.js"; -import { createDefaultDeps, logWebSelfId } from "./deps.js"; +import { startWebChatServer } from "../webchat/server.js"; +import { createDefaultDeps } from "./deps.js"; export function buildProgram() { const program = new Command(); @@ -66,14 +62,8 @@ export function buildProgram() { 'clawdis send --to +15555550123 --message "Hi" --json', "Send via your web session and print JSON result.", ], - [ - "clawdis gateway --port 18789", - "Run the WebSocket Gateway locally.", - ], - [ - "clawdis gw:status", - "Fetch Gateway status over WS.", - ], + ["clawdis gateway --port 18789", "Run the WebSocket Gateway locally."], + ["clawdis gw:status", "Fetch Gateway status over WS."], [ 'clawdis agent --to +15555550123 --message "Run summary" --deliver', "Talk directly to the agent using the Gateway; optionally send the WhatsApp reply.", diff --git a/src/gateway/client.ts b/src/gateway/client.ts index bd12fd011..6c0b024fb 100644 --- a/src/gateway/client.ts +++ b/src/gateway/client.ts @@ -11,8 +11,8 @@ import { } from "./protocol/index.js"; type Pending = { - resolve: (value: any) => void; - reject: (err: any) => void; + resolve: (value: unknown) => void; + reject: (err: unknown) => void; expectFinal: boolean; }; @@ -167,7 +167,11 @@ export class GatewayClient { } const expectFinal = opts?.expectFinal === true; const p = new Promise((resolve, reject) => { - this.pending.set(id, { resolve, reject, expectFinal }); + this.pending.set(id, { + resolve: (value) => resolve(value as T), + reject, + expectFinal, + }); }); this.ws.send(JSON.stringify(frame)); return p; diff --git a/src/gateway/protocol/index.ts b/src/gateway/protocol/index.ts index 402511299..65fea133f 100644 --- a/src/gateway/protocol/index.ts +++ b/src/gateway/protocol/index.ts @@ -9,31 +9,31 @@ import { type EventFrame, EventFrameSchema, errorShape, + type GatewayFrame, + GatewayFrameSchema, type Hello, type HelloError, HelloErrorSchema, type HelloOk, HelloOkSchema, HelloSchema, + PROTOCOL_VERSION, type PresenceEntry, PresenceEntrySchema, ProtocolSchemas, - PROTOCOL_VERSION, type RequestFrame, RequestFrameSchema, type ResponseFrame, ResponseFrameSchema, SendParamsSchema, + type ShutdownEvent, + ShutdownEventSchema, type Snapshot, SnapshotSchema, type StateVersion, StateVersionSchema, - TickEventSchema, type TickEvent, - GatewayFrameSchema, - type GatewayFrame, - type ShutdownEvent, - ShutdownEventSchema, + TickEventSchema, } from "./schema.js"; const ajv = new (