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
|