10 · Capstone — Enterprise Knowledge Platform¶
This capstone combines four Level 4 modules into one working platform: tenant-scoped retrieval that structurally cannot leak across tenants (module 02), a tenant-scoped cache (modules 02 + 07-adjacent), indirect prompt injection detection on retrieved content (module 05), and an audit log capturing every query, retrieval, and security event (module 06). The result is small enough to read in full, and it actually enforces the isolation guarantee it claims to — verified below with a real assertion, not just a demonstration.
The corpus: two tenants, one confidential document¶
docs = [
{"id": "doc1", "tenant": "acme", "text": "Acme's refund policy is 30 days for annual plans.", "classification": "public"},
{"id": "doc2", "tenant": "acme", "text": "Acme's internal salary bands are confidential.", "classification": "confidential"},
{"id": "doc3", "tenant": "globex", "text": "Globex refund policy is 14 days.", "classification": "public"},
]
Building block 1: tenant-scoped retrieval (module 02's safe pattern)¶
def keyword_search(query, corpus, top_k=2):
q = set(query.lower().split())
scored = []
for d in corpus:
overlap = len(q & set(d["text"].lower().split()))
scored.append((overlap, d))
scored.sort(key=lambda x: -x[0])
return [d for s, d in scored[:top_k] if s > 0]
def tenant_scoped_search(query, docs, tenant, top_k=2):
scoped = [d for d in docs if d["tenant"] == tenant] # filter BEFORE ranking
return keyword_search(query, scoped, top_k=top_k)
Building block 2: injection detection on retrieved content (module 05)¶
import re
INJECTION_PATTERNS = [r"ignore (all )?previous instructions", r"admin mode", r"system prompt"]
def detect_injection(text):
return [p for p in INJECTION_PATTERNS if re.search(p, text.lower())]
Building block 3: audit logging (module 06)¶
import time
class AuditLog:
def __init__(self):
self.entries = []
def record(self, actor, action, resource, metadata=None):
entry = {"ts": time.time(), "actor": actor, "action": action,
"resource": resource, "metadata": metadata or {}}
self.entries.append(entry)
return entry
audit = AuditLog()
Assembling the platform: tenant-scoped cache + retrieval + security check¶
import hashlib
cache = {}
def cached_tenant_search(query, docs, tenant):
key = hashlib.sha256(f"{tenant}:{query}".encode()).hexdigest() # tenant IS part of the key
if key in cache:
audit.record(f"tenant:{tenant}", "cache_hit", "query", {"q": query})
return cache[key], True
results = tenant_scoped_search(query, docs, tenant)
for r in results:
hits = detect_injection(r["text"])
if hits:
audit.record("system", "injection_flagged", r["id"], {"patterns": hits})
cache[key] = results
audit.record(f"tenant:{tenant}", "retrieve", "query",
{"q": query, "doc_ids": [r["id"] for r in results]})
return results, False
def platform_query(query, tenant):
audit.record(f"tenant:{tenant}", "query", "platform", {"q": query})
return cached_tenant_search(query, docs, tenant)
Running it¶
r1, c1 = platform_query("refund policy", "acme")
print("acme query result:", [d["id"] for d in r1], "cached:", c1)
r2, c2 = platform_query("refund policy", "acme") # repeat, same tenant
print("acme repeat query:", [d["id"] for d in r2], "cached:", c2)
r3, c3 = platform_query("refund policy", "globex") # different tenant, same query text
print("globex query result:", [d["id"] for d in r3], "cached:", c3)
Captured output:
acme query result: ['doc1'] cached: False
acme repeat query: ['doc1'] cached: True
globex query result: ['doc3'] cached: False
Acme's second identical query hits the cache. Globex's identical query text
does not — because the cache key includes the tenant, "refund policy"
from Globex is a distinct cache entry from Acme's, exactly the fix module 02
and module 07's traps both called for. Note that doc2 (Acme's confidential
salary bands) never appears in any result — the query "refund policy" simply
doesn't match it, which is retrieval relevance working correctly, not access
control; classification-based filtering on top of tenant scoping is a real
gap this toy platform doesn't close, called out explicitly below.
The audit trail for this run:
Captured output:
tenant:acme query platform {'q': 'refund policy'}
tenant:acme retrieve query {'q': 'refund policy', 'doc_ids': ['doc1']}
tenant:acme query platform {'q': 'refund policy'}
tenant:acme cache_hit query {'q': 'refund policy'}
tenant:globex query platform {'q': 'refund policy'}
tenant:globex retrieve query {'q': 'refund policy', 'doc_ids': ['doc3']}
Every query is attributable to its tenant, and the log distinguishes a fresh
retrieval from a cache hit — exactly the granularity module 06 argued for:
an auditor asking "did Acme ever see Globex's document" can answer it
directly from doc_ids in these entries, without re-running anything.
Verifying the isolation guarantee, not just demonstrating it¶
assert all(d["id"] != "doc3" for d in r1), "acme saw globex's document"
assert all(d["id"] not in ("doc1", "doc2") for d in r3), "globex saw acme's documents"
print("isolation check: PASSED")
Captured output:
This is the difference between a demo and a tested guarantee: the assertion actually fails loudly if tenant scoping breaks, rather than relying on eyeballing printed output. A real platform's CI suite (Level 3 module 06) would run exactly this kind of check, with many more tenants and queries, including adversarial ones engineered the way module 02's exercise describes, on every change to the retrieval or caching code.
What each Level 4 module contributed¶
| Module | What it gave this platform |
|---|---|
| 02 · Multi-Tenant Access Control | tenant_scoped_search's pre-filter pattern, and the tenant-scoped cache key |
| 05 · RAG Security | detect_injection running on every retrieved chunk before it reaches a prompt |
| 06 · Governance & Compliance | AuditLog, attributing every action to a tenant and distinguishing cache hits from fresh retrievals |
| 03 / 04 · Cost & Latency | The cache itself — cutting cost and latency on repeated queries, safely, because it's tenant-scoped |
Modules 01 (architecture pattern choice), 07 (continuous evaluation), 08 (fine-tuning vs. RAG), and 09 (ingestion scaling) apply to operating and evolving this platform in production but aren't wired into this toy version — extending it with real evaluation, a real ingestion pipeline, and a documented architecture decision (module 01) is the gap between this capstone and a production enterprise platform.
How It Actually Works¶
Why "verifying the isolation guarantee" requires a different kind of test than "demonstrating a correct answer." A demonstration runs the happy path (tenant A asks a question, gets tenant A's data back) and confirms it looks right — but that test alone cannot distinguish "isolation is enforced" from "isolation happened to hold for this particular query," because it never exercises the failure mode isolation exists to prevent. Verifying the guarantee means adversarially constructing a query designed to retrieve the other tenant's confidential document (using vocabulary drawn from that document, so an unfiltered similarity search would rank it highly) and confirming the tenant-scoped filter (level-4 lesson 2's filter-before- ranking pattern) excludes it before ranking runs, not just that the final answer happens not to mention it — because a filter applied too late could still have let that content into a reranker or cache (level-4 lesson 2's point about intermediate stages) even if the final generated text looks clean.
Why this capstone's three building blocks compose without conflicting, and what would break if they didn't. Tenant-scoped retrieval (filtering before the ANN search touches other tenants' vectors), injection detection on retrieved content (screening text after it's retrieved but before it reaches the prompt), and audit logging (recording what was retrieved and why) sit at three different points in the same pipeline — retrieval-time, post-retrieval-pre-generation, and cross-cutting observation — which is why they layer rather than interfere: tenant scoping determines what enters the candidate set, injection detection determines whether retrieved text is safe to place in context, and audit logging records the resulting decision chain regardless of what the first two stages did. If tenant scoping ran after injection detection instead of before, injection detection would be needlessly scanning other tenants' documents that should never have been candidates at all — the ordering isn't arbitrary, it mirrors each lesson's original claim about which stage the fix belongs at.
Stretch goals¶
- Add
classificationenforcement: extendtenant_scoped_searchto also filter by auser_clearanceparameter, so a query from a non-privileged user within the same tenant still cannot retrievedoc2(confidential). Write a test confirming a privileged and unprivileged user of the same tenant get different result sets for an otherwise-identical query. - Replace
detect_injection's pattern list with the paraphrase-resistant heuristic from module 05's exercise, and re-run the platform against a planted injection document that avoids every literal phrase inINJECTION_PATTERNS— confirm it's still flagged. - Add a
retention_daysfield toAuditLogentries and apurge_expiredmethod (module 06), then simulate 91 days passing and confirm entries older than a 90-day policy are removed while a summary tombstone survives. - Wire
platform_query's cost (one embedding-equivalent call per non-cached, non-injection-flagged query) into the cost model from module 03, and compute the dollar savings the tenant-scoped cache produces at 10,000 queries/day with a 25% repeat-query rate.