oc_orchestrator
Loading...
Searching...
No Matches
oc_orchestrator.hub Namespace Reference

Classes

class  HubError
class  AddressError
class  KindError
class  ProvenanceError
class  CarrierError
class  UnknownAskError
class  Provenance
class  HubMessage
class  HubEntry
class  DeliveryReceipt
class  HubLog
class  Carrier
class  Hub

Functions

str _encode (int value, int length)
str new_ulid (datetime now, *, bytes|None entropy=None)
str _now_iso ()
str parse_address (str value)
None check_kind (str kind)
str|None _optional_str (object value)
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)
None check_ruling_provenance (HubMessage ask, HubMessage ruling)

Variables

str ADDRESS_ORCHESTRATOR = "orchestrator"
str ADDRESS_OPERATOR_LOCAL = "operator@local"
str ADDRESS_OPERATOR_REMOTE = "operator@remote"
tuple FIXED_ADDRESSES
str KIND_DISPATCH = "dispatch"
str KIND_REPORT = "report"
str KIND_ALERT = "alert"
str KIND_ASK = "ask"
str KIND_RULING = "ruling"
str KIND_INSTRUCTION = "instruction"
frozenset SUPPORTED_KINDS
str REQUIRES_HUMAN = "human"
str REQUIRES_ORCHESTRATOR = "orchestrator"
tuple SUPPORTED_REQUIRES = (REQUIRES_HUMAN, REQUIRES_ORCHESTRATOR)
tuple RULING_ANSWERS = ("YES", "NO", "ACK", "HOLD")
tuple ACTOR_TYPES
tuple CHANNELS = ("session", "cli", "lane", "telegram")
tuple HUMAN_CHANNELS = ("cli", "telegram")
str HUMAN_ACTOR_TYPE = "human_operator"
str CROCKFORD_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"

Detailed Description

The hub's contract: one address grammar, six kinds, one human rule.

Purpose
-------
Plan 259 P2, from the factory-decomposition design note ยง4. The hub is a
MESSAGE CONTRACT, not a transport. Two pipes and a reply path already
exist -- the worker-report lane, the session-alert lane, and the
connector's correlated inbound reply -- and what turns them into a hub is
three additions, all declared here:

