oc_connect
Loading...
Searching...
No Matches
oc_connect.hub_lane Namespace Reference

Classes

class  HubLaneError
class  HubLaneRelayError
class  UncorrelatedReplyError
class  HubLaneEvent
class  HubLaneStore
class  HubLaneOutcome
class  HubLaneRelay
class  HubReply

Functions

Path hub_lane_dir (CommsBus bus)
str render_event (HubLaneEvent event)
None _atomic_write (Path path, str payload)
HubReply resolve_hub_reply (str inbound_message_id, *, InboxStore inbox, AuditStore audit)

Variables

str HUB_LANE = "hub"
str HUB_LANE_SOURCE = "hub"
str HUB_LANE_SCHEMA_VERSION = "1.0"
tuple HUB_REPLY_CHOICES = ("YES", "NO", "ACK", "HOLD")
str REMOTE_OPERATOR = "operator@remote"
 HubKind = Literal["dispatch", "report", "alert", "ask", "ruling", "instruction"]
str HUB_MESSAGE_BEGIN = "<<<BEGIN HUB MESSAGE {event_id}>>>"
str HUB_MESSAGE_END = "<<<END HUB MESSAGE {event_id}>>>"
str NEUTRALISED_MARKER = "<< <"

Detailed Description

The hub lane: addressed messages at rest, their relay, and the reply path.

Plan 259 P5, from the factory-decomposition design note §4. The hub has
three carriers and this is the third: the connector, for whoever is remote.
A message addressed to ``operator@remote`` is written into
``comms/lanes/hub/`` by ``oc_chat`` -- which holds no credential and knows
nothing about this package -- and ``oc_connect relay hub`` is what puts it on
the operator's phone.

**This module is the schema authority for the lane.** The writer lives in
``oc_chat/oc_chat/core/hub_file.py`` and deliberately does NOT import this
package, exactly as the worker-report and session-alert writers do not. The
two sides agree by a written contract rather than by a shared import, and the
contract is :class:`HubLaneEvent`::

    {
      "schema_version":    "1.0",
      "event_id":          "<26-character ULID; IS the message_id>",
      "lane":              "hub",
      "direction":         "outbound",
      "risk_class":        "low",
      "created_at_utc":    "2026-09-13T12:00:00Z",
      "summary":           "<the first line of the body>",
      "relayed_at_utc":    null,
      "relay_chunk_count": null,

      "message_id":        "<same as event_id>",
      "at":                "2026-09-13T12:00:00+02:00",
      "kind":              "dispatch"|"report"|"alert"|"ask"|"ruling"|"instruction",
      "sender":            "orchestrator"|"operator@local"|"operator@remote"|"agent-NNN",
      "recipient":         <the same four forms>,
      "subject":           "259-hub" | null,
      "body":              "<the text>",
      "in_reply_to":       "<an ask's id>" | null,
      "requires":          "human"|"orchestrator" | null,
      "answer":            "YES"|"NO"|"ACK"|"HOLD" | null,
      "provenance":        {actor, actor_type, channel, correlation}
    }

File naming follows the §3 bus convention, ``<utc-stamp>_hub_<ulid>.json``.

Three differences from the two lanes it mirrors are deliberate.

* **The message id is the idempotency key, unchunked.** A worker report keys
  each chunk (``<event_id>-001``) because it carries a whole closing summary;
  a hub message is ONE message, and keying it by its own id is what lets the
  operator's reply correlate back to the MESSAGE rather than merely to a send.
  A body past the transport limit therefore fails loudly instead of being
  split -- the same call :mod:`~oc_connect.session_alerts` makes, and for the
  same reason.
* **Only messages addressed to** ``operator@remote`` **are relayed.** The lane
  carries every hub message, because it is also the durable hand-off between
  local parties; the relay is the half addressed away from here.
* **A human-required ask carries the four reply buttons.** Nothing else does:
  a report, an alert, an instruction and an orchestrator-required ask are not
  questions the operator is being asked to answer. Note that plan 236's
  ``CHOICES_RULE`` still governs WHAT may be asked with buttons -- a security
  ruling is never sent with them -- and that rule is the orchestrator's to
  apply when it composes the ask, not this relay's to enforce after the fact.

The reply path is the EXISTING inbox. :func:`resolve_hub_reply` walks the
correlation the poller already recorded -- audit id, outbound record,
idempotency key -- back to the hub message id, and refuses by name when the
walk does not land on this lane. Building the ruling from that is
``oc_chat``'s job (``hub_manage rule --from-inbox``), because a ruling is a
hub message and this package does not know what one is.

