Protocol¶
A kitchen Pi, a server in the closet, a phone on the couch, maybe a second server
upstairs — for any of them to talk, they have to agree on two things: what a message
looks like, and where it goes next. That agreement is the HiveMind protocol. Every
message that crosses the wire is a single, uniform object — a HiveMessage — a JSON
envelope (or its compact binary twin) stamped with a msg_type that says what the
message is for and which way it should travel. The carrier underneath can be anything;
the envelope is always the same shape, so a satellite and hivemind-core never have to
guess.
In a nutshell
- Every message is a
HiveMessagetagged with amsg_type. There are only three flavours: payloads that carry content (BUS,SHARED_BUS,BINARY), routing verbs that steer a message up, down, or across the mesh (ESCALATE,BROADCAST,PROPAGATE,QUERY,CASCADE,INTERCOM,PING), and connection frames for setup (HELLO,HANDSHAKE). BUSis the one you'll meet most: a single hop from a satellite to the brain and back.- hivemind-core tags each message with who asked and which session it belongs to, then uses
Message.reply()to make sure the answer finds its way home to the right satellite.
HiveMessage types¶
That msg_type stamp is the first thing every node reads, and it always comes from one
list: the HiveMessageType enum in hivemind_bus_client.message. The list is long, but
it sorts cleanly into three jobs — carry content, route content, or set up the
connection. Meet them a group at a time.
Payload messages¶
These are the ones with something inside — an OVOS Message, or your own application
data. If a message is doing actual work, it's almost certainly one of these:
| Type | Purpose | Direction |
|---|---|---|
BUS |
Single-hop message to/from the AI back-end | Bidirectional |
SHARED_BUS |
Passive monitoring of a satellite's local OVOS bus | Satellite → hivemind-core |
BINARY |
Raw binary data (audio, images, files) | Bidirectional |
Ninety-nine percent of the time you'll be sending BUS. The other three are for
special jobs — mirroring a bus, smuggling your own payload, or shipping audio.
Transport / routing messages¶
The next group carries nothing of its own. Each one wraps another HiveMessage and
exists only to steer it — up the tree, down the tree, or out in every direction. Read
the Direction column as the shape of the journey:
| Type | Purpose | Direction |
|---|---|---|
ESCALATE |
Multi-hop upward — satellite asks parent for help | Satellite → hivemind-core (up the chain) |
BROADCAST |
Multi-hop downward — hivemind-core commands all satellites | hivemind-core → All satellites (down the chain) |
PROPAGATE |
Flood in all directions — reaches all reachable nodes | Bidirectional |
QUERY |
Routed request with a single expected response | Bidirectional |
CASCADE |
Scatter/gather — every node may answer; originator picks best | Bidirectional |
INTERCOM |
End-to-end encrypted point-to-point tunnel | Between any two nodes |
PING |
Topology probe — always wrapped inside PROPAGATE | Bidirectional (via PROPAGATE) |
RENDEZVOUS |
Store-and-forward mail for a peer that is offline | Bidirectional |
These only earn their keep once you have more than one hivemind-core — a plain
one-server hive never needs them. The Mesh Topology page shows them in
motion; the deep dives on QUERY, CASCADE, INTERCOM, and PING are further down
this page.
Connection management messages¶
The last group you'll never send by hand — the library and hivemind-core exchange them for you before your first real message even leaves. They're listed here so you recognise them in a packet capture, not because you have to do anything with them:
| Type | Purpose |
|---|---|
HELLO |
Node announcement on connection (carries node ID and public key) |
HANDSHAKE |
Cryptographic key exchange. Protocol v3 runs a Noise handshake (always-encrypted session); the legacy v1/v2 path uses a password (salted-hash + PBKDF2) or RSA envelope. See Security. |
Roles¶
A message type isn't a free-for-all: not every node may send every type, and not every node will accept one. hivemind-core and a satellite play different parts, so they subscribe to different halves of the list. Here's who does what:
hivemind-core (server side)¶
- Accepts:
BUS,SHARED_BUS,PROPAGATE,ESCALATE,QUERY,CASCADE,INTERCOM - Emits:
BUS,PROPAGATE,BROADCAST,QUERY,CASCADE,INTERCOM
Satellite (client protocol)¶
- Accepts:
BUS,PROPAGATE,BROADCAST,QUERY,CASCADE,INTERCOM - Emits:
BUS,SHARED_BUS,PROPAGATE,ESCALATE,QUERY,CASCADE,INTERCOM
Notice the asymmetry: only a satellite emits ESCALATE (it asks upward) and SHARED_BUS
(it mirrors its own bus), and only hivemind-core emits BROADCAST (it commands
downward). The verb you're allowed to send depends on which end of the connection you
are.
BUS message — the workhorse¶
Everything above is scaffolding for this one message. When you ask "what's the weather?",
a BUS message is what carries the question and a BUS message is what carries the
answer home. It's a plain single hop: a satellite wraps an OVOS Message in a
HiveMessage(HiveMessageType.BUS, ovos_message) and sends it to hivemind-core, which
then walks four steps:
- Checks the client's
allowed_typeswhitelist — if the OVOS message type is not allowed, the message is dropped - Runs the configured policy chain (by default
OVOSAgentPolicy) which injects per-client session data including skill/intent blacklists - Injects the OVOS message into the local bus with routing context (
source,destination,peer,session) - Routes all OVOS replies back to the originating satellite by reading
context["destination"]
Step 4 is the clever bit — how does a speak message that a skill emits seconds later
know which satellite to go back to? The trick is Message.reply() (from
ovos-bus-client). It swaps source ↔ destination on every reply, so every downstream
skill message — speak, intent.handled, and the rest — automatically carries the
originating satellite's peer ID in destination. hivemind-core reads that field and
routes each reply to the right connection.
The upshot for you: you send an utterance and the answer comes back to your device, not someone else's, without your ever addressing it. Two satellites can ask at the same moment and neither hears the other's reply. Here it is, one hop each way:

