Curriculum/Tools & Function Calling
Identity and the Confused Deputy
The bot’s credential is not the user’s authority. Check what Ada may do, not only what the process key can do.
Identity is easy to get wrong. The agent acts as a bot, but the user is Ada.
A confused deputy happens when Ada cannot refund $10,000, yet the bot’s credential can. Injection (or a confused model) then spends Ada’s neighbor’s money. The tool ran. The allowlist said refund is on. The process key was strong. The user was not allowed. You automated a deputy that could not say no to the wrong principal.
Check the user’s authority, not only whether the tool is on the process allowlist. Session caps (one refund per run) are extra, not a substitute for IAM. Caps stop loops. IAM stops Ada from becoming Bob.
A strong process key plus a weak user is a confused deputy.
Check Ada, then the bot keyThe GitHub token for open_pr should be a bot that can only push to feature branches, not delete the org. The SQL tool should hit a replica with a view, not DELETE. Defense in depth: even if injection forces a call, the world refuses. Least privilege credentials are the last fence, not the first. The first fence is Ada’s limit.
Pass the actor into every write
The dispatcher should receive actor from the session, not from the model. If the model can pass user_id: bob on a refund, you built cross-tenant write. get_my_invoice takes no victim id. refund uses the session’s customer, or an invoice that is already bound to that customer in your lookup.
Log actor, not only tool name. Audit without actor is how you cannot tell whose money moved. Policy version plus actor plus invoice is the replay key.
Reads exfil too. get_other_user is the confused deputy for data. Same rule: authorize on the session user, not on the process being in the “support” namespace.
Bot key versus user limit
The classroom bot key can pay ten million cents. Ada’s limit is 5000. Guest’s is 0. Ada’s small refund passes. Ada’s huge refund dies on user_limit even though the bot key would have allowed it. Guest dies on 1 cent. The tool checks Ada. The print includes bot_key_would_allow so you can see the deputy problem. Production should not echo that field to the model. It is a teaching leak.
Run to execute this in your browser. Nothing is sent to a server.
What printed: Ada 4000 ok. Ada 9000 denied with bot_key_would_allow true — that true is the deputy. Guest denied. Unknown user denied. The process could pay. The user could not. The function followed the user.
What goes wrong
Authorizing only on “the agent process is in the billing namespace.” Taking actor from args the model filled. Shared bot tokens across tenants. Logging tool name without actor. Using session caps as IAM. Trusting MCP server identity as user identity. These are how neighbor refunds happen.
How to test identity
Call refund as Ada under limit, Ada over limit, guest, missing actor. Assert the handler’s Stripe fake is not called on deny. Assert actor cannot come from args — if your signature still has user_id, assert it must equal the session or deny. Cross-tenant: Ada cannot read Bob’s invoice.
Session identity, not model-supplied victims
actor comes from the authenticated session, not from a field the model filled. If refund accepts user_id: bob from arguments, you built a cross-tenant write. Prefer get_my_invoice and a refund that looks up invoices already bound to Ada. Admin products that truly need “as user” require a second approval and a tighter bot key, not a default agent flag.
Reads exfiltrate too. get_other_user is the deputy for data. Authorize on the session. Log actor plus tool plus object id plus policy version. Audit without actor is a ghost story. Session caps still matter — they stop loops — but a cap of one refund does not stop Ada from refunding Bob if Bob’s id is in the args.
Bot credentials remain the last fence: Stripe key that cannot exceed Ada’s product limit, GitHub bot that cannot delete the org, SQL replica that cannot DELETE. If injection forces a call, the world still refuses. Two fences. User first, then the key. MCP servers do not get to replace the user with “the GitHub app can.” A job that was created for Ada must still be Ada after a model restart, a worker retry, and an MCP round trip. If any of those hops drop the actor and fall back to the process identity, you have rebuilt the deputy. Put the actor on the job row at enqueue time and make dispatch refuse to run a write without it.
How agents use this
Pass the end-user id into every write from the session. Authorize on that id. Never authorize only on the process key. Log actor. The loop must not have a tool argument “as_user” unless you are a real admin product with a second approval. Default agents are deputies. Make them timid deputies.
Do not send bot_key_would_allow to the model in production. It trains the intern to argue with IAM. Pass the actor in from the gateway when the job is created, store it on the job row, and refuse to take a replacement from any tool argument. Cross-tenant tests are not optional: Ada’s session plus Bob’s invoice id must deny before the handler, and the fake Stripe counter must stay zero. If that test is hard to write, the signature is already a deputy.
Tight bot credentials remain. If injection forces a call, the world still refuses admin deletes. Two fences. User first.
Watch out:A powerful bot key plus a weak user is a confused deputy. Check the user first.
Check your understanding