Blog

Authorization as Code: AI Agent Access Control That Ships

·10 min read

The policy problem no one talks about

Most teams deploying AI agents in 2026 have solved authentication. The agent gets a token, a service account, a non-human identity. What almost nobody has solved is the more important question: what may this agent actually do with the access it’s been given?

In most deployments, the answer lives in one of three places.

  • It is in the agent’s prompt, where “you should only read customer records” competes with every other instruction in the context window and loses the moment the model compresses, forgets, or gets injected.
  • It is in application code, where a developer added an if statement that checks the endpoint before calling it - a guard that is invisible to security review, untestable in isolation, and coupled to the agent framework it was written inside. Or,
  • It is nowhere at all, and the credential’s ambient scope is the policy.

None of these are authorization. They are intentions - expressed in natural language, buried in procedural code, or simply assumed.

  • They cannot be reviewed in a pull request.
  • They cannot be diffed between environments.
  • They cannot be audited after an incident. And
  • They break the moment something unexpected happens,

which is the defining characteristic of agentic systems.

Infrastructure as Code solved this problem for compute and networking a decade ago. Policy as Code solved it for cloud permissions. The same pattern - declarative, version-controlled, deterministic - is the missing piece for AI agent access control.

What Authorization As Code (AzAC) actually means

Authorization as Code is the practice of expressing every access control decision as a declarative configuration object - not procedural logic, that is evaluated deterministically, outside the agent’s reasoning loop, on every call.

Three properties make it “as code”:

Declarative. A rule says what is allowed or denied. It does not say how to check it. The enforcement engine interprets the declaration at runtime; the author states intent. This is the difference between writing if (method === 'GET' && path.match(/^\/v1\/accounts/)) inside your agent and declaring:

{
    "effect": "allow",
    "ruleActive": true,
    "methods": ["GET"],
    "path": {
        "path": { "pattern": "^/v1/accounts/[^/]+$" },
        "presence": "must_exist"
    },
    "notes": "Read a single account by ID"
}

The JSON object is a policy. The if statement is an implementation detail. One is reviewable by a security team that has never seen your codebase; the other requires reading the agent’s source.

Versionable. Because it is a data structure - not a line of code inside a function, not a sentence in a system prompt - it can be stored in Git, diffed between commits, reviewed in a pull request, and promoted through environments. When something goes wrong, you can answer “what was the policy at the time of the incident?” by checking out the commit, not by interviewing the developer who wrote the if statement.

Deterministic. The same request against the same rule produces the same decision every time. There is no temperature, no context window, no model judgment. A deny is a deny. An allow is an allow. A filter that destroys a field removes it from the response before the agent ever sees it. The enforcement point does not negotiate.

Rules: the allow-list for every call

In RequestRocket, a rule is a JSON object attached to a proxy. It declares which HTTP methods are permitted, which paths may be called, and optionally which query parameters, headers, or request body shapes are acceptable. The proxy evaluates every inbound request against its rules before forwarding anything upstream.

A proxy starts in a known state. The proxyDefaultRuleEffect field - "allow" or "deny" - sets the baseline. For AI agent traffic, the secure default is "deny": nothing gets through unless a rule explicitly permits it.

Here is a minimal policy for an agent that may read accounts and create support tickets, and nothing else:

{
    "effect": "allow",
    "ruleActive": true,
    "methods": ["GET"],
    "path": {
        "path": { "pattern": "^/v1/accounts/[^/]+$" },
        "presence": "must_exist"
    },
    "notes": "Allow reading a single account by ID"
}

Every field here comes from the API. effect, ruleActive, methods, path, body, notes - these are the actual fields you POST to /clients/{clientId}/proxies/{proxyId}/rules. They are not pseudocode. A security engineer can read them without understanding the agent framework, the LLM, or the application that calls the proxy.

The ticket rule includes a body check: the agent may create tickets, but only with priority set to low or medium. If the model generates a request with "priority": "critical" - whether through a reasoning error or a prompt injection - the proxy refuses the call before it reaches the upstream API. The agent receives a denial; the upstream system never sees the attempt.

Filters: least privilege on the response

Rules govern what goes out. Filters govern what comes back. This is the piece most access control systems skip entirely, and it is the piece that matters most for AI agents - because anything in the response lands in the model’s context window, where it can be extracted, forwarded, or used as a vector for indirect prompt injection.

A filter is a set of operations that retain or destroy fields in the API response before the agent receives it. Like rules, filters are JSON objects attached to a proxy and evaluated deterministically on every call.

Say the accounts endpoint returns a full customer record: name, email, phone, payment method, internal notes. The agent only needs the name and account status to do its job. Everything else is exposure:

{
    "filterActive": true,
    "methods": ["GET"],
    "requestPath": {
        "path": { "pattern": "^/v1/accounts/" },
        "presence": "must_exist"
    },
    "operations": [
        {
            "effect": "retain",
            "jsonPath": { "pattern": "^(name|accountId|status)$" },
            "notes": "Keep only what the agent workflow requires"
        }
    ],
    "notes": "Strip PII and payment data from account responses"
}

