AgenticGENERALFRAMEWORK-SPECIFIC

A Tool Call Is a Backend Call

The model choosing a tool is a client choosing an endpoint — and the server owes exactly what it always owed.

What actually happensHow to build it

The requirement, the obvious build, and why it breaks

Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind has a problem.

The question

If the model selects the tool and fills in the arguments, what is still the backend's responsibility?

The requirement

The assistant needs to look up orders, update addresses and issue refunds. Someone has written three tool functions and wired them to the model.

The obvious build

The tools are internal functions called by our own agent runtime, so they can take the arguments they are given and do the work. The prompt tells the model to only refund orders belonging to the current user.

Why it breaks

The prompt is a request, not a control. A model can be persuaded — by a user message, a support ticket it read, or a product description in the database — to call the refund tool with a different order id (Indirect Prompt Injection in the Agentic AI domain).

How it breaks in production
  • The prompt is a request, not a control. A model can be persuaded — by a user message, a support ticket it read, or a product description in the database — to call the refund tool with a different order id (Indirect Prompt Injection in the Agentic AI domain).
  • refund(orderId) with no ownership check is a broken-object-level-authorization vulnerability whether the caller is a browser or a model (Object-Level Authorization).
  • Arguments arrive as free-form values from a text generator: an id that is not a UUID, an amount that is negative, a date in the wrong century, a string where an enum was expected.
  • The model calls the same tool twice because the first result did not look like what it expected, and the customer is refunded twice (Idempotency in Backends).
  • A tool that takes a filter or a fragment of query text becomes an injection vector into whatever it interpolates (SQL Injection).
  • A tool with no rate limit gets called in a loop, and one user's conversation generates thousands of calls against an internal service (Rate Limiting).
RequirementAPI ContractApplication LogicData AccessExternal DepsConcurrencyFailureSecurityObservabilityDeploymentScale

What is actually happening

  • The tool schema is an API contract and the model is a client. Everything the domain says about untrusted clients applies unchanged: the contract describes what a well-behaved caller sends, and the server assumes nothing (The Trust Boundary).
  • This client is unusual in one respect that makes it *less* trustworthy, not more: its behaviour is influenced by text it reads at runtime, some of which an attacker may have written.
  • Tool selection is therefore routing, not authorization. Choosing an endpoint has never granted permission to call it, and nothing about the model changes that (Authentication vs Authorization).
  • Identity does not come from the model. It comes from the authenticated request that started the agent run, and it must be carried to the tool handler out-of-band — never as a model-supplied argument (Request Context Propagation).
  • Validation belongs in the handler even though the schema is also given to the model. The schema improves the odds of well-formed calls; it enforces nothing (Transport Validation).
  • The tool result flows back into the prompt, so the handler's output is also a trust boundary: it becomes input to the next model turn (Not Leaking Your Internals).

The same handler, written twice

LANGUAGE-SPECIFICTypeScript with a parse-style validator; the shape is identical in Python with a typed model or in Go with explicit decoding. What must not change is that identity comes from the run context and validation rejects rather than coerces.

The clearest way to see the point is a tool handler written the way it usually is first, and then the way it would be written if the caller were a browser. The second version is not more sophisticated. It is the ordinary endpoint the team already knows how to write.

A refund tool
Trusts the model
// tool: refund_order
// "the prompt says to only refund the current user's orders"
async function refundOrder(args: {
  userId: string      // <- supplied by the model
  orderId: string
  amountCents: number
}) {
  const order = await db.orders.findById(args.orderId)
  await payments.refund(order.chargeId, args.amountCents)
  return { ok: true }
}
Treats the model as a client
// identity comes from the run context, not the arguments
async function refundOrder(ctx: RunContext, raw: unknown) {
  const args = RefundArgs.parse(raw)          // reject, do not coerce

  const order = await db.orders.findById(args.orderId)
  if (!order) return toolError('not_found')

  // object-level authorization, every call
  if (order.userId !== ctx.userId) {
    metrics.inc('tool.authz_denied', { tool: 'refund_order' })
    return toolError('not_found')            // do not confirm existence
  }
  if (!policy.refundAllowed(order, ctx.actor)) return toolError('not_allowed')
  if (args.amountCents > order.refundableCents) return toolError('amount_too_large')

  // repeated selection of the same tool must not refund twice
  const key = `refund:${order.id}:${args.amountCents}`
  const res = await payments.refund(order.chargeId, args.amountCents, {
    idempotencyKey: key,
  })
  await audit.record(ctx, 'refund_order', args, res)
  return { ok: true, refundId: res.id }
}

