diff --git a/CHANGELOG.md b/CHANGELOG.md index 8585e84..fc3b2a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -121,3 +121,19 @@ tool does not recognize the token and cannot expand the template. consumer-side `ovos.mic.listen` row (defined in OVOS-AUDIO-1 §4.4). - See-also — cross-references OVOS-AUDIO-1 §4.4 as the defining spec for `ovos.mic.listen`. + +## OVOS-BRIDGE-1 — Bus Bridge and Opaque Relay + +### 2 + +- The bus bridge: a participant that terminates an external channel and + relays Messages between the internal bus and remote participants. §3 — + the normative core: inbound identity stamping (`source`), outbound + routing by `destination` / `session_id` / `site_id`, `site_id` + assignment, and the relaying vs managing session-preservation modes. + §4 — emergent patterns over MSG-1 + SESSION-1/2 + PIPELINE-1 + + TRANSFORM-1 + CONTEXT-1 + INTENT-4 at a bus boundary: policy injection, + multi-deployment topologies, out-of-utterance `ovos.session.sync`, and + satellite skill registration. §5 ordering guidance; §6 conformance. +- §3.3 — `site_id` assignment is owned here; OVOS-SESSION-1 §3.3 carries + the registry pointer and the orchestrator-pipeline consumer constraints. diff --git a/appendix/divergences.md b/appendix/divergences.md index b922541..7b93cfb 100644 --- a/appendix/divergences.md +++ b/appendix/divergences.md @@ -63,19 +63,18 @@ and needs no implementation change: The following topics exist in current ovos-core but are **not defined by any spec** and should be removed or replaced: -- **`ovos.session.sync` / `ovos.session.update_default`** — - emitted by `SessionManager` to broadcast the current default - session to interested components. SESSION-2 §6.4 acknowledges - that an orchestrator MAY emit default-session state on a - deployer-defined topic but assigns no normative name. These - ad-hoc topics should be retired: any component that needs the - default-session state can subscribe to `ovos.utterance.handled` - (PIPELINE-1 §9.5) and read the session it carries, or listen - to any other assistant-emitted Message on the default session. - A named sync topic adds an implicit state-broadcast contract - that the specs deliberately avoid; clients are expected to - track session from Message flow, not from dedicated sync - broadcasts. +- **`ovos.session.update_default`** — + emitted by `SessionManager` in legacy code to broadcast the + current default session. SESSION-2 §5.4 acknowledges that an + orchestrator MAY emit default-session state on a deployer-defined + topic but assigns no normative name. This ad-hoc topic should be + retired: any component that needs the default-session state can + subscribe to `ovos.utterance.handled` (PIPELINE-1 §9.5) and read + the session it carries, or listen to any other assistant-emitted + Message on the default session. See §5.7 for the migration mapping. + Note: `ovos.session.sync` serves a distinct purpose — explicit + out-of-utterance state sync — and is now formalized by + SESSION-2 §2.7; see §5.5 for its entry as a new topic. ### 5.3 Prescriptive shape changes @@ -209,6 +208,13 @@ defined by any spec** and should be removed or replaced: `reply` / `response` (MSG-1 §4.3). Formalizes a "MAY" convenience for in-process subsystems; not currently implemented but compatible with current behaviour. +- **`ovos.session.sync`** (SESSION-2 §2.7). Explicit + out-of-utterance session-state sync emitted by a component that + has mutated session state outside the normal utterance lifecycle + and needs the change propagated. No current spec-conformant + equivalent — `ovos.session.update_default` (now retired, see + §5.2.1) served an overlapping purpose for the default session + only. `ovos.session.sync` is generalised to any session. ### 5.6 Things the specs do *not* change @@ -263,6 +269,7 @@ a number of legacy names. Implementer migration aid: | `ovos.utterance.handled` | **unchanged** — kept as the universal end-marker. | | `:` | **unchanged** — dispatch topic; a plugin-bundled handler has `skill_id == pipeline_id`. | | `mycroft.skill.handler.start` / `.complete` / `.error` | renamed to `ovos.intent.handler.start` / `.complete` / `.error` | +| `ovos.session.update_default` | **retire** — subscribe to `ovos.utterance.handled` (PIPELINE-1 §9.5) to read updated default-session state; or to any assistant-emitted Message on the default session. See §5.2.1. | #### Out of scope @@ -280,3 +287,22 @@ a number of legacy names. Implementer migration aid: | `recognizer_loop:record_end` | `ovos.listener.record.ended` | Capture end; pairs with the start signal. | | `recognizer_loop:sleep` | `ovos.listener.sleep` | Controller-to-listener sleep request. | | `mycroft.awoken` | `ovos.listener.awoken` | Sleep→awake transition; moved into the `ovos.listener.*` namespace. | + +### 5.8 Bus bridge (BRIDGE-1) + +- **BRIDGE-1 defines source-stamping and destination-based routing; + current HiveMind bridges route primarily by session_id.** + HiveMind groups messages by `session_id` and delivers them to the + peer that owns that session. BRIDGE-1 prescribes `destination` as + the primary signal because two peers sharing the same `session_id` + (including `"default"`) cannot be distinguished by session_id + alone. HiveMind deployments that use per-peer `session_id`s are + conformant with either model; deployments that share the + `"default"` session across multiple peers must migrate to + destination-based routing for client isolation. +- **OVOS-BRIDGE-1 is new — no existing implementation fully + conforms.** The bridge spec formalizes a role that exists today + (the HiveMind gateway, the bus client, any inbound message + fan-in) but with a tighter normative core. Current + implementations are expected to adopt the source-stamping and + session-preservation requirements incrementally. diff --git a/appendix/gaps.md b/appendix/gaps.md index d6632fd..d0f9335 100644 --- a/appendix/gaps.md +++ b/appendix/gaps.md @@ -55,3 +55,58 @@ format, and `ovos-localize` (§1.4) provides the operations layer; what remains is the *scale* of the translated corpus. + +### Bridge-specific gaps (OVOS-BRIDGE-1) + +- **Multi-hop bridge cascading.** BRIDGE-1 §4.2 describes + peer-to-peer topology between two deployments, but does not + address cascaded bridges (deployment A ↔ bridge ↔ deployment B + ↔ bridge ↔ deployment C). Each bridge stamps source + independently; outer-deployment participant identity is lost + at each inner boundary unless propagated as opaque metadata. +- **Bridge-to-bridge wire format.** The spec does not prescribe + whether messages between peer bridges remain in MSG-1 envelope + form or may use a different serialization. The intent is that + they stay in MSG-1 form (per §4.2 "relays native bus messages"), + but the transport encoding is left as a deployment concern. +- **Bridge health / heartbeat.** No `ovos.bridge.ping` / + `ovos.bridge.pong` surface is defined. The orchestrator has no + spec-level way to know whether a remote participant is + reachable. Deferred to a future observability specification if + needed. +- **Audio transmission over the bus.** BRIDGE-1 §4.2.1 describes + two audio-stack placements — local (satellite runs STT and its + own audio-output layer) and hub-side (hub runs the full audio + stack, satellite transmits raw audio inbound and receives final + audio outbound). STT placement and audio-output placement are + symmetric and independent: each can live on the satellite or the + hub, giving four possible combinations. The hub-side model + requires transmitting audio as bus Message payloads (e.g. + base64-encoded PCM or compressed frames). The bus surface for + any audio transmission — topic names, payload shape, session + fields for codec and audio preferences — is not defined by any + current specification. Deferred to OVOS-AUDIO-1. +- **Session-scoped pipeline plugin registration.** BRIDGE-1 §4.4 + and INTENT-4 §11 cover session-scoped intent registration for + satellite-side skills. A satellite that implements a pipeline + plugin (not an intent-based skill) cannot register that plugin + on the hub; no bus surface exists for session-scoped plugin + loading. If needed, this requires a new specification or an + extension to OVOS-PIPELINE-1. +- **Managing mode concurrent utterance race.** BRIDGE-1 §3.4.2 + says the bridge SHOULD apply `ovos.utterance.handled` session + updates before injecting the next utterance, but makes no + guarantee when both arrive simultaneously. The handling of + overlapping utterance rounds in managing mode — whether to + queue, drop, or process with a stale session — is left as a + deployment concern. A future revision may define a normative + queuing policy. +- **NAT bijection and hub-side session cleanup.** When a bridge + using `session_id` NAT (§3.2) disconnects a participant, the + hub-side `session_id` may remain in the orchestrator's + default-session store (SESSION-2 §5). BRIDGE-1 §3.2 says the + bridge SHOULD emit cleanup events using the hub-side `session_id` + before dropping the bijection, but does not define a complete + cleanup protocol for hub-side state created during the session's + lifetime (e.g. cross-utterance context, active handlers). + Deferred to a future session-lifecycle specification. diff --git a/appendix/rationale.md b/appendix/rationale.md index 1851e17..9156fe2 100644 --- a/appendix/rationale.md +++ b/appendix/rationale.md @@ -381,7 +381,70 @@ the normative sections. signals; consolidation is the consumer's decision per SESSION-1 §3.2.7. -### 4.8 Stop pipeline plugin (STOP-1) +### 4.9 Bus bridge (BRIDGE-1) + +- **Normative minimalism is design intent.** The bridge could have + been an auth spec, a topology spec, a wire-protocol spec. By + limiting normative weight to source stamping, session preservation, + and destination-based relay (3 MUSTs in §6), the spec correctly + identifies that session fields do the heavy lifting — the bridge + just carries them. Everything else (policy injection, topology + patterns) emerges from composing existing lifecycle, + state-ownership, and session-field specifications at the bus + boundary. +- **Destination-based routing provides client isolation.** + Session-id-only routing lets one participant claim another's + `session_id` and receive messages intended for the other (§3.2). + Destination-based routing fixes this because the orchestrator uses + `.reply()` to set `destination` to the original `source`. Two + participants sharing the same `session_id` (especially + `"default"`) cannot impersonate each other — their `source` values + differ, and each receives only messages whose `destination` + matches its own identifier. The OVOS-MSG-1 §5 derivation chain + preserves routing metadata through every emission, so the bridge + never sees a message without sufficient routing information. +- **Session_id matching is a MAY convenience.** The OVOS-MSG-1 §5 + derivation chain preserves `destination` across every `forward` / + `reply` / `response` hop, so all bus messages that carry + conversation progress carry a `destination` the bridge can route + on. `session_id` matching exists for the narrow case of topic-level + subscriptions a bridge explicitly opts into, not for correctness. +- **Layer-2 systems inject policy via session fields, not bridge + protocol.** A layer-2 system (MSG-1 §3.4) mutates the session at + the bridge boundary before the message enters the bus. No + bridge-specific protocol is needed — the session fields do the + work. This is the same pattern as SESSION-2 §2.4's + SHOULD-project pathway, but with the bridge as the enforcement + point rather than the component itself. +- **The hardened minimum topic set is a deployer choice.** A bridge + MAY subscribe to everything (default, simpler, compatible with + future lifecycle additions) or restrict to the utterance-lifecycle + topic set (hardened, reduced attack surface). The trade-off is by + design, not prescribed. +- **`site_id` enables bridge-side physical grouping without + requiring skills to understand topology.** The bridge is the + natural point to assign `site_id` because it is the only + component that has visibility into both the physical deployment + and the internal bus. A concrete example: a bridge that receives + Wi-Fi or Bluetooth scan data from participants can resolve that + signal to a physical location and stamp the appropriate `site_id` + before injecting the message; a bridge that integrates with a + home-automation system can use the canonical area name from that + system (e.g. `"living_room"`, `"kitchen"`) directly as the + `site_id`, giving skills a stable identifier that is already + meaningful in the user's home model. In either case, participants + need not know their own location, and skills need not understand + network topology. + The `site_id` then travels with the session, giving any downstream + pipeline plugin or skill a stable, location-derived grouping key + for context (e.g. applying room-specific TTS voices, routing + media to the nearest speaker, or gating location-aware intents). + This is why the spec mandates `site_id` for group routing rather + than enumerating participants: the bridge encapsulates the mapping + from physical signal to logical group, and everything downstream + consumes an opaque string. + +### 4.10 Stop pipeline plugin (STOP-1) The most common reader question on first encountering STOP-1 is *why a pipeline plugin and not a skill*. Stop sounds like an diff --git a/bridge-1.md b/bridge-1.md new file mode 100644 index 0000000..282d107 --- /dev/null +++ b/bridge-1.md @@ -0,0 +1,617 @@ +# Bus Bridge and Opaque Relay Specification + +**Spec ID:** OVOS-BRIDGE-1 · **Version:** 2 · **Status:** Draft + +This specification defines the **bus bridge** — a participant on the +internal message bus that terminates an external communication +channel and relays messages between the bus and remote participants. + +The bridge itself carries very little normative weight. Most of what +this document describes are **emergent patterns** — behaviours that +arise naturally when composing the session-field, routing, +state-ownership, and lifecycle specifications across a bus boundary. +The bridge is simply the point in the architecture where these +compositions become visible. + +It builds on **OVOS-MSG-1** (envelope and derivations), +**OVOS-SESSION-1** (session field registry and wire shape), +**OVOS-SESSION-2** (state ownership and the client-authority rule), +**OVOS-PIPELINE-1** (pipeline composition and per-session overrides), +**OVOS-TRANSFORM-1** (transformer chain per-session overrides), and +**OVOS-CONTEXT-1** (declarative intention context). + +The key words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, +**MAY**, and **RECOMMENDED** are used as in RFC 2119. + +--- + +## 1. Scope + +This specification defines: + +- the **bridge role** (§2) — the architectural position of a relay + between internal and external message layers; +- the **normative core** (§3) — identity stamping, outbound routing, + `site_id` assignment, and session preservation; +- **emergent patterns** (§4) — capabilities that arise from + composing other specifications at the bridge boundary: access + control, pipeline overrides, context pre-population, + multi-deployment topologies, out-of-utterance session sync, and + satellite skill registration; +- **ordering guidance** (§5) — the expectation of FIFO delivery; +- **conformance** (§6). + +This specification does **not** define: + +- **authentication or authorization** — how an external participant + proves its identity or what it is permitted to do is a **layer-2** + concern (see OVOS-MSG-1 §3.4); the bridge is assumed to have + resolved these before injecting a message; +- **capability discovery** — how a participant signals its hardware + features (microphone, screen, etc.) is a layer-2 concern; +- **wire protocol or encryption** — whether the external network + uses WebSockets, MQTT, binary framing, or TLS is out of scope; +- **site policy** — the logic that decides to blacklist certain + skills for a given participant is a layer-2 concern; +- **audio capture and local rendering** — microphone capture, STT, + and local speaker output are deployment concerns; deployments + that cannot render audio locally MAY receive synthesised audio + over the bridge via `ovos.audio.speech` (OVOS-AUDIO-1 §4.3). + +--- + +## 2. The bridge role + +A **bridge** is a participant on the internal message bus that +terminates an external communication channel. It treats external +participants as opaque sources of messages. + +To the internal bus, the bridge is the producer of the messages it +relays. To the external participant, the bridge is its connection to +the rest of the bus. The bridge is the **enforcement point** where +external protocol-specific data is translated into conformant +`OVOS-MSG-1` envelopes. + +The bridge's normative obligations are enumerated in §6. Everything +else described in this spec — policy injection, pipeline overrides, +topology patterns — emerges from the way other specifications +interact at the bus boundary. + +--- + +## 3. Normative core + +### 3.1 Inbound identity stamping (`source`) + +On receiving a message from an external participant, the bridge +**MUST** ensure the resulting bus Message carries a unique +identifier for that participant in `context.source`. Without a +unique `source`, the orchestrator and skills cannot route responses +back to the correct participant. + +- If the external participant provides its own identifier, the + bridge MAY use it verbatim or prefix it to guarantee uniqueness on + the local bus. +- If the participant provides no identifier, the bridge MUST + generate a unique value (e.g. a UUID). +- If the bridge manages multiple participants, it MUST ensure every + active `source` string is distinct regardless of origin. + +By stamping a unique `source`, the bridge makes the remote +participant appear as a first-class, addressable entity on the bus. + +### 3.2 Outbound routing + +The bridge relays messages from the bus to its external participants. +It MAY use any combination of `context.destination`, `context.session.session_id`, +and `context.session.site_id` to determine whether and where to relay +a message. + +A participant is **known** to the bridge from the moment the bridge +assigns it a `source` identifier (§3.1) until the grace period +expires after the participant disconnects (§5). Messages matching a +known participant MUST be relayed; messages matching a participant +whose grace period has expired MUST be discarded. + +**Primary signal: `destination`.** The bridge MUST relay every +Message whose `context.destination` matches one of its known +participants. This is the mechanism prescribed by **OVOS-MSG-1 §3** +— `destination` names the intended consumer, and the bridge is the +component that fulfills that naming by delivering the message across +the external channel. + +The primary value of `destination`-based routing is **client +isolation**. The orchestrator uses `.reply()` to route responses, +setting `destination` to the original `source` stamped by the bridge +(§3.1). Two participants sharing the same `session_id` — including +`"default"` — are still distinguished: their `source` values differ, +so each receives only messages addressed to it. + +**Secondary signal: `session_id`.** The bridge MAY also relay +messages by matching `context.session.session_id` to a known +participant, including messages whose `destination` is absent or +does not match. This is useful for catching session-scoped broadcasts +(e.g. `ovos.utterance.speak`, `ovos.utterance.handled`) that carry +the participant's session but no explicit destination. A bridge that +routes by `session_id` SHOULD also route by `destination`; the two +signals are complementary, not alternatives. + +**Group signal: `site_id`.** If the bridge groups participants into +logical or physical clusters (a household, an office, a tenant), it +**MUST** use `context.session.site_id` (§3.3) to identify the group. +Group-level routing (e.g. "all satellites in this household receive +broadcast `ovos.stop`") is distinguished by `site_id`, not by +enumerating individual `session_id` or `destination` values. + +**Identity mapping (NAT).** Just as a bridge MAY rewrite +`context.source` on outbound messages ("topology hiding", §3.1), it +MAY also rewrite `context.session.session_id` as messages cross the +boundary — translating a participant-local session identifier into a +hub-side identifier and vice versa. This enables a participant to +use its own `session_id` namespace without coordinating with the +hub. When performing `session_id` mapping, the bridge MUST maintain +a stable bijection between the participant's value and the hub-side +value for the lifetime of the participant's connection. When the +participant disconnects, the bridge SHOULD emit any cleanup events +(e.g. `ovos.skill.deregister` per §4.4) using the **hub-side** +`session_id` before dropping the bijection, so that orchestrator +state keyed on the hub-side value is cleaned up correctly. + +In all routing modes: + +- On matching a Message by any routing signal, the bridge MUST + relay it to the corresponding external participant. +- The bridge SHOULD strip or translate internal routing metadata + that is irrelevant to the external protocol before relaying. + Internal metadata includes `context.pipeline_id`, + `context.skill_id`, and internal `context.source` values that + name components on the local bus; participant-facing fields + such as `context.session` and `context.destination` SHOULD be + preserved. +- A bridge MAY overwrite the `source` of outbound messages with a + generic assistant ID ("topology hiding") when the identity of the + emitting component is not meaningful to the external participant. +- A bridge MAY restrict topic subscription to a hardened minimum + set for reduced attack surface. The minimum set for a + multi-deployment topology (§4.2) is the utterance lifecycle + defined in **OVOS-PIPELINE-1 §9** (currently + `ovos.utterance.handle`, `ovos.utterance.speak`, + `ovos.utterance.handled`, `ovos.intent.unmatched`) plus + `ovos.session.sync` (**OVOS-SESSION-2 §2.7**), and any additional + topics the participant's pipeline plugins depend on. The same matching + signals (`destination`, `session_id`, `site_id`) apply within + this restricted set. + +### 3.3 `site_id` assignment + +`site_id` is the **opaque group identifier** for a session, defined +in **OVOS-SESSION-1 §3.3** and owned by this specification. It names +the physical or logical cluster the participant belongs to; the +grouping criterion is chosen by the deployer. + +A client or external participant **MAY** report a `site_id` in its +session. The bridge evaluates `site_id` assignment in this order: + +1. If the bridge has its own determination (e.g. from resolving a + Wi-Fi or Bluetooth scan, or reading a canonical area name from a + connected home-automation system), it sets `site_id` to that + value, overriding whatever the client supplied. +2. If the bridge has no determination of its own, it preserves the + client's value unchanged. +3. If neither provides a `site_id`, the field is absent; consumers + MUST treat an absent `site_id` as an unknown group and MUST NOT + infer a default. + +Once `site_id` is present on an inbound message after bridge +processing, downstream components **MUST NOT** overwrite it. The +value travels unchanged through every `forward` / `reply` / +`response` derivation per **OVOS-MSG-1 §5**. + +Consumers **MUST NOT** parse or ascribe structure to `site_id` +beyond string equality. No specific value is reserved by this +specification. + +### 3.4 Session preservation + +The bridge **MUST** ensure every inbound bus Message carries a valid +`context.session` object (**OVOS-SESSION-1**) and **MUST** include +the session from every outbound bus Message in the external payload. + +#### 3.4.1 Relaying mode (spec-aware participants) + +When the external participant runs spec-compliant code and manages +its own session — as in the satellite-and-hub topology (§4.2.1) — +the bridge is a transparent carrier. It extracts the session from +the external payload and places it in the bus Message context +unchanged, and copies the session from outbound bus Messages back +into the external payload. The client is the authority for its own +session state per **OVOS-SESSION-2 §2**; the bridge MUST NOT inspect +or modify the session content. + +A layer-2 system (§4.1) MAY apply policy mutations to the session +after extraction and before bus injection — this is an additional +layer operating on top of the transparent bridge, not a violation +of it. The bridge's transparency obligation is that it does not +alter the session on its own initiative; layer-2 policy is a +deliberate deployment-level decision separate from the bridge itself. + +In this mode, the client is responsible for merging updated sessions +from `ovos.utterance.handled` (**PIPELINE-1 §9.5**) and for sending +the current session on every subsequent message. + +#### 3.4.2 Managing mode (opaque participants) + +When the external participant has no concept of OVOS sessions — as +in a chat-room gateway, an SMS bridge, or any non-OVOS client — the +bridge owns the session lifecycle for each participant. It assigns +a `session_id` per participant (or per logical channel such as a +room), synthesizes the session object on inbound messages, and +propagates session updates from outbound `ovos.utterance.handled` +events back into its own store for use on the next inbound message. + +The bridge MUST assign a distinct `session_id` to each managed +participant. This is required to correlate `ovos.utterance.handled` +events — which carry the `session_id` of the utterance round — back +to the correct participant's session store. + +If the external payload carries no session the bridge MUST +materialize one per **OVOS-SESSION-1 §4.1**. The bridge SHOULD apply +session updates carried by `ovos.utterance.handled` to the stored +session for that participant before injecting the next utterance. If +the next utterance arrives before `ovos.utterance.handled` is +received, the bridge MAY inject it using the last known session +state; the orchestrator will supply the updated session in the +following `ovos.utterance.handled`. + +--- + +In both modes the session object on the bus MUST conform to +**OVOS-SESSION-1** and the orchestrator reads it as the +authoritative state for the round (**OVOS-SESSION-2 §2**). Any +field the bridge places in the session is automatically visible to +every downstream processing stage. + +--- + +## 4. Emergent patterns + +The behaviours described in this section are not new protocol. They +are what happens when you compose existing specifications at a bus +boundary. The bridge ensures every inbound Message carries a valid session +(§3.4); the session fields do the work. + +### 4.1 Policy injection via session fields + +A layer-2 system (see **OVOS-MSG-1 §3.4**) MAY mutate the session +object at the bridge boundary before the message is placed on the +internal bus. Because the orchestrator and plugins read session +fields to determine what to process, any field set or restricted at +the boundary is automatically enforced downstream — no +bridge-to-orchestrator protocol is needed. + +The session fields available for injection are catalogued in +**OVOS-SESSION-1 §3**. Each is owned by the specification that +defines its semantics. The bridge need not understand any of them; +it carries them transparently. A layer-2 system SHOULD limit +mutations to fields registered in **OVOS-SESSION-1 §3**; overwriting +client-owned fields violates the client-authority rule of +**OVOS-SESSION-2 §2**. + +#### 4.1.1 Access control (denylist model) + +A layer-2 system MAY populate any of the `blacklisted_*` arrays +catalogued in **OVOS-SESSION-1 §3** at the bridge boundary to +restrict what the participant can access. As of this version the +denylist comprises three **OVOS-PIPELINE-1 §5** fields +(`blacklisted_skills`, `blacklisted_intents`, +`blacklisted_pipelines`) and six **OVOS-TRANSFORM-1 §5.2** fields +(`blacklisted_audio_transformers`, +`blacklisted_utterance_transformers`, +`blacklisted_metadata_transformers`, +`blacklisted_intent_transformers`, +`blacklisted_dialog_transformers`, +`blacklisted_tts_transformers`). All are arrays of string and absent +by default. The denylist is the only access-control mechanism — +there are no allowlist equivalents. Policy can only narrow, never +expand, a participant's capabilities. + +#### 4.1.2 Pipeline and transformer preference + +A participant MAY express a preference for its utterance lifecycle +by setting `session.pipeline` (**OVOS-PIPELINE-1 §5.1**) or any of +the six `session.*_transformers` lists (**OVOS-TRANSFORM-1 §5.1**). +A layer-2 system MAY override these by setting +`blacklisted_pipelines` or `blacklisted_*_transformers` at the +bridge boundary. + +The split follows the composition model of **OVOS-PIPELINE-1 §5.5**: +preference is a wish, availability drops unloaded components, policy +removes disallowed entries. No later stage adds what an earlier +stage rejected. + +#### 4.1.3 Context pre-population + +`session.intent_context` (**OVOS-CONTEXT-1**) MAY be seeded at the +bridge boundary so that a remote participant's first utterance +benefits from declarative gating or slot values. For example, a +layer-2 system that knows the participant's locale can pre-populate +a `lang` context entry, which intent matchers may gate on. + +### 4.2 Multi-deployment topology + +The bridge MAY connect two or more spec-compliant voice-OS +deployments as peers. In this topology every participant is a +spec-compliant deployment that speaks the native bus protocol +internally. The bridge is not a gateway to a foreign API; it relays +native bus messages between autonomous deployments. + +The bridge carries no audio. Audio capture and rendering are local +concerns for each deployment; what crosses the bridge are bus +messages: utterances (post-STT, pre-intent), speak requests, +handler lifecycle events, and session mutations. A deployment MAY +run a separate audio transport channel alongside the bridge, but +that channel is outside the scope of this specification. + +#### 4.2.1 Satellite and hub + +One or more satellite deployments connect to a hub that owns intent +resolution and skill dispatch. The satellite sends utterances over +the bridge and receives speak responses back. See §4.2.5 for the +variant where the satellite has no TTS and the hub synthesises audio +on its behalf. + +The lifecycle partition is conveyed by **message type**, not by +session fields. The bridge entry point is **OVOS-PIPELINE-1 §9.1** +`ovos.utterance.handle` — the post-STT, pre-intent message that +enters the hub's utterance lifecycle. The hub processes intent +matching, dispatch, and response generation. Two messages cross the +bridge back to the satellite: `ovos.utterance.speak` (**PIPELINE-1 +§9.6**) carrying the text response, and `ovos.utterance.handled` +(**PIPELINE-1 §9.5**) carrying the terminal session state. The +satellite SHOULD wait for `ovos.utterance.handled` before accepting +the next utterance, and SHOULD merge the session it carries per +**OVOS-SESSION-2 §4**. + +Where STT and audio output run depends on the deployment: + +- **Local audio stack** — the satellite runs STT locally, injects + `ovos.utterance.handle` with the transcribed text, receives + `ovos.utterance.speak` as text, and runs TTS (and its + dialog-transformer chain) locally before rendering audio. The + bridge carries only bus messages. +- **Hub-side audio stack** — raw audio is transmitted to the hub + (via a mechanism outside this specification's scope; see + `appendix/gaps.md`). The hub runs STT and injects + `ovos.utterance.handle` internally; it also runs the full + audio-output layer including dialog-transformer and TTS, + returning final audio to the satellite rather than text. In + this model the satellite needs no local audio stack at all. + +Session fields (`session.pipeline`, `session.*_transformers`, +`blacklisted_*`) are available to communicate per-session +preferences and policy between the two deployments (§4.1), but the +fundamental "we split here" is defined by which bus event the +satellite emits and which it receives back. + +#### 4.2.2 Cascading intelligence + +A local deployment runs its own full pipeline and handles utterances +it has skills for. When the local pipeline produces no match, the +local deployment forwards the utterance to a more capable remote +deployment over the bridge. Unlike the satellite-and-hub pattern +(§4.2.1) — where the satellite has no local intent pipeline at all — +the cascading deployment actively participates in the utterance +lifecycle before deciding to escalate. + +The local deployment MAY communicate what it has already attempted +by seeding `session.intent_context` (**OVOS-CONTEXT-1**) or +adjusting `session.active_handlers` (**OVOS-PIPELINE-1 §7.1**) +before forwarding the utterance. The bridge relays the message +unchanged; the remote deployment's pipeline receives the session as +context for its own matching. The remote deployment's speak and +handled events follow the routing path back through the bridge to +the originating participant. + +#### 4.2.3 Session identity under multi-participant routing + +Multiple participants connecting through the same bridge may use the +same `session_id` — most commonly `"default"`. The bridge +distinguishes them by `source` (§3.1), but session state is keyed on +`session_id` across the deployment (OVOS-SESSION-2 §3.1). Two +participants sharing `session_id: "default"` therefore share the +orchestrator's default-session store. Deployments that need session +isolation between participants SHOULD use distinct `session_id` +values or `session_id` NAT (§3.2) to prevent state collisions. +`site_id` identifies a routing group, not a session boundary, and +does not provide isolation on its own. + +In a multi-satellite deployment, each satellite SHOULD use a +distinct `session_id`. This is necessary not only for session +isolation but for correct message routing: the orchestrator routes +responses (including `ovos.utterance.speak` and +`ovos.utterance.handled`) by deriving from the inbound message via +`.reply()`, which sets `context.destination` to the satellite's +`source`. A bridge routing by `session_id` alone cannot distinguish +two satellites sharing the same `session_id`. + +#### 4.2.4 Per-satellite response personalisation + +Because each satellite carries its own session, a single hub +utterance round may produce N distinct spoken responses — one per +satellite — each grounded on the same base orchestrator output but +shaped by that satellite's local transformer chain. + +The hub emits the same base `ovos.utterance.speak` text to every +satellite. Personalisation happens **on the satellite**, not the hub: +the dialog-transformer chain (**OVOS-TRANSFORM-1 §3.5**) runs in the +satellite's local audio-output layer just before TTS. A satellite +with an LLM-backed dialog transformer (persona rewriting, tone +adjustment, translation) rewrites the hub's plain text before +synthesis. + +The satellite's `session.dialog_transformers` field controls which +transformers its audio-output layer applies. Because the session is +carried in `ovos.utterance.speak` back to the satellite, the +satellite's audio-output service reads its own transformer +preferences from that session and applies them locally. + +This pattern requires no bridge-level protocol and no coordination +between satellites. The bridge's only role is transparent session +relay (§3.4.1): each satellite's session — including its +`dialog_transformers` preference — travels to the hub on the +inbound utterance and returns on `ovos.utterance.speak`, where the +satellite's local audio-output layer reads it and applies the +configured chain. + +> **Note — hub-side audio stack.** The pattern above assumes the +> bridge carries only bus messages and each satellite runs its own +> audio-output layer. An alternative deployment MAY transmit audio +> over the bus itself (for example, as base64-encoded payloads in +> bus Messages), in which case the full audio stack — including the +> dialog-transformer and TTS-transformer chains — runs on the hub +> rather than the satellite. The satellite in that model receives +> final audio bytes and has no local audio-output layer. This +> topology is outside the scope of BRIDGE-1 (§1 excludes audio +> input and output); its bus surface is to be defined by a future +> audio-output specification. See also: bridge-specific gaps in +> `appendix/gaps.md`. + +### 4.3 Out-of-utterance session sync + +The hub MAY emit `ovos.session.sync` (**OVOS-SESSION-2 §2.7**) to +push session-state changes to participants between utterance rounds +— for example, when configuration changes or a background process +mutates shared state. A bridge in a multi-deployment topology SHOULD +relay `ovos.session.sync` to the appropriate satellite(s) by +matching on `context.destination` or `context.session.session_id`, +using the same routing signals as for any other outbound message +(§3.2). `ovos.session.sync` is included in the hardened minimum +topic set (§3.2) for this reason. + +### 4.4 Satellite skill registration + +A satellite running its own **intent-based skills** may register +those skills on the hub's orchestrator by relaying the registration +messages through the bridge. The bridge forwards these messages +unchanged; the orchestrator keys them by the `session_id` in the +message context (**OVOS-INTENT-4 §11.1**), which is the satellite's +own `session_id`. No special registration protocol is needed. +Registration of satellite-side **pipeline plugins** on the hub is +not supported by any current specification and is out of scope here. + +The effective intent pool, inheritance rule, and blacklist +interaction are defined in **OVOS-INTENT-4 §11.2**. When a +session-scoped intent is matched, the hub dispatches it as a +`.reply()` to the inbound utterance, setting `context.destination` +to the satellite's `source`; the bridge routes it back per §3.2. + +**Disconnect.** When the satellite disconnects, the bridge SHOULD +emit `ovos.skill.deregister` (**OVOS-INTENT-4 §8.4**) with the +satellite's `session_id` for each skill the satellite registered. +When the bridge uses `session_id` NAT (§3.2), it MUST use the +hub-side `session_id` in the deregister payload. + +**Reconnect.** When the satellite reconnects, its session-scoped +registrations are gone. The satellite is responsible for +re-emitting its registration messages; the bridge relays them as on +initial connect. A bridge operating in managing mode (§3.4.2) MAY +cache the satellite's registration set and re-emit it on reconnect +on the satellite's behalf. + +**Bridge emissions.** When the bridge itself emits bus messages +(cleanup events, health signals) rather than relaying participant +messages, it SHOULD use the `"default"` session unless the emission +is explicitly scoped to a specific participant's session. + +#### 4.2.5 TTS as a service + +A satellite without a local TTS engine MAY request that the hub +synthesise speech on its behalf. The bridge translates the +`ovos.utterance.speak` message it would normally relay back to the +satellite into `ovos.utterance.speak.b64` before placing it on the +hub bus. The hub's audio output service runs the full TTS pipeline +and emits `ovos.audio.speech` (OVOS-AUDIO-1 §4.3) with the +synthesised audio encoded as base64. The bridge relays +`ovos.audio.speech` to the satellite; the satellite decodes and +plays the audio directly. + +In this topology audio crosses the bridge as base64 data rather than +as a local rendering obligation. The hub renders nothing locally for +sessions owned by the satellite. + +--- + +## 5. Message ordering + +A bridge SHOULD preserve the temporal ordering of messages for any +given `session_id` or `source`, where the underlying transport +provides ordered delivery. On transports that do not guarantee +ordering (e.g. UDP, MQTT QoS 0), out-of-order delivery is a +degradation of service quality but does not violate this +specification. + +- Sequential utterances from the same participant SHOULD be placed + on the bus in the order they were received. +- Responses from the bus targeting the same participant SHOULD be + delivered in the order they were emitted. + +A bridge MUST discard undeliverable Messages for a disconnected +participant after a deployment-defined grace period and MUST NOT +buffer them indefinitely. + +--- + +## 6. Conformance + +### A bridge **MUST**: + +- stamp a unique identifier in `context.source` for every inbound + message (§3.1); +- preserve the `session` object's content and structure during relay + (§3.4); +- relay every matched message to the corresponding external + participant, regardless of which routing signal produced the match + (§3.2); +- conform to **OVOS-MSG-1** for all bus emissions; +- discard undeliverable messages after a deployment-defined grace + period and MUST NOT buffer them indefinitely (§5). + +### A bridge **SHOULD**: + +- maintain FIFO ordering per `source` and per `session_id`, where + the transport supports it (§5). + +### A bridge **MAY**: + +- use any routing signal combination described in §3.2 + (`destination`, `session_id`, `site_id`), including restricting + to a hardened minimum topic set or performing `session_id` NAT; +- perform "topology hiding" by overwriting the `source` of outbound + messages with a generic assistant ID (§3.2); +- mutate the `session` object to inject layer-2 policy or metadata + before bus injection (§4.1); +- connect spec-compliant voice-OS deployments as peers (§4.2); +- relay `ovos.session.sync` to satellites between utterance rounds + (§4.3); +- relay intent registration messages from satellite-side skills to + the hub, and emit `ovos.skill.deregister` on satellite disconnect + (§4.4); +- translate `ovos.utterance.speak` to `ovos.utterance.speak.b64` + for satellites without local TTS, and relay `ovos.audio.speech` + back to them (§4.2.5). + +--- + +## See also + +- **OVOS-MSG-1** — envelope and routing key definitions. +- **OVOS-SESSION-1** — session field registry and wire shape. +- **OVOS-SESSION-2** — client-side state authority and resumption. +- **OVOS-PIPELINE-1** — pipeline composition and per-session + overrides. +- **OVOS-TRANSFORM-1** — transformer plugin chains and their + per-session override fields. +- **OVOS-CONTEXT-1** — `session.intent_context` and declarative + intent gating. +- **OVOS-AUDIO-1** — `ovos.utterance.speak.b64` and `ovos.audio.speech` + for TTS-as-a-service (§4.2.5).