Skip to content

symfonic.services.privacy.errors

errors

The privacy taxonomy. Typed, coded, and never matched on message text.

ERR-2 forbids keying control flow on prose, and the shipped platform is a live example of why: three routers branch on str(exc).startswith("Budget exceeded:"). Every class here carries a stable code so a transport maps the class, and an operator greps the code, without either of them reading the sentence a translator may one day rewrite.

ErasureFenceDenied

ErasureFenceDenied(message: str, *, reason: str, scope_key: str, expected_generation: int | None = None, observed_generation: int | None = None)

Bases: PrivacyError

A conditional write was refused by the fence (EFX-ER-4).

Carries reasongeneration_mismatch or subject_tombstoned — because the two are operationally different: the first says "somebody else moved", the second says "this subject is gone and retrying is resurrection".

Source code in src/symfonic/services/privacy/errors.py
def __init__(
    self,
    message: str,
    *,
    reason: str,
    scope_key: str,
    expected_generation: int | None = None,
    observed_generation: int | None = None,
) -> None:
    super().__init__(message)
    self.reason = reason
    self.scope_key = scope_key
    self.expected_generation = expected_generation
    self.observed_generation = observed_generation

ErasureSagaError

Bases: PrivacyError

The saga was asked about a subject it has no state for.

FenceAuditBindingError

Bases: PrivacyError

Something tried to take over a fence's denial recorder (HOST-3).

Two hosts in one process — a test suite, a sidecar, an adopter mounting two agents — can share a fence. A silent rebind sends the first host's EFX-ER-4 denials to the second host's sink, which is worse than either host having no audit: the events exist, in the wrong place, and nobody is looking for them there. Rebinding is therefore an explicit act, not a constructor side effect.

ParticipantRegistrationError

Bases: PrivacyError

A store is missing from, or duplicated in, the coverage registry.

PRIV-1 makes coverage checkable by making it declared. A silent overwrite would drop a store out of the erasure path with no diagnostic at all.

PrivacyError

Bases: SymfonicError

Root of the privacy taxonomy.

ReadSuppressedError

ReadSuppressedError(message: str, *, scope_key: str, unconfirmed: tuple[str, ...])

Bases: PrivacyError

An erasure is in flight and at least one store is unconfirmed.

Distinct from :class:SubjectErasedError on purpose: this one is a transient fail-closed state an operator can act on by driving the saga to completion, and it maps to a different status than "this subject is gone".

Source code in src/symfonic/services/privacy/errors.py
def __init__(self, message: str, *, scope_key: str, unconfirmed: tuple[str, ...]) -> None:
    super().__init__(message)
    self.scope_key = scope_key
    self.unconfirmed = unconfirmed

SubjectErasedError

SubjectErasedError(message: str, *, scope_key: str)

Bases: PrivacyError

The subject carries a tombstone: no admission, no read, no write.

Terminal by design. There is no "the erasure finished so it is safe again" branch, because that branch is exactly how a restored backup serves data the subject asked to have destroyed (PRIV-7).

Source code in src/symfonic/services/privacy/errors.py
def __init__(self, message: str, *, scope_key: str) -> None:
    super().__init__(message)
    self.scope_key = scope_key