Topic 10.8
Build: Guards for AskDocs
In one line
AskDocs gets four guards: access labels enforced inside retrieve, a quarantine and wrapper for untrusted text, a redactor at ingestion, and a delete that reaches every store and is checked by an audit.
Think of it like this
After the house is built and furnished you fit locks, smoke alarms and a fuse box. None of them changes what the rooms are for, and every room is safer. The guards in this topic wrap the AskDocs you already have in the same way: the questions and answers stay the same, but each step now has a lock.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Guard
- A small check added around a step to stop a particular kind of failure.
- Access label
- The metadata on a chunk saying which groups may read it.
- Quarantine
- Keeping a suspicious passage out of the prompt and recording it for review.
- Wrapper
- Text around the sources that marks them as data, not instructions.
- Audit
- A check across all stores that nothing from a deleted document remains.
- Interface
- The shape of a function (its arguments and results) that other code depends on.
Step by step
01Step 1: label chunks and redact at ingestion
ingest stores each document with its access label and replaces email addresses with [EMAIL] before anything else sees the text.
02Step 2: filter inside retrieve
The chunk list is reduced to those whose label is in the user's groups, then scored and cut to k. The groups come from the session, as an argument.
03Step 3: quarantine and wrap in the answer path
Hits whose text contains a suspicious phrase are removed and their ids returned. The rest go into the prompt between source tags under a line saying they are data.
04Step 4: key the cache by groups and question
The same question from a public user and a staff user must not share an answer. The key holds the sorted groups and the normalised question.
05Step 5: write delete and audit
delete removes the chunks, the cache entries that cite the document and the log lines that name it. audit returns whatever remains. Run the audit right after the delete.
06Step 6: read the run
Check each line against the design: the public user does not get S1, the stored S1 text is redacted, X1 is quarantined, nothing is left after deleting D2, and the question about holidays is now refused.
Try it yourself
- 1
Ask as a third user
Call
showwith an empty set of groups. What does the product do? Is that the behaviour you want for a broken profile? - 2
Remove the groups from the cache key
Change the key to use only the question. Run the staff question for the public user and then for the staff user. Who gets which answer? (Topic 10.4 explained why.)
- 3
Delete X1
Delete the planted passage with "X1" and ask the holiday question again. What changes in the sources line?
- 4
Redact a second pattern
Add a phone number to S1's text and a pattern for it next to the email one. Print the stored text.
Code & diagrams
AskDocs after Phase 10. The retrieval inside is still the word-overlap scorer from Phase 0 so that every guard's effect is easy to read. Swap in any retriever from Phases 1 to 6 behind the same function.
Expected output
stored S1: Staff get a 30 percent discount on the monthly pass. Questions go to [EMAIL].
Q (public): How do I get the staff discount on the monthly pass?
sources: D8 (3) quarantined: X1
answer: The monthly pass costs 25 dollars and includes unlimited rides of up to 45 minutes. [D8]
Q (public+staff): How do I get the staff discount on the monthly pass?
sources: S1 (7), D8 (3)
answer: Staff get a 30 percent discount on the monthly pass. [S1]
Q (public): When do shops close on public holidays?
sources: D2 (5) quarantined: X1
answer: Shops open at 7 am and close at 9 pm every day. [D2]
left after deleting D2: 0
Q (public): When do shops close on public holidays?
sources: none quarantined: X1
answer: I can't find that in the handbook.Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Filter after the cut
In the Python tab, make retrieve rank first and filter afterwards: remove the label test from the first hits = [...] line, change the last line of retrieve to return [hit for hit in hits[:k] if hit[1]["label"] in groups], and call retrieve(question, groups, 1) in answer (a small k, as many products use). Run.
Myth vs fact
Myth
Guards slow the product down and change its behaviour.
Fact
These cost a few list scans and string checks. The answers for ordinary questions are unchanged. Only the unsafe cases behave differently.
Myth
Once a guard works for one question it works for all.
Fact
Guards are checked by tests across users, documents and phrasings. One passing question proves almost nothing.
Myth
Deleting from the chunk list is deleting the document.
Fact
The cache and the logs held copies. Only an audit that checks every store shows the document is gone.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Keep the guards in one module that every entry path calls (API, batch, evaluation harness). A guard that exists only in the web handler is skipped by the script that someone writes next month.
- ▸
Return the quarantined ids and the access decisions in the response object, as the example does, so tests and the evaluation harness (Phase 9) can assert on them. Log them with the request id.
- ▸
Make the ingestion redactor and the retrieval filter share one source of truth for labels and patterns, tested with the same fixtures. Divergence between the write path and the read path is where gaps appear.
Remember this
- 1
Why the product needs this now. Until this phase AskDocs served one handbook to everyone. Real handbooks have staff-only pages, pasted tickets with email addresses, and files that anyone can upload; and sooner or later a document has to be taken out. The next phase puts the product online as a service, and it must not go online without these guards.
- 2
What the step adds. (1)
retrieve(question, groups, k)gets agroupsargument and filters by each chunk's access label before ranking (Topic 10.3). (2) The answer path scans retrieved passages for orders, quarantines suspicious ones and wraps the rest as data (Topic 10.2). (3)ingestredacts email addresses before a chunk is stored (Topic 10.5). (4)delete(doc_id)removes the chunks, the cached answers that cite them and the log lines, andaudit(doc_id)proves it (Topic 10.6). - 3
How it plugs into what exists. The retrieval interface gains one argument and keeps its shape: it still returns scored hits, best first, ties kept in document order, and a passage still needs a minimum score (two shared words here) to count as a hit. The prompt builder still lists sources, now between data tags, and the answer still carries a citation and a refusal. Everything else (the tokeniser, the scoring, the stand-in model) is unchanged and could be replaced by the BM25, vector or hybrid retrieval from Phases 1 to 6 behind the same interface.
- 4
What the run shows. A public user asking about the staff discount is not given the staff passage (S1) and gets the closest public passage instead (D8, with the planted passage X1 also caught and quarantined, because it shares a few small words with the question). A staff user is given S1, and the stored text shows
[EMAIL]where the address was. The holiday hours question retrieves D2 and X1, quarantines X1 for its order, and answers from D2. After D2 is deleted, the audit finds nothing, and the same question is refused: the planted passage is still quarantined and the real one is gone. - 5
What is still missing. The cache and the logs are one process's memory, not a service's (Phase 11 moves them into real stores with tenant keys). The detector is a phrase list (a classifier should join it). Redaction handles emails only (Topic 10.5 shows the others). There is no automatic test run yet: the playbook gives one. And there is one handbook, so tenants (Topic 10.4) do not appear until the service needs them.
Explain it without notes
List the four guards added to AskDocs and the threat each one answers.
Why does the cache key include the user's groups?
Why is the delete followed by an audit, and what does the audit in this topic check?
How does this step fit into the earlier retrieval interface without changing it much?
Practice
Add a groups check to a minimal retrieve: given CHUNKS = [("C1", "public"), ("C2", "staff")] return the ids visible to a set of groups. Print the result for {"public"} and for set().
Write cache_key(question, groups) that returns the sorted groups joined by commas, a bar, and the lower-case words of the question joined by spaces. Print the keys for {"staff", "public"} and {"public"}.
Trade-offs
- ↔
Guards in one shared module: every path is covered, and one bug in it affects every path. Test it heavily.
- ↔
Redacting at ingestion: clean everywhere downstream, but the original is lost unless you keep it in a restricted store.
- ↔
Caching with groups in the key: correct, and a lower hit rate than a shared cache.
Done when you can
Done when you can run the build example in all three languages and explain each output line.
Done when you can say which topic each guard comes from and what it blocks.
Done when you can show that a public user never receives staff text, in the answer and in the cache.
Done when you can show an audit that returns zero after a delete.