Dev.to AI 🤖 Ai 👁 0 📖 8 min read

An Auditor Is Not an Admin: Giving Someone the Evidence Is Not Giving Them the System

An auditor has to review what an AI did. The cheapest way to let them do that is to hand them an admin account. But then the person they are supposed to review becomes themselves. This piece is about building a read-on

An auditor has to review what an AI did.

The cheapest way to let them do that is to hand them an admin account. But then the person they are supposed to review becomes themselves.

This piece is about building a read-only auditor role: its reach is an explicit list, every field is rebuilt from a whitelist, row scope comes from the account rather than the request, and the signed evidence stays whole and unprojected.

  1. Admin is the cheapest answer, and the wrong one An audit reader's job is to review what someone else did. They need evidence, not operational authority.

But in most systems read access follows write access: whoever can manage everything can obviously read everything. So the shortest path to "let them see the audit" is to give them admin. The cost is that they also gain the ability to change policy, approve requests and execute tools, which is exactly what they came to review.

What is needed is not a weaker admin. It is a role whose reach can be enumerated item by item. If you cannot enumerate it, it is still an admin.

  1. The role's reach is a list Our auditor role gets five rules, and every one grants read only:

{ roleCode: UserRole.AUDITOR, subject: 'AuditLog', actions: ['read'], ownerField: null },
{ roleCode: UserRole.AUDITOR, subject: 'OperationAudit', actions: ['read'], ownerField: null },
{ roleCode: UserRole.AUDITOR, subject: 'GovernancePolicy', actions: ['read'], ownerField: null },
{ roleCode: UserRole.AUDITOR, subject: 'ToolInventory', actions: ['read'], ownerField: null },
{ roleCode: UserRole.AUDITOR, subject: 'ApprovalQueue', actions: ['read'], ownerField: null },
That list is everything the role can touch. Everything else is denied by the policy guard by default.

It deliberately gets none of the USER ownership rules. This role is a reader of the governance record, not a user of the application. It should have no write action either. The day one of these rules grows a write, the role stops being an auditor.

The shape of the rule is worth one sentence too. The underlying role seeds originally had only a subject and an owner field, and every seed implied manage. We added an optional actions; omitting it still means manage, so no existing role changed behaviour. Writing it is how a role narrows itself. Read access is written down, not inferred from management.

  1. Fields: rebuilt from a whitelist, not deleted from a full row The role decides which surface you can reach. The field list decides what you can see on that surface.

We keep one whitelist and rebuild the projection from it:

only keys named on the list are written
a column added tomorrow stays invisible to an audit reader until someone deliberately adds it to the list
deleting forbidden fields from a full row would fail open: the next column leaks by default
That is not a style choice. It is a choice about defaults. Deleting from a full row defaults to visible; rebuilding from a whitelist defaults to invisible.

The fields left off the list have reasons. A conversation id is a pointer into a person's conversation. IP and device ids bind activity to a device or a person. A user's feedback about the assistant is not the audit record. Delegation context is free text that may quote a prompt. The bounded business intent and the agent and caller ids carry the attribution a reader actually needs.

One test injects a key that is not on the list at runtime and asserts it does not appear in the projection. It is not testing deletion. It is testing the default.

  1. Rows: organisation from the account, period from the request Beyond the role and the fields there is the question of which rows. The same surface, with the same fields, must still show different rows to different readers.

The organisation comes from the account, not the request. An organisation parameter on the request can only narrow further, ANDed with the account scope, never replace it.
An auditor who belongs to no organisation falls back to their own rows. A failed organisation lookup also falls back to their own rows. A failed lookup is not a reason to widen.
The period comes from the request window. There is no per-reader period attribute today and no read-plane policy. Inventing one here would invent a dimension, so we use the window the caller asks for.
Fetching a single row by id returns 404 when it is out of scope, the same answer as "no such row". The gate already passed; telling the reader that another organisation has that row is itself a disclosure.
The by-id path and the list call the same scope function. Two implementations would eventually disagree.

  1. Aggregates need the scope too, and so does the cache key Narrowing the list while leaving the counts narrow-free is the same as not doing it.

