The capstone is a complete local application with two tools, a bounded loop and a deterministic model adapter. It searches permitted notes, reads one selected record and produces an inspectable JSON transcript. All source, fixtures, tests and command-line instructions are included in the free download. No API key, model subscription or external service is needed.
The adapter is deliberately rule-based. It requests a search, chooses the first returned ID, reads that note and formats the observed record as its final text. That lets you debug protocol and policy mechanics without confusing them with a real model’s quality. The same application boundaries can later sit behind an adapter to an actual LLM.
Validate and copy the notes before calling the model, and freeze the readable IDs. A callback that mutates the caller’s original list should not change the snapshot or permissions in the middle of the run. Closures over copied data make this ownership contract straightforward to test.
Apply permissions in both tools. A read request for a known private ID is denied before returning the body. Search iterates only the permitted IDs before testing the query, so private titles and match information never enter its results. Filtering the final answer after exposing a private tool result would be too late.
notes = {'public': {'title': 'A'}, 'private': {'title': 'B'}}
readable = frozenset({'public'})
print([{'id': key, 'title': notes[key]['title']} for key in sorted(readable)])This search tool performs a case-folded literal substring check over title, newline and body. It is intentionally different from the ranked lexical engine in Search your own documents. That course is a useful next local extension: if you replace the matcher with ranked retrieval, filter to the authorized corpus first and retain all permission tests.
The downloadable CLI also accepts an explicit replay fixture. Use it to demonstrate a forbidden read, an unknown tool, a repeated call ID or an exhausted budget, then inspect the actual transcript and counters. A final text message is not proof of success by itself. Test task outcome and data exposure separately from the existence of a final response.
allowed = ['a']
snapshot = frozenset(allowed)
allowed.append('private')
print(sorted(snapshot), sorted(allowed))Run the complete unit suite, the ordinary mock-model command and the denied-read replay from the README. Add one new failure fixture and assert its effect, transcript and stop reason. The result is a useful foundation for a real-model continuation: preserve the registry, object permissions, budgets and evidence when the decisions become nondeterministic.
Download the complete lab. The archive includes runnable code, tests, fixtures and a README.