The second version does not trust the caller for identity, existence, permission, amount or repetition — the five things a browser client is never trusted for either. The first version is a broken-object-level-authorization vulnerability that also double-refunds, and no amount of prompt engineering fixes either property.

The dispatch pipeline every tool call should pass through

Written once as middleware, this is the same pipeline an HTTP request goes through — and for the same reasons. Writing it per handler is how one tool ends up missing the ownership check.

From model output to side effect
  1. 1
    Parse the tool request

    Turn the model's output into a tool name and an argument object

    fails by Malformed or hallucinated tool name; must be rejected, not guessed at

  2. 2
    Resolve identity from context

    Attach the authenticated user and tenant from the run, never from arguments

    fails by Identity accepted as an argument — the single most damaging mistake here

  3. 3
    Check the tool is permitted

    Is this tool available to this user, tenant and plan at all?

    fails by Tool list built once at startup and assumed universal (Role-Based Access Control)

  4. 4
    Validate arguments

    Schema parse into a domain type; reject on failure

    fails by Coercion turning invalid values into plausible ones (Parse, Do Not Validate)

  5. 5
    Authorize the object

    Load the target and check this user may act on it

    fails by Checked in the runtime rather than the handler, so other callers bypass it

  6. 6
    Apply limits

    Per-user and per-conversation rate limit; cost and step budget

    fails by Global limits only, so one conversation can consume the shared allowance (Budgets, Deadlines and Step Limits)

  7. 7
    Execute with a timeout

    Run the handler under a deadline and an idempotency key

    fails by A hanging tool hanging the entire agent run (Timeouts)

  8. 8
    Sanitise the result

    Return a typed result; strip internals and other tenants' data

    fails by Raw errors and internal fields entering the model context (Not Leaking Your Internals)

  9. 9
    Record it

    Audit entry: who, which tool, which arguments, what changed

    fails by Logged only on success, so denied and failed calls are invisible (Agent Audit Logs)

Choosing the tool surface

How much a tool can do is a security decision made at design time, and it is far more effective than anything downstream. The narrower the tool, the smaller the space of harmful calls — and the less the model can accomplish, which is the honest trade.

How wide should this tool be?

What should the model be able to express through this tool?

Named action, typed arguments

when The action is known: refundOrder(orderId, amountCents, reason).

cost A new tool per capability; the tool list grows and the prompt gets longer.

Parameterised query with a fixed shape

when Search or filtering over one entity with an allowlist of fields and operators.

cost Less expressive than the model would like; more schema to maintain.

Free-form query language

when Almost never on user-facing paths. Internal analytics with a read-only, row-limited, tenant-scoped role at most.

cost The blast radius of the credential it runs under, plus injection surface (SQL Injection).

Generic HTTP fetch

when Only with a host allowlist, no redirects, and no internal network reachability.

cost SSRF surface directly proportional to how permissive the allowlist is (SSRF — When the Backend Fetches a URL).

Action requiring approval

when Irreversible, high-value or hard to reconcile: payouts, deletions, external notifications.

cost A human in the path and a slower experience — the only reliable control for irreversibility.

How to build it

Most important first.

  • Derive identity from the session, not the arguments. The handler signature should make userId impossible for the model to supply.
  • Authorize every object, every call. refund(orderId) must load the order and check ownership, exactly as the HTTP endpoint would (Object-Level Authorization).
  • Validate arguments against a schema in the handler, and reject rather than coerce. Parse into a domain type so invalid values cannot reach the logic (Parse, Do Not Validate).
  • Make write tools idempotent with a key derived from the intent, so a repeated tool call is a no-op rather than a second refund (Idempotency Keys).
  • Rate-limit and budget per user and per conversation, not just globally (Rate Limiting).
  • Give each tool a timeout and treat it as a dependency, because a hanging tool hangs the whole agent run (Timeouts).
  • Design tool surfaces narrowly. refundOrder(orderId, reason) is safer than executeSql(query) for exactly the reasons a narrow endpoint is safer than a generic one (What a Handler Is Responsible For).
  • Require human approval for irreversible or high-value actions rather than trying to express the policy in the prompt (Approval Gates and Risk Classes in the Agentic AI domain).
  • Sanitise what the tool returns. Internal errors, stack traces and other users' data must not enter the context (Error Boundaries: Three Translations, Not One).

What can go wrong

