Messaging Middleware¶
Messaging Middleware is the node-attached application messaging component
that turns Proposal 060 into runtime responsibilities for personal message
admission, outbound contact permission, mailbox indexing, and recovery metadata.
Status: hard-mvp-done
Date: 2026-05-17
Executive Summary¶
Cross-reference to Solution 032 (Local Relationship Layer): With Solution 032, the canonical owner of the
contactsconcept (relationship classes, membership facts, pairwise nym continuity) moves out of this solution. Messaging becomes a consumer of the Local Relationship Layer: it reads activecontactsclass membership for receive consent, triggers membership append through the local host capabilitylocal-relationship.membership.appendoncontact-request.accept, and emitsmessaging-receive@v1based on relationship state.Because the system is still before first release, the Solution 032 compatibility bridge was removed instead of retained. Messaging no longer writes a
contacts_membershipcache and no longer emitscontacts.membership-changed.v1; the canonical fact isrelationship-membership-fact.v1(Solution 032).Messaging may declare relationship-derived
trust_requirementsin its package manifest for autonomous decisions outside the operator loop (e.g. accepting a contact-request from a peer whose operator is already infriendsof the local operator, under bounded scope). Middleware never reads sealed relationship state; it receivesrelationship-policy-decision.v1shapes from the host policy evaluator.
The solution is deliberately stratified:
message-envelope.v1 / contact-request.v1
-> messaging-core domain invariants
-> messaging-service Maildir + SQLite domain runtime
-> daemon host capabilities and authority checks
-> Node UI mailbox and compose surface
The daemon remains the host and authority layer. It owns signing, Capability
Binding, revocation freshness, local participant-handle evidence, notification
actions, Artifact Delivery, Memarium, Pseudonym Vault access, and (via
Solution 032) the canonical Local Relationship Layer. The messaging-service
owns messaging-domain state: Maildir bodies, the hot SQLite index, outbound
queue state, and the bounded Layer 3 messaging-fact stream. contacts
membership read/write goes through the Local Relationship host capability
layer; messaging no longer owns the relationship concept end-to-end.
Scope¶
This solution implements the MVP+ personal messaging path:
- inbound
message-envelope.v1admission through Artifact Delivery; - outbound compose, route lookup, contact permission waiting, and private-direct delivery through host capabilities;
- the canonical
messaging-receive@v1passport profile; - the local contactability draft surface used by Story-010 before Contact Catalog publication is fully automated;
- local participant mailbox resolution through a daemon-owned authority store;
- Layer 1 Maildir bodies, Layer 2 SQLite indexes, and Layer 3 Memarium facts;
- degraded operation when Memarium or host capabilities are temporarily unavailable;
- recovery mirroring for contact membership and receive-passport references;
- a thin Node UI for compose, inbox, outbox, status, and diagnostics.
Out of scope for this hard-MVP are body encryption, HTML rendering, group
messaging, CC/BCC, live multi-device mailbox-state push, and full
multi-device vault merge. Read/unread sync is in scope as replayable
messaging.flag.v1 Layer 3 facts.
Passport Profile¶
messaging-receive@v1 is the canonical receive-consent passport profile. The
solution does not introduce a second shape; it documents the
MessagingReceiveProfileV1 profile already used by node/capability.
capability_id = "messaging-receive"
grant = "messaging/receive"
revocation freshness default = 300 seconds
scope profile:
request/id
sender_subjects
recipient_routes
contact_nym_id
purposes
max_revocation_staleness_seconds
limits?
The messaging acceptor applies three messaging-specific checks after Capability Binding has verified the passport signature and freshness:
- the sender subject in the envelope must match
sender_subjects; - the receiver route must match
recipient_routesandcontact_nym_idwhen the envelope carries one; - the purpose must include
messaging.
The passport may be presented inline or by reference, but the service never owns private keys and never mints the passport directly.
Verifier boundary rule: when capability_id = "messaging-receive", the
capability layer rejects passports that do not contain at least one canonical
messaging-receive@v1 scope profile, and it rejects alternate profile
discriminators such as bare messaging-receive. If
max_revocation_staleness_seconds is omitted by an older producer, the typed
profile defaults it to 300 seconds before evaluation.
Host Capabilities¶
The messaging service consumes host capabilities through the standard supervised module environment:
ORBIPLEX_HOST_CAPABILITY_BASE_URL;ORBIPLEX_HOST_CAPABILITY_AUTH_HEADER;ORBIPLEX_HOST_CAPABILITY_AUTHTOK_FILE.
The following capabilities are part of the solution boundary:
| Capability | Owner | Messaging use |
|---|---|---|
capability.passport.lookup |
Daemon / Capability Binding | Select or verify a usable messaging-receive passport for outbound queue promotion and inbound passport-ref admission. |
local-recipient-mailbox.resolve |
Daemon / local participant authority | Resolve an inbound receiver route and optional public handle to an operator or participant mailbox. |
artifact.delivery.send |
Daemon / Artifact Delivery | Send signed contact-request.v1 via contact lookup and signed message-envelope.v1 via private-direct; outbound envelopes carry classification.v1 so INAC/private routes pass the shared classification egress guard. |
signer.sign |
Daemon / signer | Sign outbound contact requests and message envelopes under the contact-request.v1 and message-envelope.v1 domains granted to messaging-service. |
memarium.write |
Daemon / Memarium | Append bounded Layer 3 messaging facts. |
notification.create |
Daemon / notification center | Notify the operator about newly stored inbound messages. |
identity.routing-subject.create |
Daemon / Pseudonym Vault | Create a private reply route for outbound contact requests. |
identity.messaging-recovery.mirror |
Daemon / Pseudonym Vault | Persist private recovery mirror records for membership and receive-passport references. |
agora.vault.put/list/get/delete |
Agora service / daemon host bridge | Store and recover encrypted generic vault artifacts for recorded messages without exposing message metadata as Agora topic records. |
Storage Model¶
Layer 1 is Maildir. It stores message bodies and serialized accepted envelopes under the node data directory. Bodies are retained until explicit user/operator delete or archive.
Layer 2 is SQLite. It stores mailbox indexes, outbound queue state, pending Layer 3 facts, and recovery replay cursors. It is rebuildable from Layer 1 plus Layer 3 and vault recovery records.
Layer 3 is Memarium. It receives separate fact schemas, not a generic
messaging.fact.v1 artifact:
messaging.passport-issued.v1;messaging.passport-revoked.v1;messaging.retention-decided.v1;messaging.crisis-marked.v1.
If Memarium is unavailable, the service stores facts in pending_facts, reports
degraded, and replays them idempotently when requested.
Recorded messages add a side path, not a fourth messaging storage layer.
message-envelope.v1.recording states that a signed envelope is intended for
encrypted preservation. The messaging service keeps delivery authoritative in
the ordinary outbox/mailbox state and then attempts a best-effort
agora.vault.put of an agora-vault-entry.v1 artifact. Vault failures update
the message's vault.* diagnostics and remain retryable; they do not roll back
message delivery. Replies or forwards to a locally known recorded parent must
carry recording.required = true; otherwise inbound admission refuses before
Maildir write with recording-lineage-required.
Mailbox Resolution¶
Inbound messaging always calls local-recipient-mailbox.resolve; it never
infers a mailbox from contact-nym/id alone.
The daemon owns local public-handle evidence in a store separate from remote
address-book contacts. verified means a fresh evidence reference to a
contact-control passport exists. A local UX projection without that evidence is
only mapped. Node-id or routing-subject delivery without a known public
handle or local participant-handle evidence falls back to the operator mailbox.
When the receiver is a nym or participant id, that identity is the primary routing authority. A public handle supplied with the message may narrow the route only when local evidence proves that the handle belongs to the same participant; otherwise the daemon refuses without giving the sender a user-unknown oracle.
Outbound Flow¶
The outbound queue is deterministic and retryable:
waiting-for-routesends a signedcontact-request.v1throughartifact.delivery.sendwithselector/kind = contact-lookup,lookup/mode = invitation-only, andselector/purpose = contact-request/messaging. The request uses the signer-derived participant id and a fresh routing-subject reply route from the Pseudonym Vault. If Contact Catalog lookup is the only known recipient address,recipient/routeis omitted; the receiving daemon binds the request to its local node id instead of trusting a sender-invented route. Theartifact-delivery-envelope.v1wrapper is labelledclassification.v1witheffective_tier = Community, matching the hard-MVP INAC/private route budget. When acontact-lookup-result.v1arrives for an outbox item, the service promotes only concrete route candidates whoseselected/route.purposescontainsmessaging;no-match,policy-denied, andambiguousare terminal failures, while stale or rate-limited results stay retryable.waiting-for-contact-permissioncallscapability.passport.lookupfor a usablemessaging-receivepassport.ready-for-deliverybuilds and signsmessage-envelope.v1, attaches the passport reference or inline passport, and sends it throughartifact.delivery.sendasprivate-direct. The AD envelope carries the sameCommunityclassification label before leaving the service boundary; message-body privacy and future per-conversation policy remain separate messaging-domain layers.in-flightbecomesdeliveredafter a successful host capability call.- Retryable transport/host failures set
next_attempt_at; terminal schema, conflict, or scope failures becomefailed-terminal.
The service never sends unsigned artifacts. If signer.sign is unavailable,
the queued row remains retryable and the service reports degraded state.
Recovery¶
The recovery boundary is private and host-owned. The messaging service mirrors:
contactsmembership records;messaging-receivepassport references needed to recover receive consent.
The daemon persists these records through
identity.messaging-recovery.mirror in a durable local recovery mirror table.
Local contact and messaging recovery bundles are sealed into
pseudonym-vault.v1; import/root-only startup replay preserves terminal
pairwise mapping states and explicit passphrase replay covers
root+local-passphrase local-contact recovery snapshots. A separate
POST /v1/messaging/reindex rebuilds SQLite mailbox indexes from Maildir and
Layer 3 facts and exposes reindexing through service status.
UI Boundary¶
Node UI owns /admin/messaging. It renders status, contactability draft
settings, provider challenge/redeem controls, compose, inbox, mailbox lists,
read/unread controls, outbox, message detail, pending-facts diagnostics, and
recovery/reindex actions by calling /v1/messaging/* daemon proxies. It does
not duplicate messaging policy logic.
The contactability panel is draft-first. Editing public handles and route
bindings never mutates Contact Catalog state until the user invokes Publish.
Publish now requires contact-control evidence from the attestation flow and
submits a signed contact-claim.v1 admission to the supervised Contact Catalog.
Local contacts are a daemon-owned UX and continuity projection. They may carry
labels, local metadata, and the active pairwise contact-nym mapping used for
operator-facing continuity, but they are not network evidence. The canonical
receive-consent state remains the messaging service's contacts membership plus
the corresponding messaging-receive@v1 passport.
mailbox.open is a host-owned notification action target. It opens
/admin/messaging/messages/{message_id} and may mark the notification handled
or the local read UX state; it does not mutate messaging-domain consent or
delivery state.
Implementation Tracker¶
| ID | Feature | Status | Evidence |
|---|---|---|---|
| S027-001 | Canonical messaging-receive@v1 profile enforcement |
done | Node capability rejects non-canonical messaging-receive profile discriminators and defaults missing max_revocation_staleness_seconds to 300 seconds. |
| S027-002 | Messaging service runtime | done | messaging-service covers inbound accept, outbox, contact-lookup-result promotion, sender-side lookup against a shared remote Contact Catalog provider, receive-passport handoff, private-direct delivery, Maildir/SQLite storage, temporal outbox transaction/event/attempt tables with outbox as the public projection, redacted outbox event snapshots that omit raw recipient handles and subjects, replay-equivalence tests, operator temporal diagnostics endpoints for status/redacted events/correlation/replay-check via daemon proxy, recorded-message lineage enforcement, best-effort Agora Vault storage diagnostics and retryable vault-job replay, kind-specific Layer 3 fact artifacts, pending Memarium replay, recovery mirroring, receiver-side revocation snapshot checks for inline messaging-receive@v1 passports, fail-closed no-host behavior for passport-based first contact, revocation-triggered messaging.passport-revoked.v1, read/unread sync through messaging.flag.v1, reindex with remote Memarium replay + local Layer 3 replay + Maildir + FTS5 rebuild, and strict Story-010 cross-node delivery smoke coverage. Mock-host coverage covers inline revocation, outbound passport lookup, signer, artifact.delivery.send, agora.vault.put, redacted failure classes, and remote replay. |
| S027-003 | Contactability and local contacts | done | Daemon exposes contactability draft/options/attest/publish endpoints, requires contact-control passport evidence at publish time, binds the published owner participant to the draft route or attestation passport subject, signs canonical route-set contact-claim.v1, admits it to the supervised Contact Catalog, validates local-contact.v1 import/export, stores local contact labels/metadata, tracks pairwise mapping lifecycle, and exposes /v1/local-contacts/resolve. Local contact and messaging recovery bundles seal into pseudonym-vault.v1, replay on import/root-only startup, preserve terminal pairwise mapping states, and explicit operator passphrase replay covers root+local-passphrase local-contact recovery snapshots. |
| S027-004 | Contact attestation service dependency | done | Node adds attestation-core, supervised opt-in attestation-service, contact attestation schemas/examples, schema-gate validators, email-attestation / phone-attestation capability ids, local/dev delivery, SMTP email delivery, SMS webhook delivery, attempt limits, challenge TTL, quotas, and delivery audit. Daemon contactability options discover trusted/fresh role/email-attestation / role/phone-attestation providers through Seed Directory, expose provider status in Node UI, and start/redeem challenges through daemon contactability endpoints. Story-010 now uses that runtime path for e-mail-control acquisition. |
| S027-005 | Node UI messaging surface | done | Node UI renders /admin/messaging with contactability draft controls, provider challenge/redeem controls, compose, local-contact based unknown-recipient warning, inbox, read/unread actions, outbox, diagnostics, and message detail. |
| S027-006 | Story-010 acceptance pack | done | node/tools/acceptance/story-010-operator/ provides two-node profile generation, launchers, UI helpers, story-smoke, and self-contained ad-smoke. Strict ad-smoke now defaults to the no-scaffold path: INAC transport is approved through the receiver's operator notification before contact-request AD admission. It also covers Attestation Service challenge/redeem, daemon contactability publish, supervised Contact Catalog admission, shared remote lookup, contact request delivery, operator accept, messaging-receive@v1 passport handoff, private-direct message-envelope.v1 delivery, delivered inbox/outbox state, messaging.flag.v1 read/unread replay, and a second recorded message stored as an encrypted generic artifact in Node B's Agora Vault. The old peer allowlist/preissued transport passport path is retained only as an explicit acceptance debug flag. |
References¶
doc/project/40-proposals/060-messaging-middleware.mddoc/project/40-proposals/061-contact-attestation-service.mddoc/project/30-stories/story-010-message-to-a-friend.mddoc/project/60-solutions/023-artifact-delivery/023-artifact-delivery.mddoc/project/60-solutions/025-contact-catalog/025-contact-catalog.mddoc/project/60-solutions/026-pseudonym-vault-and-key-roles/026-pseudonym-vault-and-key-roles.md