# Four questions an agent audit log must answer

> After an agent changes something nobody meant to change, an agent audit log has four questions to answer: which account, which actor, which rule was in force, and what else was tried.

**TL;DR** A useful trail names the account actually reached, records whether the actor was a person or an AI as a field rather than a guess, shows which rule was in force when the call ran, and includes the attempts that were refused. Elaichi writes one entry per tool-call attempt, succeeded or failed, with argument names and counts but never argument values. If one person runs one assistant against one app, that app's own log already answers all four.

## The morning after an unexpected write

A finance analyst asks an assistant to clean up duplicate vendor records. An hour later a record that was not a duplicate is gone. The question landing on IT is narrow and urgent: what happened, and can it happen again before Friday. An agent audit log either answers that in the next ten minutes or it does not.

Most logs do not, because they were built to show that a system is being monitored rather than to reconstruct one specific event. The test is not how many fields a record carries. It is whether four questions can be answered from the trail alone, without opening the assistant's chat history, without asking the analyst to remember her prompt, and without correlating two systems by timestamp.

The four are: which account did the call actually reach, who or what made it, was it permitted at the moment it ran, and what else did the same actor try. A log either records the field that answers each one at the time of the call or it does not. None of them can be reconstructed afterward from a timestamp and a user agent string.

## Question one: which account did the call actually reach

Most teams run more than one account per connector. Two Notion workspaces, one for the handbook and one for customer notes. A sandbox Stripe and a live Stripe. A shared Google Workspace account for the support alias and a personal one for each rep. A record that names only the connector answers nothing, because the connector is identical in the case you are worried about and the case you are not.

Elaichi writes one entry per tool-call attempt, succeeded or failed, and both name the account. The recorded connection is the account actually reached, taken from the execution rather than from the intent. That distinction is the whole value of the field. Intent is what the model asked for, and the model is exactly the component whose choices you are investigating.

It holds through the indirection too. Past a threshold of 30 tools, counted across control-plane operations and connected tools together, the connected half collapses behind `search_tools` and `execute_tool`. One connected app is normally enough to cross that line, so collapse is the usual case rather than an edge case. `execute_tool` is only a naming indirection: it unwraps to the same name and arguments and falls through the identical gates. The entry reads the same whether the model named the tool directly or went through the meta-tool, so you never have to work out which calling style produced a given row.

Account selection is also a place where ranking can go wrong quietly. In tool search, the description tokens `set`, `connection`, `frozen` and `more` are dropped from scoring, because a query containing "connection" once gave every merged tool the same score, the tie broke on name order, and the model was handed an arbitrary account. Account labels stay scorable on purpose. The fix reduces the failure rate. The log is what tells you it happened at all.

## Question two: who or what performed the action

Plenty of logging systems infer agency after the fact, usually from a user agent. That fails in the exact case you care about, because an AI client and a human in a console can present the same transport, and a user agent is something the caller sets.

In Elaichi, `actor_kind` is a field, not an inference. Its values include `user`, `system`, `staff`, `scim`, `api_token` and `ai_assistant`, and it is written at the point of action. Whether an AI took a step is a recorded fact you can filter on, count and report, rather than a pattern match on client strings.

Two details save time when the review runs weeks later. Actor names resolve server-side, and a member who has since left renders as "Former member" instead of dropping out of the trail or showing a bare identifier, so departures do not quietly blank out history. And when Elaichi staff act inside a customer workspace under impersonation, the action is attributed to staff in the customer's own audit log rather than appearing as the customer. The [security overview](/security/) covers the rest of that surface.

## Question three: was the call permitted at the moment it ran

Each record carries the operation and tool, the connection, the classification, whether the call was approved, the outcome, and an error code on failure. Enforcement itself happens at four points against the same resolver: browse, connect, advertise and execute, plus a final check on the fully substituted outbound URL. A call that appears in the trail as executed passed all of them.

That answers whether a call was inside the rules, which is a different question from whether it is inside the rules now. Role membership and restrictions resolve through a 60 second cache plus edge propagation, so a role change or a restriction change takes effect within about two minutes, on every surface: MCP, console and REST alike. A call that ran ninety seconds after you tightened a restriction may legitimately have run under the old rule. The trail is how you tell a rule that failed from a rule that had not landed yet.

