Skip to content

symfonic.capabilities.human.issuance

issuance

Where the issuance table and the retirement horizon actually live.

The redemption store decides the winner. It cannot, on its own, make a ledger authoritative: the record of which tokens exist, and the date the platform said it would stop supporting the legacy ones, has to outlive one worker's memory. Keep them in a dict and three things break the moment a second worker exists or the first one restarts — a legitimately issued, unspent token is refused as unissued so every in-flight pause becomes unresumable, the horizon one node recorded is unknown to the next, and the drain proof counts one process's rows while reading as a statement about the deployment.

Two implementations, one port. :class:InProcessIssuanceRecords is the single-process reference and says so: deployment_wide is False, and the ledger refuses to call a drain proof proven on top of it. :class:BackendIssuanceRecords puts the same three tables on the operator's persistence backend, keyed so a restart and a second worker read what the first one wrote.

This is deliberately a wider port than :class:~.contracts.ConditionalWritePort. Redemption gets one verb because a port that also offered a read would invite the read-then-write race LIB-TL-2 forbids; the issuance record has no such race to lose, and it has to be read back, so it asks for the reads openly.

BackendIssuanceRecords

BackendIssuanceRecords(backend: Any, *, durable: bool | None = None)

The same three tables on the operator's persistence backend.

Source code in src/symfonic/capabilities/human/issuance.py
def __init__(self, backend: Any, *, durable: bool | None = None) -> None:
    missing = [
        verb for verb in BACKEND_VERBS if not callable(getattr(backend, verb, None))
    ]
    if missing:
        raise InteractionConfigurationError(
            f"{type(backend).__name__} cannot hold the issuance table: it is "
            f"missing {', '.join(missing)}. Redemption needs one atomic "
            "conditional write and nothing more, but the issuance record and "
            "the retirement horizon have to be read back — by the next worker, "
            "and by this one after a restart — so this is the wider port"
        )
    self._backend = backend
    self._durable = declared_durability(backend) if durable is None else durable

InProcessIssuanceRecords

InProcessIssuanceRecords()

Three dicts. Correct for one process, and unwilling to claim more.

Source code in src/symfonic/capabilities/human/issuance.py
def __init__(self) -> None:
    self._issued: dict[str, IssuedToken] = {}
    self._consumed: dict[str, TokenConsumption] = {}
    self._horizon: RetirementHorizon | None = None

IssuedToken dataclass

IssuedToken(jti: str, scope_hash: str, name: str, issued_at: float, expires_at: float, legacy_pinned: bool = False, vector_hash: str = '')

One issuance row. Ids, times, and the one bit the drain gate reads.

RetirementHorizon dataclass

RetirementHorizon(at: float, reason: str)

SCP-FRZ-2: the date, and the operator's reason for it.

TokenConsumption dataclass

TokenConsumption(jti: str, scope_hash: str, name: str, consumed_at: float)

One redemption row: who won, and when. Losers are not recorded here.