node/protocol.py
node/protocol.pyBrowse 18 files
924 tokens
3,598 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1"""Wire protocol for gateway ↔ node RPC (JSON envelopes).2 3 Request: {"type": <str>, "id": <str>, "token": <str>, "payload": <dict>}4 Response: {"type": "response", "id": <req-id>, "payload": <dict>}5 Error: {"type": "error", "id": <req-id>, "error": <str>}6 7Requests carry the shared bearer token (``hermes meet node approve`` on the gateway, read off8disk on the server); mismatched tokens are rejected before dispatch.9"""10 11from __future__ import annotations12 13import json14import uuid15from typing import Any, Dict, Tuple16 17 18VALID_REQUEST_TYPES = frozenset({"start_bot", "stop", "status", "transcript", "say", "ping"})19 20 21def _nonempty_str(value: Any) -> bool:22 return isinstance(value, str) and bool(value)23 24 25def make_request(type: str, token: str, payload: Dict[str, Any], req_id: str | None = None) -> Dict[str, Any]:26 """Construct a request envelope; ``req_id`` defaults to a uuid4 hex."""27 if not _nonempty_str(type):28 raise ValueError("type must be a non-empty string")29 if type not in VALID_REQUEST_TYPES:30 raise ValueError(f"unknown request type: {type!r}")31 if not isinstance(token, str):32 raise ValueError("token must be a string")33 if not isinstance(payload, dict):34 raise ValueError("payload must be a dict")35 return {"type": type, "id": req_id or uuid.uuid4().hex, "token": token, "payload": payload}36 37 38def make_response(req_id: str, payload: Dict[str, Any]) -> Dict[str, Any]:39 """Build a success envelope; clients correlate replies by ``id``, not type."""40 if not isinstance(payload, dict):41 raise ValueError("payload must be a dict")42 return {"type": "response", "id": req_id, "payload": payload}43 44 45def make_error(req_id: str, error: str) -> Dict[str, Any]:46 return {"type": "error", "id": req_id, "error": str(error)}47 48 49def encode(msg: Dict[str, Any]) -> str:50 """Serialize a message envelope to a JSON string."""51 return json.dumps(msg, separators=(",", ":"), ensure_ascii=False)52 53 54def decode(raw) -> Dict[str, Any]:55 """Parse a JSON envelope (object with string ``type`` + ``id``) from str/bytes; ValueError otherwise.56 Token match and payload shape are checked server-side in :func:`validate_request`."""57 if isinstance(raw, (bytes, bytearray)):58 raw = raw.decode("utf-8")59 try:60 obj = json.loads(raw)61 except (TypeError, json.JSONDecodeError) as exc:62 raise ValueError(f"malformed JSON: {exc}") from exc63 if not isinstance(obj, dict):64 raise ValueError("envelope must be a JSON object")65 for key in ("type", "id"):66 if not isinstance(obj.get(key), str):67 raise ValueError(f"envelope missing string '{key}'")68 return obj69 70 71def validate_request(msg: Dict[str, Any], expected_token: str) -> Tuple[bool, str]:72 """Return ``(True, "")`` or ``(False, <reason>)``; reasons are safe to send back to the client."""73 if not isinstance(msg, dict):74 return False, "envelope must be a dict"75 t, token = msg.get("type"), msg.get("token")76 checks = ( # ordered, lazily evaluated: first failing check wins77 (lambda: _nonempty_str(t), "missing or non-string 'type'"),78 (lambda: t in VALID_REQUEST_TYPES, f"unknown request type: {t!r}"),79 (lambda: _nonempty_str(msg.get("id")), "missing or non-string 'id'"),80 (lambda: _nonempty_str(token), "missing token"),81 (lambda: token == expected_token, "token mismatch"),82 (lambda: isinstance(msg.get("payload"), dict), "payload must be a dict"))83 return next(((False, reason) for ok, reason in checks if not ok()), (True, ""))84