Agent skill

Agentsop Multi Tenant RAG

by agentsope in agentsope/SkillAlchemy

Security-first SOP for multi-tenant RAG systems. An agent skill from agentsope/SkillAlchemy.

MITAuto-check passedAI & LLM Engineering

Install Agentsop Multi Tenant RAG

skills CLI
$ npx skills add agentsope/SkillAlchemy --skill agentsop-multi-tenant-rag -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install agentsope/SkillAlchemy agentsop-multi-tenant-rag --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/agentsope/SkillAlchemy.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/agentsop-multi-tenant-rag .claude/skills/agentsop-multi-tenant-rag && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
agentsop-multi-tenant-rag
GitHub stars
466
Token cost
~9.8k tokens
SKILL.md length
3,918 words
Files
4 (incl. references)
Skills in repo
46
Repo updated
First seen
Licence
MIT

At a glance

Security-first SOP for multi-tenant RAG systems. An agent skill from agentsope/SkillAlchemy.

  • Works in 7 steps: 何时激活 (Activation Rules) → 核心心智模型 (Core Mental Model) → SOP 工作流 (Agentic Protocol) → …
  • Tasks that involve Vector databases
  • SKILL.md covers 1. 何时激活 (Activation Rules), 2. 核心心智模型 (Core Mental Model), 3. SOP 工作流 (Agentic Protocol) and 4. 操作模型 (Operation Models), plus 1 more section
  • Calls kubectl

What it does

