How do I audit what an AI agent actually accessed, rather than what it was allowed to access?

Most audit trails record grants, not retrievals. They answer "was this agent permitted to read the customers table" and not "which rows did it read, when, and in response to what." For an agent that is the wrong question, because the agent held a valid permission in essentially every incident worth investigating. The record you need names the columns returned, the volume, the time, and the identity of the agent as distinct from the human who started it.

An audit log with one denied request expanded, listing the time, actor, action, client IP and object, a notice that it was refused at layer eight by a workflow, and a layer-by-layer decision list beginning with credential, guardrails and role permissions.
One request, opened: what was asked for, what was decided, and which layer decided it. The layers are evaluated top to bottom and the first refusal stops the request.

Why database logs are usually not enough

Database audit logging exists and should be on. It falls short for agent workloads in three specific ways.

It records the connection identity, which is typically one shared credential, so every agent looks like the same user. It records the query text but frequently not the result volume, so a query returning four million rows and one returning four look alike in the log. And it has no notion of a session or task, so there is no way to group the twenty queries an agent issued while working on one request.

After an incident, this is the difference between reconstructing what happened and guessing at it.

Why agent framework logging is not evidence

Frameworks such as LangChain and LlamaIndex, and observability tools built for them, will log tool calls and their results. This is genuinely useful for debugging and it is not an audit trail.

The agent writes its own logs. If the agent is compromised, misconfigured, or simply wrong about what it did, the log inherits the error. Evidence has to be produced by something the agent does not control, which is the same reason application logs have never been accepted as the security record of an application.

What an agent-grade record contains

Four fields separate a usable record from a noisy one:

  • Agent identity, separate from human identity. Who ran it and on whose behalf are different questions, and after an incident you need both.
  • Columns actually returned, not columns permitted. This is the field most systems lack and the one investigations depend on.
  • Volume, so bulk retrieval is visible in the record rather than inferred from it.
  • Session grouping, so a task is reconstructable as a task rather than as scattered queries.

Ascent writes every request to an append-only audit log, readable at GET /audit_log and with no endpoint that modifies or deletes an entry. Each row carries the caller's ACRN with application and user identity held separately, the credential the request arrived on, the action, the object, the source address and country, an optional caller-supplied session tag, the allow or deny result, which of nine evaluation layers the request stopped at, and the per-rule reasoning behind the decision. A read additionally records how many records it served and how many fields were returned and masked, as counts: field names and record values are deliberately never written to the log. Because policy resolution happens outside the agent, the record is produced by the enforcement layer rather than by the thing being audited.

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)
Read the log, then open one entry in full
curl -sS "$ASCENT/api/audit_log" \
  -H "Authorization: Bearer $TOKEN"

AUDIT_ID=4471

curl -sS "$ASCENT/api/audit_log/$AUDIT_ID" \
  -H "Authorization: Bearer $TOKEN"

Explaining a decision after the fact

A related question auditors ask, which most systems answer badly: why was this specific request allowed or refused?

A permission model can usually reconstruct this by hand. A policy with multiple conditions often cannot, and "the policy engine decided" is not an answer that survives a compliance review.

Ascent resolves this by writing the whole evaluation onto the audit row: every policy that applied, what each concluded and the reason it gave, and for a rule carrying conditions, each condition with the value it required, the value observed and whether it held. The row also names which of the nine layers the request stopped at, so the decisive step is a stored field rather than an inference. The same evaluator answers hypotheticals at POST /authorization/dry_run, which rehearses a request without performing it. With deny-wins resolution, the decisive condition is identifiable rather than emergent.

Summarise a slice, and verify the hash chain has not been edited
curl -sS "$ASCENT/api/audit_log/summary" \
  -H "Authorization: Bearer $TOKEN"

curl -sS "$ASCENT/api/audit_log/verify" \
  -H "Authorization: Bearer $TOKEN"

What to do

Turn on database audit logging, keep framework tracing for debugging, and do not treat either as the access record. Put the record where the enforcement happens, and check that it captures returned columns and volume rather than permissions and query text. If your current logging cannot tell you how many rows an agent retrieved last Tuesday, it will not answer the question you will eventually need it to answer.

How Ascent does this: Put AI on health, payment and HR records.

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