Protocol
v1 traffic is JSON-RPC 2.0 on a length-prefixed TCP socket. Kind 0 is JSON. Kind 1 is a binary attachment, used today for framebuffer pixels.
Frames
Every message:
- 4-byte big-endian payload length
- payload: 1 kind byte, then the body
Kind 0 body is a UTF-8 JSON-RPC object. Kind 1 body is: id tag, role string, then bytes. Framebuffer attachments use role framebuffer and correlate to the JSON-RPC id of the method that returned { "encoding": "mxfb0001", "byteLength": N }.
JSON-RPC version is "2.0". Host methods that need a reply must carry an id (number or string). A hello without an id is dropped: host.v1.hello is not a notification.
| Code | Meaning |
|---|---|
-32700 | parse error |
-32600 | invalid request |
-32601 | method not found |
-32602 | invalid params |
-32000 | application error |
params that fail to deserialize become -32602 with the serde error in message. Unknown methods become -32601 with method not found: <name>.
Handshake
host.v1.hello must be the first kind-0 frame. A later hello returns -32600: host.v1.hello is only valid as the first message on a new connection. protocolVersion must be 1. extensionId must match this socket. Read the token from MX_EXTENSION_TOKEN; it is not a CLI flag. A wrong token returns -32602: hello token is not the token issued for this extension.
The request the host accepts (this is the object the unit test round-trips):
{
"jsonrpc": "2.0",
"id": 1,
"method": "host.v1.hello",
"params": {
"token": "abc",
"protocolVersion": 1,
"extensionId": "com.example.one"
}
}The result:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": 1,
"hostCapabilities": ["runtime.v1"],
"sessionId": "com.example.one-a1b2c3d4e5f60708"
}
}sessionId is <id>- plus 8 random bytes, hex-encoded. hostCapabilities is always ["runtime.v1"] on this host.
Studio then calls extension.v1.reportStatus with no params. You have 5 seconds.
{
"jsonrpc": "2.0",
"id": 1,
"method": "extension.v1.reportStatus",
"params": null
}Reply with the status object Settings already displays. state is ready, degraded, or failed:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"state": "ready",
"message": "watching",
"version": "0.1.0",
"protocolVersion": 1,
"capabilities": ["runtime.v1"]
}
}On disable, reload, or host shutdown Studio calls extension.v1.shutdown (2 second timeout) and then kills the process if it is still running.
{
"jsonrpc": "2.0",
"id": 2,
"method": "extension.v1.shutdown",
"params": null
}Reply any JSON-RPC success, then exit.
A session that speaks those messages
This process uses the env the host actually injects, frames the way frame.rs frames, and answers the two methods the host calls. After ready it calls host.v1.listVms and prints the result array the runtime page documents.
#!/usr/bin/env python3
import json
import os
import socket
import struct
import sys
HOST = os.environ["MX_EXTENSION_HOST"]
TOKEN = os.environ["MX_EXTENSION_TOKEN"]
EXTENSION_ID = os.environ["MX_EXTENSION_ID"]
PROTOCOL = int(os.environ["MX_EXTENSION_PROTOCOL"])
VERSION = "0.1.0"
def recvall(sock, n):
buf = bytearray()
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise ConnectionError("socket closed")
buf.extend(chunk)
return bytes(buf)
def read_frame(sock):
length = struct.unpack(">I", recvall(sock, 4))[0]
payload = recvall(sock, length)
return payload[0], payload[1:]
def write_json(sock, obj):
body = json.dumps(obj, separators=(",", ":")).encode("utf-8")
payload = bytes([0]) + body
sock.sendall(struct.pack(">I", len(payload)) + payload)
def reply(sock, rpc_id, result):
write_json(sock, {"jsonrpc": "2.0", "id": rpc_id, "result": result})
host, port = HOST.rsplit(":", 1)
sock = socket.create_connection((host, int(port)))
write_json(
sock,
{
"jsonrpc": "2.0",
"id": 1,
"method": "host.v1.hello",
"params": {
"token": TOKEN,
"protocolVersion": PROTOCOL,
"extensionId": EXTENSION_ID,
},
},
)
kind, body = read_frame(sock)
hello = json.loads(body)
if hello.get("error"):
sys.stderr.write(json.dumps(hello["error"]) + "\n")
sys.exit(1)
capabilities = hello["result"]["hostCapabilities"]
next_id = 2
while True:
kind, body = read_frame(sock)
if kind != 0:
continue
msg = json.loads(body)
method = msg.get("method")
if method == "extension.v1.reportStatus":
reply(
sock,
msg["id"],
{
"state": "ready",
"message": "watching",
"version": VERSION,
"protocolVersion": PROTOCOL,
"capabilities": capabilities,
},
)
write_json(
sock,
{"jsonrpc": "2.0", "id": next_id, "method": "host.v1.listVms"},
)
next_id += 1
elif method == "extension.v1.shutdown":
reply(sock, msg["id"], None)
break
elif "result" in msg or "error" in msg:
sys.stdout.write(json.dumps(msg.get("result", msg.get("error"))) + "\n")Point command at that file, or at an interpreter with the file in args. Next: Runtime.