The auditor's stats, cost, report and coverage statement all read through the same organisation scope. One thing is easy to miss: the aggregates are cached for 60 seconds, and the cache key has to carry the scope.

We hit that class of defect. With no scope in the key, the first reader's organisation answer was served to the next reader for a minute. The fix is not to clear the cache. The fix is to put the scope in the key: all, org: or own:. It is the same class as a verification verdict computed for one database being served for another. A cache does not know who is asking, so the key has to.

One thing we do not slice by organisation: the hash chain inside a signed evidence package. Slicing a chain and verifying the slice produces a false conclusion, so the chain keeps its full scope. Rows can be narrowed. A chain cannot.

  1. What stays closed Listing what the auditor can reach matters as much as listing what they still cannot:
  • Audit log, single-event explanation, chain, verify, stats, cost, report: the auditor reads projected rows within their organisation scope; an admin reads full rows.
  • Operation audit: the auditor reads, but row scope is not yet narrowed by organisation; an admin reads.
  • Governance policy, its history, tool inventory, approval queue: the auditor reads; an admin reads, writes and decides.
  • Signed evidence package and verify chain payload: the auditor reads them unprojected so they stay verifiable; an admin reads them too.
  • Users, organisation, sessions, platform analytics, writes, approvals, execution: unreachable for the auditor; reachable for an admin.

Three points deserve their own paragraph:

The signed evidence package and the verify chain payload are not projected. They are verification artifacts whose value is the complete canonical payload. Projecting them makes them unverifiable. Privacy projection covers row-shaped responses, not the evidence itself.
The auditor can read the approval queue but cannot decide it. Approving, rejecting and retrying execution stay admin-only. Reading the queue is not handling it. The same applies to flow approvers: the role list deliberately excludes the auditor. An auditor must not become the kind of person they are reviewing.
Operation audit's read access is open, but its row scope is not narrowed yet. The service filters only by the request's userId; it does not apply the reader's account scope. This is an open item from the role's widening, and saying so here keeps the five rules from implying that every surface is already scoped.

  1. The scene: widening one surface at a time Our first cut changed only the audit controller to read AuditLog. Operation audit still judged manage all, so the auditor reached that surface and got a 403.

That was not an omission. It was deliberate: open one surface, see whether it is enough, then widen one surface at a time. Later, operation audit, governance policy, tool inventory and the approval queue each got their own read subject. Writes, approvals and execution never entered the widening.

The order has a second benefit. Each surface can be verified on its own: what should be visible is visible, what should be blocked is blocked. Opening everything at once makes a hole impossible to attribute.

  1. Five checks you can run Which surfaces can your read-only role reach? Can you enumerate them, or is it "roughly like admin"? Are new fields visible by default? Deleting from a full row fails open. Rebuilding from a whitelist fails closed. Does row scope come from the account or the request? Can the request only narrow it? Where does a missing organisation fall back to? Do the aggregates and the cache carry the same scope? A narrowed list with unscoped counts is not narrowed. Is the signed evidence projected? If it is, can it still be verified? Closing Back to the opening: giving someone the evidence is not giving them the system.

What we do: five read-only rules define the role's entire reach; fields are rebuilt from a whitelist and new columns are invisible by default; organisation comes from the account and the period from the request; an out-of-scope row returns 404; aggregates and cache keys carry the same scope; signed evidence stays whole.

The mechanism above is taken from KeelBase, an open-source runtime that sits between an AI agent and a business system, re-checks identity, authorization, scope, risk and confirmation state before a tool runs, and writes every decision into a record that can be verified offline.

Repository: https://github.com/rain6fish/KeelBase

Four things stated plainly. The role is widened one surface at a time: everything outside the list is still unreachable, and that is default deny rather than unfinished work. Signed evidence cannot be projected, so privacy minimisation has an explicit boundary here. An auditor cannot decide approvals or serve as a flow approver. If they could, they would become the person they are reviewing. And operation audit's row scope is not narrowed by organisation yet: the read access is open, the scope is not, and that is an open item rather than a finished surface.

Read-only is not "a little less permission". It is a reach you can enumerate.

Drafted with an AI assistant. The system, the positions and the mistakes are mine.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.