|
oc_connect
|
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 = "<< <" |
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)
|
protected |
| 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.
| 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.
| 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.
| str oc_connect.hub_lane.HUB_LANE = "hub" |
Definition at line 101 of file hub_lane.py.
| str oc_connect.hub_lane.HUB_LANE_SCHEMA_VERSION = "1.0" |
Definition at line 106 of file hub_lane.py.
| str oc_connect.hub_lane.HUB_LANE_SOURCE = "hub" |
Definition at line 104 of file hub_lane.py.
| str oc_connect.hub_lane.HUB_MESSAGE_BEGIN = "<<<BEGIN HUB MESSAGE {event_id}>>>" |
Definition at line 120 of file hub_lane.py.
| str oc_connect.hub_lane.HUB_MESSAGE_END = "<<<END HUB MESSAGE {event_id}>>>" |
Definition at line 121 of file hub_lane.py.
| tuple oc_connect.hub_lane.HUB_REPLY_CHOICES = ("YES", "NO", "ACK", "HOLD") |
Definition at line 110 of file hub_lane.py.
| oc_connect.hub_lane.HubKind = Literal["dispatch", "report", "alert", "ask", "ruling", "instruction"] |
Definition at line 116 of file hub_lane.py.
| str oc_connect.hub_lane.NEUTRALISED_MARKER = "<< <" |
Definition at line 125 of file hub_lane.py.
| str oc_connect.hub_lane.REMOTE_OPERATOR = "operator@remote" |
Definition at line 114 of file hub_lane.py.