feat: interim identifier registrar (RMGR-WP-0005-T01)

Add is_identifier_registrar so only the production instance may mint
hub UUIDs into repository files. Workstation hubs stay read/project
caches until deterministic derivation lands.

Also close RMGR-WP-0004-T05: register cutover is STATE-WP-0080-T02.
This commit is contained in:
tegwick 2026-08-18 21:49:08 +02:00
parent 0dde030ae3
commit f509da5606
5 changed files with 90 additions and 2 deletions

View file

@ -33,3 +33,24 @@ rmgr prefix-uniqueness --root ..
Registry: [`config/workplan-prefix-registry.yaml`](../config/workplan-prefix-registry.yaml).
Work: `RMGR-WP-0004-T01`, `RMGR-WP-0004-T08`.
## Identifier registrar (interim)
Until UUIDv5 derivation lands (`RMGR-WP-0005-T03`), only the registrar
instance may mint `state_hub_workstream_id` / `state_hub_task_id` into
files (`ADR-007` decision 2). Other hubs may read and project; they must
not write new hub primary keys into git.
| Signal | Registrar? |
| --- | --- |
| `STATEHUB_REGISTRAR=1` / `true` / `yes` / `on` | yes |
| `STATEHUB_REGISTRAR=0` / `false` / `no` / `off` | no |
| env unset, hostname starts with `railiance` | yes |
| env unset, any other hostname | no |
Accepted cost: new workplan/task registration requires connectivity to
the registrar. Disconnected work cannot register until T03. Implementation:
`repo_manager.registrar.is_identifier_registrar`; consumed by
`statehub fix-consistency` C-06 / C-11 / C-32.
Work: `RMGR-WP-0005-T01`.

View file

@ -0,0 +1,30 @@
"""Interim single-writer for hub identifiers (ADR-007 decision 2).
Until deterministic derivation (RMGR-WP-0005-T03) ships, exactly one
instance may mint ``state_hub_workstream_id`` / ``state_hub_task_id``
into repository files. Other instances may read and project; they must
not write new hub primary keys into git-tracked files.
The registrar is the automated production instance (hostname prefix
``railiance``). Override with ``STATEHUB_REGISTRAR=1`` or ``=0``.
"""
from __future__ import annotations
import os
import socket
_TRUE = frozenset({"1", "true", "yes", "on"})
_FALSE = frozenset({"0", "false", "no", "off"})
REGISTRAR_HOST_PREFIX = "railiance"
def is_identifier_registrar(*, hostname: str | None = None) -> bool:
"""Return True if this process may mint hub IDs into repository files."""
raw = os.environ.get("STATEHUB_REGISTRAR", "").strip().lower()
if raw in _TRUE:
return True
if raw in _FALSE:
return False
host = (hostname if hostname is not None else socket.gethostname()).strip().lower()
return host.startswith(REGISTRAR_HOST_PREFIX)

25
tests/test_registrar.py Normal file
View file

@ -0,0 +1,25 @@
from repo_manager.registrar import is_identifier_registrar
def test_env_true_wins_over_hostname(monkeypatch):
monkeypatch.setenv("STATEHUB_REGISTRAR", "1")
assert is_identifier_registrar(hostname="bnt-lap001") is True
def test_env_false_wins_over_production_hostname(monkeypatch):
monkeypatch.setenv("STATEHUB_REGISTRAR", "0")
assert is_identifier_registrar(hostname="railiance") is False
def test_hostname_prefix_when_env_unset(monkeypatch):
monkeypatch.delenv("STATEHUB_REGISTRAR", raising=False)
assert is_identifier_registrar(hostname="railiance") is True
assert is_identifier_registrar(hostname="railiance-prod") is True
assert is_identifier_registrar(hostname="bnt-lap001") is False
def test_env_aliases(monkeypatch):
monkeypatch.setenv("STATEHUB_REGISTRAR", "yes")
assert is_identifier_registrar(hostname="other") is True
monkeypatch.setenv("STATEHUB_REGISTRAR", "off")
assert is_identifier_registrar(hostname="railiance") is False

View file

@ -170,7 +170,7 @@ flavor-blind template silently re-breaks conformant repos.
```task
id: RMGR-WP-0004-T05
status: todo
status: done
priority: medium
state_hub_task_id: "8402b8b6-2a1e-4236-b27d-d50b6161a176"
```
@ -192,6 +192,12 @@ Follow the dual-run strangler pattern already proven in `RMGR-WP-0002` and
points at `rmgr scaffold`. T03 is done. Cutover / inventory move can start
alongside `STATE-WP-0080-T02`.
**Result (2026-08-18):** Cutover is live via `STATE-WP-0080-T02`: project
flavor delegates to `rmgr scaffold` (or registers only when `GOAL.md`
exists); durable repos keep hub templating until `STATE-WP-0080-T04`
deletes it. Inventory `caller:custodian-cli` is already `move`
`repo-manager`. Coordination remains `SHR-WP-0001`.
## Add compatibility and regression tests
```task

View file

@ -50,7 +50,7 @@ model; a file carrying a hub's private key is the file holding hub state.
```task
id: RMGR-WP-0005-T01
status: todo
status: done
priority: high
state_hub_task_id: "b57a6882-280d-4f0a-9c73-899843dfc3d3"
```
@ -69,6 +69,12 @@ workstation hubs are rebuildable caches (`ADR-010` decision 2).
Interim, and deliberately so — it trades availability for correctness, and T03
removes the need for the trade.
**Result (2026-08-18):** `repo_manager.registrar.is_identifier_registrar`
(`STATEHUB_REGISTRAR` env, else hostname prefix `railiance`).
`statehub fix-consistency` skips C-06 / C-11 / C-32 mint+writeback when
this instance is not the registrar. Read/project checks are unchanged.
Cost documented in `docs/repository-standards_v0.1.md`.
## Re-register identifiers minted outside the registrar
```task