diff --git a/CHANGELOG.md b/CHANGELOG.md index 6377ce0..5e68c9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -220,6 +220,56 @@ version 2: its `{{ … }}` sequences become substitution points, and its ### 2 +- §5.2 — `reply` rewrites `source` to the identifier of the component + producing the reply, whatever shape the source Message's + `destination` had; a producer with no assigned identifier omits + `source` rather than copying one out of `context`, and an absent + `C.source` yields a broadcast reply. The array-`destination` branch + and its RECOMMENDED first-element choice are removed — a single + deterministic rule replaces them, and the addressed round-trips of + the converse, fallback, and common-query specifications depend on + it. §3.2 restated to match; §3.1 gains a worked ping/pong trace + showing why a poll response reaches the asker and not the + utterance emitter. +- §4.1 — session propagation onto derived Messages raised from + SHOULD to MUST, matching how the dependent specifications already + treat it. The blanket no-mutation rule gains a narrow exception: + a component at one of the OVOS-SESSION-2 §2.6 boundaries MAY + mutate the session fields it owns; unowned fields are carried + through unchanged. +- §2, §2.3 — one unknown-key rule for the whole envelope: unknown + top-level keys and unowned `context` keys MUST be ignored and MUST + NOT cause rejection. A `context` key's owning specification MAY + still prescribe rejection for a malformed value of its own key + (OVOS-SESSION-1 §2.5). Malformedness is scoped to parse failures + and wrong value types on defined keys. +- §6.1 (new) — "treat as malformed" defined once: drop, do not act, + do not coerce, do not crash or tear down the transport, SHOULD log, + and no error reply unless the topic's specification prescribes one. +- §2.1.1 — separator hygiene binds only **decomposed** topic shapes, + whose components a consumer splits back out. Shapes **assembled** + for exact subscription are exempt, so a `skill_id` containing a dot + is conformant in `.converse.ping`. A topic-defining + specification states which of the two its shape is; absent a + statement, decomposed. A dispatch topic MUST contain exactly one + `:` and is definable only by a specification in this family; + component-owned contracts are dotted-only. +- §3.4 — routing keys are not credentials: a producer MUST NOT claim + an unassigned identifier, a trust-domain ingress MUST overwrite + `source` with the identifier it assigned the peer, and no consumer + derives an authorization decision from the routing pair. Adds the + layer-2 and assistant-core definitions, per-member equality for an + array `destination`, and the empty-string rule. +- §5.3 — the `response` shorthand defined only where the topic + arithmetic is unambiguous (`T` neither ending in `.response` nor + containing a `:`); askers do not assume `.response` unless + the defining specification says so. +- §5.4 — correlation guidance points at echoing a discriminating + field from the request (OVOS-COMMON-QUERY-1 §6.4, the `query_id`); + topic plus session does not separate parallel requests. +- §2.1 — topic syntax forbids empty segments and leading or trailing + separators. §3.1 walkthrough names derivations in prose. §7 + conformance resynchronised throughout. - §2.1.1 — the topic convention made the single authoritative rule every topic-defining specification inherits: a `:` in a topic marks a **dispatch-shaped** topic assembled from identifiers (canonical diff --git a/msg-1.md b/msg-1.md index 158be34..49df973 100644 --- a/msg-1.md +++ b/msg-1.md @@ -13,7 +13,8 @@ runtime. It covers: object, whose wire shape is defined by OVOS-SESSION-1; this specification fixes only its existence and propagation rule; - the **derivations** that produce a new Message from an existing one - (§5) — `forward`, `reply`, `response`, and the explicit absence + (§5) — `forward` and `reply`, the `response` shorthand, and the + explicit absence of any central correlation mechanism (messages are fully async); - **serialization** rules (§6); - **conformance** (§7). @@ -37,9 +38,9 @@ This specification defines: - the session carrier `session` and its propagation behaviour (§4); its wire shape, field set, and field semantics are owned by **OVOS-SESSION-1**; -- the three normative Message derivations `forward`, `reply`, and - `response`, which propagate or rewrite the routing and session keys - above (§5); +- the two normative Message derivations `forward` and `reply`, which + propagate or rewrite the routing and session keys above, and the + `response` shorthand defined in terms of `reply` (§5); - the explicit non-prescription of any central correlation model — messages are fully asynchronous, askers correlate their own request/response chains if they need to (§5.4); @@ -49,8 +50,10 @@ This specification defines: It does **not** define: - *which* message topics exist — that is the domain of other - specifications and of each component's own - contract; + specifications and of each component's own contract. A component + that names topics under its own contract, without a specification + defining them, is bound by the topic syntax of §2.1 and uses the + dotted form only (§2.1.1); - the shape of a Message's `data` payload — fixed per-topic by the specification that defines the topic; - the **internal shape** of `session` — fields, semantics, @@ -82,7 +85,8 @@ specification is about the former only. ## 2. The envelope -A Message is a **JSON object** with exactly these top-level keys: +A Message is a **JSON object** with no top-level keys other than +these: | Key | Type | Required | Meaning | |------|------|----------|---------| @@ -93,13 +97,21 @@ A Message is a **JSON object** with exactly these top-level keys: Producers **MAY** omit `data` and/or `context` when they would be empty; consumers **MUST** treat an absent `data` or `context` as equivalent to `{}`. Producers **MUST NOT** emit any other top-level -key. A consumer that receives a Message with unknown top-level keys -**SHOULD** treat it as malformed, but **MAY** instead ignore the -unknown keys and process the envelope normally. The asymmetry is -deliberate: strictness belongs on the producer side, where the -defect originates; a hard consumer-side reject would let a single -non-conformant emitter sever otherwise-valid traffic for every -consumer on the bus. +key. + +A consumer that receives a Message carrying top-level keys it does +not know **MUST NOT** reject the Message on that ground alone, and +**MUST** ignore those keys. A Message is malformed only when it +cannot be parsed as a JSON object (§6) or when a key defined here +carries a value of the wrong type — a non-string or empty `type`, a +`data` or `context` that is not a JSON object. Malformed Messages +are handled per §6.1. + +*Informative.* The asymmetry is deliberate: strictness belongs on +the producer side, where the defect originates; a consumer-side +reject on an unknown key would let a single non-conformant emitter +sever otherwise-valid traffic for every consumer on the bus, and +would make every additive envelope extension a breaking change. ### 2.1 `type` @@ -108,7 +120,10 @@ match the syntax: - ASCII letters, digits, `.`, `:`, `_`, `-`; - no whitespace; -- lowercase RECOMMENDED for new topics. +- no empty segment: a topic **MUST NOT** begin or end with `.` or + `:`, and **MUST NOT** contain two adjacent separators (`..`, `::`, + `.:`, `:.`); +- lowercase **RECOMMENDED** for new topics. Dots segment a topic into a readable hierarchy — `assistant.intent.register.keyword`, `XXX.response`. The dot has no @@ -125,31 +140,59 @@ Two topic shapes exist on the bus, distinguished by one character: from identifiers — the canonical shape is `:`, the per-intent handler-dispatch topic. The `:` **is the marker** that a topic addresses a - specific registered handler rather than naming an event. Only a - formal specification **MAY** define a colon-bearing topic shape, - and it **MUST** define the identifier roles on each side of the - `:`. + specific registered handler rather than naming an event. A + dispatch topic **MUST** contain exactly one `:`, so that a + consumer can split it into its two identifier roles without + ambiguity. Only a specification in this family **MAY** define a + colon-bearing topic shape, and it **MUST** define the identifier + role on each side of the `:`. A component **MUST NOT** invent a + colon-bearing topic under its own contract. 2. **All other topics** — events, requests, responses, lifecycle signals — use the dotted form `..` (any depth) and - **MUST NOT** contain `:`. + **MUST NOT** contain `:`. This is the only form available to a + component naming topics under its own contract (§1). Consequently a consumer **MAY** classify any topic by a single test: a `:` anywhere in `type` means a dispatch-shaped topic per the specification that defined that shape; no `:` means an ordinary dotted topic. -**Separator hygiene.** An identifier used as a component of a topic -**MUST NOT** contain the character(s) the topic shape uses +**Decomposed and assembled shapes.** A topic shape built from +identifiers is either: + +- **decomposed** — a consumer is expected to recover the component + identifiers by splitting the received topic string. The dispatch + shape `:` is decomposed: the handler reads + `skill_id` and `intent_name` back out of the topic; or +- **assembled** — the identifier is used only to *build* a topic + string that is then matched by exact subscription. No consumer + splits it, because the identifier is already known to every + participant, carried in `data`, or both. The addressed poll shape + `.converse.ping` (OVOS-CONVERSE-1 §4.2) is assembled: + the owner subscribes to the one topic built from its own + `skill_id`, and the same value is repeated in `data.skill_id`. + +A topic-defining specification **MUST** state which of the two its +shape is. Absent such a statement the shape is decomposed. + +**Separator hygiene.** In a **decomposed** shape, an identifier used +as a component **MUST NOT** contain the character(s) the shape uses structurally: -- in `:`, neither A nor B may contain `:`; -- in `.`, neither A nor B may contain `.`; +- in `:`, neither A nor B contains `:`; +- in `.`, neither A nor B contains `.`; - shapes combining both separators impose both constraints on the components they delimit. +An **assembled** shape imposes no such constraint: an identifier +containing the shape's separator (a `skill_id` such as `wiki.test` +in `wiki.test.converse.ping`) is conformant, because the resulting +topic is never split. The identifier must still yield a topic +matching §2.1. + Each topic-defining specification declares only what its own separators require of its own identifiers; a character is -constrained only where it is structural. +constrained only where it is structural in a decomposed shape. **Recommended identifier form.** When defining a new identifier intended for use as a topic component, prefer values that contain only @@ -184,9 +227,19 @@ The distinction between `data` and `context` is intentional: Other specifications **MAY** define additional `context` keys for their own purposes (GUI routing, security context, tracing identifiers, and -so on). A consumer **MUST NOT** reject a Message because of the -presence, absence, or value of any `context` key; a consumer that does -not understand a `context` key **MUST** ignore it. +so on). A consumer that does not understand a `context` key **MUST** +ignore it, and **MUST NOT** reject a Message because of the presence, +absence, or value of a key it does not own. + +A key's **owning specification** — the specification that defines the +key and its value semantics — **MAY** prescribe how a consumer of +that key handles a malformed value, up to and including dropping the +Message. That prescription binds only consumers acting on that key: +OVOS-SESSION-1 §2.5 defines such a rule for a `session` value that is +not a JSON object, where the carrier itself is unusable. Rejection is +therefore always the owning specification's deliberate choice about +its own key, never a consumer's reaction to metadata it does not +understand. --- @@ -206,25 +259,51 @@ Message crosses the boundary: 1. An **emitter** (a microphone service, a chat UI, a remote client, a test harness) sends an utterance Message and sets `source` to itself. The Message is going *user → assistant*; the assistant - core is the consumer. **A -> B** + core is the consumer. 2. The assistant core classifies the utterance and matches an intent. It dispatches the handler on the per-intent topic - `:` via `.reply`. The Message is now going - *assistant → user*. **B -> A** -3. The handler runs and announces its outcome, preserving the - dispatch's `context` via `.forward`. Observers still see the - emitter as `destination`. **B -> A** + `:` using the `reply` derivation (§5.2): + `destination` becomes the emitter, and `source` becomes the + orchestrator's own identifier. The Message is now going + *assistant → user*. +3. The handler runs and announces its outcome using the `forward` + derivation (§5.1), preserving the dispatch's `context`. Observers + still see the emitter as `destination` and the orchestrator as + `source`. At each step the pair `(source, destination)` answers one question unambiguously: *which side of the boundary is talking, and to whom?* +**A worked round-trip.** The rule of §5.2 — a reply names its own +producer as `source` — is what keeps an addressed poll from bouncing +back at the asker. Take a converse poll (OVOS-CONVERSE-1 §4.2) for a +remote skill reached through a bridge, with the utterance emitted by +a satellite the bridge has stamped `sat-7`: + +| Step | Producer | Derivation | `source` | `destination` | +|------|----------|------------|----------|---------------| +| utterance | satellite, via bridge | origination | `sat-7` | absent (broadcast) | +| `.converse.ping` | converse plugin | `reply` of the utterance | `converse-plugin` | `sat-7` | +| `.converse.pong` | polled skill | `reply` of the ping | `` | `converse-plugin` | + +The pong is addressed to the converse plugin, which asked, and not +to `sat-7`, which did not — because the ping's producer wrote its +own identifier into `source`, and the pong reverses that pair. Had +the ping instead copied the emitter's identifier forward, the pong +would have come back addressed to `sat-7`, and the bridge would have +relayed an internal poll response out to the satellite while the +converse plugin waited for an answer that never arrived. + ### 3.2 `source` `source` — string — opaque identifier of the **producer** of the Message. The emitter sets it on origination; the `reply` derivation -(§5.2) rewrites it on each hop so it always names the current sender. -`forward` (§5.1) preserves it. +(§5.2) rewrites it to the identifier of the component producing the +reply, so a replied Message always names its own producer. +`forward` (§5.1) preserves it, deliberately: a forwarded Message is +a relay of someone else's Message and keeps naming the original +producer. ### 3.3 `destination` @@ -232,6 +311,12 @@ Message. The emitter sets it on origination; the `reply` derivation the intended consumer(s). Absence (or an empty array) means **broadcast** — every subscriber to the topic is an intended consumer. +A producer **MUST NOT** emit an empty string as `destination`, or as +a member of a `destination` array; no identifier is ever the empty +string. A consumer that receives one **MUST** treat that value as +absent — an empty-string `destination` is a broadcast, and an +empty-string array member is ignored. The same holds for `source`. + The bus is not an authorization boundary: a consumer whose identifier is not in `destination` may still observe the Message. `source` and `destination` are **informational metadata** under this specification — @@ -244,18 +329,48 @@ envelope. `source` and `destination` are **opaque strings** from the perspective of this specification. A consumer **MUST NOT** parse or ascribe -structure to their values beyond string equality. How identifiers are -minted (UUID, hostname-derived, etc.) is a deployment +structure to their values beyond string equality. Where `destination` +is an array, the test is per-member string equality: a consumer is an +intended consumer when its own identifier equals any member. How +identifiers are minted (UUID, hostname-derived, etc.) is a deployment concern. -Because the pair cleanly identifies *who is on the external side of -the boundary*, it is the natural attachment point for **layer-2 -systems** that build authentication, authorization, multi-tenant -routing, or remote participation on top of the bus. A typical -layer-2 pattern populates `source` / `destination` with peer -identifiers so a satellite device or a remote client is addressable -on the same bus as a local handler, without the assistant core -itself learning about peers. +**Layer-2 systems.** A *layer-2 system* is a system that composes the +mechanisms of this specification without extending the envelope: it +reads and writes `source`, `destination`, and the fields of `session` +that it owns, and derives whatever behaviour it needs — authentication, +authorization, multi-tenant routing, remote participation — from those +values alone. It adds no top-level key, no derivation, and no +correlation mechanism. The **assistant core** — the components that +classify an utterance, select a handler, and dispatch it — is +unaware of any layer-2 system above it, and remains conformant to +this specification whether one is present or not. + +Because the routing pair cleanly identifies *who is on the external +side of the boundary*, it is the natural attachment point for such +systems. A typical layer-2 pattern populates `source` / +`destination` with peer identifiers so a satellite device or a +remote client is addressable on the same bus as a local handler, +without the assistant core itself learning about peers. + +**Identifiers are not credentials.** `source` and `destination` are +routing keys, not authentication: a Message asserts its `source`, and +nothing in this envelope proves the assertion. Accordingly: + +- a producer **MUST NOT** set `source` to an identifier that was not + assigned to it, and **MUST** omit `source` when it has no assigned + identifier; +- a component that admits Messages into the bus from outside its + trust domain — a bridge, a gateway, any transport terminator — + **MUST** overwrite `source` on every inbound Message with the + identifier it has assigned to that peer, discarding whatever the + peer supplied. It **MUST NOT** trust a peer-supplied `source`, on + the first Message or on any later one; +- a consumer **MUST NOT** treat a matching `source` as proof of + identity, and **MUST NOT** derive an authorization decision from + `source` or `destination` alone. Authorization, where a deployment + needs it, is a layer-2 concern built on evidence this envelope does + not carry. --- @@ -276,16 +391,30 @@ the propagation rule that applies to it across the derivations of §5. ### 4.1 Propagation Producers **SHOULD** set `session` on every Message that arises -within a session; consumers **SHOULD** propagate it onto Messages -derived from it (`forward`, `reply`, `response`) unchanged. Whatever -value the source Message carried for `session` — including an absent -or empty `session` — is preserved by propagation; a downstream -decision keyed on any session field continues to fire for every -derived Message in the same chain. - -A producer **MUST NOT** modify a `session` already present on the -source Message during propagation. Propagation preserves the existing -session unchanged (§5.1). +within a session. A component that derives a Message from another +(`forward`, `reply`, `response`) **MUST** propagate `session` onto +the derived Message. Whatever value the source Message carried for +`session` — including an empty `session`, and including an absence +that no derivation chose to materialize (§5.1) — is carried through; +a downstream decision keyed on any session field continues to fire +for every derived Message in the same chain. + +Propagation is **MUST**, not **SHOULD**, because the specifications +built on this one treat an unpropagated session as a defect rather +than a variation: a derived Message that drops the session detaches +its chain from the conversation it belongs to, and every +session-keyed decision downstream of it silently changes answer. + +**Session mutation.** A producer **MUST NOT** modify a `session` +already present on the source Message during propagation, with one +exception: a component acting at one of the mutation boundaries +enumerated in **OVOS-SESSION-2 §2.6** — transformer, pipeline, and +handler boundaries — **MAY** mutate the session fields **it owns**, +and the derived Message then carries the mutated session forward. +Fields the component does not own **MUST** be carried through +unchanged, whether or not the component understands them. Outside +those boundaries, propagation preserves the existing session +unchanged (§5.1). ### 4.2 The layer-2 picture @@ -307,9 +436,10 @@ via the `session` carrier. Many topics participate in request/response chains, or relay Messages across components. To make those chains **wire-portable** — independent of any one implementation — this specification defines -three normative derivations that produce a new Message from an -existing one, propagating or rewriting the routing keys of §3 and the -session carrier of §4. +two normative derivations that produce a new Message from an existing +one, propagating or rewriting the routing keys of §3 and the session +carrier of §4, plus the `response` shorthand defined in terms of +`reply`. An implementation **MAY** offer the derivations under any names; what matters is that the resulting Message has the shape described. @@ -330,8 +460,9 @@ routing and session field. The forwarder does **not** become the new `source` — the original producer remains named. If the source Message has no `session`, the derivation **MAY** -materialize a default session on the result per OVOS-SESSION-1; -it **MUST NOT** modify a `session` already present. +materialize a default session on the result per OVOS-SESSION-1. A +`session` already present is carried unchanged, subject to the +owned-field exception of §4.1. ### 5.2 `reply(T', D')` @@ -339,31 +470,44 @@ Produces a new Message: - `type` = `T'`, - `data` = `D'`, -- `context` = a copy of `C` with the routing keys of §3 **reversed** - so the new Message is addressed back to `M`'s producer: - - 1. If `C.source` is set, the new context's `destination` is set to - `C.source`. - 2. If `C.destination` is set: - - and is a single string, the new context's `source` is set to - `C.destination`; - - and is an array of strings, the new context's `source` **MAY** - be set to the identifier of the component producing the reply - (typically one of the array entries). Selecting the **first - element** is RECOMMENDED, so that independently written - components converge on the same deterministic choice; the - choice remains implementation-defined, and consumers **MUST - NOT** rely on a particular member being chosen. +- `context` = a copy of `C` with the routing keys of §3 rewritten so + the new Message names its own producer and is addressed back to + `M`'s producer: + + 1. **`destination`.** If `C.source` is set, the new context's + `destination` is set to `C.source`. If `C.source` is absent, + the new context's `destination` is omitted — the reply is a + broadcast, which is the only well-defined behaviour when there + is no asker to name. + 2. **`source`.** The new context's `source` is set to the + identifier of the **component producing the reply** — its own + assigned identifier, not any value read out of `C`. This holds + whatever shape `C.destination` had: a string, an array, or + absent. A producer with no assigned identifier **MUST** omit + `source`, and **MUST NOT** copy `C.destination` or `C.source` + into it. 3. All other `context` keys, including `session` (§4), are preserved unchanged. As with `forward`, if the source Message has no `session`, the derivation **MAY** populate a default session on the result (§4.1). -`reply` is the basis of any "send back to the asker" Message. A -producer that does not maintain `source`/`destination` at all **MAY** -treat `reply` as equivalent to `forward` — the reply will be -broadcast, which is the only well-defined behaviour absent addressing -information. +`reply` is the basis of any "send back to the asker" Message. Its +`source` rule is what makes an addressed round-trip terminate at the +component that opened it: because each hop names itself, the answer +to a reply is addressed to the component that asked, not to whoever +asked *that* component (§3.1). Every request/response round-trip in +the specifications built on this one — the converse and fallback +polls, the common-query contest, the handler dispatch — relies on +that property. A producer that maintains no identifier at all still +conforms: its replies carry no `source`, and the component it +answered addresses its own next Message by broadcast. + +*Informative.* A single rule replaces the older reversal: `source` +comes from the producer, never from `C.destination`. Copying +`C.destination` gave the same answer only in the case where the +replying component was the sole addressee, and gave a wrong or +undefined answer everywhere else — on a broadcast the producer had +no value to copy, and on a multi-addressee Message it had several. ### 5.3 `response(D')` @@ -372,6 +516,21 @@ whose topic is the source topic suffixed with `.response`. Topics defined in other specifications **MAY** rely on the `.response` suffix convention to mark a Message as the answer to a prior one. +The shorthand is defined only where the arithmetic is unambiguous: +`T` **MUST NOT** already end in `.response`, and **MUST NOT** contain +a `:`. A dispatch topic (§2.1.1) has no `.response` counterpart, and +suffixing an answer topic again produces +`.response.response`, which no specification defines. Where either +condition fails, the answering component names the answering topic +explicitly and derives via `reply` instead. + +An asking component **MUST NOT** assume that the answer to a Message +on topic `T` arrives on `T + ".response"` unless the specification +that defines `T` says so. Several specifications name their answering +topic directly — `.converse.pong` answers +`.converse.ping` (OVOS-CONVERSE-1 §4.2) — and the answering +topic is always whatever the defining specification states. + ### 5.4 No central correlation Messages on the bus are **fully asynchronous**. This specification @@ -382,14 +541,25 @@ request/response bookkeeping. What the spec *does* provide is the raw material an asker can use to do its own correlation, if it wants to: -- the response is emitted on `.response` (§5.3); -- `session` (§4) is preserved across `reply` / `response` / - `forward` (§5.1–§5.2), so an asker can match an incoming - `.response` against an outstanding request in the - same `session`. - -Whether to do that, and how, is entirely the asker's -responsibility. Each component (skills, pipeline plugins, external +- the answering topic, which the specification defining the request + topic states (§5.3); +- `session` (§4), which is propagated across `reply` / `response` / + `forward` (§5.1–§5.2), so an asker can narrow an incoming answer + to the conversation it belongs to; +- `data` itself: an answering component **SHOULD** echo back a + discriminating field from the request it answers, so the asker can + pair answer to request without any host bookkeeping. The + common-query contest does exactly this — a skill's answer echoes + the opaque `query_id` it was asked about (OVOS-COMMON-QUERY-1 + §6.4) — and it is the pattern to follow for any topic where + several requests may be outstanding at once. + +Topic and session alone do **not** discriminate parallel requests: +two requests on one topic in one session produce two indistinguishable +answers. An echoed field is what separates them, and a specification +that expects parallel requests **SHOULD** name the field to echo. +Whether to correlate at all, and how, is otherwise entirely the +asker's responsibility. Each component (skills, pipeline plugins, external clients) tracks its own state as needed, keyed on the session identifier (per OVOS-SESSION-1) when it cares about per-channel continuity. Components that need richer discrimination than @@ -412,7 +582,6 @@ layer-2 routing model viable. A Message is serialized as **UTF-8 JSON** per RFC 8259, with the following constraints: -- No comments and no trailing commas (RFC 8259 already excludes both). - Object key order is **not significant**. Producers and consumers **MUST NOT** rely on it. - Numbers **MUST** be finite. `NaN`, `+Infinity`, `-Infinity` are @@ -428,19 +597,54 @@ A consumer that cannot parse a received payload as a JSON object conforming to §2 **MUST** treat it as malformed and **MUST NOT** silently coerce it. +### 6.1 Handling a malformed Message + +"Treat as malformed" has one meaning throughout this specification +and every specification that cites it. A consumer that treats a +Message as malformed: + +- **MUST NOT** act on it — no handler runs, no state changes, no + derived Message is emitted from it; +- **MUST** drop it, and **MUST NOT** repair or coerce it into a + conformant shape by guessing at the producer's intent; +- **MUST NOT** crash, and **MUST NOT** let the fault tear down its + transport or its subscriptions. A malformed Message is a + **per-message producer fault**, never a transport fault; a consumer + that drops its bus connection over one bad Message can be held + offline indefinitely by a single misbehaving producer; +- **SHOULD** log the violation, with enough detail to identify the + producer, so that the defect is fixable; +- **MUST NOT** emit an error Message in reply, unless the + specification defining the topic prescribes one. A malformed + Message often carries no usable routing keys, and an unprompted + error reply to a broadcast is itself a source of bus noise. + +Dropping is silent to the bus and loud to the operator. That is the +intended asymmetry: the defect is reported where it can be fixed, +without a second Message that other consumers must now interpret. + --- ## 7. Conformance ### A **producer** of Messages **MUST**: -- emit a top-level `type` matching §2.1, and no top-level keys - beyond `type`, `data`, `context` (§2); -- give `type` a non-empty string value matching §2.1; +- give `type` a non-empty string value matching §2.1, and emit no + top-level keys beyond `type`, `data`, `context` (§2); - when present, give `data` and `context` JSON-object values - (possibly empty); they MAY be omitted when empty (§2); + (possibly empty); they **MAY** be omitted when empty (§2); - when deriving a Message from another (`forward` / `reply` / - `response`), follow §5; + `response`), follow §5 — in particular, set the `source` of a + `reply` to its own identifier, never to a value read out of the + source Message's `context` (§5.2); +- propagate `session` from a source Message onto every Message + derived from it (§4.1, §5.1–§5.2), mutating only session fields it + owns and only at the boundaries of OVOS-SESSION-2 §2.6; +- omit `source` when it has no assigned identifier, and never claim + an identifier assigned to another component (§3.4); +- overwrite `source` on every Message it admits from outside its + trust domain, when it is a bridge, gateway, or other transport + terminator (§3.4); - emit serialization conformant to §6. A producer **SHOULD**: @@ -448,34 +652,46 @@ A producer **SHOULD**: - set `source` to its own identifier when one is assigned (§3.2); - set `destination` when the Message is targeted at a known consumer (§3.3); -- propagate `session` from a source Message to derived Messages - unchanged (§4, §5.1–§5.2); - when deriving a Message that answers another, use the `.response` - suffix convention of §5.3 so observers can recognize the answer. + suffix convention of §5.3 where it applies, so observers can + recognize the answer; +- echo a discriminating field from the request in any answer it + produces on a topic that may carry parallel requests (§5.4). ### A **consumer** of Messages **MUST**: -- treat a Message that violates §2 (wrong top-level value types, - missing or non-string `type`) as malformed; for unknown top-level - keys the consumer **SHOULD** treat the Message as malformed but - **MAY** ignore the unknown keys (§2); +- treat a Message that violates §2 as malformed — an unparseable + payload (§6), a missing, non-string, or empty `type`, a `type` that + does not match §2.1, or a `data` or `context` that is not a JSON + object — and handle it per §6.1; +- ignore unknown top-level keys, and **MUST NOT** reject a Message on + that ground alone (§2); - treat an absent `data` or `context` as equivalent to `{}` (§2); - tolerate any `context` shape, including an empty object, and ignore - `context` keys it does not understand (§2.3); -- treat the values of `source` and `destination` as opaque (§3.4); - the contents of `session` are opaque to this specification — - consumers consult OVOS-SESSION-1 for the field set and - consumption semantics; + `context` keys it does not understand, without rejecting the + Message over a key it does not own (§2.3); +- treat the values of `source` and `destination` as opaque, comparing + them by string equality only, per member where `destination` is an + array (§3.4); the contents of `session` are opaque to this + specification — consumers consult OVOS-SESSION-1 for the field set + and consumption semantics; +- not treat `source` as proof of identity, and not derive an + authorization decision from the routing keys alone (§3.4); - not require any of `source`, `destination`, or `session` to be present — they are all optional, and a Message without them is well-formed. A consumer **SHOULD**: -- propagate `session` (§4) onto Messages it derives from the received - one; - not rely on a particular order of `data` or `context` keys. +A consumer that owns a `context` key **MAY** prescribe, in the +specification defining that key, how a malformed value of that key is +handled — including dropping the Message (§2.3, OVOS-SESSION-1 §2.5). +A consumer that derives Messages from the ones it receives is a +producer of those derived Messages and is bound by the producer rules +above, including session propagation (§4.1). + ### Non-goals The following are explicitly **outside** this specification and @@ -492,4 +708,11 @@ opaque layer-2 substrate of §3.4 / §4.2. ## See also - **OVOS-SESSION-1** — the wire shape of `session`, its field - set, and consumption semantics. + set, and consumption semantics; the malformed-carrier rule (§2.5) + that this specification's §2.3 carve-out permits. +- **OVOS-SESSION-2** — the session mutation boundaries (§2.6) that + bound the owned-field exception of §4.1. +- **OVOS-PIPELINE-1** — the dispatch topic shape + `:` (§7.1), the decomposed shape of §2.1.1. +- **OVOS-BRIDGE-1** — a layer-2 system built on the routing keys of + §3 and the session carrier of §4.