Pre-release. v0.1 is not out yet, so there is nothing to install and no public source to clone — the quickstart builds from a checkout.
The audit ledger
Every authorisation decision is appended to the ledger before the answer goes out — every allow,
and every denial that names an agent, a person, a task or a token. A denial that names nobody is
written once per reason per window and the rest are counted into one summary record carrying
count (Onbe:Audit:Aggregation:*), so an anonymous flood cannot fill a table that refuses
DELETE.
A record
Section titled “A record”{ "seq": 10428, "chain": 6, "ts": "2026-09-09T14:03:41.882Z", "event": "token.issued", "task_id": "task_01HQZX9K4M", "agent_id": "jira-triage", "sponsor": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "audience": "https://jira.internal", "scope": "jira:read jira:comment", "jti": "tok_01HQZX9K5P", "delegation_depth": 1, "decision": "allow", "reason": null, "prev_hash": "9c1f…a20b", "hash": "4e77…c913"}sponsor is the human the whole task is being run for. It is the field you filter on when
someone asks what any agent did on behalf of a person.
chain is which of the ledger’s chains the record sits on. It is absent on records written
before the ledger was sharded.
Events written by the control plane: token.issued, token.denied, token.refreshed,
token.revoked, task.created, task.revoked, task.expired, agent.registered,
agent.updated, agent.deleted and admin.denied.
tool.called is reserved and not produced. The specification marks it that way because there is
no endpoint to report a tool call to and no SDK that sends one; if that lands, it will be
opt-in, non-blocking and off the request path, because a tool server that cannot reach the
control plane must still answer. What a tool server records today, it records in its own log —
see the log line.
The chain
Section titled “The chain”hash = sha256(canonical_json(record minus hash) || prev_hash)Each record hashes its own contents together with the hash of the record before it in its chain;
the record that opens a chain has prev_hash: null. Change one record and its hash changes;
leave the stored hash alone and that record no longer hashes to it; recompute it and the next
record’s prev_hash no longer matches. There is no edit that does not show. chain and count
sit outside the hashed JSON, so a ledger written before either existed still verifies.
The ledger is a fixed number of chains — Onbe:Audit:Chains, sixteen by default, one at the
least — each with its own transaction-scoped advisory lock, so any number of instances write
coherent chains and writers on different chains do not queue behind each other. A record is
placed by the human it names, so one person’s history is one provable chain; one naming no human
goes by the agent, and one naming neither is spread. The table refuses UPDATE, DELETE and
TRUNCATE by trigger.
Sequence numbers may have holes. A rolled-back append consumes a number without touching the chain, so a gap is normal and is not evidence of anything.
What audit-verify proves
Section titled “What audit-verify proves”Onbe.Server audit-verifyAudit ledger intact: 621 record(s) verified across 3 chain(s), heads 609:2b8d0e4c…,617:9c1f6a30…,621:484fa8a9….It walks every chain from its first record and recomputes every link, reporting the sequence
number and kind of the first fault: a broken link — a record missing, inserted or replaced — or
a record that no longer hashes to its stored hash. It exits 0 when every chain is intact, 3
when one is not, 1 when the ledger could not be read, and 2 for a malformed head argument.
It cannot tell a cut tail from a short ledger. If somebody with write access removes the
last hundred records, what remains is a perfectly valid chain that is simply shorter. Nothing
inside the database can detect that, which is why each run prints the head of every chain as a
comma-separated seq:hash list:
Onbe.Server audit-verify 609:2b8d0e4c…,617:9c1f6a30…,621:484fa8a9…Pass that list back and the command also confirms each chain still reaches its head unchanged. Keep the list somewhere the database cannot reach — a different system, a chat channel, a printed page. That is the part that makes the chain worth having; without it you are trusting the same storage to hold both the evidence and the proof of the evidence.
Reading it back
Section titled “Reading it back”GET /audit?sponsor=f47ac10b-…&from=2026-09-01&to=2026-09-30Authorization: Bearer <admin api key>Filters, all optional and combined with and: sponsor, agent_id, task_id, from
(inclusive), to (exclusive) and decision. A time is an ISO 8601 timestamp or a bare date
meaning midnight UTC; a bare date in to covers that whole day, so the example above is all of
September.
Records come back oldest first, limit per page (default 100, at most 1000), with an opaque
next_cursor to pass back as cursor. The sponsor and agent filters are served from dedicated
indexes, so a page of one person’s month is one index range in page order, never a scan.
Copies
Section titled “Copies”The ledger is the record; a sink is a copy. Set Onbe:Audit:Sink:Url and every append is also
queued and posted to that URL in batches, in sequence order. A slow, failing or absent sink
changes nothing about what the token endpoint answers — the endpoint only queues.
Delivery is at least once, and batches from several instances can arrive out of order, so a sink
should treat seq as the identity and order by it.
© 2026 Onbe