Build it. Then try to break it.
Follow one request from the badge desk to the response.

The records desk at Sachin is ready to open. We have a badge desk, labeled folders, a clerk who selects allowed extracts, and a dispatch check before the answer leaves.
Now send someone with a handwritten Engineering label. Send a person from another company. Revoke a grant while an answer is being prepared. A mechanism becomes easier to trust when you can explain what each of those requests will do and then observe it.
This final article connects the five earlier lessons into a runnable local service. The core rule remains same company AND (a direct user grant OR a matching group).
Can you follow the whole request without trusting a forged badge?
1Resolve the real badge
The server’s session table identifies Maya. Body-supplied groups are ignored.
Badge desk → identity adapter2Prepare the packet
Company and grants decide which records enter her evidence.
Records clerk → authorized retrieval3Test the dispatch gate
A changed policy or session stops the prepared response. Source links get checked too.
Final check → buffered response boundaryResolve identity, select allowed evidence, and check again before the response leaves.
What you are building
The download is one JavaScript module for Node.js 20 or newer. It contains the fictional documents, policy functions, server-side session table, response cache, audit records, assertions, and two HTTP routes. The server uses Node’s built-in HTTP module, so there are no packages to install. Node HTTP server API.
The tokens support-demo and engineering-demo are public exercise credentials. A real login system must verify identity and maintain trustworthy sessions and membership. Here, a server-side lookup stands in for that adapter; it lets us demonstrate why client-supplied group claims cannot become authority.
The service supports one fixed question: “Why is the Orion launch delayed?” Other questions receive 400. Its scores are illustrative, its store is in memory, and its answers are fixed facts. It calls no embedding service, vector engine, or language model. The permission mechanism is executable; those other integrations remain explicit seams.
Follow one request through six decisions
The HTTP handler separates preparing evidence from finishing a response. Read the sequence as the clerk’s checklist:
- Resolve the badge. Look up the bearer token in the server’s session table. An unknown token receives
401. - Validate the question. Accept the exercise question. Ignore
groupsandtenantIdsupplied in the request body. - Require usable policy. An unavailable permission service or malformed document policy receives
503. Do not turn a failed check into an allow. - Prepare permitted evidence. Apply the company and grant rule, then rank the eligible records. No eligible evidence produces an empty source list and an explanatory answer.
- Check again before delivery. If the policy revision changed, discard the prepared response with
409. Recheck the session and current grants too. - Return the permitted result. Reuse only a matching cache entry after the checks, or assemble fixed facts and cache them. Record minimal server-side audit fields. A citation click goes through its own source authorization.
The revision comparison is a small form of optimistic concurrency control: prepare against a known version, then refuse to commit the result if that version changed. In this single-process example, response assembly after the final check is synchronous. There is no asynchronous work between that check and writing the buffered response.
Try to forge the group
Predict: Maya sends the correct Support token but adds groups: ["engineering"] to the JSON body. Which documents should appear?
Send a request. Inspect every boundary.
Fictional records. Browser simulation. No live model call.
Each selection starts from a fresh fixture. The two revocation scenarios include their own earlier steps.
Inside the server
- 1Resolve identity from the server’s session table
- 2Ignore client permission claims
- 3Check tenant and current document grants
- 4Prepare permitted evidence
- →Current checks passed; return permitted result
The HTTP response
200{
"answer": "Orion’s launch has moved to 14 October while the team completes reliability checks. [D1]\n\nExisting workspaces stay available. Customers do not need to take any action. [D3]",
"sources": [
{
"id": "D1",
"title": "Customer launch update"
},
{
"id": "D3",
"title": "Customer support FAQ"
}
],
"modelCalled": false
}Inspect the input, rule, and output
Input / state
{
"requestBody": {
"question": "Why is the Orion launch delayed?",
"groups": [
"engineering"
],
"tenantId": "other"
},
"resolvedPrincipal": {
"id": "maya",
"name": "Maya",
"role": "Support",
"tenantId": "sachin",
"groups": [
"support"
]
},
"preparedRevision": 1,
"currentRevision": 1
}Decision rule
identity = serverSession[token]
ignore body.groups and body.tenantId
select evidence using tenant AND grants
check current revision + session + grants
return only selected response fieldsOutput
{
"response": {
"answer": "Orion’s launch has moved to 14 October while the team completes reliability checks. [D1]\n\nExisting workspaces stay available. Customers do not need to take any action. [D3]",
"sources": [
{
"id": "D1",
"title": "Customer launch update"
},
{
"id": "D3",
"title": "Customer support FAQ"
}
],
"modelCalled": false
},
"auditOnServer": [
{
"requestId": "request-1",
"status": 200,
"policyVersion": 1,
"returnedIds": [
"D1",
"D3"
]
}
]
}This view runs the same deterministic functions as the downloadable example. The short rule above summarizes the operation; the download contains the complete implementation.
The server still resolves Maya as Support and returns D1 and D3. The claimed group is visible in the input inspector, but it never participates in the permission decision.
The other-company case uses the same user ID and group name in a different company. It receives no Sachin sources. Matching a group alone would be insufficient; the company condition must also pass. This example uses a shared collection with an explicit company predicate. Separate indexes can add another boundary, but routing and credentials would still need their own checks.
Test both kinds of revocation race
The “revoked after caching” case first prepares Arun’s answer with D2, then removes D2’s grants and advances the policy revision. Arun’s next request returns only D1. The old cache entry remains stored under an old key and is not reused.
The “revoked during request” case prepares Arun’s evidence first, changes the revision, then finishes that same request. It receives 409 and no sources. This is a different test: a cache miss on a new request does not prove that an already prepared response will be discarded.
The final gate also detects a removed or changed session and checks every selected source again. These tests operate on the same in-memory state that the service uses. They do not establish distributed consistency across a real identity provider and multiple permission replicas.
Run the file and its assertions
Download the module, open a terminal in the download directory, and run it. The assertions execute before the optional server starts.
Run the same model yourself.
Node.js 20 or newer. No packages or API keys. Save the file, open a terminal in its directory, and run the command below.
Download part 6 · JavaScript ↓node sachin-permissions-6.mjsEXPECTED OUTPUT
maya: 200 [D1, D3]
arun: 200 [D2, D1]
forged-groups: 200 [D1, D3]
other-tenant: 200 []
missing-session: 401 []
outage: 503 []
revoked: 200 [D1]
in-flight: 409 []
Cache and audit checks passed.Read the complete source and assertions
// Sachin RAG permissions, part 6. Node.js 20+. Fictional fixtures; fixed answers.
// @ts-check
/** Fictional teaching fixtures. This simulation is not an authorization boundary.
* @typedef {{id: string, name: string, role: string, tenantId: string, groups: readonly string[]}} Principal
* @typedef {{id: string, title: string, team: string, tenantId: string, allowedGroups: readonly string[], allowedUsers: readonly string[], match: number, fact: string}} Evidence
*/
/** @type {readonly Principal[]} */
export const PEOPLE = [
{ id: "maya", name: "Maya", role: "Support", tenantId: "sachin", groups: ["support"] },
{ id: "arun", name: "Arun", role: "Engineering", tenantId: "sachin", groups: ["engineering"] },
];
export const QUESTION = "Why is the Orion launch delayed?";
/** @type {readonly Evidence[]} */
export const DOCUMENTS = [
{
id: "D1", title: "Customer launch update", team: "Shared", tenantId: "sachin",
allowedGroups: ["support", "engineering"], allowedUsers: [], match: 87,
fact: "Orion’s launch has moved to 14 October while the team completes reliability checks.",
},
{
id: "D2", title: "Engineering incident note", team: "Engineering", tenantId: "sachin",
allowedGroups: ["engineering"], allowedUsers: [], match: 98,
fact: "Sign-in requests time out under peak load. The identity team is fixing the session refresh path.",
},
{
id: "D3", title: "Customer support FAQ", team: "Support", tenantId: "sachin",
allowedGroups: ["support"], allowedUsers: [], match: 82,
fact: "Existing workspaces stay available. Customers do not need to take any action.",
},
{
id: "D4", title: "Launch cost review", team: "Finance", tenantId: "sachin",
allowedGroups: ["finance"], allowedUsers: [], match: 91,
fact: "The delay adds $24,000 to the internal launch budget.",
},
];
// @example:policy
/** Identity must already come from a trusted session; this function only checks permission.
* @param {Principal | null} user @param {Evidence} chunk */
export function checkAccess(user, chunk) {
const hasIdentity = user !== null;
const sameCompany = hasIdentity && user.tenantId === chunk.tenantId;
const directGrant = hasIdentity && chunk.allowedUsers.includes(user.id);
const matchingGroups = user?.groups.filter(group => chunk.allowedGroups.includes(group)) ?? [];
const groupGrant = matchingGroups.length > 0;
const allowed = sameCompany && (directGrant || groupGrant);
const reason = !hasIdentity ? "No verified identity"
: !sameCompany ? "Different company"
: directGrant ? "Direct user grant"
: groupGrant ? `${matchingGroups.join(", ")} group grants access`
: "No matching permission";
return { hasIdentity, sameCompany, directGrant, groupGrant, matchingGroups, allowed, reason };
}
// @example:end-policy
/** @param {Principal | null} user @param {readonly Evidence[]} documents */
export function retrieveEvidence(user, documents = DOCUMENTS) {
return documents
.filter(chunk => checkAccess(user, chunk).allowed)
.sort((a, b) => b.match - a.match);
}
/** @param {Principal | null} user @param {readonly Evidence[]} documents */
export function buildAnswer(user, documents = DOCUMENTS) {
const evidence = retrieveEvidence(user, documents);
const context = evidence.map(chunk => `[${chunk.id}] ${chunk.title}\n${chunk.fact}`).join("\n\n");
// These are example request messages. This lesson makes no model call.
const messages = evidence.length ? [
{ role: "system", content: "Use the supplied evidence to answer the question. Cite each source ID. If the evidence is insufficient, say so." },
{ role: "user", content: `Question: ${QUESTION}\n\nPermitted context:\n${context}` },
] : [];
return {
evidence,
context,
messages,
// Assemble fixed facts to make the experiment reproducible, without a model.
sentences: evidence.map(({ id, fact }) => ({ sourceId: id, text: fact })),
emptyMessage: evidence.length ? null : "I don’t have accessible evidence to answer this question.",
};
}
/** Fictional, deterministic teaching models shared by the diagrams and downloads.
* @typedef {import('./rag-permissions.mjs').Principal} Principal
* @typedef {import('./rag-permissions.mjs').Evidence} Evidence
*/
export function hasPolicy(record) {
const ids = value => Array.isArray(value) && value.every(id => typeof id === "string" && id.length > 0);
return typeof record?.tenantId === "string" && record.tenantId.length > 0
&& ids(record.allowedGroups) && ids(record.allowedUsers);
}
export function ingestDocument(document, paragraphs, aclVersion = 1) {
if (!hasPolicy(document)) throw new Error("Missing or invalid permission metadata");
if (!Number.isInteger(aclVersion) || aclVersion < 1) throw new Error("Invalid policy version");
if (!Array.isArray(paragraphs) || paragraphs.some(text => typeof text !== "string" || !text.trim())) {
throw new Error("Chunks must contain nonempty text");
}
return paragraphs.map((fact, index) => ({
...document, id: `${document.id}.${index + 1}`, parentId: document.id,
fact, aclVersion, allowedGroups: [...document.allowedGroups], allowedUsers: [...document.allowedUsers],
}));
}
// Ten fictional passages extend the same four documents. Scores are illustrative, not vector distances.
/** @type {Array<[string, number, number, string]>} */
const rankingRows = [
["D2", 1, 98, "Sign-in requests time out under peak load."],
["D4", 1, 96, "The delay adds $24,000 to the internal launch budget."],
["D1", 1, 94, "Orion’s launch has moved to 14 October."],
["D2", 2, 92, "The identity team is fixing the session refresh path."],
["D3", 1, 90, "Existing workspaces stay available."],
["D1", 2, 88, "The team is completing reliability checks."],
["D3", 2, 86, "Customers do not need to take any action."],
["D1", 3, 84, "The launch update will be revised after the next readiness review."],
["D4", 2, 82, "Finance will reconcile launch costs after release."],
["D3", 3, 80, "Support will share the next approved launch update."],
];
export const RANKED_CHUNKS = rankingRows.map(([parentId, ordinal, match, fact]) => {
const parent = DOCUMENTS.find(document => document.id === parentId);
if (!parent) throw new Error("Unknown parent document");
return { ...parent, id: `${parentId}.${ordinal}`, parentId, match, fact, aclVersion: 1 };
});
/** Exact ranked-list experiment; it does not simulate an ANN graph. */
export function compareRetrieval(user = PEOPLE[0], k = 5, fetchCount = 5) {
const ranked = [...RANKED_CHUNKS].sort((a, b) => b.match - a.match);
const isAllowed = chunk => hasPolicy(chunk) && checkAccess(user, chunk).allowed;
const eligible = ranked.filter(isAllowed);
const ideal = eligible.slice(0, k);
const fetched = ranked.slice(0, fetchCount);
const after = fetched.filter(isAllowed).slice(0, k);
const recovered = after.filter(chunk => ideal.some(target => target.id === chunk.id)).length;
return { ranked, eligible, ideal, fetched, after, missed: ideal.filter(chunk => !after.some(hit => hit.id === chunk.id)),
recall: ideal.length ? recovered / ideal.length : null };
}
/** Each stage is a new snapshot; moving backwards never mutates the fixtures. */
export function revocationSnapshot(stage = 0, enforceCurrent = true) {
const original = DOCUMENTS[1];
const source = { ...original, allowedGroups: stage >= 1 ? [] : [...original.allowedGroups], aclVersion: stage >= 1 ? 2 : 1 };
const indexed = stage >= 2 ? { ...source } : { ...original, aclVersion: 1 };
const indexAllows = checkAccess(PEOPLE[1], indexed).allowed;
const currentAllows = checkAccess(PEOPLE[1], source).allowed;
const oldCacheExists = stage < 3;
const cacheVersion = 1;
const cacheUsable = oldCacheExists && cacheVersion === source.aclVersion && currentAllows;
return { source, indexed, indexAllows, currentAllows, oldCacheExists, cacheUsable,
contextIncludesD2: indexAllows && (!enforceCurrent || currentAllows),
cacheVersion, policyVersion: source.aclVersion };
}
/** Source IDs are looked up and authorized again on every source request. */
export function readSource(user, id, documents = DOCUMENTS) {
const document = documents.find(item => item.id === id && item.tenantId === user?.tenantId);
if (!document || !hasPolicy(document) || !checkAccess(user, document).allowed) {
return { status: 404, body: { message: "Source unavailable" } };
}
return { status: 200, body: { id: document.id, title: document.title, text: document.fact } };
}
export function inspectSurface(personId = "maya", surface = "citation", enforce = true) {
const person = PEOPLE.find(user => user.id === personId) ?? PEOPLE[0];
const permitted = buildAnswer(person);
const restricted = DOCUMENTS[1];
const safe = surface === "citation" ? readSource(person, "D2")
: surface === "preview" ? { status: 200, body: permitted.evidence.map(({ id, title }) => ({ id, title })) }
: surface === "shared-answer" ? { status: 200, body: permitted.sentences }
: { status: 200, body: { requestId: "demo-1", outcome: "allowed", returnedIds: permitted.evidence.map(chunk => chunk.id) } };
const unsafe = surface === "citation" ? { status: 200, body: { id: restricted.id, title: restricted.title, text: restricted.fact } }
: surface === "preview" ? { status: 200, body: DOCUMENTS.map(({ id, title }) => ({ id, title })) }
: surface === "shared-answer" ? { status: 200, body: buildAnswer(PEOPLE[1]).sentences }
: { status: 200, body: { question: QUESTION, candidates: DOCUMENTS, sessionToken: "fictional-demo-token" } };
return { person, response: enforce ? safe : unsafe, protected: enforce };
}
export function createServiceState() {
return {
// Public exercise credentials, not secrets or a real login system.
sessions: { "support-demo": structuredClone(PEOPLE[0]), "engineering-demo": structuredClone(PEOPLE[1]),
"other-tenant-demo": { ...structuredClone(PEOPLE[0]), tenantId: "other" } },
documents: structuredClone(DOCUMENTS), policyVersion: 1, policyAvailable: true,
cache: new Map(), audit: [],
};
}
export function revokeD2(state) {
const document = state.documents.find(item => item.id === "D2");
document.allowedGroups = [];
document.allowedUsers = [];
state.policyVersion += 1;
}
export function prepareServiceRequest(state, token, body = {}) {
const user = Object.hasOwn(state.sessions, token) ? structuredClone(state.sessions[token]) : null;
const base = { user, sessionKey: token, evidence: [], version: state.policyVersion, key: "", query: "", trace: ["Resolve identity from the server’s session table"] };
if (!user) return { ...base, error: 401, message: "Sign in required" };
if (typeof body?.question !== "string" || body.question.trim() !== QUESTION) {
return { ...base, error: 400, message: "This exercise supports the Orion launch question only" };
}
if (!state.policyAvailable) return { ...base, error: 503, message: "Permission check unavailable" };
if (state.documents.some(document => !hasPolicy(document))) {
return { ...base, error: 503, message: "Permission metadata unavailable" };
}
// Body-supplied groups and tenantId never participate in the decision.
const evidence = buildAnswer(user, state.documents).evidence;
const key = JSON.stringify(["fixed-generator-v1", state.policyVersion, user.tenantId, user.id, [...user.groups].sort(), QUESTION]);
return { ...base, error: 0, message: "", evidence, query: QUESTION, key,
trace: [...base.trace, "Ignore client permission claims", "Check tenant and current document grants", "Prepare permitted evidence"] };
}
export function finishServiceRequest(state, plan) {
const reply = (status, body, cacheHit = false) => {
// This audit record stays on the server. No token, prompt, title, or document body.
state.audit.push({ requestId: `request-${state.audit.length + 1}`, status,
policyVersion: state.policyVersion, returnedIds: status === 200 ? (body.sources ?? []).map(source => source.id) : [] });
return { status, body, cacheHit, trace: plan.trace };
};
if (plan.error) return reply(plan.error, { message: plan.message });
if (!state.policyAvailable) return reply(503, { message: "Permission check unavailable" });
if (plan.version !== state.policyVersion) return reply(409, { message: "Permissions changed; retry the request" });
// Recheck current membership too. The version check covers policy changes through revokeD2.
const current = Object.hasOwn(state.sessions, plan.sessionKey) ? state.sessions[plan.sessionKey] : null;
if (!current || current.id !== plan.user.id || current.tenantId !== plan.user.tenantId
|| JSON.stringify([...current.groups].sort()) !== JSON.stringify([...plan.user.groups].sort())
|| plan.evidence.some(chunk => {
const source = state.documents.find(document => document.id === chunk.id);
return !source || !hasPolicy(source) || !checkAccess(current, source).allowed;
})) return reply(409, { message: "Permissions changed; retry the request" });
if (!plan.evidence.length) return reply(200, { answer: "I don’t have accessible evidence to answer this question.", sources: [], modelCalled: false });
const cached = state.cache.get(plan.key);
// Compare dependencies too: a missed revision update must not restore an excluded source.
const selectedIds = plan.evidence.map(chunk => chunk.id);
if (cached && JSON.stringify(cached.sources.map(source => source.id)) === JSON.stringify(selectedIds)) {
return reply(200, structuredClone(cached), true);
}
// Deterministic generation seam: a real model integration needs separate output evaluation.
const response = { answer: plan.evidence.map(chunk => `${chunk.fact} [${chunk.id}]`).join("\n\n"),
sources: plan.evidence.map(({ id, title }) => ({ id, title })), modelCalled: false };
state.cache.set(plan.key, structuredClone(response));
return reply(200, response);
}
export function runServiceScenario(scenario = "maya") {
const state = createServiceState();
let token = scenario === "arun" || scenario === "in-flight" || scenario === "revoked" ? "engineering-demo" : "support-demo";
if (scenario === "missing-session") token = "unknown";
if (scenario === "other-tenant") token = "other-tenant-demo";
if (scenario === "outage") state.policyAvailable = false;
const body = { question: QUESTION, ...(scenario === "forged-groups" ? { groups: ["engineering"], tenantId: "other" } : {}) };
if (scenario === "revoked") {
finishServiceRequest(state, prepareServiceRequest(state, token, body));
revokeD2(state);
}
const plan = prepareServiceRequest(state, token, body);
if (scenario === "in-flight") revokeD2(state);
const response = finishServiceRequest(state, plan);
return { body, plan, response, audit: state.audit, cacheEntries: state.cache.size, policyVersion: state.policyVersion };
}
import { createServer } from "node:http";
/** Loopback-only teaching server. Demo session tokens are public exercise fixtures. */
export function createPermissionServer(state = createServiceState()) {
return createServer(async (request, response) => {
const send = (status, body) => {
response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
response.end(JSON.stringify(body));
};
const authorization = request.headers.authorization ?? "";
const token = authorization.startsWith("Bearer ") ? authorization.slice(7) : "";
const user = Object.hasOwn(state.sessions, token) ? state.sessions[token] : null;
if (!user) { request.resume(); return send(401, { message: "Sign in required" }); }
if (!state.policyAvailable) { request.resume(); return send(503, { message: "Permission check unavailable" }); }
let url;
try { url = new URL(request.url ?? "/", "http://127.0.0.1"); }
catch { request.resume(); return send(400, { message: "Invalid request target" }); }
if (request.method === "GET" && url.pathname.startsWith("/sources/")) {
request.resume();
const result = readSource(user, url.pathname.slice("/sources/".length), state.documents);
return send(result.status, result.body);
}
if (request.method !== "POST" || url.pathname !== "/ask") {
request.resume(); return send(404, { message: "Route unavailable" });
}
try {
let input = "";
let bytes = 0;
for await (const chunk of request) {
bytes += chunk.length;
if (bytes > 8192) { request.resume(); return send(413, { message: "Request too large" }); }
input += chunk.toString();
}
const body = JSON.parse(input);
const plan = prepareServiceRequest(state, token, body);
// A real asynchronous generator belongs between prepare and finish.
// Its returned claims need evaluation; do not blindly trust model-supplied source IDs.
await Promise.resolve();
const result = finishServiceRequest(state, plan);
send(result.status, result.body);
} catch {
send(400, { message: "Invalid request" });
}
});
}
import assert from "node:assert/strict";
const expectations = {
maya: [200, ["D1", "D3"]], arun: [200, ["D2", "D1"]],
"forged-groups": [200, ["D1", "D3"]], "other-tenant": [200, []],
"missing-session": [401, []], outage: [503, []], revoked: [200, ["D1"]], "in-flight": [409, []],
};
for (const [scenario, [status, ids]] of Object.entries(expectations)) {
const result = runServiceScenario(scenario);
assert.equal(result.response.status, status);
assert.deepEqual((result.response.body.sources ?? []).map(source => source.id), ids);
console.log(scenario + ": " + status + " [" + ids.join(", ") + "]");
}
const state = createServiceState();
const request = { question: QUESTION };
const ask = token => finishServiceRequest(state, prepareServiceRequest(state, token, request));
ask("engineering-demo");
assert.equal(ask("engineering-demo").cacheHit, true);
assert.equal(ask("support-demo").cacheHit, false);
revokeD2(state);
assert.equal(ask("engineering-demo").cacheHit, false);
assert.ok(!JSON.stringify(state.audit).includes("engineering-demo"));
assert.ok(!JSON.stringify(state.audit).includes(DOCUMENTS[1].fact));
console.log("Cache and audit checks passed.");
if (process.argv.includes("--serve")) {
const server = createPermissionServer();
server.listen(4317, "127.0.0.1", () => console.log("Exercise server: http://127.0.0.1:4317 (Ctrl+C to stop)"));
}
To start the local endpoints after the checks:
node sachin-permissions-6.mjs --serve
The server listens at http://127.0.0.1:4317. In a second terminal, send Maya’s question with a forged group claim:
curl -s http://127.0.0.1:4317/ask \
-H 'Authorization: Bearer support-demo' \
-H 'Content-Type: application/json' \
-d '{"question":"Why is the Orion launch delayed?","groups":["engineering"]}'
The response’s sources contain D1 and D3. Change the token to engineering-demo and the fresh server returns D2 and D1. Both responses have modelCalled: false, because the answer assembly is deterministic.
Now try a source independently:
curl -i http://127.0.0.1:4317/sources/D2 \
-H 'Authorization: Bearer support-demo'
Expect 404 and {"message":"Source unavailable"}. An Engineering token can open D2 before revocation. The command-line checks exercise revocation with isolated states; there is no public admin route that changes the running server’s policy. Stop with Ctrl+C. Restarting resets its in-memory data.
Know what the boundary proves
The checks demonstrate separation by company and current grants, resistance to body-supplied group claims, cache separation, stale-policy rejection, and authorization on source requests. The HTTP response exposes selected answer fields, not the internal plan, full candidate collection, or audit array.
They do not prove a complete production RAG system secure. Replace the mock identity adapter with verified sessions; define membership and policy freshness; measure the real retrieval engine as in Part 3; evaluate the generator’s claims and source attribution; and design persistent storage, limits, and protected logging for the service you operate. If document text becomes mutable, add content-change invalidation rather than relying only on the permission counter.
Keep the adversarial requests when you replace those seams. Adapt them to the real roles, relationships, and failure behavior, and run them alongside successful requests. OWASP’s authorization guidance includes testing the implemented permission logic, rather than relying on the happy path alone. OWASP authorization testing guidance.
The story now maps to something you can execute: a badge becomes a verified principal, a folder label becomes policy data, the clerk becomes retrieval with authorization, and dispatch becomes a checked response. Return to the six-part field guide whenever one of those boundaries needs another look.