5. Connection (Layer 2) — MMP Specification

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:

KeyRequiredValue
node-idMUSTNode UUID
node-nameMUSTHuman-readable name
public-keyMUSTEd25519 public key (base64url, RFC 4648 Section 5)
hostnameSHOULDMachine hostname
roomMAYMesh room identifier (Section 5.8). Default "default" if absent.

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

The TCP or WebSocket dialler is the client; the listener is the server. 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
  • protocolVersion is exactly 2.0. Product/package versions belong in the transcript-bound implementation object.
  • room is explicit and NFC-normalized. The default room is the literal default; 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.
  • The listener MUST require client-hello first and client-finish before 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 signed role-grant chain, 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))
  • sessionId is 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 in client-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 cmb-encrypted envelope. 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.3 Connection State Machine

DISCONNECTED
initial state
TCP connect / accept
AUTHENTICATING
10s timeout
client-finish + both proofs valid
CONNECTED
peer registered, frames routed
timeout / close
DISCONNECTED
peer removed, re-discover
FromToTrigger
DISCONNECTEDAUTHENTICATINGTCP/WebSocket connect or accept
AUTHENTICATINGCONNECTEDTranscript, identity proofs and E2E key confirmations valid within 10,000 ms
AUTHENTICATINGDISCONNECTEDTimeout, malformed frame, proof failure, room/version mismatch or duplicate nodeId
CONNECTEDDISCONNECTEDHeartbeat timeout, TCP close, or error

Implementations MUST NOT process any non-handshake frame in the AUTHENTICATING state.

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. The room identifier default MUST NOT be owned: it is the public mesh. An identifier that does not survive the room↔service-type round trip MUST NOT be owned either, because two such names can denote one room and an “owned” one would silently be the public square.

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.

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, 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 the proving handshake that would supply a proven key is not reachable from the admission path. The consequence is worth stating plainly rather than leaving to be discovered: because the verifier refuses for want of proof before it compares keys, a gated room in that runtime is currently 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, but it means the key binding described above is specified and implemented without being reachable, and no measurement of a refusal in that runtime can yet distinguish the binding working from the door being closed. Revocation exists only as expiry. Treat the grant mechanism as specified and implemented, the binding as not yet exercisable, 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 nodeGateway node
interiorone agent (mind + store + SVAF)a sub-mesh (many nodes)
identityits ownits own (it represents the interior)
projectsa lossy view of its private statea lossy view of its interior’s aggregate cognition
admitsinto its own storeinto a boundary policy; MAY re-project inward
membershipone roomone 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). A knows B as one node-id at one address; B is a peer, never a visible population.
  • Projection. What crosses is B’s own emissions — its lossy CAT7 projection of what it admitted internally — never B’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 A and projected back toward A should 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 — so A detects 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 B is 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.