Agentsop Multi Tenant RAG is an agent skill from agentsope/SkillAlchemy. Security-first SOP for multi-tenant RAG systems. Activate when a calling agent is building, reviewing, or debugging any retrieval pipeline whose vector store is shared across more than one user, organisation, workspace, customer, or permission scope. Encodes the single non-negotiable rule — filter at the vector store query, never after retrieval / never after rerank — together with the per-vendor query-time filter APIs (Pinecone namespaces + $eq/$in, Weaviate multiTenancyConfig + tenant handle, Qdrant istenant…

Its SKILL.md is about 9.8k tokens, which your agent loads only when the skill is triggered. The skill folder holds 5 other files, including reference files (for example `intermediate/research_notes.md`, `references/R1-vendor-filter-cheatsheet.md` and `references/R2-cross-tenant-test-recipes.md`).

It sits in AI & LLM Engineering, covering Vector databases, Retrieval-augmented generation and Multi-tenancy. It works with pgvector, Pinecone, LangChain and LlamaIndex. The repository describes itself as: From thought to skill. From signal to structure. The licence is MIT.

When your agent uses it

  • Tasks that involve Vector databases
  • Tasks that involve Retrieval-augmented generation
  • Tasks that involve Multi-tenancy

Example prompts

  • “adding a filter for relevance”
  • “/agentsop-multi-tenant-rag”

Requirements

  • Python 3

Workflow steps

7 steps, taken from the step headings in SKILL.md.

  1. 何时激活 (Activation Rules)
  2. 核心心智模型 (Core Mental Model)
  3. SOP 工作流 (Agentic Protocol)
  4. 操作模型 (Operation Models)
  5. 困境决策案例 (Dilemma Cases)
  6. 反模式与边界 (Anti-patterns & Boundaries)
  7. 跨框架对照 (Cross-Framework Reference Table)

What it can do on your machine

Read from SKILL.md and the folder at commit d0f0355. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • kubectl

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Links to these hosts (documentation or services it may open):

    • qdrant.tech
    • docs.pinecone.io
    • developers.llamaindex.ai
    • pinecone.io
    • docs.weaviate.io
    • weaviate.io
    • docs.trychroma.com
    • cookbook.chromadb.dev
    • python.langchain.com
    • we45.com
    • csoonline.com
    • christian-schneider.net
    • kiteworks.com
    • beyondscale.tech

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Agentsop Multi Tenant RAG loads about 9.8k tokens when it runs, and up to ~15k if it reads all its reference files. Until then it costs about 210 tokens; SKILL.md has 3,918 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~210
When it runs · the whole SKILL.md, loaded when a task matches
~9.8k
With references · SKILL.md plus every file in references/, read only if the agent opens them
~15k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from agentsope/SkillAlchemy at commit d0f0355, republished under its MIT licence (© agentsope). 3,918 words, ~9,783 tokens.

Download SKILL.mdSave it as .claude/skills/agentsop-multi-tenant-rag/SKILL.md (or your agent's skills folder). This skill also uses 3 other files; get the full folder from GitHub.
name
agentsop-multi-tenant-rag
description
Security-first SOP for multi-tenant RAG systems. Activate when a calling agent is building, reviewing, or debugging any retrieval pipeline whose vector store is shared across more than one user, organisation, workspace, customer, or permission scope. Encodes the single non-negotiable rule — **filter at the vector store query, never after retrieval / never after rerank** — together with the per-vendor query-time filter APIs (Pinecone namespaces + `$eq`/`$in`, Weaviate `multiTenancyConfig` + tenant handle, Qdrant `is_tenant` payload index + `Filter.must`, Chroma `where`, pgvector RLS), and the cross-framework adapters (LlamaIndex `MetadataFilters`, LangChain `filter=` dict). Frame the work as preventing CVE-2024-41892 / EchoLeak / Slack-AI-class cross-tenant leakage, not as "adding a filter for relevance".
version
0.1.0
trigger_keywords
multi-tenant RAG, tenant isolation, metadata filter, namespace per tenant, multitenancy vector store, RLS pgvector, cross-tenant leak, RAG security, workspace…
when_to_use
any RAG / retrieval pipeline whose corpus is shared across >1 tenant, customer, org, user, or permission scope, reviewing PRs that touch `vector_store.query`…
when_not_to_use
single-tenant / single-user corpora with no access scopes — over-engineering, purely internal eval / offline notebooks against fully public data, the…

Multi-Tenant RAG · Security-First Isolation SOP

Third-person operating model for a coder agent that owns retrieval correctness across tenant boundaries. The audience is the LLM agent writing or reviewing the code — not the end user.

One sentence: Isolation lives at the vector store query boundary, not at the model. Anything that reaches the LLM's context window has already leaked.


1. 何时激活 (Activation Rules)

Activate this skill whenever any of the following holds:

  1. The codebase contains a retrieval call (vector_store.query, query_points, similarity_search, as_retriever().retrieve(...), raw pgvector ORDER BY embedding <-> $1) and the corpus serves more than one tenant, customer, organisation, workspace, user, or permission scope.
  2. The user mentions any of: multi-tenant RAG, namespace, tenant, workspace, tenant_id, org_id, user_id, "cross-customer", "shared index", "knowledge base per team".
  3. A bug report says "User A saw User B's document", "wrong company's data surfaced", "the assistant cited a doc I don't have access to", or anything that smells like cross-context bleed.
  4. PR review: any new code calling a vector store without a tenant-scoped filter argument, or filtering only on the returned nodes / documents list after retrieval.
  5. A new RAG endpoint is about to ship and tenant scoping has not been explicitly audited.

Do not activate when:

  • The corpus is fully public and there is no per-tenant view (e.g. open-data Q&A).
  • The retrieval pipeline already enforces a physically separate index / collection per tenant via infrastructure the application code cannot override (e.g. one Pinecone index per customer with credentials issued per-tenant). In that case the isolation lives in IAM, not in this skill.

2. 核心心智模型 (Core Mental Model)

Three principles. If a design violates any of them the system is exploitable, regardless of how good the LLM prompt is.

Principle 1 — Isolation lives at the boundary, not at the model

The boundary is the vector store query call. Whatever crosses that boundary is in the trust set. An LLM, a reranker, or a postprocessor that "filters out" foreign-tenant chunks is operating inside the breach: those tokens have already been embedded, retrieved, scored, and exposed to attacker-controlled prompts. CVE-2024-41892 (Pinecone, 2024) is the canonical demonstration — RBAC checks executed after retrieval allowed sentinel data to cross namespace boundaries before the access check fired. (See we45, CSO Online references.)

Operational corollary: any code shaped results = vs.query(...); results = [r for r in results if r.metadata["tenant"] == ctx.tenant] is a security defect, not a style nit. The leak already happened — you only hid it from the user.

Principle 2 — Embed the tenant key at ingestion, not just at query

A query-time filter is only enforceable if every vector carries the key. The two failure shapes:

  • Forgotten field: a single document ingested without tenant_id becomes a shared global record returned to all tenants — it matches no filter predicate that requires equality.
  • Mutable field: tenant_id derived from the document body / LLM extraction rather than from the request's authenticated session — attacker can craft document content that re-labels itself.

The tenant key must come from the authenticated session at ingestion time and be written into the immutable payload / metadata column. Treat it like a foreign key to your tenants table.

Principle 3 — Defence in depth: namespace and filter, not namespace or filter

Most production-grade stacks layer two mechanisms:

LayerMechanismWhat it stops
StoragePer-tenant namespace / shard / collection / RLS policyOperator bugs, mis-routed queries, ops mistakes
Queryfilter={tenant_id: $session.tenant} on every callApp bugs, namespace selection mistakes, cross-shard joins

Pinecone's official guidance is "namespace per tenant in serverless"; Weaviate mandates a tenant handle on every CRUD when multiTenancyConfig.enabled=true; Qdrant marks the field as is_tenant=true and requires Filter.must on read; pgvector pairs schema partitioning with RLS. The principle is the same: one layer is one bug away from failing open.


3. SOP 工作流 (Agentic Protocol)

Eight stages. Each gates the next. Stop and remediate at the first failure.

Stage 0 — Threat-model the boundary

Before any code change, write down:

  1. Tenant key: what identifier separates customers? (tenant_id, org_id, workspace_id, user_id, acl_label). Pick one — do not mix.
  2. Trust source: where does the request-time tenant key come from? It must be from the authenticated session / JWT claim / signed header — never from a query param, prompt, or document body.
  3. Cardinality: how many tenants? (<100 → namespace/collection viable;

    10k → payload partition + index is mandatory). Drives Stage 2 choice.

  4. Blast radius: if a single vector leaks, what's the worst document? (PII / regulated / IP / public). Raises bar on Stages 4-6.

Artifact: a 5-line THREAT_MODEL.md snippet co-located with the retriever.

Stage 1 — Pick the isolation primitive (vendor-specific)
VendorRecommended primitiveWhen
PineconeOne namespace per tenant in a serverless indexDefault; cheapest query (1 RU per 1 GB / namespace); offboarding = delete_namespace
WeaviatemultiTenancyConfig.enabled=True on the collectionNative, per-shard isolation; mandatory tenant handle on CRUD
QdrantSingle collection, payload key with is_tenant=true, keyword indexScales to millions of tenants; physical co-location, logical isolation
Chromawhere={"tenant_id": "..."} on every query/get/update/deleteSmallest stacks; cookbook calls this the "naive multi-tenancy strategy" — accept its limits
pgvectorPostgres schema or row-level security (RLS) on tenant_id columnWhen the rest of the app already lives in Postgres
Milvuspartition_key on tenant fieldSame shape as Qdrant is_tenant

Decision rule: prefer the strongest physical primitive your tenant cardinality can afford, then always add the filter at query time anyway (Principle 3).

Stage 2 — Ingestion: write the key, lock the schema
python
# Canonical shape — vendor-agnostic
node.metadata = {
    "tenant_id": session.tenant_id,        # MUST come from authenticated session
    "doc_id":    doc.id,
    "source":    doc.uri,
    "ingested_at": now_utc(),
    # ... domain fields
}
assert node.metadata["tenant_id"], "refuse to embed without tenant_id"

Hard rules:

  • Refuse to write a vector if tenant_id is missing or empty — fail loud, do not default.
  • tenant_id is never taken from document content; never mutable post-ingestion.
  • Schema-validate metadata before embedding (Pydantic / JSON Schema) — type drift breaks $eq silently.
  • For Qdrant: create the payload index before the first upsert (create_payload_index(field="tenant_id", schema="keyword", is_tenant=True)).
  • For Weaviate: create the tenant (collection.tenants.create([Tenant(name=t)])) before any write.
Stage 3 — Query-time filter (the only place the rule applies)

Every retrieval call must accept a tenant key from the session and pass it into the vendor's filter argument. Templates in OP-04 through OP-08.

python
# Generic LlamaIndex shape — works across Pinecone, Qdrant, Weaviate, Chroma
from llama_index.core.vector_stores import (
    MetadataFilter, MetadataFilters, FilterOperator,
)

filters = MetadataFilters(filters=[
    MetadataFilter(key="tenant_id", value=session.tenant_id,
                   operator=FilterOperator.EQ),
])
retriever = index.as_retriever(similarity_top_k=8, filters=filters)

If the framework or vendor SDK does not expose a query-time filter, switch the framework or vendor — do not patch with a post-filter.

Stage 4 — Cross-tenant property test (the gate)

Before merging, the PR must include — and CI must run — a test that:

  1. Ingests N≥3 documents per tenant for at least 2 tenants (A and B).
  2. Issues a query as tenant A whose semantic top-1 in the unfiltered search would be a tenant-B document (craft the query against B's content).
  3. Asserts zero tenant-B results returned.
  4. Repeats symmetrically as tenant B.

If the unfiltered top-1 is not a foreign-tenant doc, the test is dishonest — rewrite the corpus to make foreign-tenant semantically closer, otherwise the test always passes vacuously.

See OP-09 for a runnable scaffold.

Stage 5 — Defense in depth (Principle 3)

Layer at least one of:

  • Per-tenant namespace / shard / collection on top of the metadata filter.
  • Per-tenant API key / scoped credential at the vector store layer (Pinecone API key project scope, Postgres role per tenant).
  • DB-level enforcement (pgvector RLS policy USING (tenant_id = current_setting('app.current_tenant')::uuid)).

A single layer is one config bug away from open.

Stage 6 — Audit logging

Every retrieval call logs:

{ts, request_id, session.tenant_id, query_hash,
 vs.namespace_or_collection, filter_clause,
 returned_count, returned_tenant_ids_set}

Then add a synchronous assertion in the request path:

python
assert returned_tenant_ids_set.issubset({session.tenant_id, GLOBAL_TENANT})

This converts a silent leak into a loud 500 — the right failure mode.

Stage 7 — Cache / rerank / postprocessing audit

Walk every component after retrieval and confirm none of them:

  • Caches results across tenants under a tenant-agnostic key (e.g. cache_key = hash(query) is a leak; correct is cache_key = hash((tenant_id, query))).
  • Returns "fallback" results from a global pool when the tenant pool is empty.
  • Logs full retrieved content to a shared monitoring store readable by other tenants' staff.

Reranker / NodePostprocessor / synthesizer can only safely narrow the set; they never restore foreign tenants and never invent context — confirm by reading the code path.

Stage 8 — Ongoing verification
  • Add the cross-tenant property test to nightly CI, not just merge gate.
  • On schema migration: re-run with the new chunking / embedding model.
  • On vendor upgrade: re-read the changelog for filter / namespace semantics (this changes more than vendors admit — see Pinecone disk-based metadata filtering, Qdrant 1.16 tiered multitenancy).

4. 操作模型 (Operation Models)

Format: Trigger / Action / Output / Evidence. Vendor-specific filter syntax canonicalised against current docs (May 2026).

OP-01 EmbedTenantAtIngest
  • Trigger: New ingestion pipeline; any system not currently writing tenant_id into metadata.
  • Action: Add tenant_id (from authenticated session) to every node's metadata at chunk creation. Validate non-empty before add() / upsert(). Make the field part of the ingestion schema.
  • Output: 100% of vectors carry the immutable tenant key; ingestion refuses to write otherwise.
  • Evidence: Qdrant Multitenancy docs; Pinecone Implement multitenancy guide; we45 RAG leakage post-mortem.
OP-02 ChooseIsolationPrimitive
  • Trigger: Greenfield RAG, or migration from single-tenant.
  • Action: Use the cardinality / latency / cost matrix in Stage 1. Default ladder: <100 tenants → namespace/collection per tenant; 100-10k → payload partition + index (is_tenant); >10k → tiered (hot collection + cold archive). Pair with query filter (Principle 3).
  • Output: A documented primitive + the one-line justification.
  • Evidence: Pinecone Namespaces vs. metadata filtering; Qdrant Multitenancy and custom sharding; Weaviate Multi-tenancy operations.
OP-03 LockTenantSourceToSession
  • Trigger: Code review reveals tenant_id = request.json["tenant_id"] or similar untrusted source.
  • Action: Bind tenant_id to the authenticated principal (JWT claim, session row, signed header). Pass through one chokepoint (Context / RequestState) — never read user input again downstream.
  • Output: A single get_tenant_id(ctx) accessor used everywhere; no string-typed tenant ids floating through function args.
  • Evidence: OWASP LLM01 (prompt injection) + LLM06 (sensitive info disclosure); Christian Schneider RAG security: the forgotten attack surface.
OP-04 PineconeFilterAtQuery
  • Trigger: Pinecone-backed RAG; multi-tenant.
  • Action: One namespace per tenant; and pass filter={"tenant_id": {"$eq": tenant_id}}. Avoid $in lists >10,000 (hard cap).
    python
    index.query(
        namespace=tenant_id,                       # primary isolation
        vector=embedding,
        top_k=8,
        filter={"tenant_id": {"$eq": tenant_id}},  # defence-in-depth
        include_metadata=True,
    )
  • Output: Two-layer isolation; delete_namespace(tenant_id) for offboarding.
  • Evidence: docs.pinecone.io/guides/index-data/implement-multitenancy, docs.pinecone.io/troubleshooting/namespaces-vs-metadata-filtering.
OP-05 WeaviateTenantHandle
  • Trigger: Weaviate-backed RAG.
  • Action: Enable multiTenancyConfig(enabled=True) on the collection; create one tenant per customer; pass tenant= on every read/write.
    python
    coll = client.collections.get("Docs").with_tenant(session.tenant_id)
    coll.query.near_vector(vector=emb, limit=8)
    Optionally auto_tenant_activation=True if tenant set is sparse.
  • Output: Each tenant on a separate shard; cross-tenant reads physically impossible from a single client handle.
  • Evidence: docs.weaviate.io/weaviate/manage-collections/multi-tenancy; Rethinking Vector Search at Scale (Weaviate blog).
OP-06 QdrantIsTenantPayload
  • Trigger: Qdrant-backed RAG with ≥hundreds of tenants.
  • Action: Single collection; create payload index with is_tenant=True; filter on every search.
    python
    client.create_payload_index(
        collection_name="docs",
        field_name="tenant_id",
        field_schema=models.KeywordIndexParams(
            type="keyword", is_tenant=True))
    
    client.query_points(
        collection_name="docs",
        query=emb, limit=8,
        query_filter=models.Filter(must=[
            models.FieldCondition(
                key="tenant_id",
                match=models.MatchValue(value=session.tenant_id))]),
    )
  • Output: Per-tenant sub-indexes co-located on shared shards; scales to millions of tenants.
  • Evidence: qdrant.tech/documentation/manage-data/multitenancy/; qdrant.tech/articles/multitenancy/; qdrant.tech/documentation/examples/llama-index-multitenancy/.
OP-07 ChromaWhereFilter
  • Trigger: Chroma-backed RAG (often local / smaller scale).
  • Action: Filter on every call.
    python
    collection.query(
        query_embeddings=[emb], n_results=8,
        where={"$and": [
            {"tenant_id": {"$eq": session.tenant_id}},
            {"deleted":   {"$eq": False}},
        ]},
    )
    Same where= on get, update, delete.
  • Output: Logical isolation. Pair with per-tenant collection for stronger isolation if cardinality permits.
  • Evidence: docs.trychroma.com/docs/querying-collections/metadata-filtering; cookbook.chromadb.dev/strategies/multi-tenancy/naive-multi-tenancy/.
OP-08 PgvectorRLS
  • Trigger: pgvector-backed RAG inside an existing Postgres app.
  • Action: Add a tenant_id column; enable RLS; create a policy.
    sql
    ALTER TABLE chunks ENABLE ROW LEVEL SECURITY;
    CREATE POLICY tenant_isolation ON chunks
        USING (tenant_id = current_setting('app.current_tenant')::uuid);
    
    -- per-request, before the ORDER BY embedding <-> $1:
    SET LOCAL app.current_tenant = '...';
    Application can never see other tenants' rows even with SELECT *.
  • Output: Database-enforced isolation; survives ORM bugs and SQL typos.
  • Evidence: Supabase pgvector multi-tenant RAG references; standard Postgres RLS docs.
OP-09 CrossTenantPropertyTest
  • Trigger: Any retrieval code change merges.
  • Action:
    python
    def test_cross_tenant_isolation(retriever_factory):
        ingest("alpha", ["alpha-secret about widgets X1, X2"])
        ingest("beta",  ["beta confidential roadmap for Q3"])
    
        # Query as alpha for a topic beta owns
        r_alpha = retriever_factory(tenant="alpha").retrieve("Q3 roadmap")
        assert all(n.metadata["tenant_id"] == "alpha" for n in r_alpha)
        assert len(r_alpha) >= 0   # zero is fine; foreign is not
    
        # And the reverse
        r_beta = retriever_factory(tenant="beta").retrieve("widget X1")
        assert all(n.metadata["tenant_id"] == "beta" for n in r_beta)
  • Output: Regression-proof guarantee that the filter is wired through. Gates merge.
  • Evidence: Direct application of Principle 1; mirrors red-team test patterns from CSO Online Securing RAG pipelines in enterprise SaaS.
OP-10 RuntimeAssertReturned
  • Trigger: Production endpoint serving multi-tenant RAG.
  • Action: After every retrieve, before passing to LLM:
    python
    bad = [n for n in nodes
           if n.metadata.get("tenant_id") not in {ctx.tenant, GLOBAL_TENANT}]
    if bad:
        log.critical("cross_tenant_leak", tenant=ctx.tenant, bad=bad)
        raise SecurityError("retrieval boundary violated")
  • Output: Silent leak → loud 500; alert fires on the first occurrence.
  • Evidence: Defence-in-depth with audit logging; mirrors EchoLeak / CVE-2024-41892 mitigations.
OP-11 LlamaIndexAutoRetriever (advanced, with caveat)
  • Trigger: Heterogeneous metadata; tenant + doc_type + date filters; want LLM to derive filter from natural language.
  • Action: VectorIndexAutoRetriever with a VectorStoreInfo describing filterable fields. Pin tenant_id server-side — the auto-retriever decides other filters, but tenant_id is always injected from the session.
  • Output: Flexible filter selection without exposing tenant to LLM.
  • Evidence: LlamaIndex MetadataFilters + AutoRetriever docs; Principle 2 (tenant from session, not from content).
OP-12 NamespaceVsFilterDecision
  • Trigger: Greenfield; one tenant per namespace vs payload partition.
  • Action: Use the rubric:
    • Cross-tenant queries ever needed (admin search, support tooling)? → payload partition (filter); namespace per tenant blocks cross-namespace queries.
    • Per-tenant deletion / GDPR erasure frequent? → namespace per tenant (delete_namespace is O(1)).
    • Tenant count >10k? → payload partition (namespaces have administrative overhead at scale).
    • Read cost-sensitive on Pinecone? → namespace per tenant (RU billed per namespace size).
  • Output: One documented choice + a written rationale.
  • Evidence: docs.pinecone.io/troubleshooting/namespaces-vs-metadata-filtering.

5. 困境决策案例 (Dilemma Cases)

Dilemma 1 — Shared embeddings vs per-tenant embeddings

困境: Re-embedding the same public chunk (e.g. shared regulation text) once per tenant doubles cost and complicates updates. But sharing a vector across tenants means it lives in a "global" pool that must be readable by all — opening a path for poisoned global content to reach every tenant.

约束:

  • Embeddings are deterministic given (model, text) — no cryptographic per- tenant signal in the vector itself.
  • A shared vector with tenant_id="GLOBAL" is by definition in scope for every tenant's filter — it is not isolated, it is intentionally shared.
  • Poisoning at ingestion time of shared content propagates to every tenant simultaneously (Slack AI 2024 incident pattern).

决策步骤:

  1. Classify content: tenant-private vs shared-public.
  2. Shared-public goes to a separate global namespace / collection with write-restricted ingestion (only platform admins, signed source) and is served via a second retrieval call, not by widening the tenant filter.
  3. Per-query: retrieve from tenant pool + (optionally) from global pool, union and dedupe at the application layer with explicit provenance tagging in each node's metadata.
  4. Never mix them by filter={"tenant_id": {"$in": [tenant, "GLOBAL"]}} — that pattern hides which pool a chunk came from in downstream logs.

结果: Two pools, two retrieval calls, one application-layer merge. Cost deduplicated for public content; private content stays in private isolation; provenance is auditable.

可提取的操作: OP-01, OP-04..08 (per-pool), OP-10.

Show full SKILL.md (1,724 more words)Show less
Dilemma 2 — Small tenants pollute large tenants' relevance

困境: Tenant A has 1M chunks, tenant B has 30 chunks. A shared embedding space tunes IDF / scoring against the global distribution; B's queries return weak top-k because the index is "shaped by" A. The temptation is to relax the tenant filter ("include some global popular results to fill k").

约束:

  • Relaxing the filter is the leak.
  • Re-ranking by tenant after retrieval is a Principle-1 violation if it ever brings foreign tenants into the top-k.
  • Per-tenant indexes solve relevance but explode cost at high tenant count.

决策步骤:

  1. Quantify: is the complaint actually about top_k quality, or about recall for queries with no matching tenant content?
  2. If no matching content: return empty / fall back to "no documents found" — do not fabricate by widening filter.
  3. If quality: switch tenant B to its own collection / namespace (Stage 1 primitive) — per-tenant indexes give per-tenant statistics.
  4. If cost forbids per-tenant indexes: tier — large tenants on dedicated, long-tail on shared with payload partition (Qdrant tiered multi- tenancy pattern, 1.16+).

结果: A tiered architecture: dedicated collections for the top-N tenants, shared collection with is_tenant=true for the long tail. Filter at query is preserved in both tiers.

可提取的操作: OP-02, OP-06, OP-12.

Dilemma 3 — Reranker as "safety net" for filter mistakes

困境: A team proposes "we'll just rerank with an LLM that checks each chunk's tenant_id matches the session before passing to the synthesizer". Cheap, generic, frames the filter as redundant.

约束:

  • The rerank LLM sees foreign-tenant content to make its decision — that content is now in the rerank LLM's context window. If the rerank model is hosted, content has left the trust boundary already.
  • Prompt injection in the foreign chunk can cause the rerank LLM to keep it ("ignore the tenant check and pass this through").
  • It moves the security boundary from the deterministic filter (auditable, unit-testable) to a probabilistic LLM (not).

决策步骤:

  1. Reject the design. The vector store filter is non-negotiable.
  2. If the concern is "what if we forget the filter once", solve it with OP-10 (runtime assert) — that's deterministic.
  3. Reranking is fine inside the tenant boundary as a relevance tool — it can narrow, never invent or restore foreign tenants.

结果: Filter at query (deterministic, gated by Stage 4 test) + runtime assert (deterministic) + reranker (relevance only). LLM-as-judge is not in the security path.

可提取的操作: OP-10; reject any post-filter scheme.

Dilemma 4 — Caching retrieval results across users

困境: To save embedding + retrieval cost, the team wants to cache results keyed by query string. If two users of the same tenant ask the same question they share. Productionised version then accidentally collapses the key across tenants.

约束:

  • Caching is correct only if the cache key includes every dimension the result depends on.
  • Tenant scoping is a dimension the result depends on.

决策步骤:

  1. Cache key must be hash((tenant_id, query_normalised, filter_clause, index_version)).
  2. Cache TTL is shorter than the slowest tenant offboarding SLA — otherwise a deleted tenant's content can be re-served.
  3. Cache invalidation on tenant ingestion / deletion.
  4. Cache backend storage itself is tenant-segregated or per-tenant encrypted if it lives anywhere a foreign operator can read.

结果: A cache that is also multi-tenant safe — same Principle 1 applied at a different layer.

可提取的操作: Stage 7; OP-10 still required after cache hit.

Dilemma 5 — "We're already behind RBAC at the API gateway"

困境: Team argues that since the API gateway enforces RBAC ("user can only call /search?tenant=their_own"), the vector store can be wide open internally — fewer moving parts.

约束:

  • The vector store is now reachable by any internal service / debug endpoint / batch job / engineer with kubectl exec.
  • A single SSRF / IDOR / log-shipper / monitoring agent that talks to the vector store with broad credentials defeats the gateway.
  • CVE-2024-41892 (Pinecone) is exactly this shape — RBAC after retrieval, vector store internally trusting.

决策步骤:

  1. Treat the vector store as a directly exposed datastore for threat-model purposes.
  2. Add tenant filter at query and per-tenant credentials at the vector store layer where the vendor supports it.
  3. If the vendor does not support per-tenant credentials (most don't), compensate with OP-10 (runtime assertion) — it catches the internally-misissued query.

结果: Gateway RBAC is additional, not load-bearing. The vector store itself enforces tenancy.

可提取的操作: OP-10, OP-08 (RLS where applicable), Stage 5.


6. 反模式与边界 (Anti-patterns & Boundaries)

Top anti-patterns (instant red flags in code review)
#Anti-patternWhy it's wrongCorrect move
A1results = vs.query(...); results = [r for r in results if r.metadata["tenant"] == ctx.tenant]Post-filter; the foreign content was already retrieved, scored, and (next step) injected into LLM contextFilter at query: pass tenant into vendor's filter= / where= / query_filter=
A2Trusting the LLM with system_prompt += "only use chunks where tenant=X"LLMs don't enforce; chunks already in contextNever inside the context window
A3tenant_id derived from request JSON body or query paramAttacker-controlled; trivial IDORBind to authenticated session/JWT claim once at the chokepoint
A4tenant_id extracted from document content at ingestionDocument body is attacker-controlled (for any user-uploaded doc)Source from upload-time session, write immutable
A5Same vector index, no tenant field at all, "we filter by ACL later"Without the key in metadata, the filter is impossible at queryOP-01: embed at ingest, refuse without key
A6Rerank LLM as the security boundaryForeign content already in rerank model's context; prompt-injectableRerank narrows only; filter is the boundary (Dilemma 3)
A7Cache keyed on query alone, not (tenant, query)Cross-tenant cache hit returns foreign tenant's answerOP cache-key contract (Dilemma 4)
A8One shared "global" namespace queried with {"tenant_id": {"$in": [t, "global"]}} and no provenance trackingLoses audit trail; lets a poisoned global chunk reach all tenants invisiblyTwo pools, two queries, explicit provenance (Dilemma 1)
A9"We have one index per customer so we don't need a filter"One config typo / one shared client reused across customers and isolation collapsesDefence in depth: namespace + filter both (Principle 3)
A10No cross-tenant property test in CIFirst leak detected by a customerOP-09 mandatory; runs on every PR
A11Schema drift — tenant_id is sometimes int, sometimes UUID, sometimes string$eq silently fails to match across typesSchema-validate metadata at ingest (Pydantic) and assert on read
A12Verbose logging of full chunk text in shared observability backendForeign tenant content readable by ops staff of other tenantsLog hashes / IDs; redact body in shared sinks
Boundaries — when this skill is not the right move
  • B1 Single-tenant or public corpora — no tenant key exists; skill is over-engineering.
  • B2 Truly per-tenant infrastructure (one cluster per customer, separate creds) — the boundary is now infra/IAM. Apply this skill only if app code still mixes contexts.
  • B3 Hard real-time retrieval (sub-50ms) with vendor that materially penalises filters — measure first; if real, partition into per-tenant indexes (Stage 1 ladder) rather than weaken the filter.
  • B4 Offline batch / eval over labelled internal data — tenancy is documentation, not security.
PR review smells
  • similarity_search(query) without filter= argument and the corpus is multi-tenant.
  • vs.query(... namespace=request.json["ns"]) — namespace from user input.
  • if node.metadata["tenant_id"] != ctx.tenant: continue anywhere after a retrieval call.
  • A reranker / postprocessor that "decides" which tenant a chunk belongs to.
  • Mock vector store in tests that ignores filter= — tests pass, prod leaks.
  • tenant_id typed as str in one file and int in another — type drift = silent filter miss.
  • OPEN_TO_PUBLIC / __all__ sentinel in metadata used as a fallback when the filter returns empty.

7. 跨框架对照 (Cross-Framework Reference Table)

How the same "filter at query" surface looks across the common stacks. All verified against current docs (May 2026); URLs in References.

7.1 LlamaIndex (vendor-agnostic adapter)
python
from llama_index.core.vector_stores import (
    MetadataFilter, MetadataFilters, FilterOperator,
)

filters = MetadataFilters(
    filters=[MetadataFilter(key="tenant_id",
                            value=ctx.tenant_id,
                            operator=FilterOperator.EQ)],
    # condition=FilterCondition.AND for multi-clause
)
retriever = index.as_retriever(similarity_top_k=8, filters=filters)

Supported operators: ==, !=, >, <, >=, <=, in, nin, text_match. Caveat: the in-memory default vector store does not honour filters — use Qdrant/Chroma/Pinecone/Weaviate/pgvector for any real multi-tenant deployment.

7.2 LangChain (vendor-agnostic adapter)
python
docs = vectorstore.similarity_search(
    query="Q3 roadmap", k=8,
    filter={"tenant_id": ctx.tenant_id},        # simple equality
)

# Chroma-style operators:
docs = vectorstore.similarity_search(
    query="Q3 roadmap", k=8,
    filter={"$and": [
        {"tenant_id": {"$eq": ctx.tenant_id}},
        {"deleted":   {"$eq": False}},
    ]},
)

Filter syntax is vendor-passed-through — Chroma uses $and/$or/$eq, Pinecone uses its own dialect, etc. The filter is forwarded to the vendor; LangChain does not enforce.

7.3 Pinecone (raw SDK)
python
index.query(
    namespace=ctx.tenant_id,                    # primary isolation
    vector=emb, top_k=8,
    filter={"tenant_id": {"$eq": ctx.tenant_id}, # defence-in-depth
            "doc_type":  {"$in": ["policy", "wiki"]}},
    include_metadata=True,
)

Operators: $eq, $ne, $gt, $gte, $lt, $lte, $in (≤10000 values), $nin, $and, $or. Disk-based metadata filtering (2025) lets high- cardinality filters scale without memory blow-up.

7.4 Weaviate (raw client)
python
collection = client.collections.get("Docs").with_tenant(ctx.tenant_id)
res = collection.query.near_vector(
    near_vector=emb, limit=8,
    filters=Filter.by_property("doc_type").equal("policy"),
)

multiTenancyConfig.enabled=True makes the tenant= handle mandatory — the client cannot issue a tenant-less query.

7.5 Qdrant (raw client)
python
client.query_points(
    collection_name="docs",
    query=emb, limit=8,
    query_filter=models.Filter(must=[
        models.FieldCondition(key="tenant_id",
            match=models.MatchValue(value=ctx.tenant_id)),
        models.FieldCondition(key="doc_type",
            match=models.MatchValue(value="policy")),
    ]),
)

With is_tenant=True on the payload index, Qdrant co-locates per-tenant vectors on shared shards and accelerates the filter.

7.6 Chroma (raw client)
python
collection.query(
    query_embeddings=[emb], n_results=8,
    where={"$and": [
        {"tenant_id": {"$eq": ctx.tenant_id}},
        {"deleted":   {"$eq": False}},
    ]},
)

Same where= on get/update/delete. Operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $contains, $not_contains, $and, $or.

7.7 pgvector (raw SQL)
sql
-- Per-request:
SET LOCAL app.current_tenant = :tenant_id;

SELECT chunk_id, body, 1 - (embedding <=> :query_vec) AS score
  FROM chunks
 ORDER BY embedding <=> :query_vec
 LIMIT 8;
-- RLS policy filters tenant_id; no WHERE clause needed in app code.

The RLS policy (OP-08) makes the filter unforgeable from the application code.

7.8 Side-by-side: "is the filter enforced if I forget it?"
StackForgot the filter → what happens
Pinecone (namespace only)Wrong namespace = wrong tenant; namespace argument missing = default "" namespace (cross-tenant if you wrote there)
Weaviate (MT enabled)Client raises — no tenant = no operation
Qdrant (is_tenant only)Returns all tenants' vectors — silent leak
ChromaReturns all docs — silent leak
pgvector + RLSReturns nothing for unauthenticated tenant context — safe by default
LlamaIndex / LangChain wrappersForwards whatever you (don't) pass — same as underlying vendor

This table is the argument for Principle 3: pick a stack where the failure mode is "loud" (Weaviate, pgvector+RLS) or add OP-10 runtime assertion on stacks where the failure mode is "silent" (Qdrant, Chroma, raw Pinecone-without-namespace).


References

Primary vendor docs (cited inline above)
Security / incident references
Companion files
  • references/R1-vendor-filter-cheatsheet.md — exhaustive vendor filter syntax with code blocks; safe to paste into PR descriptions.
  • references/R2-cross-tenant-test-recipes.md — five copy-pasteable property tests (LlamaIndex/LangChain × Pinecone/Qdrant/Chroma).
  • intermediate/research_notes.md — raw research dump (provenance).

© agentsope, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

SKILL.md and 3 other files (references) in skills/agentsop-multi-tenant-rag of agentsope/SkillAlchemy.

  • SKILL.md
  • intermediate/research_notes.md
  • references/R1-vendor-filter-cheatsheet.md
  • references/R2-cross-tenant-test-recipes.md

Open the folder on GitHubat commit d0f0355

Compare with similar skills

Agentsop Multi Tenant RAG next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Agentsop Multi Tenant RAG compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Agentsop Multi Tenant RAG this skillagentsope/SkillAlchemy466—~9.8kAutomated safety check: PassMIT
RAG ArchitectJeffallan/claude-skills12k—~2kAutomated safety check: PassMIT
RAG Implementationwshobson/agents40k9 repos~1.1kAutomated safety check: PassMIT
Hunt RAG Vectorelementalsouls/Claude-BugHunter4.8k—~2.6kAutomated safety check: PassMIT
Pinecone Vector DatabaseOrchestra-Research/AI-Research-SKILLs13k5 repos~2kAutomated safety check: PassMIT
RAG Patternssoftspark/ai-toolkit179—~1.8kAutomated safety check: PassApache-2.0

Similar skills

  • RAG Architect

    Jeffallan/claude-skills

    Designs retrieval-augmented generation systems: document chunking, embeddings, vector store setup, hybrid search, reranking and retrieval evaluation, with checks at each step.

    12k GitHub stars~2k tokensUpdated 6 days ago
    AI & LLM EngineeringAuto-check passed
  • RAG Implementation

    wshobson/agents

    Build retrieval-augmented generation systems: pick a vector database and embedding model, choose retrieval and reranking strategies, and start from a LangGraph pipeline.

    40k GitHub starsUsed in 9 repos~1.1k tokens
    AI & LLM EngineeringAuto-check passed
  • Hunt RAG Vector

    elementalsouls/Claude-BugHunter

    Hunt vector-store / embedding-layer weaknesses in RAG pipelines (OWASP LLM08 Vector and Embedding Weaknesses) — persistent corpus poisoning that survives across sessions and users (distinct from…

    4.8k GitHub stars~2.6k tokensUpdated yesterday
    AI & LLM EngineeringAuto-check passed
  • Pinecone Vector Database

    Orchestra-Research/AI-Research-SKILLs

    Shows how to use Pinecone, a managed vector database, for production RAG, semantic search and recommendations: indexes, upserts, queries, filters and namespaces.

    13k GitHub starsUsed in 5 repos~2k tokens
    DatabasesAuto-check passed
  • RAG Patterns

    softspark/ai-toolkit

    RAG: embeddings, chunking, hybrid search (BM25+vector), reranking, CRAG, multi-hop.

    179 GitHub stars~1.8k tokensUpdated yesterday
    AI & LLM EngineeringAuto-check passed
  • Neo4j Graphrag Skill

    neo4j-contrib/neo4j-skills

    Build GraphRAG retrieval pipelines on Neo4j using the neo4j-graphrag Python package (v1.22.0+).

    114 GitHub stars~4.2k tokensUpdated 3 days ago
    Knowledge ManagementAuto-check: notes

More from agentsope/SkillAlchemy

All 46 skills in this repo
  • Agentsop Aider

    agentsope/SkillAlchemy

    SOP for terminal-based, git-native AI pair programming with Aider (git work-tree + tree-sitter repo-map + edit-format + human-in-loop REPL).

    466 GitHub stars~3.5k tokensUpdated yesterday
    Auto-check passed
  • Agentsop Context Scope Discipline

    agentsope/SkillAlchemy

    Coder-agent working-file budget discipline: keep the editable working set (files you /add into writable context) under ~25k tokens, separate "read" from "edit", delegate breadth to a read-only…

    466 GitHub stars~3k tokensUpdated yesterday
    Auto-check passed
  • Agentsop Cost Tiered Models

    agentsope/SkillAlchemy

    Split a multi-call LM workflow by cognitive load, not by accuracy: let one strong model make the few reasoning decisions and a cheap model do the many mechanical executions (Aider architect+editor…

    466 GitHub stars~3k tokensUpdated yesterday
    Auto-check passed
  • Agentsop Crewai

    agentsope/SkillAlchemy

    SOP for building multi-agent systems with CrewAI — role-based collaboration, sequential/hierarchical processes, Flows, memory, delegation.

    466 GitHub stars~4.8k tokensUpdated yesterday
    Auto-check passed
  • Agentsop Dify

    agentsope/SkillAlchemy

    SOP for building LLM applications on Dify — visual workflow + chatflow + agent + RAG knowledge base + plugin marketplace + observability, self-hostable.

    466 GitHub stars~5.4k tokensUpdated yesterday
    Auto-check: notes
  • Agentsop Multiscale Chunking

    agentsope/SkillAlchemy

    Designs multiscale chunking for RAG by embedding small units for retrieval precision and returning larger context for synthesis.

    466 GitHub stars~4.9k tokensUpdated yesterday
    Auto-check passed

Questions about Agentsop Multi Tenant RAG

What does Agentsop Multi Tenant RAG do?

Security-first SOP for multi-tenant RAG systems. An agent skill from agentsope/SkillAlchemy. Agentsop Multi Tenant RAG is an agent skill from agentsope/SkillAlchemy. Security-first SOP for multi-tenant RAG systems.

When should I use Agentsop Multi Tenant RAG?

Agentsop Multi Tenant RAG fits situations like: tasks that involve Vector databases; tasks that involve Retrieval-augmented generation; tasks that involve Multi-tenancy.

How do I install Agentsop Multi Tenant RAG in Claude Code?

Run `npx skills add agentsope/SkillAlchemy --skill agentsop-multi-tenant-rag -a claude-code`. Or copy the skill folder (skills/agentsop-multi-tenant-rag in agentsope/SkillAlchemy) into .claude/skills/agentsop-multi-tenant-rag in your project. Claude Code loads it when a task matches its description.

How do I install Agentsop Multi Tenant RAG in Codex?

Run `npx skills add agentsope/SkillAlchemy --skill agentsop-multi-tenant-rag -a codex`. Or copy the skill folder (skills/agentsop-multi-tenant-rag in agentsope/SkillAlchemy) into .agents/skills/agentsop-multi-tenant-rag in your project. Codex loads it when a task matches its description.

Can I use Agentsop Multi Tenant RAG in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add agentsope/SkillAlchemy --skill agentsop-multi-tenant-rag -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/agentsop-multi-tenant-rag, .gemini/skills/agentsop-multi-tenant-rag, .github/skills/agentsop-multi-tenant-rag and .opencode/skills/agentsop-multi-tenant-rag in your project.

What does Agentsop Multi Tenant RAG need to run?

Going by SKILL.md and its folder, Agentsop Multi Tenant RAG needs the command-line tools its instructions call (kubectl). Our summary lists: Python 3.

Does Agentsop Multi Tenant RAG access the network?

SKILL.md names 14 domains. As links in the text: qdrant.tech, docs.pinecone.io, developers.llamaindex.ai, pinecone.io, docs.weaviate.io, weaviate.io, docs.trychroma.com, cookbook.chromadb.dev, python.langchain.com, we45.com, csoonline.com, christian-schneider.net, kiteworks.com and beyondscale.tech. This is read from the text; nothing was executed.

Is Agentsop Multi Tenant RAG safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Agentsop Multi Tenant RAG use?

Agentsop Multi Tenant RAG is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Agentsop Multi Tenant RAG use?

About 9.8k tokens (SKILL.md is roughly 39k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 5k tokens, read only when the agent opens those files.

What are the alternatives to Agentsop Multi Tenant RAG?

Skills that share tags, products or a category with Agentsop Multi Tenant RAG: RAG Architect (Jeffallan/claude-skills, 12k stars), RAG Implementation (wshobson/agents, 40k stars), Hunt RAG Vector (elementalsouls/Claude-BugHunter, 4.8k stars) and Pinecone Vector Database (Orchestra-Research/AI-Research-SKILLs, 13k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Agentsop Multi Tenant RAG?

agentsope (a GitHub user) maintains it in agentsope/SkillAlchemy, which has 466 GitHub stars. The repository holds 46 skills in this directory. The repository was last updated on October 9, 2026.

Source: agentsope/SkillAlchemy on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.