The registry backend for the hub log: one message, one operation-log row.
Purpose
-------
Plan 259 P4. The hub has one log contract and two backends, and this is
the one oc_chat plugs in. It lives on the REGISTRY side on purpose,
exactly as ``registry/queue_backend.py`` does: the operation log already
carries actor, actor type, credential id and an append-only guarantee
enforced by a database trigger, so the hub inherits those instead of
growing a second copy of them.
A hub message is ONE row of ``operation_log`` with
``operation_type='hub.message'``, ``command`` = the hub kind and
``related_plan_id`` = the subject when the subject is a plan id. There is
no projection: a message changes no projected state, it IS the state, and
``SQLiteBackend._apply_domain_projection`` is an ``if``/``elif`` chain
with no ``else``, so an unmatched type projects nothing. **No projector
was added, because none is required** -- a witness asserts the
observable half of that.
**The row's signature and the message's claim are different things.**
``actor_type`` and ``hmac_key_id`` are written by the storage layer from
the SLOT, and a caller cannot choose them; the message's ``provenance``
is a payload field a caller writes freely. The hub's human rule is
enforced on the claim, in ``core/hub.check_ruling_provenance``, and this
module does not second-guess it -- but the row beside it records who the
process was, which is the cross-check the design note asked for.
The measured preflight fact
---------------------------
A ``human_operator`` row written from a CLI process that supplies no
``auth_proof`` **is accepted, unsigned**. ``Registry._preflight_auth``
(``registry/api.py:115``) runs the role guard and returns ``None`` the
moment ``auth_proof`` is ``None``; ``hub.message`` matches no entry in
``ROLE_ALLOWSETS`` and no prefix in ``PREFIX_ALLOWSETS``, so the guard
permits all three slot kinds. The row still lands carrying an
``hmac_key_id``, because the storage layer falls back to the slot's
ACTIVE credential when the caller supplies none -- so that key id names a
credential which did not sign this row.
So the registry mode's signature is a **claim**, like the file mode's,
and the hub's own human rule is what does the work in both. That is a
finding, not a design; it is filed rather than fixed here, because
demanding a credential would be a security-layer change and those are
the operator's.
Responsibilities
----------------
- :class:`RegistryHubLog` -- the four :class:`~oc_orchestrator.hub.HubLog`
operations over the operation log.
- :data:`HUB_OPERATION_TYPE` -- the one operation type, pinned as a
constant so a witness can assert it.
- :class:`UnregisteredSenderError` -- the adapter's named refusal.
Diagnostics
-----------
Domain: OC_CHAT.MAIN
Levels:
L2 - lifecycle events (a message recorded)
L3 - semantic details (a refusal, a filter)
L4 - deep tracing (module load)
Contracts
---------
- This module MUTATES only through ``registry.domain.hub_message``: no
SQL write, no direct table update.
- The READ is one ``SELECT`` in this file rather than a fifth function in
``registry/readers.py``. That is the honest place: ``readers.py``
returns ``registry/dataclasses.py`` types, and a hub reader must return
:class:`~oc_orchestrator.hub.HubEntry` -- a DISPATCH-layer type. Putting
that import into the general reader library would push a dispatch type
into every consumer of it, including the registry CLI. The adapter is
already the one place the two vocabularies meet, so the read lives
here, exactly as ``queue_backend`` keeps its own ``plans`` projection
query rather than widening ``readers``.
- The import direction is registry -> dispatch, which is the permitted
one and the one ``registry/mirror_observer.py`` and
``registry/queue_backend.py`` already take. Nothing in the dispatch
layer imports this module; the CLI reaches it through one lazy import
behind ``--backend registry``.
- ``seq`` is the row's ``state_version``: monotonic within the store, as
the contract requires. It is NOT comparable with the file log's
line count, and the equivalence witness compares the FOLD rather than
the numbering for that reason.
Dependencies
------------
- oc_diagnostics (structured debug logging via _dbg)
- oc_orchestrator.hub (the contract; moved there by plan 272, and
named directly rather than through the shim because this module is
the registry-side backend of the seam, not a caller that always
spelled the old path)
- oc_chat.registry.api (the Registry facade)
- oc_chat.registry.domain (the typed hub operation)