7The hub's contract: one address grammar, six kinds, one human rule.
11Plan 259 P2, from the factory-decomposition design note §4. The hub is a
12MESSAGE CONTRACT, not a transport. Two pipes and a reply path already
13exist -- the worker-report lane, the session-alert lane, and the
14connector's correlated inbound reply -- and what turns them into a hub is
15three additions, all declared here:
17**Addresses**, so a message is ROUTED rather than deposited. Four forms
18and no others: ``orchestrator``, ``operator@local`` (the control centre,
19which today is the CLI in the operator's own session), ``operator@remote``
20(the connector), and ``agent-NNN`` for each worker instance.
22**Kinds with replies.** ``report`` and ``alert`` flow one way. An ``ask``
23expects an answer and a ``ruling`` IS that answer, naming the ask it
24answers, so a question any human channel can answer is a first-class
25thing. ``dispatch`` and ``instruction`` are records of what was done and
26what was told, so the orchestrator's picture of the fleet is the log and
29**Provenance on every message, and a rule about rulings.** A ruling
30carries who answered and through which channel. This is not a nicety: it
31derives from an operator ruling already in force, that a security
32approval must be in the operator's own voice and never relayed by an
33agent. In a hub where the orchestrator agent, the control centre and
34Telegram can all answer, an unsigned reply makes an agent-relayed yes
35indistinguishable from the human's. :func:`check_ruling_provenance` is
36the ONE place that rule lives.
40- :func:`parse_address` -- the whole grammar, refusing by name. Agent
42 :func:`oc_orchestrator.agent_instances.validate_agent_id`, IMPORTED
43 rather than reimplemented, so there is one definition of ``agent-NNN``
45- :data:`SUPPORTED_KINDS` -- the six, checked in one place.
46- :class:`Provenance` / :class:`HubMessage` -- frozen value types;
47 :func:`new_message` is the one construction path.
48- :func:`check_ruling_provenance` -- the human rule.
49- :class:`HubLog` / :class:`Carrier` -- the two Protocols the backends
50 and the carriers satisfy.
51- :class:`Hub` -- ``send`` (record, then route) and ``rule`` (build the
52 ruling from the ask and send it).
53- :class:`HubError` and its named subclasses -- every refusal.
59 L2 - lifecycle events (a message sent, a carrier chosen)
60 L3 - semantic details (a refusal, with what it refused)
61 L4 - deep tracing (module load)
65- **The log is the truth.** :meth:`Hub.send` RECORDS BEFORE IT DELIVERS,
66 and a carrier failure after the record is a :class:`CarrierError` with
67 the entry left standing -- the file lane redelivers, and a message
68 that reached no carrier is still a message that was sent. That is
69 design-note invariant 1 made mechanical: direct does not mean unlogged.
70- **A ruling on a human-required ask is refused from any agent actor.**
71 Not discouraged, not flagged: refused, by construction, in
72 :func:`check_ruling_provenance`, which :meth:`Hub.send` calls before
73 it records. A refused ruling is never recorded and never delivered.
74- ``answer`` is one of :data:`RULING_ANSWERS` -- the connector's reply
75 vocabulary -- so a Telegram tap, a CLI word and a control-centre
76 button all produce the same four values.
77- :class:`Provenance.correlation` is an IMMUTABLE mapping. A correlation
78 a caller can edit after the check is not evidence.
79- ``message_id`` is a ULID: :func:`new_ulid` and the Crockford alphabet
80 are lifted VERBATIM from ``plan_status.py:450-464`` and ``:132``,
81 waiver text included, because the lane readers compare these bytes and
82 a second algorithm here would be a second answer. The two now sit in
83 one package and could in principle be deduplicated; plan 272 P8 leaves
84 that to the follow-up that also settles the comms-root rule, because a
85 shared ULID module is a decision about the package's shape rather than
86 a consequence of the move.
87- This module imports nothing but the standard library, the diagnostics
88 runtime and the one agent-id validator beside it -- no registry, no
89 consumer, no toolchain. A witness measures that with the committed
90 extractor, and ``tests/test_t1_leaf_guard.py`` measures it statically.
91- Nothing here reads the process working directory, opens a file, or
92 starts a subprocess. Routing is a decision; moving bytes is a carrier's
97- oc_diagnostics (structured debug logging via _dbg)
98- oc_orchestrator.agent_instances (the ONE definition of ``agent-NNN``)
101from __future__
import annotations
104from collections.abc
import Mapping
105from dataclasses
import dataclass, field
106from datetime
import datetime, timezone
107from types
import MappingProxyType
108from typing
import Any, Callable, Protocol, runtime_checkable
110from oc_diagnostics
import _dbg
114_dbg(
"OC_CHAT.MAIN",
"module",
"hub loaded", 4)
121ADDRESS_ORCHESTRATOR =
"orchestrator"
126ADDRESS_OPERATOR_LOCAL =
"operator@local"
129ADDRESS_OPERATOR_REMOTE =
"operator@remote"
133FIXED_ADDRESSES: tuple[str, ...] = (
134 ADDRESS_ORCHESTRATOR,
135 ADDRESS_OPERATOR_LOCAL,
136 ADDRESS_OPERATOR_REMOTE,
143KIND_DISPATCH =
"dispatch"
144KIND_REPORT =
"report"
147KIND_RULING =
"ruling"
148KIND_INSTRUCTION =
"instruction"
152SUPPORTED_KINDS: frozenset[str] = frozenset(
153 {KIND_DISPATCH, KIND_REPORT, KIND_ALERT, KIND_ASK, KIND_RULING, KIND_INSTRUCTION}
157REQUIRES_HUMAN =
"human"
158REQUIRES_ORCHESTRATOR =
"orchestrator"
159SUPPORTED_REQUIRES: tuple[str, ...] = (REQUIRES_HUMAN, REQUIRES_ORCHESTRATOR)
165RULING_ANSWERS: tuple[str, ...] = (
"YES",
"NO",
"ACK",
"HOLD")
170ACTOR_TYPES: tuple[str, ...] = (
172 "orchestrator_agent",
178CHANNELS: tuple[str, ...] = (
"session",
"cli",
"lane",
"telegram")
184HUMAN_CHANNELS: tuple[str, ...] = (
"cli",
"telegram")
187HUMAN_ACTOR_TYPE =
"human_operator"
198CROCKFORD_ALPHABET =
"0123456789ABCDEFGHJKMNPQRSTVWXYZ"
203 for _
in range(length):
204 value, remainder = divmod(value, 32)
205 digits.append(CROCKFORD_ALPHABET[remainder])
206 return "".join(reversed(digits))
209def new_ulid(now: datetime, *, entropy: bytes |
None =
None) -> str:
210 """Return a 26-character ULID: 48 bits of time, 80 bits of randomness.
212 Lifted VERBATIM from ``scripts/plan_status.py:445-460``, which says
213 the same of ``oc_connect.comms.new_ulid``. Time-first encoding makes
214 lexical order arrival order, which is what lets a reader walk the hub
215 lane with a plain directory sort, exactly as the other two lanes are
218 if now.tzinfo
is None:
219 now = now.replace(tzinfo=timezone.utc)
220 milliseconds = int(now.astimezone(timezone.utc).timestamp() * 1000)
221 raw = entropy
if entropy
is not None else secrets.token_bytes(10)
223 raise ValueError(
"ULID entropy must be exactly ten bytes")
224 return _encode(milliseconds, 10) +
_encode(int.from_bytes(raw,
"big"), 16)
228 """A local-with-offset ISO 8601 stamp, seconds resolution.
230 The project-wide convention, matching ``work_queue_file._now_iso``
231 and ``plan_inventory._now_iso``. An offset is not decoration: a
232 stamp without one is not a point in time.
234 return datetime.now().astimezone().replace(microsecond=0).isoformat()
241 """Base class for every refusal the hub makes."""
244class AddressError(HubError):
245 """A string is not one of the four address forms."""
249 """A message names a kind the hub does not carry, or breaks that kind's shape."""
253 """A message's provenance does not support what the message claims."""
257 """No carrier would take the message, or the one that did failed.
259 Raised AFTER the entry is recorded, always. The log is the truth and
260 a message nobody carried is still a message that was sent.
265 """A ruling names something that is not an open ask."""
272 """Return *value* if it is an address, else refuse it by name.
274 The whole grammar, in one function. ``agent-NNN`` is delegated to
275 :func:`oc_orchestrator.agent_instances.validate_agent_id` -- imported,
276 never reimplemented -- so the tree has exactly one definition of what
277 an instance identity looks like and a change there is a change here.
280 AddressError: VALIDATION_ERROR (L3) -- the message names the
281 value it refused and every form it accepts.
283 func =
"parse_address"
284 if isinstance(value, str)
and value
in FIXED_ADDRESSES:
286 if isinstance(value, str)
and value.startswith(
"agent-"):
288 return validate_agent_id(value)
291 _dbg(
"OC_CHAT.MAIN", func, f
"VALIDATION_ERROR: address {value!r}", 3)
293 f
"{value!r} is not a hub address; expected one of "
294 f
"{', '.join(FIXED_ADDRESSES)}, or an agent-NNN instance identity"
299 """Refuse *kind* unless it is in :data:`SUPPORTED_KINDS`, by name."""
301 if kind
not in SUPPORTED_KINDS:
302 accepted =
", ".join(sorted(SUPPORTED_KINDS))
303 _dbg(
"OC_CHAT.MAIN", func, f
"VALIDATION_ERROR: kind {kind!r}", 3)
304 raise KindError(f
"unknown hub kind {kind!r}; the hub carries: {accepted}")
310@dataclass(frozen=True)
312 """Who is speaking, through which channel, and what act proves it.
314 ``correlation`` is what makes the claim checkable, and what it holds
315 depends on the channel: for ``telegram`` the inbound message id, the
316 audit id and the content hash, which together name one tap the
317 connector recorded; for ``lane`` the source lane and event id; for
318 ``cli`` the process user and working directory; empty for
319 ``session``, because an agent speaking in its own session has no
320 off-process act to point at -- which is exactly why the human rule
321 refuses that channel.
323 It is stored as an immutable mapping: a correlation a caller can edit
324 after :func:`check_ruling_provenance` has passed is not evidence.
334 correlation: Mapping[str, str] = field(default_factory=dict)
337 """Validate the three vocabularies and freeze the correlation."""
338 func =
"Provenance.__post_init__"
341 _dbg(
"OC_CHAT.MAIN", func, f
"VALIDATION_ERROR: actor_type {self.actor_type!r}", 3)
343 f
"actor_type {self.actor_type!r} is not one of the registry's "
344 f
"three: {', '.join(ACTOR_TYPES)}"
346 if self.
channel not in CHANNELS:
347 _dbg(
"OC_CHAT.MAIN", func, f
"VALIDATION_ERROR: channel {self.channel!r}", 3)
349 f
"channel {self.channel!r} is not declared; the hub knows: "
350 f
"{', '.join(CHANNELS)}"
352 frozen = MappingProxyType(
353 {str(key): str(value)
for key, value
in dict(self.
correlation).items()}
355 object.__setattr__(self,
"correlation", frozen)
358 """This provenance as plain JSON-safe data, for a log line or a row."""
368 """Rebuild a provenance from what :meth:`as_dict` wrote."""
369 correlation = raw.get(
"correlation")
or {}
370 if not isinstance(correlation, Mapping):
373 actor=str(raw.get(
"actor",
"")),
374 actor_type=str(raw.get(
"actor_type",
"")),
375 channel=str(raw.get(
"channel",
"")),
376 correlation={str(k): str(v)
for k, v
in correlation.items()},
380@dataclass(frozen=True)
382 """One addressed message. Construct through :func:`new_message`."""
400 in_reply_to: str |
None =
None
402 requires: str |
None =
None
404 answer: str |
None =
None
406 provenance: Provenance = field(
408 actor=ADDRESS_ORCHESTRATOR, actor_type=
"orchestrator_agent",
415 """The first non-blank line of the body -- what a lane envelope carries."""
416 for line
in self.
body.splitlines():
422 """This message as plain JSON-safe data."""
439 """Rebuild a message from what :meth:`as_dict` wrote.
441 Deliberately NOT re-validated through :func:`new_message`: this
442 reads back what the hub itself recorded, and a stored message
443 that no longer passes today's rules is a fact about the log, not
444 an input to reject. The readers that care assert their own
445 invariants; this one reconstructs.
448 message_id=str(raw[
"message_id"]),
450 kind=str(raw[
"kind"]),
451 sender=str(raw[
"sender"]),
452 recipient=str(raw[
"recipient"]),
454 body=str(raw.get(
"body",
"")),
458 provenance=Provenance.from_mapping(
459 raw.get(
"provenance")
or {}
465 return None if value
is None else str(value)
468@dataclass(frozen=True)
470 """One recorded message: its position in the log, and the message."""
477@dataclass(frozen=True)
479 """What a carrier reports when it moved one message."""
484 detail: str |
None =
None
496 provenance: Provenance,
497 subject: str |
None =
None,
498 in_reply_to: str |
None =
None,
499 requires: str |
None =
None,
500 answer: str |
None =
None,
501 now: datetime |
None =
None,
502 entropy: bytes |
None =
None,
504 """Build one validated :class:`HubMessage`, or refuse it by name.
506 The ONE construction path, so every shape rule is checked in one
507 place: the kind is in the closed set, the two addresses parse, and
508 the three kind-conditional fields agree with the kind. A ruling must
509 name the ask it answers and carry an answer from the reply
510 vocabulary; only an ask may declare what it requires.
513 AddressError: VALIDATION_ERROR (L3) -- a party is not an address.
514 KindError: VALIDATION_ERROR (L3) -- the kind is unknown, or a
515 kind-conditional field is present where it may not be, or
516 absent where it must be.
517 ProvenanceError: VALIDATION_ERROR (L3) -- from
518 :class:`Provenance`'s own validation.
525 if kind == KIND_RULING:
528 "a ruling must name the ask it answers in in_reply_to; a "
529 "ruling that answers nothing is a claim, not an answer"
531 if answer
not in RULING_ANSWERS:
533 f
"a ruling's answer must be one of {', '.join(RULING_ANSWERS)}; "
534 f
"got {answer!r}. That vocabulary is the connector's, so any "
535 "human channel can produce it."
538 if in_reply_to
is not None:
539 raise KindError(f
"only a ruling may set in_reply_to; {kind!r} may not")
540 if answer
is not None:
541 raise KindError(f
"only a ruling may carry an answer; {kind!r} may not")
544 if requires
not in SUPPORTED_REQUIRES:
546 f
"an ask must require one of {', '.join(SUPPORTED_REQUIRES)}; "
549 elif requires
is not None:
550 raise KindError(f
"only an ask may set requires; {kind!r} may not")
553 message_id=
new_ulid(now
or datetime.now(timezone.utc), entropy=entropy),
560 in_reply_to=in_reply_to,
563 provenance=provenance,
565 _dbg(
"OC_CHAT.MAIN", func, f
"{kind} {sender} -> {recipient} ({message.message_id})", 3)
573 """Refuse *ruling* unless its provenance supports answering *ask*.
575 THE one place the human rule lives. An ask that requires a human is
576 answerable only by a ``human_operator`` actor, on a channel a human
577 can actually speak through (:data:`HUMAN_CHANNELS`), with a non-empty
578 correlation naming the act that carried the answer. An
579 ``orchestrator_agent`` or ``worker_agent`` actor can never satisfy
580 it, which is the design note's "an agent-relayed yes is
581 distinguishable from the human's" made mechanical rather than
584 An ask that requires only the orchestrator is answerable by the
585 orchestrator in its own session, because routing an ask to the human
586 and waiting is autonomous while answering a human's ask is not.
589 ProvenanceError: SECURITY_ERROR (L1) -- the message names the
590 actor type, the channel and what was required.
592 func =
"check_ruling_provenance"
593 if ruling.in_reply_to != ask.message_id:
595 f
"ruling {ruling.message_id} has in_reply_to "
596 f
"{ruling.in_reply_to!r} and cannot answer ask {ask.message_id}"
598 if ask.requires != REQUIRES_HUMAN:
601 provenance = ruling.provenance
602 if provenance.actor_type != HUMAN_ACTOR_TYPE:
603 _dbg(
"OC_CHAT.MAIN", func,
604 f
"SECURITY_ERROR: {provenance.actor_type} ruled on a human ask", 1,
607 f
"ask {ask.message_id} requires a human, and a ruling from actor_type "
608 f
"{provenance.actor_type!r} cannot supply one: only "
609 f
"{HUMAN_ACTOR_TYPE!r} may. A security approval is in the operator's "
610 "own voice and is never relayed by an agent."
612 if provenance.channel
not in HUMAN_CHANNELS:
613 _dbg(
"OC_CHAT.MAIN", func,
614 f
"SECURITY_ERROR: human ruling on channel {provenance.channel}", 1,
617 f
"ask {ask.message_id} requires a human, and channel "
618 f
"{provenance.channel!r} cannot carry one: a human answers through "
619 f
"{' or '.join(HUMAN_CHANNELS)}, each of which leaves a trace "
620 "outside the answering process."
622 if not provenance.correlation:
623 _dbg(
"OC_CHAT.MAIN", func,
"SECURITY_ERROR: human ruling with no correlation",
626 f
"ask {ask.message_id} requires a human, and the ruling's correlation "
627 "is empty: an answer that names no channel act is not attributable "
628 "to anyone, which is the property the rule exists to keep."
630 _dbg(
"OC_CHAT.MAIN", func,
631 f
"human ruling on {ask.message_id} accepted via {provenance.channel}", 2)
639 """The log, defined once and implemented by both backends."""
641 def record(self, message: HubMessage) -> HubEntry:
642 """Append *message* and return the entry it became."""
647 kind: str |
None =
None,
648 recipient: str |
None =
None,
649 subject: str |
None =
None,
650 since_seq: int |
None =
None,
651 ) -> tuple[HubEntry, ...]:
652 """Recorded entries, oldest first, narrowed by whatever is given."""
654 def open_asks(self, *, recipient: str |
None =
None) -> tuple[HubEntry, ...]:
655 """Asks with no ruling in the log -- the questions still waiting."""
658 """The ruling that answered *ask_id*, or ``None``."""
663 """Something that can move a message to a recipient."""
669 """Can this carrier reach *address* right now?"""
671 def deliver(self, message: HubMessage) -> DeliveryReceipt:
672 """Move *message*, or raise."""
679 """Validate, record, then route. In that order, always."""
681 def __init__(self, *, log: HubLog, carriers: tuple[Carrier, ...] = ()) ->
None:
682 func =
"Hub.__init__"
685 _dbg(
"OC_CHAT.MAIN", func,
686 f
"over {len(self.carriers)} carrier(s): "
687 f
"{', '.join(c.name for c in self.carriers) or 'none'}", 2)
689 def send(self, message: HubMessage) -> DeliveryReceipt:
690 """Record *message*, then hand it to the first present carrier.
692 The order is the contract. A ruling is checked against its ask
693 BEFORE the record, because a ruling the human rule refuses must
694 leave no trace of having been accepted; everything else is
695 recorded first, because the log is the truth and a carrier that
696 fails afterwards has lost a delivery, not a message.
699 ProvenanceError: SECURITY_ERROR (L1) -- a ruling the human
700 rule refuses. Nothing is recorded and nothing is sent.
701 UnknownAskError: VALIDATION_ERROR (L3) -- a ruling naming
702 something the log does not hold as an ask.
703 CarrierError: EXTERNAL_ERROR (L2) -- no carrier is present
704 for the recipient, or the chosen one raised. The entry
708 if message.kind == KIND_RULING:
711 entry = self.
log.record(message)
712 _dbg(
"OC_CHAT.MAIN", func,
713 f
"recorded seq {entry.seq}: {message.kind} -> {message.recipient}", 2)
716 if not carrier.present(message.recipient):
719 receipt = carrier.deliver(message)
720 except Exception
as exc:
721 _dbg(
"OC_CHAT.MAIN", func,
722 f
"EXTERNAL_ERROR: {carrier.name} failed: {exc}", 2)
724 f
"carrier {carrier.name!r} could not deliver "
725 f
"{message.message_id} to {message.recipient}: {exc}. The log "
726 f
"entry at seq {entry.seq} stands; the message was sent."
728 _dbg(
"OC_CHAT.MAIN", func,
729 f
"{carrier.name} delivered {message.message_id}", 2)
732 names =
", ".join(c.name
for c
in self.
carriers)
or "none"
733 _dbg(
"OC_CHAT.MAIN", func,
734 f
"EXTERNAL_ERROR: no carrier present for {message.recipient}", 2)
736 f
"no carrier is present for {message.recipient}; tried: {names}. "
737 f
"The log entry at seq {entry.seq} stands; the message was sent."
740 def attach(self, address: str, callback:
"Callable[[HubMessage], None]") ->
None:
741 """Register *callback* as the in-process reader for *address*.
743 Delegated to the first carrier that accepts attachments, which
744 today is ``oc_chat.core.hub_bus.BusCarrier`` -- a CONSUMER's
745 module, injected through ``carriers=``, which this package neither
746 imports nor names at any depth beyond this sentence. The
747 delegation is duck-typed ON PURPOSE: this module declares what a
748 hub IS, and a hub that imported a specific carrier to offer this
749 method would have the routing decision baked into the contract
750 instead of injected beside it. Plan 272 P4 is the proof that the
751 duck typing was load-bearing rather than decorative: the bus
752 carrier stayed behind when the hub moved, and nothing here
756 AddressError: VALIDATION_ERROR (L3) -- *address* is not one.
757 CarrierError: VALIDATION_ERROR (L3) -- no injected carrier
758 accepts attachments, so there is nowhere to put it.
764 """Remove *address*'s in-process reader. See :meth:`attach`."""
769 """The first carrier exposing ``attach``, or a refusal naming the set."""
771 if hasattr(carrier,
"attach")
and hasattr(carrier,
"detach"):
773 names =
", ".join(c.name
for c
in self.
carriers)
or "none"
775 f
"no injected carrier accepts an attach; tried: {names}. An "
776 "in-process reader needs a carrier that can hold one."
785 provenance: Provenance,
786 now: datetime |
None =
None,
787 entropy: bytes |
None =
None,
789 """Answer the open ask *ask_id* and send the ruling back to its asker.
791 The ruling is BUILT from the ask rather than supplied beside it,
792 so its recipient, subject and ``in_reply_to`` cannot disagree
793 with the question it answers.
796 UnknownAskError: VALIDATION_ERROR (L3) -- no such message, it
797 is not an ask, or it already has a ruling.
798 ProvenanceError: SECURITY_ERROR (L1) -- the human rule.
802 existing = self.
log.ruling_for(ask_id)
803 if existing
is not None:
805 f
"ask {ask_id} was already ruled by {existing.message.message_id} "
806 f
"({existing.message.answer}); a second ruling would make the log "
807 "ambiguous about which answer was acted on"
811 sender=provenance.actor,
812 recipient=ask.sender,
815 in_reply_to=ask.message_id,
817 provenance=provenance,
822 entry = self.
log.record(ruling)
823 _dbg(
"OC_CHAT.MAIN", func, f
"{answer} on {ask_id} (seq {entry.seq})", 2)
828 """Route an already-recorded message; a failure is still a refusal."""
829 func =
"Hub._deliver_recorded"
831 if not carrier.present(message.recipient):
834 return carrier.deliver(message)
835 except Exception
as exc:
836 _dbg(
"OC_CHAT.MAIN", func,
837 f
"EXTERNAL_ERROR: {carrier.name} failed: {exc}", 2)
839 f
"carrier {carrier.name!r} could not deliver "
840 f
"{message.message_id} to {message.recipient}: {exc}. The log "
841 "entry stands; the message was sent."
844 f
"no carrier is present for {message.recipient}. The log entry "
845 "stands; the message was sent."
849 """The ask *ask_id* names, or a refusal saying what it found instead."""
850 for entry
in self.
log.entries():
851 if entry.message.message_id != ask_id:
853 if entry.message.kind != KIND_ASK:
855 f
"{ask_id} is a {entry.message.kind!r}, not an ask; only an "
856 "ask can be ruled on"
864 "ADDRESS_OPERATOR_LOCAL",
865 "ADDRESS_OPERATOR_REMOTE",
866 "ADDRESS_ORCHESTRATOR",
868 "CROCKFORD_ALPHABET",
879 "REQUIRES_ORCHESTRATOR",
882 "SUPPORTED_REQUIRES",
897 "check_ruling_provenance",
DeliveryReceipt deliver(self, HubMessage message)
bool present(self, str address)
DeliveryReceipt|None _deliver_recorded(self, HubMessage message)
HubEntry rule(self, str ask_id, *, str answer, str body, Provenance provenance, datetime|None now=None, bytes|None entropy=None)
None __init__(self, *, HubLog log, tuple[Carrier,...] carriers=())
None attach(self, str address, "Callable[[HubMessage], None]" callback)
HubMessage _ask_for(self, str ask_id)
DeliveryReceipt send(self, HubMessage message)
None detach(self, str address)
tuple[HubEntry,...] open_asks(self, *, str|None recipient=None)
tuple[HubEntry,...] entries(self, *, str|None kind=None, str|None recipient=None, str|None subject=None, int|None since_seq=None)
HubEntry|None ruling_for(self, str ask_id)
HubEntry record(self, HubMessage message)
"HubMessage" from_mapping(cls, Mapping[str, object] raw)
dict[str, object] as_dict(self)
dict[str, object] as_dict(self)
"Provenance" from_mapping(cls, Mapping[str, object] raw)
str new_ulid(datetime now, *, bytes|None entropy=None)
str parse_address(str value)
None check_ruling_provenance(HubMessage ask, HubMessage ruling)
HubMessage new_message(*, str kind, str sender, str recipient, str body, Provenance provenance, str|None subject=None, str|None in_reply_to=None, str|None requires=None, str|None answer=None, datetime|None now=None, bytes|None entropy=None)
str|None _optional_str(object value)
str _encode(int value, int length)
None check_kind(str kind)