# SPDX-License-Identifier: GPL-3.0-or-later """DECNET bus interop. Aligns BEHAVE Observation with DECNET Event payload shape. DECNET's Event (decnet/bus/base.py:26) carries ``(topic, payload, type, v, ts, id)``. A BEHAVE Observation maps onto that envelope as follows: topic = "attacker.observation." + observation.primitive payload = observation.model_dump(exclude={"id", "ts", "v"}) type = observation.primitive v = observation.v ts = observation.ts id = observation.id The publisher must set ``topic`` from the primitive when calling ``bus.publish()``; DECNET's bus does not trust topic from the wire (anti-spoofing, base.py:60-76). This module does NOT import DECNET. The adapter speaks dicts; consumers wire it to their own bus. """ from __future__ import annotations from typing import Any from .envelope import Observation TOPIC_PREFIX: str = "attacker.observation" def event_topic_for(primitive: str) -> str: """Return the canonical DECNET bus topic for a BEHAVE primitive.""" return f"{TOPIC_PREFIX}.{primitive}" def to_event_payload(obs: Observation) -> dict[str, Any]: """Project an Observation into a dict suitable for ``Event.payload``. Excludes ``id``, ``ts``, and ``v`` because those are carried at the Event envelope level by DECNET, not in the payload body. """ return obs.model_dump(exclude={"id", "ts", "v"}, mode="json") def from_event_payload(primitive: str, payload: dict[str, Any]) -> Observation: """Reconstruct an Observation from ``(topic-derived primitive, Event.payload)``. The ``primitive`` argument is the trailing segment of the bus topic, NOT a field read from the payload — relying on the wire-side ``primitive`` field would let a misbehaving publisher spoof observations on topics they don't actually publish to. This mirrors DECNET's ``Event.from_dict`` discipline (decnet/bus/base.py:60-76). """ if "primitive" in payload and payload["primitive"] != primitive: raise ValueError( f"payload.primitive ({payload['primitive']!r}) does not match " f"topic-derived primitive ({primitive!r}); refusing to reconstruct" ) return Observation.model_validate({**payload, "primitive": primitive})