Protocol: lint fixes for client/program

main
Peter Steinberger 2025-12-09 15:18:34 +01:00
parent d1217e84c7
commit cf5769753a
4 changed files with 54 additions and 23 deletions

37
docs/typebox.md Normal file
View File

@ -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 (~150200 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 dont 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).

View File

@ -5,7 +5,6 @@ import { healthCommand } from "../commands/health.js";
import { sendCommand } from "../commands/send.js"; import { sendCommand } from "../commands/send.js";
import { sessionsCommand } from "../commands/sessions.js"; import { sessionsCommand } from "../commands/sessions.js";
import { statusCommand } from "../commands/status.js"; import { statusCommand } from "../commands/status.js";
import { loadConfig } from "../config/config.js";
import { callGateway, randomIdempotencyKey } from "../gateway/call.js"; import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
import { startGatewayServer } from "../gateway/server.js"; import { startGatewayServer } from "../gateway/server.js";
import { danger, info, setVerbose } from "../globals.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 { runRpcLoop } from "../rpc/loop.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { VERSION } from "../version.js"; import { VERSION } from "../version.js";
import { import { startWebChatServer } from "../webchat/server.js";
ensureWebChatServerFromConfig, import { createDefaultDeps } from "./deps.js";
startWebChatServer,
} from "../webchat/server.js";
import { createDefaultDeps, logWebSelfId } from "./deps.js";
export function buildProgram() { export function buildProgram() {
const program = new Command(); const program = new Command();
@ -66,14 +62,8 @@ export function buildProgram() {
'clawdis send --to +15555550123 --message "Hi" --json', 'clawdis send --to +15555550123 --message "Hi" --json',
"Send via your web session and print JSON result.", "Send via your web session and print JSON result.",
], ],
[ ["clawdis gateway --port 18789", "Run the WebSocket Gateway locally."],
"clawdis gateway --port 18789", ["clawdis gw:status", "Fetch Gateway status over WS."],
"Run the WebSocket Gateway locally.",
],
[
"clawdis gw:status",
"Fetch Gateway status over WS.",
],
[ [
'clawdis agent --to +15555550123 --message "Run summary" --deliver', 'clawdis agent --to +15555550123 --message "Run summary" --deliver',
"Talk directly to the agent using the Gateway; optionally send the WhatsApp reply.", "Talk directly to the agent using the Gateway; optionally send the WhatsApp reply.",

View File

@ -11,8 +11,8 @@ import {
} from "./protocol/index.js"; } from "./protocol/index.js";
type Pending = { type Pending = {
resolve: (value: any) => void; resolve: (value: unknown) => void;
reject: (err: any) => void; reject: (err: unknown) => void;
expectFinal: boolean; expectFinal: boolean;
}; };
@ -167,7 +167,11 @@ export class GatewayClient {
} }
const expectFinal = opts?.expectFinal === true; const expectFinal = opts?.expectFinal === true;
const p = new Promise<T>((resolve, reject) => { const p = new Promise<T>((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)); this.ws.send(JSON.stringify(frame));
return p; return p;

View File

@ -9,31 +9,31 @@ import {
type EventFrame, type EventFrame,
EventFrameSchema, EventFrameSchema,
errorShape, errorShape,
type GatewayFrame,
GatewayFrameSchema,
type Hello, type Hello,
type HelloError, type HelloError,
HelloErrorSchema, HelloErrorSchema,
type HelloOk, type HelloOk,
HelloOkSchema, HelloOkSchema,
HelloSchema, HelloSchema,
PROTOCOL_VERSION,
type PresenceEntry, type PresenceEntry,
PresenceEntrySchema, PresenceEntrySchema,
ProtocolSchemas, ProtocolSchemas,
PROTOCOL_VERSION,
type RequestFrame, type RequestFrame,
RequestFrameSchema, RequestFrameSchema,
type ResponseFrame, type ResponseFrame,
ResponseFrameSchema, ResponseFrameSchema,
SendParamsSchema, SendParamsSchema,
type ShutdownEvent,
ShutdownEventSchema,
type Snapshot, type Snapshot,
SnapshotSchema, SnapshotSchema,
type StateVersion, type StateVersion,
StateVersionSchema, StateVersionSchema,
TickEventSchema,
type TickEvent, type TickEvent,
GatewayFrameSchema, TickEventSchema,
type GatewayFrame,
type ShutdownEvent,
ShutdownEventSchema,
} from "./schema.js"; } from "./schema.js";
const ajv = new ( const ajv = new (