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_logtable at first boot, and the writer starts with Postern. No setting turns it off.
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 singleINSERT. 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, withagent_idnull. 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_idis null; an unauthorized push writesdenywithunauthorized.
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
Two traps in that list. Therevoke 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.
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,authorizationorcookiewith[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.
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 CSVWhat 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