An AI agent is, operationally, a process holding credentials. Someone provisions it a service account, an API key, or an IAM role so it can do its job, and from that moment it can reach everything those credentials reach. The job description said “export billing data nightly.” The credential said “read and write the production database, and the object store, and the queue.” Nobody reconciled the two. Months later, no one on the team can answer a question that should be trivial: what can this agent actually reach, and what does it actually touch?
That gap is not a hypothetical. Independent industry research (CSA/Token) found that roughly 82% of organisations have AI agents running they don’t know about, while only about 21% maintain a real-time inventory of them. You cannot govern what you cannot see, and right now most estates cannot see their agents at all — let alone the edges connecting each agent to the resources it can reach.
Three different questions, routinely conflated
The reason “what can this agent reach” is hard to answer is that it collapses three separate questions into one. Keeping them distinct is the whole game.
- Held — the credentials the agent possesses. A token, a key, a role binding. This is an identity fact: what doors the agent has keys to.
- Permitted — what those credentials are allowed to do, per policy: the IAM grants, the database roles, the network rules. This is a policy fact, and it is almost always broader than anyone remembers, because grants accrete and nobody revokes.
- Observed — what the agent was actually seen doing: which resources it reached, and whether each access was a read or a write. This is a behavioural fact, and until you collect it, it simply does not exist anywhere.
Most security tooling stops at permitted. It reads your IAM and tells you the agent could write to the billing exports bucket. That is necessary but insufficient, because “could” is a set of thousands of latent capabilities, and a human cannot triage thousands of maybes. What changes the conversation is putting observed next to permitted and looking at the difference.
The access map
An access map is the graph of those edges: for every discovered agent, an edge to each resource it can reach, annotated with what was actually observed across it. The unit that matters is the edge, and the most important annotation on the edge is the read/write distinction.
R (read) means the agent retrieved data without changing it: a SELECT, a GET, a file read. RW (read/write) means it mutated state: an INSERT/UPDATE/DELETE, a PUT to object storage, a schema migration. The blast radius of those two is not comparable. A read edge on a sensitive table is an exposure question. A write edge — especially an unintended one — is an integrity and an incident question. Treating them as the same “access” is how teams miss the finding that mattered.
Here is what a single agent’s row looks like once the map is built:
| Agent | Resource | Permitted | Observed | Confidence |
|---|---|---|---|---|
| data-export-job | prod-postgres | RW | RW | attributed |
| data-export-job | s3://billing-exports | RW | R | attributed |
| data-export-job | internal-metrics-api | R | — | approximate |
Look at the first row. The export job was intended to read from prod-postgres and write the result to s3://billing-exports. Read source, write destination. But the credential it was handed grants RW on the database, and the collector observed it writing to prod-postgres — a mutation on the production system of record that no one reviewed, intended, or can currently explain. The permitted column quietly said this was allowed all along. That is the seed of an incident, sitting in plain sight, and without the observed column there is nothing to flag.
Permitted minus observed equals drift
The signature output is the diff. Least-privilege drift is what you get when you subtract one column from the other, and it cuts both ways:
- Permitted but never observed — the agent can reach
internal-metrics-apibut in the entire observation window never did. That is dead scope: a grant you can safely propose to revoke, shrinking the attack surface without touching behaviour. - Observed beyond what was reviewed — the write to
prod-postgresthat nobody signed off on. This is the high-signal finding: an action happening that, had someone been asked at provisioning time, they would not have approved.
The second category is the headline risk, and the honest version of the product never invents it. Every observed edge carries a confidence level — attributed when the action is tied to a specific agent identity with corroborating signal, approximate when the evidence is weaker. You show the difference rather than laundering a guess into a certainty. A finding you cannot stand behind is worse than no finding.
Discovered passively, attributed per agent
Two design choices make this trustworthy rather than just another dashboard.
First, discovery is passive. The collector observes — logs, OpenTelemetry traces, database audit streams like PostgreSQL’s pgAudit, and eBPF at the kernel as a ground-truth backstop — and it does not sit in the agent’s data path. There is no mandatory proxy. If the collector fails, the agent keeps working; observability degrades, production does not. For a system whose whole point is governing critical infrastructure, that asymmetry is the design: low downside, high signal.
Second, every action is attributed to a specific agent identity, not a shared service account. This is the difference between an audit ledger that says “the export role wrote to prod-postgres at 02:14” and one that says which agent did, under whose ownership, against which intended scope. Per-agent identity is the precondition for least-privilege to mean anything at all — without it, an action can be seen but never traced back to a responsible owner.
The edges and the read/write facts are the only thing stored. The map records relationships — agent to resource, R or RW — not payloads, not the rows that were read, not secrets. Inputs that might carry secrets or PII are redacted and secret-scanned before anything is written. The graph is sensitive precisely because it is an access map, so it is built to hold the minimum that makes governance possible and nothing more.
Signals from the agent’s own ecosystem help but are not trusted on their own. An MCP server may advertise that a tool is read-only via annotations like readOnlyHint; per the MCP specification those hints are untrusted and must be corroborated, never believed blindly. The kernel-level view is what catches a tool that claimed read-only and wrote anyway.
Once the map exists, the next step is enforcement: pinning the export job to read-only on the database, denying the write, and alerting instead of merely logging — policy applied at access time, not reconstructed after the incident.
agent "data-export-job" {
resource "prod-postgres" {
access = "read" # intended scope: read source only
deny = ["write", "ddl"]
}
resource "s3://billing-exports" {
access = "read-write" # intended scope: write destination
}
on_violation {
action = "block"
alert = "secops"
}
}
The map tells you the policy you should have written, because it shows you the one access the agent actually needed and the dozen it never used. That is the difference between guessing at least-privilege and deriving it from ground truth.
If this is the question your estate can’t answer today — what can each agent reach, and what does it actually touch — that is exactly what the access map is for. The product overview walks through discovery and the permitted-versus-observed diff; the architecture covers how passive collection, per-agent identity and the audit ledger fit together without ever sitting in your agents’ data path.