The retain effect keeps matching fields and destroys everything else. After this filter runs, the JSON the agent receives contains name, accountId, and status - and nothing adjacent. The payment method, the phone number, the internal notes: gone before they enter the context window.

Filters also support destroy, which removes matching fields and keeps everything else - useful when a response is mostly safe but contains a few sensitive fields. The two effects compose: destroy always wins when both match the same element, so you can layer broad retain policies with targeted destroy overrides.

Filters can scope themselves to specific request paths, methods, headers, query parameters, and even response status codes. A filter that only fires on GET /v1/accounts/ does not affect ticket creation responses. Each filter is a self-contained policy document that says exactly which responses it shapes and how.

Credentials: the agent never holds the key

Authorization as Code extends to how credentials are managed. In RequestRocket, the agent authenticates to the proxy with a managed proxy credential - a bearer token or API key that proves it is allowed to use this proxy. The real credential for the upstream API - the OAuth2 client secret, the production API key, the JWT - is stored on the target credential and injected by the proxy at forward time.

The agent never sees, stores, or logs the upstream secret. It cannot leak it in a verbose log line, expose it through a model hallucination, or have it extracted by a prompt injection. If you need to rotate the upstream key, you update one credential record; no agent code changes, no redeployment, no configuration drift across fifty agent instances.

This is the credential half of authorization as code: the binding between “this agent” and “this upstream system” is a configuration object, not an environment variable.

The pull request is the security review

The real shift is not in the enforcement engine - it is in the workflow. When authorization is code, the security review process looks like every other code review:

  1. An engineer defines the agent’s access policy as rule and filter JSON.
  2. The policy goes into a pull request alongside the agent code it governs.
  3. A security reviewer reads the rules. The format is self-documenting: effect, methods, path, body checks, operations with retain or destroy. No framework knowledge required.
  4. The policy merges and deploys through the same CI/CD pipeline as everything else.
  5. After an incident, the Git history shows exactly what the policy was at any point in time - who changed it, when, and why.

Compare this to the alternative: a developer wrote an if statement in a LangChain tool wrapper, another developer added a system prompt instruction, a third configured scopes in a dashboard that is not version-controlled, and after the breach nobody can reconstruct what the agent was supposed to be allowed to do versus what it actually could do.

Authorization as code does not prevent mistakes. It makes mistakes visible, reviewable, and reversible - which is most of what “governance” means in practice.

Variables: dynamic policy without procedural code

Static path patterns cover most cases, but real-world policies often need to reference values from the request itself. A rule that says “the agent may only read the account it was asked about” requires comparing a path segment to a value from the request body or a JWT claim.

RequestRocket rules support variables - named bindings that extract a value from the request headers, query parameters, body, a static literal, or a verified JWT claim - and make it available as a {{token}} in any pattern:

{
    "effect": "allow",
    "ruleActive": true,
    "methods": ["GET"],
    "variables": [
        {
            "name": "callerOrg",
            "source": "jwtClaim",
            "key": "org_id"
        }
    ],
    "path": {
        "path": { "pattern": "^/v1/orgs/{{callerOrg}}/accounts/[^/]+$" },
        "presence": "must_exist"
    },
    "notes": "Agent may only read accounts within its own organisation"
}

The callerOrg variable is extracted from the verified JWT’s org_id claim and interpolated into the path pattern at evaluation time. The agent can read accounts - but only under the organisation its token was issued for. This is tenant isolation expressed as a single rule, not as application logic scattered across middleware.

Variables keep policies declarative. The rule does not contain a function call, a database query, or a conditional branch. It states a relationship between a claim and a path, and the engine evaluates it on every request. The policy is still a JSON object, still diffable, still reviewable.

Why this matters now

The EU AI Act’s high-risk system obligations arrive in August 2026. ISO/IEC 42001 certification requires documented, auditable AI controls. NIST’s AI Agent Standards Initiative is building the framework regulators will reference. Every one of these requires you to demonstrate what controls exist, how they are enforced, and that they were in place at any given point in time.

Authorization as code is the cheapest honest answer to all three questions. The controls are the rule and filter definitions. The enforcement is the proxy evaluating them on every call. The audit trail is the Git history of the policies plus the per-request log of which rules matched and what decision was made.

The alternative - reconstructing what an agent’s access policy was from a combination of prompt instructions, application code, and dashboard settings that were never versioned - is not an audit. It is forensic archaeology.

Next steps

Pick one AI agent your team has deployed. Write down the shortest list of API operations it actually needs - method, path, and the tightest constraint you can put on each. That list is your allow-list. If you can express it as a set of JSON objects rather than a set of if statements, you have authorization as code.

RequestRocket is built for exactly this: declarative rules and filters evaluated on every call, credentials injected at runtime, and an audit trail linking every decision to the policy that made it. No changes to your agent code or the upstream API. Read the documentation or start for free.

Enhance ISO 27001
Enhance SOC 2
Enhance GDPR
Enhance HIPAA

Add outbound API security
without changing code

Start on your own or talk to our team about improving the security of every API call you make.