symfonic.services.privacy.guard¶
guard ¶
The inward guard: what an in-flight invocation or a worker actually calls.
The acceptance criterion is explicit that in-flight invocations and workers use this port without importing platform, and it is not a stylistic preference. A background consolidation worker that had to import the HTTP layer to ask "is this subject deleted?" would drag FastAPI into every deployment that runs one — and, worse, would make the check skippable by anything that could not import it.
Three checks, one for each place PRIV-5 requires one:
- :meth:
require_admission— no new invocation starts for a tombstoned subject; - :meth:
require_readable— reads are suppressed while any store is unconfirmed, and refused outright once the subject is erased; - :meth:
guarded_write— every write commits through the dual-condition conditional write, or does not commit.
All three ask about the scope's lineage, not just its key. Erasure is
subtree-scoped, so a tenant erasure has to refuse an invocation, a read, and a
write at tenant/team1 — and the saga whose completion gates read suppression
is recorded under the erased scope's key, not the reader's.
SubjectGuard ¶
Constructor-injected, narrow, and holding no state of its own.
Source code in src/symfonic/services/privacy/guard.py
guarded_write
async
¶
Observe the generation, then commit under both conditions.
Reading the generation first is not a check-then-write: the read is EFX-ER-2's admission observation, and the decision is made inside the conditional write against whatever is true at commit time. A writer that loses the race is denied there, not here.
A denied write is never retried. EFX-ER-4 is unambiguous: retrying against the new generation is resurrection, and a caller that wants to proceed must re-derive from post-erasure sources instead.
Source code in src/symfonic/services/privacy/guard.py
require_admission
async
¶
SCOPE-14 step 2. Fail closed: a tombstoned subject does not run.
Source code in src/symfonic/services/privacy/guard.py
require_readable
async
¶
Reads: suppressed while any store is unconfirmed, refused once erased.
Both outcomes deny; the distinction is what an operator can do about it. An incomplete saga is a transient state somebody can clear by driving the remaining stores to confirmed absence, so it reports which stores are outstanding. A completed erasure is terminal — finishing the saga does not reopen the subject, it closes it — so the tombstone check is the fallthrough rather than the first gate.
Source code in src/symfonic/services/privacy/guard.py
unconfirmed_stores
async
¶
Diagnostics: which participants still owe a confirmed absence.