Skip to content

Audit events ​

The audit wire: one ndjson line on stderr per op outcome, plus an OTLP span per event when OTEL_EXPORTER_OTLP_ENDPOINT is set. cuora keeps no local ledger (ADR-0001 — aggregation is peregrin's); the stderr stream is the human contract and the OTLP stream the machine one, so a wrapper — peregrin, sepia, your own harness — can capture either, and a human sees a breadcrumb.

Shape ​

json
{"ts":"2026-01-01T00:00:00Z","actor":"devin","op":"run","rref":"billing/stripe","env":"dev","outcome":"ok"}
fieldnotes
tsRFC 3339, UTC
actor--actor → CUORA_ACTOR → $USER/$LOGNAME → anon. Self-claimed — attribution, not identity
opthe verb: init put get list rm rotate run env check deploy ui serve manifest schema
rrefthe resolved ref path, env pin stripped — same on denied/error when the arg parses, else the raw arg verbatim. Absent on op-scoped events
envthe env the op ran under
outcomeok | denied | error
policyserve-path only, optional — the PDP verdict {allow, reason?, obligations?} behind the outcome; absent on every other path

When events fire ​

eventmeaning
ok, ref-scopedone per ref materialized or mutated — put, get, rotate, rm, deploy, and per resolved ref inside run/env
ok, op-scopedref-less verb completed — init, list, check, ui, serve
denied[guardrails] — or a serve-path PDP verdict of allow: false (including fail-closed on an unreachable PDP) — refused the op before it touched the vault; rref is the requested ref (normalized when parseable)
errorthe op ran and failed — missing secret, vault i/o, bad ref; a check with findings also audits error (it exits 1); also emitted when config load itself fails (best-effort actor/env from flags + process env)

Two deliberate silences:

  • manifest, schema, gen-cli-docs run before config load — pure introspection, no event.
  • A run child's non-zero exit is the child's outcome — no error event for it. cuora's own exit code is the child's.

serve and the policy field ​

cuora serve emits the same events — get/put/rm/rotate requests audit exactly like the CLI verbs, under the request's claimed actor when it carries one. The PDP consult covers the ops that can materialize — get/put/rm/rotate andlist/check (record decrypts, vault probes, scoped-token gets) — so a check's incidental token gets ride the same policy-bearing events. On the serve path only, every event a consulted [serve.pdp] gated gains a policy object — {allow, reason?, obligations?} — so the stream shows the verdict behind the outcome: a PDP allow, a PDP deny (outcome: "denied", policy.allow: false), or a degraded allow recorded when allow_on_error overrode an unreachable PDP. The serve verb itself closes with one op-scoped ok. See the serve protocol.

ui and the TUI ​

cuora ui emits one op-scoped ok when the browser exits cleanly. Inside it, browsing is silent — listing refs and metadata is not a materialization, and live stderr audit is muted for the whole session so events can't splatter over the alternate screen; everything held replays to stderr after the terminal is restored. The r→r reveal goes through the audited get path: a real {"op":"get","rref":…,"outcome":"ok"} event per value shown — held like the rest, replayed on exit.

OTLP export ​

Set OTEL_EXPORTER_OTLP_ENDPOINT (e.g. http://localhost:4318) and every audit event is additionally buffered as an cuora.<op> span — ExportTraceServiceRequest JSON posted to <endpoint>/v1/traces on flush. Span attributes mirror the ndjson fields: cuora.op, cuora.rref (when present), cuora.env, cuora.actor, cuora.outcome, and cuora.policy.* on serve-path events; Outcome::Error marks the span STATUS_CODE_ERROR. service.name comes from OTEL_SERVICE_NAME (default cuora).

Flush timing: CLI verbs export once at exit; cuora serve flushes per request (it's long-running); the TUI flushes at exit like any verb.

Related env vars: OTEL_EXPORTER_OTLP_HEADERS (k=v pairs, percent-decoded), OTEL_EXPORTER_OTLP_TIMEOUT (ms, default 10000), OTEL_SDK_DISABLED=true turns export off entirely, and CUORA_AUDIT_STDERR=0 silences the stderr wire (OTLP-only mode).

Failure posture ​

Audit emission is best-effort on both wires: a closed stderr (broken pipe) never takes the broker down — the write error is swallowed by design — and a failed OTLP export warns once on stderr, never fails the op.

Note: stderr can also carry non-ndjson cuora: notice lines (permission warnings, keyring fallback notes) — UX chatter, not audit wire. Consumers parsing stderr as pure ndjson should tolerate lines that don't start with {. During cuora ui both audit events and notices are held and replayed after the terminal is restored.

Granularity ​

Per-ref events fire when a value crosses the store boundary toward a caller: get, put, rotate, rm, deploy, run, env, and serve get/put/rotate/rm. Bulk reads — list, check, the TUI reload, serve list — emit op-scoped events only: they decrypt records internally to verify or enumerate, but no value is returned to the caller, so there is no per-ref materialization to attribute. If your threat model counts "decrypted in-process" as materialization, treat bulk ops as single events covering N refs — the op name tells you which verb ran, not which refs it touched.