Proposal: a formal, implementation-independent specification for the Cynitor server protocol

I’ve been playing with Cynitor this weekend and it has a lot of promise. I was able to get it working on an Arduino UnoQ which was neat.

Looking at how Cynitor’s server and dashboard communicate over a REST API (/api/...) and a WebSocket stream (/ws) and I’m wanting to have another level of formality here. Today the Python server is the specification, and WEBSOCKET_README.md describes it in prose. I want to write the protocol down as a versioned, machine-checkable specification built from five existing standards (see below). Then any server and any client (and any agent), in any language, can implement this protocol as a standard way to remotely monitor Cyphal systems.

This will also allow us to build an MCP server adapter in front of Cynitor servers which would unlock significant intellegence for Cyphal networks.

Proposal

Layer Standard Describes
Data types JSON Schema 2020-12 Every payload, defined and shared by both API documents.
DSDL Cyphal technical document DSDL json interop
REST OpenAPI 3.1 Endpoints, parameters, status codes, auth
WebSocket AsyncAPI 3.0 /ws messages: who sends which, which ones get a reply
Errors RFC 9457 Problem Details The body of every HTTP error
Behaviour RFC 2119 / RFC 8174 keywords Semantics schemas can’t express: ordering, rates, filter matching

We also would include RFC 6750 Bearer tokens, OAuth 2 scopes, RFC 9728 metadata for authentication and authorization in the protocol.

Core rules (draft)

  1. Versioning. The protocol has its own Semantic Version, independent of Cynitor’s release number. A server MUST report it as protocol_version. A server MUST accept the WebSocket subprotocol cynitor.v<major> (RFC 6455 §1.9). A client MAY offer that subprotocol.
  2. Extensibility. Receivers MUST ignore object members and WebSocket message types they don’t recognise. Within a major version, senders MUST NOT remove a member or change its type or meaning. Adding members, message types or endpoints is a minor change.
  3. WebSocket framing. Every WebSocket message MUST be a single JSON object in a text frame, with a string member type.
  4. Errors. Every HTTP 4xx/5xx response MUST carry an RFC 9457 application/problem+json body. Endpoint-specific details are extension members. A WebSocket error MUST be sent as {"type": "error", "error": "<message>"}, and the connection stays open.
  5. Numbers. A floating-point value that is NaN or infinite MUST be encoded as null, since JSON can’t represent those values.
  6. Time. timestamp members MUST be RFC 3339 strings with a UTC offset. Members ending in _unix MUST be Unix seconds as a JSON number.
  7. Mapping DSDL to JSON. PROTOCOL.md defines normatively how a decoded Cyphal value becomes JSON in attributes, for example that a port-ID bitmask becomes the list of IDs it sets. This part can’t be a fixed schema, because the types depend on the DSDL loaded at run time.
  8. Deprecation. A deprecated member is marked "deprecated": true in its schema and kept until the next major version.
  9. Authentication and authorization. A server that requires authentication MUST accept RFC 6750 Bearer tokens in the Authorization header. Every operation and every client WebSocket message type declares the scope it requires. A server MUST answer a request without valid credentials with 401 and one with insufficient scope with 403 , each with an RFC 9457 body and a WWW-Authenticate header. Since browsers cannot set headers on WebSocket or link requests, a server MUST also accept a single-use ticket from POST /api/tickets as the ticket query parameter. A server MUST close a WebSocket with code 4401 when its credential stops being valid.

References