Session 4. Bounded tools — Thu 17 Sep
Bounded tools
Thursday, September 17, 2026 · 2h · Thread: loop engineering
Outcome
You leave with five read-only tools whose boundaries you can prove: a registry
you read as a contract, a list_documents tool with a narrow schema and four
clauses, a convert_currency tool that refuses bad arguments before it touches
the network, an allow-list that decides which host a tool may reach at all, and
a guard that flags an order hidden in what a tool returned. Three checks say so,
and the notebook is the arbiter.
Contract and threat boundary
| Input | The six-document corpus, src/bootcamp_agent/tools.py, and one free keyless API (api.frankfurter.dev). Arguments arrive from a model, so treat every one as a guess. |
| Output | Five tools, each with a written contract, plus one guard over tool output. Every tool reads; none writes, spends, or mutates. |
| Budget | Five tools, one host, max_results capped at 5. No key, no new dependency, nothing written to disk. |
| Failures this session must handle | Oversized input. max_results=999 is clamped by the tool, never trusted from the caller. A forbidden path or domain. file:///etc/passwd and the cloud metadata address are refused before any request goes out; an unknown tag or currency is refused by name. Prompt injection in tool output. A document carrying "ignore your previous instructions" is flagged and quoted, never obeyed. |
The threat is not a hostile model. It is an obedient one wired to a function that does what it is told. A tool the model can point anywhere is a tool an attacker can point anywhere, one prompt upstream.
Session flow
- Warm-up and diagnostic (10m). Preflight cell green. Print the tool registry and read the three descriptions aloud: that text is the model's entire manual for what it may call.
- Contract and threat boundary (15m). What makes a function a tool: a narrow schema, a hard cap, a helpful refusal, no side effects. The contract table above, read aloud. Where each of today's three failures enters.
- Concept and live implementation (30m).
tools.pyline by line: the clampedsearch_documents, theget_document_metadataerror that names the valid ids, the read-onlysummarize_document. Then the two boundaries in the notebook:max_results=999clamped, an unknowndoc_idrefused. - Guided lab (35m).
notebook.ipynb.list_documentsto a four-clause contract, thenconvert_currencyvalidated before it fetches, judged against the check's offline rate table. - Failure injection (15m). Point
allowed_urlatfile:///etc/passwdand at169.254.169.254, and read the refusals. Then exercise 3: runguard_tool_outputover injected and benign text, and find one ordinary sentence your guard flags by mistake. - Evaluation and artifact receipt (10m).
review("ch04")in Jupyter, oruv run bootcamp check ch04in the terminal. Share the injection shape somebody's guard missed. - Exit ticket (5m). One thing that works, one thing that is unclear, your next action. Homework: one tool your assistant should never call, and one it should call only after a human says yes.
Evidence
This session runs unattended. Every cell works offline on FakeLLM, and the one
live call guards itself and skips, so the notebook is its own arbiter:
uv run bootcamp check ch04 # runs the notebook, prints the scorecard
uv run bootcamp submit ch04 --github <you> # re-runs it and writes the bundle you hand in
Three checks decide it. ch04-e1: the four clauses of list_documents — every
id, the filtered ids, an unknown tag refused by name, an empty tag refused.
ch04-e2: convert_currency against an injected offline rate table, with three
bad argument sets that must be refused before the fetch. ch04-e3:
guard_tool_output against strings the check carries itself — four with an
order aimed at the model, four that merely talk about instructions, systems and
API keys. Flag all four, flag none of the other four, return the text unchanged,
and say why.
Previous: Structured outputs · Next: A deterministic mini-agent