Docs/Lanes Link/Built-ins/Audit log

Audit log

Every capability call, allowed or refused, in a hash-chained append-only record. What an entry keeps, what it withholds, and how to verify nothing was altered.

Every capability invocation is recorded. Allowed, denied, rate-limited, or failed: one event each, written by the dispatcher as the call goes through it.

The log is the answer to "what did my agents actually do", and it is the reason a single endpoint in front of your accounts is worth more than the same accounts wired up separately. Nothing else in the system knows the whole picture.

Reading it

console
$ lanes link audit tail --limit 25 --profile personal --workspace local
$ lanes link audit tail --denied-only --format md --profile personal --workspace local

--denied-only is usually the interesting half. A refusal is the permission system working, and a run of them tells you either that an agent is reaching for something you meant to withhold, or that you narrowed something further than you meant to.

Verifying it

Each record carries the hash of the one before it, so the log is append-only in a way you can check rather than trust:

console
$ lanes link audit verify --profile personal --workspace local

An edit shows as a hash mismatch, a removal as a sequence gap, and a truncated run as a count that disagrees with its marker. It exits non-zero on a break.

What an entry keeps

The rule across every provider is the same: keep the shape, withhold the words.

Each provider declares which of its arguments are identifiers and which are content, because an audit log that reproduced the content would be a second copy of the thing it was meant to make accountable. A message id is kept. The message is not.

Three consequences worth stating outright:

A search query is withheld everywhere. gmail.users.messages.list records nothing at all, not even q. A search term is frequently more revealing than its result, because it says what somebody was looking for. The same reasoning covers memory.search, tasks.list, and icloud_contacts.search_contacts.

Withholding is the default, not the exception. A provider that declares no redaction rules gets every argument value withheld. That is why a vendor-run MCP server gives you a thinner record than Gmail does: nobody authored a rule list for capabilities nobody here wrote, so nothing is assumed safe to keep.

Length can be a disclosure. A vault value is recorded as <withheld> rather than <string:40>, because the length of a secret is itself worth something.

Each provider page says what its own calls keep and withhold.

The six results

allowedit ran
denied_by_policya rule you wrote refused it
denied_defaultnothing granted it; absent policy grants nothing
denied_unauthenticatedno valid caller
denied_rate_limitedtoo many, too fast
denied_connection_unauthorizedthe capability was allowed, that connection was not

What is not audited

Two gaps, documented rather than glossed over. Both are also rows in the security model.

Failed authentication is logged, not audited. A refusal record needs a principal, and failing authentication is precisely not having one. A rejected credential goes to the endpoint's operational log instead, naming the reason and never the value presented.

Arguments that fail schema validation. A call to an advertised tool whose arguments are rejected is stopped by the protocol layer before dispatch runs. Nothing is invoked, the caller gets a clear error, and no row is written.

A third, narrower one: a client speaking the 2025-era protocol can enumerate which capabilities exist without leaving a refusal trace. It is not reachable unauthenticated and nothing is invoked either way. Security model has the full accounting.

Where it lives

With the workspace, alongside the other stores. It is never moved into a repository by lanes link knowledge use github, and never will be: a log you can rewrite by force-pushing is not a log.


Next: the security model for the guarantee table, or Capabilities for why each capability takes the shape it does.