A customer asks the banking assistant to order a replacement card. You need to confirm the order they intend to place and check that the requested delivery address is permitted. These are two separate decisions.
In this chapter, you will put confirmation in Mantle’s tool constraint and keep the address policy inside the ordering tool. The configuration below asks the engine to pause the pending tool call for confirmation:
tool_constraints:
- reissue_card:
requires_confirmation:
enabled: true
utter_for_confirmation: utter_confirm_reissue
Confirmation pauses the call
requires_confirmation is an execution gate, not merely a suggestion about which tools to offer. Mantle holds the pending call while it asks for confirmation; the native confirmation tests distinguish a later confirmation from an answer supplied in the original request.
That gate does not establish that an address is allowed by the bank. The tool still needs the customer’s verified tier and the address’s source. It also needs to enforce those checks if another caller invokes the underlying Python function directly, outside Mantle’s confirmation path.
Keep the address check inside the ordering tool
| Check | Where it runs | What it establishes in this example |
|---|---|---|
requires_confirmation | Mantle’s pending-call gate | Whether the pending call may proceed after the confirmation exchange |
guard_reissue() | Inside the Python ordering function | Whether the address and verification satisfy the fixture policy |
The checks complement one another. A confirmation does not make an address permitted; an address-policy pass does not establish that the customer confirmed the order.
The inner one looks like this, in tools/cards.py:
address = classify_address(line1, city, postcode, customer["addresses_on_file"])
# ---- the line before the side effect --------------------------------
try:
guard_reissue(address, auth_tier, on_file_since=on_file_since)
except ReissueRefused as exc:
return _as_dict(exc.outcome)
# ---------------------------------------------------------------------
reference = f"RC-{uuid.uuid4().hex[:8].upper()}"
Every function in that module that changes the world has the same shape: resolve inputs, classify provenance, guard, act, return an outcome. If you copy one thing from this tutorial, copy that ordering.
Why it raises instead of returning False
def guard_reissue(...) -> Decision:
decision = evaluate(...)
if decision.allowed:
return decision
raise ReissueRefused(...)
A boolean can be ignored by writing guard_reissue(...) on its own line and
carrying on — which reads, at a glance, exactly like calling a guard. Reviewers
skim past it. An exception cannot be ignored that way: the only route to the
line that posts the card is a return.
The test this makes possible
You can exercise the address-policy function directly without a model:
make policy
An address supplied during the call:
✓ medium is NOT enough — this is the account-takeover path
-> ok=False result=step_up_required
No LLM, no sampling, no judge. This is a fact about the process.
This exercise checks the fixture’s address policy. It does not verify caller identity or replace a conversation test. Check Mantle’s confirmation exchange separately, then inspect the service result before telling the customer that the order was placed.