Chat, tools, and approvals

Treat tools as authorized actions

Validate tool inputs, narrow authority, require approval for consequential actions, and make repeated calls safe.

A tool is a function the model can ask to run. The model doesn’t run it. It emits a request, “call getOrder with orderId: 812”, and your server decides what happens next. That distinction is the whole security model of agents, so let’s be precise about it.

A model suggesting a tool call is untrusted input. It is not authorization. The model can be talked into asking for anything by a clever prompt, or by a poisoned document it retrieved. The server defines which tools exist, validates the arguments, checks that this user may perform this action on this resource, and holds the credentials. The model never sees an API key.

Two classes of tools

Read-only tools can run automatically. Searching documents, reading an order the user owns, checking a status. If the model calls them wrong, nothing breaks.

Everything with a side effect is different. Sending email, publishing, deleting, purchasing, changing infrastructure. These should require explicit confirmation from the human and leave an audit record. The Agents SDK supports this pattern: a tool that needs approval pauses the turn, the client shows the user what’s about to happen, and the action runs only after they confirm.

Retries make side effects worse. A resumed or retried turn can call the same tool twice. Give every consequential action an idempotency key, a stable ID for “this specific action”, so the second call finds the first one already done and does nothing.

Redesign a dangerous tool

Take a deleteProject tool as it would be written on a Friday afternoon: the model passes a project ID, the server deletes it. Now threat-model it. The model could invent an ID. It could pass another user’s ID. It could call it twice. The provider could time out after the delete ran, and the retry deletes something else.

Rebuild it in four moves. Preview: the tool returns what would be deleted, not a deletion. Approval: the actual delete requires the user to confirm that preview. Narrow scope: the server only deletes projects the authenticated user owns, whatever ID arrives. Recovery: it’s a soft delete with a restore window, so a wrong confirmation is not the end.

Try this redesign on one tool in your own project. The pattern below is the shape every tool should start from.

Define tools as narrow actions with validated arguments:

const tools = {
  getOrder: async ({ orderId }, user) =>
    orders.findOwned(orderId, user.id)
}

The model may suggest the action, but trusted code authenticates the user and checks ownership. Log the tool name and outcome without storing unnecessary prompt data. Test invented IDs, another user’s ID, repeated calls, and provider timeouts.

Lesson completed