---
title: 'Your agent is secured at the wrong layer'
description: 'Tool allowlists and OAuth scopes secure the verb. The exposure is the noun: which rows, which columns, which tenant. Your ORM already knows how to enforce that, and almost nobody is using it.'
canonical_url: 'https://darkfactory.dev/blog/your-agent-is-secured-at-the-wrong-layer'
markdown_url: 'https://darkfactory.dev/blog/your-agent-is-secured-at-the-wrong-layer.md'
collection: blog
---

# Your agent is secured at the wrong layer

Every agent permission system I have looked at asks the same question: may this agent call this tool? Allow the database query tool, deny the shell tool. Grant read scope, withhold write scope. Approve `search_customers`, decline `delete_customer`.

That is an authorization model with a verb and no object. It tells you what the agent may do and says nothing about what it may do it to. `search_customers` is not one permission. It is one permission per customer, and by approving the tool you approved all of them.

The exposure in an agentic system is not the tool call. It is the row.

## The allowlist is answering a question nobody asked

Consider a support agent with a read-only database tool. The tool is read-only, so the blast radius looks bounded. Nothing can be destroyed.

Now ask what it can read. Every customer record in the table, including the ones belonging to tenants unrelated to the ticket it is working. Every column, including the ones holding payment identifiers and internal notes. Every row of an audit log, which is often where the most sensitive material accumulates precisely because nobody thinks of an audit log as sensitive.

The tool permission was granted once, at configuration time, by someone reasoning about the tool. The data access happens thousands of times, at runtime, shaped by a prompt that person never saw. Those are different security decisions made at different times by different parties, and only one of them was reviewed.

This is not a hypothetical failure mode. It is the ordinary operating condition of most agent deployments I have seen, including some of mine before I went looking.

Read-only is not a security boundary. It is a durability boundary. It protects your data from your agent. It does not protect your customers from your agent.

## Everyone is building the same four workarounds

What I find interesting is that people clearly sense this, because the workarounds keep appearing independently, always described as pragmatic hacks rather than as an architecture:

**A dedicated read-only database user.** Correct instinct, wrong granularity. It constrains the verb again, not the object.

**Functions instead of raw SQL.** Rather than giving the agent a query tool, you give it `get_ticket_history(ticket_id)`. This is better, and it is the most common answer, and it works right up until you have forty such functions and no coherent story about what the set of them can collectively reach.

**A scratch database.** Copy a subset out, let the agent play in the copy. This works and is safe. It is also an admission that you could not express the constraint in the real system, so you built a second one.

**Anonymized clones.** Same admission, plus a data pipeline to maintain, plus the discovery that anonymization is much harder than it looks the first time someone re-identifies a record by joining three supposedly safe columns.

Four patterns, all reasonable, all reached independently by people who did not think they were doing security architecture. When that many builders converge on workarounds for the same missing thing, the missing thing usually has a name. I want to call this layer the agent data plane: the set of rules determining which rows, columns, and tenants a given agent identity may reach, enforced below the model rather than in front of it.

## Your ORM has been doing this for years

Here is the part that annoys me, having spent twenty years in this stack. The enforcement primitives already exist, in mature form, in the framework I was already using.

Laravel global scopes apply a constraint to every query against a model, automatically, whether or not the caller remembers. Policies express per-record authorization as a function of the actor and the record. Tenant scoping composes with both. Postgres row-level security does the same thing one layer further down; run the agent's queries as a role that is neither the table owner nor a superuser, and even raw SQL cannot escape it.

None of this was built for agents. It was built because multi-tenant web applications have always had the problem that a request handler might forget a `where` clause, and forgetting once is a breach. The framework's answer was to make the constraint impossible to forget by attaching it to the model rather than to the query.

An agent is a request handler that forgets constantly and cannot be trained out of it. It is precisely the caller these mechanisms were designed to survive.

So the implementation is less exotic than the framing suggests. Give the agent a real identity in your system rather than a shared service account. Attach that identity to a scope. Let the ORM enforce it. The agent gets a query tool that looks unrestricted, and reaches only what its identity permits, and no prompt can talk it past the boundary because the boundary is not in the prompt.

The rule I keep coming back to: a guardrail that executes above the model is advice. A guardrail that executes below the model is a boundary. Tool allowlists live above. Row scoping lives below.

## Where this argument is weak

The hardest objection is that this pushes the difficulty rather than removing it. Instead of one hard question, may this agent use this tool, you now have a per-identity policy design problem, and policy design is where security programs go to die. That is a real cost, and I do not want to pretend the row-level version is free. What I would say is that it is the same cost you already pay for human users, and most teams have already paid it once.

It also does nothing about exfiltration through a legitimately permitted path. If the agent may read a record and may also send email, it can send the record. Data-plane scoping bounds what it can reach, not what it does with what it reached. That is a separate problem and I do not want this framing to imply it is solved.

Row-level security has a real performance cost at scale, particularly when policies involve subqueries. I have not run this at a size where it hurt. Someone will, and I would rather hear about it from them than guess.

And there is a version of this critique that is too strong. Tool allowlists are not useless. Keeping an agent away from a shell is worth doing. My claim is that the allowlist is necessary and radically insufficient, not that it is theater.

## What this changes in my factory

Every agent gets its own identity. Not a shared key, not the application's own database user. An identity that appears in the same tables my human users appear in, subject to the same policy machinery.

Scoping lives in the model layer, not in the tool definitions. If I express the constraint in a tool's implementation, the next tool has to remember it. If I express it as a scope on the model, the next tool inherits it whether or not I was paying attention when I wrote it.

I stopped treating read-only as a meaningful description of an agent's access. When I catch myself saying it, I make myself finish the sentence: read-only across which rows.

Audit logs and internal notes are scoped harder than customer records, on the theory that the tables nobody thinks of as sensitive are the ones with no history of being defended.

And I assume the prompt is hostile, not because I expect an attacker, but because it is the only assumption that produces a design I trust. Content the agent reads can contain instructions. If my boundary can be argued with, it is not a boundary.

## What to ask about your agent's database access

- If I approve this tool, how many rows did I just approve? Say the number out loud.
- Does my agent have an identity, or is it borrowing the application's?
- Is the tenant constraint attached to the model, or is it attached to whoever remembers to write it?
- If a prompt instructed the agent to read another tenant's records, what stops it, and does that thing run above the model or below it?
- Which table would I least want in a transcript, and is it scoped more tightly than the customer table or less?
- If the answer to any of these is a scratch database, what am I unable to express in the real one?

Tool permissions are a useful inventory of what your agent can do. They are not a security model, because they never mention the data. The good news is that the enforcement layer you need is probably already sitting in your framework, unused, waiting for a caller careless enough to justify turning it on. You have one now.
