Skip to content

symfonic.memory.backends.postgres_retirement_receipt

postgres_retirement_receipt

Opt-in versioned native receipts; no legacy identity is inferred (CORE-428).

Only the eraser receives signing material. The resolver is an explicit operator callable returning a retained key by ID, or None when custody was lost. Anchor key rotation remains unsupported; its separately supplied key stays unchanged.

ReceiptKeys

ReceiptKeys(key_id: str, key: bytes, resolver: Any)

Explicit active signer and retained verification keys; never runtime data.

Source code in src/symfonic/memory/backends/postgres_retirement_receipt.py
def __init__(self, key_id: str, key: bytes, resolver: Any) -> None:
    if type(key_id) is not str or not key_id or len(key_id) > 128:
        raise ValueError("receipt_key_id must be a nonempty string of at most 128 characters")
    if resolver is not None and not callable(resolver):
        raise TypeError("receipt_verification_keys must be a callable key-ID resolver")
    self.key_id, self._key, self._resolver = key_id, require_receipt_key(key), resolver

configure

configure(eraser: Any, pool: Any, schema: str, key: bytes, anchor: Any, key_id: str | None, resolver: Any, anchor_key: bytes | None) -> None

Keep receipt signing distinct from stable anchor-key custody.

Omit key_id to retain the historical ordinary eraser API. Versioned retirement requires the explicit migration and never relabels old jobs.

Source code in src/symfonic/memory/backends/postgres_retirement_receipt.py
def configure(
    eraser: Any,
    pool: Any,
    schema: str,
    key: bytes,
    anchor: Any,
    key_id: str | None,
    resolver: Any,
    anchor_key: bytes | None,
) -> None:
    """Keep receipt signing distinct from stable anchor-key custody.

    Omit key_id to retain the historical ordinary eraser API. Versioned
    retirement requires the explicit migration and never relabels old jobs.
    """
    from symfonic.memory.backends.postgres_erase_incarnation import anchored

    if type(schema) is not str or not schema:
        raise ValueError("the native eraser needs the schema it erases in (CORE-199)")
    if key_id is None and resolver is not None:
        raise ValueError("receipt_verification_keys requires receipt_key_id")
    eraser._pool, eraser._schema, eraser._ready = pool, schema, False
    eraser._receipt_key = require_receipt_key(key)
    eraser._receipt_keys = ReceiptKeys(key_id, key, resolver) if key_id is not None else None
    eraser.proof, eraser._guard = anchored(
        pool,
        anchor,
        schema,
        key if anchor_key is None else require_receipt_key(anchor_key),
    )

identity async

identity(conn: Any, eraser: Any) -> tuple[str, str]

Stable through process/server restart; external proof checks copy lineage.

Source code in src/symfonic/memory/backends/postgres_retirement_receipt.py
async def identity(conn: Any, eraser: Any) -> tuple[str, str]:
    """Stable through process/server restart; external proof checks copy lineage."""
    binding = await conn.fetchval(_BINDING)
    if binding is None:
        raise MemoryContractError(INVALID)
    anchor_conn = await eraser.proof.anchor_pool.acquire()
    try:
        store = await anchor_conn.fetchval(_ANCHOR_STORE)
    finally:
        await eraser.proof.anchor_pool.release(anchor_conn)
    if store is None:
        raise MemoryContractError(INVALID)
    anchor = json.dumps([*eraser.proof._key, json.loads(store)], separators=(",", ":"))
    return binding, anchor

key_commitment

key_commitment(key: bytes, key_id: str) -> str

Domain-separated non-secret commitment; key IDs cannot silently change keys.

Source code in src/symfonic/memory/backends/postgres_retirement_receipt.py
def key_commitment(key: bytes, key_id: str) -> str:
    """Domain-separated non-secret commitment; key IDs cannot silently change keys."""
    return hmac.new(key, _KEY_DOMAIN + key_id.encode(), hashlib.sha256).hexdigest()

runtime_identity async

runtime_identity(pool: Any) -> tuple[str, Any]

The routed graph identity; pool checkout proves current anchored lineage.

Source code in src/symfonic/memory/backends/postgres_retirement_receipt.py
async def runtime_identity(pool: Any) -> tuple[str, Any]:
    """The routed graph identity; pool checkout proves current anchored lineage."""
    from symfonic.memory.backends.postgres_erase_binding import UNBOUND, resolve
    from symfonic.memory.backends.postgres_eraser_roles import EraserNotReady

    try:
        conn = await pool.acquire()
    except MemoryContractError as error:
        raise EraserNotReady(str(error)) from error
    try:
        login = await conn.fetchval("SELECT session_user")
        binding = await resolve(conn)
    finally:
        await pool.release(conn)
    if binding is None:
        raise MemoryContractError(UNBOUND)
    return login, binding