**Addresses**, so a message is ROUTED rather than deposited. Four forms
and no others: ``orchestrator``, ``operator@local`` (the control centre,
which today is the CLI in the operator's own session), ``operator@remote``
(the connector), and ``agent-NNN`` for each worker instance.

**Kinds with replies.** ``report`` and ``alert`` flow one way. An ``ask``
expects an answer and a ``ruling`` IS that answer, naming the ask it
answers, so a question any human channel can answer is a first-class
thing. ``dispatch`` and ``instruction`` are records of what was done and
what was told, so the orchestrator's picture of the fleet is the log and
not a guess.

**Provenance on every message, and a rule about rulings.** A ruling
carries who answered and through which channel. This is not a nicety: it
derives from an operator ruling already in force, that a security
approval must be in the operator's own voice and never relayed by an
agent. In a hub where the orchestrator agent, the control centre and
Telegram can all answer, an unsigned reply makes an agent-relayed yes
indistinguishable from the human's. :func:`check_ruling_provenance` is
the ONE place that rule lives.

Responsibilities
----------------
- :func:`parse_address` -- the whole grammar, refusing by name. Agent
  ids are validated by
  :func:`oc_orchestrator.agent_instances.validate_agent_id`, IMPORTED
  rather than reimplemented, so there is one definition of ``agent-NNN``
  in the tree.
- :data:`SUPPORTED_KINDS` -- the six, checked in one place.
- :class:`Provenance` / :class:`HubMessage` -- frozen value types;
  :func:`new_message` is the one construction path.
- :func:`check_ruling_provenance` -- the human rule.
- :class:`HubLog` / :class:`Carrier` -- the two Protocols the backends
  and the carriers satisfy.
- :class:`Hub` -- ``send`` (record, then route) and ``rule`` (build the
  ruling from the ask and send it).
- :class:`HubError` and its named subclasses -- every refusal.

Diagnostics
-----------
Domain: OC_CHAT.MAIN
Levels:
    L2 - lifecycle events (a message sent, a carrier chosen)
    L3 - semantic details (a refusal, with what it refused)
    L4 - deep tracing (module load)

Contracts
---------
- **The log is the truth.** :meth:`Hub.send` RECORDS BEFORE IT DELIVERS,
  and a carrier failure after the record is a :class:`CarrierError` with
  the entry left standing -- the file lane redelivers, and a message
  that reached no carrier is still a message that was sent. That is
  design-note invariant 1 made mechanical: direct does not mean unlogged.
- **A ruling on a human-required ask is refused from any agent actor.**
  Not discouraged, not flagged: refused, by construction, in
  :func:`check_ruling_provenance`, which :meth:`Hub.send` calls before
  it records. A refused ruling is never recorded and never delivered.
- ``answer`` is one of :data:`RULING_ANSWERS` -- the connector's reply
  vocabulary -- so a Telegram tap, a CLI word and a control-centre
  button all produce the same four values.
- :class:`Provenance.correlation` is an IMMUTABLE mapping. A correlation
  a caller can edit after the check is not evidence.
- ``message_id`` is a ULID: :func:`new_ulid` and the Crockford alphabet
  are lifted VERBATIM from ``plan_status.py:450-464`` and ``:132``,
  waiver text included, because the lane readers compare these bytes and
  a second algorithm here would be a second answer. The two now sit in
  one package and could in principle be deduplicated; plan 272 P8 leaves
  that to the follow-up that also settles the comms-root rule, because a
  shared ULID module is a decision about the package's shape rather than
  a consequence of the move.
- This module imports nothing but the standard library, the diagnostics
  runtime and the one agent-id validator beside it -- no registry, no
  consumer, no toolchain. A witness measures that with the committed
  extractor, and ``tests/test_t1_leaf_guard.py`` measures it statically.
- Nothing here reads the process working directory, opens a file, or
  starts a subprocess. Routing is a decision; moving bytes is a carrier's
  job.

Dependencies
------------
- oc_diagnostics (structured debug logging via _dbg)
- oc_orchestrator.agent_instances (the ONE definition of ``agent-NNN``)

Function Documentation

◆ _encode()

str oc_orchestrator.hub._encode ( int value,
int length )
protected

Definition at line 201 of file hub.py.

Here is the caller graph for this function:

◆ _now_iso()

str oc_orchestrator.hub._now_iso ( )
protected
A local-with-offset ISO 8601 stamp, seconds resolution.

The project-wide convention, matching ``work_queue_file._now_iso``
and ``plan_inventory._now_iso``. An offset is not decoration: a
stamp without one is not a point in time.

Definition at line 227 of file hub.py.

Here is the caller graph for this function:

◆ _optional_str()

str | None oc_orchestrator.hub._optional_str ( object value)
protected

Definition at line 464 of file hub.py.

Here is the caller graph for this function:

◆ check_kind()

None oc_orchestrator.hub.check_kind ( str kind)
Refuse *kind* unless it is in :data:`SUPPORTED_KINDS`, by name.

Definition at line 298 of file hub.py.

Here is the caller graph for this function:

◆ check_ruling_provenance()

None oc_orchestrator.hub.check_ruling_provenance ( HubMessage ask,
HubMessage ruling )
Refuse *ruling* unless its provenance supports answering *ask*.

THE one place the human rule lives. An ask that requires a human is
answerable only by a ``human_operator`` actor, on a channel a human
can actually speak through (:data:`HUMAN_CHANNELS`), with a non-empty
correlation naming the act that carried the answer. An
``orchestrator_agent`` or ``worker_agent`` actor can never satisfy
it, which is the design note's "an agent-relayed yes is
distinguishable from the human's" made mechanical rather than
procedural.

An ask that requires only the orchestrator is answerable by the
orchestrator in its own session, because routing an ask to the human
and waiting is autonomous while answering a human's ask is not.

Raises:
    ProvenanceError: SECURITY_ERROR (L1) -- the message names the
        actor type, the channel and what was required.

Definition at line 572 of file hub.py.

Here is the caller graph for this function:

◆ new_message()

HubMessage oc_orchestrator.hub.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 )
Build one validated :class:`HubMessage`, or refuse it by name.

The ONE construction path, so every shape rule is checked in one
place: the kind is in the closed set, the two addresses parse, and
the three kind-conditional fields agree with the kind. A ruling must
name the ask it answers and carry an answer from the reply
vocabulary; only an ask may declare what it requires.

Raises:
    AddressError: VALIDATION_ERROR (L3) -- a party is not an address.
    KindError: VALIDATION_ERROR (L3) -- the kind is unknown, or a
        kind-conditional field is present where it may not be, or
        absent where it must be.
    ProvenanceError: VALIDATION_ERROR (L3) -- from
        :class:`Provenance`'s own validation.

Definition at line 490 of file hub.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ new_ulid()

str oc_orchestrator.hub.new_ulid ( datetime now,
* ,
bytes | None entropy = None )
Return a 26-character ULID: 48 bits of time, 80 bits of randomness.

