Proposal 027: Middleware Peer-Message Dispatch¶
Status¶
Accepted for MVP.
Problem¶
Orbiplex Node already has three middleware integration shapes:
- host-owned workflow envelope execution through
POST /v1/module-dispatch/execute, - local middleware gate decisions through
POST /v1/middleware/invoke, - built-in in-process peer protocol handlers through the daemon-owned
PeerMessageHandlerchain.
Without one bridge between the peer message chain and local sidecars, every new peer-facing capability must either:
- be compiled into the daemon as a Rust handler,
- or open a parallel network surface outside the authenticated peer session.
That would grow the trusted daemon core and duplicate transport channels.
Decision¶
Middleware modules attach to transport-defined host chains, not to domain-labeled router branches.
The preferred registration surface is input_chains in
middleware-module-report.
For the peer path the relevant chains are:
pre-inputinbound-peerpre-sendaudit
Modules may still use the older peer-only compatibility fields:
handles_peer_message_typespeer_message_filterpeer_message_phase
but the daemon projects those into the transport-defined chain model.
For each claimed msg kind, the daemon may forward the inbound peer message to
the module through the existing loopback middleware invoke endpoint:
POST /v1/middleware/invoke
The daemon sends a new host-owned request envelope:
peer-message-invoke.v1
with fields:
schema_versionenvelope_kind = "peer-message"msgchain_kindcorrelation_idremote_node_idpayload
The module responds with the existing MiddlewareDecision contract.
Each registration may also override the executor-level invoke endpoint through
optional input_chains[].invoke_path. When absent, the daemon reuses the
default http.invoke_path.
Pre-Dispatch Filtering¶
input_chains[].filter is compiled by the daemon once, at middleware
registration time. The compiled predicate is evaluated before any loopback HTTP
call is made.
Supported predicate nodes:
{ "msg": "..." }{ "field": value }{ "AND": [ ... ] }{ "OR": [ ... ] }{ "NOT": { ... } }
Semantics:
msgmatches the peer envelope kind itself,- every other field matches
payload[field], - comparison is exact JSON equality.
Evaluation order:
pre-inputmiddleware runs first,- built-in Rust handlers get next chance,
inbound-peersidecar routes are considered only ifmsgappears in the claimedmessage_types,- if present, the compiled
filteris evaluated locally, - only then does the daemon call
/v1/middleware/invoke, - response traffic may then flow through
pre-send, auditruns after send as fire-and-forget observation.
This keeps the sidecar path cheap for hot sessions while still letting module authors describe domain-local interest declaratively.
The same projection mechanism now also feeds inbound-broadcast and
inbound-local; this proposal remains peer-focused, but the runtime no longer
special-cases peer registration as a one-off shape.
Outbound counterpart¶
The node now also carries one generic outbound host capability:
peer.message.dispatch
This is not a new public transport surface. It is one local host-owned write contract that lets middleware ask the daemon to send one peer message either:
- to one explicit
node_id, or - to one capability-selected peer set resolved through Seed Directory.
The outbound contract intentionally separates:
- candidate selection:
selection_mode = first-n | last-nlimit- delivery semantics:
delivery_mode = one | all
That avoids overloading one field with both "which targets are considered" and "how many successful sends are required".
Capability-routed dispatch may also carry:
- optional
seed_filterusing the same JSON predicate tree shape as inbound middleware filters, - optional
cache_ttl_msfor discovery-result memoization, execution_mode = async | blocking.
seed_filter is evaluated against the Seed Directory entry JSON itself, so
there is no special msg meaning on that path; dotted paths such as
passport.capability_id are the intended form.
Decision Semantics For Peer Messages¶
In peer-message context only three decisions are meaningful:
return- the module handled the message,
patchis used as the payload of the peer response sent back to the remote node,drop- the module handled the message,
- no peer response is sent,
allow- the module does not claim the message,
- the daemon continues to the next peer handler in the chain.
Other decision kinds remain valid in the generic middleware contract, but the daemon treats them as unexpected for peer-message dispatch and falls through.
Ordering In The Chain¶
The daemon-owned peer message chain is ordered:
pre-inputmiddleware,- built-in capability handlers,
- middleware sidecar handlers on
inbound-peer, pre-sendmiddleware,auditmiddleware.
This preserves a small trusted core while still allowing decorating middleware to run deterministically before terminal handlers claim a message.
Rust Middleware¶
This proposal does not force all middleware through loopback HTTP.
Rust-native middleware can still implement the daemon-owned PeerMessageHandler
trait directly and join the chain in-process. The HTTP bridge exists for
supervised local sidecars, not as the only extension surface.
Operational Boundaries¶
- The session worker currently runs in a dedicated
std::thread, so the loopback call usesreqwest::blocking. - Sidecars claiming peer messages must respond within the configured local HTTP timeout budget.
- Sidecars should treat
remote_node_idas an informational trust input, not as an authority stronger than the already-authenticated peer session.
Consequences¶
Positive:
- one authenticated peer channel for network-ledger, offer-catalog, and future middleware-backed capabilities,
- no extra public TCP listeners,
- a smaller daemon core with a stable extension seam,
- compatibility with existing supervised HTTP middleware modules.
Additional positive effect:
- one sidecar can cheaply subscribe to one narrow payload family without
re-parsing and rejecting every matching
msginside its own request handler.
Trade-off:
- a slow sidecar can temporarily head-of-line block one session worker thread.
This is acceptable for MVP and bounded local handlers such as catalog-listener.