|
oc_orchestrator
|
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" |
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``)
|
protected |
|
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.
|
protected |
| None oc_orchestrator.hub.check_kind | ( | str | kind | ) |
| 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.
| 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.
| 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.
| 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.
| tuple oc_orchestrator.hub.ACTOR_TYPES |
| str oc_orchestrator.hub.ADDRESS_OPERATOR_LOCAL = "operator@local" |
| str oc_orchestrator.hub.ADDRESS_OPERATOR_REMOTE = "operator@remote" |
| str oc_orchestrator.hub.ADDRESS_ORCHESTRATOR = "orchestrator" |
| tuple oc_orchestrator.hub.CHANNELS = ("session", "cli", "lane", "telegram") |
| str oc_orchestrator.hub.CROCKFORD_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" |
| tuple oc_orchestrator.hub.FIXED_ADDRESSES |
| tuple oc_orchestrator.hub.HUMAN_CHANNELS = ("cli", "telegram") |
| str oc_orchestrator.hub.REQUIRES_ORCHESTRATOR = "orchestrator" |
| tuple oc_orchestrator.hub.RULING_ANSWERS = ("YES", "NO", "ACK", "HOLD") |
| frozenset oc_orchestrator.hub.SUPPORTED_KINDS |
| tuple oc_orchestrator.hub.SUPPORTED_REQUIRES = (REQUIRES_HUMAN, REQUIRES_ORCHESTRATOR) |