Lifted VERBATIM from ``scripts/plan_status.py:445-460``, which says
the same of ``oc_connect.comms.new_ulid``. Time-first encoding makes
lexical order arrival order, which is what lets a reader walk the hub
lane with a plain directory sort, exactly as the other two lanes are
walked.

Definition at line 209 of file hub.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ parse_address()

str oc_orchestrator.hub.parse_address ( str value)
Return *value* if it is an address, else refuse it by name.

The whole grammar, in one function. ``agent-NNN`` is delegated to
:func:`oc_orchestrator.agent_instances.validate_agent_id` -- imported,
never reimplemented -- so the tree has exactly one definition of what
an instance identity looks like and a change there is a change here.

Raises:
    AddressError: VALIDATION_ERROR (L3) -- the message names the
        value it refused and every form it accepts.

Definition at line 271 of file hub.py.

Here is the caller graph for this function:

Variable Documentation

◆ ACTOR_TYPES

tuple oc_orchestrator.hub.ACTOR_TYPES
Initial value:
1= (
2 "human_operator",
3 "orchestrator_agent",
4 "worker_agent",
5)

Definition at line 170 of file hub.py.

◆ ADDRESS_OPERATOR_LOCAL

str oc_orchestrator.hub.ADDRESS_OPERATOR_LOCAL = "operator@local"

Definition at line 126 of file hub.py.

◆ ADDRESS_OPERATOR_REMOTE

str oc_orchestrator.hub.ADDRESS_OPERATOR_REMOTE = "operator@remote"

Definition at line 129 of file hub.py.

◆ ADDRESS_ORCHESTRATOR

str oc_orchestrator.hub.ADDRESS_ORCHESTRATOR = "orchestrator"

Definition at line 121 of file hub.py.

◆ CHANNELS

tuple oc_orchestrator.hub.CHANNELS = ("session", "cli", "lane", "telegram")

Definition at line 178 of file hub.py.

◆ CROCKFORD_ALPHABET

str oc_orchestrator.hub.CROCKFORD_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"

Definition at line 198 of file hub.py.

◆ FIXED_ADDRESSES

tuple oc_orchestrator.hub.FIXED_ADDRESSES
Initial value:
1= (
2 ADDRESS_ORCHESTRATOR,
3 ADDRESS_OPERATOR_LOCAL,
4 ADDRESS_OPERATOR_REMOTE,
5)

Definition at line 133 of file hub.py.

◆ HUMAN_ACTOR_TYPE

str oc_orchestrator.hub.HUMAN_ACTOR_TYPE = "human_operator"

Definition at line 187 of file hub.py.

◆ HUMAN_CHANNELS

tuple oc_orchestrator.hub.HUMAN_CHANNELS = ("cli", "telegram")

Definition at line 184 of file hub.py.

◆ KIND_ALERT

str oc_orchestrator.hub.KIND_ALERT = "alert"

Definition at line 145 of file hub.py.

◆ KIND_ASK

str oc_orchestrator.hub.KIND_ASK = "ask"

Definition at line 146 of file hub.py.

◆ KIND_DISPATCH

str oc_orchestrator.hub.KIND_DISPATCH = "dispatch"

Definition at line 143 of file hub.py.

◆ KIND_INSTRUCTION

str oc_orchestrator.hub.KIND_INSTRUCTION = "instruction"

Definition at line 148 of file hub.py.

◆ KIND_REPORT

str oc_orchestrator.hub.KIND_REPORT = "report"

Definition at line 144 of file hub.py.

◆ KIND_RULING

str oc_orchestrator.hub.KIND_RULING = "ruling"

Definition at line 147 of file hub.py.

◆ REQUIRES_HUMAN

str oc_orchestrator.hub.REQUIRES_HUMAN = "human"

Definition at line 157 of file hub.py.

◆ REQUIRES_ORCHESTRATOR

str oc_orchestrator.hub.REQUIRES_ORCHESTRATOR = "orchestrator"

Definition at line 158 of file hub.py.

◆ RULING_ANSWERS

tuple oc_orchestrator.hub.RULING_ANSWERS = ("YES", "NO", "ACK", "HOLD")

Definition at line 165 of file hub.py.

◆ SUPPORTED_KINDS

frozenset oc_orchestrator.hub.SUPPORTED_KINDS
Initial value:
1= frozenset(
2 {KIND_DISPATCH, KIND_REPORT, KIND_ALERT, KIND_ASK, KIND_RULING, KIND_INSTRUCTION}
3)

Definition at line 152 of file hub.py.

◆ SUPPORTED_REQUIRES

tuple oc_orchestrator.hub.SUPPORTED_REQUIRES = (REQUIRES_HUMAN, REQUIRES_ORCHESTRATOR)

Definition at line 159 of file hub.py.