Answer whether an agent was allowed to do that, after the fact
The authority as it stood at the moment of the call, the decision taken on it, and a review bound to a digest.
On this page
Reconstructing last quarter’s authority from this morning’s configuration
The question arrives in a specific shape. Somebody in finance found a refund, or a customer found an email, or an auditor picked a row at random, and the question is whether the agent that did it was allowed to. The first place anyone looks is the current configuration, which answers a different question: what that agent may do today. The role has been edited twice since, and nothing in the configuration says what was in it at the time.
The second place is the access review, and this is the quiet failure that most estates carry. A review recording that agent X holds role Y certifies almost nothing, because role Y is editable and the review does not say what was in it. Six months later a named reviewer’s attestation sits beside a permission set they never saw, and nothing in the record distinguishes that situation from a genuine one.
The third place is the application log, which usually records that the call succeeded and not why it was allowed. That is the distinction between an outcome and a decision, and it is the whole difference between a log and evidence: an outcome tells you what happened, a decision tells you which rule permitted it and which named human the authority traced back to. Logs written by the agent framework are also written by the component under examination, which an auditor will notice before you do.
The fourth is the one people discover during the investigation itself. Refusals are missing. A trace that opens only for requests that succeed is a record of successes, and the interesting evidence in a governance review is the refusal: which agent tried to call which tool it did not hold a grant on, and when. An agent probing for grants it does not have is precisely the pattern an investigation needs, and it is invisible if a refused request never got a row.
How to actually do it
Each step is something you can go and do. Where a step depends on a decision somebody has to make rather than a setting somebody has to change, it says so.
- 01
Make the enforcement point read the same record as the inventory
Register every agent with a named human owner, a team and a declared purpose, and let the gateway resolve that row on every call rather than an exported copy. An inventory maintained beside the runtime is updated by whoever remembers while the runtime is updated by whoever ships, and the two diverge from the first week; there is no sync job here because there is only one list. - 02
Recertify against an exact configuration digest, not a role id
A reviewer submits the agent’s updatedAt and the snapshot digest they inspected; the route checks both before writing the audit intent, and the store re-derives the whole canonical snapshot inside the insert transaction and compares again. A concurrent edit — including one in the same clock tick — returns a typed 409 rather than certifying configuration nobody read. - 03
Keep the decision, not just the outcome
Confirm that your traces carry the reason strings: allowed by which role and which pattern, refused at which delegation link out of how many, denied by default, or blocked by which policy in which mode. Those strings are what an investigation reads, and deny by default is the phrase an operator will search for when an agent starts failing. - 04
Bind every human approval to the payload and the chain
An approval carries a SHA-256 over the canonical action plus its execution context — subject, team, tags, effective grants, the ordered delegation identities with their grants, the on-behalf-of identity, session id, tags and tier hint. That is what makes an approval record answer who authorised exactly what, rather than who clicked approve on a category. - 05
Seal the administrative history while it is short
Every role creation, edit and deletion is written to the hash-chained audit log with the acting user, and an edit records the previous permission list beside the new one. Configure the audit MAC key and start anchoring early: both guarantees are forward-looking, so the cost of waiting is a permanently weaker prefix on the history you will one day be asked about. - 06
Retrieve it in the shape the question arrives in
Search the flight recorder in English — the model’s only output is a validated filter object with fourteen allow-listed fields, never SQL — read the interpreted filter shown back as editable chips before you act on the answer, open the trace timeline, and export the set as a sealed bundle. The read itself is audited with the actor, the filter and the result count.
What the record can say about authority, and what it says about a refusal
The trace id is minted at step 3 of the eleven-step request path — before unicode sanitisation, before the personal-data and injection scanners and before the policy verdict — so a request blocked a millisecond later is recorded rather than missing, and the id comes back on x-acp-trace-id on every outcome including refusals. It cannot be first, because a trace belongs to an agent and authentication has to name one; the opening event carries counts and names rather than content, because at that point nothing has been sanitised, scanned or redacted and no payload text may be persisted yet.
The decision that follows is taken at one place rather than scattered across the path, and the internal order is fixed: engaged kill switches, then agent lifecycle status, then deny-by-default permissions including every link of any delegation chain, then budget and rate ceilings, then the policies whose scope selects this subject. Because it is one function with no input or output of its own, the identical logic decides a request at the gateway, a tool call on the tool path, a replay inside a backtest, and the bundle compiled for a developer’s laptop — so a rule means the same thing everywhere it is evaluated.
What lands on the trace is the reason as well as the verdict. An allow names the role and the resource pattern that produced it. A refusal on a delegation chain names the earliest failing link and out of how many, followed by the ordinary reason that link produced, which is the difference between a five-minute fix and an afternoon. A refusal with no matching allow ends in deny by default. A policy match writes a decision event carrying the policy id, its name, its mode, its action and the detail line explaining why it matched — including for a rule running in shadow, because a shadow policy nobody can see is not a dry run.
One narration detail matters more than it looks and it is worth checking in any product you evaluate. Shadow mode has to be answered before the verdict when a timeline is rendered, because asking what a policy did before asking whether it was live narrates a shadow match as an enforced block — the exact inversion shadow mode exists to let an operator avoid. That went wrong here in precisely that way once, painting a red blocked banner on a decision whose recorded verdict was require_approval, and the fix was to read all three shapes in which the pipeline writes a policy decision rather than only one of them.
- Refusals are recorded
- A tool call denied for want of a grant opens a trace, records the denial with a note of whether the tool actually existed, and closes as blocked. Existence and visibility collapse into one answer to the caller, and the probe is still visible to an investigation.
- The on-behalf-of setting is in the record
- Off resolves no principal at all. Shadow computes the intersection and writes it to the trace as a decision saying what it would have refused, with a status that is never blocked, so a dry run stays distinguishable from an outage. Only enforce refuses anything.
- A failed run is recorded as failed
- An interrupted stream closes the trace as an error with an error event, and its partial usage is still metered. A failed run recorded as a success is worse than no record at all, because it is a record that lies.
- An empty timeline means something
- A trace with no recorded steps is a request rejected at authentication, before the pipeline started, and the page says so rather than showing a blank list.
Why a review binds a digest, and what invalid honestly means
A recertification is an append-only attestation by one named reviewer over one exact governance-bearing configuration: name, owner, team, lifecycle status, role ids, the effective role grants, framework, tags, purpose, risk tier, budgets, rate limits, data policy, routing, the on-behalf-of requirement and metadata. Presentation-only fields and storage timestamps are excluded, because a review invalidated by somebody fixing a typo in a description trains reviewers to click through.
The part that does the work is the role grant. Each referenced role is bound by name, permission count and a SHA-256 over its normalised permissions rather than merely by its stable identifier — which is what makes the attestation mean something six months later. Editing a role changes the live digest immediately and marks every affected review stale, while the historical snapshot of what was actually attested is left untouched. Ordering is normalised where it grants no different authority, so reordering tags or actions produces no false staleness and does not dilute the signal.
Two digests are stored, and the second is the one people miss. The snapshot digest covers the configuration attested. The record digest is a second SHA-256, under its own domain separator, over the review id, agent id, reviewer id and email, the review and expiry times, the note and the snapshot digest — so the identity of the reviewer is bound to the thing reviewed rather than sitting beside it in a column. Before the review row is inserted, a durable audit intent is appended to the chain and the review stores the exact sequence number and hash of that entry.
One consequence is deliberately awkward and is published as such. First initialisation of the audit key does not retroactively authenticate isolated entries from the earlier unkeyed chain, so a review created before the first keyed checkpoint becomes invalid and has to be repeated. Promoting a legacy row without verifying its descendant path would let a database writer rewrite both the row and the domain record referencing it, so Token Observe asks for the human decision again rather than inventing a replacement for it.
- never / current / due / overdue
- No named reviewer has ever attested this agent, or the latest review verifies and is in date, inside the 30-day due window, or past its validity. Nothing here suspends the agent: an overdue review is a posture, and enforcement is a person’s decision that is itself audited.
- stale
- The review verifies and the live configuration digest no longer matches the one attested. Any edit to a governance-bearing field, or to a role the agent references, produces this immediately.
- invalid
- The stored snapshot’s canonical digest no longer matches its recorded digest, the record digest no longer matches the reviewer identity and times it seals, or the audit binding does not hold. Never presented as current.
- The audit diff
- An edit names which of fourteen diffed fields moved, alongside the new status and the previous one. Routing and the on-behalf-of requirement are audited as an edit without being named in that list; the recertification digest, which does cover both, is what catches them.
Retrieving it a year later, and the ceiling on what you can ask
Search takes the question in English and turns it into a validated filter object with fourteen allow-listed fields — never into SQL. That is a written decision with its alternatives recorded beside it: natural language to SQL is an interpreter input generated from untrusted text, and trace content is attacker-influenced by construction, since the events table holds prompts, tool arguments and tool results some of which were written by an external party who wanted them read. A read-only database user and a SQL parser narrow that blast radius without closing it; a filter object closes it, because there is no interpreter for a payload to reach.
The cost of that choice is named in the same place as the benefit. Misinterpretation replaces injection as the main failure mode: the model will confidently return a filter meaning something slightly different from the question, and the explanation shown beside the results is the only mitigation and is advisory. So the response carries the interpreted filter, a plain-English explanation naming every field that was set, and whether a model or the deterministic keyword parser produced it, and the console renders each field as an editable chip written into the URL — which makes an interpretation a link you can hand to somebody else.
The expressiveness ceiling is low and deliberate. There is no aggregation, no grouping and no cross-trace correlation, so which agents used the same card number twice is not a question you can ask, and adding a question shape is a schema, validator and query-builder change rather than a prompt change. When no translation model is configured or the call fails, the keyword parser answers instead and the response says which path ran — it is materially worse and it degrades quietly, falling through to an over-broad full-text term rather than raising an error, which returns too much rather than nothing.
Reading the evidence is itself an act, and it is recorded as one. Listing, searching, opening and exporting each append an audit entry naming the actor and the thing acted on: the list and search entries carry the interpreted filter, the teams the account was effectively authorised for and the row count, and the export entry carries the digest of the bundle it issued. Evidence reads are scoped by team, derived from the signed-in account rather than accepted from the request, and the same check is repeated on the detail page and on the export, because scoping the list and leaving the detail URL open is the usual way this goes wrong.
What this still does not solve
Doing everything above leaves a residue. It is smaller than what you started with and it is not nothing, and knowing its shape in advance is the difference between a control you trust correctly and one you trust too much.
- A delegation chain and an on-behalf-of principal are asserted rather than proven. Both arrive as request headers with no signed claim behind them; the intersection is what makes that acceptable, since a forged chain can only add links and every link must allow — but where a caller authenticates with an exchanged workload capability the header is refused outright rather than recorded as evidence.
- Under the default configuration the audit chain is unkeyed, so an operator with write access can rewrite an entry and recompute every downstream hash, at which point verification reports valid. That is tamper-evidence against alteration that does not also recompute, and it is not what the word valid invites a reader to assume.
- The search cannot aggregate, group or correlate across traces, so a question about patterns across a population is not expressible and a new question shape is a code change. The keyword fallback degrades quietly, returning too much rather than nothing.
- Retention and erasure act on the live primary database only. A restored pre-erasure backup can resurrect erased traces and can lose the audit row that recorded the erasure, and approvals, discovery findings and webhook delivery rows are not purged with the traces they relate to.
If one of those residues is the thing that actually worries you, that is the conversation worth having rather than the one about the steps above it.
Talk it throughThe parts of the platform this uses
Flight recorder
Every governed request in a timeline a compliance officer can read, and a search box that never writes SQL.
The filter cannot group, count or correlate across traces
Agent registry
One record per agent, and it is the record the gateway enforces against.
An overdue review never suspends the agent itself
Agent permissions
Deny by default, explicit deny wins, and delegation intersects — so an agent cannot borrow authority it was never granted.
Off by default; only enforce refuses anything
Audit chain
Every administrative act hash-chained; seal it under a key held off the box, and anchor it with a signature your auditor can check alone.
Unkeyed, a rewrite that re-hashes everything verifies clean
The other jobs
Why does editing a role make an agent’s review stale?
Because the review attests what the agent could actually do, not which identifiers it referenced. The snapshot binds each role’s name, permission count and a SHA-256 over its normalised permissions, so widening a role changes the live digest and marks every agent that references it stale on the next read. The alternative — binding role ids only — would let a reviewer’s name sit beside a permission set they never saw, which is the defining failure of access recertification rather than a detail of it. Ordering is normalised first, so reordering tags or actions grants no different authority and produces no false staleness.
Does an expired recertification stop the agent?
No, and that is a deliberate limit rather than an oversight. Token Observe derives and exposes the posture — never, current, due, overdue, stale or invalid — in the console and in a paginated fleet register, and it does not run a notification scheduler and does not auto-suspend. Automatic suspension would turn a compliance calendar into an availability control, which needs an explicit per-install grace period, a named escalation owner and a dry-run path rather than a surprising default. The lifecycle API is the enforcement action, taken by a person whose decision is audited.
Can we prove the record was not edited after the fact?
To a stated degree, and the degree is the answer rather than a caveat. Governance-plane changes are hash-chained, so an edit or a deletion breaks verification at a named sequence number — but under the default unkeyed configuration a writer who recomputes every downstream hash produces a chain that verifies clean, and the repository ships a forgery test asserting exactly that. Configure an audit MAC key from a secret manager the database administrator cannot read and the rewrite needs the key too. Retain an Ed25519 anchor off the box and any rewrite made after you took that copy is contradicted by it.
What can the record say about an agent acting for a named person?
The on-behalf-of value is recorded on the trace in every configuration, which is what makes a request attributable. Whether it also narrowed authority depends on a setting with three positions: off resolves no principal at all, shadow computes the intersection and records what it would have refused while the request proceeds, and only enforce appends the roles mapped from that person’s directory groups as the last link of the delegation chain. The groups are a snapshot from their last single sign-on rather than a live directory read, bounded to 24 hours by default, after which the request is refused rather than decided on stale evidence.
How far back can we answer this question?
As far back as your retention window, which is unset by default and unset means keep forever — so a deployment with a storage-limitation duty has to set one deliberately. Once set, an hourly pass ages traces out in batches of 250, each in its own short transaction so a purge interleaves with gateway traffic. The audit chain is a separate table with no foreign key to the traces, so a purge or a subject erasure leaves chain verification passing and the record that a deletion happened outlives the deleted data, with the erasure entry carrying a digest of the subject identifier rather than the identifier.
Describe the version of this you actually have.
The steps above are the general shape. Which of them matter, and in what order, depends on what your agents do and which of them worries you — say that and you will get a straight answer, including when the answer is that Token Observe is not what you need for it.
no form · no qualification step · no sales desk · the other three ways in