Wire reference
Enough to write a peer: the framing, the shapes, the encodings, the version rule and the failure modes. Three transports, two shapes, two encodings.
Three transports
| transport | framing | for |
|---|---|---|
| argv | one JSON object on stdout, then exit | a program that runs once per call. Nothing to keep alive, and a fresh process knows everything the last one did. |
| pipe | one object per line, newline-delimited | a program held for the session, talking to the one that started it. |
| socket | four bytes of big-endian length, then that many bytes | anything that may knock. Self-delimiting, so a peer that dies mid-frame is caught at the length. |
Two shapes
a call — somebody is waiting {"call": "<verb>", "args": [ … ]} {"ok": true, "family": 1, "n": <count>, "result": [ … ]} an event — nobody is {"event": "<name>", … }
result is always a list, so one call answers with what the function
did. Two ends disagreeing about that fail silently: a client that unpacks reads a
bare-value server as having returned nothing at all, so the bug presents as an empty memory
rather than as an error.
It is the rows, and n is how many. A verb that lists things puts each
thing in result as its own value; it does not put the whole listing in as one
value that is a list. "result": [[…]] with "n": 1 is the mistake,
and it is invisible from one side — one sibling sent every listing it had that way while the
other three sent theirs flat, and the coordinator reading them row by row found an array
where a declaration belonged and concluded it declared nothing at all.
Two version rules
family is the revision of the wire a reply is written in. A newer peer is
refused by name; an older one is not. A reply with no family at all is from
before the check and is accepted.
verbs also carries surface — the revision of what a
third party writes against. Two numbers, because they move for different reasons:
| versions | read by | |
|---|---|---|
family | the wire between these programs — the reply shape, the encodings, which verbs exist | a sibling, or anything speaking to one |
surface | the registrar names, the fields each declaration owes, what a callback is handed | somebody's extension |
Both go up only when something already published stops working. Adding a registrar, a field, an event or a verb moves neither.
The constants are duplicated in each sibling rather than shared. A crate held in common would be a dependency between repositories, and this family has none.
Two encodings
| JSON | the default. What every peer understands, and what somebody with socat can read. |
| CBOR | the same shape as bytes, for a caller that is not going to read it. |
On a one-shot door, ask with --cbor. On a socket, just send it: the server
answers in whatever it was asked in.
how the encoding is read off a body — no handshake, no setting first non-space byte '{' or '[' → json 0x80..0xBF → cbor (a cbor map or array) anything else → json (so the error message is the useful one)
Refusals
A refused verb is ok: false with exit status zero, and a refusal is a
reply: only the transport failing closes anything. That is what lets a caller tell “it said
no” from “it is not there”.
| fault | the caller loses |
|---|---|
refused | a feature. Carry on. |
unavailable | the sibling. Carry on without it. |
failed | the write. Do not assume it was recorded. |
malformed | nothing yet — but this is a bug to report, not a condition to handle. |
Limits
| frame | 16 MiB on magi's own wire; 1 MiB between sessions. A message between instances is a sentence, not a payload — an unbounded read is a way to make a session allocate until it dies. |
| socket path | 108 bytes, which is the kernel's limit and not anybody's choice. Long temporary directories are how this is usually met. |
| identity | SO_PEERCRED. A caller the kernel will not identify gets nothing. |