Grant revocation is the exception worth memorizing. `revoked_at` is re-read from the org store on every single call with no cache, and removing or suspending a member revokes every live grant in the same transaction as the membership change. For grant revocation, member removal and suspension, and only those, the next call is already blocked.

## Question four: what the same actor tried that did not run

The successful call tells you what changed. The blocked calls tell you what the agent was reaching for, and that is usually the more useful half of a review. Because an entry is written per attempt, a run of denials against the same tool by the same person is visible as a pattern. It reads two ways and both are actionable: either the restriction is drawn in the wrong place and somebody is blocked from work they are meant to do, or an assistant is repeatedly probing an operation nobody intended it to have. A cluster of refusals against one connector at 2am is a different incident from a single successful write, and you want to tell them apart before you start interviewing people.

One trap shows up here often enough to name. The allowlist stage engages on the presence of an allow rule, not on its contents, so an allow rule that names nothing denies everything. It is the strictest thing you can express, it is easy to create by accident while editing, and in the trail it looks like one member whose every call fails at once. Ideally you find it there before the support team finds it as a wall of broken workflows.

Tools withheld by a restriction are excluded from the discovery count entirely. They were advertised to nobody, so they do not generate denial noise from a model idly trying them.

## What you change once you have the answer

Two speeds apply, and mixing them up leads to people declaring an incident closed while calls are still landing. If calls have to stop now, suspend or remove the member, or revoke the grant. If the fix is a narrower rule, write the restriction against a role or a user, then wait out about two minutes before testing it. Restrictions target a role or a user; the organization default is the absence of any rule.

For the durable fix, a restriction is often the wrong instrument and a frozen parameter is the right one. A frozen entry pins values over a tool's flattened argument space. The frozen keys are stripped from the advertised schema, so the model never sees them, and the frozen values are merged over caller arguments at execution, so passing the key cannot un-freeze it. Precedence runs entry defaults, then caller or model arguments, then frozen parameters. That is how an agent keeps a tool it needs while losing the ability to set the one field that caused the incident.

## What an agent audit log refuses to record

Argument names and counts are logged. Argument values never are. The trade cuts against you in one direction: you cannot reconstruct the payload of a write from the Elaichi trail, and if you need the body you fetch it from the third party's own log using the account and timestamp the trail gave you. What you gain is that a record readable by everyone with `audit:view` never becomes a second copy of the data the agent touched.

The same reasoning produces the error-text firewall. Two error strings exist per failed call. The one returned to the caller is derived from the third party's response body and is never written anywhere else. The one written to the audit trail is never derived from the request or the response, because audit records are org-visible, readable by the in-product assistant, and fanned out to whatever SIEM the customer configured. A remote error body reaching one of those is third-party payload leaving the system through the log pipe.

Promoted metadata is a fixed allowlist rather than flattening whatever keys arrive, since metadata keys can be user-influenced and unbounded flattening would let one org's traffic grow the field namespace for its whole tenant. There is one log tenant per organization, enforced in the type system rather than by a WHERE clause: a dropped WHERE clause leaks, a wrong tenant returns nothing. Customer-visible audit and internal application logs are separate tenants, which matters more than usual because the in-product assistant can read the audit log.

## Reading the trail, and getting it out

Audit events and application logs share one record shape, which is what lets a single query answer "what happened" instead of correlating two systems by eye. The trail is append-only, newest-first, cursor-paginated and filterable by free text, category, actor, action kind and time. The organization's region, `eu`, `us` or `apac`, also selects which regional log instance it lands in.

A compliance reviewer does not need a paid seat to read it. Auditor is a free seat class alongside Guest and Billing Admin, it carries `audit:view`, and it lacks `tool:execute`, so a reviewer can read everything and call nothing through the endpoint.

For export, forwarding to a customer destination is filterable by log type, where an absent filter forwards everything and an empty list forwards nothing. Read that twice before you configure it. Datadog is implemented today. Splunk HEC and Microsoft Sentinel are accepted but not yet delivering, so plan around Datadog or the in-product view if you need this now.

## Where the trail stays thinner than you want

The audit trail is eventually consistent. A row may take a moment to appear, so a missing entry ten seconds after a call is not evidence that the call did not happen.

