5. Layer 2: Connection
5.1 Discovery
Nodes MUST advertise via DNS-SD with service type _sym._tcp in
the local. domain. The instance name MUST be the node’s nodeId.
TXT record fields:
| Key | Required | Value |
|---|---|---|
| node-id | MUST | Node UUID |
| node-name | MUST | Human-readable name |
| public-key | MUST | Ed25519 public key (base64url, RFC 4648 Section 5) |
| mmp | MUST (Core Secure) | A comma-separated list of the protocol versions the listener’s handshake speaks, today exactly 2.0: the listener runs the Core Secure handshake (§5.2). Absent on a legacy (pre-2.0) advertisement. See the profile marker below. |
| hostname | SHOULD | Machine hostname |
| room | MUST | Mesh room identifier (Section 5.8), as sent at the handshake (§5.2, NFC-normalized). A receiver reads an absent room as "default" when choosing whom to dial; membership is decided by the handshake regardless. |
One service type for every room. Rooms are not mapped to DNS-SD service types: every node
advertises _sym._tcp and carries its room only in the TXT
room key, so every §5.8-valid identifier — dots,
underscores and all 64 characters — is advertisable (a DNS-SD service name is at most 15 characters
and admits neither, RFC 6335 §5.1; the advertised label prefixes it with _,
RFC 6763 §4.1.2), and two identifiers can never collapse onto one advertised name. A TXT string is at
most 255 bytes per key=value (RFC 6763 §6.1), which a 64-character
room fits. A node SHOULD connect only to advertisements
whose TXT room equals its own. The TXT room is a hint,
never an admission: membership is decided by the authenticated handshake (§5.2, §5.8). It is also
broadcast in cleartext to the whole segment: a room name is not a credential (§5.8.1) and
MUST NOT be relied on as one, and an operator who treats a room name as sensitive SHOULD use an
opaque identifier.
Profile marker. Legacy and Core Secure nodes advertise the same service type, so the TXT record is the
only discovery-time signal that tells them apart. The value of mmp is a comma-separated list of
the protocol versions the listener’s handshake speaks, with no spaces; this specification defines one,
2.0, so today the value is mmp=2.0. A node whose listener runs the Core Secure handshake
(§5.2) MUST list 2.0, and a node MUST NOT list it on an advertisement whose listener does not.
A record with no mmp key is a legacy advertisement. A node ignores versions in the list that it does not
know, and MUST NOT dial a record as 2.0 unless the list contains 2.0. TXT keys are read as RFC 6763
§6.4 says: compared case-insensitively, and when a key occurs more than once, the first occurrence wins.
A Core Secure node SHOULD dial, as Core Secure, only records whose mmp lists 2.0.
It MUST NOT dial a record without the marker under any other profile unless its operator has configured a
Legacy Import route for that nodeId (§17.3): the absence of the marker never selects a profile. On the
listening side, a Core Secure listener MUST close, without retained state, a connection whose first frame is
the legacy one-frame handshake (§5.2.1), and SHOULD bound the work and the logging it
spends on such closes per remote address, because pre-2.0 implementations that dial every _sym._tcp
advertiser (migration note below) reach Core Secure listeners in default.
The marker is a hint like every TXT key, never authenticated (§18.3). Adding it to a legacy endpoint yields
a Core Secure handshake that fails. Stripping it from a Core Secure advertisement hides that node from Core Secure
peers on the segment, which an attacker on the segment can do anyway by suppressing the advertisement; it is a
denial of discovery and never a downgrade, because no profile is chosen from a discovery record. The protocol
version actually spoken is the protocolVersion bound into the
handshake transcript.
Migration (non-normative, except as marked). An implementation that advertises a per-room service
type MAY also advertise _sym._tcp with TXT
room during a transition, and SHOULD drop the per-room type at
its next major version. A 2.0 node SHOULD also browse the per-room service type that implementations derive from its room
(for the SYM reference runtime, _<room>._tcp), where that is a valid
RFC 6335 service name, for as long as it supports migration. The reason is 2.0 nodes that still advertise only a
per-room type: they are otherwise undiscoverable, and the partition is silent. (A pre-2.0 peer is not dialled from
discovery at all once §5.1’s mmp marker applies, so browsing does
not reach it.) Some pre-2.0 implementations (the SYM reference runtime among them) dial every
_sym._tcp advertiser in default
without a TXT filter, and are refused at the handshake by every named-room advertiser. §5.8’s
unconditional mismatch refusal, inbound and outbound, is what makes that safe: an implementation that has
not verified it in both directions MUST NOT advertise _sym._tcp for a
named room. While per-room service types are still advertised anywhere, the room
sym maps to _sym._tcp and is
indistinguishable there from default: it MUST NOT be owned and
SHOULD NOT be used as a room name. Pre-2.0 advertisements, under either service type, carry no
mmp marker, so a 2.0 node that finds one this way dials it only through
a configured Legacy Import route (profile marker, above).
To prevent duplicate connections, the node with the lexicographically smaller nodeId MUST initiate the outbound TCP connection. The other node MUST NOT initiate.
Relay-based discovery. On platforms where mDNS is unavailable (cloud VMs, Windows without
Bonjour SDK), nodes SHOULD use the relay’s relay-peers response
as the discovery mechanism. Implementations SHOULD support both: DNS-SD for LAN,
relay-peers for WAN.
5.2 Handshake
On a direct connection the TCP or WebSocket dialler is the client and the listener is the server. Over a relay, where both peers dial the relay and neither listens, the roles are assigned by nodeId (§5.2.2). They MUST complete this authenticated exchange in order:
client → server client-hello { protocolVersion: "2.0", room, nodeId, name,
identityPublicKey, e2ePublicKey, nonce,
implementation, extensions }
server → client server-hello { same server offer, clientNonce, selectedExtensions,
proof, keyConfirmation }
client → server client-finish { transcriptHash, proof, keyConfirmation }
Only after client-finish verifies: CONNECTED - —
protocolVersionis exactly2.0. Product/package versions belong in the transcript-boundimplementationobject. - —
roomis explicit and NFC-normalized. The default room is the literaldefault; room mismatch closes the connection before peer admission. - —Identity and X25519 public keys are raw 32-byte values encoded as unpadded base64url. Each side contributes a fresh 32-byte random nonce.
- —The signed transcript binds both nonces, nodeIds, names, identity keys, E2E keys, implementation identifiers, room, protocol version, both extension offers and the selected extension set.
- —Ed25519 proofs establish identity-key possession. HMAC-SHA256 confirmations under X25519/HKDF-derived finished keys establish E2E private-key possession.
- —No peer identity, key, role or room membership MUST be pinned before both required proofs validate. Failure closes the connection without retained peer state.
- —When both proofs validate, the peer’s nodeId is bound to its proven identity key (§3.4). If that nodeId is already bound to a different key, the handshake is an identity conflict: the connection MUST be closed with error 1009 and the conflict recorded; nothing else is retained. If it is bound to the same key, the session belongs to that peer and MUST NOT be refused as a duplicate.
- —The listener MUST require
client-hellofirst andclient-finishbefore any non-handshake frame. Timeout is 10,000 ms by default.
The byte-exact transcript, proof, HKDF and key-confirmation constructions are normative in the handshake vector. Any lifecycle role advertised by an extension is a hint only. Authority is resolved from the receiver’s in-force set of signed authority statements (§6.6), never self-declared handshake data.
5.2.1 Core Secure key schedule
The following construction is normative. lp(x) is the ASCII
decimal byte length of x, then :,
then the bytes of x. Every quoted label is an exact,
case-sensitive UTF-8 byte string with no trailing NUL. HKDF is RFC 5869 HKDF-Extract followed by
HKDF-Expand using SHA-256; the full transcript hash is the salt, not a zero or empty salt.
T = handshakeTranscriptV2(clientOffer, serverOffer, selectedExtensions)
TH = SHA-256(T) // exactly 32 bytes
sessionId = lowercaseHex(TH[0..15]) // first 16 bytes
SS = X25519(localPrivateKey, peerPublicKey) // exactly 32 bytes
HKDF32(info) = HKDF-SHA256(IKM=SS, salt=TH, info=UTF8(info), L=32)
clientFinishedKey = HKDF32("mmp-finished-v2 client")
serverFinishedKey = HKDF32("mmp-finished-v2 server")
clientToServerKey = HKDF32("mmp-aead-v2 client-to-server")
serverToClientKey = HKDF32("mmp-aead-v2 server-to-client")
proofPayload(role) = UTF8("mmp-handshake-proof-v2\n") ||
lp(role) || lp(lowercaseHex(TH))
proof(role) = Ed25519-Sign(identityPrivateKey(role), proofPayload(role))
confirmPayload(role) = UTF8("mmp-key-confirm-v2\n") ||
lp(role) || lp(lowercaseHex(TH))
keyConfirmation(role) = HMAC-SHA256(finishedKey(role), confirmPayload(role)) - —
sessionIdis derived directly from the transcript hash; it is not an HKDF output. - —The X25519 shared secret and every finished or traffic key are raw 32-byte values. An invalid peer key or all-zero shared secret MUST abort authentication.
- —The server sends the server proof and server key confirmation in
server-hello; the client sends the client proof and client key confirmation inclient-finish. - —Proofs are unpadded base64url Ed25519 signatures. Confirmations are unpadded base64url HMAC-SHA256 values and MUST be compared in constant time.
- —The two traffic keys feed only their named direction of the sealed envelopes,
cmb-encryptedandcontrol-encrypted(§18.2.1), which share that direction’s sequence. Reversing or reusing a direction key is non-conformant.
Deprecated. The one-frame handshake that pins its own
unproven keys is a Legacy Import/migration protocol and MUST NOT be accepted by Core Secure.
state-sync is also retired: hidden state never crosses the wire.
5.2.2 Handshake over a relay
Over a relay (§4.4) both peers are WebSocket diallers of the relay, so the dialler and listener roles of §5.2 do not exist, and a peer’s arrival, departure and restart are relay events rather than connection events. A relay session is the §5.2 exchange and the §5.2.1 key schedule, unchanged, carried in routing envelopes addressed to the peer (§4.4.4), with the following rules.
- —Roles. The node with the smaller nodeId is the client, comparing the lowercase wire forms (§3.1.1) bytewise — the order §5.1 uses on the LAN. The node with the larger nodeId MUST NOT send
client-helloto that peer over the relay, and a node MUST ignore a relayclient-hellofrom a peer whose nodeId is larger than its own. Both sides apply one order, so there is no simultaneous open to resolve. - —Trigger. When
relay-peerslists a peer that is not offline (§4.4.2), orrelay-peer-joinedannounces one (§4.4.3), the client-role node SHOULD sendclient-helloto it if it holds no relay session with that nodeId. An announcement for a peer it does hold a session with is ambiguous: the peer may have restarted and replaced its own relay connection (§4.4.7), losing the session’s keys, or the relay may simply have announced it again. A node MUST NOT re-handshake on that announcement alone, which would supersede working sessions. It SHOULD instead probe: send apingto that peer, at most once a second per peer. A live peer answerspong; a restarted one holds no session and answersUNKNOWN_SESSION(below), which tells the client to re-handshake. - —Addressing. Every handshake frame and every sealed frame of a relay session MUST be sent in an envelope whose
tois the peer’s nodeId; none is ever broadcast. A node MUST refuse aclient-helloorserver-hellowhose nodeId differs from the envelope’sfrom, and MUST accept a session’s frames only from envelopes whosefromis that session’s peer, arriving on the relay connection the session was established on. - —Timeout. The server-role node MUST require
client-hellofirst andclient-finishbefore any non-handshake frame from that peer, exactly as a listener does. Either role MUST abandon, retaining no state, a handshake that has not confirmed within 10,000 ms of its first frame. The client SHOULD retry with exponential backoff (for example from 1 s, doubling, capped at 60 s) and MUST NOT have more than one handshake in flight per peer on one relay connection. - —Supersession. A handshake that confirms for the same (nodeId, identity key) as an existing session on that relay connection supersedes it, once each side knows the other holds it. The server supersedes when it admits the new session (§5.8.1; in an ungated room, when
client-finishverifies). The client cannot, since itsclient-finishmay have been lost, so: the server MUST send a sealed frame on a relay session as soon as it admits it, and that frame is the mandatorycmb-anchors(§9.4). The client MUST NOT supersede its existing session until a sealed frame other than anerroropens on the new session; if none opens within the handshake timeout, it MUST abandon the new session, retaining no state, and keep the old one. When a side supersedes, it discards the old session’s keys and counters and refuses frames sealed under the oldsessionIdfrom then on. Supersession itself sends nothing. This is how a peer restart recovers. A handshake that proves a different identity key for the nodeId never supersedes: a nodeId keeps one key (§3.4), the new handshake is refused, and the existing session is unaffected. - —An unconfirmed hello changes nothing. A handshake in progress MUST NOT tear down, pause or alter a confirmed session. Only confirmation supersedes; a timeout, a failure or an abandoned handshake leaves the confirmed session exactly as it was, so a forged or replayed
client-hellocosts the peers nothing. - —Teardown. A node MUST tear down every relay session with a nodeId when
relay-peer-leftnames it, and every relay session on a relay connection when that connection closes; tearing down discards the session’s keys and counters. A node that closes a confirmed relay session for any other reason except supersession SHOULD first send, sealed in that session and addressed to the peer, anerrorwith a Close action (§7.2): 1010SESSION_CLOSEDwith the reason inmessage, unless another code names the reason. A sealed error with a Close action ends the session at its receiver too, and the client-role side then begins a new handshake, with backoff, except after 1009, which it does not retry automatically (§7.2). A node whose handshake fails or is refused MAY send 1006 or 1007 in clear; like every clear error it changes nothing at the receiver (§7.1). 1009 is found only after both proofs validate, so it is sent sealed on the session (§5.2). - —Sequence. Sealed frames follow §18.2.1. A relay may drop frames and never retransmits them, so a gap is final: the receiver MUST close the session, and SHOULD first send a sealed 1010
SESSION_CLOSED, and the client re-handshakes; nobody waits for a missing sequence. A frame that fails authentication does not advance the receive sequence and does not close the session, and neither does a replayed one, since the relay can inject and replay frames. A sealed frame naming asessionIdthe receiver does not hold is discarded and answered withUNKNOWN_SESSION(below).
Unknown session. A node that receives, from a relay from, a sealed
frame (cmb-encrypted, or any other sealed envelope) naming a session it does
not hold, or a ping from a from it holds
no relay session with, SHOULD answer with error 1011 UNKNOWN_SESSION:
{ "type": "error", "code": 1011, "message": "unknown session",
"detail": "session:<32 lowercase hex>" } - —
detailnames the session assession:followed by its 32 lowercase hex characters when the triggering frame named one, and is absent when it did not (aping). The error travels in clear, as a routing-envelope payload addressed to the sender: the node holds no session to seal it with. - —A node MUST NOT send it more than once a second per relay
from, and SHOULD also cap its replies across all senders (for example 2 a second, with a burst of 8), which bounds the replies a stream of stale or forged frames can draw. - —The client-role receiver, if the peer is present on the relay, SHOULD begin a new handshake, but only when the error’s
detailnames the session it holds with that peer or it has a probepingoutstanding to that peer, and it is not already waiting for a newer session with that peer to supersede the one named, and no faster than its handshake retry backoff. It MUST keep its existing session until the new one supersedes it, so a peer’s restart produces no peer-left at the application. The server-role receiver ignores it: the client will re-handshake. - —Like every error frame it is informational (§7.2). It MUST NOT tear down a session by itself. Anyone on the relay path can forge it, and the worst it can do is prompt a handshake that only a peer holding the right key can confirm.
Why the session, not the relay, decides. The relay’s view of a nodeId is weaker than a handshake (§4.4.1),
so nothing a relay says can end a session both peers confirmed, except relay-peer-left
and the loss of the relay connection, and those cost only a re-handshake. Everything that would let a third
party end or replace a session — a hello, an error, a forged or replayed frame — either needs the
peer’s keys to confirm or changes nothing.
5.3 Connection State Machine
| From | To | Trigger |
|---|---|---|
| DISCONNECTED | AUTHENTICATING | TCP/WebSocket connect or accept |
| AUTHENTICATING | CONNECTED | Transcript, identity proofs and E2E key confirmations valid within 10,000 ms |
| AUTHENTICATING | DISCONNECTED | Timeout, malformed frame, proof failure, room/version mismatch, or identity conflict (a bound nodeId proving a different key, §3.4) |
| CONNECTED | DISCONNECTED | Heartbeat timeout, TCP close, or error |
Implementations MUST NOT process any non-handshake frame in the AUTHENTICATING state.
A relay session (§5.2.2) passes through the same states. It enters AUTHENTICATING when the client sends, or
the server accepts, client-hello; it becomes CONNECTED on confirmation; and
it returns to DISCONNECTED on timeout, failure, supersession by a newer confirmed session for the same (nodeId,
key), relay-peer-left, the closing of the relay connection, a sequence gap, or a sealed error with a Close action (§7.2).
An existing CONNECTED session stays CONNECTED while a newer handshake with the same peer is AUTHENTICATING.
5.4 Heartbeat
Nodes MUST send a ping frame to each peer if no frame has been received from that peer within the heartbeat interval (SYM reference default: 10,000 ms).
Upon receiving ping, a node MUST respond with pong.
If no frame is received from a peer within the heartbeat timeout (SYM reference default: 120,000 ms), the connection MUST be closed. These defaults are local policy, not interoperability constants.
5.5 Connection Loss and Transport Failover
When a transport connection closes unexpectedly (TCP reset, timeout, OS-level close), the node MUST check whether other transports for the same peer are still active (see Section 4.6 Multi-Transport Per Peer).
- —If other transports remain healthy: the node MUST switch sending to the next highest-priority transport. The peer MUST NOT be removed. No peer-left event is emitted. The node SHOULD log the transport switch.
- —If no transports remain: the node MUST remove the peer from its coupling engine, discard buffered frames, and emit a peer-left event. The node SHOULD attempt re-discovery via DNS-SD.
Unexpected disconnection of a single transport MUST be treated as a transport-level event, not a peer-level event. The peer is only unreachable when all transport paths are exhausted.
5.6 Peer Gossip
After handshake, nodes SHOULD exchange peer-info frames containing
known peer metadata (nodeId, name, wake channels, last-seen timestamps). This enables
transitive peer discovery — a node that has never been online simultaneously with a
sleeping peer can learn its wake channel through gossip from a relay node.
5.7 Wake
Nodes MAY register a wake channel (APNs, FCM, or other push mechanism) via the wake-channel frame.
Peers MAY use this channel to wake a sleeping node when they have a signal to deliver.
Wake requests SHOULD be rate-limited (default cooldown: 300,000 ms per peer).
5.8 Mesh Rooms
A node MUST declare membership in one mesh room at handshake time via the explicit room field (Section 5.2). A room is a named cohort of nodes that exchange application-layer frames only with each other. Rooms let an operator host multiple mutually isolated meshes on the same relay or LAN segment.
Room identifier syntax. A room identifier is a string of [a-z0-9-_.]+, max 64 characters, case-sensitive. The literal string default is explicit; absence is invalid in Core Secure.
Protocol guarantee. A node in room R_A MUST NOT exchange application-layer MMP frames with a node in room R_B when R_A ≠ R_B. Room is part of the authenticated handshake transcript and every signed record assertion. A mismatch fails authentication; application traffic never begins.
Layer placement. A mesh room is a Layer 2 (Connection) concept. The application layer SHOULD declare its room at SDK initialisation. A relay MAY scope routing by room as defense in depth, but endpoint authentication is authoritative: the room is signed in the handshake transcript and bound into each record signature. A receiver MUST reject a frame whose authenticated room differs from its own, even if a relay misdelivers it. LAN peers perform the same check during authentication and close on mismatch with ROOM_MISMATCH.
Recommended naming convention (non-normative). The protocol does not parse room identifiers beyond the character set and length checks above. Operators of meshes with more than a handful of rooms SHOULD adopt a hierarchical dotted-path convention <app>[.<environment>][.<cohort>], e.g. acme.prod, acme.dev, assistants.default, research.lab. The dots are convention only; tooling MAY use them for prefix grouping.
SVAF and room filtering. Authentication and room filtering run before SVAF. A record from a different room never reaches the evaluator. For an authenticated same-room record, category verdicts inform one whole-record admission result (§9.2).
The naming convention above is the complete normative surface; deeper design rationale is runtime documentation, not part of this specification.
5.8.1 Room Admission
Consent to hear is not consent to believe. Admission decides whether a node’s frames are exchanged at all. It says nothing about whether their content is true, and a receiver that treats membership as credibility has confused the two. Every admitted record is still evaluated by SVAF (§9.2), and admission gives it no standing there.
A room name is not a credential. §5.8 makes a room a named cohort, and every discovery path
— LAN advertisement, same-host registry, relay routing — turns knowledge of that name into
membership without asking anyone. A deployment that needs membership to be a right rather than a
string MAY gate a room. A gated room has exactly one owner, identified by node
identifier and Ed25519 public key together. A node that enforces a gated room MUST have the owner’s
nodeId and key pinned out of band, for that room: it selects the owner by the room, never by anything a grant
or a peer carries. The room identifier default MUST NOT be owned: it is the public mesh. Because rooms are carried verbatim and never mapped to a
service type (§5.1), every §5.8-valid identifier denotes exactly one room and any of them other
than default MAY be owned (but see the migration note in
§5.1 on the room sym).
The room-join grant. A node joins a gated room by presenting a grant signed by that room’s owner. The grant MUST bind, under one signature, the room identifier, the grantee’s node identifier, the grantee’s public key, and an expiry. A grant that names a grantee but binds no key is a bearer token: anyone holding the bytes can present it, and the verifier MUST refuse it.
The key must be proven, not asserted. A handshake field stating a public key proves nothing — the key an impostor would have to state is printed inside the grant it is holding. A verifier MUST compare the grant’s bound key against a key the peer proved possession of during the handshake, and MUST refuse when no such proof is available. Failing closed here costs a deployment nothing it had; failing open hands the room to whoever copied a file.
Presenting the grant: the room-join frame. The handshake transcript
has no field for a grant, and a grant carried in a hello would be presented before anything was proven. A grant is
therefore presented inside the confirmed session, where the key it binds can be compared with the key the session
proved. It travels as a sealed control frame (§7.1, §18.2.1):
{ "type": "room-join",
"grant": { "type": "room-join", "room": "<room>",
"grantee": "<grantee nodeId>", "granteeKey": "<43-char base64url>",
"grantedBy": "<owner nodeId>", "grantedAt": 1786611600000,
"expiresAt": 1786698000000, "sigAlg": "ed25519",
"sig": "<unpadded base64url>" } }
signed payload = UTF8("mmp-room-join-v1
") || lp(room) || lp(grantee) ||
lp(granteeKey) || lp(grantedBy) ||
lp(decimal(grantedAt)) || lp(decimal(expiresAt)) - —The grant’s signature.
grant.sigis the owner’s Ed25519 identity-key signature over the payload above, using the §8.8.4lpencoding, and is verified by the one rule of §18.3.2.granteeKeyis the grantee’s identity key as unpadded base64url, andgrantedAtandexpiresAtare integer milliseconds. The frame adds no signature of its own; the session protects it. - —Sending. A node that holds a grant for its room MUST send
room-joinon every newly confirmed session, as its first control frame, after both proofs validate and its own key-registry check of the peer (§3.4) has passed. Both ends of a session between two grantees send one. - —Pending admission. In a gated room, a receiver MUST hold each newly confirmed session as pending, and MUST NOT process any content-bearing frame on it until it is admitted (the per-frame door, below). It admits the owner at once, recognised by the session’s proven nodeId and identity key together matching the pinned owner. Any other session it admits only on a
room-jointhat arrives within the handshake timeout (10,000 ms by default, §5.2), and it MUST close a session that presents none in time. It MUST decide admission on aroom-joinbefore it handles the session’s next frame, so no frame is judged against a decision still being made. - —Verification. The receiver MUST first check the frame against room-join.schema.json, so the times are integers before any signature work, and then verify the grant against the pinned owner key for this room under §18.3.2. It MUST require all of the following:
roomis its own room;grantedByis the pinned owner’s nodeId;granteeis the session’s proven nodeId;granteeKeyis the session’s proven identity key;expiresAtis no more than 24 hours aftergrantedAt(the cap below); and, allowing 5 minutes of clock skew, the grant is already valid (grantedAtis no more than 5 minutes ahead of the receiver’s clock) and not expired (the clock is no more than 5 minutes pastexpiresAt). If every check passes it admits the session; if any fails it closes the session. A grant copied onto another key’s session therefore fails, whoever presents it. - —Expiry closes the session. A session admitted on a grant stays admitted only while the grant lasts: the receiver MUST close it when its clock reaches the grant’s
expiresAt. A grant admitted inside the 5-minute skew allowance after itsexpiresAtis therefore closed at once. The grantee presents a fresh grant on a new session. - —Otherwise ignored. A
room-joinon a session already admitted, or in an ungated room, MUST be ignored.
Expiry is the revocation window. A grant MUST NOT be accepted with a lifetime exceeding 24 hours, and the cap MUST be enforced by the verifier, not only by the issuer: a cap only the issuer honoured is a suggestion, and a receiver that accepts a ten-year grant has no window at all. Revocation is live gossip with no catch-up replay, so a peer that is offline when a revocation publishes never learns of it. The grant’s own lifetime is therefore the true exposure, and this number is that exposure rather than a bound on it.
The door is consulted per frame, not once per greeting. Admission is decided from the handshake and the grant presented after it, but a receiver MUST record that decision and consult it on every frame that carries meaning. A receiver MUST NOT process a record, a message, or any other content-bearing frame from a peer it refused, nor from a peer for which no admission decision was ever made while the room is gated. This is not a restatement of §5.8: an implementation can satisfy the handshake rule and still dispatch content from a node that never greeted it, because frame handling is commonly attached to a transport before, or independently of, the handshake that would have judged it. Handshake frames MUST NOT be gated this way — a grant-holder could then never join. Liveness frames MAY be answered, since a keep-alive tells a refused peer nothing that a closed connection does not.
A door governs a room, not a node’s voice. Admission decides what is exchanged within a room; it does not make a node inaudible outside one. A receiver that also hears a band the room does not scope — a discovery advertisement, a broadcast channel, any transport it shares with non-members — MUST NOT ingest what arrives there as room content, because nothing on that band passed the door. This is not hypothetical: an independent implementation built against this specification was measured on 2026-09-16 emitting an adversarial node’s records to every peer on the local segment without ever joining the room, and its door counted nothing, correctly — the node never knocked. A door that is never approached refuses nothing, and a receiver MUST NOT read that silence as evidence of admission.
What the door cannot do. In an ungated room there is no admission to enforce: declaring the room at handshake is membership, by §5.8. A receiver in an ungated room therefore has no basis to refuse an unknown peer’s frames on admission grounds, and MUST NOT report the absence of grants there as evidence that nobody was admitted — nothing was required. An interface that reports a room’s gating MUST distinguish ungated from not determined; the two call for opposite responses and collapsing them into one false negative is the failure this clause exists to prevent.
Implementation status
The reference runtime implements owner-signed grants with the key binding, the verifier-enforced
24-hour cap, and the per-frame door described above. Two parts of this section are specified but not
exercised: no shipped deployment gates a room, so the refusal paths are covered by tests rather than
by traffic; and, through sym 0.13, the proving handshake that would supply a proven key was not
reachable from the admission path. Because the verifier refuses for want of proof before it
compares keys, a gated room in those versions is shut to everyone — a legitimate grant-holder is
refused for the same reason as a thief presenting a stolen grant. That is fail-closed, and it is the right
direction to fail. sym 0.14.0 completes the §5.2 handshake on every transport and presents the grant after
it in the room-join frame defined above, so the key
binding is reachable there. Revocation
exists only as expiry. Treat the grant mechanism as specified and implemented, the binding as reachable
from sym 0.14.0, and the gated-room deployment as untried.
§5.9–5.11 — Informative
Sections 5.9–5.11 describe an informative design pattern for composing meshes — not a normative wire. The single-mesh protocol (§1–§5.8, §6–§20) is complete and unaffected without it. There is one reference implementation (a gateway prototype) and it realizes only a subset of the pattern (see “Reference implementation” below); the core runtime is single-room. Adopt this as a topology pattern with a stated production-security bar, not as a shipped protocol feature.
5.9 Interior and Boundary (the composition idea)
A mesh presents itself to another mesh as a single node: a gateway. A gateway participates in its own interior room as an ordinary node (§5.2, single room on the wire) and presents a boundary to exterior gateways over a separate transport. It is not one handshake declaring several rooms — interior participation and the exterior boundary are distinct connections.
Admit-then-reproject, not relay (the intended grammar). The pattern forbids forwarding an interior frame outward. Instead a gateway admits its interior cognition through SVAF (§9) and emits a new lossy CAT7 projection (§2.7) outward — its own cognition, signed, carrying its own lineage (§15), never a relayed copy. This preserves the §5.8 guarantee (no interior frame crosses a room boundary) by construction. Implementation status: the reference prototype does not yet do this — see below.
Its own lineage means ONLY its own, and the reason is not tidiness. A projection emitted outward MUST NOT carry a lineage reference to the interior record it was admitted from, even though the exterior could never dereference one. By §8.2.1 a CMB’s key is a content address — a SHA-256 over the canonical serialization, which two conforming implementations compute identically for the same logical block. A reference to an interior record is therefore a hash of that record’s content, published outside the boundary. An exterior observer cannot open it, but can guess a candidate interior content, compute its address, and compare — confirming the interior’s contents without ever being admitted to it. Where the interior’s vocabulary is small or enumerable, that is not a narrow leak but a complete one. The determinism that makes a content address work for lineage, dedup and citation is the same property that makes it an oracle across a boundary, and “the reader cannot dereference it” is the argument for safety that gets this wrong: inability to dereference removes the audit value while leaving the confirmation intact.
The audit trail this appears to sacrifice is not lost, only relocated to where the entitlement is. A gateway SHOULD record, in its own store, which admitted interior observations a given outward projection derived from. That is readable by the gateway and by anyone with interior access — precisely the parties entitled to see both sides — and unlike a pointer the exterior cannot open, it can actually be followed. Where a gateway must later prove such a derivation to a third party, it MAY publish a salted commitment over the interior key rather than the key itself, revealing the salt when it chooses to prove the link; a commitment carries no oracle, because a correct guess cannot be confirmed without the salt.
5.10 The Gateway Node
A node is a membrane over an arbitrary interior. What makes something a node is entirely its boundary behavior: a stable identity (Section 3), a CAT7 projection of its state (Section 8), and a sovereign SVAF admission of others’ projections (Section 9). The interior behind the membrane is unconstrained. Two node types share this one membrane:
| Atom node | Gateway node | |
|---|---|---|
| interior | one agent (mind + store + SVAF) | a sub-mesh (many nodes) |
| identity | its own | its own (it represents the interior) |
| projects | a lossy view of its private state | a lossy view of its interior’s aggregate cognition |
| admits | into its own store | into a boundary policy; MAY re-project inward |
| membership | one room | one interior room + an exterior boundary (Section 5.9) |
A gateway node is an ordinary node whose interior happens to be a mesh; its domain lens (Section 3.1) is “represent my interior.” Because the same emit / admit grammar holds at every scale — agent, team, org, cross-org — the mesh is fractal: any mesh MAY appear as a single node inside a larger mesh.
Relation to Section 3.2. “One agent, one node” is preserved. A gateway is not a shared identity: it has its own nodeId, its own keypair, and its own SVAF. Its interior agents are separate nodes on a separate (interior) room; the gateway participates in that interior room as an ordinary node and presents an exterior boundary (§5.9) — evaluating each side through its own lens, the very property Section 3.2 protects.
No center, per level. The “no center” invariant (Section 2.3) is enforced at each boundary, not as a claim about interiors. A gateway’s interior MAY be organized however it likes — centered or not; that choice does not leak, because only the gateway’s projection crosses. A member of an outer mesh MAY therefore be a gateway over a centered interior while the outer mesh remains center-free. Federation couples meshes; it MUST NOT synchronize them.
5.11 Boundary Behavior
For two gateway nodes A and B,
each a mesh’s membrane, all cross-mesh behavior is the existing grammar applied at the edge:
- —Discovery. Cross-mesh discovery is by invitation or registry, not mDNS (Section 5.1 is LAN-only).
AknowsBas one node-id at one address;Bis a peer, never a visible population. - —Projection. What crosses is
B’s own emissions — its lossy CAT7 projection of what it admitted internally — neverB’s raw interior CMBs. - —Membrane lineage. A gateway’s outward emission is a boundary root: its lineage (Section 15) MUST NOT carry the content-addresses of interior CMBs. An outer node citing it traces to the gateway and no further; the interior is opaque past the membrane, as required by hidden-state locality (Section 2.7). A gateway MAY retain the interior-to-boundary mapping privately, so it can re-project admitted outer cognition inward with correct interior lineage; that mapping MUST NOT cross the outward boundary.
- —Faithful projection. A gateway’s outward CAT7 should be a truthful lossy summary of the interior it represents, not a material misrepresentation. Because summarization is lossy, faithfulness is attestable, not bitwise: the intended design has the gateway sign its projection and record, in an admission attestation (§6.5), the interior verdict aggregate it was derived from — so an outer admitter can weigh the boundary claim by earned authority. This attestation is part of the production security bar and is unbuilt in the prototype.
- —Echo control. A cognition admitted from
Aand projected back towardAshould origin-dedup at the boundary so cross-mesh loops do not amplify. Because a boundary root strips interior lineage, the dedup cannot match interior roots; instead a boundary projection carries its own cross-mesh provenance key (a boundary address, not an interior content-address), and a reprojection cites that key — soAdetects the loop without the interior ever being exposed. (Unbuilt in the prototype; §15.7.1, on which an earlier draft leaned, is itself unimplemented.) - —Partition. If
Bis unreachable,A’s mesh keeps cohering. Each mesh is independently alive; there is no cross-mesh consensus to stall on.
Boundary transport. The boundary is a dumb request/response transport (HTTP in the reference implementation): a gateway POSTs its projection to each configured peer gateway, and the peer ingests it as an opaque cross-mesh observation. The transport carries the projection only; it holds no shared store and performs no admission or routing on behalf of the meshes — a component that grew a shared store or an admission brain would be a center and must not be introduced. Discovery is by configuration (a gateway knows its peers by id + address + credential), not mDNS (§5.1 is LAN-only) and not a registry.
Production security bar
Federation crosses organizational trust boundaries, so a production gateway boundary requires, at
minimum: (1) the projection is a signed cmb- CMB authored by the gateway (§18.3.1), with the
from gateway origin-authenticated by that signature —
never a self-declared, unsigned field; (2) anti-replay (the signed metadata.createdTimestamp plus receiver-side
dedup); and (3) a boundary credential scoped to the boundary — never a full control-plane
token. A boundary that accepts unsigned projections, trusts a self-declared origin, or authenticates with
an admin credential is not safe for cross-org use.
Reference implementation (prototype)
The reference gateway realizes a subset: it computes a lossy summary of its interior’s cognition and HTTP-POSTs it to configured peers, which ingest it opaquely. It does not yet admit inbound projections through SVAF, reproject them inward, sign or attest its projection, or echo-dedup; its projection is a summary object, not yet a schema-valid CAT7 CMB; and it does not yet meet the production security bar above. Treat it as a prototype of the pattern, not a complete or production implementation.
Sections 5.9–5.11 are informative and change no single-mesh contract: they describe how meshes may compose, drawing on the concepts of Sections 2.3, 2.7, 3.2, and 15. 1.0.6 introduced the composition pattern; a normative cross-mesh wire is not claimed — the reference implementation is a prototype and the production-security bar above is a prerequisite, not a shipped guarantee. Every single-mesh node is unaffected.