Read the audit stream
Tail the enclave's or the controlplane's audit as it happens, read the record shape that each event arrives in, and trace a client action back to the server-side row that proves what the platform did.
The audit stream shows what the platform actually did after a client action. This guide shows how to tail it and how to read a record. For the full catalog of actions, attributes, and evidence tiers, see Audit events.
Where each plane stores the audit
Each plane serves the same two read routes over its own audit store:
GET /api/v1/audit/events: a filtered, paginated read.GET /api/v1/audit/stream: a Server-Sent-Events live tail with the same filters.
The enclave broker serves the sovereign enclave audit. Reach it through the
relay (:8452), which proxies both routes, or at the broker itself (:8451).
The controlplane serves its own identity and policy audit. The --source flag
selects the plane: enclave (the default) or controlplane. There is no merge:
a query reads one plane’s store.
Tail the stream
dvx audit stream prints one JSON record per line until you interrupt it.
Authenticate with --token, or log in once with dvx login so the CLI can
reuse the cached device-flow credential. Point TLS trust at the platform’s CA
bundle with --ca-cert (--insecure-skip-verify exists but is dev-only).
dvx audit stream \ --source enclave \ --enclave https://relay.enclave.example.com:8452 \ --ca-cert ca.pem \ --action gateway.egress{"event_time":"...","action":"gateway.egress","outcome":"deny",...,"source":"network_flow"}Both commands take the same filter flags: --scope, --agent, --action,
--outcome, --proto, --dst-host, --since, and --until (RFC3339).
dvx audit events adds --limit (default 100, at most 1000) and --offset
for paging. Use events when you want a bounded, exact answer and stream
when you want rows as they happen.
For a raw client (a dashboard, a log shipper, curl), the stream is plain SSE:
each event carries an id: line and one JSON record on the data: line, and
the server writes : keepalive while the stream is idle. The id is the row’s
ingest time in unix nanoseconds. Send it back as Last-Event-ID on reconnect
and the tail resumes exactly after that row.
The server does not keep a tail connection open forever: it bounds each
response with a write deadline and closes the connection after about 30
seconds. A long watch
reconnects in a loop with the last id. When the close arrives as a read error,
dvx audit stream prints the error and exits.
What a record looks like
Both routes return the same unified record. One timeline covers all of a
plane’s audit: generic audit events, typed network-flow records, and typed
inference records. The source field tells you which store the row came from,
and each source fills a different set of fields:
network_flow: a gateway egress flow. Fills the network fields (proto,src,dst,dst_host,identified,agent_name, and the narrower address fields likedst_ipanddst_port) and leavesresourceandattrsempty.audit_event: a generic audit action. Fillsresourceandattrsand leaves the network fields empty.- The unified view projects each typed inference record as action
inference.requestwith its metadata inattrs. The record’s body ciphertext never appears in the timeline.
The shared correlation fields are present on every row: event_time, action,
actor, actor_kind, outcome, reason, scope, org_id, group_id,
agent_id, session_id, trace_id, and enclave_id.
dvx audit events --enclave https://relay.enclave.example.com:8452 \ --ca-cert ca.pem --action gateway.egress --outcome deny --limit 1{ "records": [ { "event_time": "2026-10-08T12:16:46.035Z", "enclave_id": "c359fcbd890f...", "enclave_name": "daevix-dev", "scope": "agent", "org_id": "dev-org", "agent_id": "egressdoc-verify", "action": "gateway.egress", "actor": "agent:egressdoc-verify", "outcome": "deny", "reason": "default_deny", "identified": true, "agent_name": "egressdoc-verify", "src": "10.244.1.192:47522", "dst": "146.75.121.55:443", "dst_host": "www.kernel.org", "dst_ip": "146.75.121.55", "dst_port": 443, "proto": "tcp", "source": "network_flow" } ], "next_offset": 1}The same record arrives as one data: line on the stream.
Correlate a client action with server-side evidence
The read is fail-closed in both directions. Every query is force-scoped to the
caller’s org, taken from the token, never from the request. A caller without an
org claim gets 403. On the enclave, the broker resolves the caller’s
enclave-local grants: audit:read:all sees every actor in the org, and
audit:read pins the read to records attributed to the caller’s own actor. On
the controlplane, the same personas resolve from capabilities. A caller can
narrow with filters but can never widen past the computed scope.
Correlation then works like this:
- Trigger the action from the client (write a secret, author a policy, run an agent against the gateway).
- Watch the stream (or page
events) for the row with the action and outcome that you expect, matched bytrace_id,agent_id, orrequest_idwhere the action carries one. - On the controlplane, a cross-org read is itself audited: each affected org
gets an
audit.read.cross_orgevent in its own view, and a paged read refuses to disclose its results if that event cannot be durably recorded.
The stream is a best-effort tail: the events endpoint is the source of truth
for exact and complete queries. Each component that writes to an audit store
also emits audit.heartbeat records on a fixed interval, and the store emits
audit.gap records when it lost rows, so a reader can tell silence from loss. See
Author an egress policy for an example of
reading the deny verdicts that a policy produces, and
Operator API for the authentication model these
routes share.