Expand description
mcpmesh-local/1 protocol types. Shared vocabulary between the daemon and its clients (porcelain, connect proxy, later the host shell). Wire framing is the family NDJSON codec — carried by the caller, not defined here.
Request/response asymmetry: requests are one typed, closed enum (Request);
responses are per-method typed structs deserialized from the JSON-RPC result
Value — Status → StatusResult, RegisterService → an ack, OpenSession →
no JSON-RPC result at all: the socket STOPS being JSON-RPC and becomes a raw
byte pipe.
Additive-only: new fields (capabilities on Hello, groups/user_id on
PeerInfo, device on OpenSession) MUST land as
#[serde(default, skip_serializing_if = ...)] so older payloads still deserialize.
Structs§
- Active
Session - One live mesh session, in a
StreamFrame::Snapshot. Surface-clean:peeris the user_id-or-nickname the audit records carry, never an endpoint-id.opened_atis epoch seconds. - Audit
Record - One audit record — the union of the event classes, and the
recordpayload of aStreamFrame::Event. ONE schema for the on-disk JSONL log and the live stream. Every field beyondts/kindis optional and elided when absent (skip_serializing_if), so each class serializes to just its relevant keys (a session record has nomethod; a trust record has nobytes_out). - Audit
Summary Result - Result of
Request::AuditSummary: LOCAL per-peer / per-service session counts aggregated from this node’s OWN audit log — NEVER transmitted (local-only). Surface-clean: peer names are nicknames / user_ids (NEVER EndpointIds), service names are the registered service names (NEVER transport vocabulary). A “session” is oneSessionOpenrecord.per_peer/per_serviceare sorted ascending by name (deterministic). Tuples mirror kb’sInsightResponse::per_peer_contribution—["bob", 2]on the wire. - Blob
Fetch Params - Params of
Request::BlobFetch: themcpmesh/blob/1ticket and the LOCAL export path. - Blob
Fetch Result - Result of
Request::BlobFetch: the verified hash + byte length written todest_path. Additive-only. - Blob
Grant Params - Params of
Request::BlobGrant: the scope and the flat-namespace principal to grant it to. - Blob
List Params - Params of
Request::BlobList(#84b). ALL optional —blob_list {}still works, which matters because the verb took no params beforeapi_minor20. - Blob
Publish Params - Params of
Request::BlobPublish: the scope to publish into and the LOCAL file to add. - Blob
Publish Result - Result of
Request::BlobPublish: the copyablemcpmesh/blob/1ticket + the blob’s blake3 hash. A ticket/hash here is blob-reference vocabulary (NOT a transport-vocab leak — the same carve-out as the pairing invite line). Additive-only. - Blob
Republish Params - Params of
Request::BlobRepublish(#83): the scope and the blake3 hex to add to it. - Blob
Revoke Params - Params of
Request::BlobRevoke(#62): the scope and the principals to withdraw from it. - Blob
Scope List - Result of
Request::BlobList: the daemon’s scopes. Additive-only. - Blob
Unpublish Params - Params of
Request::BlobUnpublish(#62): the scope and the blake3 hex to remove from it. - Hello
- The first exchange on any
*-local/Nsocket (the family’s hello convention). - Invite
Params - Params of
Request::Invite: the services the minted invite grants. Rejects unknown fields (so{service: "kb"}— a singular typo — is a loud error, not a silently grants-nothing invite), and the daemon additionally rejects an empty/absentserviceslist (an invite that grants nothing is useless — #34). - Invite
Result - Result of an
Request::Inviterequest: the copyablemcpmesh-invite:artifact (the ONE pairing artifact deliberately carved out of the transport-vocabulary blocklist, so this is NOT a transport-vocab leak) plus its absolute expiry in epoch seconds (≤ now + 24h). - Open
Session Params - Params of
Request::OpenSession: thepeer/servicetarget to dial. Both fields are defaultable — an empty target simply fails the dial (a clean-32055error). - OrgJoin
Params - Params of
Request::OrgJoin: the[identity]pin.user_keyis a LOCAL path — the key never crosses the API. - OrgJoin
Result - Result of
Request::OrgJoin— the pinned org id echoed back (surface-clean; the fingerprint is computed porcelain-side from the invite’s org_root_pk). Additive-only. - Pair
Params - Params of
Request::Pair: the copyablemcpmesh-invite:line. Defaultable — an absent field reads as an empty line, which simply fails to decode (a clean pair error). - Pair
Result - Result of a
Request::Pairrequest: the inviter’s suggested nickname (the redeemer’s local name for the new peer) plus the display-only short authentication code (SAS) — a few words the human reads aloud to a second channel to catch a whole-invite forgery / address-swap MITM. The SAS is a pairing-ceremony artifact (like the invite line), NOT a transport-vocabulary leak. - Peer
AddParams - Params of
Request::PeerAdd(reserved/internal — see the variant): a rawendpoint_id(iroh base32) plus the nickname and service allow list to install it under. - Peer
Info - A known peer as reported by
status(nickname only — never the EndpointId). - Peer
Reachability - Advisory reachability of a paired peer (pairing-mode liveness). Surface-clean: a nickname, a
bool, latency/age NUMBERS, the stable
eid:principal (#42), and since #64 the PATH KIND — direct vs relay, plus the relay URL when relayed. Never a socket address, an IP, or a key: the path field says WHICH KIND of route is in use, never where the peer is. - Peer
Remove Params - Params of
Request::PeerRemove: the nickname to unpair. - Peer
Rename Params - Params of
Request::PeerRename: the contact to rename — every device sharinguser_idwhen given, else the single provisionalnicknameentry — and the new nicknameto. - Peer
Services Params - Params of
Request::PeerServices(#52): the peer to query — a nickname, aneid:device principal, or ab64u:user_id. - Peer
Services Result - Result of
Request::PeerServices(#52): the services the queried peer CURRENTLY grants the caller — computed authoritatively on the peer (which owns the truth), always current, only the caller’s own admitted services (never the peer’s full registry). - Presence
Peer - One reachable roster peer device as reported by
status(the advisory presence read). ADVISORY — this is a display convenience, never an authorization surface. Surface-clean: FLAT vocabulary ONLY — auser_id, a humandevice_label, itsroleword, and anonlineboolean. It carries NO EndpointId / pubkey / hash / ALPN or any transport vocabulary. - Recent
Pairing - One recently completed INVITER-side pairing, surfaced by
statusso the inviter’s human can read the short authentication code (SAS) and compare it with the redeemer’s out-of-band — the pairing ceremony is “both humans compare the code”: the redeemer sees it in itsPairResult; this is the inviter’s porcelain surface for the same words. DISPLAY-ONLY ceremony state: held in-memory by the daemon (a small ring), lost on restart, NEVER an authorization input or trust data. Surface-clean: a nickname + the SAS wordlist words + an epoch — never an EndpointId. - Register
Service Params - Params of
Request::RegisterService: the[services.*]entry to write/update. - Roster
Install Params - Params of
Request::RosterInstall: the LOCAL roster filepath, plus the org-root pin on FIRST install (b64u:; omit once pinned — config carries it). - Roster
Install Result - Result of a
Request::RosterInstallrequest (the manual install path): the installed roster’s org id + serial (roster-status vocabulary the confirmation line is permitted to render) plus how many live sessions the install severed. Surface-clean: NO keys / EndpointIds / paths. - Roster
Status - Roster-mode status. Surface-clean roster VOCABULARY only: org_id, serial, a plain state word, and the pinned org-root FINGERPRINT in short words — never raw keys/EndpointIds/serials- as-transport-vocab. Absent in a pure-pairing daemon.
- Scope
Info - One scope in a
BlobScopeList: its name + the hashes it contains + the principals it grants. Flat vocabulary ONLY — no EndpointId/pubkey/ALPN. Additive-only. - Service
Allow Params - Params of
Request::ServiceAllowGrant/Request::ServiceAllowRevoke(#44): toggle a single stableprincipal(b64u:/eid:) on a singleservice’s allow list, WITHOUT unpairing. The per-peer “sharing” switch primitive the embedder drives. - Service
Info - A registered service as reported by
status(no transport vocabulary). - SetApp
Metadata Params - Params of
Request::SetAppMetadata: this node’s opaque app-metadata blob (#39). The daemon NEVER interprets it — the embedder structures its own bytes (a version string, small JSON, …). Capped at 256 bytes;""clears it. Roster-mode only (it rides the signed presence heartbeat); a pure-pairing daemon accepts + stores it but never gossips it. - SetNickname
Params - Params of
Request::SetNickname: this node’s new self-nickname (#37). Display-only semantics: it names this node in FUTURE invites/presentations; peers keep the nickname they stored at pairing time until a re-invite. - SetRelays
Params - Params of
Request::SetRelays(#53): the node’s desired CUSTOM relay set. Declarative — “make the custom relay set exactly this” — applied as a live insert/remove diff against the running endpoint (iroh 1.0.3Endpoint::insert_relay/remove_relay) when the node is already inrelay_mode = "custom", then persisted to[network]. Each entry must parse as an irohRelayUrl; an empty list is rejected (custom mode requires ≥1 relay — fully disabling relays is arelay_mode = "disabled"restart, not this verb). Switching a node that is currentlydefault/disabledonto custom persists the config but needs a restart to take effect (iroh cannot live-transition the relay MODE) — signalled bySetRelaysResult::restart_required. - SetRelays
Result - Result of
Request::SetRelays(#53). - SetRoster
UrlParams - Params of
Request::SetRosterUrl: the HTTPS roster URL to pin. - Status
Result - Unregister
Service Params - Params of
Request::UnregisterService(#50): the persistent (or ephemeral) service name to remove — the deregistration mirror ofregister_service.
Enums§
- Audit
Kind - The event class of an
AuditRecord(the four audit event classes). An additive discriminant on top of the base record schema: it removes no field and makes the JSONL self-describing so a consumer can filter by class without guessing from which optional fields are present. - Backend
Kind - The kind of backend answering a service — the two valid values, enforced at the
type level and kept in lockstep with
BackendSpec’s variants. Status reports the kind only, never the command/path (no transport vocabulary). - Backend
Spec - How a service is answered. Mirrors the config
[services.*]kinds; Config→BackendSpec is a hand-written match, not a serde passthrough. - Peer
Path - HOW a peer is reached (#64): a direct/hole-punched QUIC path, or through a relay.
- Request
- Control-API requests. Serialized as
{ "method": "...", "params": {...} }(JSON-RPC-shaped; the id/jsonrpc envelope is added by the transport layer). - Stream
Frame - One frame of the
Request::Subscribestream (pairing liveness & health telemetry). Tagged ontype(snake_case), so a frame is{"type":"snapshot",...}/{"type":"event",...}/{"type":"lagged",...}.Event.recordis theAuditRecordverbatim, so the stream and the on-disk log carry ONE schema. The daemon serializes these; an embedding consumer deserializes them (seedocs/local-protocol.md“Live event stream”).#[non_exhaustive]: a future frame kind must not break a downstreammatch. AddingReachabilityin 0.13.0 DID break exhaustive matches — which is why that release is a MINOR, perRELEASING.md’s pre-1.0 rule that breaking changes bump the minor. Consumers now write a_ =>arm and later additions are additive for Rust as well as for JSON.
Constants§
- API_
MINOR - The integer MINOR of
API_VERSION— see there. Bumped from 0 to 1 when params validation became strict (#34); to 2 with theset_nicknameverb +StatusResult.self_nickname(#37); to 3 whenallow/grant strings became STABLE principals —b64u:/eid:/roster names, never nicknames (#38); to 4 with theset_app_metadataverb +PresencePeer.meta(#39); to 5 withPeerReachability.meta— pairing-mode app metadata on the probe pong (#40); to 6 withPeerInfo.principal— the peer’s eid: device principal onstatus(#41); to 7 withPeerReachability.principal— the same on reachability rows (#42); to 8 with theservice_allow_grant/service_allow_revokeper-peer access verbs (#44); to 9 covering theunregister_service(#50) /peer_services(#52) / Runenv+cwd(#51) surface that shipped in 0.10.1 without a bump, PLUS theset_relayslive relay-set verb (#53); to 10 whenservice_allow_revoke/peer_removebecame IMMEDIATE — no verb shape changed, but their observable contract did: a revoked principal’s next session is refused even on a connection it already holds, and its live connections are severed. Previously both waited for the peer to disconnect on its own, which is unbounded (#54). A consumer can guard onapi_minor >= 10before telling a user that revocation has taken effect; to 11 whenservice_allow_grant/service_allow_revokegained EPHEMERAL-service support and became strict about an unknown service name — a name in neither the config nor the ephemeral registry now answersERR_NO_SUCH_SERVICEinstead of a silent{}(#55, #69); to 12 with the pushedStreamFrame::Reachabilityliveness transition frame (#58); to 13 withPeerReachability::path— direct-vs-relay attribution on every reachability row (#64); to 14 with therun-backendMCPMESH_PEER_EIDidentity var — the caller’s stable device principal, unconditionally present, so arunserver can scope per caller without keying on a nickname (#60); to 15 with theblob_revoke/blob_unpublishverbs — per-scope withdrawal of a grant and of a published hash, so un-sharing a file no longer requires unpairing the person (#62); to 16 when the app-blob provider became available in PAIRING mode — the blob verbs previously errored on any daemon without an org root key, though their scope gate never needed one (#61); to 17 when the service answer began coming from the LIVE registry rather than config + overlay, so a grant the accept path would refuse is no longer advertised. Three surfaces share that resolver and all changed together:status‘sservices[].allow,peer_services’ name list, and themcpmesh/ping/1probe’sservices. No wire shape changed, only the source of truth — exactly the class of change a downstream cannot see in a type diff (#100); to 18 withblob_republish, so a fetched blob can be re-served and every recipient becomes a source (#83); to 19 with durable blob revocation — an unpublish now survives a later republish via a per-scope withdrawal set, andERR_BLOB_WITHDRAWNdistinguishes “deliberately withdrawn” from “never had it” (#107); to 20 withblob_listfilters + paging AND a DEFAULT limit of 256 scopes (the clamp is 4096) — a daemon with more scopes than that previously answered with everything, and past the 16 MiB frame cap the CLIENT rejected the response as malformed, leaving the caller an opaque failure with no way to page. The connection survived: the control surface carries no strike bound. This is a behaviour change for existing callers, detectable via the newtotal/truncated(#84b); to 21 when a PATH change became a reachability transition —StreamFrame::Reachabilitystopped being an up/down toggle and same-verdict frames became possible (#92); to 22 with a SECOND producer for that frame: a live per-session watcher that pushes when a session’s selected path changes, rather than waiting for a probe, at a cadence probes never had (#92); to 23 whenPeerReachability::rtt_msstopped including the path-settle window — a relayed peer could previously never report under 600ms, so “relayed AND fast” was unreachable by construction (#123); to 24 whenreachablestopped sharing a deadline with path classification — a relayed peer whose pong arrived after ~2.4s was reported OFFLINE while it was answering (#128). - API_
NAME - API_
VERSION - The protocol-compatibility version as
"MAJOR.MINOR", distinct from the crate/stack version. - ERR_
BLOB_ WITHDRAWN - The blob was deliberately withdrawn from this scope (#107). Distinct from
ERR_NO_SUCH_BLOB: that means “fetch it first”, this means “someone un-shared this on purpose —blob_publishfrom the file if the re-share is intended”. - ERR_
NO_ SUCH_ BLOB - The named blob is not held COMPLETE by this daemon (#83,
blob_republish). Distinct fromERR_NO_SUCH_SERVICEbecause the remedy differs: fetch the blob first. - ERR_
NO_ SUCH_ SERVICE - Control-API error code: the named service exists in neither
config.tomlnor the ephemeral registry (#55). Distinct from the generic-32000so a caller can BRANCH on “no such service” instead of parsing a message —service_allow_grant/service_allow_revokepreviously answered{}(success) for an unknown name, which silently included every ephemeral service.
Functions§
- method_
of - Extract the
methodtag from a raw request value without deserializing the whole message. The daemon’s dispatcher uses this: match on the method string, then deserializeparamsper-method — which tolerates omitted / null /{}params for parameterless methods (adjacent tagging rejectsparams:{}on unit variants).