Skip to main content
Before you start
  • You can read this only on the machine Postern runs on. From your own computer, forward the port first, then open http://localhost:8787:
  • It grows forever. Nothing in Postern deletes a row and there is no retention job. You must trim it, back it up and provide the disk yourself.
  • There is nothing to switch on. The migrations create the audit_log table at first boot, and the writer starts with Postern. No setting turns it off.
Three names, one thing. The record is the history of agent calls. The ledger is the Console screen that shows it. audit_log is the table that holds it. You need no agent key to read it: whether you can reach this computer is the whole of the authorization. The record tells you what your agents did. It does not tell you what you did to your agents. Six acts leave no row at all: create a key, rotate one, edit its sectors or its deadline, revoke it, delete it, and set a connection’s sync window. Postern records everything else on this page. And nothing here stops anything: the record is a witness, never a guard. Two more pages carry the rest of the record. Read the record is the ledger screen and GET /api/audit — the filters, the paging and Export CSV. What the record cannot promise is how far it can be trusted — what is dropped before it lands, what nothing in the database prevents, and the five questions it cannot answer.

What one row holds

One row is one recorded call. Postern writes rows in batches with a single INSERT. It binds every value as a parameter, so no argument an agent sent ever reaches SQL as text. One index: (user_id, created_at desc). created_at is when the row was inserted, not when the call happened. Postern flushes every 1000 ms by default, so a stamp trails its call by up to that much. Every row in one flush carries the same instant, and under load the gap widens. Nothing records the call’s own time.

What gets a row

Agent calls — all seven tools. describe_context, get_schema, query, get_record, fetch_live, list_actions, invoke_action. The agent port writes a row for every one, served or refused. The REST read mirror carries the five reads, and writes the same row for those. The agent key is never in any field, on any branch. That covers a sector refusal on both surfaces. A refused key is the one asymmetry, and what does not get a row has it. A refusal writes decision: deny with the reason. A call Postern allowed, which then failed at the work, writes decision: allow with outcome: error. The Console paints that third state as Errored. Console actions. These carry agent_id of operator, always write decision: allow, and carry outcome of ok or error. The handshake and the bridge.
  • initialize — Postern writes this only on a refusal, with agent_id null. It always writes a key that was presented and refused. It writes an anonymous probe that carries no key only when the OAuth bridge is on. A successful session start writes nothing; the first tool call is what appears.
  • oauth_token — the bridge issued or refreshed a token for a hosted agent. An allowed row carries { grant: "authorization_code" | "refresh_token", clientId }.
  • ingest — a device pushed records over a webhook. agent_id is null; an unauthorized push writes deny with unauthorized.
Postern caps two kinds of refusal on the agent port, each at 30 a minute. Past the cap it counts them rather than records them. Refused initialize calls get the summary row throttled: <n> initialize denials coalesced. The bridge’s cheap refusals — an unknown client, an address that does not match, a stale form — get throttled: <n> further bridge denials suppressed. Both land at the start of the next window, and only if another refusal arrives. So if the flood stops, the count goes with it. Under a flood, “no row” no longer means “nothing arrived”. A refused agent key is never one of the counted ones. At the sign-in page the bridge records every one, at any rate: authorize: key refused. A key that never expires gets authorize: key has no expiry. A rate you would suppress is the alarm itself.

What does not get a row

Six Console acts write no row at all: create a key, rotate one, edit an agent’s sectors or its deadline, revoke the agent, delete the agent, and set a connection’s sync window. Five of the six are agent-key management — the acts you would most want a record of. Keep your own note.Four of those five end every live session that agent holds: a rotate, an edit to its sectors or its deadline, a revoke and a remove. So an agent can stop mid-work with nothing in the record to say why. What each verb does.
Two traps in that list. The revoke verb above is a source credential, not an agent. And set_sync_interval is a different control from the sync window, and is recorded. Postern does not record reads you make yourself. The Console’s data inspector is POST /api/data/:sector and GET /api/data/:sector/:id. It runs the same query engine an agent runs, as the operator with every sector granted, and writes nothing. Nor do GET /api/operator/context, GET /api/operator/schema, reads of the record itself, or Export CSV. Why reachability here counts as operator trust. On the REST read mirror, a refused key leaves no row. That covers both refusals it can give: a missing or malformed key (401), and an unknown, revoked or expired key (403). The handler resolves the key first and writes the row second, so a key that never resolves fails before the write runs. An ungranted sector is different: the key resolved, so Postern records that refusal. On the agent port, the same refused key does leave a row — against the tool initialize, with the reason under deny_reason, no agent name, and no sector. A reason never travels in the sector column, so a sector filter never shows that row. One surface remembers a bad key and the other does not. The two refusals. The line falls where a call touches a source or changes state, not where it only reads. Postern records an operator fetch_live, because it reaches a source and changes the cache. It records export too, which returns the whole store.
The record is not a list of everything that has read your data. You can read every row of every sector through the Console’s inspector and leave no trace. So can anything else on that machine that can reach the Console port. What the record answers completely is which agent came through and what it asked for. What it cannot answer is who has seen this data.

What the arguments hold

args is the call’s own parameters — the query, the action target and its values — stored as JSON. Two defences run before Postern queues the entry.
  • Field names. Postern replaces any value whose field name contains pass, secret, token, key, authorization or cookie with [redacted], matched without regard to case. It recurses through nested objects and arrays.
  • Size. Past roughly 4 KB serialized, the whole payload collapses to [truncated]. So does a cyclic or non-serializable value.
The honest bound: redaction matches field names, never values. Put a secret in a value under a name the pattern does not match, and Postern records it as it arrived. Secrets are not meant to travel in tool arguments. This list is a second line, not the first. The record holds the act, never the row values. result_count is a count; what a read returned is not in the table. The Console renders args two ways. The table drops fields whose value is null. The row detail panel prints them whole under Args (redacted, verbatim), and Export CSV carries them exactly as recorded.

What you have now

A record of every agent call and of the Console verbs listed above. It does not cover agent-key management, and it does not cover reads you make yourself.

Next

Read the record

the ledger screen and GET /api/audit — filters, paging, and Export CSV

What the record cannot promise

what is dropped before it lands, and what nothing in the database prevents

Connect an agent

a running Postern and an agent key · a few minutes, plus a restart of your client · a key shown once

Grants and sectors

what a grant covers, and how to revoke, rotate or expire one