ZyrisDocs
Protocol specification

Connection lifecycle

Authentication at the upgrade, the hello handshake, heartbeat and close, and why reconnects are lossy.

CLOSED ──dial──▶ AUTHENTICATING ──upgrade ok──▶ HELLO ──hello/hello_ack──▶ READY
   ▲                   │ HTTP 401                  │ unsupported_version → CLOSED (4400)
   │                   ▼                           │
   └────────────── CLOSED ◀── CLOSING ◀── zyris.closing / ws close / heartbeat timeout
        reconnect with resume_token within grace ⇒ presence continuity

Authentication

The dialer sends Authorization: Bearer <credential> on the websocket upgrade request. A failure refuses the upgrade with HTTP 401 before either side has spoken the protocol — there is no websocket to close yet.

What a valid credential looks like is the deployment's choice; the wire only requires that it ride in that header. On Attacca it is a zc_ credential — see Credentials.

The socket is authenticated once, at the upgrade, and nothing re-checks it. A deployment that revokes credentials needs an in-band way to act on it. The recommended shape, and Attacca's: the periodic heartbeat also reports whether the node was revoked, and the peer holding the socket closes it with websocket code 4401 when it was.

Handshake

The first frame in each direction is the handshake. It is always msgpack, whatever the negotiation settles on.

// dialer → acceptor
{ "t": "hello",
  "protocol": { "major": 1, "minors_supported": [0] },
  "serialization": ["msgpack", "json"],
  "agent": "zyris/0.2.0 (myrepo; service)",
  "kind": "service",                                          // optional
  "node_name": "myrepo",                                      // optional
  "features": ["cancel", "attachments", "video-webrtc", "video-mjpeg"],
  "resume": { "conn_id": "…", "resume_token": "…" } }        // optional

// acceptor → dialer
{ "t": "hello_ack",
  "protocol": { "major": 1, "minor": 0 },
  "serialization": "msgpack",
  "conn_id": "…", "resume_token": "…",
  "node_id": "…",
  "node": { "system": "laptop", "program": "zyris-code", "name": "myrepo-2" },   // optional
  "heartbeat": { "interval_s": 20, "timeout_s": 45 },
  "limits": { "max_control_frame": 1048576, "max_chunk": 262144,
              "max_inflight_reqs": 64, "initial_stream_credit": 262144 },
  "resumed": false,
  "features": ["cancel", "attachments"] }
  • Version. A major-version mismatch is answered with err unsupported_version, then websocket close 4400.
  • Features. A feature is usable only when it appears in both lists. A peer MUST NOT send frames gated behind a feature the other side did not advertise. An acceptor that predates the features field advertises nothing, which is the safe reading.
  • node_name is the name this connection asks to be known by. It is optional on the wire, but a deployment that names nodes may require it: Attacca answers a non-cli hello without one with err invalid_params and closes. A kind: "cli" dialer is a consumer that registers no node, and its node_name is ignored.
  • node is where the acceptor placed the connection: three slugs, written as the path system/program/name. The name is the requested one if it is free among live nodes at that system/program path, and the lowest free -2, -3… otherwise — so read it back rather than assuming it. It is absent for a cli dialer.

The Rust runtime never sends hello.resume. Instead, a node_name that matches a node of the same credential released within the grace, or gone stale, takes that node back by name, keeping its node_id and node — so a crash does not turn desktop into desktop-2. Any other connect is a new node with a new node_id.

Heartbeat and close

Websocket ping/pong is the liveness primitive. The acceptor pings every interval_s; either side closes after timeout_s of silence.

A graceful close:

  1. Send note zyris.closing { reason }.
  2. Stop issuing new requests.
  3. Finish or cancel in-flight work.
  4. Close the websocket with code 1000.

A server shutting down for a rolling restart sends reason: "server_restart", so nodes reconnect immediately instead of backing off.

Reconnect is deliberately lossy

On disconnect, every in-flight request on both sides fails locally with connection_lost (retriable), and every open stream is dead.

Reconnecting with a valid resume_token within the grace window (30 seconds) restores identity continuity only: the node is never marked offline, and its capability announcement is assumed unchanged (resumed: true). Otherwise the node MUST announce again.

Resuming interrupted work is an application concern, by design: turn streams resume by cursor, file transfers by byte offset, PTYs by reopening and redrawing. Streaming tools MUST document how they resume.

Presence

Enrollment and identity are out of band — they happen over HTTP before the websocket exists — and are described in Concepts and Credentials.

On the wire, a node exists while its connection does. When it drops, a resume grace runs (30 seconds on Attacca); after it, the node is gone and its name is free. A periodic sweep removes nodes whose last heartbeat is older than twice the heartbeat interval, as a backstop against a server replica dying with a socket open.

On this page