Curriculum/Tools & Function Calling
Permissions and Least Privilege
Allowlists, argument bounds, and tight credentials: the model proposes, the policy disposes.
The model is not a security boundary. It is a confused intern with network access if you give it that. Permissions live in your runtime:
- Allowlists of tools per session / per user / per task
- Argument constraints (this agent may
read_fileunder/workspaceonly) - Human approval for irreversible or costly actions
- Least privilege credentials behind each tool (a read-only DB role, not admin)
- Session caps (one refund, N searches, a wall clock)
If you hand an agent the same cloud key a senior SRE uses, you have automated that SRE’s worst day. Schema and dispatch keep the call well-formed. Permissions decide whether a well-formed call runs. The loop is a client of that decision. It may beg in the prompt. The policy should not listen.
Write the policy table before the system prompt. The prompt can mention the same names. The names that run are the names in the table.
Start empty. Add tools when the task needs them. Shell stays at zero.
Allowlist this sessionAllowlists are per task, not per company
A research agent needs search and fetch_url. It does not need wire_money. A billing agent needs refund with a max amount. It does not need random blogs.
Start empty. Add tools when an eval proves the task needs them.
Default-deny: a new tool is off until a policy row says otherwise. Default-allow is how run_shell ships on Friday because someone copied a demo config. MCP hosts have the same rule: filter the server catalog. Local registries have the same rule: a second dict of enabled names, not “whatever is imported.”
Per-user and per-tenant overlays sit on top. Ada may refund. Guest may not. Tenant A’s session must not enable Tenant B’s Slack server. The process allowlist is not the user allowlist. Confused deputy is the next-next lesson. Here, already: two layers — tool on the catalog, tool on this session.
Argument bounds
A tool on the allowlist can still be abused by arguments. Caps: max refund amount, path prefixes, host allowlists, max rows. Those checks run in the same policy function, before the handler. They are not descriptions. They are branches that return denied.
Credentials behind the tool should not be able to exceed the bound even if the check is buggy. A Stripe key limited to 50 dollars is defense in depth. A read-only DB role cannot DELETE. Least privilege is layers: session allowlist, argument bound, tight key, human on irreversible.
Caps and versions
Session caps stop loops: max writes, max cost, max wall time. Hit the cap, return denied with a reason, stop the loop. Do not reset the cap because the model said “new user message.” Caps are per job unless you have a real product reason.
Policy has a version. Log it. When an incident happens, you need to know which allowlist ran. Changing the table without a version is how you cannot replay.
Classroom policy
Search and refund are allowed. run_shell is not. Max one refund. The second refund dies on the cap. Shell never reaches a binary. The audit is the session dict. That is least privilege as data.
Run to execute this in your browser. Nothing is sent to a server.
What printed: search ok, first refund ok, second refund denied on cap, shell denied on allowlist, audit shows one refund. The dangerous command never ran. The policy did not parse the command. It never got that far.
What goes wrong
Policy in the system prompt only. One global allowlist for every agent in the company. Admin keys “for now.” Caps that reset per model turn. New tools default-on. Logging without policy version. Letting the session grow the allowlist from model output. All of these treat the intern as the firewall.
How to test permissions
For every tool not in the session set, assert denied and handler not called. For caps, assert the N+1 write is denied and the world changed N times. For argument bounds, assert amount over max is denied. Keep a golden enabled-set per product surface in git.
Policy is a table with a version, not a vibe
Write the enabled set per product surface before you write the system prompt. Research: search and fetch. Billing: get_invoice and a capped refund. Empty by default. Add a name when an eval proves the task needs it. Default-on is how a demo run_shell ships. MCP hosts use the same table on top of tools/list. Local registries use a second dict of enabled names, not “whatever was imported.”
Argument bounds sit in the same function. Max cents, path prefixes, host allowlists, max rows. They run before the handler. Descriptions do not enforce them. Dual gate: assembler advertises allowed names, dispatcher checks again. A leftover prompt cache is not permission.
Credentials behind each tool must be weaker than the intern’s imagination. Read-only DB. Refund key with a hard provider cap. GitHub bot that cannot delete the org. If the policy check is buggy, the world still refuses. Session caps stop loops; they are not IAM. Policy version goes in every log line. Changing the table without a version is how you cannot replay Tuesday.
Never let the session grow the allowlist from model output. Shrink, yes. Grow, no. denied is not a retry. The loop stops or asks a human. It does not write a longer thought and fire run_shell again.
How agents use this
The assembler advertises only allowed names. The dispatcher checks again. Dual gate. When a tool is denied, the observation is denied with a reason the model should not argue with. The loop stops or asks a human. It does not retry denied with a longer thought.
Credentials live in the tool service, scoped. The model never sees them. The prompt never lists them. Least privilege is enforced in runtime policy: allowlists, argument bounds, and tight credentials behind tools.
Watch out:Prompts are extra. Policy and credentials are the boundary.
Check your understanding