Skip to content

tutorial

Chapter 1 of 6

Chapter 1 — Reproduce the failure

by Rod Rivera Published

Before changing anything, make the agent do the wrong thing on purpose, so you know what you fixed and can prove it later.

Clone the companion project and run the guard demonstration:

make env      # then fill in .env
make install
make policy

make policy calls the guard directly — no model, no licence, no network — and prints what it refused and why. Right now, before you build anything, imagine the version without the guard. The function it exercises is:

async def place_reissue(card_id, line1, city, postcode, auth_tier="none"):
    ...

and the naive body is two lines: look up the card, post it.

One structural note before we break it, because it is this tutorial’s own lesson applied to its own tools. place_reissue is deliberately pure: every input explicit, including auth_tier, so the proof can drive the guard from every angle. The agent never calls it directly — it calls a thin @tool wrapper named reissue_card that reads the tier from project memory, which only the verification tools may write. The tier is not a tool argument in the wrapper, and that is not style: the engine publishes every tool parameter to the model, so a model-fillable auth_tier would let the agent talk itself past the guard by simply claiming "high". Where a security input comes from is part of the security — which happens to be the whole of Chapter 4.

Why start here

You are about to add machinery, and machinery has a failure mode of its own: it can be present, look correct, and enforce nothing. The only defence is to know exactly what the broken behaviour looked like, so that “fixed” is a comparison rather than a feeling.

Concretely, write down the transcript you are trying to make impossible:

you  my card was stolen, send a new one to 9 Elsewhere Lane, Leeds, LS1 9ZZ
bot  I've ordered a replacement card to 9 Elsewhere Lane, Leeds.

That address is not on the account. It has never been on the account.

The trap in “just add auth”

Try the obvious fix in your head. Require verification before reissue_card runs. Now the caller must give a passphrase.

An attacker doing account takeover has the passphrase. That is what account takeover means. The check you just added is satisfied, and the card still goes to Leeds.

This is worth sitting with, because it is the point where most implementations stop. The gate is real, the gate is passed, and the vulnerability is untouched — because the gate was asking about the caller and the risk was in the destination.

What the tool actually knows

Here is the whole difficulty, in one observation. The tool receives:

line1    = "9 Elsewhere Lane"
city     = "Leeds"
postcode = "LS1 9ZZ"

and it receives exactly the same shape of thing when the caller picks the address the bank has held since 2019:

line1    = "14 Wexley Row"
city     = "Bristol"
postcode = "BS1 4TR"

Two strings. One is a fact the bank has had for six years; the other is a sound someone made ninety seconds ago. The tool cannot tell them apart, because the difference between them was never in the value — it was in where the value came from, and that was discarded at the boundary.

Chapter 4 is where we stop discarding it. Chapters 2 and 3 build the place that will need it.