Organization deletion tears down the rest of the workspace but has no path to purge the organization's log tenant, and the response returns that residue by name rather than pretending otherwise. If your retention policy requires removal of the trail itself, plan for that conversation early.

And the trail cannot tell you why the model decided to make the call. The prompt-injection write gate in the Elaichi agent window does not apply to `POST /mcp` and cannot, because an MCP server never sees a user prompt. What holds on the endpoint is RBAC per operation, the `forbidden` classification, output redaction, OAuth scope limits and the logging described here. Against injection specifically, the log is a detective control. It tells you an agent was talked into something. It does not stop the first one.

## When your existing logs already answer all four

If one person connects one app to one assistant, you do not need any of this. The app's own audit log names the OAuth application and the record that changed, the actor is unambiguous because there is one, and there is no second account to confuse with the first. Adding a control plane to that buys a fifth copy of a log you already have and costs a seat. The honest version of that argument is laid out in [the case against putting a gateway in front of two people](/blog/when-you-dont-need-a-gateway/).

The four questions get hard at a specific point: when the same connector holds more than one account, when more than a handful of people share the endpoint, or when a reviewer needs to read across every app without holding a license in each one. Past that, every call arrives at the third party from the same OAuth application, and you are correlating admin logs by timestamp. Timestamps do not tell you which workspace the write landed in. The same arithmetic decides [whether to keep operating MCP servers yourself](/blog/running-your-own-mcp-servers/), where credentials, revocation and the trail are the expensive parts rather than the servers.

If you are setting the rules this trail records, the rest of [the governance writing](/blog/category/governance/) covers restrictions, frozen arguments and offboarding, and the [company-wide endpoint with SSO](/blog/mcp-server-with-sso/) walkthrough covers how access gets assigned before any of it becomes an incident. If you are still deciding which accounts belong behind the endpoint, start with 400+ connectors in the [catalog](/connectors/) and the [team-by-team breakdown](/use-cases/) of who tends to need what.

## FAQ

### What should an agent audit log record for every tool call?

At minimum: the operation and tool, the account actually reached rather than the one requested, whether the actor was a person, a system process or an AI assistant, the tool's classification, whether the call was approved, the outcome, and an error code on failure. Elaichi records these once per tool-call attempt, succeeded or failed. Argument names and counts are recorded; argument values are not, so the trail never becomes a second copy of the data the agent touched.

### How do you tell whether an action was taken by a person or by an AI assistant?

By reading a recorded field rather than inferring it. In Elaichi, actor_kind is written at the point of action and its values include user, system, staff, scim, api_token and ai_assistant. Inferring agency afterward from a user agent is unreliable, because the caller sets the user agent and a human console session and an AI client can present the same transport.

### Does the audit log show which of two accounts of the same app was used?

It does when the connection is taken from the execution rather than from the request. Elaichi records the account actually reached, which is what answers "which of my two Notion workspaces did the agent write to" instead of recording the ambiguous intent the model expressed. Both successful and failed attempts name the account.

### If I change a restriction after an incident, when does it take effect?

Within about two minutes. Role membership and restrictions resolve through a 60 second cache plus edge propagation, on the MCP endpoint, the console and the REST API alike, so wait that out before testing. Grant revocation, member removal and suspension behave differently: the revocation flag is re-read on every call with no cache and removal revokes live grants in the same transaction, so those are effective on the next call.

### Do compliance reviewers need a paid seat to read the audit trail?

No. Auditor is a free seat class in Elaichi, alongside Guest and Billing Admin. It carries audit:view and lacks tool:execute, so a reviewer can read the full append-only trail, filter it by free text, category, actor, action kind and time, and cannot call any tool through the MCP endpoint.

## Read next

- [Running an MCP server with SSO for a whole company](/blog/mcp-server-with-sso/) — An MCP server with SSO answers who is calling. It does not answer what they may reach, and the gap between those two questions is where most rollouts get stuck.
- [Do you need an MCP gateway? Often, no](/blog/when-you-dont-need-a-gateway/) — Do you need an MCP gateway? Four tests that say no, one that says not yet, and the three events that change the answer. Written by a vendor, on purpose.
- [Running your own MCP servers, or not](/blog/running-your-own-mcp-servers/) — Running your own MCP servers is cheap for one team and expensive for a company. The line is not the servers, it is credentials, revocation and the audit trail.
