MPP Protocol Reference
- Version: MPP v2
- Audience: Developers implementing a mobrule Pack Adapter in any language.
Breaking change from v1. MPP v2 introduces a mandatory
pull{queue}inbound frame as the credit grant for dispatch, adds aqueuerouting tag to the outboundinvocationframe, narrows the disconnect-requeue scope to in-flight rows only, and rejects legacympp:1envelopes withprotocol_error code=INVALID_FRAME. There is no fallback path: adapters that do not sendpullcannot pump invocations; the bridge does not eagerly dispatch on row insert. MPP v1 is deprecated and unsupported.
1. Overview
Section titled “1. Overview”The Mobrule Pack Protocol (MPP) is the wire format used between the mobrule bridge and a Pack’s Adapter process. The bridge is the platform-owned router that relays Invocations (Pack events that need to run inside the game). The Adapter is community-supplied: it speaks MPP and translates each Invocation into actual in-game effects.
The protocol is line-oriented JSON over TCP. The Adapter connects;
the bridge listens. After a successful authentication handshake the
Adapter must issue a pull{queue} for each queue it is ready to
service; the bridge then dispatches at most one matching Invocation
per outstanding pull, and the Adapter reports the outcome back. This
document is the authoritative reference for the v2 wire format.
Adapters written from this document alone are guaranteed to
interoperate with any conforming bridge.
A complete session, from handshake to the per-credit dispatch loop:
2. Transport
Section titled “2. Transport”- Protocol: TCP. The bridge binds a port (default
7777, configurable via--port); the Adapter is the initiator. - Framing: NDJSON, exactly one JSON object per line. Each frame
is terminated by a single LF byte (
\n,0x0A). CRLF is not emitted by either side; receivers SHOULD tolerate a trailing\rbefore the LF as a safety net but MUST NOT depend on it. - Encoding: UTF-8.
- No length prefix. Parsers buffer until LF and parse the preceding bytes as a single JSON value.
- Max frame size: 64 KiB (65,536 bytes including the trailing
LF). If either side observes a line exceeding this cap, it MUST
close the connection. The bridge SHOULD send a
protocol_errorwithcode = "FRAME_TOO_LARGE"before closing if at all possible. - Forward compatibility: parsers MUST tolerate unknown keys. Future minor versions of v2 may add optional fields; receivers ignore unrecognised keys silently.
3. Versioning
Section titled “3. Versioning”Every frame, in either direction, MUST include:
{"mpp": 2, "type": "<frame-type>"}"mpp"(integer) is the protocol major version. v2 implementations emit and accept only2."type"(string) is the frame discriminator and selects how the remaining fields are interpreted.
JSON objects have no guaranteed key ordering. Senders SHOULD emit
"mpp" and "type" first by convention, but parsers MUST tolerate
any key order.
3.1 Negotiation
Section titled “3.1 Negotiation”The Adapter’s hello MUST carry "mpp": 2. The mpp version is
checked at frame-parse time, before the hello payload is interpreted.
If the value is anything other than 2, the bridge replies with
protocol_error code=INVALID_FRAME and closes the connection. There is
no hello_ack for a version-rejected hello. Legacy mpp:1 envelopes
are rejected by this same path: there is no compatibility shim.
Worked example, successful negotiation:
{"mpp": 2, "type": "hello", "pack_id": "com.example.mypack", "manifest_hash": "sha256:abc123...", "bridge_token": "<raw-token>"}{"mpp": 2, "type": "hello_ack", "ok": true, "broadcaster_login": "streamer_login", "session_id": 0}Worked example, rejected mpp:1:
{"mpp": 1, "type": "hello", "pack_id": "com.example.mypack", "manifest_hash": "sha256:abc123...", "bridge_token": "<raw-token>"}{"mpp": 2, "type": "protocol_error", "code": "INVALID_FRAME", "message": "unsupported mpp version: Some(1)"}Future versions may introduce a counter-proposal field in
hello_ack to advertise a supported downgrade; v2 does not.
4. Frame Inventory
Section titled “4. Frame Inventory”A→B denotes Adapter-to-Bridge; B→A denotes Bridge-to-Adapter.
| Frame | Direction | Lifecycle phase | Purpose |
|---|---|---|---|
hello | A→B | auth | Adapter self-registers |
hello_ack | B→A | auth | Bridge confirms or rejects |
pull | A→B | dispatch | Adapter grants one credit on a queue |
invocation | B→A | dispatch | Bridge sends an Event to the Adapter |
ack | A→B | dispatch | Adapter confirms receipt of invocation |
applied | A→B | dispatch | Adapter reports effect applied in-game |
done | A→B | dispatch | Adapter signals completion (terminal) |
failed | A→B | dispatch | Adapter reports failure (terminal) |
config | B→A | dispatch | Bridge delivers a latched Pack-config value |
heartbeat | A→B | keepalive | Liveness ping; no response required |
log | A→B | telemetry | Adapter emits a log line |
state_write | A→B | telemetry | Adapter writes an adapter-owned Pack-state key |
protocol_error | B→A | error | Bridge signals a protocol violation; some (e.g. bad version, malformed frame) close the connection, others (e.g. UNKNOWN_QUEUE) do not; see §5.12 |
The thirteen frame types listed above constitute the complete MPP v2 frame set.
5. Frame Definitions
Section titled “5. Frame Definitions”In every example below the "mpp": 2 and "type" fields are
mandatory; field tables document the additional payload.
5.1 hello (A→B)
Section titled “5.1 hello (A→B)”{"mpp": 2, "type": "hello", "pack_id": "com.example.mypack", "manifest_hash": "sha256:abc123...", "bridge_token": "<raw-token>"}| Field | Type | Required | Notes |
|---|---|---|---|
pack_id | string | yes | Reverse-DNS Pack identifier. MUST equal the bridge’s loaded Pack. |
manifest_hash | string | yes | Pack Manifest hash in the form "sha256:" + 64 lowercase hex. |
bridge_token | string | yes | Raw bearer token issued out-of-band by the platform during pairing. |
The Adapter MUST send hello as the first frame on a fresh
connection. The bridge MUST receive hello within 10 seconds of
accept; otherwise it closes the connection silently. A second
hello on an already-authenticated connection is rejected with
protocol_error code=INVALID_FRAME (“duplicate hello”) and the
connection is closed.
5.2 hello_ack: success (B→A)
Section titled “5.2 hello_ack: success (B→A)”{"mpp": 2, "type": "hello_ack", "ok": true, "broadcaster_login": "streamer_login", "session_id": 0}| Field | Type | Required | Notes |
|---|---|---|---|
ok | boolean | yes | true on success. |
broadcaster_login | string | yes (ok) | The Twitch broadcaster login the bridge is authenticated as. |
session_id | u64 | yes (ok) | Currently always 0. STDB sessions are identity-keyed, not numbered; the field is a forward-compat placeholder for a future numbered-session scheme. |
persisted | array | no | Replay of durably stored persist Pack-state keys. Omitted when empty. See below. |
On success the bridge server-pins pack_id and
broadcaster_login from its loaded Manifest and session record. The
Adapter is now considered the active Adapter for that session and
MUST issue a pull for each queue it intends to service before any
invocation will be sent.
persisted[] replay. For every Pack-state key declared
persist = true in pack.toml that has a durably stored value, the bridge
includes one { key, value, scope } row:
{"mpp": 2, "type": "hello_ack", "ok": true, "broadcaster_login": "streamer_login", "session_id": 0, "persisted": [{"key": "resets", "value": 3, "scope": "trainer:42"}]}| Field | Type | Required | Notes |
|---|---|---|---|
key | string | yes | The declared persist Pack-state key. |
value | any | yes | The last durably stored value (schema-valid under the key’s current schema). |
scope | string | no | The opaque run-scope tag the Adapter attached on the durable write. Omitted when none. |
The bridge replays unconditionally. The bridge never interprets,
compares, or parses scope: it stores it, echoes it here verbatim, and the
Adapter alone matches each scope against its current run to decide
resume-vs-reset. The Adapter MUST apply persisted[] before its first
state_write, or it would clobber the seeded value with zeros under
last-write-wins. A pack with no persist keys sees no persisted field at
all.
5.3 hello_ack: failure (B→A)
Section titled “5.3 hello_ack: failure (B→A)”{"mpp": 2, "type": "hello_ack", "ok": false, "reason": "manifest_hash_mismatch"}| Field | Type | Required | Notes |
|---|---|---|---|
ok | boolean | yes | false on failure. |
reason | string | yes (!ok) | One of the reason codes below. |
Reason codes:
invalid_token:bridge_tokendoes not match.manifest_hash_mismatch: Adapter’s manifest_hash differs from bridge’s.pack_id_mismatch: Adapter claims a differentpack_id.unpaired: bridge has not completed its pairing handshake with the cloud.
A wrong mpp version does not produce a failure hello_ack: it is
rejected earlier with protocol_error code=INVALID_FRAME (see §3.1).
After sending a failure hello_ack the bridge closes the TCP
connection.
5.4 invocation (B→A)
Section titled “5.4 invocation (B→A)”{"mpp": 2, "type": "invocation", "id": 123, "queue": "default", "event": "give_item", "params": {"item_id": 4, "count": 1}, "username": "ViewerName"}| Field | Type | Required | Notes |
|---|---|---|---|
id | u64 | yes | Unique Invocation identifier. |
queue | string | yes | Routing tag identifying which Pack-declared queue this Invocation belongs to. Matches the [events.<name>].queue value in the Pack Manifest. The Adapter MUST route this Invocation to its internal per-queue handler. |
event | string | yes | Event name as declared in the Pack Manifest. |
params | object | yes | Event parameters; already JSON-Schema-validated by the bridge before dispatch. For an Event declaring viewer input, the bridge may inject the redeemer’s free-text input into one or more declared fields before validation, so treat params as viewer-influenced data rather than purely broadcaster/Pack-controlled. |
username | string | no | The redeemer’s cased Twitch display name (never the lowercase login), for in-game attribution. Omitted when no name resolves (e.g. a manual invoke with no login). |
When the bridge sends an invocation it has already transitioned
the Invocation from pending to dispatched, and a matching
pull{queue} credit has been consumed (see §5.5 and §6). The
Adapter SHOULD respond promptly with at least one of ack,
applied, done, or failed, and MUST wait until the queue’s
configured ready_after state is reached before sending the next
pull for the same queue.
5.5 pull (A→B)
Section titled “5.5 pull (A→B)”{"mpp": 2, "type": "pull", "queue": "default"}| Field | Type | Required | Notes |
|---|---|---|---|
queue | string | yes | Name of the queue the Adapter is ready to service. MUST be a queue declared in the active Pack Manifest. |
Credit-of-1 semantics. Each pull{queue} grants the
bridge permission to dispatch at most one matching Invocation on
that queue. The bridge does not pre-credit and does not multi-pull.
After the Adapter sends pull{queue} and the bridge dispatches a
matching pending Invocation, the Adapter MUST wait until that
Invocation reaches the queue’s ready_after state (applied by
default, or done if [queues.<name>].ready_after = "done") before
sending the next pull for that same queue. Sending an additional
pull for a queue that already has an outstanding (parked) credit
is tolerated: the bridge treats it as a benign refresh and re-parks
the credit rather than rejecting it. Adapters SHOULD still send
exactly one pull per completed Invocation on a queue; relying on
the refresh behaviour is not a substitute for correct credit
bookkeeping.
Per-queue independence. Pulls on different queues are independent: the Adapter MAY have one outstanding pull per declared queue simultaneously. This is how an Adapter pumps multiple queues concurrently while preserving strict serial ordering inside each queue.
Unknown queue. If queue is not declared in the active Pack
Manifest, the bridge replies with
protocol_error code="UNKNOWN_QUEUE" and does not dispatch, but
the connection stays open and the dispatch loop keeps running
(see §5.12).
No fallback. If the Adapter never sends pull{queue}, the
bridge will never dispatch on that queue. Pending Invocations
accumulate indefinitely. This is by design: there is no
eager-dispatch escape hatch.
5.6 ack (A→B)
Section titled “5.6 ack (A→B)”{"mpp": 2, "type": "ack", "id": 123}The Adapter confirms it received the invocation frame. The bridge
logs receipt; the Invocation’s state does not change (it remains
dispatched). ack is informational and does NOT release the
queue’s in-flight slot (see §6).
5.7 applied (A→B)
Section titled “5.7 applied (A→B)”{"mpp": 2, "type": "applied", "id": 123, "result": {"resolved_item_id": 4, "resolved_count": 1}}| Field | Type | Required | Notes |
|---|---|---|---|
id | u64 | yes | The Invocation id this frame relates to. |
result | object or null | yes | Effect-specific result payload. May be null if there is nothing to report. |
The bridge transitions the Invocation from dispatched to applied.
If the queue’s ready_after = "applied" (the default), this also
clears the queue’s in-flight slot, freeing it to dispatch the next
parked or future pull for the queue (see §6).
5.8 done (A→B)
Section titled “5.8 done (A→B)”{"mpp": 2, "type": "done", "id": 123}The Adapter signals completion. The bridge transitions the Invocation
from applied to done. done is terminal; no further frames for
this id are expected or honoured. If the queue’s
ready_after = "done", done clears the queue’s in-flight slot.
5.9 failed (A→B)
Section titled “5.9 failed (A→B)”{"mpp": 2, "type": "failed", "id": 123, "reason": "item not found in inventory"}{"mpp": 2, "type": "failed", "id": 124, "reason": "target already fainted", "refund": true}| Field | Type | Required | Notes |
|---|---|---|---|
id | u64 | yes | The Invocation id this frame relates to. |
reason | string | yes | Human-readable failure reason; recorded verbatim. |
refund | bool | no | If true, signals the originating Cue should be refunded to the viewer. Default false. |
When refund is omitted or false, the bridge transitions the Invocation to
terminal state failed.
refund=true reaches terminal state refunded only when both hold:
- The Invocation’s source is
channel_points. For every other source (bits, subs, raids, manual) the bridge ignoresrefundand settles terminalfailed: there is no upstream redemption to cancel. - The Invocation has not yet been
applied. While the Invocation is pre-applied(pendingordispatched),refund=truemints a refund request and settlesrefunded. Afterapplied, the effect provably ran, sorefund=truesettles terminalfailedinstead. A held event that was dispatched but never applied (e.g. a battle event abandoned when the battle ended) is the caserefund=trueexists for.
On a refund the platform calls the upstream provider’s refund API (Twitch
Helix UpdateRedemptionStatus set to CANCELED for channel-point
redemptions), returning the viewer’s points.
failed, refunded (terminal) always clear the queue’s in-flight slot.
Adapters SHOULD use refund=true only when the Invocation could not be
meaningfully applied (target already in requested state, prerequisite resource
missing, gating impossible). Adapters MUST NOT use refund=true to express
“applied, but partially”: that is applied followed by done or failed
without refund.
5.10 heartbeat (A→B)
Section titled “5.10 heartbeat (A→B)”{"mpp": 2, "type": "heartbeat"}The Adapter MAY emit heartbeat at any time after the handshake. The
bridge does not emit heartbeat (there is no outbound variant). No
response is required. The bridge accepts and logs each heartbeat
but does not currently track staleness or enforce any deadline on
the MPP connection based on it.
5.11 log (A→B)
Section titled “5.11 log (A→B)”{"mpp": 2, "type": "log", "level": "info", "message": "loaded map in 12 ms"}| Field | Type | Required | Notes |
|---|---|---|---|
level | string | yes | One of trace, debug, info, warn, error. |
message | string | yes | Free-form log line. |
The bridge writes log frames to its own debug log; it does not
retain them anywhere the Adapter or platform can query later.
5.11a state_write (A→B)
Section titled “5.11a state_write (A→B)”{"mpp": 2, "type": "state_write", "key": "resets", "value": 3, "scope": "trainer:42"}| Field | Type | Required | Notes |
|---|---|---|---|
key | string | yes | A [state.<key>] declared in the active Pack Manifest. |
value | object/any | yes | Arbitrary JSON; validated against the key’s declared schema (if any). |
scope | string | no | Opaque run-scope tag. Stored + replayed for a persist = true key; ignored for non-persist keys. The bridge never interprets it. |
The Adapter is the writer for a Pack-state key only when that
key’s [state.<key>] writer = "adapter". On receipt the bridge
enforces, in order:
keyis declared in the active Manifest’s[state.*]table, elseprotocol_error code=INVALID_FRAMEand close.- The key’s declared
writerisadapter, elseprotocol_error code=INVALID_FRAMEand close (the frame is structurally valid but the Adapter is not the authorised writer). valuevalidates against the key’s compiled JSON Schema (when the key declares a schema; keys with no schema accept any JSON), elseprotocol_error code=SCHEMA_VIOLATIONand close.
On success the bridge inserts the value into the Pack-state store
keyed by (pack_id, key). There is no outbound acknowledgement
frame: an accepted write is silent, and the snapshot/diff fan-out to
overlay subscribers is the store’s concern, not this frame’s.
When the key is declared persist = true, the accepted value +
scope are additionally durably stored (eventually durable: an async
write, not commit-before-ack) keyed by (pack_id, key), last-write-wins, and
replayed to the Adapter in the next hello_ack persisted[] leg (§5.2). The
durable store survives both Adapter and bridge restarts.
5.11b config (B→A)
Section titled “5.11b config (B→A)”{"mpp": 2, "type": "config", "key": "hardcore_mode", "value": {"enabled": true}}| Field | Type | Required | Notes |
|---|---|---|---|
key | string | yes | A [config.<key>] declared in the active Pack Manifest. |
value | any | yes | The current broadcaster-edited value, schema-valid under the key’s declared [config.<key>].schema. |
config carries the current broadcaster-edited value for one declared
[config.<key>]. The bridge replays one config frame per declared key that
has a known value immediately after hello_ack (after the persisted[] leg),
and pushes a fresh config frame whenever the value changes (a broadcaster
edit in the Dashboard). The value is schema-validated against the key’s declared
schema before it is sent, so the Adapter only ever receives a valid value.
config is latched, not one-shot: the Adapter receives the current value on
every connect and on every change, and may receive the same value more than
once. The Adapter MUST apply each config value idempotently. There is no
acknowledgement frame.
5.12 protocol_error (B→A)
Section titled “5.12 protocol_error (B→A)”{"mpp": 2, "type": "protocol_error", "code": "INVALID_FRAME", "message": "unknown type: 'foo'"}| Field | Type | Required | Notes |
|---|---|---|---|
code | string | yes | Machine-readable error code (see list below). |
message | string | yes | Human-readable detail. |
Codes:
INVALID_FRAME: JSON parse failed, a non-2mppvalue, missingmpp/type, or unknowntype. Also covers a duplicatehelloon an already-authenticated connection, and astate_writeto an undeclared key or to a key the Adapter does not own.UNKNOWN_QUEUE: Adapter sentpull{queue}for a queue not declared in the active Pack Manifest.SCHEMA_VIOLATION: params for an Invocation, or astate_writevalue, failed schema validation.FRAME_TOO_LARGE: line exceeded 64 KiB.
An ack, applied, done, or failed referencing an id the bridge
did not dispatch is not a protocol error: the bridge logs a warning,
ignores the frame, and leaves the connection open.
Not every protocol_error closes the connection. Whether the
connection closes depends on where the error originates:
- Read-loop errors close the connection. A malformed frame or bad
mppversion, a duplicatehello, astate_writethat violates the declared key/writer/schema, andFRAME_TOO_LARGEare all detected in the frame read loop; the bridge sendsprotocol_error(when it can) and closes the TCP connection immediately. - Dispatch-side errors leave the connection open.
UNKNOWN_QUEUE(from an errantpull) and a dispatch-timeSCHEMA_VIOLATION(an Invocation’s params failing schema validation before it reaches the Adapter) are reported the same way but do not close the connection; the dispatch loop keeps running and the Adapter can keep pulling on its other queues.
An Adapter MUST treat any protocol_error as a serious diagnostic and
log it, but the correct reaction (reconnect vs. carry on) depends on
whether the underlying TCP connection is still open.
An Adapter-originated protocol_error (sent A→B in unusual
recovery scenarios) is logged by the bridge and closes the
connection.
6. Dispatch Semantics
Section titled “6. Dispatch Semantics”MPP v2 is pull-driven. Dispatch occurs on a given queue if and only if both conditions hold:
- The Adapter has sent a
pull{queue}whose credit is still outstanding (parked) for that queue. - A
pendingInvocation exists whose queue equals that queue and whosepack_id+manifest_hashmatch the bridge’s server-pinned identity.
The bridge does not dispatch eagerly when an Invocation is
created. A new pending Invocation whose queue has no parked pull
simply waits; it will be picked up the next time the Adapter sends
pull{queue}. There is no fallback path: Adapters that do not pull
do not pump.
Each Invocation walks a fixed state machine. done, failed, and refunded
are terminal; a mid-dispatch Adapter disconnect requeues the in-flight row from
dispatched back to pending:
6.1 Per-queue state
Section titled “6.1 Per-queue state”For each queue the bridge maintains an in-memory record of:
in_flight: set when an Invocation has been dispatched on this queue and not yet cleared; empty otherwise.pending_pull: set when apull{queue}has arrived for this queue and not yet been consumed by a dispatch; cleared on dispatch.ready_after: resolved from the active Manifest’s[queues.<name>].ready_afterat the time of the first pull.
A queue is dispatch-eligible iff in_flight is empty and
pending_pull is set. When both conditions hold and a matching
pending Invocation exists, the bridge dispatches the oldest such
Invocation, marks it in-flight, and clears pending_pull.
6.2 ready_after
Section titled “6.2 ready_after”[queues.<name>].ready_after controls when the bridge clears
in_flight for that queue:
"applied"(default):in_flightclears as soon as the bridge receivesappliedfor the in-flight Invocation. The queue immediately becomes dispatch-eligible if a newpullis parked, even though the Invocation may still progress todoneorfailed. Use this when the Adapter’s “applied” event represents the point at which the next Invocation can safely run."done":in_flightclears only when the bridge receivesdone(orfailed) for the in-flight Invocation. The queue is held strictly until the terminal transition. Use this for queues whose effects must fully complete in-game before the next Invocation begins.
failed (terminal) always clears in_flight, regardless of
ready_after.
6.3 Per-queue serial dispatch
Section titled “6.3 Per-queue serial dispatch”Within a single queue, at most one Invocation is in flight at any time. The credit-of-1 + in-flight gate guarantees this without any extra coordination: while an Invocation is in flight, no subsequent pull on the same queue can be consumed.
Across queues, dispatches are independent. An Adapter with N declared queues can have up to N Invocations in flight simultaneously (one per queue).
6.4 Unknown queue handling
Section titled “6.4 Unknown queue handling”Queue names are not validated when a Cue produces an Invocation; validation happens at bridge dispatch time:
- If a
pendingInvocation names a queue that the bridge’s loaded Manifest does not declare, the bridge marks itfailedwithreason = "unknown_queue: <name>"and does not dispatch it. - If the Adapter sends
pull{queue}for a queue the Manifest does not declare, the bridge replies withprotocol_error code="UNKNOWN_QUEUE"and does not dispatch, but leaves the connection open.
7. Identity Pinning
Section titled “7. Identity Pinning”After a successful hello_ack, the bridge records pack_id and
broadcaster_login as the server-pinned identity for the
connection. These values are taken from the bridge’s loaded
Manifest and its platform session, not from anything the Adapter
sends in subsequent frames.
The Adapter MUST NOT include pack_id or broadcaster_login in
non-hello frames. If the bridge sees those keys in any non-hello
frame, they are silently ignored. This rule prevents a compromised
or buggy Adapter from claiming another Pack’s identity once
connected.
Similarly, queue is a routing tag on invocation and pull
only: the bridge does not honour queue on terminal frames
(ack, applied, done, failed). The bridge resolves the
queue for those frames from its own in-flight map keyed by the
Invocation id.
8. Disconnect Behaviour
Section titled “8. Disconnect Behaviour”When the Adapter’s TCP connection closes (clean FIN, RST, read
error, or protocol_error), the bridge:
- Stops dispatching further Invocations to this Adapter.
- For every queue, requeues only the Invocation currently
in flight (i.e. dispatched-but-not-yet-cleared) back to
pending. Invocations that have already reachedappliedon aready_after = "done"queue are left inapplied: they are NOT requeued. This narrowed scope eliminates the double-application risk that the v1 “requeue everything dispatched” rule had for long-runningready_after = "done"workflows. - Pending Invocations whose queue had a parked pull at disconnect
time stay
pending; the next Adapter connection’s firstpull{queue}will pick them up. - The bridge clears its in-memory per-queue state and resumes listening for a new Adapter connection. State is built fresh on the next hello.
Invocations belonging to other bridge sessions are untouched. There is no timeout-based retry. The bridge does not re-send or duplicate Invocations after dispatch. Resilience is provided exclusively by the narrowed re-queue-on-disconnect mechanism above.
9. Connection Lifecycle
Section titled “9. Connection Lifecycle”Adapter Bridge | ---- hello (mpp:2) -----> | | <---- hello_ack (ok) ------- | bridge server-pins pack_id + broadcaster_login | | | ----- pull (queue=default) -> | bridge parks credit on "default" | <--- invocation (id=42, | bridge: invocation 42 pending → dispatched | queue=default) -------- | in_flight[default] = 42; pending_pull[default] = false | ----- ack (id=42) ----------> | | ---- applied (id=42) -------> | bridge: 42 dispatched → applied | | ready_after=applied → in_flight[default] = None | ------ done (id=42) --------> | bridge: 42 applied → done (terminal) | ----- pull (queue=default) -> | bridge parks next credit | | | ----- heartbeat ------------> | | ---- log (info, "...") -----> | bridge writes to its own debug log | | | [TCP close] | bridge requeues ONLY in-flight rows | | per queue → pending9.1 Invocation lifecycle (for cross-reference)
Section titled “9.1 Invocation lifecycle (for cross-reference)”pending ──► dispatched ──► applied ──► done (terminal)pending ──► dispatched ──► applied ──► failed (terminal; refund=true ignored post-applied)pending ──► dispatched ──► failed (terminal; refund=false, or non-channel_points)pending ──► dispatched ──► refunded (terminal; refund=true, channel_points, pre-applied)pending ──► failed (validation-fail before dispatch, e.g. unknown_queue)The bridge owns every transition. The Adapter never talks to the
platform’s database directly. The refunded state is reached only via
a failed frame carrying refund: true, and only when the Invocation
is a channel_points source still pre-applied (§5.9). A refund=true
on any other source, or after applied, settles terminal failed.
10. Error Handling
Section titled “10. Error Handling”| Scenario | Action |
|---|---|
| Inbound JSON parse error | Bridge sends protocol_error code=INVALID_FRAME, closes connection. |
Frame missing "mpp" or value ≠ 2 (hello or any later frame) | Bridge sends protocol_error code=INVALID_FRAME, closes connection. The version is checked at parse time, before the payload, so the hello path is no exception. |
pull{queue} referencing a queue not in the active Manifest | Bridge sends protocol_error code=UNKNOWN_QUEUE; connection stays open, dispatch loop keeps running. |
ack/applied/done/failed referencing an id this connection did not dispatch | Bridge logs a warning, ignores the frame, leaves the connection open. |
| Bridge-side schema validation fails before dispatch | Bridge marks the Invocation failed with reason = "schema_validation: <details>" and sends protocol_error code=SCHEMA_VIOLATION; connection stays open. The Adapter never receives the invocation frame for this row. |
| Pending Invocation naming a queue not in the active Manifest | Bridge marks it failed with reason = "unknown_queue: <name>"; no invocation is emitted. |
Duplicate hello on an already-authenticated connection | Bridge sends protocol_error code=INVALID_FRAME, closes connection. |
Adapter sends protocol_error | Bridge logs the frame and closes the connection. |
| Frame exceeds 64 KiB | Receiver closes the connection; bridge SHOULD send protocol_error code=FRAME_TOO_LARGE first. |
state_write to a key not declared in the active Manifest, or to a key the Adapter does not own (writer ≠ adapter) | Bridge sends protocol_error code=INVALID_FRAME, closes connection; no store write. |
state_write value fails the key’s compiled schema | Bridge sends protocol_error code=SCHEMA_VIOLATION, closes connection; no store write. |
11. Security & Threat Model (informative)
Section titled “11. Security & Threat Model (informative)”MPP v2 is designed for localhost deployment: the bridge binds
to a local TCP port and the Adapter runs on the same machine. The
bridge_token is a shared secret of approximately 32+ bytes. The
following are explicit choices, not oversights:
- No TLS: the assumption is loopback transport. Operators who expose the bridge port off-host are responsible for tunnelling it themselves.
- Token comparison is a plain hash equality (
sha256(received) == stored_hash). Constant-time comparison is not specified because the threat model precludes a meaningful timing-attack adversary. - The bridge ignores
pack_id/broadcaster_loginoutsidehelloprecisely to limit damage from a hostile Adapter. - Queue names are capped at 64 bytes at ingress and validated against the loaded Manifest at bridge dispatch; a hostile or buggy caller cannot inflate storage by injecting arbitrary queue names.
12. Version History
Section titled “12. Version History”| Version | Status | Notes |
|---|---|---|
| v2 | Current | Adds pull{queue} credit grant + invocation.queue routing tag; narrows disconnect requeue to in-flight per queue; rejects mpp:1. Breaking; no v1 fallback. |
| v1 | Deprecated | Initial release. Eager dispatch; single-slot serial dispatch; no queue concept. Unsupported as of v2; mpp:1 envelopes are rejected with protocol_error code=INVALID_FRAME. |
See also
Section titled “See also”- Adapter tutorial: Python: walks through using these frames.
- pack.toml reference: the Manifest the bridge pins per session.