Events and the audit log
Last updated October 5, 2026
On this page
- What is recorded
- The Events page
- Filtering by type
- Following one run, task or thread
- In the workspace
- From the command line
- ok log
- The tamper-evident chain
- What the chain proves, and what it does not
- Checkpoints
- Anchors: a hash kept elsewhere
- When the chain breaks
- Restores and backups
- Verifying the chain
- With ok audit verify
- In Governance, Audit
- A log from before the chain
- Not possible yet
- Known problems on this page
- For developers
- API
- Audit events
- Break kinds
Draft for review
Everything that happens in OrchKernel is written to one append-only event log: every policy decision, every write, every approval, every step of an agent's run and every tool it calls. The log is what the Events page shows, what a run's replay is built from, and the company's audit record.
Each event is also chained to the one before it by a hash (a fingerprint of the event's content), so a row that is later edited, deleted or slipped in by hand can be found. This page explains what is recorded, how to find and replay the events of one run, and how to check the chain. By the end you can find every event of the demo refund, replay it, verify the log and see a checkpoint.
All of it works in the demo without a model key: with no model configured, the demo's stub planner answers instead of a model, and it knows the refund ask from the Quickstart. The examples below come from a fresh demo company after two steps of the Quickstart: maya's refund ask approved by ada ("Approve the parked refund as Ada"), and, where noted, alice's follow-up email ("Watch a governed write wait"). Event numbers in your demo match if you do the same steps in the same order; hashes, ids and times differ.
This page describes the event log as it works today on the golive branch.
What is not possible yet, and known problems, are listed
at the end.
What is recorded#
Every event has a number (#403), a time, the person or agent responsible
(or kernel when the system itself did it), and, where they apply, the run,
task and thread it belongs to. The product never changes or removes an
event; it adds a new one instead.
| Area in the Type filter | What is recorded |
|---|---|
| Runs | A run starting, each step, pausing for approval, resuming, finishing or being stopped; each tool and model call; questions an agent asks and their answers |
| Work | Tasks being created, assigned, changing state or escalated; triggers firing or being dropped |
| Threads | Threads, messages, artifact versions, decisions, people joining or leaving, threads closing |
| Approvals and policy | Every decision of the gate, approvals asked and decided, kill switches, rule and budget changes |
| Delegation | Delegations created, revoked or refused |
| Data | Records written, rolled back, flagged or purged; schema changes; calls to other systems |
| Knowledge | Knowledge proposed, accepted, rejected or contested |
| People, agents and modules | People and agents added or offboarded, modules installed, changes to skills and playbooks, connections |
| Sign-in and outside agents | Tokens issued, narrowed or revoked; outside agents' sessions and actions |
| Audit | The chain's own records: checkpoints, breaks, lost events, low disk |
Two things worth knowing:
- The gate's decision is written first. The gate is the check every action passes before it runs (see How the gate decides). Every step gets a Policy decision event (allowed, needs approval or denied) before it runs, even if the step then fails. If the gate cannot write its decision (a full disk, a failing file), it refuses the action rather than act unrecorded.
- Provenance is on both sides. The event says who did what, in which run, task and thread; each version of a record in the brain also names who wrote it, who it acted for, the run, the skill and the source (see The brain).
The Events page#
Click Events in the sidebar, under Company. The page lists events newest first, one line each, and adds new events at the top as they happen, without a reload.
What you see depends on who you are:
| You are | You see |
|---|---|
| An admin (ada) | Every event, and to the right of the Type filter the state of the audit chain. |
| Anyone else (maya, alice, ...) | Events you did yourself, and events of runs, tasks and threads you can see. No chain line. |
In the demo, after the refund, maya sees 37 events: the 6 ticket writes she made when the demo was set up, and the 31 events of her conversation, her task and the watcher's run. alice sees only the 10 events of the records she wrote at setup.
Each line has three parts: the time, the kind of event, and a sentence saying what happened. Some examples from the demo:
| Kind | Sentence |
|---|---|
| Policy decision | Churn-risk watcher: tool.call(support-ops.billing.refund) needs approval |
| Approval requested | Approval asked of Ada Park for tool.call(support-ops.billing.refund) |
| Approval decided | Approval granted by Ada Park |
| Tool called | support-ops.billing.refund called by Churn-risk watcher |
| Record written | Roadmap items checkpoint-test written by Pat Kim |
| Token issued | Ada Park issued a token for Maya Patel · expires Oct 12 |
Some kinds have no sentence yet and repeat their name instead: Run step, Run finished, Model called, Thread created, Message posted and Audit checkpoint. Open the line to see the detail.
Click a line to open it. It shows the event number, the full date and time
with your time zone, who did it, the event's type as stored (tool_called)
and every field it carries:
#398 · Monday, October 5, 2026 at 5:27:27 PM GMT+5:30 · by Churn-risk watcher · tool_called
{ "by": "churn-watcher", "cost_cents": 0, "ok": true, "tool": "support-ops.billing.refund" }
Filtering by type#
The page has one filter, Type. It lists every kind of event, grouped by the areas in the table above. A kind the list does not know yet shows under Other.
- Sign in as ada and click Events.
- In Type, choose Tool called. After the refund you see two lines: "support-ops.billing.refund called by Churn-risk watcher" and "support-ops.billing.account called by Churn-risk watcher". If you also did alice's email step, a third line reads "gmail.send called by Sales Assistant".
- Choose Approval requested: "Approval asked of Ada Park for tool.call(support-ops.billing.refund)".
- Choose Audit checkpoint. Before the first checkpoint the page says "No audit checkpoint events."
- Choose All types to go back.
The page shows the newest 200 events of the type chosen. It cannot filter by person, run, task, thread or date; to follow one run, use the replay below.
Following one run, task or thread#
In the workspace#
The run panel rebuilds a run from the log.
- Sign in as ada and click Work, then the Runs tab.
- Click the Churn-risk watcher run. A panel opens with "Churn-risk
watcher acting for Maya Patel", the run's Trace and its Output.
The address bar now ends in
?run=<run id>; keep that id for the command line. - Open Replay (raw events) at the bottom: one line per event, with
number, who, type and fields. For the refund you see 23 lines, from
run_started(#378) torun_finished(#402). The refund'spolicy_evaluated(#391) saysrequire_approvalwith the reasonrule support-ops-refunds-need-admin: ..., followed byapproval_requested,run_parked, andapproval_resolvedbyada.
Replay (raw events) is shown to admins only. maya can open the same run from her Runs tab and see its Trace and Output, but not the replay. To see the events, she opens the task instead:
- Sign in as maya and click Work. The task is done, so the list of open tasks is empty ("Open tasks (0) · 1 done").
- Choose All, then Details on "Northwind wants a refund for the double charge".
- Its Events section lists the task's events, each with its number,
time, who, type and fields, starting with
task_created(#377).
From the command line#
ok replay prints the events of one run, task or thread, oldest first.
It reads the log; it does not run anything again.
ok --state <path/to/state.db> replay --run <run id>
ok --state <path/to/state.db> replay --task <task id>
ok --state <path/to/state.db> replay --thread <thread id>
ok replay and ok log open the state file as a full kernel and write it
back when they finish. Do not run them on the file a running server is
using: stop the server first, or work on a copy. ok backup is safe while
the server runs. In the directory where you ran ok demo:
-
Make a copy:
ok --state orchkernel-demo/state.db backup copy.db. You seebacked up orchkernel-demo/state.db to copy.db. -
Replay the run, with the id from the run panel's address:
ok --state copy.db replay --run 8c46321e-7084-49b3-b32a-14c7510e655c(your id differs). You see the same 23 lines as Replay (raw events), for example:#391 churn-watcher policy_evaluated {"action":"tool.call(support-ops.billing.refund)", ... "decision":"require_approval", ...} #392 churn-watcher approval_requested {"action":"tool.call(support-ops.billing.refund)", ... "approver":"ada"} #393 churn-watcher run_parked {...} #395 ada approval_resolved {... "approved":true,"by":"ada"} #396 kernel run_resumed {...} -
Replay the task. Its id is the
taskfield on therun_startedline (or the end of the address,?task=<task id>, after Details). You see 23 lines again, now withtask_createdand the threetask_state_changedlines (running, parked, done), and without the model calls andrun_resumed. -
Replay the thread. Its id is the end of the address when maya has the conversation open (
/threads/<thread id>). You see 11 lines: the thread's creation, maya's message and the policy decisions and approval request of the run (see Known problems).
An id that matches nothing prints nothing.
Run, task and thread do not cover the same events:
| Replay by | Includes | Leaves out |
|---|---|---|
| Run | Every step, policy decision, tool and model call, the approval and the run's end | The task's creation and state changes, the thread's messages |
| Task | The task's creation and state changes, the run's steps, decisions, tool calls and approval | Model calls and Run resumed |
| Thread | The thread's creation, messages, and the policy decisions and approval requests of runs in it | Most of a run: its start, steps, tool and model calls, the approval decision and its end |
To see everything an agent did, replay by run; for the conversation, open the thread (see Threads).
ok log#
ok log prints the newest events of the whole log, 50 by default, oldest
of them first:
ok --state copy.db log # the newest 50 events
ok --state copy.db log --type tool_called # only one type
ok --state copy.db log --limit 200 # the newest 200
Each line is the number, who, the type and the event as JSON. --type
takes one type name as stored (approval_requested, not Approval
requested). The command line reads the file directly: whoever can read the
file sees every event, whatever --as says.
The tamper-evident chain#
Each event row stores the hash of the row before it and a hash of itself.
A row that is changed, removed, renumbered or inserted by hand no longer
links, and ok audit verify names the first place where it does not.
The product itself never changes a row: the database refuses updates,
deletes and unchained inserts. Those guards stop mistakes and old
software, not someone who can edit the file directly; the chain is what
catches that.
What the chain proves, and what it does not#
| The chain shows | The chain does not show |
|---|---|
| That the rows have not changed since they were written | A rewrite of everything after some point by someone who recomputes every hash. Only a hash kept outside the file (an anchor, below) catches that. |
| That no row was removed or inserted | Who wrote a row: someone who can write the file can add a well-formed false row at the end. |
| That the rows belong to this log (each log has its own id) | An event that was never written, for example by a process that crashed first. |
Any other event that cannot be written is counted, and an Events lost event (how many, since when) is written before the next one, so the gap is in the chain. When the disk runs low, the server writes Disk low and tells every admin in their inbox, before the gate has to start refusing actions.
Checkpoints#
While the server runs, it checks the newest part of the log on its own and records a checkpoint: an Audit checkpoint event saying "every event up to #N was verified, and the hash at #N is this". It writes one once 1,000 events have been added since the last checkpoint, or 60 minutes after it, whichever comes first, so even a quiet hour gets one. Before the first checkpoint, the count starts at event #1 and the hour starts when the server starts. Separately, every 24 hours it verifies the whole log and records the result as the last full verify.
To see a checkpoint in the demo without waiting an hour:
-
Bring the log past 1,000 events. Each save of a record is two events (the policy decision and the write), so save one of pat's roadmap items 500 times, with pat's token from the
ok demo serveoutput and the port it printed under Workspace (8080 unless you chose another):TOKEN=<pat's token> URL=http://127.0.0.1:8080/api/brain/collections/roadmap_items/records for i in $(seq 500); do curl -s -o /dev/null -X POST -H "Authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{"id":"checkpoint-test","fields":{"owner":"pat","quarter":"Q3","size":"s","status":"proposed","title":"Checkpoint test"}}' \ "$URL" doneThis creates the roadmap item "Checkpoint test" and saves it 499 more times.
-
A checkpoint is written within half a minute of the log passing #1,000, often while the loop is still running. As ada, the line next to the Type filter on Events changes from "Audit chain: 403 events, not checkpointed yet" to "Audit chain verified through event #1,189 · 10/5/2026, 5:32:05 PM". Your number depends on when the server's clock ticked.
-
In Type, choose Audit checkpoint and open the line: by
kernel, with"events": 1189,"head": "sha256:5306...","through": 1189and"verified_from": 1. -
On the command line,
ok --state orchkernel-demo/state.db audit checkpointslists it:#1190 through #1189 sha256:5306... events 1189 verified from #1 at 2026-10-05T12:02:05.... Before the first checkpoint the command prints nothing.
The ok audit commands are safe beside a running server: they open the
file read-only and never write it.
The intervals are server settings, read when the server starts:
| Setting | Default | What it does |
|---|---|---|
OK_AUDIT_CHECKPOINT_EVENTS |
1000 | Events between checkpoints |
OK_AUDIT_CHECKPOINT_MINUTES |
60 | Longest time between checkpoints |
OK_AUDIT_VERIFY_HOURS |
24 | Hours between full verifies; 0 turns them off |
OK_AUDIT_MIN_FREE_MB |
1024 | Free space, in MB, under which Disk low is raised |
Anchors: a hash kept elsewhere#
Because the checkpoints live in the same file, someone who rewrites the log can rewrite them too. An anchor is an event number and its hash, written down somewhere the file's editor cannot reach: a ticket, a password manager, another system. Verifying against an anchor catches a rewrite that recomputed every hash.
ok --state orchkernel-demo/state.db audit head # the current head: an anchor
ok --state orchkernel-demo/state.db audit checkpoints --anchors > anchors.txt
ok --state orchkernel-demo/state.db audit verify --anchors anchors.txt
ok --state orchkernel-demo/state.db audit verify --anchor 403=sha256:8901...
ok audit head prints the log id, how the chain started, the head
(head #403 sha256:8901...), the last checkpoint and the last full verify
(none until there is one). checkpoints --anchors prints one
<number> sha256:<hash> line per checkpoint, the format verify --anchors
reads. An anchor on the command line is written <number>=sha256:<hash>,
with the full 64-character hash.
We checked this on a copy of the demo: after editing event #398 and
recomputing every hash after it, ok audit verify reported "no break", and
the same command with the head written down before the edit
(--anchor 403=sha256:8901...) reported
BREAK at #403: anchor_mismatch: the stored hash differs from the anchor.
An anchor tells you the log changed at or before its number, not where: the
"last good" line it prints (#402) is just the event before the anchor.
When the chain breaks#
When a checkpoint finds a break instead, the server:
- writes Audit chain broken with the event number and the kind of break;
- puts "The audit chain is broken at #1030: hash_mismatch" in every admin's inbox;
- shows admins a red banner on the Events page: "The audit chain is broken at #1030: hash_mismatch. Investigate before anything is pruned or exported."
The banner stays after later checkpoints, which only check the events after the break. It goes away only when the server's next full verify passes, which can be up to 24 hours later. A passing Verify now does not clear it.
The Events page shows what is stored, edited row included; it does not
mark the edited row. Use ok audit verify to find it.
Restores and backups#
A backup is a state file like any other: verify it before you trust it
(ok --state /backups/state-2026-10-12.db audit verify). A copy made with
ok backup keeps the log id of the file it came from, so anchors taken
from the live file apply to it. See
Self-hosting, backups and upgrades.
Verifying the chain#
With ok audit verify#
-
Run
ok --state orchkernel-demo/state.db audit verify. On an untouched demo after the refund you see:log 98e5d089-... chain audit-chain/1 genesis #0 (started chained) checked #1..#403 (403 events, 0 anchors) in 0.0 s; trust: genesis no break; head #403 sha256:8901...and the exit code is 0. "genesis" is the start of the chain: the check began there, so it trusts nothing it did not check itself.
-
Check only part of the log with
--fromand--to:audit verify --from 350 --to 403. The line now saystrust: stored hash of #349: the range is consistent with itself and with the hash stored at #349, nothing more. Add an anchor at or before the start for a stronger answer. -
Add
--jsonfor the same result as JSON, or--all-breaksto list up to 100 breaks instead of stopping at the first.
When something was changed, the output names the first break and the last
good event. We deleted #300 and edited #398 on a copy. Plain verify stops
at the first break:
checked #1..#403 (300 events, 0 anchors) in 0.0 s; trust: genesis
BREAK at #300: missing: row #300 is absent
last good #299 sha256:49f8...
--all-breaks adds
BREAK at #398: hash_mismatch: the stored body does not hash to the stored hash.
With --from 350 only the edit shows, and a third line says whether the
next event still links to the broken one (one row edited in place) or not
(a rewrite from there on):
BREAK at #398: hash_mismatch: the stored body does not hash to the stored hash
last good #397 sha256:eebb...
next row #399 links to the stored hash of #398 (one row edited in place)
| Exit code | Meaning |
|---|---|
| 0 | No break |
| 1 | A break |
| 2 | The file or the arguments could not be used ("nothere.db does not exist or is not a file", a malformed anchor) |
| 3 | The file is not chained: written before the chain and never opened by a newer version |
In Governance, Audit#
Admins have an Audit tab under Governance. Others do not see the
tab; opening its address (/governance/audit) directly says "The audit log
is for admins."
- Sign in as ada, click Governance, then Audit.
- The log shows the log id, how the chain started ("The chain started
with the log: every event is chained."), and the head, shortened
(
head #403 sha256:8901d2bb3c2daade…), with a reminder to write the head down. - Checks shows "last checkpoint: none yet" (or "through #1189 at 10/5/2026, 5:32:05 PM" once there is one) and "last full verify: not yet run" until the server's first daily verify.
- Press Verify now. The whole log is checked and the result appears below: "Verify: no break", "checked #1..#403 (403 events) · trust: genesis". On a broken log it lists each break with its kind, number and detail, for example "hash_mismatch at #1030: the stored body does not hash to the stored hash (one row edited in place)", and the last good event.
Verify now writes nothing: it does not count as the last full verify, add an event or clear the Events banner.
A log from before the chain#
A state file written before the chain existed is sealed the first time a version with the chain opens it, and an Audit chain started event records how many events were sealed. The Audit tab then reads, for example, "41,207 earlier events sealed at upgrade on 12 October; the chain is strict from #41,208." Sealing proves only that those events have not changed since the upgrade. Every program that opens the file must run the new version, or its events are lost; see Self-hosting, backups and upgrades. A new company, like the demo, starts chained.
Not possible yet#
- Filtering the Events page by person, run, task, thread or date, or
paging past the newest 200 events. Use
ok replay, or the API below. - Recording a full verify from the screen, or clearing the break banner after investigating, other than waiting for the server's daily verify.
- Streaming events to a SIEM (a security monitoring system), redacting them, or pruning old events.
- Changing checkpoint intervals from the screen (server settings only).
Known problems on this page#
- A thread's replay misses its runs. Runs in a thread write their
start, steps, tool calls, approval decision and end without the thread's
id, so
ok replay --thread(and the API'sthreadfilter) shows 11 of the 31 events of maya's refund conversation. Replay the run instead. ok logandok replaywrite the state file. They only read, but they save the file when they finish (a copy's checksum changes afterok log --limit 1), so beside a running server they can write over what the server saved. Use a copy made withok backup.- One unreadable event empties the Events page. If a row was edited so that it no longer reads as an event, the page shows "Couldn't load events" for everyone, just when the chain banner asks you to investigate. Filtering by another type still works.
- Thin sentences. Run finished never says whether the run failed, Thread created never shows the goal, and Run step, Model called, Message posted, Audit checkpoint and Audit chain broken only repeat their name.
- Quiet commands.
ok log --type a,bprints nothing (it takes one type), andok replaywith an unknown id prints nothing and succeeds. - "Refusals summarised". The Type filter spells this one label the British way.
For developers#
API#
All under /api, with Authorization: Bearer <token>.
| Method and path | Notes |
|---|---|
GET /events |
Events the caller may see, oldest first. Query: type (comma-separated), run, thread, task, actor, since (event number), limit. Returns { events, last_seq }. With no since or limit, the newest 200. With limit and no since, the oldest N, so page forward with since. |
GET /events/stream |
Server-sent events of new events, same filters. Each has id: (the number), event: (the type) and data: (the event). |
GET /runs/:id |
{ run, events, transcript }; transcript holds the replay lines. |
GET /audit/head |
{ log, chain, genesis_seq, sealed, sealed_at, head, last_checkpoint, last_break, last_verify }. |
GET /audit/checkpoints?since=&limit= |
{ checkpoints, next }; limit 1 to 1000, default 100. |
POST /audit/verify |
Body { from?, to?, anchors?: ["<seq> sha256:<hex>"] }, or no body for the whole log. Returns the verify report. Writes nothing. A range over 1,000,000 events is 422 range_too_large. |
The /audit routes are for human admins only. Anyone else, an agent acting
for an admin included, gets 403 denied ("policy denied audit.read: the
audit log is for human admins only"), and no token scope reaches them.
Audit events#
| Event | When | Fields |
|---|---|---|
audit_checkpoint |
Every OK_AUDIT_CHECKPOINT_EVENTS events or OK_AUDIT_CHECKPOINT_MINUTES |
through, head, events, verified_from |
audit_chain_broken |
A checkpoint or full verify found a break | at, kind, detail |
audit_chain_started |
A pre-chain file was sealed | log, sealed, first, last, head, gaps |
events_lost |
Events could not be written | count, first_at |
audit_disk_low |
Free space fell under OK_AUDIT_MIN_FREE_MB |
free_mb |
refusals_summarized |
Repeated refusals were folded into one | route, reason, issuer, count, addresses, first_at, last_at |
state_restored |
A hosted tenant was restored from a backup | backup_at, backup_head, previous_head |
Break kinds#
| Break | Meaning | Likely cause |
|---|---|---|
hash_mismatch |
The stored event does not hash to its stored hash | An event was edited |
link_mismatch |
An event does not link to the stored hash of the one before | An event was edited and rehashed, or events were renumbered |
missing |
A number after the start of the chain is absent | An event was deleted |
unchained |
An event has no hash | Written by hand or by an old version |
column_mismatch |
An index column disagrees with the event | Edited to hide it from filtered reads |
genesis_mismatch |
The first event does not link to this log's start | Events spliced in from another file |
checkpoint_mismatch |
A checkpoint's hash differs from the stored one | The log was rewritten, the checkpoint was not |
anchor_mismatch |
An anchor differs from the stored hash | The log was rewritten and rehashed at or before that number |
The hash construction and the upgrade rules are in docs/audit.md.