In brief
Runtime controls on AI agents are decisions that run at each action, not at the connection. When an agent reads from a CRM, a data warehouse, or an internal API, put a decision point inside the tool handler that runs before the read or write completes. Give that decision point three things to check: a budget keyed on the identity that you are protecting and sitting at the tool call, injection detection on every untrusted input including data that the agent just read, and sensitive-information detection at the boundary.
The reason this is necessary rather than redundant is that identity controls have already passed by that point. They were designed to.
The agent is authorized, and that is the problem
Authorization of the agent is not authorization of the action. Scoped credentials answer whether an agent may reach a system. They cannot answer whether this particular operation, with these arguments, for this user, should happen now.
When you connect an agent to an enterprise system, the access review passes. It has a service identity, scoped credentials, and an audit log on the target system. Everything that an IAM review asks for is in place.
Then the agent reads a record, decides its next step from what it read, and acts. The credential was valid at every step. Nothing in the identity layer was violated. The outcome is one that nobody authorized, because you granted authorization to the agent, not to the action.
Runtime controls vs IAM vs API gateway
A runtime control, an identity grant, and an API gateway are three different layers. Each answers a different question, and none of them substitutes for the others.
| Layer | Question it answers | What it cannot see |
|---|---|---|
| IAM / identity | Can this agent reach this system? | The specific operation, its arguments, or consumption across a loop |
| API gateway | Does this HTTP request match a route policy? | Tool handlers and background jobs that never become an HTTP request |
| Runtime control | Is this operation, with these arguments, for this user, allowed right now? | Fleet-wide inventory of which agents exist across the company |
Identity gets the agent in the door. A gateway can govern which HTTP routes it may call. The control that bounds a CRM read, a warehouse export, or an internal write has to run at the action, because that is the only place those three facts exist together.
What does a runtime control check?
A runtime control is a check that runs at the moment of the action and uses information that only exists then: the user, the arguments, the data just read, and the budget consumed so far.
| Control | What it catches | Why the identity layer misses it |
|---|---|---|
| Budget per identity, across the loop | A workflow that consumes far more than intended, whether from a non-terminating loop, a retry storm, or an injection | Credentials have no notion of consumption, and per-request limits only count workflow starts |
| Injection detection on untrusted input | Hostile instructions arriving through enterprise data the agent reads and then acts on | A record the agent is permitted to read is still untrusted content once it re-enters model context |
| Local sensitive-information detection | Protected data moving through a tool into a response, a log, or an embedding | Access grants say who can read, not where the data can go afterwards |
Budgets that hold across the loop
A token-bucket budget is a refillable allowance keyed on an identity, drawn down by each call in proportion to cost. Calls share a bucket by key, not by correlationId. The following sample keys on session.userId, so every matching call for that user draws from the same bucket, including concurrent runs.
A limit applied only at the HTTP entrypoint would see one request and allow all of the downstream calls. A misconfigured retry loop can issue 40 or more tool calls in a single workflow execution; an entrypoint limit fires once and the spend happens 40 times. If you want a per-run budget instead, key on the run ID and say so.
Token bucket is the right algorithm for agent loops because the work is bursty and variable-cost. A legitimate turn can call several tools in a few seconds; a fixed window treats that burst the same as a runaway retry storm, and it also doubles at the clock boundary. A token bucket absorbs the short burst up to maxTokens, then throttles sustained consumption at refillRate. Each call can spend a different requested cost, so a cheap metadata read and an expensive warehouse scan draw different amounts. Fixed-window and sliding-window remote rules on protect() do not support that per-call cost, and they do not run on tool handlers.
Enterprise data is untrusted input
Indirect prompt injection is hostile instructions that reach a model through retrieved content rather than through the user's opening message. A comment field in a CRM record, a description on a warehouse row, a ticket body, or a filename in a document store can all carry text authored outside your organization. Once the agent reads that text and feeds it back into model context, the model can treat it as an instruction. The fact that the bytes came from your own database does not make them safe.
The OWASP Top 10 for LLM Applications 2025 ranks prompt injection first as LLM01, and treats the indirect variant as the form most likely to appear in an internal integration. Injection checks belong on every untrusted input, including the user's opening prompt and the tool outputs you feed back to the model. This is the part that teams underestimate. Framework wiring for LangChain, LlamaIndex, and the Vercel AI SDK is in the prompt injection guide for those stacks.
Enforce where there is no HTTP request
A tool handler is not an HTTP request. Agent access to enterprise systems runs through tool calls and background jobs. There is no request object, so proxies, WAFs, and HTTP middleware never see it. The control has to live inside the function. For the broader placement argument, see SDK-based security vs WAF vs API gateway.
import { launchArcjet, tokenBucket } from "@arcjet/guard";
const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
const budget = tokenBucket({ refillRate: 100, intervalSeconds: 60, maxTokens: 500,});
// Inside the tool handler, before the agent reads anythingconst decision = await arcjet.guard({ label: "tools.read-customer-record", actor: session.userId, correlationId: workflowRunId, rules: [budget({ key: session.userId, requested: 1 })],});
if (decision.conclusion === "DENY" || decision.hasFailedOpen()) { // Stop before the agent reads throw new Error(`Denied: ${decision.reason}`);}Two details are worth getting right. Derive actor from an authenticated server-side identity, never from user-controlled input. A policy can be conditioned on the actor, so an attacker who controls the actor can escape their own policy scope. Set correlationId to your workflow run ID, so that decisions from a single agent run are attributable to that run rather than scattered as unrelated events.
A direct Guard call fails open: an allow can mean evaluation failed, and the return includes error codes. Check hasFailedOpen() on sensitive reads; otherwise a timeout lets the action proceed. Vercel AI SDK and LangChain wrappers fail closed unless you opt into continuing on error. HTTP request checks can fail open when Arcjet's cloud can't be reached; that behavior is configurable.
Arcjet validates label server-side as a slug: lowercase letters, digits, dash, and dot only, starting and ending with a letter or digit. Use one hardcoded label per specific operation rather than building it from a variable, so the label stays greppable and groups cleanly in reporting.
Separation of enforcement and policy
Separation of enforcement and policy means where the check runs and who can change the rule are different jobs. Developers embed the controls in code, reviewed like any other change, in the same pull request as the tool that they protect. Security teams change Guard remote policies without a redeploy (allow/deny lists, length, membership, prompt injection, and local sensitive-info), roll out in dry run to measure first, and inspect decisions through the Console, CLI, or MCP server.
Token-bucket refill and capacity are not remote: those numbers live in application code. HTTP remote rules on protect() (bots, Shield, filters, fixed-window and sliding-window rate limits) are a separate system, site-wide, and don't support token bucket. Threat modeling, incident response, and posture stay with the security team. Only the enforcement point moves.
Most products in this category make you pick one of those two. A control plane gives the security team a console and leaves engineers out. A developer library gives engineers rules in code and leaves the security team unable to change anything without a deployment. Supporting both against the same enforcement point is what makes the control survive contact with an organization that has both functions.
Roll out runtime controls
A measurable rollout is a sequence that inventories consequence first, measures against real traffic, then enforces one rule family at a time.
- Inventory the tools that touch enterprise systems, and rank them by what a failure would cost rather than by traffic.
- Add a guard inside the highest-consequence handlers first.
- Run in dry run against real traffic, so you learn where thresholds actually sit before anything blocks.
- Move rules to enforcing one at a time, starting with budgets, which have the clearest failure signature.
- Tag every decision with the workflow run, so that the activity of a single agent run is reconstructable.
Where this is heading
Sequence-aware enforcement is policy that carries what happened earlier in the run into the current decision. Enforcing at each action is the floor, and it is the part that you can build first. The harder problem is sequence-shaped.
An agent reads a customer list, then calls an export tool. Each action is permitted, each passes its own check, and together they are an exfiltration. Per-action enforcement doesn't deny a later step because of earlier ones. correlationId tags the run so that the sequence is reconstructable, but it doesn't change the current allow or deny. For the same incident worked as a teardown, see anatomy of an agent incident. For irreversible steps such as a refund or a send, pair the floor with a hold: see preventing irreversible AI agent actions. The broader failure-mode model is in runtime security for LLM applications.
What per-action enforcement does do is make the question answerable at all. Without decision points at the consequential actions, and without a correlation ID tying them to a run, there is nothing to reconstruct later. Getting the floor dependable is the prerequisite.
Where Arcjet fits
Arcjet is the in-code enforcement layer underneath identity and gateways: a library that runs inside the application, in the path of each tool call, deciding whether to allow a specific action given the user, the arguments, and the budget consumed so far.
Securing agents against enterprise systems usually involves several layers. An identity provider issues the agent's credentials. A gateway can govern which MCP servers it can reach. An observability platform records what happened. Arcjet reaches the code paths that a gateway can't, because tool handlers and background jobs never route through one.
Arcjet doesn't replace agent identity, and it doesn't do fleet-wide discovery of which agents exist across your company. Those are different layers. For more information about them, see the AI agent security platform category map. When the tool calls Stripe, GitHub, or send-email instead of a CRM, use runtime controls on external APIs.
Learn more: AI runtime protection ยท Arcjet Guards
Implement this now
Put a token-bucket budget on the first enterprise tool you already run in production. The budget control quickstart walks through the Guard call, the key, and the deny path.
Frequently asked questions
How do I enforce runtime controls on agents accessing enterprise systems?
Put a decision point inside each tool handler that runs before the read or write completes, checking a budget keyed on the identity you are protecting, injection detection on untrusted input including data the agent just read, and sensitive-information detection at the boundary. Identity controls have already passed by that point, because they were designed to.
Why are scoped credentials not enough for AI agents?
Scoped credentials answer whether an agent may reach a system. They cannot answer whether this particular operation, with these arguments, for this user, should happen now. Authorization was granted to the agent rather than to the action.
How do runtime controls differ from IAM and an API gateway?
IAM answers whether the agent may reach the system. An API gateway answers whether an HTTP request matches a route policy. A runtime control answers whether this operation, with these arguments, for this user, is allowed right now. Tool handlers and background jobs never become HTTP, so a gateway cannot see them.
Is data from our own database untrusted input for an agent?
Yes, once the agent reads it and acts on it. A comment field in a CRM record, a ticket body, or a filename in a document store can all contain text authored outside your organization. The OWASP Top 10 for LLM Applications 2025 ranks prompt injection first as LLM01, and treats the indirect variant as the form most likely in an internal integration.
Can a proxy or WAF enforce controls on agent tool calls?
No. Tool calls and background jobs have no request object, so there is nothing for a proxy or WAF to inspect. The control has to live inside the function that runs the operation.
Why use a token bucket instead of a fixed window for agent budgets?
Agent loops are bursty and variable-cost. A token bucket absorbs a short legitimate burst up to capacity, then throttles sustained consumption, and each call can spend a different requested cost. A fixed window treats a multi-tool turn the same as a retry storm and can double at the clock boundary.
AI runtime security in your code
Protect your AI agent workflows with Arcjet
A decision inside each tool handler before the read or write. Get budgets, injection checks, and local sensitive-info on the enterprise tool path.