Session isolation between clients (session NAT)¶
A non-admin client can declare session_id: "default" on a BUS payload, but that must
never let a remote client reach the OVOS bus's own local, device-owned "default"
session. That would let one satellite read or mutate another's device state.
hivemind-core prevents this with a NAT-like rewrite. On the way in, it stamps every
inbound BUS message with a Layer-1 session id, "<session-namespace>:<declared-session-id>",
so a declared "default" becomes a namespaced id private to that client before the policy
chain or the bus ever sees it. The namespace is derived from the client's durable database
identity, sha256(hub_salt:client_id)[:16], not from the live connection, so it stays the
same across a reconnect. It falls back to a per-connection nonce only when no durable
identity can be resolved. On the way out, HiveMindClientConnection.send() strips this
client's own namespace prefix back off, so the client only ever sees the plain session id
it originally declared. The internal namespace never crosses the wire in either direction.
This is symmetric and lives entirely in hivemind-core, so every agent and binary-protocol
plugin built on it inherits the isolation automatically.
SHARED_BUS — passive monitoring¶
BUS is a satellite asking hivemind-core to do something. SHARED_BUS is the
opposite posture: a satellite letting hivemind-core watch its own local OVOS bus,
read-only. hivemind-core sees the traffic but never injects it into its own bus — think
one-way mirror, not a shared room. The ovos-hivemind-pipeline-plugin,
run with its slave_mode setting enabled, is the usual reason you'd reach for it.

