Skip to content

symfonic.memory.backends.postgres_retirement_witness

postgres_retirement_witness

Internal signed envelope for a separately verified chat retirement (CORE-428).

Signing is a coordinator operation, never an Agent/runtime API. The envelope does not itself prove that the chat intent was read from the protected DB; the issuer must perform that read before calling seal. SQL completion must verify the MAC with its own NOLOGIN-owned key before any chat DML.

seal

seal(evidence: TerminalRetirementEvidence, chat: ChatRetirementIntent, *, witness_key_id: str, witness_key: bytes, receipt_key: bytes, anchor_key: bytes) -> SignedRetirementWitness

Seal already-verified evidence; do not use from the API/worker process.

Source code in src/symfonic/memory/backends/postgres_retirement_witness.py
def seal(
    evidence: TerminalRetirementEvidence,
    chat: ChatRetirementIntent,
    *,
    witness_key_id: str,
    witness_key: bytes,
    receipt_key: bytes,
    anchor_key: bytes,
) -> SignedRetirementWitness:
    """Seal already-verified evidence; do not use from the API/worker process."""
    if type(evidence) is not TerminalRetirementEvidence or type(chat) is not ChatRetirementIntent:
        raise MemoryContractError(INVALID)
    witness_key = _key(witness_key)
    if hmac.compare_digest(witness_key, _key(receipt_key)) or hmac.compare_digest(
        witness_key, _key(anchor_key)
    ):
        raise MemoryContractError("retirement witness key must differ from receipt and anchor keys")
    if (
        evidence.tenant_id != chat.tenant_id
        or not all(
            _required(x)
            for x in (
                evidence.tenant_id,
                evidence.job_id,
                evidence.graph_binding,
                evidence.graph_anchor,
                evidence.receipt_key_id,
                chat.chat_db_identity,
                chat.chat_anchor_digest,
                witness_key_id,
            )
        )
        or type(chat.generation) is not int
        or chat.generation < 1
        or type(evidence.anchor_head) is not int
        or evidence.anchor_head < 0
        or type(chat.chat_anchor_seq) is not int
        or chat.chat_anchor_seq < 1
        or not _hex64(chat.manifest_sha256)
        or not _hex64(chat.chat_anchor_digest)
        or len(witness_key_id) > 128
        or len(evidence.receipt_key_id) > 128
    ):
        raise MemoryContractError(INVALID)
    claims = RetirementClaims(
        1,
        evidence.tenant_id,
        chat.generation,
        evidence.graph_binding,
        evidence.graph_anchor,
        chat.chat_db_identity,
        evidence.job_id,
        evidence.receipt_key_id,
        evidence.anchor_head,
        chat.chat_anchor_seq,
        chat.chat_anchor_digest,
        chat.manifest_sha256,
        witness_key_id,
    )
    encoded = json.dumps(asdict(claims), sort_keys=True, separators=(",", ":"), ensure_ascii=True)
    mac = hmac.new(witness_key, DOMAIN + encoded.encode(), hashlib.sha256).hexdigest()
    return SignedRetirementWitness(encoded, mac)

verify_envelope

verify_envelope(witness: SignedRetirementWitness, *, witness_key_id: str, witness_key: bytes) -> RetirementClaims

Pure test/reference verifier; the chat DB must repeat this independently.

Source code in src/symfonic/memory/backends/postgres_retirement_witness.py
def verify_envelope(
    witness: SignedRetirementWitness, *, witness_key_id: str, witness_key: bytes
) -> RetirementClaims:
    """Pure test/reference verifier; the chat DB must repeat this independently."""
    if (
        type(witness) is not SignedRetirementWitness
        or type(witness.claims_json) is not str
        or not 0 < len(witness.claims_json) <= 65536
        or not _hex64(witness.mac)
        or not _required(witness_key_id)
    ):
        raise MemoryContractError(INVALID)
    expected = hmac.new(_key(witness_key), DOMAIN + witness.claims_json.encode(), hashlib.sha256)
    if not hmac.compare_digest(witness.mac, expected.hexdigest()):
        raise MemoryContractError(INVALID)
    try:
        fields = json.loads(witness.claims_json)
        if type(fields) is not dict or set(fields) != set(RetirementClaims.__dataclass_fields__):
            raise ValueError("wrong claims")
        claims = RetirementClaims(**fields)
    except (TypeError, ValueError) as error:
        raise MemoryContractError(INVALID) from error
    if (
        json.dumps(asdict(claims), sort_keys=True, separators=(",", ":"), ensure_ascii=True)
        != witness.claims_json
    ):
        raise MemoryContractError(INVALID)
    if (
        claims.version != 1
        or type(claims.version) is not int
        or claims.witness_key_id != witness_key_id
        or type(claims.generation) is not int
        or claims.generation < 1
        or type(claims.memory_anchor_head) is not int
        or claims.memory_anchor_head < 0
        or type(claims.chat_anchor_seq) is not int
        or claims.chat_anchor_seq < 1
        or len(claims.witness_key_id) > 128
        or len(claims.receipt_key_id) > 128
        or not all(
            _required(x)
            for x in (
                claims.tenant_id,
                claims.graph_binding,
                claims.graph_anchor,
                claims.chat_db_identity,
                claims.job_id,
                claims.receipt_key_id,
                claims.witness_key_id,
            )
        )
        or not _hex64(claims.manifest_sha256)
        or not _hex64(claims.chat_anchor_digest)
    ):
        raise MemoryContractError(INVALID)
    return claims