How do I stop an AI agent from pulling my entire customer table?

A database has no concept of how much is too much. If an agent holds a credential that permits reading the customers table, a query returning one row and a query returning four million rows are equally valid, and the database answers both. Stopping bulk extraction requires something outside the database that counts what is leaving and refuses when the volume stops looking like the work the agent was asked to do.

There are four approaches, and only two of them survive an agent that is behaving reasonably from its own point of view.

A workflow called Bulk decryption: freeze, revoke, deactivate, drawn as a trigger chip followed by freeze, revoke and deactivate response actions and a terminal refuse step, with its JSON source listed underneath.
The third approach as it actually ships: a pre-request threshold on decrypts per token, which then locks the data, revokes the credential and deactivates the agent. The numbers below are an example; this one triggers on five in sixty seconds.

Why a read-only role does not solve this

The most common first answer is to give the agent a read-only role scoped to specific tables. This is worth doing and it is not sufficient. Read-only prevents damage, not disclosure. An agent that can read the customers table can read all of it, and bulk extraction is a read.

This is the gap that catches teams by surprise, because the permission model was configured correctly and still allowed the outcome.

Why row limits in the application do not hold

The second answer is to cap result sets in the application layer or in the tool definition the agent calls. This works until the agent works around it, which it will do without any adversarial intent. An agent told to summarize customer complaints and handed a thousand-row cap will page. It will issue twenty queries of a thousand rows each, because that is a sensible way to accomplish what it was asked. No single request violated the limit.

Capping per request measures the wrong thing. What matters is cumulative volume across a session.

Rate and volume limits outside the data path

The third approach is a layer that tracks how much an agent has retrieved over a window and refuses further requests past a threshold. This catches the paging case, because the counter spans requests rather than resetting with each one.

The design questions that determine whether it works in practice:

  • What is counted. Rows returned is the useful unit. Queries issued is not, since one query can return everything.
  • What the window is. Per session, per hour, per day. Sessions are usually right for agents, because an agent's work is bounded by a task.
  • What happens at the threshold. Refusing is safe and noisy. Throttling is quieter and lets a legitimate long-running job finish late rather than fail.

Ascent counts requests rather than rows, and does it outside the permission document. A workflow carries a threshold scoped to an action, and fires when that action passes a count within a window, grouped by credential, application, user, source address or data repository (a protected set of records with its own keys):

{
  "scope": {
    "on_action": "record:get_with_decrypt",
    "type": "post",
    "threshold": {
      "group_by": "application_id",
      "count": 200,
      "per_seconds": 300
    }
  },
  "start_at": "revoke_the_grants",
  "steps": {
    "revoke_the_grants": {
      "type": "revoke_grant",
      "parameters": { "application_id": "{{requestor.application_id}}" }
    }
  }
}

Because the threshold sits outside the permission document it is not subject to deny-wins resolution, and because it counts actions it does not distinguish a request that returned one row from one that returned thousands. A row bound does exist, separately and per query: AQL_SCAN_LIMIT caps how many records a single query may scan, 5,000 by default.

First, your Ascent and a token to talk to it
ASCENT=https://ascent.example.com

TOKEN=$(curl -sS -X POST "$ASCENT/api/login" \
  -H 'Content-Type: application/json' \
  -d '{"email": "you@example.com", "password": "..."}' \
  | jq -r .access_token)
Create that threshold as a workflow. A name, a version and an active flag are required alongside the scope above
curl -sS -X POST "$ASCENT/api/workflow" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Bulk decryption: revoke the grants",
    "version": 1,
    "active": true,
    "scope": {
      "on_action": "record:get_with_decrypt",
      "type": "post",
      "threshold": {
        "group_by": "application_id",
        "count": 200,
        "per_seconds": 300
      }
    },
    "start_at": "revoke_the_grants",
    "steps": {
      "revoke_the_grants": {
        "type": "revoke_grant",
        "parameters": {
          "application_id": "{{requestor.application_id}}"
        }
      }
    }
  }'
Then read what it has actually done
WORKFLOW_ID=1

curl -sS "$ASCENT/api/workflow/$WORKFLOW_ID/results" \
  -H "Authorization: Bearer $TOKEN"

Restricting the shape of the query itself

The fourth approach is to constrain what the agent may ask for rather than how much comes back. An agent that can only query aggregates cannot extract rows at all. An agent that must supply a filter on a specific column cannot issue an unbounded scan.

This is stronger than volume limiting where it fits, because it removes the capability rather than capping it. It fits badly where the agent's task is genuinely exploratory, which is most coding assistant work.

Ascent does not scope query shape. Policy scopes the actions an identity may take and the objects it may take them on, so an agent can be held to a view — searching a data repository through the anonymized action rather than the decrypting one — but it cannot be restricted to aggregates:

"actions": [
  "acrn:12345678:action/container:search_with_anonymize",
  "acrn:12345678:action/record:get_with_anonymize"
],
"on_objects": [
  "acrn:12345678:object/workspace:Commerce:container:orders:record:*"
]

The related control is AQL's oracle rule. Outside the decrypt view an encrypted field cannot be ordered on, pattern-matched or grouped on, because each of those reads the value — so the shapes that would turn a redacted view into a way of extracting real values are refused, even though the shape itself is not what policy scopes.

A role editor for a role named Demo: AI Agent Backstop (grantable), with one permission rule whose decision is set to Deny and whose mode is set to Enforce, and a rehearse panel alongside it.
What policy can scope: the actions an identity may take and the objects it may take them on. This rule denies the whole decrypting family to AI agents while no approved grant is live.

What to do

Combine the third and fourth. Restrict query shape where the task allows it, and apply a cumulative volume ceiling everywhere else as the backstop. Keep the read-only role as well, since it costs nothing and covers a different failure.

The thing to avoid is stopping at the read-only role and believing the problem is solved. It is the most common configuration and it permits exactly the outcome you were trying to prevent.

How Ascent does this: Stop a misbehaving AI agent mid-request.

Three ways to see Ascent working.

Book a demo

Talk it through with us.

30 minutes with the team, on your questions.

Book a demo

Try the labs

Hands-on, synthetic data, no sign-up.

Two hands-on labs that simulate Ascent on synthetic data, with nothing to install: the containment lab and the vibe coding lab.

Try the labs

Request a trial

Your own instance.

We set up an instance for you on our infrastructure and email you within one business day. No credit card. Please use test or sample data.

0 of 1,000 characters