symfonic.services.privacy.ports¶
ports ¶
Port registry rows 14 and 20, plus the saga's own state port.
All three are runtime-service-owned. Row 20's ownership is the load-bearing choice: an integration must be able to implement "erase this subject from my backend" without importing platform, for the same reason SEC-TEN-5 puts isolation enforcement in the backends. A platform-owned participant protocol would make every storage adapter depend on the HTTP layer.
Signatures follow CON-P: async-first, transport-neutral vocabulary, narrow enough that a test double is three methods rather than a mock framework.
ErasureFence ¶
Bases: Protocol
Registry row 14 — the erasure generation and the subject tombstone.
The write side is one method on purpose. Exposing "check" and "write" separately would make the time-of-check/time-of-use gap a supported usage of the port, and EFX-F-3 requires the gap to be closed by the atomic operation rather than by callers agreeing to be careful.
An implementation resolves a scope key to the nearest tombstone over its
lineage, not to its exact row. Erasure is subtree-scoped, so a tenant
tombstone governs every scope beneath it, and an exact-key implementation
fails open for sub-scopes — the sub-scope write commits, and the
subtree-scoped export reads the resurrected data straight back out.
:func:symfonic.services.privacy.lineage.scope_key_lineage is the walk.
complete_erasure
async
¶
conditional_write
async
¶
conditional_write(scope_key: str, *, expected_generation: int, apply: Callable[[], Any]) -> WriteOutcome
Commit apply only if the generation matches AND no tombstone exists.
Both conditions and the write happen in one linearizable operation — a transaction with a predicate, a conditional update, or a single-lock section, per backend. Never as separate steps.
"No tombstone exists" means none over the scope's lineage: a write at
tenant/team1 is refused by the tombstone published for tenant.
Source code in src/symfonic/services/privacy/ports.py
publish_tombstone
async
¶
read_generation
async
¶
What a writer observes at admission (EFX-ER-2).
The returned scope_key names the scope the tombstone was published
for, which may be an ancestor of the one asked about.
Source code in src/symfonic/services/privacy/ports.py
ErasureSagaStore ¶
Bases: Protocol
The saga's durable per-participant completion state (PRIV-3).
Runtime-service-owned because the guard reads it — an in-flight invocation asks "is any store still unconfirmed?" before serving a read, and the guard may not import platform to find out.
claim_generation_advance
async
¶
Compare-and-set: True exactly once per saga, for the caller that won.
There is no plain "mark it advanced" setter on this port, on purpose. Exactly-once completion cannot be assembled from a read plus a write — the await between them is where an operator retry and a scheduled resume both decide to advance, and the generation ends two ahead of the erasure that caused it. The store owns the transition because the store is the only thing both runs share.
Source code in src/symfonic/services/privacy/ports.py
incomplete_under
async
¶
Any saga at or below scope_key that still has a store unconfirmed.
The descendant half of read suppression, and it is not symmetry for its
own sake. Reads match subtree-wide with narrows, so a reader at
tenant sees the rows an in-flight erasure of tenant/team1 has
not destroyed yet. Walking ancestors alone suppresses the readers at or
below the erased scope and serves the one above it — the same rows,
through a wider query.
Matching is segment-wise (SEC-TEN-1): acme-evil is not under
acme, whatever a string comparison would say.
Source code in src/symfonic/services/privacy/ports.py
start
async
¶
SubjectDataStore ¶
Bases: Protocol
Registry row 20 — one store's side of export and erasure.
confirm_absent is what turns SEC-PRIV-3's verification-by-absence from a
claim into a call. A store that only reports how many rows it deleted
cannot answer the question an auditor actually asks.
SubjectDataStoreWriter ¶
Bases: Protocol
Row 20's write obligation, for the stores that accept writes.
Separate from :class:SubjectDataStore rather than folded into it, because
not every participant has a write path — a legacy erase-and-count adapter
implements the four methods above and nothing else, and demanding a writer
of it would misreport a complete participant as a broken one.
Where it does exist, EFX-ER-3 says what it must be: the commit goes
through :meth:ErasureFence.conditional_write, never straight to the
backend. CS-20 asks for this entry point by name, so a store that owns
subject rows and does not declare it fails the suite's first write case
instead of crashing it.
write
async
¶
write(scope: SubjectScope, record: Any, *, fence: ErasureFence, expected_generation: int) -> WriteOutcome
Commit record for scope through the fence, or not at all.