Dependencies
------------
- oc_connect.audit (AuditStore, content_hash, timestamp, utc_now - audits relay outcomes and stamps relayed_at_utc)
- oc_connect.comms (CommsBus - locates the hub lane directory under lanes/)
- oc_connect.delivery (OutboundSender - the grant-checked send path every relayed message crosses)
- oc_connect.egress (require_safe, EgressBlockedError - scans the rendered message before anything is queued)
- oc_connect.inbound.inbox (InboxStore - reads the stored reply when resolving it back to a hub message)
- oc_diagnostics (_dbg structured logger)
- option_c_connector_contracts (lane - the hub lane's declared direction and risk ceiling)
- pydantic (BaseModel, ConfigDict, field_validator - the HubLaneEvent schema)

Function Documentation

◆ _atomic_write()

None oc_connect.hub_lane._atomic_write ( Path path,
str payload )
protected

Definition at line 248 of file hub_lane.py.

Here is the caller graph for this function:

◆ hub_lane_dir()

Path oc_connect.hub_lane.hub_lane_dir ( CommsBus bus)
Return the hub lane directory for *bus*.

A function rather than a :class:`~oc_connect.comms.CommsBus` property so
plan 259 adds no method to that class: the lane is created by whoever
first writes or reads it, and an orchestrator that has never hubbed
should not find a directory implying otherwise -- the same rule the
worker-report and session-alert lanes state about ``directories()``.

Definition at line 140 of file hub_lane.py.

◆ render_event()

str oc_connect.hub_lane.render_event ( HubLaneEvent event)
Return the text an operator reads for one hub message, fenced.

The header names the ROUTE, because on a hub the question "who is asking
me this, about what" is most of the message. The body is fenced with the
event's own id and any literal ``<<<`` inside it is neutralised, so a body
cannot forge its own closing fence. The rendered copy is the only thing
that changes; the stored text does not.

Definition at line 221 of file hub_lane.py.

Here is the caller graph for this function:

◆ resolve_hub_reply()

HubReply oc_connect.hub_lane.resolve_hub_reply ( str inbound_message_id,
* ,
InboxStore inbox,
AuditStore audit )
Resolve one stored inbound message back to the hub message it answers.

The walk is the poller's own, read backwards: the stored message carries
the audit id of the send it replied to; that record carries the
idempotency key; and for this lane the key IS the hub message id. Every
missing link, and every link that lands on a different lane, is refused by
name rather than guessed -- turning an uncorrelated message into a ruling
is precisely the failure the hub's provenance rule exists to prevent.

Returns the three correlation fields the hub's ``telegram`` channel
requires. Building the ruling from them is ``oc_chat``'s job: a ruling is
a hub message, and this package does not know what one is.

Raises:
    UncorrelatedReplyError: VALIDATION_ERROR (L3) -- the message is not
        stored, carries no correlation, names an audit record that does
        not exist, or answers a send on another lane.

Definition at line 487 of file hub_lane.py.

Variable Documentation

◆ HUB_LANE

str oc_connect.hub_lane.HUB_LANE = "hub"

Definition at line 101 of file hub_lane.py.

◆ HUB_LANE_SCHEMA_VERSION

str oc_connect.hub_lane.HUB_LANE_SCHEMA_VERSION = "1.0"

Definition at line 106 of file hub_lane.py.

◆ HUB_LANE_SOURCE

str oc_connect.hub_lane.HUB_LANE_SOURCE = "hub"

Definition at line 104 of file hub_lane.py.

◆ HUB_MESSAGE_BEGIN

str oc_connect.hub_lane.HUB_MESSAGE_BEGIN = "<<<BEGIN HUB MESSAGE {event_id}>>>"

Definition at line 120 of file hub_lane.py.

◆ HUB_MESSAGE_END

str oc_connect.hub_lane.HUB_MESSAGE_END = "<<<END HUB MESSAGE {event_id}>>>"

Definition at line 121 of file hub_lane.py.

◆ HUB_REPLY_CHOICES

tuple oc_connect.hub_lane.HUB_REPLY_CHOICES = ("YES", "NO", "ACK", "HOLD")

Definition at line 110 of file hub_lane.py.

◆ HubKind

oc_connect.hub_lane.HubKind = Literal["dispatch", "report", "alert", "ask", "ruling", "instruction"]

Definition at line 116 of file hub_lane.py.

◆ NEUTRALISED_MARKER

str oc_connect.hub_lane.NEUTRALISED_MARKER = "<< <"

Definition at line 125 of file hub_lane.py.

◆ REMOTE_OPERATOR

str oc_connect.hub_lane.REMOTE_OPERATOR = "operator@remote"

Definition at line 114 of file hub_lane.py.