Failure modes
  • Authorization implemented once in the agent runtime rather than in each handler, so a tool called from anywhere else — a test, a job, a second agent — bypasses it.
  • A schema-validation library used to *coerce* rather than reject, silently turning "-50" into -50.
  • Idempotency keys generated by the model, which means they are not stable across attempts and provide no protection.
  • A "read-only" tool that is not: a search tool that writes an audit row, or a lookup that triggers a cache refill with side effects.
  • Tool errors returned as raw exception text, leaking internal structure into the model context and often into the user-visible answer.
  • Per-tool rate limits with no per-conversation limit, so a loop stays under every individual limit and still generates enormous load.
What can race
  • A model may issue parallel tool calls that write the same row concurrently, with no coordination between them (Optimistic Concurrency).
  • A retried tool call can race the original if the first attempt timed out but is still running (Duplicate Detection).
  • Two conversations for the same user can act on the same entity at once, exactly like two browser tabs (Backend Races).
Security
  • Missing object-level authorization in a tool is the same vulnerability class as missing it in an endpoint, and it is the most common serious flaw in agent backends (Object-Level Authorization).
  • Prompt instructions are not a security control. "Only refund orders belonging to the current user" is a hope; a query filtered by session user id is a control (The Model Is Not the Authorization Layer in the Security domain).
  • Any tool that accepts a URL, host or path is SSRF and path-traversal surface with a new caller (SSRF — When the Backend Fetches a URL).
  • Tool results become model input, so a tool that returns attacker-controlled content is an injection channel — and one that returns a secret has exfiltrated it into the provider's context (Tool Misuse and Data Exfiltration in the Agentic AI domain).
  • Tool schemas and descriptions are read by the model at runtime; treat a third-party tool server's descriptions as untrusted content, not as configuration (MCP Primitives: Tools, Resources, Prompts in the Agentic AI domain).
Misreads
  • "The model picked the tool, so the model authorized it." Choosing an endpoint has never been permission to call it (Authentication vs Authorization).
  • "The schema validates the arguments." The schema is a hint to the model and a contract for your validator. Only the validator enforces (The Three Validations).
  • "These are internal functions, not an API." They are called by the least predictable client you have ever had.
  • "We told it in the system prompt." A prompt influences a probability distribution. It does not deny a request.
  • "Tool output is our own data, so it is safe to feed back." Your own database contains user-submitted text, which makes tool output an injection channel (Every Input Surface).

Operating it

How you see it in production
  • Per-tool call count, error rate, latency and authorization-denial count. A rising denial rate is either a prompt problem or an attack.
  • Argument validation failures per tool — the direct measure of how often the model produces malformed calls.
  • Idempotency-key hits on write tools: a non-zero rate means duplicate tool calls are happening and being absorbed (Duplicate Detection).
  • Tool calls per conversation, as a distribution with a tail. The tail is where loops and abuse live.
  • A span per tool call inside the request trace, with the tool name and the authorization outcome as attributes (Tracing From the Backend's Side).
What changes at 10x and 100x
  • Nothing about the enforcement changes with scale; it is per call and it is cheap.
  • Tool fan-out multiplies internal load: measure calls per user request, because one conversation can become dozens of internal requests (Cascading Failure).
  • With many tools, a shared middleware for identity, authorization, validation and logging becomes necessary — the same argument as middleware for HTTP routes (The Middleware Pipeline).
  • At very small scale the temptation is to skip enforcement "for now"; the resulting vulnerability is identical to skipping it on a public endpoint.
What this costs
  • Narrow tools are safer and less capable. A generic query tool is dramatically more flexible and gives the model an attack surface as wide as your data.
  • Strict validation causes the model to fail calls it could almost have made correctly, costing retries and tokens. That is the correct trade.
  • Approval gates on high-value actions slow the experience and are the only reliable control for irreversible operations (In-the-Loop vs On-the-Loop and Escalation in the Agentic AI domain).
  • Per-tool middleware adds indirection and is the only way enforcement stays consistent across a growing tool list.

Where this applies

Backend advice is context-sensitive. These labels say what each claim is specific to, and where a different stack or scale would differ.

  • GENERALIndependent of model provider, framework and transport — direct function calls, an MCP server or an HTTP tool endpoint all present the same obligations.
  • FRAMEWORK-SPECIFICAgent frameworks differ in whether they give you a place to hook authorization and validation before dispatch. Where a framework offers no such hook, the enforcement has to live inside every handler, and the consistency problem is yours to solve.

Where the depth lives

This domain teaches the application-side mechanism and hands the rest off.