INTERCOM — end-to-end encrypted peer-to-peer¶
Every message so far passes through hivemind-core, which means hivemind-core can read
it. INTERCOM is for when two nodes want to say something that even the servers relaying
it can't overhear — a sealed letter passed hand to hand, where every courier can carry it
but only the addressee can open it.
The sealing is a hybrid envelope: a random AES-256-GCM session key is wrapped with the recipient node's RSA public key (PKCS#1 OAEP) and the payload is encrypted with AES-256-GCM. The envelope carries base64 fields encrypted_key, ciphertext, tag, nonce, and signature. Intermediate nodes — including hivemind-core — cannot read the payload. Only the target node, which holds the corresponding RSA private key, can unwrap the session key and decrypt it. The exact envelope hivemind-core itself accepts is narrower than this, and the message-types reference spells out both shapes.
INTERCOM is usually the payload of a transport message (ESCALATE or PROPAGATE) so it reaches its destination through the mesh. Intermediate nodes forward it without being able to read it. The recipient is not hidden from them. The outer target_pubkey field is cleartext, and each node reads it to decide whether to consume the frame or pass it on.
Origin verification is fail-closed
The receiving node verifies the sender's RSA signature (PSS over SHA-256) over the raw ciphertext bytes, before it decrypts, against the public key pinned from the sender's HELLO. A missing signature, a signature that fails to verify, or no pinned key for the sender each drop the frame. A dropped frame stops there: the node does not relay it to peers and does not escalate it upstream, and the sender receives no error.
Every session is encrypted, so a hivemind-core node also drops any INTERCOM that carries
no signed envelope, with no opt-out, and logs dropping unauthenticated message. If
INTERCOM traffic stopped after an upgrade, this is why. The remedy is to sign the
envelope.
See Bootstrapping satellite-to-satellite trust for the key-pinning rules.
Request/response — QUERY¶
The next three types only matter once your hive is more than one server deep — when a
question might have to travel past the first hivemind-core to find an answer. QUERY is
the first of them.
Picture a guest assistant that can't answer "what's on my shared calendar?" on its own,
so it passes the question up to the household server. QUERY is ESCALATE with a promise
of a reply: it climbs the chain like an escalation, but the node that finally answers
sends the response all the way back down to whoever asked. Reach for it when you need one
definite answer from one node somewhere above you.
Request flow:
- A satellite wraps an OVOS
MessageinHiveMessage(HiveMessageType.QUERY, bus_hive_message)and sends it to hivemind-core, including aquery_idin the message metadata. - Each instance in the chain attempts to answer from its local agent (within a timeout). If the local agent produces a response, that instance wraps it as a QUERY response and sends it back downstream.
- If the local agent does not answer, the instance forwards the QUERY upstream via
query_to_master. At the top-level master with no upstream, ahive.query.timeouterror response is returned instead.
Response envelope:
The answer needs to find its way back down to the exact node that asked, so the response
carries a little bookkeeping. A QUERY response is itself a HiveMessage(HiveMessageType.QUERY, ...) with metadata["is_response"] = True. The payload wraps an inner BUS HiveMessage, which in turn carries the OVOS reply Message. The metadata fields are what the return trip routes on:
| Field | Value |
|---|---|
is_response |
True |
query_id |
UUID correlating request and response |
originator_peer |
Peer that issued the original request |
responder_peer |
Peer that produced the answer |
A node that receives a QUERY response with is_response = True routes it toward originator_peer without re-processing it as a new request.
Scatter/gather — CASCADE¶
QUERY wants the answer from one node. CASCADE wants every answer from everyone.
It floods the hive like PROPAGATE, but every reachable node that can respond does — and
the originator gathers the pile and picks a winner. Use it to ask the whole hive a
question at once: "who's currently playing music?", "what's each node's status?", "which
of you can handle this?"
Request flow:
- A satellite sends
HiveMessage(HiveMessageType.CASCADE, bus_hive_message)with aquery_id. - Each instance that receives the CASCADE: (a) tries its local agent and, if it gets a response, sends that back as a CASCADE response; (b) forwards the CASCADE onward to all other connected peers and upstream.
- Multiple responses can arrive at the originator from different nodes.
Response collection:
On the client side, CascadeAggregator buffers responses for a configurable cascade_timeout (default 5 s). Once the timeout expires — or the expected number of responses has arrived (derived from HiveMapper.nodes) — a cascade_select_callback picks the winning response and delivers it. The default select callback chooses randomly; applications should supply a domain-appropriate selector.
Trust model:
Here's the catch worth remembering. A QUERY answer comes from a known node up your own
chain; a CASCADE answer can come from anywhere in the hive, including nodes you have
no particular reason to trust. So the select callback isn't just picking the prettiest
reply — it's your one chance to vet who you're listening to before you act on it.
PING and topology mapping¶
Before any of the routing above can work, a node has to know what the hive even looks
like — who's out there, and how many hops away. PING is how it finds out, and it
works by echo rather than reply.
A PING is always wrapped inside a PROPAGATE, so it floods to every reachable node.
There's no PONG: each node that hears a PING simply re-emits its own PING carrying
the same flood_id, which floods onward in turn. Receivers dedupe on flood_id so a
probe is only ever processed once, and the payload each one carries is small —
{flood_id, peer, site_id, timestamp, public_key, lang}. HiveMapper in hivemind_bus_client.hive_map sits and
watches those echoes come back, and from the pattern it draws a live map of the whole
hive.
flood_id dedup is specific to PING. Underneath it sits a generic rule that covers
PROPAGATE, ESCALATE, CASCADE and PING alike: every relaying node appends a route
hop naming its own public key, and a node drops any message whose route already lists
that key. There is no hop counter and no TTL. A client that relays without appending its
own hop gives its peers nothing to suppress, so write that hop. See
ESCALATE for the field detail.
Session and context keys¶
Back down at the level of a single BUS message: how does hivemind-core keep two
satellites' conversations from bleeding into each other, and enforce that a guest device
can't invoke the skills you blacklisted for it? The answer is a bundle of metadata it
quietly staples onto every message before handing it to OVOS. You rarely set these
yourself, but knowing they're there explains a lot of "how did it know that?" moments.
For the Session object and IntentService themselves, see the
OVOS technical manual:
| Key | Value | Purpose |
|---|---|---|
context["peer"] |
Satellite peer ID, e.g. "HiveMindV0.0@127.0.0.1:8222/0" |
Identifies the originating satellite |
context["source"] |
Same as peer |
OVOS origin identifier |
context["destination"] |
"skills" by default |
Prevents message being treated as a broadcast |
context["session"] |
Serialized Session dict |
Per-satellite session state |
context["session"]["session_id"] |
Stable UUID per satellite connection | Maintains conversational state across utterances |
context["session"]["blacklisted_skills"] |
List of skill IDs | Enforced by IntentService |
context["session"]["blacklisted_intents"] |
List of intent names | Enforced by IntentService |
And this is where the reply trick from the BUS section pays off: once OVOS calls
Message.reply(), source and destination swap, so destination now holds the
satellite's peer ID. The per-client OVOS bus subscription wired up in
HiveMindListenerProtocol.handle_inject_agent_msg() reads that one field and the answer
knows its way home.
Protocol version¶
Every connection speaks protocol v3: the Noise handshake
(Noise_XXpsk2_25519_ChaChaPoly_SHA256 by default), giving an always-encrypted,
forward-secret session from an access key and a password. There is no negotiable range of
older versions to fall back to, and no legacy plaintext or pre-shared-key path. A client
that cannot complete the v3 Noise handshake is refused: the server closes the connection
with code 1008 and the reason this node requires protocol v3 (the Noise handshake). See
Security → There is no legacy path.
The ProtocolVersion enum still enumerates ZERO/ONE/TWO/THREE on the wire for
historical reasons, and max_protocol_version still appears in the HANDSHAKE payload —
it is exactly what a v3 client reads to select the Noise handshake. Only THREE is ever
accepted; the earlier rungs of the ladder, and the min_protocol_version floor that used to
gate them, no longer exist. (One footnote to avoid confusion: this ProtocolVersion enum is
a different thing from the binary-serialization PROTOCOL_VERSION constant in
serialization.py.)
Source¶
Validated against the HiveMind source:
hivemind_bus_client/message.py—HiveMessageand theHiveMessageTypeenumhivemind_bus_client/serialization.py— JSON/binary serialization and thePROTOCOL_VERSIONconstanthivemind_core/protocol.py— server/client roles,binarizegating, routing, and theProtocolVersionenum