Minutes to hours
Background re-indexing before revocation actually applies.
Every request — read or write — passes through the same pipeline, in the same order. This page is the full trace: why static ACLs leak, how authority is resolved, and what happens at each of the thirteen stages.
Traditional multi-tenant RAG stamps access-control lists onto vector embeddings at ingestion time. When access is revoked, the vector store doesn't know yet — and won't, until a background re-index catches up.
// 1. Ingestion: ACL stamped into vector metadata INSERT INTO vector_chunks (embedding, metadata_acl) VALUES ([...], '{"allowed_users":["u123","u456"]}'); // 2. Revocation happens in the core database // u123 is offboarded — the vector store isn't told // 3. Drift window: still queryable SELECT chunk FROM vector_chunks WHERE metadata_acl @> '{"allowed_users":"u123"}'; -- leak
The window can run minutes to hours depending on re-index cost. During it, revoked access is still fluently synthesized into answers.
-- Executed BEFORE vector search, every request SELECT c.id FROM tenants t JOIN families f ON f.tenant_id = t.id AND f.quarantine_status = FALSE JOIN children c ON c.family_id = f.id JOIN user_assignments ua ON ua.child_id = c.id WHERE t.id = :tenant_id AND ua.user_id = :user_id AND NOW() BETWEEN ua.effective_from AND ua.effective_to;
Revocation, suspension, or quarantine take effect in under a millisecond — there's no index to catch up, because permission was never stamped onto the vector.
Background re-indexing before revocation actually applies.
Authority is a live relational join, evaluated at query time.
The vector store carries meaning and distance, zero permission metadata.
Authority isn't a flag on a row. It's a chain: Tenant → Family → Child (document) → User Assignment. Break any link and the candidate set collapses to zero — by definition, not by exception handling.
A quarantined family — say, a legal-hold container — doesn't get filtered out after retrieval. It structurally disappears from the join chain, so it was never a candidate the AI could have seen in the first place. That's the difference between a blind spot and a filter.
Every request — whether it only asks a question or also wants to change something — runs the same read path. Only requests that produce a mutating action continue through the approval-bound block before reaching the ledger.
TenantSage doesn't replace your retrieval stack — it constrains what any of these architectures are allowed to see before they run.
Sparse lexical + dense vector search, both constrained to the sealed EEB candidate set.
Subgraphs and community reports — quarantined nodes structurally excluded from traversal.
Tool-calling loops where every tool invocation re-enters the pipeline at S0.
Quality-evaluator retries stay inside the same sealed boundary, never a wider one.
Visual and page-level vectors (e.g. ColPali-style) inherit authority from their source document, same as text.
If it can accept a pre-computed candidate ID set, it can be governed this way.
Every governed request writes a chained, hashed entry — authority snapshot, policy decision, EEB hash, retrieval receipt, generation and validation results, and (for actions) the execution receipt. Each entry's hash depends on the one before it, so a tampered entry breaks the chain visibly rather than silently.