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

  1. 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.
  2. 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.
  3. Concept and live implementation (30m). tools.py line by line: the clamped search_documents, the get_document_metadata error that names the valid ids, the read-only summarize_document. Then the two boundaries in the notebook: max_results=999 clamped, an unknown doc_id refused.
  4. Guided lab (35m). notebook.ipynb. list_documents to a four-clause contract, then convert_currency validated before it fetches, judged against the check's offline rate table.
  5. Failure injection (15m). Point allowed_url at file:///etc/passwd and at 169.254.169.254, and read the refusals. Then exercise 3: run guard_tool_output over injected and benign text, and find one ordinary sentence your guard flags by mistake.
  6. Evaluation and artifact receipt (10m). review("ch04") in Jupyter, or uv run bootcamp check ch04 in the terminal. Share the injection shape somebody's guard missed.
  7. 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