03

Filter Tenant Policy Before Ranking

Week 3 of 10 · Make cross-tenant retrieval and effects impossible by construction • Public source target: https://github.com/ZGTR/enterprise-access-agent-evals/tree/v1.0.2

System map · Day 03

Whole-system design

Five stable layers. Today's work is expanded and linked; the rest stays in context.

Entry and authorization control

Covered — Request entry · Approval gate

Tenant and policy authority

Source-backed today

Supplies tenant scope from runtime context rather than prompt text.

Compute and execution

Covered — Typed tool broker

Agent runtime compute

Source-backed today

Consumes only tenant-scoped candidates and immutable citation identifiers.

Policy retrieval compute

Source-backed today

Filters before ranking and preserves stale or conflicting versions for evaluation.

Enterprise resource boundary

Simulated enterprise system

Source-backed today

Makes colliding Acme and Globex identifiers unable to cross the adapter boundary.

Storage and state

Covered — Run and effect state

Evaluation evidence store

Source-backed today

Stores ten collision attacks with paired same-tenant positive controls.

Evidence and release control

Ahead — Evaluation harness compute · Trace and diagnosis plane · Release gate

Traversed today

bound request · Request entryAgent runtime computescoped policy search · Tenant and policy authorityPolicy retrieval computeauthorized effect · Typed tool brokerSimulated enterprise systempost-effect state · Simulated enterprise systemAgent runtime compute

Treat tenant scope as authorization

Week 2 left R2-agent-v1, 30 cases, and five brokered tools. Those tools are bounded, but a search system can still retrieve a persuasive Globex policy for an Acme request if tenant filtering happens after global ranking. This week you make tenant scope an authorization predicate applied before candidates exist, then prove ten collision attacks return zero foreign rows, citations, or effects.

Retrieval-augmented generation (RAG) supplies selected source passages to a model before it answers. Retrieval improves factual context but does not establish access control. Search ranking answers “which allowed document is relevant?”; authorization answers “which documents may become candidates?” Authorization must run first.

Trusted tenant enters through tagged contracts.py. Policy versions live on passages in the offline reference; the richer request-level as-of binding is an exercise target:

@dataclass(frozen=True)
class RuntimeContext:
    tenant_id: str
    actor_id: str

The server creates context; request prose cannot override it. A wrong or absent tenant is a denial, not a model clarification.

Build colliding Acme and Globex corpora

Friendly fixtures rarely reveal isolation bugs, so both tenants need the same names, resource aliases, document IDs, and policy phrases. Give Acme and Globex an employee named Sarah, a salesforce-standard permission, and a POL-17 document whose rule differs. The tenant namespace, not identifier uniqueness, carries isolation.

Tagged retrieval.py constrains candidates before scoring:

    scoped = (policy for policy in policies if policy.tenant_id == tenant_id)
    terms = {term.casefold() for term in query.split()}
    ranked = sorted(
        scoped,
        key=lambda policy: (
            -sum(term in policy.text.casefold() for term in terms),
            not policy.active,
            policy.citation_id,
        ),
    )
    return tuple(ranked[:limit])

Each returned passage carries immutable tenant_id, document_id, version, and chunk ID. Stale or conflicting same-tenant documents stay available as controlled fixtures so later graders can distinguish retrieval from reasoning. Foreign documents never enter the ranker, prompt, logs, or citations.

Carry scope through agent and adapter boundaries

Filtering policy search alone is insufficient if employee lookup or state adapters accept a model-supplied tenant. Every boundary derives tenant from the same trusted context and verifies returned objects before use.

Tagged agent.py provides a deterministic normalization seam. A provider-backed runtime must consume opaque citation and employee references without synthesizing identifiers or rewriting tenant ownership.

class DeterministicProvider:
    def normalize(self, request: RestoreAccessRequest) -> RestoreAccessRequest:
        return request

The simulated authority in state.py keys all state by tenant and rejects a mismatched employee/resource pair with non-enumerating absence. “Not found” must not reveal whether the same ID exists in another tenant.

Attack collisions and measure unaffected controls

Ten attacks cover prompt-selected tenant, foreign employee ID, foreign resource ID, colliding document ID, globally ranked passage, citation substitution, stale foreign approval, cross-tenant retry, error-message enumeration, and poisoned policy text asking the agent to switch scope. Each attack expects zero foreign identifiers and zero effects.

The dataset grows to 40 cases. Each attack is paired with a same-tenant positive control so a broken search service cannot “pass” isolation by returning nothing to everyone. Record actor, resource, scope, precondition, expected/observed result, environment, timestamp, and immutable run ID.

Inject one bug that filters after ranking. The attack should fail because a foreign passage reached intermediate evidence even if final output hides it. Repair by moving the predicate into candidate construction, rebuild the index namespace, rerun attacks and controls, then clean up only the disposable corpus version.

The scored checkpoint supplies one collision trace; the unknown is allow or deny. Correct evidence contains no foreign IDs/effects and a passing same-tenant control. Misconception feedback contrasts output scrubbing with authorization-before-ranking. Decline any answer that invents tenant from prompt text.

Spend 1.5 hours on the Hugging Face Agentic RAG unit and RAGAS concepts, 4–5 hours on corpora, adapters, attacks, and repair, and 1 hour explaining why namespace constraints precede relevance. The handoff is R3-tenant-proof, D40-cases, and policy corpus v1. Week 4 adds traces capable of locating the earliest causal failure.