Raxol.Harness.EventBoundary (Raxol v2.6.1)

View Source

The live-session security seam: converts a live agent contract event (atom-keyed struct/map, atom payload keys AND atom payload values -- e.g. payload: %{item_type: :tool_use}) into the fixture wire shape Raxol.Harness.Projection.project/2 and the stall detector already consume (atom top-level fields; payload with STRING keys and JSON-shaped values -- exactly what Jason.encode!/1 |> Jason.decode!/1 would produce).

Security posture

A live event crosses a PROCESS boundary (Raxol.Harness.SessionLane's subscribe/1 delivers {:session_event, session_id, event} messages from a process this package does not control) and is therefore untrusted input, not merely differently-shaped input. normalize/1 enforces four properties, every one load-bearing:

  • No atom minting. String.to_atom/1 is never called anywhere in this module -- an attacker (or a buggy producer) who controls type/family/payload key or value strings must never be able to grow the atom table by sending events. Every string stays a string; every already-existing atom on the INPUT side is turned into a string on the way out (never the reverse).
  • Unknown fields are dropped. The output map carries ONLY the nine fields the projection/status-strip pipeline understands (:id, :turn_id, :ts, :family, :type, :tier, :scope, :provenance, :payload) -- anything else on the input (extra struct fields, a producer's internal bookkeeping) never reaches the surface.
  • Taint is never laundered. :provenance.trust accepts exactly :trusted and :tainted from the input; every other value (including an unrecognized atom, nil, or a missing key) is absorbed to :tainted -- this seam may only ever ADD taint on an ambiguous signal, never remove it.
  • :tier is never guessed. Tier decides whether content becomes permanent transcript (Raxol.Harness.Fixture.Event's own :durable/:ephemeral split). Anything other than the two literal atoms :durable/:ephemeral is a hard {:error, :invalid_event} -- there is no safe default to fall back to.

:type and :family are the one deliberate exception to "never pass untyped data through": both pass through UNCHANGED, whether the input value is an atom or not. Raxol.Harness.Projection.Recovery's own partitioning already treats a non-atom :type/unrecognized :family as unrecognized and demotes it safely (N-DORM-04) -- this boundary would only be "fixing" that by minting a fresh atom out of whatever a hostile string claims to be, which is exactly the failure mode this module exists to prevent.

Summary

Functions

Normalize a live event into the fixture wire shape.

Functions

normalize(event)

@spec normalize(map()) :: {:ok, map()} | {:error, :invalid_event}

Normalize a live event into the fixture wire shape.

Accepts any map, including a struct (fields are read via Map.get/2, never struct pattern matching, so any atom-keyed struct works). Returns {:ok, map} on success; {:error, :invalid_event} when :id, :ts, :payload, or :tier fail their respective shape checks (see moduledoc).