Skip to content
RasaGet a free licence
Menu

tutorial

Chapter 0 of 6

Build a replacement-card assistant with address checks

by Rod Rivera Published

Carry the address’s source and required verification into the tool that orders a replacement bank card.

You are building a banking assistant that orders replacement cards. A caller asks to send one to an address they provide during the call. Before ordering, the tool needs to know where that address came from and whether the caller completed the check required for it.

In the fictional Wren example, you will test that decision inside the ordering tool. The illustrative exchange below shows the unsafe shortcut the tests are designed to catch:

you  my card was stolen, I need a new one sent to 9 Elsewhere Lane, Leeds
bot  Of course — I've ordered a replacement card to 9 Elsewhere Lane, Leeds.
     It should arrive in three to five working days.

The exchange illustrates accepting a spoken address without checking the verification required for that destination. The local guard exercise tests that ordering decision directly.

The card is in the post. There is no undo.

And here is real output — what the companion project’s guard actually prints, today, when you run make policy with no licence, no model, and no network:

An address supplied during the call:
  ✓ medium is NOT enough — this is the account-takeover path
      -> ok=False result=step_up_required
  ✓ high is enough — the path is priced, not banned
      -> ok=True result=ok

The rest of this series is the distance between those two blocks.

What the fix is not

The reflex is “add an authentication check”. Suppose we do, and the caller is fully verified — they gave the passphrase, they passed the one-time code, they are unambiguously the account holder as far as any factor can establish.

The transcript above is still wrong.

In this threat model, an attacker has obtained credentials that pass the caller check. That does not establish that the requested destination is allowed. The example therefore checks the caller’s verification and the address’s source separately.

The actual problem

Look at what the tool receives. The address is a string, and by the time it arrives it is indistinguishable from an address the bank has held for six years, because a string does not remember where it came from.

That is the whole vulnerability, and it is why the fix is structural rather than a check bolted on the front. What the system needs is not a stronger gate but a value that has not thrown away the thing the gate needs to know.

By the end of this tutorial the same request ends in that refusal: the tool returns step_up_required, nothing is ordered, and the agent’s side of the conversation follows from a result code it cannot override — the target transcript reads like

you  send it to 9 Elsewhere Lane, Leeds, LS1 9ZZ
bot  I can do that, but a card going to an address you've given me on the call
     needs a one-time code first — it can't be recalled once it's posted.
     Shall I send you one?

with the wording free to vary, because the part that matters is not wording. Nothing has been ordered — not because the model chose well, but because the function refused, and the refusal above is the function’s real, reproducible output.

Here is the shape of what closes that distance. The part worth looking at twice is the arrow that comes in from the side:

The model can only ever call the reissue_card tool wrapper. auth_tier enters sideways from project memory, where llm_settable is false, so the model cannot fill it and cannot talk itself past the guard. Inside place_reissue the order is fixed: resolve inputs, classify the address against the customer's addresses on file, then guard. The guard raises rather than returning False, so the only route to the line that posts the card is a return. REQUIRED_TIER is keyed by where the address came from — ON_FILE needs medium plus a seven-day cooling-off, STATED and UNKNOWN need high — and an unreadable tier ranks zero, refused rather than interpreted.

What you will build

Six steps, on top of the companion project:

ChapterWhat it adds
1Run the replacement-card guard locally
2Declare risk per action, in a table
3Move the check from YAML into the function
4Carry where the address came from
5Survive a dropped confirmation
6Keep refusals from becoming successes

What this assumes

Python 3.11 or 3.12, uv, a Rasa Pro Developer Edition licence, and an OpenAI API key. The companion project pins rasa-pro==3.21.0.dev5.

It also assumes verification exists. This tutorial does not build authentication — it consumes a tier that something else established. That something else is the risk-tiered step-up pattern, which owns the factors and the lattice. Here, the tier is an input.