Reconnecting
Restoring connection…
Reconnecting
Restoring connection…
Docs navigation
Get started
Connect an LLM
Operate
Build packs
Audit & SIEM
Everything that matters leaves a record: every dispatch and its outcome, every approval decision, every policy edit, every credential minted or revoked, every sign-in — even the failed ones.
$ curl -sN -H "Authorization: Bearer $AUDIT_KEY" \
https://emisar.dev/api/audit?since=2026-07-23T00:00:00Z
{"id":"019f8a5f-7759-7a33-a47e-9a7c28e10bd1","request_id":"req_cF-s3B3Z-Iu38MDhBCdqlw","account_id":"019f8a5f-74c7-7b9a-980a-0e00422d2541","actor_kind":"user","actor_label":null,"actor_id":"019f8a5f-749b-72b9-b70f-04b56ebf9e83","ip_address":null,"event_type":"action_run.pending_approval","occurred_at":"2026-07-22T15:09:00.121466Z","target_id":"019f8a5f-75b2-7a64-a955-152da6a993f3","mcp_client_metadata":null,"user_agent":null,"payload":{"action":"postgres.reload_conf","executed_command_truncated":false,"run_id":"019f8a5f-7758-7cfc-8790-77c9aa9400fd"},"target_label":"pg-primary-iad","target_kind":"runner"}
event_type, the
actor_id
/ target_id
to correlate on, the request metadata (ip_address, request_id), and the full payload.
What gets recorded#
Events are named
domain.action
— action_run.denied
(a dispatch policy rejected, with the reason), approval.approved, policy.updated, membership.role_changed, user.sessions_revoked, …
over sixty types across runs, approvals, policies, packs, runners, credentials, team,
and account changes. Each carries the actor (operator, API key, or runner), the
affected entity, a structured payload (a policy edit records its exact diff), and an
account-scoped timestamp. Audit rows are written in the
same database transaction
as the change they
describe — an action that committed has its audit row, always.
Reading it in the dashboard#
Audit filters by event type or actor kind; every
row expands to the full payload, and references resolve to live labels (the runner's
name, the user's email) so you're not cross-referencing UUIDs. LLM-driven runs carry
their attribution — which client, which key, which session — plus the required reason, so you can answer what the
agent did last night, and why, with a single filter.
Exporting the trail#
Two ways out: the Export CSV button on the Audit log downloads exactly the view you've filtered to, and the export endpoint serves NDJSON — one event per line, cursor-paginated, forward-only. Mint a read-only audit-export token from the Audit page; that token can read events and nothing else — it's a separate credential kind from an MCP key, so it can never list runners or execute an action. Only available on Team & Enterprise.
# first pull — everything since a timestamp $ curl -s "https://emisar.dev/api/audit?since=2026-06-01T00:00:00Z&limit=500" \ -H "Authorization: Bearer $AUDIT_KEY" # follow-ups — pass the cursor from the previous response $ curl -s "https://emisar.dev/api/audit?cursor=$NEXT" \ -H "Authorization: Bearer $AUDIT_KEY"
-
—
X-Next-Cursoris your resume point — the last row on the page — set whenever a page returns events, so your next poll starts strictly after what you already have and a short page is never re-read. The RFC 5988Link: <…>; rel="next"header is the separate "there's more right now" signal and rides only a full page — Splunk-style collectors follow it to keep paging, Datadog-style ones just track the X-header. No Link means you're caught up: poll again later from the lastX-Next-Cursoryou saw (an empty page sets neither header, so you hold your place). -
—
Pages are capped at 1,000 events; ordering is stable
(
occurred_at, then id), so a poller never misses or double-counts an event. - — Retention follows your plan — 7 days on Free, 90 on Team, 365 on Enterprise. Ship to your SIEM ahead of the window and retention becomes your SIEM's policy, not ours. Each event's horizon is fixed when it's written, so a plan downgrade only shortens the window for new events — it never retroactively deletes history — and both the plan change and every retention prune are themselves recorded in the log.
The runner-side journal#
Independently of the cloud, every runner writes a JSONL line per action attempt to
/var/log/emisar/events.jsonl
— argv hashes, exit codes, redaction counts, output digests — each line chained to the
previous by SHA-256.
emisar audit verify --all
walks the chain (including rotated files) and flags a break. Treat it as tamper-evident, not
tamper-proof: root on the host can truncate the tail or rewrite lines and re-chain them so
verify
still passes. The record that can't be forged from the host is the
cloud audit
— it's off-host and append-only. So ship the host journal through your log pipeline
and
stream the cloud audit to your SIEM, then cross-reference them: an action in one record but not
the other is the tamper signal, and hiding an action means forging both consistently — with one
of them out of reach on the host.
user.mfa_reset_by_admin
on an account you didn't expect,
policy.updated
(the rules
changed), pack-trust transitions (new bytes were approved), and on the runner side
validation_failed
/ action_blocked_by_admission
— an agent repeatedly asking for things it can't have is the most interesting signal
you'll get.